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).
Aspect Contract
Base https://api.<domain>/api/v1
Headers Authorization: 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)
Pagination page (≥1), limit (1–100, default 20), sort (-field), q (pagination-query.dto.ts:5-30); meta = {page, limit, totalItems, totalPages, hasNext, hasPrevious} (:32-55)
Idempotency fees payments: client idempotencyKey; payments-v2: server transactionReference
Caching reads cached client-side (structure 24 h, dues 5 min); no cache on money-write responses
Offline reads from cache; writes blocked (no fees offline queue)
Retry backoff on 5xx/network; no auto-retry on 429 (api tier 100/min)
Endpoint GET /api/v1/fees/structures (fees.controller.ts:34-38)
Query page, limit, sort, q (accepted; q unused in service today — client filters)
Success 200 data: [FeeStructureDoc…] + meta — skip/limit (page−1)*limit (fees.service.ts:62-71)
Errors 400 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, ...
Method
Endpoint GET /api/v1/fees/structures/:id (fees.controller.ts:40-44)
Success 200 data: FeeStructureDoc
Errors 404 RESOURCE_NOT_FOUND "Fee structure not found." (fees.service.ts:76); 400 invalid id (http-exception.filter.ts:47-48 CastError→VALIDATION_ERROR)
Method
Endpoint POST /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)
Success 201 data: FeeStructureDoc (server defaults currency='XAF', lateFee=0, isActive=true)
Side effect emits FeeStructureCreated {feeStructureId, name, classId, academicYearId, totalAmount} → in-app/fee-structure-created (fees.service.ts:49-56, event-queue-map.ts:40)
Errors 400 VALIDATION_ERROR (decorators: IsMongoId, IsArray, IsNumber Min(0), IsDateString, …)
Method
Endpoint PATCH /api/v1/fees/structures/:id (fees.controller.ts:46-50)
Body any subset of {name, items, totalAmount, currency, dueDate, lateFee, isActive} (update-fee-structure.dto.ts:15-54) — classId/academicYearId immutable
Success 200 doc; $set + version+1 (fees.service.ts:85-89, base.repository.ts:57-66)
Errors 404 (fees.service.ts:91); 400 invalid dueDate/items
Method
Endpoint DELETE /api/v1/fees/structures/:id (fees.controller.ts:52-56)
Success 200 {message:"OK"} — soft delete (fees.service.ts:95-98, base.repository.ts:68-74); invoice preserved, no cascade
Errors 404 (fees.service.ts:97)
Method
Endpoint POST /api/v1/fees/invoices/generate (fees.controller.ts:58-62)
Body {studentId, feeStructureId, academicYearId, discounts?:[{name,amount}]} (generate-invoice.dto.ts:24-42)
Success 201 data: InvoiceDoc — totalAmount = max(0, structure.totalAmount − Σ discounts), status:'issued', issuedAt:now, dueDate from structure (fees.service.ts:117-130)
Side effect InvoiceIssued {invoiceId, studentId, feeStructureId, totalAmount, dueDate} → emails/send-invoice (event-queue-map.ts:41)
Errors 404 "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
Note Generation 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).
Method
Endpoint GET /api/v1/fees/students/:studentId/invoices (fees.controller.ts:64-68)
Success 200 data: [InvoiceDoc…] non-paginated (no meta, fees.service.ts:142-144)
Errors 400 invalid id; 5xx
Method
Endpoint POST /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)
Success 201 data: PaymentDoc(fees) — status: 'completed' always (fees.service.ts:162-166); invoice paidAmount+status recomputed (fees.service.ts:168-176)
Idempotency idempotencyKey unique (payment.schema.ts:40-41); replay returns the existing payment — no double-count (fees.service.ts:148-152)
Side effect PaymentCompleted {paymentId, invoiceId, amount, status} → emails/send-receipt (event-queue-map.ts:42)
Errors 404 "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')…
Method
Endpoint GET /api/v1/fees/dues (fees.controller.ts:76-80)
Query page, limit (applied; sort/q not applied by the service) — service hard-filters status ∈ {issued,partial,overdue} (fees.service.ts:192-212)
Success 200 data: [{invoice: InvoiceDoc, due: totalAmount−paidAmount}] (fees.service.ts:213-216); meta
Errors 400 invalid ints; 5xx
Screen Endpoint
Payment capture POST /api/v1/payments (body process-payment.dto.ts:5-54) → 201 {payment, receipt}; transactionReference server-generated (payments.service.ts:35-51)
Refund POST /api/v1/payments/refund (body refund-payment.dto.ts:4-19); only completed, partial allowed (payments.service.ts:74-111)
Reconcile PATCH /api/v1/payments/:transactionRef/reconcile body {status} (payments.controller.ts:36-43); success→completed, failed→failed, else→pending (payments.service.ts:121-127)
Payment list GET /api/v1/payments?page&limit (defaults 1/20; :45-49)
Payment detail GET /api/v1/payments/:id (:51-55)
By invoice GET /api/v1/payments/invoice/:invoiceId (:57-61)
Receipts list GET /api/v1/payments/receipts (:63-67)
Receipt detail GET /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).
Screen Loading Streaming Realtime
Dues / fees home AppSkeleton— (planned) WS invoice.updated (00-shared/07 §8 )
Structures list skeleton — re-fetch on focus
Structure detail skeleton — —
Student invoices skeleton — re-fetch on focus
Invoice detail invoice + payments Future.wait — invoice.updated (planned)
Record payment button spinner upload (none) —
Payments list skeleton — —
Receipt doc render — —
Screen code UI
record payment 409 DUPLICATE_RESOURCE info banner (paid/cancelled) or success (replay)
generate invoice 409 "already exists" banner + link
any list/detail 404 RESOURCE_NOT_FOUND AppEmptyState
create/update 400 VALIDATION_ERROR field errors
any 401 → refresh → fail sessionExpired
any 429 RATE_LIMITED countdown, no retry
any 5xx generic + requestId, retry
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).
InvoiceIssued → emails/send-invoice, PaymentCompleted → emails/send-receipt
(event-queue-map.ts:41-42); FeeStructureCreated → in-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.