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

03 — User Journey (Fees Module)

End-to-end journeys mapped to the implemented API. Every step cites the exact endpoint + rule source. Async/reminder steps marked (planned) where the worker wiring is incomplete (OQ-6/OQ-7 in 01).


1. Journey: Term kickoff — define fee structures

  1. Admin opens Fee Structures list → GET /fees/structures?page&limit (fees.controller.ts:34-38), paginated, meta = {page, limit, totalItems, totalPages, hasNext, hasPrevious} (pagination-query.dto.ts:32-55).
  2. Create structurePOST /fees/structures (fees.controller.ts:28-32) body {name, classId, academicYearId, items[], totalAmount, currency?, dueDate, lateFee?, isActive?} (create-fee-structure.dto.ts:26-68). Server stamps dueDate Date, defaults currency='XAF', lateFee=0, isActive=true (fee-structure.schema.ts:27-37).
  3. Server emits FeeStructureCreatedin-app queue job fee-structure-created (fees.service.ts:49-56, event-queue-map.ts:40).
  4. List refreshes (200 envelope, doc incl. _id, tenantId, createdAt… from BaseSchema).
  5. (planned) Duplicate a structure for the next class → POST /fees/structures/:id/duplicate (IMPLEMENTATION_PLAN.md:205).

2. Journey: Issue an invoice to a student

  1. From a structure detail, admin picks a student → POST /fees/invoices/generate (fees.controller.ts:58-62) body {studentId, feeStructureId, academicYearId, discounts?} (generate-invoice.dto.ts:24-42).
  2. Server: 404 if structure missing; 409 if an invoice already exists for (student, structure, year) ("Invoice already exists for this student and term.") — fees.service.ts:101-115; unique index backs it (invoice.schema.ts:58-60).
  3. totalAmount = max(0, structure.totalAmount − Σ discounts); status issued, issuedAt=now, dueDate copied from structure (fees.service.ts:117-130).
  4. InvoiceIssued event → emails queue job send-invoice (fees.service.ts:131-138, event-queue-map.ts:41).
  5. Parent sees it under GET /fees/students/:studentId/invoices (fees.controller.ts:64-68).
  6. (planned) Bulk/async generation for a whole class → INVOICE_GENERATE (queue.constants.ts:8) + FinanceWorker exist (finance.worker.ts:16-91) but generateInvoice runs synchronously today (OQ-8).

3. Journey: Collect a payment at the till (fees module)

  1. Cashier opens Dues → GET /fees/dues?page&limit — only status issued|partial|overdue (fees.service.ts:190-212); each row due = totalAmount − paidAmount (fees.service.ts:213-216).
  2. Taps "Record payment" → POST /fees/invoices/:id/payments (fees.controller.ts:70-74) body {invoiceId, amount, paymentMethod, reference?, idempotencyKey, paidAt, notes?} (record-payment.dto.ts:11-44).
  3. Server: replay with same key → returns existing payment, no double-count (fees.service.ts:148-152); invoice PAID/CANCELLED → 409 (fees.service.ts:155-160).
  4. Status recomputed: paidTotal >= total → paid else partial (fees.service.ts:168-176); PaymentCompletedemails/send-receipt (fees.service.ts:178-185, event-queue-map.ts:42).
  5. Dues row updates (or disappears when paid).

4. Journey: Pay online (payments module) (forward-looking)

  1. Parent taps "Pay" on an invoice → POST /payments (payments.controller.ts:24-28) body {amount, currency?, gateway, gatewayTransactionId?, invoiceId?, invoiceType?, payerId?, payerEmail?, payerName?, description?} (process-payment.dto.ts:5-54).
  2. Server: transactionReference = TXN-{ts}-{uuid8} (payments.service.ts:215-217), payment stored in payments_v2 with status: completed by default (payments.service.ts:36-49).
  3. Receipt auto-created RCP-{ts}-{n} (payments.service.ts:180-198).
  4. If invoiceId given → linkToInvoice recomputes invoice paidAmount + status (payments.service.ts:200-213).
  5. Gateway async path: PATCH /payments/:transactionRef/reconcile body {status:'success'|'failed'|…} → completed/failed/pending (payments.controller.ts:36-43, payments.service.ts:113-134).
  6. Refund → POST /payments/refund (only completed; partial allowed) (payments.controller.ts:30-34, payments.service.ts:74-111).
  7. Receipts read via GET /payments/receipts[:id] (payments.controller.ts:63-73).

5. Journey: Reminder + overdue lifecycle (planned)

  1. Daily fee-reminder cron 0 8 * * * UTC on payment-reminder queue (scheduler.service.ts:91-97).
  2. FeeReminderJob scans issued|partial invoices with now ≤ dueDate ≤ now+3d, enqueues send-payment-reminder jobs (fee-reminder.job.ts:16-42).
  3. Daily overdue-scan cron 0 6 * * * UTC on invoice-generate (scheduler.service.ts:48-55); FinanceWorker marks issued|partial invoices past due as overdue (finance.worker.ts:75-91).
  4. Status today: crons registered, but .execute() is never called and no worker listens on payment-reminder → deliveries/overdue marking not active (OQ-6). UI must render overdue chips when the status arrives, and treat reminders as absent until wired.

6. Journey: Reconciliation (support)

  1. GET /payments/invoice/:invoiceId (payments.controller.ts:57-61) vs GET /fees/students/:studentId/invoices — note the two payment collections (fees payments vs payments_v2) and two sumByInvoice implementations (fees sums all statuses fees/repositories/payment.repository.ts:23-35, payments sums only completed payments/repositories/payment.repository.ts:25-37) — OQ-5.
  2. Mismatch handling: mark refunds/partials on the v2 side; invoice paidAmount recomputed on next post (linkToInvoice).

7. Journey: Report on dues (admin) (proposed)

  1. No dedicated report endpoint — GET /fees/reports from Fees.md:31 is not in the controller (fees.controller.ts:1-81). Client sums the paginated dues screen client-side today; a real report is (planned) (IMPLEMENTATION_PLAN.md:230 "Reports"; :211 per-student statement).
  2. Per-student statement → GET /fees/students/:studentId/invoices + per-invoice GET /payments/invoice/:invoiceId composed client-side (proposed).