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

12 — API Mapping (Fees Module)

Exact wire contract for every screen → endpoint. Base /api/v1; envelope per 00-shared/07. All endpoints from src/modules/fees/controllers/fees.controller.ts and src/modules/payments/payments.controller.ts; business rules from the services. Guards: @UseGuards(JwtAuthGuard) on both controllers (fees.controller.ts:24, payments.controller.ts:19) — no RBAC metadata (OQ-9). Tenant from JWT only; never in body (base.repository.ts:20-30).


0. Module-wide request envelope & client policy

AspectContract
Basehttps://api.<domain>/api/v1
HeadersAuthorization: Bearer <accessToken>; x-request-id; Content-Type: application/json
Response{success, message:"OK", data, meta?, timestamp, requestId} (response-envelope.interceptor.ts:45-61)
Error{success:false, message, error:{code, details?}, timestamp, requestId} (http-exception.filter.ts:73-81); codes VALIDATION_ERROR/UNAUTHENTICATED/PERMISSION_DENIED/RESOURCE_NOT_FOUND/DUPLICATE_RESOURCE/BUSINESS_RULE_VIOLATION/RATE_LIMITED/INTERNAL_SERVER_ERROR (:27-35)
Paginationpage (≥1), limit (1–100, default 20), sort (-field), q (pagination-query.dto.ts:5-30); meta = {page, limit, totalItems, totalPages, hasNext, hasPrevious} (:32-55)
Idempotencyfees payments: client idempotencyKey; payments-v2: server transactionReference
Cachingreads cached client-side (structure 24 h, dues 5 min); no cache on money-write responses
Offlinereads from cache; writes blocked (no fees offline queue)
Retrybackoff on 5xx/network; no auto-retry on 429 (api tier 100/min)

Screen: Fee structures list — GET /fees/structures

EndpointGET /api/v1/fees/structures (fees.controller.ts:34-38)
Querypage, limit, sort, q (accepted; q unused in service today — client filters)
Success200 data: [FeeStructureDoc…] + meta — skip/limit (page−1)*limit (fees.service.ts:62-71)
Errors400 invalid ints (pagination-query.dto.ts:8-19); 5xx

FeeStructureDoc (fee-structure.schema.ts:8-38, base.schema.ts:8-35): _id, tenantId, name, classId, academicYearId, items[{name,amount}], totalAmount, currency, dueDate, lateFee, isActive, createdAt, updatedAt, version, ...


Screen: Fee structure detail — GET /fees/structures/:id

Method
EndpointGET /api/v1/fees/structures/:id (fees.controller.ts:40-44)
Success200 data: FeeStructureDoc
Errors404 RESOURCE_NOT_FOUND "Fee structure not found." (fees.service.ts:76); 400 invalid id (http-exception.filter.ts:47-48 CastErrorVALIDATION_ERROR)

Screen: Create structure — POST /fees/structures

Method
EndpointPOST /api/v1/fees/structures (fees.controller.ts:28-32)
Body{name, classId, academicYearId, items:[{name,amount}], totalAmount, currency?, dueDate, lateFee?, isActive?} (create-fee-structure.dto.ts:26-68); dueDate ISO string → Date (fees.service.ts:45-47)
Success201 data: FeeStructureDoc (server defaults currency='XAF', lateFee=0, isActive=true)
Side effectemits FeeStructureCreated {feeStructureId, name, classId, academicYearId, totalAmount}in-app/fee-structure-created (fees.service.ts:49-56, event-queue-map.ts:40)
Errors400 VALIDATION_ERROR (decorators: IsMongoId, IsArray, IsNumber Min(0), IsDateString, …)

Screen: Edit structure — PATCH /fees/structures/:id

Method
EndpointPATCH /api/v1/fees/structures/:id (fees.controller.ts:46-50)
Bodyany subset of {name, items, totalAmount, currency, dueDate, lateFee, isActive} (update-fee-structure.dto.ts:15-54) — classId/academicYearId immutable
Success200 doc; $set + version+1 (fees.service.ts:85-89, base.repository.ts:57-66)
Errors404 (fees.service.ts:91); 400 invalid dueDate/items

Screen: Delete structure — DELETE /fees/structures/:id

Method
EndpointDELETE /api/v1/fees/structures/:id (fees.controller.ts:52-56)
Success200 {message:"OK"}soft delete (fees.service.ts:95-98, base.repository.ts:68-74); invoice preserved, no cascade
Errors404 (fees.service.ts:97)

Screen: Generate invoice — POST /fees/invoices/generate

Method
EndpointPOST /api/v1/fees/invoices/generate (fees.controller.ts:58-62)
Body{studentId, feeStructureId, academicYearId, discounts?:[{name,amount}]} (generate-invoice.dto.ts:24-42)
Success201 data: InvoiceDoctotalAmount = max(0, structure.totalAmount − Σ discounts), status:'issued', issuedAt:now, dueDate from structure (fees.service.ts:117-130)
Side effectInvoiceIssued {invoiceId, studentId, feeStructureId, totalAmount, dueDate}emails/send-invoice (event-queue-map.ts:41)
Errors404 "Fee structure not found." (fees.service.ts:105); 409 DUPLICATE_RESOURCE "Invoice already exists for this student and term." (fees.service.ts:112-114) + unique index {tenantId,studentId,feeStructureId,academicYearId} (invoice.schema.ts:58-60); 400
NoteGeneration is synchronous today (OQ-8)

InvoiceDoc shape (invoice.schema.ts:16-51): _id, studentId, feeStructureId, academicYearId, totalAmount, paidAmount, status (draft|issued|partial|paid|overdue| cancelled), dueDate, issuedAt?, discounts[], createdAt, updatedAt. Indexes {tenantId,status} (:57), {tenantId,studentId} (:55).

Screen: Student invoices — GET /fees/students/:studentId/invoices

Method
EndpointGET /api/v1/fees/students/:studentId/invoices (fees.controller.ts:64-68)
Success200 data: [InvoiceDoc…] non-paginated (no meta, fees.service.ts:142-144)
Errors400 invalid id; 5xx

Screen: Record payment (till) — POST /fees/invoices/:id/payments

Method
EndpointPOST /api/v1/fees/invoices/:id/payments (fees.controller.ts:70-74)
Body{invoiceId, amount, paymentMethod, reference?, idempotencyKey, paidAt, notes?} (record-payment.dto.ts:11-44); paidAt ISO → Date (fees.service.ts:164)
Success201 data: PaymentDoc(fees)status: 'completed' always (fees.service.ts:162-166); invoice paidAmount+status recomputed (fees.service.ts:168-176)
IdempotencyidempotencyKey unique (payment.schema.ts:40-41); replay returns the existing payment — no double-count (fees.service.ts:148-152)
Side effectPaymentCompleted {paymentId, invoiceId, amount, status}emails/send-receipt (event-queue-map.ts:42)
Errors404 "Invoice not found." (fees.service.ts:154); 409 "Invoice is already paid or cancelled." (fees.service.ts:155-160); 400 validation

PaymentDoc (fees) shape (fees/schemas/payment.schema.ts:22-55): _id, tenantId, invoiceId, amount, paymentMethod (cash|bank_transfer|mobile_money| cheque|other), reference?, idempotencyKey, paidAt, notes?, status('completed')…

Screen: Dues list — GET /fees/dues

Method
EndpointGET /api/v1/fees/dues (fees.controller.ts:76-80)
Querypage, limit (applied; sort/q not applied by the service) — service hard-filters status ∈ {issued,partial,overdue} (fees.service.ts:192-212)
Success200 data: [{invoice: InvoiceDoc, due: totalAmount−paidAmount}] (fees.service.ts:213-216); meta
Errors400 invalid ints; 5xx

Payments module (online) — shared screens (v2)

ScreenEndpoint
Payment capturePOST /api/v1/payments (body process-payment.dto.ts:5-54) → 201 {payment, receipt}; transactionReference server-generated (payments.service.ts:35-51)
RefundPOST /api/v1/payments/refund (body refund-payment.dto.ts:4-19); only completed, partial allowed (payments.service.ts:74-111)
ReconcilePATCH /api/v1/payments/:transactionRef/reconcile body {status} (payments.controller.ts:36-43); success→completed, failed→failed, else→pending (payments.service.ts:121-127)
Payment listGET /api/v1/payments?page&limit (defaults 1/20; :45-49)
Payment detailGET /api/v1/payments/:id (:51-55)
By invoiceGET /api/v1/payments/invoice/:invoiceId (:57-61)
Receipts listGET /api/v1/payments/receipts (:63-67)
Receipt detailGET /api/v1/payments/receipts/:id (:69-73)

PaymentDoc (v2) shape (payments/schemas/payment.schema.ts:28-77): transactionReference, amount, fee, refundedAmount, currency (default USD), gateway (stripe|paypal|flutterwave|paystack|razorpay|cash|bank_transfer|cheque), gatewayTransactionId?, gatewayResponse?, status (pending|processing|completed| failed|refunded|partially_refunded|cancelled), invoiceId?, invoiceType?, payerId?, payerEmail?, payerName?, description?, metadata?. ReceiptDoc shape (receipt.schema.ts:7-41): receiptNumber (RCP-{ts}-{n}), paymentId, invoiceId?, amount, fee, currency, paymentMethod, payerName?, payerEmail?, description?, issuedAt? (required).


Loading / streaming / realtime

ScreenLoadingStreamingRealtime
Dues / fees homeAppSkeleton(planned) WS invoice.updated (00-shared/07 §8)
Structures listskeletonre-fetch on focus
Structure detailskeleton
Student invoicesskeletonre-fetch on focus
Invoice detailinvoice + payments Future.waitinvoice.updated (planned)
Record paymentbutton spinnerupload (none)
Payments listskeleton
Receiptdoc render

Client-side error mapping (module)

ScreencodeUI
record payment409 DUPLICATE_RESOURCEinfo banner (paid/cancelled) or success (replay)
generate invoice409"already exists" banner + link
any list/detail404 RESOURCE_NOT_FOUNDAppEmptyState
create/update400 VALIDATION_ERRORfield errors
any401 → refresh → failsessionExpired
any429 RATE_LIMITEDcountdown, no retry
any5xxgeneric + requestId, retry

Optimistic / undo

  • No optimistic mutations anywhere in fees — payment/invoice/refund are all server-confirmed (00-shared/07 §9).
  • Undo: only in-form edits (line items, discounts). Delete = confirm dialog, no undo (soft delete only).

Notifications surface

InvoiceIssuedemails/send-invoice, PaymentCompletedemails/send-receipt (event-queue-map.ts:41-42); FeeStructureCreatedin-app/fee-structure-created (:40). Bridge dedups by correlationId + eventType (queue-bridge.service.ts:44-49,69). Reminder notifications (planned) — see fee-reminder.job.ts:16-46.