09 — User Behaviour (Fees Module)
- 1. Expected behaviour (server-contract-driven)
- 2. Common mistakes & countermeasures
- 3. Power & new users
- 4. Mobile & a11y users
- 5. Cross-device & conflict
- 6. Interrupted sessions & retries
- 7. Abandonment
- 8. Frustration points → UX treatments (summary)
- 9. Adoption path (holistic)
Expected behaviour derived from server contracts, plus the failure modes that money UIs invite. Sources:
fees.service.ts,payments.service.ts, fees DTOs, fees/payments schemas.
1. Expected behaviour (server-contract-driven)
- Balance is server truth.
due = totalAmount − paidAmountis produced byGET /fees/dues(fees.service.ts:213-216); the client never recomputes an authoritative balance, only derives display from server values (06 §0). - Payment posts are idempotent. Reusing an
idempotencyKeyreturns the original payment (fees.service.ts:148-152). Double-tap or retry-after-timeout must NOT double-count. - Invoice generation is once per term. Duplicate → server 409
("Invoice already exists for this student and term.",
fees.service.ts:107-115); the UI treats it as "navigate to existing", never as a hard error. - Status is derived/pushed, never client-mutated.
issued → partial/paidbypaidTotal >= totalAmount(fees.service.ts:168-176);overdueis pushed by the finance worker (finance.worker.ts:74-91)(planned). - Overdue rendering should prefer
invoice.status == 'overdue'(invoice.schema.ts:12); money past-due that still holdsissued/partial(worker not yet run) is styled identically client-side but flagged(proposed).
2. Common mistakes & countermeasures
| Mistake | Countermeasure (client) |
|---|---|
| Double-tap "Record payment" | disable while posting + reuse the same idempotencyKey on retry (record-payment.dto.ts:31-34) |
| Wrong amount paid | prefill amount = due; MoneyField >= 0; overpay inline warning (server allows, OQ-2) |
| Posting to a paid/cancelled invoice | hide the action when `paid |
| Misreading a 409 replay as failure | render as success — the returned doc is the original payment (fees.service.ts:148-152) |
Items sum != totalAmount | LineItemsTable warning banner; auto-sum helper on create (server never validates) |
Confusing XAF vs USD | currency locked from context: structures default XAF (fee-structure.schema.ts:27-28), v2 payments default USD (payments/schemas/payment.schema.ts:42-43) |
| Reading refunded/failed sums as paid | use server paidAmount/dues; never hand-sum v2 logs (OQ-5 two sumByInvoice implementations) |
| Editing class/year after create | fields absent from UpdateFeeStructureDto (update-fee-structure.dto.ts:15-45) — disabled + note |
| Offline payment attempt | blocked with guidance (no offline write queue for fees; 00-shared/07 §10) |
3. Power & new users
- Power (cashier): remember last payment method;
paidDate = todaydefault; one-tap "Collect" on dues rows; keyboard-friendly sheet (.next,.done). - Power (admin): structure duplicate
(planned)(IMPLEMENTATION_PLAN.md:205), bulk invoice generation(planned)(today generation is per-student sync, OQ-8). - New (parent): only the child-invoices read surface; every screen explains
"Total / Paid / Due"; "Pay online"
(forward-looking).
4. Mobile & a11y users
- Touch targets ≥ 48 (00-shared/02 §9); rows ≥ 56.
- Amounts with currency symbol/word +
tabularFigures; screen readers announce localized (e.g. "fifty thousand CFA francs"). - Status chips are icon + text + color — never color-only.
- Forms: labels +
Semantics(error); errors announced;MoneyFieldannounces value + currency. - Payment sheet: live-region announce submit progress, success, errors.
5. Cross-device & conflict
- Two terminals posting the same invoice: idempotency prevents doubles; the last
write recomputes
paidAmount/statusfrom totals (fees.service.ts:168-176). - Parent pays online while a cashier posts offline: both recorded;
linkToInvoicesums all (payments.service.ts:200-213). - Lists vs version drift: re-fetch on focus + pull-to-refresh +
invoice.updatedWS(planned)topic (00-shared/07 §8).
6. Interrupted sessions & retries
- Failure mid-
POSTpayment: sameidempotencyKeyretried → replay success. - Failure mid-
POST invoices/generate→ retry hits 409 → navigate to existing invoice with info banner. - Session expiry mid-payment: key retained while the sheet is open; discard invalidates client flow (server key stays inert until reused).
- Background kill mid-edit structure: re-fetch on open; form preserves draft
(proposed).
7. Abandonment
- Structure form dirty → "Discard changes?" dialog;
(proposed)draft toshared_preferences. - Payment sheet back mid-post → "Payment not recorded — discard?"; discarding abandons the key.
- Generate-invoice dialog cancel → nothing sent; invoice exists only after 201.
8. Frustration points → UX treatments (summary)
| Frustration | Treatment |
|---|---|
| Duplicate-invoice 409 | info banner + link to the existing invoice, dialog open |
| Pay on paid/cancelled invoice | close sheet + info banner, never an error dialog |
| Overdue backlog with 5-10 rows | group/filter dues by status; overdue chip distinct from "valid error" |
| $0 balance | "Settled" text, never "−0" |
| Receipt print/share | (proposed) RepaintBoundary render → system share/print |
| No single-invoice endpoint | invoice detail composes from student invoices + GET /payments/invoice/:id (OQ-3) |
9. Adoption path (holistic)
Fees is the highest-trust workflow; order screens by trust: (1) dues at a glance
(fees home → dues list), (2) collect at the till (dues → record payment; cash is
the dominant method), (3) structure management once a term, (4) online pay +
receipts for parents (forward-looking), (5) reports / reminders once server
lands (planned).