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

01 — Product Overview (Fees Module)

StudyLyon — multi-tenant ERP / School Management API. This package designs the Fees module client (Flutter, forward-looking spec) against the implemented NestJS backend. All endpoints, DTO fields, schemas, domain events, queue routes, permissions, and wire contracts are derived directly from src/modules/fees/**, src/modules/payments/**, src/modules/scheduler/**, src/infrastructure/bullmq/**, src/infrastructure/workers/finance.worker.ts, src/modules/rbac/permissions.constants.ts, studylyon-blueprint/04-Modules/Fees.md, and docs/IMPLEMENTATION_PLAN.md. No feature is invented — anything not present in source is flagged (planned) / (proposed) / (forward-looking) in Assumptions & Open Questions.

Heads-up: per the PRD, the mobile client is out of Phase 1; this package is the forward-looking spec the client will be built against later. Everything below is best-effort UI design on top of the current backend surface.


1. Purpose

Fees is the money lifecycle of a school: an admin defines a fee structure per class + academic term (line items such as tuition/lab/transport, a total, currency, due date, optional late fee); invoices are generated per student; payments are recorded against invoices (offline cash/mobile-money via the fees module, online gateway capture via the payments module); the system tracks dues (outstanding balances) and — via the scheduler — flags overdue invoices and enqueues payment reminders. Money rules come from studylyon-blueprint/04-Modules/Fees.md; the code is the source of truth.

ResponsibilitySource
Fee structure CRUD (create/list/get/patch/soft-delete)fees.controller.ts:28-56, fees.service.ts:42-98
Generate one invoice per (student, feeStructure, academicYear)fees.service.ts:101-140, unique index invoice.schema.ts:58-60
List a student's invoicesfees.service.ts:142-144, invoice.repository.ts:17-21
Record a payment idempotently (idempotencyKey unique)fees.service.ts:147-188, payment.schema.ts:40-41
Track dues (ISSUED/PARTIAL/OVERDUE, due = total − paid)fees.service.ts:190-221
Online payment capture + refund + reconcile + receiptspayments.controller.ts:24-73, payments.service.ts:31-217
Overdue detection (OVERDUE status)finance.worker.ts:75-91
Reminder scan (due within 3 days) → payment-reminder queuefee-reminder.job.ts:16-46
Repeatable scheduler jobs (daily cron)scheduler.service.ts:48-119
Tenant scoping + soft-delete on every querybase.repository.ts:20-30

2. Business goals

GoalMeasure
No invoice duplicatesunique index {tenantId, studentId, feeStructureId, academicYearId} + service 409 (invoice.schema.ts:58-60, fees.service.ts:107-113)
No double-counted paymentsunique idempotencyKey; replay returns existing payment (payment.schema.ts:40-41, fees.service.ts:148-152)
Dues always computabledue = totalAmount − paidAmount per invoice (fees.service.ts:213-216)
Cross-tenant isolationevery query tenant-scoped via BaseRepository.scopedFilter (base.repository.ts:20-30)
Audit + notification trailFeeStructureCreated / InvoiceIssued / PaymentCompleted events → BullMQ (event-queue-map.ts:40-42)

3. User goals

  • Admin / bursar (accounts): define a term's fee structure once; generate invoices per student; see outstanding dues per invoice.
  • Accountant (cashier): collect cash/mobile-money against an invoice and post a payment; never double-post (idempotency key).
  • Parent / payer: see child's invoices and dues; pay online through the payments module; read receipts.
  • Org admin: financial oversight (dues report, payment history).

4. Stakeholders

Org admin, bursar/accounts staff, cashier (Accountant role), parents, students, the notification/email pipeline, the scheduler (overdue + reminders), audit/compliance (event log), QA + design + engineering.

5. Why this exists

Fee collection is a money path: the backend already enforces the important invariants (one invoice per term, idempotent payments, soft-deleted structures, tenant isolation). The Client's job is to present balances and payment statuses authoritatively, never guess server state, and treat every write (payment, invoice generation, waiver (planned)) as server-confirmed.

6. Dependencies

DependencyRoleSource
Payments moduleOnline capture (gateway), refund, reconcile, receiptspayments.controller.ts:21-73, payments_v2 schema
Notifications/emailsInvoiceIssued → send-invoice; PaymentCompleted → send-receiptevent-queue-map.ts:41-42
Scheduleroverdue scan + fee-reminder cronsscheduler.service.ts:48-97
BullMQ invoice-generateFinanceWorker (overdue marking; event handling)finance.worker.ts:16-91
BullMQ payment-reminderreminder fan-out jobsfee-reminder.job.ts:26-39
Collectionsfee_structures, invoices, payments, payments_v2, receipts*-schema.ts

7. Success metrics

  • Invoice generation (single student) round-trip < 2 s (sync path today).
  • Duplicate-invoice attempt handled as 409 100% of the time (never a crash).
  • Payment replay with same idempotencyKey returns the original payment (no dup).
  • Zero cross-tenant leaks in any fees list/detail (BaseRepository scope).
  • Dues balance renders exact: totalAmount − paidAmount from server, not summed on the client.

8. Edge cases (contract level)

  • Duplicate invoice (same student+structure+year) → 409 DUPLICATE_RESOURCE "Invoice already exists for this student and term." (fees.service.ts:107-113)
    • unique index.
  • Pay on PAID/CANCELLED invoice → 409 DUPLICATE_RESOURCE "Invoice is already paid or cancelled." (fees.service.ts:155-160).
  • Unknown structure / invoice → 404 RESOURCE_NOT_FOUND (fees.service.ts:71-77,101-106).
  • Invalid Mongo idCastError → 400 VALIDATION_ERROR "Invalid resource identifier." (http-exception.filter.ts:47-48,91-95).
  • Delete structure is a soft delete (softDelete, base.repository.ts:68-74); invoices are not cascaded.
  • Discounts reduce the invoice total (totalAmount = max(0, structureTotal − Σdisc), fees.service.ts:117-119) — invoices total never below 0; the discount is snapshotted on the invoice as discounts[].
  • Overpayment has no cap server-side — @Min(0) only (record-payment.dto.ts:19); paidTotal >= totalAmount is what flips status to PAID (fees.service.ts:169-172).
  • Human currency is not integer-minor-enforced in code — blueprint says minor units (Fees.md:63) but DTOs/schemas store plain Number (fee-structure.schema.ts:19-28); the client must format/dealMinor and the server should floor (gap, OQ-1).

9. Assumptions (module)

  • PRD: mobile client out of Phase 1. This design is a forward-looking client-side spec; the backend is the contracts authority and the scheduler/late-fee surface is partially wired (see OQ-6/OQ-7).
  • Money appears as plain numbers (Number) in every schema; the blueprint's "integer minor units" rule (Fees.md:63) is not enforced in code — treat server values as decimal units unless a conversion is established (OQ-1).
  • Payment posting uses an idempotency key on the fees side (record-payment.dto.ts:31-34) but the online payments module instead uses a server-generated transactionReference (payments.service.ts:215-217) — two idempotency models.
  • InvoiceIssued / PaymentCompletedemails queue (event-queue-map.ts:41,42) → email worker; the FinanceWorker also handles these event types on invoice-generate (finance.worker.ts:49-68) but no. event is routed to that queue by the map — that branch is effectively idle today (OQ-5).
  • Reminder + overdue are partially wired: repeatable cron jobs are registered (scheduler.service.ts:48-97) but OverdueScanJob.execute / FeeReminderJob.execute are not invoked anywhere, and no worker listens on payment-reminder (OQ-6).
  • Late fees KML: FeeStructure.lateFee exists (fee-structure.schema.ts:33-34) but nothing consumes it — no late-fee application in code (OQ-7).
  • Fees endpoints are guarded only by JwtAuthGuard (fees.controller.ts:24) — no @Permissions() metadata; only fees.collect exists in permissions.constants.ts:31 and default Accountant role has only ['fees.collect', 'student.read'] (role.schema.ts:42-48). Real RBAC is (planned) (OQ-9).

10. Open questions (module-grain; global ledger in 00-shared/12)

#ItemImpact
OQ-1Money is plain Number in schemas; blueprint says minor units — no rounding guaranteeDisplay/format strategy; currency decimals
OQ-2Overpayment allowed (no cap) — paidTotal >= total flips PAID; excess is never refunded into fees UIOverpay UX; catch-up ledger values
OQ-3GET /fees/dues returns only ISSUED/PARTIAL/OUVERDUE; no per-student dues endpoint; q and filtering of structured reports (planned); GET /fees/reports in Fees.md:31 removedReport UI can only sum locally
OQ-4Payments module is a separate payments_v2 stack keyed on transactionReference, linked via invoiceId — two parallel collectionsWhich data feeds a "payments history" screen
OQ-5Two sumByInvoice implementations — fees sums all statuses, payments sums only completed (fees/repositories/payment.repository.ts:23-35 vs payments/repositories/payment.repository.ts:25-37) — refunded/failed could skew paidAmountReconciliation display
OQ-6Overdue/reminder jobs registered but their .execute() is never called; no payment-reminder workerReminder + overdue status "(planned)"
OQ-7Late fees: field exists, behavior absent"Late fee" UI deferred
OQ-8Invoice generation is synchronous today; INVOICE_GENERATE queue + FinanceWorker exist but generation never enqueuesBulk generation later (planned)
OQ-9RBAC: no @Permissions on fees endpoints; only fees.collect (+ payments.*, receipts.read) are in ALL_PERMISSIONSRole-gated UI waits for perms
OQ-10Receipt print/share has no endpoint — receipt is a stored doc (receipt.schema.ts:9-40); print is client-side representationPrint/share receipt (proposed)

11. Glossary (this module)

TermMeaning
Fee structurefee_structures doc: per class+term, line-item items[], totalAmount, currency, dueDate, lateFee, isActive (fee-structure.schema.ts:8-38)
Invoiceinvoices doc: snapshot of structure total + discounts, per student, status draft/issued/partial/paid/overdue/cancelled (invoice.schema.ts:7-51)
Payment (fees)payments doc: amount against invoice, idempotencyKey unique, always completed today (payment.schema.ts:22-54)
Payment (payments module)payments_v2 doc: gateway capture, transactionReference, status incl. partially_refunded (payments/schema/payment.schema.ts:28-77)
Receiptreceipts doc with receiptNumber RCP-{ts}-{n} (payments.service.ts:180-198)
Duesoutstanding invoice: due = totalAmount − paidAmount (fees.service.ts:213-216)
Envelope{success, message, data, meta?, timestamp, requestId} (response-envelope.interceptor.ts:45-61)