feat: integrate enakgame and voucher
Build & Deploy iOS to TestFlight / build-and-deploy (push) Canceled after 0s

This commit is contained in:
efrilm
2026-10-10 02:33:16 +07:00
parent e5515d5871
commit 38850b281f
104 changed files with 7122 additions and 8063 deletions
-35
View File
@@ -1,35 +0,0 @@
part of '../game.dart';
@freezed
abstract class Game with _$Game {
const Game._();
const factory Game({
required String id,
required String name,
required String type,
required bool isActive,
required Map<String, dynamic> metadata,
required List<GamePrize> prizes,
required String createdAt,
required String updatedAt,
}) = _Game;
factory Game.empty() => const Game(
id: '',
name: '',
type: '',
isActive: false,
metadata: {},
prizes: [],
createdAt: '',
updatedAt: '',
);
/// Biaya sekali main dalam EnakCoin (`metadata.coin_cost`), default 1.
int get coinCost {
final value = metadata['coin_cost'];
final cost = value is num ? value.toInt() : int.tryParse('$value');
return cost == null || cost <= 0 ? 1 : cost;
}
}
@@ -1,22 +0,0 @@
part of '../game.dart';
@freezed
abstract class GamePrize with _$GamePrize {
const factory GamePrize({
required String id,
required String gameId,
required String name,
required Map<String, dynamic> metadata,
required String createdAt,
required String updatedAt,
}) = _GamePrize;
factory GamePrize.empty() => const GamePrize(
id: '',
gameId: '',
name: '',
metadata: {},
createdAt: '',
updatedAt: '',
);
}
@@ -1,12 +0,0 @@
part of '../game.dart';
@freezed
abstract class SpinResult with _$SpinResult {
const factory SpinResult({
required String gamePlayId,
required int coinsUsed,
required String prizeId,
required String prizeName,
required int coinsRemaining,
}) = _SpinResult;
}
@@ -1,9 +0,0 @@
part of '../game.dart';
@freezed
sealed class GameFailure with _$GameFailure {
const factory GameFailure.serverError(ApiFailure failure) = _ServerError;
const factory GameFailure.unexpectedError() = _UnexpectedError;
const factory GameFailure.dynamicErrorMessage(String erroMessage) =
_DynamicErrorMessage;
}
-12
View File
@@ -1,12 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../common/api/api_failure.dart';
part 'game.freezed.dart';
part 'entity/game_entity.dart';
part 'entity/game_prize_entity.dart';
part 'entity/spin_result_entity.dart';
part 'failures/game_failure.dart';
part 'repositories/i_game_repository.dart';
File diff suppressed because it is too large Load Diff
@@ -1,8 +0,0 @@
part of '../game.dart';
abstract class IGameRepository {
Future<Either<GameFailure, Game>> ferrisWheel();
/// Main satu kali; memotong EnakCoin sebesar biaya game. Tanpa PIN.
Future<Either<GameFailure, SpinResult>> spin({required String gameId});
}
@@ -71,18 +71,11 @@ abstract class CustomerOrderItem with _$CustomerOrderItem {
@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';
}
@@ -8,9 +8,6 @@ abstract class Outlet with _$Outlet {
required String name,
String? address,
/// Kasir di outlet ini menerima pembayaran EnakPoint.
required bool acceptsPointPayment,
/// Belanja di outlet ini memberi EnakPoint.
required bool earnsPoints,
@@ -1,10 +0,0 @@
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,72 @@
part of '../wallet.dart';
/// Voucher di katalog yang bisa ditukar dengan EnakPoint sekarang (§9.1).
@freezed
abstract class Voucher with _$Voucher {
const Voucher._();
const factory Voucher({
required String id,
required String name,
String? description,
String? imageUrl,
required VoucherType type,
/// Nilai voucher dalam rupiah.
required int faceValue,
/// EnakPoint yang dipotong saat ditukar.
required int pointCost,
/// Batas tukar per customer; `null` berarti tanpa batas.
int? maxPerCustomer,
DateTime? validUntil,
/// Sisa stok; `null` berarti stok tidak dihitung.
int? available,
}) = _Voucher;
bool get isOutOfStock => available != null && available! <= 0;
}
/// Voucher hasil tukar milik customer (§9.3).
@freezed
abstract class VoucherRedemption with _$VoucherRedemption {
const factory VoucherRedemption({
required String id,
required String voucherId,
required String voucherName,
String? voucherImageUrl,
required VoucherType voucherType,
required VoucherRedemptionStatus status,
required int faceValue,
required int pointCost,
/// `null` bila voucher tidak memakai kode; customer menunjukkan layar
/// voucher ke kasir.
String? code,
DateTime? codeExpiresAt,
DateTime? completedAt,
DateTime? createdAt,
}) = _VoucherRedemption;
}
@freezed
abstract class VoucherRedemptionPage with _$VoucherRedemptionPage {
const factory VoucherRedemptionPage({
required List<VoucherRedemption> items,
required int page,
required int totalPages,
}) = _VoucherRedemptionPage;
}
@freezed
abstract class VoucherRedeemResult with _$VoucherRedeemResult {
const factory VoucherRedeemResult({
required VoucherRedemption redemption,
/// Saldo EnakPoint setelah ditukar.
required int pointBalance,
required bool replayed,
}) = _VoucherRedeemResult;
}
+69 -18
View File
@@ -13,29 +13,47 @@ enum WalletCurrency {
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 _bothCurrencies = [WalletCurrency.point, WalletCurrency.coin];
const WalletTransactionType(this.apiValue, this.label);
enum WalletTransactionType {
earn('EARN', 'Dari belanja', _bothCurrencies),
earnReversal(
'EARN_REVERSAL',
'Dibatalkan (order di-void/refund)',
_bothCurrencies,
),
exchangeOut('EXCHANGE_OUT', 'Ditukar ke EnakPoint', [WalletCurrency.coin]),
exchangeIn('EXCHANGE_IN', 'Hasil tukar EnakCoin', [WalletCurrency.point]),
transferOut('TRANSFER_OUT', 'Transfer keluar', _bothCurrencies),
transferIn('TRANSFER_IN', 'Transfer masuk', _bothCurrencies),
gameSpend('GAME_SPEND', 'Main game', [WalletCurrency.coin]),
gameSpendRefund('GAME_SPEND_REFUND', 'Biaya main dikembalikan', [
WalletCurrency.coin,
]),
gameReward('GAME_REWARD', 'Hadiah game', [WalletCurrency.coin]),
rewardRedeem('REWARD_REDEEM', 'Ditukar ke voucher', [WalletCurrency.point]),
rewardRedeemRefund('REWARD_REDEEM_REFUND', 'Penukaran voucher dibatalkan', [
WalletCurrency.point,
]),
expire('EXPIRE', 'Kedaluwarsa', _bothCurrencies),
adjustment('ADJUSTMENT', 'Koreksi', _bothCurrencies),
migration('MIGRATION', 'Saldo awal', _bothCurrencies),
/// Tipe baru dari backend yang belum dikenal app: tampilkan `description`
/// tanpa label.
unknown('', '', []);
const WalletTransactionType(this.apiValue, this.label, this.currencies);
final String apiValue;
final String label;
/// Tipe yang bisa dipilih di filter riwayat.
static List<WalletTransactionType> get filterable =>
values.where((e) => e != unknown).toList();
/// Mata uang yang bisa memiliki tipe ini.
final List<WalletCurrency> currencies;
/// Tipe yang bisa dipilih di filter riwayat untuk tab [currency].
static List<WalletTransactionType> filterableFor(WalletCurrency currency) =>
values.where((e) => e.currencies.contains(currency)).toList();
static WalletTransactionType fromApi(String? value) =>
values.firstWhere((e) => e.apiValue == value, orElse: () => unknown);
@@ -49,3 +67,36 @@ enum PinOtpPurpose {
final String apiValue;
}
enum VoucherType {
fixedValue('FIXED_VALUE'),
percentage('PERCENTAGE'),
freeItem('FREE_ITEM'),
merchantBenefit('MERCHANT_BENEFIT'),
unknown('');
const VoucherType(this.apiValue);
final String apiValue;
static VoucherType fromApi(String? value) =>
values.firstWhere((e) => e.apiValue == value, orElse: () => unknown);
}
enum VoucherRedemptionStatus {
completed('COMPLETED'),
/// Sedang diproses penyedia luar; EnakPoint sudah terpotong.
pending('PENDING'),
/// Gagal; EnakPoint dikembalikan otomatis (`REWARD_REDEEM_REFUND`).
failed('FAILED'),
unknown('');
const VoucherRedemptionStatus(this.apiValue);
final String apiValue;
static VoucherRedemptionStatus fromApi(String? value) =>
values.firstWhere((e) => e.apiValue == value, orElse: () => unknown);
}
@@ -1,7 +1,7 @@
part of '../wallet.dart';
/// Riwayat order customer. App hanya melihat order; tidak membuat atau
/// membayarnya (§7).
/// membayarnya; EnakPoint tidak bisa dipakai membayar (§1).
abstract class IOrderRepository {
Future<Either<WalletFailure, CustomerOrderPage>> getOrders({
required int page,
@@ -0,0 +1,19 @@
part of '../wallet.dart';
/// Tukar EnakPoint ke voucher (§9).
abstract class IVoucherRepository {
/// Voucher yang aktif, dalam masa berlaku, dan masih ada stoknya.
Future<Either<WalletFailure, List<Voucher>>> getVouchers();
Future<Either<WalletFailure, VoucherRedeemResult>> redeem({
required String voucherId,
required String pin,
required String idempotencyKey,
});
/// Penukaran milik customer, terbaru di atas.
Future<Either<WalletFailure, VoucherRedemptionPage>> getRedemptions({
required int page,
int limit = 20,
});
}
@@ -14,10 +14,6 @@ abstract class IWalletRepository {
Future<Either<WalletFailure, WalletExpiring>> getExpiring();
Future<Either<WalletFailure, PaymentCode>> createPaymentCode({
required String pin,
});
Future<Either<WalletFailure, ExchangePreview>> previewExchange({
required int coins,
});
+2 -1
View File
@@ -9,15 +9,16 @@ 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 'entities/voucher_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';
part 'repositories/i_voucher_repository.dart';
File diff suppressed because it is too large Load Diff