Statement
Route /statement?chargeId=CHG-... — bonus B-05. Read-only ledger of unpaid charges and mock Pay.
Model
Unpaid = outstanding|contested|accepted (terminal resolved|paid excluded). Group totals per currency (never summed across currencies) — mirrors backend money rule.
class StatementView {
final Map<String /*currency*/, int /*minor*/> totals; // { "GBP": 4200 }
final List<Charge> outstandingCharges;
final List<Charge> acceptedCharges; // each with Pay CTO
final List<Charge> contestedCharges;
}
Behavior
- Header shows per-currency running total (e.g.,
£42.00from minor4200) with correct localized grouping (en42.00,cylocale match). - Each charge row shows
itemName — amountMinor→currency+ status pill + deadline hint. Payon anacceptedrow callsChargePorts.pay(chargeId, expectedEpoch, expectedVersion)→ optimisticpaid→ row leaves statement (history-only).- Global
Pay allCTA (when multiple accepted in same currency) iterates with sequentialIdempotency-Keys — not a batch endpoint.
No real settlement — button clearly labelled mock, documented in ADR.
Fees & presentation
KxSectionHeaderseparates totals from groups.KxSkeletonplaceholder on initial Drift stream.- Reduced motion collapses amount ticker animation.
Tests
test/features/statement/statement_page_test.dart asserts totals per currency, resolved|paid excluded, mock pay journey, and golden responsive states.