15 — Flutter Implementation Guide (Payments)
How Flutter developers build the payments feature. Base architecture: 00-shared/11.
Folder structure
features/payments/
├── data/
│ ├── dto/payment_dto.dart # fromJson/toJson (envelope payload)
│ ├── dto/receipt_dto.dart
│ ├── dto/process_payment_request.dart
│ ├── models/payment.dart # domain: status enum, gateway enum, money
│ ├── models/receipt.dart
│ └── repositories/payment_repository.dart
├── presentation/
│ ├── cubit/payments_list_cubit.dart
│ ├── cubit/payment_detail_cubit.dart
│ ├── cubit/new_payment_cubit.dart
│ ├── cubit/receipts_list_cubit.dart
│ ├── pages/payments_list_page.dart
│ ├── pages/payment_detail_page.dart
│ ├── pages/new_payment_page.dart
│ ├── pages/receipt_view_page.dart
│ ├── pages/receipts_list_page.dart
│ └── widgets/ (payment_row, amount_text, status_badge, receipt_view_card,
│ payment_form_fields, actions_row, money_progress)
Key implementation notes
DTO/model mapping
PaymentDto.fromJsonparses snake_case payload; mapstatus/gatewayto enums with unknown-value fallback (unknown).- Money: keep server
doubleinternally BUT format viaNumberFormat.currency; never do arithmetic with floats where avoidable — round to 2 decimals on display. Document minor-units migration as future work. Paymentmodel exposes:refundableAmount = amount - refundedAmount.
Repository
fetchPayments(page, limit)→ parse{data,total}(notmeta— deviation!).processPayment(ProcessPaymentRequest)→ returns(Payment, Receipt).refundPayment(paymentId, amount?, reason?).reconcilePayment(ref, status).fetchReceipts(page, limit),fetchReceipt(id),fetchByInvoice(invoiceId).- Throws typed
ApiException(code, status, details)viaAppDiointerceptors.
Cubits
PaymentsListCubitextendsPaginatedListMixin<Payment>(client-side filter keepsList<Payment> filteredderived in UI or viaemitter).NewPaymentCubit:Idle/Submitting/Success/Failed; on Success emitPaymentCreatedto refresh list + invoice.- WS:
WsClient.subscribe('payment.processed')→ debounce reload.
Navigation
- Routes:
/payments,/payments/new,/payments/:id,/payments/receipts,/payments/receipts/:id. - Deep links (forward-looking) mapped in
AppRouter. - Guard:
permissionGuard('payments.read')on routes; action visibility via permissions selector.
Money formatting
- Extension
double.toMoney(currency, locale)→NumberFormat.currency(locale: locale, name: currencyCode); tabular figures.
Localization
- Keys:
payments.*(list.title, record.title, receipt.title, errors., status.).
Testing
- Unit:
PaymentMapperTest,RefundRulesTest(max/partial/full),CubitTest(list pagination + filters, new-payment state machine, 409 mapping). - Widget: S1 states (loading/empty/error/data), S3 validation + success view, S4 sheet guards.
- Golden: PaymentRow, StatusBadge (7 states), ReceiptViewCard — light/dark, 3 sizes.
- Integration: record cash payment against prefilled invoice → receipt visible → invoice status PAID.
Performance
ListView.builder; rowsconst-friendly;RepaintBoundaryon receipt card + charts; pagination via footer button not scroll listener (server contract).- Avoid rebuilding filter chips per keystroke (debounce 300 ms).
Gotchas from source
GET /paymentsand/payments/receiptsreturn{data, total}— client pagination derived manually; do not assumemeta.PaymentProcessedis WS-broadcast but not BullMQ-routed — realtime refresh is the only push signal; receipt email (PaymentCompleted→emails) is a dead map entry.- Receipt numbers derive from an in-process counter — do not rely on monotonicity across instances.