Fees API Reference
Complete API contracts for the fee management module, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Fees - API Reference
Audience: Frontend engineers, mobile engineers, backend engineers, QA, and the accounting integration's developers (for the read-only vocabulary the accounting API mirrors).
Scope: Admin (/api/fees/**) and mobile portal (/api/mobile/fees/**) routes owned by the fee management module. The accounting-facing /api/integration/fees/** surface is documented in the integration module's API doc; this page cross-references it where the same data appears.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | All 22 apps/api/src/modules/fees/**/*.controller.ts | Every route, method, guard, permission, decorator |
| DTOs | apps/api/src/modules/fees/**/dto/*.ts | Request/response shapes, validation |
| Schema | packages/db/src/schema/school/fees-*.ts | Enums, generated fields, constraints surfaced in responses |
| Error codes | apps/api/src/common/types/error-codes.ts | Every FEE_* code, plus ACCOUNTING_RECEIPT_UNAVAILABLE |
| Permission catalog | packages/db/src/authorization/permission-catalog.ts | Every fee permission module |
| Module composition | apps/api/src/modules/fees/fees.module.ts, apps/api/src/modules/mobile/mobile.module.ts | Route prefixes, ADMIN_MODULES vs. MOBILE_CHILDREN |
2. Module Summary
| Field | Value |
|---|---|
| Module name | Fees |
| Module slug | fees |
| Primary actors | Office/admin (permissioned), pupil, guardian, worker |
| API surfaces | admin, mobile (portal) |
| Base route prefixes | /api/fees/..., /api/mobile/fees/... |
| Auth model | JwtAuthGuard + RoleGuard + @Permissions("Fee*_ACTION") (admin); JwtAuthGuard + RoleGuard + MyFees_READ + audience assertion (portal) |
| Persistence | PostgreSQL (packages/db), BullMQ (QueueName.FEES) |
| Runtime source of truth | fee_obligations and its lines; fee_settings/fee_notification_settings |
| Sibling docs | Features/flows, Backend |
3. Concepts and Terminology
| Term | Meaning | Source File |
|---|---|---|
| Fee period | One Bikram Sambat month, owned by exactly one academic session | fees-catalog.ts |
| Fee structure | The base fee for a set of grades/sections from an effective date | fees-structures.ts |
| Structure version | An immutable snapshot of what a structure billed from a date; obligations reference the version, never the mutable structure | fees-structures.ts |
| Modifier | A scholarship or charge — one table, kind tells them apart | fees-modifiers.ts |
| Stint | One period of a modifier applying to one student (fee_modifier_assignments row) | fees-modifiers.ts |
| Obligation | One student's bill for one period | fees-obligations.ts |
| Line | One component/scholarship/charge/adjustment/waiver entry inside an obligation | fees-obligations.ts |
| Generation run | A bulk operation (generate, recalculate, apply/end a modifier, revise a structure) with one item per (student, period) | fees-runs.ts |
| Evaluation date (E) | The date placement/eligibility is decided against — the period's start, or a mid-period join date | fee-resolution.service.ts |
| Charge-once slot | The scope (session/term/all-time) an annual/term/one_time component or a once_per_session modifier bills in at most once | fee-charge-once.util.ts |
| Basis points (bp) | 1 bp = 0.01%; 10000 bp = 100% | fees-contract.ts |
| Paisa | 1/100 of a rupee; every stored amount is an integer number of paisa | fees-catalog.ts |
4. API Surface Map
Catalogue
| Method | Path | Permission | Controller |
|---|---|---|---|
GET | /api/fees/calendar/bs-months | FeeDashboard_READ | FeeCalendarController |
GET | /api/fees/calendar/today | FeeDashboard_READ | FeeCalendarController |
GET | /api/fees/categories | FeeCategories_READ | FeeCategoriesController |
POST | /api/fees/categories | FeeCategories_CREATE | FeeCategoriesController |
PATCH | /api/fees/categories/:id | FeeCategories_UPDATE | FeeCategoriesController |
POST | /api/fees/categories/:id/status | FeeCategories_UPDATE | FeeCategoriesController |
DELETE | /api/fees/categories/:id | FeeCategories_DELETE | FeeCategoriesController |
GET | /api/fees/components | FeeComponents_READ | FeeComponentsController |
GET | /api/fees/components/:id | FeeComponents_READ | FeeComponentsController |
POST | /api/fees/components | FeeComponents_CREATE | FeeComponentsController |
PATCH | /api/fees/components/:id | FeeComponents_UPDATE | FeeComponentsController |
POST | /api/fees/components/:id/status | FeeComponents_UPDATE | FeeComponentsController |
DELETE | /api/fees/components/:id | FeeComponents_DELETE | FeeComponentsController |
GET | /api/fees/periods | FeeSettings_READ | FeePeriodsController |
POST | /api/fees/periods/prepare | FeeSettings_UPDATE | FeePeriodsController |
PATCH | /api/fees/periods/:id | FeeSettings_UPDATE | FeePeriodsController |
GET | /api/fees/terms | FeeSettings_READ | FeeTermsController |
POST | /api/fees/terms | FeeSettings_UPDATE | FeeTermsController |
PATCH | /api/fees/terms/:id | FeeSettings_UPDATE | FeeTermsController |
DELETE | /api/fees/terms/:id | FeeSettings_UPDATE | FeeTermsController |
Structures
| Method | Path | Permission | Controller |
|---|---|---|---|
GET | /api/fees/structures | FeeStructures_READ | FeeStructuresController |
POST | /api/fees/structures | FeeStructures_CREATE | FeeStructuresController |
GET | /api/fees/structures/:id | FeeStructures_READ | FeeStructuresController |
PATCH | /api/fees/structures/:id | FeeStructures_UPDATE | FeeStructuresController |
PUT | /api/fees/structures/:id/targets | FeeStructures_UPDATE | FeeStructuresController |
PUT | /api/fees/structures/:id/lines | FeeStructures_UPDATE | FeeStructuresController |
POST | /api/fees/structures/:id/copy | FeeStructures_CREATE | FeeStructuresController |
DELETE | /api/fees/structures/:id | FeeStructures_DELETE | FeeStructuresController |
GET | /api/fees/structures/:id/conflicts | FeeStructures_READ | FeeStructuresController |
GET | /api/fees/structures/:id/versions/:versionId | FeeStructures_READ | FeeStructuresController |
POST | /api/fees/structures/:id/activate | FeeStructureActivation_UPDATE | FeeStructureLifecycleController |
POST | /api/fees/structures/:id/deactivate | FeeStructureActivation_UPDATE | FeeStructureLifecycleController |
POST | /api/fees/structures/:id/reactivate | FeeStructureActivation_UPDATE | FeeStructureLifecycleController |
POST | /api/fees/structures/:id/archive | FeeStructureActivation_UPDATE | FeeStructureLifecycleController |
POST | /api/fees/structures/:id/revisions/preview | FeeStructureActivation_UPDATE | FeeStructureLifecycleController |
POST | /api/fees/structures/:id/revisions | FeeStructureActivation_UPDATE | FeeStructureLifecycleController |
Scholarships and Charges
Identical 13-route shape under /api/fees/scholarships (FeeScholarships_*, FeeScholarshipAssignments_*) and /api/fees/charges (FeeCharges_*, FeeChargeAssignments_*):
| Method | Path | Permission (scholarships / charges) |
|---|---|---|
GET | .../{scholarships,charges} | FeeScholarships_READ / FeeCharges_READ |
POST | .../{scholarships,charges} | FeeScholarships_CREATE / FeeCharges_CREATE |
GET | .../{scholarships,charges}/:id | FeeScholarships_READ / FeeCharges_READ |
PATCH | .../{scholarships,charges}/:id | FeeScholarships_UPDATE / FeeCharges_UPDATE |
DELETE | .../{scholarships,charges}/:id | FeeScholarships_DELETE / FeeCharges_DELETE |
POST | .../{scholarships,charges}/:id/activate | *_UPDATE |
POST | .../{scholarships,charges}/:id/end | *_UPDATE |
POST | .../{scholarships,charges}/:id/archive | *_UPDATE |
POST | .../{scholarships,charges}/:id/eligibility-preview | FeeScholarshipAssignments_CREATE / FeeChargeAssignments_CREATE |
POST | .../{scholarships,charges}/:id/apply | FeeScholarshipAssignments_CREATE / FeeChargeAssignments_CREATE |
GET | .../{scholarships,charges}/:id/assignments | FeeScholarshipAssignments_READ / FeeChargeAssignments_READ |
POST | .../{scholarships,charges}/:id/assignments | FeeScholarshipAssignments_CREATE / FeeChargeAssignments_CREATE |
POST | .../{scholarships,charges}/:id/assignments/:assignmentId/end | FeeScholarshipAssignments_UPDATE / FeeChargeAssignments_UPDATE |
Obligations
| Method | Path | Permission | Controller |
|---|---|---|---|
GET | /api/fees/student-fees | StudentFees_READ | FeeStudentFeesController |
GET | /api/fees/student-fees/:id | StudentFees_READ | FeeStudentFeesController |
GET | /api/fees/student-fees/:id/history | StudentFees_READ | FeeStudentFeesController |
GET | /api/fees/students/:studentId/summary | StudentFees_READ | FeeStudentSummaryController |
POST | /api/fees/student-fees/:id/adjustments | StudentFeeAdjustments_CREATE | FeeStudentFeeActionsController |
POST | /api/fees/student-fees/:id/waivers | StudentFeeAdjustments_CREATE | FeeStudentFeeActionsController |
POST | /api/fees/student-fees/:id/adjustments/:lineId/reverse | StudentFeeAdjustments_UPDATE | FeeStudentFeeActionsController |
POST | /api/fees/student-fees/:id/finalize | StudentFeeAdjustments_UPDATE | FeeStudentFeeActionsController |
POST | /api/fees/student-fees/:id/cancel | StudentFeeCancellation_UPDATE | FeeStudentFeeActionsController |
POST | /api/fees/student-fees/:id/reissue | StudentFeeCancellation_UPDATE | FeeStudentFeeActionsController |
Runs
| Method | Path | Permission | Controller |
|---|---|---|---|
GET | /api/fees/generation-runs | StudentFees_READ | FeeRunsController |
POST | /api/fees/generation-runs | StudentFees_CREATE | FeeRunsController |
GET | /api/fees/generation-runs/:id | StudentFees_READ | FeeRunsController |
GET | /api/fees/generation-runs/:id/items | StudentFees_READ | FeeRunsController |
POST | /api/fees/generation-runs/:id/retry | StudentFees_CREATE | FeeRunsController |
POST | /api/fees/generation-runs/:id/cancel | StudentFees_CREATE | FeeRunsController |
Payments, mappings, sync
| Method | Path | Permission | Controller |
|---|---|---|---|
POST | /api/fees/student-fees/:id/payments | FeePayments_CREATE | FeePaymentsController |
POST | /api/fees/student-fees/:id/payments/:entryId/reverse | FeePaymentCorrections_UPDATE | FeePaymentsController |
POST | /api/fees/student-fees/:id/payment-correction | FeePaymentCorrections_UPDATE | FeePaymentsController |
GET | /api/fees/payments | FeePayments_READ | FeePaymentsController |
GET | /api/fees/payments/:paymentId/receipt | FeeReceipts_READ | FeePaymentsController |
GET | /api/fees/student-mappings | FeeMappings_READ | FeeMappingsController |
POST | /api/fees/student-mappings | FeeMappings_CREATE | FeeMappingsController |
POST | /api/fees/student-mappings/:id/retire | FeeMappings_UPDATE | FeeMappingsController |
GET | /api/fees/invoice-mappings | FeeMappings_READ | FeeMappingsController |
POST | /api/fees/invoice-mappings | FeeMappings_CREATE | FeeMappingsController |
GET | /api/fees/sync-events | FeeSync_READ | FeeSyncEventsController |
GET | /api/fees/sync-events/:id | FeeSync_READ | FeeSyncEventsController |
POST | /api/fees/sync-events/:id/retry | FeeSyncResolution_UPDATE | FeeSyncEventsController |
POST | /api/fees/sync-events/:id/resolve | FeeSyncResolution_UPDATE | FeeSyncEventsController |
FeeMappingsController and FeeSyncEventsController both carry @RequiresAccountingIntegration() at
the class level, so every route above from student-mappings through sync-events/:id/resolve — reads
included — answers 404 ACCOUNTING_INTEGRATION_DISABLED while the school's ACCOUNTING_INTEGRATION_ENABLED
switch is off, checked after JwtAuthGuard/RoleGuard so an unauthenticated or unauthorised caller still
gets 401/403 first. See the integration module's docs for the switch itself.
Reports and settings
| Method | Path | Permission | Controller |
|---|---|---|---|
GET | /api/fees/change-log | FeeDashboard_READ | FeeChangeLogController |
GET | /api/fees/dashboard | FeeDashboard_READ | FeeDashboardController |
GET | /api/fees/exports/student-fees | FeeExports_READ | FeeExportsController |
GET | /api/fees/settings | FeeSettings_READ | FeeSettingsController |
PATCH | /api/fees/settings | FeeSettings_UPDATE | FeeSettingsController |
GET | /api/fees/settings/financial | FeeFinancialSettings_READ | FeeSettingsController |
PATCH | /api/fees/settings/financial | FeeFinancialSettings_UPDATE | FeeSettingsController |
GET | /api/fees/notification-settings | FeeSettings_READ | FeeNotificationSettingsController |
PATCH | /api/fees/notification-settings/:eventKey | FeeSettings_UPDATE | FeeNotificationSettingsController |
Mobile portal (/api/mobile/fees, MOBILE_CHILDREN only — never in ADMIN_MODULES)
| Method | Path | Permission | Audience Assertion | Controller |
|---|---|---|---|---|
GET | /api/mobile/fees/me | MyFees_READ | student | FeeStudentPortalController |
GET | /api/mobile/fees/me/:obligationId | MyFees_READ | student | FeeStudentPortalController |
GET | /api/mobile/fees/children | MyFees_READ | guardian | FeeGuardianPortalController |
GET | /api/mobile/fees/children/:studentId | MyFees_READ | guardian | FeeGuardianPortalController |
GET | /api/mobile/fees/children/:studentId/:obligationId | MyFees_READ | guardian | FeeGuardianPortalController |
GET | /api/mobile/fees/me/payments/:paymentId/receipt | MyFees_READ | student | FeeStudentPortalController |
GET | /api/mobile/fees/children/:studentId/payments/:paymentId/receipt | MyFees_READ | guardian | FeeGuardianPortalController |
Every route above resolves at /api/... — no version segment (URI versioning is enabled but no controller declares one). Every controller declares @Controller("fees") or @Controller("fees/...") under FeesModule, or @Controller("fees") under FeePortalModule mounted at /api/mobile.
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Notes |
|---|---|---|---|---|
| Admin | JwtAuthGuard, RoleGuard | req.user, activeRole | Exact Fee*_ACTION code (see §4) | No fee code is auto-granted to staff/teacher; money/disclosure codes are superadmin-only and must be granted to a named role |
| Portal (pupil) | JwtAuthGuard, RoleGuard | req.user, activeRole.scopeKind = "student" | MyFees_READ | ALSO asserts assertPortalAudience(actor, "student") in the service — belt and braces |
| Portal (guardian) | JwtAuthGuard, RoleGuard | req.user, activeRole.scopeKind = "guardian" | MyFees_READ | Same, plus the child is resolved against the guardian's own links before any row is read |
6. DTO and Model Reference
6.1 FeeStudentFeeSummaryDto / FeeStudentFeeDto (obligation)
| Field | Type | Notes |
|---|---|---|
id | string (public id) | — |
student | {id, name} | — |
academicSession | {id, name} | — |
period | {id, bsYear, bsMonth} | — |
structure | {id, name, versionNo} | The VERSION that priced it |
lifecycle | FeeObligationLifecycle | open | finalized | cancelled |
paymentStatus | FeePaymentStatus | GENERATED, mirrors the DB column |
grossPaisa, scholarshipPaisa, chargePaisa, adjustmentPaisa, waiverPaisa, netPaisa, paidPaisa, balancePaisa | number | Integer paisa |
dueDate | FeeDateDto | null | {date, bs} — both calendars |
version | number | Optimistic concurrency token every write must supply |
lines (detail only) | FeeObligationLineDto[] | Every LIVE line, with origin, label, amountPaisa, reversed, createdBy |
payments (detail only) | FeePaymentEntryDto[] | Every payment/reversal/correction entry; each carries receiptAvailable (§6.7) |
6.2 CreateAdjustmentDto / CreateWaiverDto
| Field | Type | Required | Validation |
|---|---|---|---|
amountPaisa | number | Yes | Non-zero integer; a waiver is stored negated internally |
label | string | adjustments only | — |
reason | string | Yes | 3–1000 chars |
version | number | Yes | The version last read |
6.3 CreateModifierDto (scholarship/charge)
| Field | Type | Required | Validation |
|---|---|---|---|
academicSessionId, categoryId | string (uuid) | Yes | — |
name | string | Yes | ≤150 chars |
valueType | "flat" | "percent" | Yes | Shape-checked against amountPaisa/percentBp |
amountPaisa | number | flat only | 1–10,000,000,000 |
percentBp | number | percent only | 1–10000 |
baseScope | "gross" | "category" | "component" | Yes | Determines required baseCategoryId/baseComponentId |
recurrence | "per_period" | "once_per_session" | Yes | — |
eligibilityMode | "rule" | "manual" | Yes | rule requires eligibilityRules; manual forbids it |
eligibilityRules | EligibilityRulesDto | rule mode | gradeIds, sectionIds, ethnicityIds, genders, disabilityTypes, transportModes, enrollmentStatuses — every key AND-ed, each array OR-ed |
autoApplyNewMatches | boolean | rule mode only | — |
stackGroup | string | scholarships only | 1–40 chars, lower-cased |
effectiveFromBs | string | Yes | Must be a fee period's start date |
6.4 AssignStudentsDto
| Field | Type | Notes |
|---|---|---|
studentIds | string[] (uuid) | Manual assignment target |
effectiveFromBs | string | undefined | Defaults to the modifier's own effectiveFrom |
6.5 ListStudentFeesQueryDto
Extends QueryDto. Sort whitelist: dueDate, netPaisa, balancePaisa, studentName, period, updatedAt. Filters: academicSessionId, periodId, studentId, lifecycle, paymentStatus, gradeId, sectionId, search (name/student code), overdue (boolean). pagination=false is REFUSED (PAGINATION_LIMIT_INVALID) — a roll of every child's fee is never returned unpaginated.
6.6 FeePortalFeeDto / FeePortalFeeListDto (portal)
Same shape as the admin summary DTO with everything internal removed: no reason, no createdBy, no actor identity, no manual-line detail beyond amount and label.
FeePortalFeeSummaryDto (each item of fees):
| Field | Type | Notes |
|---|---|---|
id | uuid | The fee's public id |
academicSessionId | uuid | Public id of the fee's academic session. The portal builds its session selector from these values, because pupils and guardians cannot read the admin sessions list |
academicSessionName | string | |
period | FeePortalPeriodDto | label, bsYear, bsMonth, startDate, endDate |
gradeName, sectionName | string | Placement the fee was calculated for |
dueDate | FeeDateDto | null | |
overdue | boolean | |
netPaisa, paidPaisa, balancePaisa | integer paisa | |
paymentStatus | FeePaymentStatus | |
lifecycle | open | finalized | Cancelled fees are never shown in the portal |
lastPaymentAt | ISO instant | null |
FeePortalFeeListDto is { totals: { netPaisa, paidPaisa, balancePaisa, overdueBalancePaisa }, fees: FeePortalFeeSummaryDto[] }. FeePortalFeeDto adds grossPaisa, scholarshipPaisa, chargePaisa, adjustmentPaisa, waiverPaisa, lines and payments.
6.7 FeePaymentEntryDto / FeePortalPaymentDto (payment entries)
FeePaymentEntryDto (admin: the payments of GET /api/fees/student-fees/:id, each item of GET /api/fees/payments, and the entry of record/reverse/correct results):
| Field | Type | Notes |
|---|---|---|
id | uuid | The entry's public id; the receipt route addresses it |
kind | payment | reversal | correction | — |
source | manual | external | external is money the accounting application reported |
amountPaisa | integer paisa | Signed: a reversal or downward correction is negative |
paidOn | FeeDateDto | null | — |
reference, note, reason, reversesEntryId | string | null | — |
createdAt | ISO instant | — |
recordedBy | {id, name} | null | — |
receiptAvailable | boolean | true only when the accounting application holds a receipt for the entry and the receipt route will try to serve it (§8.6). Always false for an entry the office recorded, a correction, a reversal, a cancelled fee, an entry whose accounting transaction is not applied or has no applied money, one from another integration client, one whose reference is not a uuid, one whose sync events matched a different student, and while receipt download is switched off. Computed from the school's own records without calling accounting |
FeePaymentListItemDto adds studentFeeId, student, period, academicSessionId and externalTransactionRef.
FeePortalPaymentDto (the payments of a portal fee detail): id (the entry's public id), paidOn, amountPaisa, kind, sourceLabel (School office | Accounting system), reference (office entries only; always null for an external entry) and receiptAvailable (as above, for this caller: it is true only for an entry the receipt route would serve to the same pupil or guardian).
7. Enum Reference
| Enum | Values | Source |
|---|---|---|
FeeCategoryKind | fee, scholarship, charge | fees-contract.ts |
FeeCategoryStatus | active, inactive, archived | — |
FeeValueType | flat, percent | — |
FeeFrequency | monthly, one_time, annual, term | — |
FeeStructureStatus | draft, active, inactive, archived | — |
FeeModifierKind | scholarship, charge | — |
FeeModifierStatus | draft, active, ended, archived | — |
FeeBaseScope | gross, category, component | — |
FeeRecurrence | per_period, once_per_session | — |
FeeEligibilityMode | rule, manual | — |
FeeAssignmentSource | manual, rule | — |
FeeObligationLifecycle | open, finalized, cancelled | — |
FeePaymentStatus | unpaid, partially_paid, paid, overpaid, waived, nothing_due, cancelled | GENERATED column |
FeeLineType | component, scholarship, charge, adjustment, waiver | — |
FeeLineOrigin | system, manual | — |
FeeRoundingMode | half_up, half_even, down, up | — |
FeeRoundingUnit | 1, 100 (paisa) | — |
FeeRunKind | generate, recalculate_open, modifier_apply, modifier_end, structure_revision, auto, placement_review | — |
FeeRunStatus | queued, planning, processing, completed, completed_with_exceptions, failed, cancelled | — |
FeeRunItemStatus | pending, succeeded, skipped, failed, cancelled | — |
FeeRunItemOutcome | created, recalculated, unchanged, already_exists, cancelled_exists, finalized_protected, not_applicable, cancelled_no_placement | — |
FeeScholarshipStacking | stack, highest_only | Global setting |
FeePaymentAuthority | external, manual_review | Global setting |
FeeSectionChangePolicy | keep, recalculate_open | Global setting |
FeeMidPeriodJoinPolicy | charge_period, next_period | Global setting |
FeeWithdrawalPolicy | cancel_unpaid_open, flag | Global setting |
FeePaymentEntryKind | payment, reversal, correction | — |
FeePaymentSource | manual, external | — |
FeeExternalTransactionState | applied, held_unmatched, held_conflict, rejected | — |
FeeSyncStatus | pending, succeeded, failed, duplicate, rejected, conflict, unmatched | — |
FeeMappingStatus | active, retired | — |
FeeMappingOrigin | api, admin, intake | — |
FeeObligationEventType | generated, recalculated, adjusted, waived, adjustment_reversed, cancelled, reissued, finalized, payment_recorded, payment_reversed, payment_corrected, payment_synced, due_date_changed, slot_unbilled | — |
FeeNotificationEventKey (26 values) | fee_generated, fee_updated, fee_structure_changed, scholarship_applied, scholarship_changed, scholarship_ended, scholarship_review_required, charge_applied, charge_changed, charge_ended, payment_received, payment_partial, payment_full, payment_status_changed, payment_corrected, fee_adjusted, fee_cancelled, reminder_before_due, reminder_due, reminder_overdue, sync_failed, payment_unmatched, sync_conflict, bulk_completed, bulk_exception, reconciliation_completed | — |
FeeNotificationAudience | student, guardian, admin | — |
FeeNotificationChannel | in_app, push, email, sms | — |
FeeSweepPass | finalize_started_periods, auto_generate, reminders, rule_new_matches | Plus watchdog/sync_rematch (not ledger-checkpointed) |
8. Endpoint Reference (representative)
8.1 POST /api/fees/generation-runs
Purpose
Launches a bulk fee generation over a scope (one or more periods, optionally filtered by grade/section/student). Returns immediately with a queued run; the actual per-student work happens in the background via FeeJob.GENERATION_PLAN/GENERATION_CHUNK.
Auth and Permissions
- Auth: JWT
- Guard chain:
JwtAuthGuard,RoleGuard - Permission:
StudentFees_CREATE - Idempotency:
requestKey— a repeated request with the same key returns the original run
Request
{
"kind": "generate",
"academicSessionId": "0199a3b2-...",
"periodIds": ["0199a3b2-..."],
"gradeId": null,
"sectionId": null,
"studentIds": null,
"requestKey": "generate-2083-07"
}Response
{
"message": "Generation run queued.",
"data": {
"id": "0199a3b2-...",
"kind": "generate",
"status": "queued",
"totalItems": 0,
"succeededItems": 0,
"skippedItems": 0,
"failedItems": 0
}
}Side Effects
Database write (fee_generation_runs), outbox enqueue (GENERATION_PLAN), and eventually every item's obligation write, change-feed events, and family/admin notifications.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
409 | FEE_SESSION_LOCKED | An activation/revision holds the exclusive session lock |
409 | FEE_RUN_SCOPE_EMPTY | The scope resolves to no targets up front |
409 | FEE_RUN_TOO_LARGE | The scope exceeds the configured item ceiling |
8.2 POST /api/fees/student-fees/:id/adjustments
Purpose
Records a manual adjustment (any non-zero signed amount) on an obligation — used for corrections the automatic calculation cannot express.
Request
{ "amountPaisa": -5000, "label": "Sibling discount", "reason": "Second child in the family, per office policy", "version": 3 }Response
The full FeeStudentFeeDto, recalculated totals included.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
409 | FEE_VERSION_CONFLICT | version no longer matches |
400 | FEE_ADJUSTMENT_INVALID | amountPaisa === 0 |
409 | FEE_NET_NEGATIVE | The resulting net would be below zero |
404 | FEE_OBLIGATION_NOT_FOUND | Unknown or not visible to the caller |
8.3 POST /api/fees/student-fees/:id/cancel
Purpose
Cancels an obligation nothing has been paid on, releasing its charge-once lines and freeing the (student, period) slot for a later reissue.
Request
{ "reason": "Withdrawn mid-term, no payments recorded", "version": 2 }Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
409 | FEE_OBLIGATION_HAS_PAYMENTS | Any payment entry exists |
409 | FEE_OBLIGATION_NOT_OPEN | Already cancelled (idempotency is NOT implied — a second cancel is refused, not a no-op) |
400 | Validation | reason shorter than 3 characters |
8.4 POST /api/fees/{scholarships,charges}/:id/apply
Purpose
Applies a rule-mode modifier to the population a prior eligibility-preview call showed, after office confirmation.
Request
{ "confirm": true, "expectedCount": 42, "version": 1 }Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
400 | FEE_CONFIRMATION_REQUIRED | confirm !== true |
409 | FEE_PREVIEW_STALE | The matched population changed since the preview (expectedCount mismatch) |
409 | FEE_MODIFIER_STATUS_TRANSITION_INVALID | The modifier is not active or not rule-mode |
8.5 GET /api/mobile/fees/me
Purpose
The pupil's own fee list — the family-facing surface with every internal field removed.
Auth and Permissions
- Guard chain:
JwtAuthGuard,RoleGuard - Permission:
MyFees_READ - Additional:
assertPortalAudience(actor, "student")inside the service
Response
{
"message": "Fees fetched.",
"data": {
"items": [
{ "id": "0199a3b2-...", "period": { "bsYear": 2083, "bsMonth": 7 }, "netPaisa": 580000, "paidPaisa": 300000, "balancePaisa": 280000, "paymentStatus": "partially_paid", "dueDate": { "date": "2026-11-10", "bs": "2083-07-24" } }
]
}
}8.6 Payment receipts
| Method | Path | Audience | Permission | Scoping |
|---|---|---|---|---|
GET | /api/fees/payments/:paymentId/receipt | Office | FeeReceipts_READ | Any payment entry that has a receipt |
GET | /api/mobile/fees/children/:studentId/payments/:paymentId/receipt | Guardian | MyFees_READ + assertPortalAudience(actor, "guardian") | The entry belongs to a live obligation of that child, and the child is one of the caller's own children (graduate access included) |
GET | /api/mobile/fees/me/payments/:paymentId/receipt | Pupil | MyFees_READ + assertPortalAudience(actor, "student") | The entry belongs to the caller's own student row |
Purpose
Streams the receipt PDF the accounting application issued for a payment it reported. The school keeps no copy: each request fetches the PDF from accounting and relays the bytes.
Request
paymentId (and studentId) are uuids; a non-uuid is a 400 from ParseUUIDPipe. No body, no query. Each route is rate-limited per account (RATE_LIMITS.FEE_RECEIPT_DOWNLOAD) and per address.
Response
200 with the PDF bytes (not the ResponseDto envelope) and headers the school builds itself:
| Header | Value |
|---|---|
Content-Type | application/pdf |
Content-Length | The byte length |
Cache-Control | no-store |
X-Content-Type-Options | nosniff |
Content-Disposition | attachment; filename="receipt-<paidOn>-<first 8 characters of the entry id>.pdf" (plus the RFC 5987 filename*); receipt-<first 8 characters>.pdf when the entry has no date |
No header the accounting application sent is forwarded.
When a receipt exists
An entry has a receipt when all of these hold, by the same rule that sets receiptAvailable: it is a payment entry from the accounting application (source = external); its transaction is applied, has applied money and belongs to the integration client named by ACCOUNTING_INTEGRATION_CLIENT_ID; the transaction's external_ref is a uuid (the accounting receipt id); its fee is not cancelled; and no sync event of the transaction matched a student other than the fee's own. Several payment entries of one transaction share one receipt.
Receipt download is active only when ACCOUNTING_INTEGRATION_ENABLED is true and ACCOUNTING_API_URL, ACCOUNTING_API_TOKEN and ACCOUNTING_INTEGRATION_CLIENT_ID are all set. Otherwise every receipt route answers 404 FEE_RECEIPT_NOT_AVAILABLE and receiptAvailable is false everywhere.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
403 | PERMISSION_INSUFFICIENT | A portal route called by the wrong audience |
404 | FEE_RECEIPT_NOT_AVAILABLE | Receipt download is off; the entry has no receipt (see above); the entry is not the caller's; or accounting answered that the receipt does not exist. All of these look the same to the caller |
503 | ACCOUNTING_RECEIPT_UNAVAILABLE | Accounting is unreachable or slow (15 s), answered 5xx, 401, a redirect or any status other than 200/404, answered 404 for a reason other than a missing receipt (its internal API switched off, or a 404 from something else), returned a media type other than application/pdf, a body without the %PDF- magic, or a body over 5 MB; or four downloads are already in flight in this process |
A client should not request a receipt when receiptAvailable is false, and may retry a 503 later.
9. Flow Diagrams
9.1 Route Ownership
9.2 Error Decision Tree (a versioned write)
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination | Default Size | Max Size | Sort Fields | pagination=false |
|---|---|---|---|---|---|
/api/fees/student-fees | page/size | 20 | 100 | dueDate, netPaisa, balancePaisa, studentName, period, updatedAt | Refused — PAGINATION_LIMIT_INVALID |
/api/fees/categories, /components | page/size | 20 | 100 | name, sortOrder, updatedAt, createdAt (category default sortOrder asc) | Allowed, bounded by UNPAGINATED_HARD_CAP (1000) |
/api/fees/generation-runs | page/size | 20 | 100 | createdAt (default desc) | Allowed |
/api/fees/payments, /sync-events | page/size | 20 | 100 | createdAt/receivedAt | Allowed |
/api/mobile/fees/me, /children | none (a session's own fee count is always small) | — | — | — | N/A |
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details |
|---|---|---|
| Redis cache | No | Every fee read goes to PostgreSQL directly, indexed for it |
| BullMQ | Yes | QueueName.FEES — generation_plan, generation_chunk, payment_sync, change_fanout, sweep (see the backend doc §9) |
| External API | Indirect | The integration module's accounting API reads these same tables read-only and writes payments/mappings through its own routes — see that module's docs |
| Accounting receipts | Optional | GET {ACCOUNTING_API_URL}/internal/receipts/{receiptId}/pdf?studentId=<students.id> with Authorization: Bearer <ACCOUNTING_API_TOKEN>, redirect: manual, a 15 s timeout, a 5 MB body cap and at most four requests in flight per process (§8.6). studentId is the student bound to the payment's fee; accounting answers 404 unless the receipt's invoice belongs to that student |
12. Permission Reference
The fee permission modules (packages/db/src/authorization/permission-catalog.ts). None is part of the automatic admin grant except the general catalogue/structure/modifier/obligation/payment/sync/settings read-and-operate set; six are SUPERADMIN_ONLY_MODULES and must be granted to a named role:
| Module | Superadmin-only? | Covers |
|---|---|---|
FeeDashboard | No | Dashboard, change log, BS calendar |
FeeCategories, FeeComponents | No | Catalogue |
FeeStructures | No | Draft structures: create, edit, copy, targets, lines |
FeeStructureActivation | No | Activate, deactivate, reactivate, archive, revise |
FeeScholarships, FeeCharges | No | The modifier records themselves |
FeeScholarshipAssignments, FeeChargeAssignments | No | Preview, apply, assign, end stints |
StudentFees | No | Read obligations; create generation runs |
StudentFeeAdjustments | No | Adjustments, waivers, reversals, finalization |
StudentFeeCancellation | No | Cancel and reissue |
FeePayments | No | Read payment info; record a manual payment |
FeeReceipts | No | Download the receipt PDF of a payment the accounting application reported; only READ is used, and permissions:sync grants it to every role that holds FeePayments_READ |
FeePaymentCorrections | Yes | Reverse or correct a manual payment |
FeeSync | No | Read the accounting sync inbox |
FeeSyncResolution | Yes | Resolve and retry held sync events |
FeeMappings | Yes | Student and invoice mappings to the accounting system |
FeeSettings | No | Calendar, periods, terms, policies, reminders, notification matrix |
FeeFinancialSettings | Yes | Rounding, stacking, scholarship cap, payment authority, frequencies |
FeeExports | Yes | CSV export of student fees |
FeeIntegrations | Yes | API clients, keys, webhooks for the accounting application (see integration module) |
MyFees | No (portal) | The pupil's/guardian's own fee screens — READ granted to student and guardian roles by seed |
13. Error Code Reference (FEE_*)
Every code below is a literal member of ErrorCodes in apps/api/src/common/types/error-codes.ts.
| Error Code | Condition |
|---|---|
FEE_ADJUSTMENT_ALREADY_REVERSED | The adjustment/waiver line named already has a reversal |
FEE_ADJUSTMENT_INVALID | A manual line's amount is zero, or otherwise malformed |
FEE_ADJUSTMENT_NOT_FOUND | The named adjustment line does not exist on this obligation |
FEE_ASSIGNMENT_NOT_FOUND | The named modifier assignment (stint) does not exist |
FEE_ASSIGNMENT_OVERLAP | A new stint would overlap an existing one for the same (modifier, student) |
FEE_ASSIGNMENT_STUDENT_INVALID | The student is not placed in the modifier's academic session |
FEE_BS_DATE_INVALID | A supplied Bikram Sambat date does not convert (out of the supported BS↔AD range or malformed) |
FEE_CATEGORY_INACTIVE | The category is not active and cannot be used on a new component/modifier |
FEE_CATEGORY_IN_USE | The category is referenced by a component/modifier and cannot be deleted |
FEE_CATEGORY_KIND_MISMATCH | A fee-kind operation named a scholarship/charge category or vice versa |
FEE_CATEGORY_NAME_TAKEN | Another category of the same kind already has this name |
FEE_CATEGORY_NOT_FOUND | Unknown category id |
FEE_CHARGE_SLOT_UNBILLED | A charge-once slot was not billed this period (an earlier period anchors it, or it is already taken) |
FEE_COMPONENT_INACTIVE | The component is not active and cannot be added to a new structure line |
FEE_COMPONENT_IN_USE | The component is referenced by a structure line and cannot be deleted |
FEE_COMPONENT_NAME_TAKEN | Another component already has this name |
FEE_COMPONENT_NOT_FOUND | Unknown component id |
FEE_CONFIRMATION_REQUIRED | An apply-style action was called without confirm: true |
FEE_EFFECTIVE_DATE_NOT_PERIOD_START | A structure's/modifier's effectiveFrom is not a fee period's start date |
FEE_END_DATE_BEFORE_FINALIZED | A modifier's chosen end date is before a finalized obligation's live line for it |
FEE_EXPORT_TOO_LARGE | A CSV export's filter matches too many rows |
FEE_FREQUENCY_DISABLED | The chosen component frequency is not in fee_settings.enabled_frequencies |
FEE_IDEMPOTENCY_KEY_INVALID | A supplied Idempotency-Key is not a UUID, or is reused with a different request body |
FEE_MAPPING_NOT_FOUND | The named student/invoice mapping does not exist |
FEE_MAPPING_TAKEN | The (client, student) or (client, ref) already has an active mapping |
FEE_MODIFIER_BASE_INVALID | A modifier's baseScope/baseCategoryId/baseComponentId combination is malformed |
FEE_MODIFIER_NAME_TAKEN | Another modifier of the same kind in this session already has this name |
FEE_MODIFIER_NOT_FOUND | Unknown modifier id, or a scholarship id read through the charges route (and vice versa) |
FEE_MODIFIER_RULE_INVALID | The eligibility rules DSL is malformed or references an unknown id |
FEE_MODIFIER_STATUS_TRANSITION_INVALID | The requested lifecycle transition (activate/end/archive) is not valid from the current status |
FEE_NET_NEGATIVE | The calculation's net would be below zero |
FEE_NOTIFICATION_SETTING_INVALID | An audience/channel combination not allowed for that event key |
FEE_OBLIGATION_CANCELLED | The action is not valid on a cancelled obligation |
FEE_OBLIGATION_HAS_PAYMENTS | Cancelling an obligation that has any payment entry |
FEE_OBLIGATION_NOT_CANCELLED | Reissuing an obligation that is not cancelled |
FEE_OBLIGATION_NOT_FOUND | Unknown obligation id |
FEE_OBLIGATION_NOT_OPEN | The action requires lifecycle = 'open' |
FEE_OBLIGATION_SLOT_TAKEN | A live obligation already exists for this (student, period) |
FEE_PAYMENT_ALREADY_REVERSED | The payment entry named already has a reversal |
FEE_PAYMENT_AMOUNT_INVALID | A payment/correction amount is zero, wrong sign for its kind, or out of bounds |
FEE_PAYMENT_ENTRY_NOT_FOUND | Unknown payment entry id |
FEE_PAYMENT_IDEMPOTENCY_CONFLICT | The same Idempotency-Key was reused with a different request body |
FEE_PAYMENT_MANUAL_PORTION_NEGATIVE | Reversing a manual payment would take the office's manual total, already reduced by a correction, below zero — correct the total instead |
FEE_PAYMENT_NOT_REVERSIBLE | The entry is not a manual payment, or is not of this obligation |
FEE_PAYMENT_PAID_NEGATIVE | A reversal/correction would take paidPaisa below zero |
FEE_PERIOD_LIMIT | Too many periods requested in one prepare call |
FEE_PERIOD_NOT_FOUND | Unknown period id |
FEE_PERIOD_OWNED_BY_OTHER_SESSION | The Bikram Sambat month is already owned by a different session |
FEE_PLACEMENT_AMBIGUOUS | Two placements tie for the evaluation date |
FEE_PLACEMENT_MISSING | The student has no placement at the evaluation date |
FEE_PREVIEW_STALE | A rule-mode apply's expectedCount no longer matches the live population |
FEE_RECEIPT_NOT_AVAILABLE | No receipt can be served for the payment (404; §8.6) |
FEE_RECORD_IMMUTABLE | An attempt to change a field a guard trigger has frozen (e.g. a finalized obligation's calculation) |
FEE_RUN_ITEM_POISON | A run item has failed enough retries to be permanently skipped |
FEE_RUN_NOT_CANCELLABLE | The run is already terminal |
FEE_RUN_NOT_FOUND | Unknown run id |
FEE_RUN_NOT_RETRYABLE | The run has no failed items, or is not terminal |
FEE_RUN_SCOPE_EMPTY | The run's scope resolves to zero targets |
FEE_RUN_TOO_LARGE | The scope exceeds the configured item ceiling |
FEE_SESSION_LOCKED | An activation/revision holds the exclusive session lock the requested action also needs |
FEE_SESSION_NOT_FOUND | Unknown academic session id |
FEE_SETTINGS_INVALID | A settings PATCH value fails a domain check (e.g. an unknown rounding mode) |
FEE_STRUCTURE_AMBIGUOUS | Two structures tie in specificity and date for one placement |
FEE_STRUCTURE_CONFLICT | A new/edited target would overlap another structure's target ambiguously |
FEE_STRUCTURE_EMPTY | Activating a structure with no target or no line |
FEE_STRUCTURE_NAME_TAKEN | Another structure in this session already has this name |
FEE_STRUCTURE_NOT_DRAFT | The action requires status = 'draft' |
FEE_STRUCTURE_NOT_FOUND | Unknown structure id |
FEE_STRUCTURE_NOT_FOUND_FOR_STUDENT | No structure targets the student's placement at all |
FEE_STRUCTURE_STATUS_TRANSITION_INVALID | The requested lifecycle transition is not valid from the current status |
FEE_STRUCTURE_TARGET_INVALID | A target's grade/section combination is malformed or duplicated |
FEE_SYNC_EVENT_ID_REUSED | An eventId was reused by the SAME client with a DIFFERENT payload (not a true idempotent replay) |
FEE_SYNC_EVENT_NOT_FOUND | Unknown sync event id |
FEE_SYNC_IDENTITY_DISAGREES | Two events on one externalRef resolve to different students/obligations |
FEE_SYNC_NOT_RESOLVABLE | The event is not in a state resolve/retry can act on — including a later event on a transaction the office already rejected, which is held as conflict rather than applied automatically |
FEE_SYNC_OBLIGATION_CANCELLED | The matched obligation is cancelled and cannot receive a payment |
FEE_SYNC_PAYLOAD_INVALID | The posted payment payload fails validation FeeSyncIntakeService performs itself (bypassing the stripped ValidationPipe) |
FEE_SYNC_PROCESSING_FAILED | A payment sync event exhausted every retry; recorded on the event as status: "failed", distinct from a payload the intake rejects up front |
FEE_TERM_IN_USE | The term is referenced by a term-frequency line and cannot be deleted |
FEE_TERM_NAME_TAKEN | Another term of the session already has this name |
FEE_TERM_NOT_FOUND | Unknown term id |
FEE_TERM_OUTSIDE_SESSION | The term's dates fall outside its academic session |
FEE_TERM_OVERLAP | The term's dates overlap another term of the same session (fee_terms_no_overlap) |
FEE_VALUE_INVALID | A flat/percent value's shape, or a manual line's magnitude, is malformed (mirrors calculateFee's own validation) |
FEE_VERSION_CONFLICT | The supplied version no longer matches the row (optimistic concurrency) |
FEE_VERSION_NOT_FOUND | The named structure version does not exist on this structure |
The receipt routes also answer ACCOUNTING_RECEIPT_UNAVAILABLE (503): accounting did not return a usable PDF (§8.6).
14. Zero-Omission API Checklist
- Every controller route is documented (§4).
- Every DTO field, enum, and default is documented (§6, §7).
- Every permission module and its superadmin-only status is documented (§12).
- Every
FEE_*error code is documented (§13). - Representative endpoints carry full request/response/error detail (§8).
- Route ownership, sequence, and error-decision diagrams are included (§9).
- Pagination behavior per list endpoint is documented (§10).
- The doc links to backend and features/flows.
See Also
- Backend doc:
/docs/developer/fees/backend - Features and flows doc:
/docs/developer/fees/feature
Fees Backend Documentation
Backend architecture, data model, the calculation engine, services, queues, and operational behavior for the fee management module.
Salary Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the salary management module.