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

08 — Form Specifications (Fees Module)

Every form field-by-field. Validation mirrors class-validator decorators exactly from src/modules/fees/dto/*.ts; errors arrive as 400 VALIDATION_ERROR with error.details[].message (http-exception.filter.ts:27-35, 103-108). Client validates inline, submits, and lets server 400 shadow the client.


1. Create Fee Structure Form — POST /api/v1/fees/structures

(create-fee-structure.dto.ts:26-68)

#FieldLabelKeyboardValidation (server)Client UX
1nameNametext@IsString() required (:28-29)required; autofocus; TextInputAction.next
2classIdClass@IsMongoId() required (:32-33)AppDropdown (Academics classes); immutable after create
3academicYearIdAcademic year@IsMongoId() requiredAppDropdown; immutable after create
4items[]Line itemsrequired@IsArray() + @ValidateNested FeeItemDto (:39-43)LineItemsEditor — dynamic list of {name, amount}
4aitems[].nameItem nameyes@IsString() (fee-item dto :16-18)text; required
4bitems[].amountItem amountyes@IsNumber() @Min(0) (:20-23)MoneyField; ≥ 0; currency context
5totalAmountTotalrequired@IsNumber() @Min(0) (create-fee-structure.dto.ts:45-48)auto-sum hint Σ items; editable; flag mismatch (server won't)
6currencyCurrencyno@IsOptional() @IsString(); server default XAF (:50-53, fee-structure.schema.ts:27-28)AppDropdown default XAF (USD etc. future (proposed))
7dueDateDue daterequired@IsDateString() (:55-57), server converts to Date (fees.service.ts:46-47)AppDatePicker; client min = today
8lateFeeLate feeno@IsOptional() @IsNumber() @Min(0); default 0MoneyField optional; unused by backend today (OQ-7)
9isActiveActiveno@IsOptional() @IsBoolean(); default trueAppSwitch default on
  • Server sends dueDate as ISO string; store Date in schema (fee-structure.schema.ts:30-31, fees.service.ts:88).
  • SubmitPOST → 201 doc → list + snackbar. Errors: 400 → field errors; 5xx → keep form.

2. Edit Fee Structure Form — PATCH /api/v1/fees/structures/:id

(update-fee-structure.dto.ts:15-54)

#FieldRequiredServer validationClient UX
1nameno@IsOptional() @IsString()prefilled
2items[]no@IsOptional() + nestedprefilled; full replace on PATCH (fees.service.ts:86-89 — whole array swaps)
3totalAmountno@IsOptional() @IsNumber() @Min(0)prefilled
4currencyno@IsOptional() @IsString()prefilled
5dueDateno@IsOptional() @IsDateString()picker; past-due warned (server allows)
6lateFeeno@IsOptional() @IsNumber() @Min(0)prefilled
7isActiveno@IsOptional() @IsBoolean()prefilled switch
  • Immutable: classId, academicYearId are not in the DTO — disabled in edit form with note "Class and academic year cannot be changed after creation".
  • Submit → PATCH → 200 doc; 404 "Fee structure not found." (fees.service.ts:91).

3. Generate Invoice Form (dialog) — POST /api/v1/fees/invoices/generate

(generate-invoice.dto.ts:24-42)

#FieldLabelRequiredValidation (server)Client UX
1studentIdStudentrequired@IsMongoId() (:28-29)AppDropdown (Students module); searchable
2feeStructureIdFee structurerequired@IsMongoId() (:32-33)hidden — bound to the structure being viewed
3academicYearIdAcademic yearrequired@IsMongoId() (:36-37)from active academic year selector
4discounts[]Discountsno@IsOptional() @IsArray() + nested DiscountDto (:40-42)DiscountEditor (name + amount ≥ 0)
4adiscounts[].namenameyes (per item)@IsString() (:16-18)text
4bdiscounts[].amountamountyes (per item)@IsNumber() @Min(0) (:20-22)MoneyField ≥ 0
  • Side effects (server): totalAmount = max(0, structure.totalAmount − Σ disc) (fees.service.ts:117-119); status issued, issuedAt: now (fees.service.ts:122-130); InvoiceIssuedemails/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) → inline info banner, do not close dialog.
  • Preview: client shows totalAmount after discounts before submitting.

4. Record Payment Form (sheet) — POST /api/v1/fees/invoices/:id/payments

(record-payment.dto.ts:11-44)

#FieldLabelRequiredValidation (server)Client UX
1invoiceIdInvoicerequired@IsMongoId() (:14-15)hidden from route :id — never editable
2amountAmountrequired@IsNumber() @Min(0) (:17-19)MoneyField, prefilled = due; overpay warn allowed (OQ-2); shows currency
3paymentMethodMethodrequired@IsString() + enum list (:22-25) — not @IsEnum, invalid string is 400 validationPaymentMethodSelector; values cash, bank_transfer, mobile_money, cheque, other (payment.schema.ts:7-13)
4referenceReferenceno@IsOptional() @IsString()optional; placeholder "Bank ref / MoMo tx id"
5idempotencyKeyidempotency keyrequired@IsString() (:33-34); unique index (payment.schema.ts:40-41)hidden, uuid(); field reused on retry (critical: same key = replay, no double-post) — fees.service.ts:148-152`
6paidAtPaid onrequired@IsDateString() (:36-38), server → Date (fees.service.ts:164)AppDatePicker default today; no future-date cap server-side (2026)
7notesNotesno@IsOptional() @IsString()optional, maxLength 500 (client)
  • Success: 201 payment doc; server sets status: completed always (fees.service.ts:162-166) and recomputes invoice paidAmount + status (fees.service.ts:168-176).
  • 409: "Invoice is already paid or cancelled." (fees.service.ts:159) → close + info banner.
  • 409 replay: same key → returns original payment: treat as success.
  • Offline: blocked (write-op; no offline queue for fees).

5. Payments (v2) forms — POST /payments, POST /payments/refund

(process-payment.dto.ts:5-54, refund-payment.dto.ts:4-19)

Process (online):

FieldRequiredServer validationClient
amountyes@IsNumber() @Min(0)MoneyField
currencyno@IsOptional() @IsString(); default USD (:13-15)locked to tenant/display
gatewayyes@IsEnum(PaymentGateway)stripe,paypal,flutterwave,paystack,razorpay,cash,bank_transfer,cheque (payments/schemas/payment.schema.ts:7-14)embed/gateway SDK (forward-looking)
gatewayTransactionIdnooptional stringgateway response
invoiceIdnooptional stringfrom invoice detail
invoiceTypenooptional string
payerId/Email/Namenooptional stringspayer identity
descriptionnooptional stringline item text

Refund:

FieldRequiredValidationClient
paymentIdyes@IsString()hidden
amountno@IsOptional() @IsNumber() @Min(0); server caps refundedAmount + amount ≤ amount (payments.service.ts:81-86)default = remaining
reasonnooptional stringtext
  • Refund rules: only completed payments refundable (payments.service.ts:77-79); full → refunded, partial → partially_refunded (payments.service.ts:87-90).

Form-level rules (all)

  • Double-submit: disabled while pending; idempotency key guards rerun.
  • Optimistic: no optimistic writes — payment/invoice/structure writes are server-confirmed (00-shared/07 §9); only discount/line-item local editing and filters are local.
  • Undo: line-item removal inside a form = UNDO snackbar (4 s); structure delete = confirm dialog, no undo (soft-delete, no restore endpoint).
  • Abandonment: create/edit structure + record-payment keep drafts locally (proposed) (shared_preferences); back from dirty structure form → "Discard changes?" dialog; record-payment sheet back → "Payment not recorded — discard?" (key NOT reused after explicit discard).
  • Keyboard: number pad for amounts; .next sequence; Ctrl/Cmd+Enter submits (desktop).
  • Error copy: business 4xx from envelope message; codes for the rest (00-shared/07 §11). Never render raw server internals.

Client-side error priority

  1. 400 VALIDATION_ERROR → field errors (map details[].message by field hint, http-exception.filter.ts:103-108).
  2. 404 RESOURCE_NOT_FOUND → empty/back state.
  3. 409 DUPLICATE_RESOURCE → info banner (invoice exists / already paid / replay).
  4. 422 BUSINESS_RULE_VIOLATION → banner (currently unused by fees; reserved).
  5. 429 → countdown; 5xx → keep form + retry.