EnakPoint & EnakCoin wallet, PIN, outlet, order, community, reward
- Wallet EnakPoint & EnakCoin: beranda, riwayat, saldo kedaluwarsa, kode bayar, tukar EnakCoin, transfer - PIN (buat, ganti, lupa), outlet, riwayat order, game spin - Halaman community, webview, coming soon, dan ikon menu beranda baru - Redesign Reward Saya: kartu tiket, filter chip, detail kode + QR - Hapus customer point loader, menu, dan checkout lama - Base URL ke enaklo-pos-api.altru.id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5954510b93
commit
f5e3075205
@@ -0,0 +1,88 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
/// Order milik customer di daftar riwayat (§4.5).
|
||||
@freezed
|
||||
abstract class CustomerOrder with _$CustomerOrder {
|
||||
const CustomerOrder._();
|
||||
|
||||
const factory CustomerOrder({
|
||||
required String id,
|
||||
required String orderNumber,
|
||||
required String outletName,
|
||||
required String orderType,
|
||||
required String status,
|
||||
required String paymentStatus,
|
||||
required int totalAmount,
|
||||
required int itemCount,
|
||||
required bool isVoid,
|
||||
required bool isRefund,
|
||||
required int pointsEarned,
|
||||
required int coinsEarned,
|
||||
DateTime? createdAt,
|
||||
}) = _CustomerOrder;
|
||||
|
||||
bool get isDineIn => orderType == 'dine_in';
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class CustomerOrderPage with _$CustomerOrderPage {
|
||||
const factory CustomerOrderPage({
|
||||
required List<CustomerOrder> items,
|
||||
required int page,
|
||||
required int totalPages,
|
||||
}) = _CustomerOrderPage;
|
||||
}
|
||||
|
||||
/// Detail order: field [order] ditambah rincian item dan pembayaran.
|
||||
@freezed
|
||||
abstract class CustomerOrderDetail with _$CustomerOrderDetail {
|
||||
const factory CustomerOrderDetail({
|
||||
required CustomerOrder order,
|
||||
String? tableNumber,
|
||||
required int subtotal,
|
||||
required int discountAmount,
|
||||
required int taxAmount,
|
||||
required int refundAmount,
|
||||
required List<CustomerOrderItem> items,
|
||||
required List<CustomerOrderPayment> payments,
|
||||
}) = _CustomerOrderDetail;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class CustomerOrderItem with _$CustomerOrderItem {
|
||||
const CustomerOrderItem._();
|
||||
|
||||
const factory CustomerOrderItem({
|
||||
required String id,
|
||||
required String productName,
|
||||
String? variantName,
|
||||
required int quantity,
|
||||
|
||||
/// Hanya untuk item timbangan, mis. 4,2 [unitName].
|
||||
double? weight,
|
||||
String? unitName,
|
||||
required int unitPrice,
|
||||
required int totalPrice,
|
||||
required int refundQuantity,
|
||||
}) = _CustomerOrderItem;
|
||||
|
||||
bool get isWeighted => weight != null;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class CustomerOrderPayment with _$CustomerOrderPayment {
|
||||
const CustomerOrderPayment._();
|
||||
|
||||
const factory CustomerOrderPayment({
|
||||
required String id,
|
||||
required String methodName,
|
||||
required String methodType,
|
||||
required int amount,
|
||||
required int refundAmount,
|
||||
|
||||
/// Hanya pada pembayaran EnakPoint.
|
||||
int? pointsUsed,
|
||||
}) = _CustomerOrderPayment;
|
||||
|
||||
bool get isPoint => methodType == 'point';
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ExchangePreview with _$ExchangePreview {
|
||||
const factory ExchangePreview({
|
||||
required int coinAmount,
|
||||
required int pointAmount,
|
||||
required int coinBalance,
|
||||
required int coins,
|
||||
required int points,
|
||||
required bool valid,
|
||||
String? reason,
|
||||
}) = _ExchangePreview;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ExchangeResult with _$ExchangeResult {
|
||||
const factory ExchangeResult({
|
||||
required int coins,
|
||||
required int points,
|
||||
required int coinBalance,
|
||||
required int pointBalance,
|
||||
required List<WalletLot> lots,
|
||||
required bool replayed,
|
||||
}) = _ExchangeResult;
|
||||
}
|
||||
|
||||
/// Bagian saldo dengan tanggal kedaluwarsanya sendiri.
|
||||
@freezed
|
||||
abstract class WalletLot with _$WalletLot {
|
||||
const factory WalletLot({required int amount, DateTime? expiresAt}) =
|
||||
_WalletLot;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
/// Outlet aktif di organisasi customer, tempat saldo berlaku (§4.4).
|
||||
@freezed
|
||||
abstract class Outlet with _$Outlet {
|
||||
const factory Outlet({
|
||||
required String id,
|
||||
required String name,
|
||||
String? address,
|
||||
|
||||
/// Kasir di outlet ini menerima pembayaran EnakPoint.
|
||||
required bool acceptsPointPayment,
|
||||
|
||||
/// Belanja di outlet ini memberi EnakPoint.
|
||||
required bool earnsPoints,
|
||||
|
||||
/// Belanja di outlet ini memberi EnakCoin.
|
||||
required bool earnsCoins,
|
||||
}) = _Outlet;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
@freezed
|
||||
abstract class PaymentCode with _$PaymentCode {
|
||||
const factory PaymentCode({
|
||||
required String code,
|
||||
required String qrPayload,
|
||||
required DateTime expiresAt,
|
||||
}) = _PaymentCode;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
@freezed
|
||||
abstract class PinStatus with _$PinStatus {
|
||||
const PinStatus._();
|
||||
|
||||
const factory PinStatus({
|
||||
required bool hasPin,
|
||||
DateTime? lockedUntil,
|
||||
DateTime? transferBlockedUntil,
|
||||
}) = _PinStatus;
|
||||
|
||||
bool get isLocked =>
|
||||
lockedUntil != null && lockedUntil!.isAfter(DateTime.now());
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class PinOtp with _$PinOtp {
|
||||
const factory PinOtp({
|
||||
required PinOtpPurpose purpose,
|
||||
required String otpToken,
|
||||
DateTime? expiresAt,
|
||||
}) = _PinOtp;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
/// Penerima transfer; [name] dan [phoneNumber] sudah disamarkan oleh server.
|
||||
@freezed
|
||||
abstract class TransferRecipient with _$TransferRecipient {
|
||||
const factory TransferRecipient({
|
||||
required String name,
|
||||
required String phoneNumber,
|
||||
}) = _TransferRecipient;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TransferResult with _$TransferResult {
|
||||
const factory TransferResult({
|
||||
required WalletCurrency currency,
|
||||
required int amount,
|
||||
required TransferRecipient recipient,
|
||||
required List<WalletLot> lots,
|
||||
required int balance,
|
||||
required bool replayed,
|
||||
}) = _TransferResult;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
enum WalletCurrency {
|
||||
point('POINT', 'EnakPoint'),
|
||||
coin('COIN', 'EnakCoin');
|
||||
|
||||
const WalletCurrency(this.apiValue, this.label);
|
||||
|
||||
final String apiValue;
|
||||
final String label;
|
||||
|
||||
static WalletCurrency fromApi(String? value) =>
|
||||
value == coin.apiValue ? coin : point;
|
||||
}
|
||||
|
||||
enum WalletTransactionType {
|
||||
earn('EARN', 'Dari belanja'),
|
||||
earnReversal('EARN_REVERSAL', 'Dibatalkan (order di-void/refund)'),
|
||||
payment('PAYMENT', 'Bayar pesanan'),
|
||||
paymentRefund('PAYMENT_REFUND', 'Pengembalian pembayaran'),
|
||||
exchangeOut('EXCHANGE_OUT', 'Ditukar ke EnakPoint'),
|
||||
exchangeIn('EXCHANGE_IN', 'Hasil tukar EnakCoin'),
|
||||
transferOut('TRANSFER_OUT', 'Transfer keluar'),
|
||||
transferIn('TRANSFER_IN', 'Transfer masuk'),
|
||||
gameSpend('GAME_SPEND', 'Main game'),
|
||||
expire('EXPIRE', 'Kedaluwarsa'),
|
||||
adjustment('ADJUSTMENT', 'Koreksi'),
|
||||
migration('MIGRATION', 'Saldo awal'),
|
||||
unknown('', 'Transaksi');
|
||||
|
||||
const WalletTransactionType(this.apiValue, this.label);
|
||||
|
||||
final String apiValue;
|
||||
final String label;
|
||||
|
||||
/// Tipe yang bisa dipilih di filter riwayat.
|
||||
static List<WalletTransactionType> get filterable =>
|
||||
values.where((e) => e != unknown).toList();
|
||||
|
||||
static WalletTransactionType fromApi(String? value) =>
|
||||
values.firstWhere((e) => e.apiValue == value, orElse: () => unknown);
|
||||
}
|
||||
|
||||
enum PinOtpPurpose {
|
||||
setup('pin_setup'),
|
||||
reset('pin_reset');
|
||||
|
||||
const PinOtpPurpose(this.apiValue);
|
||||
|
||||
final String apiValue;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
@freezed
|
||||
abstract class WalletExpiring with _$WalletExpiring {
|
||||
const factory WalletExpiring({
|
||||
required List<ExpiringAmount> point,
|
||||
required List<ExpiringAmount> coin,
|
||||
}) = _WalletExpiring;
|
||||
|
||||
factory WalletExpiring.empty() => const WalletExpiring(point: [], coin: []);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
@freezed
|
||||
abstract class WalletSummary with _$WalletSummary {
|
||||
const factory WalletSummary({
|
||||
required int pointBalance,
|
||||
required int coinBalance,
|
||||
required int pointValue,
|
||||
required int pointDiscountValue,
|
||||
ExpiringAmount? nearestExpiringPoint,
|
||||
ExpiringAmount? nearestExpiringCoin,
|
||||
required List<WalletTransaction> recentTransactions,
|
||||
}) = _WalletSummary;
|
||||
|
||||
factory WalletSummary.empty() => const WalletSummary(
|
||||
pointBalance: 0,
|
||||
coinBalance: 0,
|
||||
pointValue: 0,
|
||||
pointDiscountValue: 0,
|
||||
recentTransactions: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// Jumlah saldo yang akan hangus pada [date] (tanggal WIB, berlaku sampai
|
||||
/// 23:59:59 WIB di tanggal itu).
|
||||
@freezed
|
||||
abstract class ExpiringAmount with _$ExpiringAmount {
|
||||
const factory ExpiringAmount({required int amount, required DateTime date}) =
|
||||
_ExpiringAmount;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
@freezed
|
||||
abstract class WalletTransaction with _$WalletTransaction {
|
||||
const WalletTransaction._();
|
||||
|
||||
const factory WalletTransaction({
|
||||
required String id,
|
||||
required WalletCurrency currency,
|
||||
required WalletTransactionType type,
|
||||
required int amount,
|
||||
required int balanceAfter,
|
||||
required String description,
|
||||
String? groupId,
|
||||
DateTime? expiresAt,
|
||||
DateTime? createdAt,
|
||||
}) = _WalletTransaction;
|
||||
|
||||
bool get isIncoming => amount > 0;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class WalletTransactionPage with _$WalletTransactionPage {
|
||||
const factory WalletTransactionPage({
|
||||
required List<WalletTransaction> items,
|
||||
required int page,
|
||||
required int totalPages,
|
||||
required int totalCount,
|
||||
}) = _WalletTransactionPage;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
@freezed
|
||||
sealed class WalletFailure with _$WalletFailure {
|
||||
const WalletFailure._();
|
||||
|
||||
/// Gagal di level transport (jaringan, timeout, parsing).
|
||||
const factory WalletFailure.serverError(ApiFailure failure) = _ServerError;
|
||||
const factory WalletFailure.unexpectedError() = _UnexpectedError;
|
||||
const factory WalletFailure.unauthorized() = _Unauthorized;
|
||||
|
||||
/// `303`, `310`: request tidak lengkap / salah format.
|
||||
const factory WalletFailure.invalidRequest() = _InvalidRequest;
|
||||
|
||||
/// `304`: ditolak aturan bisnis. [cause] berbahasa Inggris, jangan ditampilkan
|
||||
/// mentah ke customer.
|
||||
const factory WalletFailure.rejected(String cause) = _Rejected;
|
||||
|
||||
/// `404`.
|
||||
const factory WalletFailure.notFound() = _NotFound;
|
||||
|
||||
/// `429`: minta OTP terlalu cepat.
|
||||
const factory WalletFailure.tooManyRequests(int retryAfterSeconds) =
|
||||
_TooManyRequests;
|
||||
|
||||
/// `900`.
|
||||
const factory WalletFailure.internalError() = _InternalError;
|
||||
|
||||
const factory WalletFailure.pinNotSet() = _PinNotSet;
|
||||
const factory WalletFailure.pinInvalid(int? remainingAttempts) = _PinInvalid;
|
||||
const factory WalletFailure.pinLocked(DateTime? lockedUntil) = _PinLocked;
|
||||
const factory WalletFailure.transferBlocked(DateTime? transferBlockedUntil) =
|
||||
_TransferBlocked;
|
||||
|
||||
bool get isPinError => maybeMap(
|
||||
pinNotSet: (_) => true,
|
||||
pinInvalid: (_) => true,
|
||||
pinLocked: (_) => true,
|
||||
transferBlocked: (_) => true,
|
||||
orElse: () => false,
|
||||
);
|
||||
|
||||
/// Request mungkin tidak sampai / jawaban tidak diterima, sehingga hasilnya
|
||||
/// tidak diketahui. Hanya pada kondisi ini `Idempotency-Key` dipakai ulang.
|
||||
bool get isNetworkError => maybeMap(
|
||||
serverError: (value) => value.failure.maybeMap(
|
||||
connectionError: (_) => true,
|
||||
connectionTimeout: (_) => true,
|
||||
serverError: (e) => e.statusCode == 0,
|
||||
orElse: () => false,
|
||||
),
|
||||
orElse: () => false,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
/// Registrasi device untuk push notification (FCM).
|
||||
abstract class IDeviceRepository {
|
||||
/// Dipanggil setelah login dan setiap kali FCM memberi token baru.
|
||||
Future<Either<WalletFailure, Unit>> register({
|
||||
required String fcmToken,
|
||||
required String platform,
|
||||
required String appVersion,
|
||||
});
|
||||
|
||||
/// Dipanggil saat logout, sebelum token login dihapus. Tidak memanggil API
|
||||
/// bila device ini belum pernah diregistrasi.
|
||||
Future<Either<WalletFailure, Unit>> unregister();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
/// Riwayat order customer. App hanya melihat order; tidak membuat atau
|
||||
/// membayarnya (§7).
|
||||
abstract class IOrderRepository {
|
||||
Future<Either<WalletFailure, CustomerOrderPage>> getOrders({
|
||||
required int page,
|
||||
int limit = 20,
|
||||
});
|
||||
|
||||
Future<Either<WalletFailure, CustomerOrderDetail>> getOrderDetail(String id);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
abstract class IOutletRepository {
|
||||
/// Outlet aktif di organisasi customer, urut nama.
|
||||
Future<Either<WalletFailure, List<Outlet>>> getOutlets();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
abstract class IPinRepository {
|
||||
Future<Either<WalletFailure, PinStatus>> getStatus();
|
||||
|
||||
/// Mengirim OTP ke WhatsApp customer.
|
||||
Future<Either<WalletFailure, PinOtp>> requestOtp(PinOtpPurpose purpose);
|
||||
|
||||
Future<Either<WalletFailure, PinStatus>> setup({
|
||||
required String otpToken,
|
||||
required String otpCode,
|
||||
required String pin,
|
||||
required String confirmPin,
|
||||
});
|
||||
|
||||
Future<Either<WalletFailure, Unit>> change({
|
||||
required String oldPin,
|
||||
required String pin,
|
||||
required String confirmPin,
|
||||
});
|
||||
|
||||
/// Reset juga membuka PIN yang terkunci; setelahnya transfer keluar ditahan
|
||||
/// 24 jam.
|
||||
Future<Either<WalletFailure, Unit>> reset({
|
||||
required String otpToken,
|
||||
required String otpCode,
|
||||
required String pin,
|
||||
required String confirmPin,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
part of '../wallet.dart';
|
||||
|
||||
abstract class IWalletRepository {
|
||||
Future<Either<WalletFailure, WalletSummary>> getWallet();
|
||||
|
||||
Future<Either<WalletFailure, WalletTransactionPage>> getTransactions({
|
||||
required int page,
|
||||
int limit = 20,
|
||||
WalletCurrency? currency,
|
||||
List<WalletTransactionType> types = const [],
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
});
|
||||
|
||||
Future<Either<WalletFailure, WalletExpiring>> getExpiring();
|
||||
|
||||
Future<Either<WalletFailure, PaymentCode>> createPaymentCode({
|
||||
required String pin,
|
||||
});
|
||||
|
||||
Future<Either<WalletFailure, ExchangePreview>> previewExchange({
|
||||
required int coins,
|
||||
});
|
||||
|
||||
Future<Either<WalletFailure, ExchangeResult>> exchange({
|
||||
required int coins,
|
||||
required String pin,
|
||||
required String idempotencyKey,
|
||||
});
|
||||
|
||||
Future<Either<WalletFailure, TransferRecipient>> checkRecipient({
|
||||
required String phone,
|
||||
});
|
||||
|
||||
Future<Either<WalletFailure, TransferResult>> transfer({
|
||||
required WalletCurrency currency,
|
||||
required int amount,
|
||||
required String recipientPhone,
|
||||
required String pin,
|
||||
required String idempotencyKey,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../common/api/api_failure.dart';
|
||||
|
||||
part 'wallet.freezed.dart';
|
||||
|
||||
part 'entities/wallet_enum.dart';
|
||||
part 'entities/wallet_summary_entity.dart';
|
||||
part 'entities/wallet_transaction_entity.dart';
|
||||
part 'entities/wallet_expiring_entity.dart';
|
||||
part 'entities/payment_code_entity.dart';
|
||||
part 'entities/exchange_entity.dart';
|
||||
part 'entities/transfer_entity.dart';
|
||||
part 'entities/pin_entity.dart';
|
||||
part 'entities/outlet_entity.dart';
|
||||
part 'entities/customer_order_entity.dart';
|
||||
part 'failures/wallet_failure.dart';
|
||||
part 'repositories/i_wallet_repository.dart';
|
||||
part 'repositories/i_pin_repository.dart';
|
||||
part 'repositories/i_device_repository.dart';
|
||||
part 'repositories/i_outlet_repository.dart';
|
||||
part 'repositories/i_order_repository.dart';
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user