From ffe076e120cc73d542ff3ccef6c9720306940246 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 7 Aug 2025 14:48:59 +0700 Subject: [PATCH] feat: payment split --- .../datasources/order_remote_datasource.dart | 35 ++++ lib/data/models/request/payment_request.dart | 54 ++++++ .../payment/pages/payment_page.dart | 62 ++++-- .../blocs/payment_form/payment_form_bloc.dart | 17 ++ .../payment_form_bloc.freezed.dart | 182 +++++++++++++++--- .../payment_form/payment_form_event.dart | 7 +- .../split_bill/pages/split_bill_page.dart | 14 +- 7 files changed, 325 insertions(+), 46 deletions(-) diff --git a/lib/data/datasources/order_remote_datasource.dart b/lib/data/datasources/order_remote_datasource.dart index 4e129bd..a0f35d8 100644 --- a/lib/data/datasources/order_remote_datasource.dart +++ b/lib/data/datasources/order_remote_datasource.dart @@ -587,4 +587,39 @@ class OrderRemoteDatasource { return const Left('Terjadi kesalahan tak terduga'); } } + + Future> createPaymentSplitBill( + PaymentSplitBillRequest request) async { + final authData = await AuthLocalDataSource().getAuthData(); + final url = '${Variables.baseUrl}/api/v1/orders/split-bill'; + + try { + final response = await dio.post( + url, + data: request.toMap(), + options: Options( + headers: { + 'Authorization': 'Bearer ${authData.token}', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + ), + ); + + if (response.statusCode == 200) { + return Right(PaymentSuccessResponseModel.fromMap(response.data)); + } else { + return const Left('Gagal membuat pembayaran'); + } + } on DioException catch (e) { + final errorMessage = + e.response?.data['message'] ?? 'Terjadi kesalahan, coba lagi nanti.'; + log("💥 Dio error: ${e.message}"); + log("💥 Dio response: ${e.response?.data}"); + return Left(errorMessage); + } catch (e) { + log("💥 Unexpected error: $e"); + return const Left('Terjadi kesalahan tak terduga'); + } + } } diff --git a/lib/data/models/request/payment_request.dart b/lib/data/models/request/payment_request.dart index 8386f1a..c27372d 100644 --- a/lib/data/models/request/payment_request.dart +++ b/lib/data/models/request/payment_request.dart @@ -78,3 +78,57 @@ class PaymentOrderItemModel { "amount": amount, }; } + +class PaymentSplitBillRequest { + final String orderId; + final String paymentMethodId; + final String customerId; + final String type; // e.g., "AMOUNT" or "ITEM" + final int amount; + final List items; + + PaymentSplitBillRequest({ + required this.orderId, + required this.paymentMethodId, + required this.customerId, + required this.type, + required this.amount, + required this.items, + }); + + Map toMap() { + Map data = { + 'order_id': orderId, + 'payment_method_id': paymentMethodId, + 'customer_id': customerId, + 'type': type, + }; + + if (type == "AMOUNT") { + data['amount'] = amount; + } + + if (type == "ITEM") { + data['items'] = items.map((e) => e.toJson()).toList(); + } + + return data; + } +} + +class SplitItem { + final String orderItemId; + final int quantity; + + SplitItem({ + required this.orderItemId, + required this.quantity, + }); + + Map toJson() { + return { + 'order_item_id': orderItemId, + 'quantity': quantity, + }; + } +} diff --git a/lib/presentation/payment/pages/payment_page.dart b/lib/presentation/payment/pages/payment_page.dart index c8cd64f..6a153e1 100644 --- a/lib/presentation/payment/pages/payment_page.dart +++ b/lib/presentation/payment/pages/payment_page.dart @@ -17,7 +17,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class PaymentPage extends StatefulWidget { final Order order; - const PaymentPage({Key? key, required this.order}) : super(key: key); + final bool isSplit; + final String? splitType; + const PaymentPage({ + super.key, + required this.order, + this.isSplit = false, + this.splitType, + }); @override State createState() => _PaymentPageState(); @@ -434,23 +441,42 @@ class _PaymentPageState extends State { final itemPending = widget.order.orderItems ?.where((item) => item.status == "pending") .toList(); + if (widget.isSplit == false) { + final request = PaymentRequestModel( + amount: widget.order.totalAmount ?? 0, + orderId: widget.order.id, + paymentMethodId: selectedPaymentMethod?.id, + splitDescription: '', + splitNumber: 1, + splitTotal: 1, + transactionId: '', + paymentOrderItems: itemPending + ?.map((item) => PaymentOrderItemModel( + orderItemId: item.id, + amount: item.totalPrice, + )) + .toList(), + ); - final request = PaymentRequestModel( - amount: widget.order.totalAmount ?? 0, - orderId: widget.order.id, - paymentMethodId: selectedPaymentMethod?.id, - splitDescription: '', - splitNumber: 1, - splitTotal: 1, - transactionId: '', - paymentOrderItems: itemPending - ?.map((item) => PaymentOrderItemModel( - orderItemId: item.id, - amount: item.totalPrice, - )) - .toList(), - ); - - context.read().add(PaymentFormEvent.create(request)); + context.read().add(PaymentFormEvent.create(request)); + } else { + final request = PaymentSplitBillRequest( + amount: widget.order.totalAmount ?? 0, + customerId: '', + items: itemPending + ?.map((item) => SplitItem( + orderItemId: item.id ?? "", + quantity: item.quantity ?? 0, + )) + .toList() ?? + [], + orderId: widget.order.id ?? "", + paymentMethodId: selectedPaymentMethod?.id ?? "", + type: widget.splitType ?? "", + ); + context.read().add( + PaymentFormEvent.createSplitBill(request), + ); + } } } diff --git a/lib/presentation/sales/blocs/payment_form/payment_form_bloc.dart b/lib/presentation/sales/blocs/payment_form/payment_form_bloc.dart index f491395..2eb42c5 100644 --- a/lib/presentation/sales/blocs/payment_form/payment_form_bloc.dart +++ b/lib/presentation/sales/blocs/payment_form/payment_form_bloc.dart @@ -29,5 +29,22 @@ class PaymentFormBloc extends Bloc { } }, ); + on<_CreateSplitBill>( + (event, emit) async { + emit(const _Loading()); + + try { + final result = await _orderRemoteDatasource + .createPaymentSplitBill(event.payment); + + result.fold( + (error) => emit(_Error(error)), + (success) => emit(_Success(success.data!)), + ); + } catch (e) { + emit(_Error("Failed to create payment split bill: $e")); + } + }, + ); } } diff --git a/lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart b/lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart index 77ad755..01cf3ce 100644 --- a/lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart +++ b/lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart @@ -16,45 +16,45 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$PaymentFormEvent { - PaymentRequestModel get payment => throw _privateConstructorUsedError; + Object get payment => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ required TResult Function(PaymentRequestModel payment) create, + required TResult Function(PaymentSplitBillRequest payment) createSplitBill, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ TResult? Function(PaymentRequestModel payment)? create, + TResult? Function(PaymentSplitBillRequest payment)? createSplitBill, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ TResult Function(PaymentRequestModel payment)? create, + TResult Function(PaymentSplitBillRequest payment)? createSplitBill, required TResult orElse(), }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult map({ required TResult Function(_Create value) create, + required TResult Function(_CreateSplitBill value) createSplitBill, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ TResult? Function(_Create value)? create, + TResult? Function(_CreateSplitBill value)? createSplitBill, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ TResult Function(_Create value)? create, + TResult Function(_CreateSplitBill value)? createSplitBill, required TResult orElse(), }) => throw _privateConstructorUsedError; - - /// Create a copy of PaymentFormEvent - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $PaymentFormEventCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc @@ -62,8 +62,6 @@ abstract class $PaymentFormEventCopyWith<$Res> { factory $PaymentFormEventCopyWith( PaymentFormEvent value, $Res Function(PaymentFormEvent) then) = _$PaymentFormEventCopyWithImpl<$Res, PaymentFormEvent>; - @useResult - $Res call({PaymentRequestModel payment}); } /// @nodoc @@ -78,27 +76,13 @@ class _$PaymentFormEventCopyWithImpl<$Res, $Val extends PaymentFormEvent> /// Create a copy of PaymentFormEvent /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? payment = null, - }) { - return _then(_value.copyWith( - payment: null == payment - ? _value.payment - : payment // ignore: cast_nullable_to_non_nullable - as PaymentRequestModel, - ) as $Val); - } } /// @nodoc -abstract class _$$CreateImplCopyWith<$Res> - implements $PaymentFormEventCopyWith<$Res> { +abstract class _$$CreateImplCopyWith<$Res> { factory _$$CreateImplCopyWith( _$CreateImpl value, $Res Function(_$CreateImpl) then) = __$$CreateImplCopyWithImpl<$Res>; - @override @useResult $Res call({PaymentRequestModel payment}); } @@ -163,6 +147,7 @@ class _$CreateImpl implements _Create { @optionalTypeArgs TResult when({ required TResult Function(PaymentRequestModel payment) create, + required TResult Function(PaymentSplitBillRequest payment) createSplitBill, }) { return create(payment); } @@ -171,6 +156,7 @@ class _$CreateImpl implements _Create { @optionalTypeArgs TResult? whenOrNull({ TResult? Function(PaymentRequestModel payment)? create, + TResult? Function(PaymentSplitBillRequest payment)? createSplitBill, }) { return create?.call(payment); } @@ -179,6 +165,7 @@ class _$CreateImpl implements _Create { @optionalTypeArgs TResult maybeWhen({ TResult Function(PaymentRequestModel payment)? create, + TResult Function(PaymentSplitBillRequest payment)? createSplitBill, required TResult orElse(), }) { if (create != null) { @@ -191,6 +178,7 @@ class _$CreateImpl implements _Create { @optionalTypeArgs TResult map({ required TResult Function(_Create value) create, + required TResult Function(_CreateSplitBill value) createSplitBill, }) { return create(this); } @@ -199,6 +187,7 @@ class _$CreateImpl implements _Create { @optionalTypeArgs TResult? mapOrNull({ TResult? Function(_Create value)? create, + TResult? Function(_CreateSplitBill value)? createSplitBill, }) { return create?.call(this); } @@ -207,6 +196,7 @@ class _$CreateImpl implements _Create { @optionalTypeArgs TResult maybeMap({ TResult Function(_Create value)? create, + TResult Function(_CreateSplitBill value)? createSplitBill, required TResult orElse(), }) { if (create != null) { @@ -224,12 +214,154 @@ abstract class _Create implements PaymentFormEvent { /// Create a copy of PaymentFormEvent /// with the given fields replaced by the non-null parameter values. - @override @JsonKey(includeFromJson: false, includeToJson: false) _$$CreateImplCopyWith<_$CreateImpl> get copyWith => throw _privateConstructorUsedError; } +/// @nodoc +abstract class _$$CreateSplitBillImplCopyWith<$Res> { + factory _$$CreateSplitBillImplCopyWith(_$CreateSplitBillImpl value, + $Res Function(_$CreateSplitBillImpl) then) = + __$$CreateSplitBillImplCopyWithImpl<$Res>; + @useResult + $Res call({PaymentSplitBillRequest payment}); +} + +/// @nodoc +class __$$CreateSplitBillImplCopyWithImpl<$Res> + extends _$PaymentFormEventCopyWithImpl<$Res, _$CreateSplitBillImpl> + implements _$$CreateSplitBillImplCopyWith<$Res> { + __$$CreateSplitBillImplCopyWithImpl( + _$CreateSplitBillImpl _value, $Res Function(_$CreateSplitBillImpl) _then) + : super(_value, _then); + + /// Create a copy of PaymentFormEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? payment = null, + }) { + return _then(_$CreateSplitBillImpl( + null == payment + ? _value.payment + : payment // ignore: cast_nullable_to_non_nullable + as PaymentSplitBillRequest, + )); + } +} + +/// @nodoc + +class _$CreateSplitBillImpl implements _CreateSplitBill { + const _$CreateSplitBillImpl(this.payment); + + @override + final PaymentSplitBillRequest payment; + + @override + String toString() { + return 'PaymentFormEvent.createSplitBill(payment: $payment)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreateSplitBillImpl && + (identical(other.payment, payment) || other.payment == payment)); + } + + @override + int get hashCode => Object.hash(runtimeType, payment); + + /// Create a copy of PaymentFormEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CreateSplitBillImplCopyWith<_$CreateSplitBillImpl> get copyWith => + __$$CreateSplitBillImplCopyWithImpl<_$CreateSplitBillImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(PaymentRequestModel payment) create, + required TResult Function(PaymentSplitBillRequest payment) createSplitBill, + }) { + return createSplitBill(payment); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(PaymentRequestModel payment)? create, + TResult? Function(PaymentSplitBillRequest payment)? createSplitBill, + }) { + return createSplitBill?.call(payment); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(PaymentRequestModel payment)? create, + TResult Function(PaymentSplitBillRequest payment)? createSplitBill, + required TResult orElse(), + }) { + if (createSplitBill != null) { + return createSplitBill(payment); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Create value) create, + required TResult Function(_CreateSplitBill value) createSplitBill, + }) { + return createSplitBill(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Create value)? create, + TResult? Function(_CreateSplitBill value)? createSplitBill, + }) { + return createSplitBill?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Create value)? create, + TResult Function(_CreateSplitBill value)? createSplitBill, + required TResult orElse(), + }) { + if (createSplitBill != null) { + return createSplitBill(this); + } + return orElse(); + } +} + +abstract class _CreateSplitBill implements PaymentFormEvent { + const factory _CreateSplitBill(final PaymentSplitBillRequest payment) = + _$CreateSplitBillImpl; + + @override + PaymentSplitBillRequest get payment; + + /// Create a copy of PaymentFormEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreateSplitBillImplCopyWith<_$CreateSplitBillImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$PaymentFormState { @optionalTypeArgs diff --git a/lib/presentation/sales/blocs/payment_form/payment_form_event.dart b/lib/presentation/sales/blocs/payment_form/payment_form_event.dart index bc34461..117209e 100644 --- a/lib/presentation/sales/blocs/payment_form/payment_form_event.dart +++ b/lib/presentation/sales/blocs/payment_form/payment_form_event.dart @@ -2,5 +2,10 @@ part of 'payment_form_bloc.dart'; @freezed class PaymentFormEvent with _$PaymentFormEvent { - const factory PaymentFormEvent.create(PaymentRequestModel payment) = _Create; + const factory PaymentFormEvent.create( + PaymentRequestModel payment, + ) = _Create; + const factory PaymentFormEvent.createSplitBill( + PaymentSplitBillRequest payment, + ) = _CreateSplitBill; } diff --git a/lib/presentation/split_bill/pages/split_bill_page.dart b/lib/presentation/split_bill/pages/split_bill_page.dart index 334ad63..3057d8e 100644 --- a/lib/presentation/split_bill/pages/split_bill_page.dart +++ b/lib/presentation/split_bill/pages/split_bill_page.dart @@ -760,7 +760,11 @@ class _SplitBillPageState extends State { log("Split Order: ${splitItems.length}"); // Navigate to PaymentPage with the split order - context.push(PaymentPage(order: splitOrder)); + context.push(PaymentPage( + order: splitOrder, + isSplit: true, + splitType: 'ITEM', + )); } else { AppFlushbar.showError(context, "Pilih minimal satu produk untuk split"); } @@ -781,7 +785,13 @@ class _SplitBillPageState extends State { ); // Navigate to PaymentPage with the split order - context.push(PaymentPage(order: splitOrder)); + context.push( + PaymentPage( + order: splitOrder, + isSplit: true, + splitType: 'AMOUNT', + ), + ); } else if (splitAmount > totalAmount) { AppFlushbar.showError( context, "Jumlah split tidak boleh melebihi total bill");