import 'package:dio/dio.dart'; import 'package:uuid/uuid.dart'; /// Interceptor yang secara otomatis menambahkan header X-Idempotency-Key /// untuk endpoint-endpoint kritis yang memerlukan idempotency. /// /// Key sebaiknya di-generate di level BLoC/Cubit saat user trigger action, /// lalu dikirim via headers. Interceptor ini hanya berfungsi sebagai fallback /// jika key belum di-set. /// /// Jika response 409 dengan error code `request_in_progress`, interceptor /// akan retry setelah delay 1-2 detik. class IdempotencyInterceptor extends Interceptor { static const _headerKey = 'X-Idempotency-Key'; static const _replayHeader = 'X-Idempotent-Replay'; // Endpoints that require idempotency keys (exact match) static const _idempotentPaths = [ '/api/v1/payments', '/api/v1/orders/void', ]; // Endpoints that require idempotency keys (pattern match) static final _idempotentPathPatterns = [ RegExp(r'/api/v1/orders/.+/add-items$'), RegExp(r'/api/v1/orders/.+/refund$'), RegExp(r'/api/v1/payments/.+/refund$'), ]; static const _maxRetries = 3; static const _retryDelay = Duration(seconds: 2); final Dio _dio; IdempotencyInterceptor(this._dio); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { if (options.method == 'POST' && _requiresIdempotencyKey(options.path)) { // Gunakan key yang sudah di-set dari BLoC, atau generate baru sebagai fallback options.headers[_headerKey] ??= const Uuid().v4(); } handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { // Log jika response adalah replay dari request sebelumnya final isReplay = response.headers.value(_replayHeader); if (isReplay == 'true') { // Response ini adalah hasil dari request pertama yang sudah diproses. // Treat as success — tidak perlu perlakuan khusus. } handler.next(response); } @override void onError(DioException err, ErrorInterceptorHandler handler) async { final response = err.response; final options = err.requestOptions; if (response == null) { handler.next(err); return; } // Handle 409 Conflict with request_in_progress if (response.statusCode == 409 && _isRequestInProgress(response.data) && _requiresIdempotencyKey(options.path)) { final retryCount = options.extra['_idempotency_retry_count'] ?? 0; if (retryCount < _maxRetries) { await Future.delayed(_retryDelay); options.extra['_idempotency_retry_count'] = retryCount + 1; try { final retryResponse = await _dio.fetch(options); handler.resolve(retryResponse); } on DioException catch (e) { handler.next(e); } return; } } handler.next(err); } bool _requiresIdempotencyKey(String path) { if (_idempotentPaths.any((p) => path.endsWith(p))) return true; if (_idempotentPathPatterns.any((r) => r.hasMatch(path))) return true; return false; } bool _isRequestInProgress(dynamic data) { if (data is Map) { final errorCode = data['error_code'] ?? data['code'] ?? ''; return errorCode == 'request_in_progress'; } return false; } }