Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.fromJson parses snake_case payload; map status/gateway to enums with unknown-value fallback (unknown).
  • Money: keep server double internally BUT format via NumberFormat.currency; never do arithmetic with floats where avoidable — round to 2 decimals on display. Document minor-units migration as future work.
  • Payment model exposes: refundableAmount = amount - refundedAmount.

Repository

  • fetchPayments(page, limit) → parse {data,total} (not meta — 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) via AppDio interceptors.

Cubits

  • PaymentsListCubit extends PaginatedListMixin<Payment> (client-side filter keeps List<Payment> filtered derived in UI or via emitter).
  • NewPaymentCubit: Idle/Submitting/Success/Failed; on Success emit PaymentCreated to refresh list + invoice.
  • WS: WsClient.subscribe('payment.processed') → debounce reload.
  • 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; rows const-friendly; RepaintBoundary on 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 /payments and /payments/receipts return {data, total} — client pagination derived manually; do not assume meta.
  • PaymentProcessed is 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.