import 'package:dio/dio.dart'; import '../../domain/wallet/wallet.dart'; /// Hitung mundur bawaan bila server menjawab `429` tanpa header `Retry-After`. const int kDefaultOtpRetrySeconds = 60; /// Membaca envelope `{ success, data, errors }`. Mengembalikan `null` bila /// sukses, atau [WalletFailure] yang sesuai bila gagal. WalletFailure? parseWalletFailure(Response response) { if (response.statusCode == 401) return const WalletFailure.unauthorized(); final body = response.data; if (body is! Map) return _failureFromStatus(response); if (body['success'] == true) return null; // Error PIN: `data` berisi `code` dan field tambahan. final data = body['data']; if (data is Map) { final pinFailure = _pinFailure(data['code']?.toString(), data); if (pinFailure != null) return pinFailure; } final errors = body['errors']; final first = errors is List && errors.isNotEmpty && errors.first is Map ? errors.first as Map : null; final code = first?['code']?.toString(); switch (code) { case '303': case '310': return const WalletFailure.invalidRequest(); case '304': return WalletFailure.rejected(first?['cause']?.toString() ?? ''); case '404': return const WalletFailure.notFound(); case '429': return WalletFailure.tooManyRequests(_retryAfterSeconds(response)); case '900': return const WalletFailure.internalError(); } return _pinFailure(code, const {}) ?? _failureFromStatus(response); } /// Isi `data` dari envelope sukses. Beberapa endpoint (mis. `/wallet`) /// membungkus lagi payload-nya dalam `{ status, message, data }`, jadi lapisan /// itu ikut dikupas. Map walletDataOf(Response response) { final data = response.data['data'] as Map; final isInnerEnvelope = data.containsKey('status') && data.containsKey('message') && data['data'] is Map; return Map.from(isInnerEnvelope ? data['data'] : data); } WalletFailure? _pinFailure(String? code, Map data) { switch (code) { case 'PIN_NOT_SET': return const WalletFailure.pinNotSet(); case 'PIN_INVALID': return WalletFailure.pinInvalid(_toInt(data['remaining_attempts'])); case 'PIN_LOCKED': return WalletFailure.pinLocked(_toDateTime(data['locked_until'])); case 'TRANSFER_BLOCKED': return WalletFailure.transferBlocked( _toDateTime(data['transfer_blocked_until']), ); } return null; } WalletFailure _failureFromStatus(Response response) { final status = response.statusCode ?? 0; if (status == 404) return const WalletFailure.notFound(); if (status == 429) { return WalletFailure.tooManyRequests(_retryAfterSeconds(response)); } if (status >= 500) return const WalletFailure.internalError(); return const WalletFailure.unexpectedError(); } int _retryAfterSeconds(Response response) => int.tryParse(response.headers.value('retry-after') ?? '') ?? kDefaultOtpRetrySeconds; int? _toInt(Object? value) => value is int ? value : int.tryParse(value?.toString() ?? ''); DateTime? _toDateTime(Object? value) => value == null ? null : DateTime.tryParse(value.toString());