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:
efrilm
2026-10-01 01:43:22 +07:00
co-authored by Claude Opus 5.5
parent 5954510b93
commit f5e3075205
253 changed files with 47212 additions and 17437 deletions
@@ -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,
});
}
+23
View File
@@ -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