feat: implement idempotency key for critical API endpoints

This commit is contained in:
Efril
2026-06-04 00:48:50 +07:00
parent 2ab20a1150
commit b228538725
21 changed files with 431 additions and 37 deletions
@@ -3,6 +3,7 @@ import 'package:collection/collection.dart';
import 'package:dartz/dartz.dart' hide Order;
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart' hide Order;
import 'package:uuid/uuid.dart';
import '../../../common/types/void_type.dart';
import '../../../domain/order/order.dart';
@@ -105,13 +106,22 @@ class VoidFormBloc extends Bloc<VoidFormEvent, VoidFormState> {
});
}
emit(state.copyWith(isSubmitting: true, failureOrVoid: none()));
// Generate new idempotency key saat user intent (tap Void)
// Kalau sedang retry, pakai key yang sama
final idempotencyKey = state.idempotencyKey ?? const Uuid().v4();
emit(state.copyWith(
isSubmitting: true,
failureOrVoid: none(),
idempotencyKey: idempotencyKey,
));
failureOrVoid = await _repository.voidOrder(
orderId: state.order.id,
reason: state.voidReason ?? '',
orderItems: voidItems,
type: state.voidType.toStringType(),
idempotencyKey: idempotencyKey,
);
emit(
@@ -119,6 +129,8 @@ class VoidFormBloc extends Bloc<VoidFormEvent, VoidFormState> {
isSubmitting: false,
failureOrVoid: optionOf(failureOrVoid),
voidItems: state.voidType.isItem ? voidItems : state.pendingItems,
// Clear key on success, keep on failure for retry
idempotencyKey: failureOrVoid.isRight() ? null : idempotencyKey,
),
);
},
@@ -1256,6 +1256,11 @@ mixin _$VoidFormState {
throw _privateConstructorUsedError;
bool get isSubmitting => throw _privateConstructorUsedError;
/// Idempotency key untuk void submission.
/// Di-generate saat user tap "Void", dipakai ulang saat retry,
/// di-clear setelah sukses atau clearState.
String? get idempotencyKey => throw _privateConstructorUsedError;
/// Create a copy of VoidFormState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -1280,6 +1285,7 @@ abstract class $VoidFormStateCopyWith<$Res> {
int totalPriceVoid,
Option<Either<OrderFailure, Unit>> failureOrVoid,
bool isSubmitting,
String? idempotencyKey,
});
$OrderCopyWith<$Res> get order;
@@ -1309,6 +1315,7 @@ class _$VoidFormStateCopyWithImpl<$Res, $Val extends VoidFormState>
Object? totalPriceVoid = null,
Object? failureOrVoid = null,
Object? isSubmitting = null,
Object? idempotencyKey = freezed,
}) {
return _then(
_value.copyWith(
@@ -1348,6 +1355,10 @@ class _$VoidFormStateCopyWithImpl<$Res, $Val extends VoidFormState>
? _value.isSubmitting
: isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,
idempotencyKey: freezed == idempotencyKey
? _value.idempotencyKey
: idempotencyKey // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
@@ -1383,6 +1394,7 @@ abstract class _$$VoidFormStateImplCopyWith<$Res>
int totalPriceVoid,
Option<Either<OrderFailure, Unit>> failureOrVoid,
bool isSubmitting,
String? idempotencyKey,
});
@override
@@ -1412,6 +1424,7 @@ class __$$VoidFormStateImplCopyWithImpl<$Res>
Object? totalPriceVoid = null,
Object? failureOrVoid = null,
Object? isSubmitting = null,
Object? idempotencyKey = freezed,
}) {
return _then(
_$VoidFormStateImpl(
@@ -1451,6 +1464,10 @@ class __$$VoidFormStateImplCopyWithImpl<$Res>
? _value.isSubmitting
: isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,
idempotencyKey: freezed == idempotencyKey
? _value.idempotencyKey
: idempotencyKey // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
@@ -1469,6 +1486,7 @@ class _$VoidFormStateImpl implements _VoidFormState {
required this.totalPriceVoid,
required this.failureOrVoid,
this.isSubmitting = false,
this.idempotencyKey,
}) : _pendingItems = pendingItems,
_voidItems = voidItems,
_selectedItemQuantities = selectedItemQuantities;
@@ -1512,9 +1530,15 @@ class _$VoidFormStateImpl implements _VoidFormState {
@JsonKey()
final bool isSubmitting;
/// Idempotency key untuk void submission.
/// Di-generate saat user tap "Void", dipakai ulang saat retry,
/// di-clear setelah sukses atau clearState.
@override
final String? idempotencyKey;
@override
String toString() {
return 'VoidFormState(order: $order, pendingItems: $pendingItems, voidItems: $voidItems, voidType: $voidType, selectedItemQuantities: $selectedItemQuantities, voidReason: $voidReason, totalPriceVoid: $totalPriceVoid, failureOrVoid: $failureOrVoid, isSubmitting: $isSubmitting)';
return 'VoidFormState(order: $order, pendingItems: $pendingItems, voidItems: $voidItems, voidType: $voidType, selectedItemQuantities: $selectedItemQuantities, voidReason: $voidReason, totalPriceVoid: $totalPriceVoid, failureOrVoid: $failureOrVoid, isSubmitting: $isSubmitting, idempotencyKey: $idempotencyKey)';
}
@override
@@ -1544,7 +1568,9 @@ class _$VoidFormStateImpl implements _VoidFormState {
(identical(other.failureOrVoid, failureOrVoid) ||
other.failureOrVoid == failureOrVoid) &&
(identical(other.isSubmitting, isSubmitting) ||
other.isSubmitting == isSubmitting));
other.isSubmitting == isSubmitting) &&
(identical(other.idempotencyKey, idempotencyKey) ||
other.idempotencyKey == idempotencyKey));
}
@override
@@ -1559,6 +1585,7 @@ class _$VoidFormStateImpl implements _VoidFormState {
totalPriceVoid,
failureOrVoid,
isSubmitting,
idempotencyKey,
);
/// Create a copy of VoidFormState
@@ -1581,6 +1608,7 @@ abstract class _VoidFormState implements VoidFormState {
required final int totalPriceVoid,
required final Option<Either<OrderFailure, Unit>> failureOrVoid,
final bool isSubmitting,
final String? idempotencyKey,
}) = _$VoidFormStateImpl;
@override
@@ -1602,6 +1630,12 @@ abstract class _VoidFormState implements VoidFormState {
@override
bool get isSubmitting;
/// Idempotency key untuk void submission.
/// Di-generate saat user tap "Void", dipakai ulang saat retry,
/// di-clear setelah sukses atau clearState.
@override
String? get idempotencyKey;
/// Create a copy of VoidFormState
/// with the given fields replaced by the non-null parameter values.
@override
@@ -12,6 +12,10 @@ class VoidFormState with _$VoidFormState {
required int totalPriceVoid,
required Option<Either<OrderFailure, Unit>> failureOrVoid,
@Default(false) bool isSubmitting,
/// Idempotency key untuk void submission.
/// Di-generate saat user tap "Void", dipakai ulang saat retry,
/// di-clear setelah sukses atau clearState.
String? idempotencyKey,
}) = _VoidFormState;
factory VoidFormState.initial() => VoidFormState(