import 'package:dio/dio.dart'; import 'package:enaklo/common/api/interceptors/unauthorized_interceptor.dart'; import 'package:enaklo/domain/wallet/wallet.dart'; import 'package:enaklo/infrastructure/wallet/wallet_dtos.dart'; import 'package:enaklo/infrastructure/wallet/wallet_response.dart'; import 'package:enaklo/presentation/pages/reward/reward_ui.dart'; import 'package:flutter_test/flutter_test.dart'; Response _response(int status, Object? data) => Response( requestOptions: RequestOptions(path: '/'), statusCode: status, data: data, ); Map _error(String code, String entity, String cause) => { 'success': false, 'data': null, 'errors': [ {'code': code, 'entity': entity, 'cause': cause}, ], }; void main() { group('token ditolak (§2.2)', () { test('401 dan 304 dari auth_handler berarti login ulang', () { expect(isTokenRejected(_response(401, null)), isTrue); expect( isTokenRejected(_response(400, _error('304', 'auth_handler', 'x'))), isTrue, ); }); test('304 aturan bisnis dan 403 PIN tidak dianggap logout', () { expect( isTokenRejected( _response(400, _error('304', 'wallet_service', 'not enough')), ), isFalse, ); expect( isTokenRejected( _response(403, { 'success': false, 'data': {'code': 'PIN_NOT_SET'}, 'errors': [ {'code': 'PIN_NOT_SET'}, ], }), ), isFalse, ); }); test('parseWalletFailure memetakan auth_handler ke unauthorized', () { final failure = parseWalletFailure( _response(400, _error('304', 'auth_handler', 'token expired')), ); expect(failure, const WalletFailure.unauthorized()); }); test('parseWalletFailure tetap membaca error PIN dari data', () { final failure = parseWalletFailure( _response(400, { 'success': false, 'data': {'code': 'PIN_INVALID', 'remaining_attempts': 3}, 'errors': [ {'code': 'PIN_INVALID'}, ], }), ); expect(failure, const WalletFailure.pinInvalid(3)); }); }); group('tipe mutasi (§4.2)', () { test('tipe game dan voucher dikenali, PAYMENT tidak lagi', () { expect( WalletTransactionType.fromApi('GAME_REWARD'), WalletTransactionType.gameReward, ); expect( WalletTransactionType.fromApi('REWARD_REDEEM_REFUND'), WalletTransactionType.rewardRedeemRefund, ); expect( WalletTransactionType.fromApi('PAYMENT'), WalletTransactionType.unknown, ); }); test('filter per tab hanya menawarkan tipe milik mata uang itu', () { final point = WalletTransactionType.filterableFor(WalletCurrency.point); final coin = WalletTransactionType.filterableFor(WalletCurrency.coin); expect(point, contains(WalletTransactionType.rewardRedeem)); expect(point, isNot(contains(WalletTransactionType.gameSpend))); expect(coin, contains(WalletTransactionType.gameSpendRefund)); expect(coin, isNot(contains(WalletTransactionType.exchangeIn))); expect(point, isNot(contains(WalletTransactionType.unknown))); }); }); group('penolakan tukar voucher (§9.2)', () { ({String message, bool reloadCatalog}) describe(String cause) => describeVoucherRedeemFailure(WalletFailure.rejected(cause)); test('setiap cause punya pesan ramah', () { expect( describe('not enough EnakPoint').message, 'EnakPoint kamu kurang.', ); expect( describe( 'this voucher can be redeemed at most 2 times per customer', ).message, 'Kamu sudah mencapai batas penukaran voucher ini.', ); expect( describe('the customer is not active').message, 'Akun tidak aktif.', ); }); test('voucher habis / tidak tersedia memuat ulang katalog', () { for (final cause in [ 'the voucher is out of stock', 'the voucher is not available', 'the voucher cannot be redeemed yet', 'the voucher has ended', 'the voucher is not available yet', ]) { expect(describe(cause).reloadCatalog, isTrue, reason: cause); } expect(describe('not enough EnakPoint').reloadCatalog, isFalse); }); test('cause tidak pernah ditampilkan mentah', () { const cause = 'this Idempotency-Key was already used to redeem another voucher'; expect(describe(cause).message, isNot(contains('Idempotency'))); }); }); group('DTO dari contoh dokumen', () { test( 'voucher katalog (§9.1): stok null tidak dihitung, 0 berarti habis', () { Voucher voucher(int? available) => VoucherDto.fromJson({ 'id': 'v1', 'name': 'Kopi Susu Gratis', 'voucher_type': 'FREE_ITEM', 'face_value': 20000, 'point_cost': 15000, 'max_per_customer': null, 'valid_until': '2026-12-31T16:59:59Z', 'terms': {'a': 'b'}, 'available': available, }).toDomain(); expect(voucher(null).isOutOfStock, isFalse); expect(voucher(0).isOutOfStock, isTrue); expect(voucher(120).type, VoucherType.freeItem); }, ); test('hasil tukar voucher (§9.2)', () { final result = VoucherRedemptionDto.fromJson({ 'id': 'r1', 'voucher_id': 'v1', 'voucher_name': 'Kopi Susu Gratis', 'voucher_type': 'FREE_ITEM', 'status': 'COMPLETED', 'face_value': 20000, 'point_cost': 15000, 'code': 'KOPI-7F3C-2291', 'code_expires_at': '2026-12-31T16:59:59Z', 'point_balance': 2500, 'replayed': true, }).toRedeemResult(); expect(result.pointBalance, 2500); expect(result.replayed, isTrue); expect(result.redemption.code, 'KOPI-7F3C-2291'); expect(result.redemption.status, VoucherRedemptionStatus.completed); }); }); }