Skoolsewa - School Management System Docs
Developer ResourcesFees

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

AreaFiles InspectedWhat Was Verified
ControllersAll 22 apps/api/src/modules/fees/**/*.controller.tsEvery route, method, guard, permission, decorator
DTOsapps/api/src/modules/fees/**/dto/*.tsRequest/response shapes, validation
Schemapackages/db/src/schema/school/fees-*.tsEnums, generated fields, constraints surfaced in responses
Error codesapps/api/src/common/types/error-codes.tsEvery FEE_* code, plus ACCOUNTING_RECEIPT_UNAVAILABLE
Permission catalogpackages/db/src/authorization/permission-catalog.tsEvery fee permission module
Module compositionapps/api/src/modules/fees/fees.module.ts, apps/api/src/modules/mobile/mobile.module.tsRoute prefixes, ADMIN_MODULES vs. MOBILE_CHILDREN

2. Module Summary

FieldValue
Module nameFees
Module slugfees
Primary actorsOffice/admin (permissioned), pupil, guardian, worker
API surfacesadmin, mobile (portal)
Base route prefixes/api/fees/..., /api/mobile/fees/...
Auth modelJwtAuthGuard + RoleGuard + @Permissions("Fee*_ACTION") (admin); JwtAuthGuard + RoleGuard + MyFees_READ + audience assertion (portal)
PersistencePostgreSQL (packages/db), BullMQ (QueueName.FEES)
Runtime source of truthfee_obligations and its lines; fee_settings/fee_notification_settings
Sibling docsFeatures/flows, Backend

3. Concepts and Terminology

TermMeaningSource File
Fee periodOne Bikram Sambat month, owned by exactly one academic sessionfees-catalog.ts
Fee structureThe base fee for a set of grades/sections from an effective datefees-structures.ts
Structure versionAn immutable snapshot of what a structure billed from a date; obligations reference the version, never the mutable structurefees-structures.ts
ModifierA scholarship or charge — one table, kind tells them apartfees-modifiers.ts
StintOne period of a modifier applying to one student (fee_modifier_assignments row)fees-modifiers.ts
ObligationOne student's bill for one periodfees-obligations.ts
LineOne component/scholarship/charge/adjustment/waiver entry inside an obligationfees-obligations.ts
Generation runA 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 datefee-resolution.service.ts
Charge-once slotThe scope (session/term/all-time) an annual/term/one_time component or a once_per_session modifier bills in at most oncefee-charge-once.util.ts
Basis points (bp)1 bp = 0.01%; 10000 bp = 100%fees-contract.ts
Paisa1/100 of a rupee; every stored amount is an integer number of paisafees-catalog.ts

4. API Surface Map

Catalogue

MethodPathPermissionController
GET/api/fees/calendar/bs-monthsFeeDashboard_READFeeCalendarController
GET/api/fees/calendar/todayFeeDashboard_READFeeCalendarController
GET/api/fees/categoriesFeeCategories_READFeeCategoriesController
POST/api/fees/categoriesFeeCategories_CREATEFeeCategoriesController
PATCH/api/fees/categories/:idFeeCategories_UPDATEFeeCategoriesController
POST/api/fees/categories/:id/statusFeeCategories_UPDATEFeeCategoriesController
DELETE/api/fees/categories/:idFeeCategories_DELETEFeeCategoriesController
GET/api/fees/componentsFeeComponents_READFeeComponentsController
GET/api/fees/components/:idFeeComponents_READFeeComponentsController
POST/api/fees/componentsFeeComponents_CREATEFeeComponentsController
PATCH/api/fees/components/:idFeeComponents_UPDATEFeeComponentsController
POST/api/fees/components/:id/statusFeeComponents_UPDATEFeeComponentsController
DELETE/api/fees/components/:idFeeComponents_DELETEFeeComponentsController
GET/api/fees/periodsFeeSettings_READFeePeriodsController
POST/api/fees/periods/prepareFeeSettings_UPDATEFeePeriodsController
PATCH/api/fees/periods/:idFeeSettings_UPDATEFeePeriodsController
GET/api/fees/termsFeeSettings_READFeeTermsController
POST/api/fees/termsFeeSettings_UPDATEFeeTermsController
PATCH/api/fees/terms/:idFeeSettings_UPDATEFeeTermsController
DELETE/api/fees/terms/:idFeeSettings_UPDATEFeeTermsController

Structures

MethodPathPermissionController
GET/api/fees/structuresFeeStructures_READFeeStructuresController
POST/api/fees/structuresFeeStructures_CREATEFeeStructuresController
GET/api/fees/structures/:idFeeStructures_READFeeStructuresController
PATCH/api/fees/structures/:idFeeStructures_UPDATEFeeStructuresController
PUT/api/fees/structures/:id/targetsFeeStructures_UPDATEFeeStructuresController
PUT/api/fees/structures/:id/linesFeeStructures_UPDATEFeeStructuresController
POST/api/fees/structures/:id/copyFeeStructures_CREATEFeeStructuresController
DELETE/api/fees/structures/:idFeeStructures_DELETEFeeStructuresController
GET/api/fees/structures/:id/conflictsFeeStructures_READFeeStructuresController
GET/api/fees/structures/:id/versions/:versionIdFeeStructures_READFeeStructuresController
POST/api/fees/structures/:id/activateFeeStructureActivation_UPDATEFeeStructureLifecycleController
POST/api/fees/structures/:id/deactivateFeeStructureActivation_UPDATEFeeStructureLifecycleController
POST/api/fees/structures/:id/reactivateFeeStructureActivation_UPDATEFeeStructureLifecycleController
POST/api/fees/structures/:id/archiveFeeStructureActivation_UPDATEFeeStructureLifecycleController
POST/api/fees/structures/:id/revisions/previewFeeStructureActivation_UPDATEFeeStructureLifecycleController
POST/api/fees/structures/:id/revisionsFeeStructureActivation_UPDATEFeeStructureLifecycleController

Scholarships and Charges

Identical 13-route shape under /api/fees/scholarships (FeeScholarships_*, FeeScholarshipAssignments_*) and /api/fees/charges (FeeCharges_*, FeeChargeAssignments_*):

MethodPathPermission (scholarships / charges)
GET.../{scholarships,charges}FeeScholarships_READ / FeeCharges_READ
POST.../{scholarships,charges}FeeScholarships_CREATE / FeeCharges_CREATE
GET.../{scholarships,charges}/:idFeeScholarships_READ / FeeCharges_READ
PATCH.../{scholarships,charges}/:idFeeScholarships_UPDATE / FeeCharges_UPDATE
DELETE.../{scholarships,charges}/:idFeeScholarships_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-previewFeeScholarshipAssignments_CREATE / FeeChargeAssignments_CREATE
POST.../{scholarships,charges}/:id/applyFeeScholarshipAssignments_CREATE / FeeChargeAssignments_CREATE
GET.../{scholarships,charges}/:id/assignmentsFeeScholarshipAssignments_READ / FeeChargeAssignments_READ
POST.../{scholarships,charges}/:id/assignmentsFeeScholarshipAssignments_CREATE / FeeChargeAssignments_CREATE
POST.../{scholarships,charges}/:id/assignments/:assignmentId/endFeeScholarshipAssignments_UPDATE / FeeChargeAssignments_UPDATE

Obligations

MethodPathPermissionController
GET/api/fees/student-feesStudentFees_READFeeStudentFeesController
GET/api/fees/student-fees/:idStudentFees_READFeeStudentFeesController
GET/api/fees/student-fees/:id/historyStudentFees_READFeeStudentFeesController
GET/api/fees/students/:studentId/summaryStudentFees_READFeeStudentSummaryController
POST/api/fees/student-fees/:id/adjustmentsStudentFeeAdjustments_CREATEFeeStudentFeeActionsController
POST/api/fees/student-fees/:id/waiversStudentFeeAdjustments_CREATEFeeStudentFeeActionsController
POST/api/fees/student-fees/:id/adjustments/:lineId/reverseStudentFeeAdjustments_UPDATEFeeStudentFeeActionsController
POST/api/fees/student-fees/:id/finalizeStudentFeeAdjustments_UPDATEFeeStudentFeeActionsController
POST/api/fees/student-fees/:id/cancelStudentFeeCancellation_UPDATEFeeStudentFeeActionsController
POST/api/fees/student-fees/:id/reissueStudentFeeCancellation_UPDATEFeeStudentFeeActionsController

Runs

MethodPathPermissionController
GET/api/fees/generation-runsStudentFees_READFeeRunsController
POST/api/fees/generation-runsStudentFees_CREATEFeeRunsController
GET/api/fees/generation-runs/:idStudentFees_READFeeRunsController
GET/api/fees/generation-runs/:id/itemsStudentFees_READFeeRunsController
POST/api/fees/generation-runs/:id/retryStudentFees_CREATEFeeRunsController
POST/api/fees/generation-runs/:id/cancelStudentFees_CREATEFeeRunsController

Payments, mappings, sync

MethodPathPermissionController
POST/api/fees/student-fees/:id/paymentsFeePayments_CREATEFeePaymentsController
POST/api/fees/student-fees/:id/payments/:entryId/reverseFeePaymentCorrections_UPDATEFeePaymentsController
POST/api/fees/student-fees/:id/payment-correctionFeePaymentCorrections_UPDATEFeePaymentsController
GET/api/fees/paymentsFeePayments_READFeePaymentsController
GET/api/fees/payments/:paymentId/receiptFeeReceipts_READFeePaymentsController
GET/api/fees/student-mappingsFeeMappings_READFeeMappingsController
POST/api/fees/student-mappingsFeeMappings_CREATEFeeMappingsController
POST/api/fees/student-mappings/:id/retireFeeMappings_UPDATEFeeMappingsController
GET/api/fees/invoice-mappingsFeeMappings_READFeeMappingsController
POST/api/fees/invoice-mappingsFeeMappings_CREATEFeeMappingsController
GET/api/fees/sync-eventsFeeSync_READFeeSyncEventsController
GET/api/fees/sync-events/:idFeeSync_READFeeSyncEventsController
POST/api/fees/sync-events/:id/retryFeeSyncResolution_UPDATEFeeSyncEventsController
POST/api/fees/sync-events/:id/resolveFeeSyncResolution_UPDATEFeeSyncEventsController

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

MethodPathPermissionController
GET/api/fees/change-logFeeDashboard_READFeeChangeLogController
GET/api/fees/dashboardFeeDashboard_READFeeDashboardController
GET/api/fees/exports/student-feesFeeExports_READFeeExportsController
GET/api/fees/settingsFeeSettings_READFeeSettingsController
PATCH/api/fees/settingsFeeSettings_UPDATEFeeSettingsController
GET/api/fees/settings/financialFeeFinancialSettings_READFeeSettingsController
PATCH/api/fees/settings/financialFeeFinancialSettings_UPDATEFeeSettingsController
GET/api/fees/notification-settingsFeeSettings_READFeeNotificationSettingsController
PATCH/api/fees/notification-settings/:eventKeyFeeSettings_UPDATEFeeNotificationSettingsController

Mobile portal (/api/mobile/fees, MOBILE_CHILDREN only — never in ADMIN_MODULES)

MethodPathPermissionAudience AssertionController
GET/api/mobile/fees/meMyFees_READstudentFeeStudentPortalController
GET/api/mobile/fees/me/:obligationIdMyFees_READstudentFeeStudentPortalController
GET/api/mobile/fees/childrenMyFees_READguardianFeeGuardianPortalController
GET/api/mobile/fees/children/:studentIdMyFees_READguardianFeeGuardianPortalController
GET/api/mobile/fees/children/:studentId/:obligationIdMyFees_READguardianFeeGuardianPortalController
GET/api/mobile/fees/me/payments/:paymentId/receiptMyFees_READstudentFeeStudentPortalController
GET/api/mobile/fees/children/:studentId/payments/:paymentId/receiptMyFees_READguardianFeeGuardianPortalController

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

SurfaceGuard/DecoratorIdentity ShapePermissionNotes
AdminJwtAuthGuard, RoleGuardreq.user, activeRoleExact 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, RoleGuardreq.user, activeRole.scopeKind = "student"MyFees_READALSO asserts assertPortalAudience(actor, "student") in the service — belt and braces
Portal (guardian)JwtAuthGuard, RoleGuardreq.user, activeRole.scopeKind = "guardian"MyFees_READSame, 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)

FieldTypeNotes
idstring (public id)—
student{id, name}—
academicSession{id, name}—
period{id, bsYear, bsMonth}—
structure{id, name, versionNo}The VERSION that priced it
lifecycleFeeObligationLifecycleopen | finalized | cancelled
paymentStatusFeePaymentStatusGENERATED, mirrors the DB column
grossPaisa, scholarshipPaisa, chargePaisa, adjustmentPaisa, waiverPaisa, netPaisa, paidPaisa, balancePaisanumberInteger paisa
dueDateFeeDateDto | null{date, bs} — both calendars
versionnumberOptimistic 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

FieldTypeRequiredValidation
amountPaisanumberYesNon-zero integer; a waiver is stored negated internally
labelstringadjustments only—
reasonstringYes3–1000 chars
versionnumberYesThe version last read

6.3 CreateModifierDto (scholarship/charge)

FieldTypeRequiredValidation
academicSessionId, categoryIdstring (uuid)Yes—
namestringYes≤150 chars
valueType"flat" | "percent"YesShape-checked against amountPaisa/percentBp
amountPaisanumberflat only1–10,000,000,000
percentBpnumberpercent only1–10000
baseScope"gross" | "category" | "component"YesDetermines required baseCategoryId/baseComponentId
recurrence"per_period" | "once_per_session"Yes—
eligibilityMode"rule" | "manual"Yesrule requires eligibilityRules; manual forbids it
eligibilityRulesEligibilityRulesDtorule modegradeIds, sectionIds, ethnicityIds, genders, disabilityTypes, transportModes, enrollmentStatuses — every key AND-ed, each array OR-ed
autoApplyNewMatchesbooleanrule mode only—
stackGroupstringscholarships only1–40 chars, lower-cased
effectiveFromBsstringYesMust be a fee period's start date

6.4 AssignStudentsDto

FieldTypeNotes
studentIdsstring[] (uuid)Manual assignment target
effectiveFromBsstring | undefinedDefaults 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):

FieldTypeNotes
iduuidThe fee's public id
academicSessionIduuidPublic 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
academicSessionNamestring
periodFeePortalPeriodDtolabel, bsYear, bsMonth, startDate, endDate
gradeName, sectionNamestringPlacement the fee was calculated for
dueDateFeeDateDto | null
overdueboolean
netPaisa, paidPaisa, balancePaisainteger paisa
paymentStatusFeePaymentStatus
lifecycleopen | finalizedCancelled fees are never shown in the portal
lastPaymentAtISO 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):

FieldTypeNotes
iduuidThe entry's public id; the receipt route addresses it
kindpayment | reversal | correction—
sourcemanual | externalexternal is money the accounting application reported
amountPaisainteger paisaSigned: a reversal or downward correction is negative
paidOnFeeDateDto | null—
reference, note, reason, reversesEntryIdstring | null—
createdAtISO instant—
recordedBy{id, name} | null—
receiptAvailablebooleantrue 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

EnumValuesSource
FeeCategoryKindfee, scholarship, chargefees-contract.ts
FeeCategoryStatusactive, inactive, archived—
FeeValueTypeflat, percent—
FeeFrequencymonthly, one_time, annual, term—
FeeStructureStatusdraft, active, inactive, archived—
FeeModifierKindscholarship, charge—
FeeModifierStatusdraft, active, ended, archived—
FeeBaseScopegross, category, component—
FeeRecurrenceper_period, once_per_session—
FeeEligibilityModerule, manual—
FeeAssignmentSourcemanual, rule—
FeeObligationLifecycleopen, finalized, cancelled—
FeePaymentStatusunpaid, partially_paid, paid, overpaid, waived, nothing_due, cancelledGENERATED column
FeeLineTypecomponent, scholarship, charge, adjustment, waiver—
FeeLineOriginsystem, manual—
FeeRoundingModehalf_up, half_even, down, up—
FeeRoundingUnit1, 100 (paisa)—
FeeRunKindgenerate, recalculate_open, modifier_apply, modifier_end, structure_revision, auto, placement_review—
FeeRunStatusqueued, planning, processing, completed, completed_with_exceptions, failed, cancelled—
FeeRunItemStatuspending, succeeded, skipped, failed, cancelled—
FeeRunItemOutcomecreated, recalculated, unchanged, already_exists, cancelled_exists, finalized_protected, not_applicable, cancelled_no_placement—
FeeScholarshipStackingstack, highest_onlyGlobal setting
FeePaymentAuthorityexternal, manual_reviewGlobal setting
FeeSectionChangePolicykeep, recalculate_openGlobal setting
FeeMidPeriodJoinPolicycharge_period, next_periodGlobal setting
FeeWithdrawalPolicycancel_unpaid_open, flagGlobal setting
FeePaymentEntryKindpayment, reversal, correction—
FeePaymentSourcemanual, external—
FeeExternalTransactionStateapplied, held_unmatched, held_conflict, rejected—
FeeSyncStatuspending, succeeded, failed, duplicate, rejected, conflict, unmatched—
FeeMappingStatusactive, retired—
FeeMappingOriginapi, admin, intake—
FeeObligationEventTypegenerated, 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—
FeeNotificationAudiencestudent, guardian, admin—
FeeNotificationChannelin_app, push, email, sms—
FeeSweepPassfinalize_started_periods, auto_generate, reminders, rule_new_matchesPlus 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 StatusError CodeCondition
409FEE_SESSION_LOCKEDAn activation/revision holds the exclusive session lock
409FEE_RUN_SCOPE_EMPTYThe scope resolves to no targets up front
409FEE_RUN_TOO_LARGEThe 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 StatusError CodeCondition
409FEE_VERSION_CONFLICTversion no longer matches
400FEE_ADJUSTMENT_INVALIDamountPaisa === 0
409FEE_NET_NEGATIVEThe resulting net would be below zero
404FEE_OBLIGATION_NOT_FOUNDUnknown 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 StatusError CodeCondition
409FEE_OBLIGATION_HAS_PAYMENTSAny payment entry exists
409FEE_OBLIGATION_NOT_OPENAlready cancelled (idempotency is NOT implied — a second cancel is refused, not a no-op)
400Validationreason 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 StatusError CodeCondition
400FEE_CONFIRMATION_REQUIREDconfirm !== true
409FEE_PREVIEW_STALEThe matched population changed since the preview (expectedCount mismatch)
409FEE_MODIFIER_STATUS_TRANSITION_INVALIDThe 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

MethodPathAudiencePermissionScoping
GET/api/fees/payments/:paymentId/receiptOfficeFeeReceipts_READAny payment entry that has a receipt
GET/api/mobile/fees/children/:studentId/payments/:paymentId/receiptGuardianMyFees_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/receiptPupilMyFees_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:

HeaderValue
Content-Typeapplication/pdf
Content-LengthThe byte length
Cache-Controlno-store
X-Content-Type-Optionsnosniff
Content-Dispositionattachment; 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 StatusError CodeCondition
403PERMISSION_INSUFFICIENTA portal route called by the wrong audience
404FEE_RECEIPT_NOT_AVAILABLEReceipt 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
503ACCOUNTING_RECEIPT_UNAVAILABLEAccounting 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)

EndpointPaginationDefault SizeMax SizeSort Fieldspagination=false
/api/fees/student-feespage/size20100dueDate, netPaisa, balancePaisa, studentName, period, updatedAtRefused — PAGINATION_LIMIT_INVALID
/api/fees/categories, /componentspage/size20100name, sortOrder, updatedAt, createdAt (category default sortOrder asc)Allowed, bounded by UNPAGINATED_HARD_CAP (1000)
/api/fees/generation-runspage/size20100createdAt (default desc)Allowed
/api/fees/payments, /sync-eventspage/size20100createdAt/receivedAtAllowed
/api/mobile/fees/me, /childrennone (a session's own fee count is always small)———N/A

11. Caching, Jobs, and External Integrations

IntegrationUsed?Details
Redis cacheNoEvery fee read goes to PostgreSQL directly, indexed for it
BullMQYesQueueName.FEES — generation_plan, generation_chunk, payment_sync, change_fanout, sweep (see the backend doc §9)
External APIIndirectThe 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 receiptsOptionalGET {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:

ModuleSuperadmin-only?Covers
FeeDashboardNoDashboard, change log, BS calendar
FeeCategories, FeeComponentsNoCatalogue
FeeStructuresNoDraft structures: create, edit, copy, targets, lines
FeeStructureActivationNoActivate, deactivate, reactivate, archive, revise
FeeScholarships, FeeChargesNoThe modifier records themselves
FeeScholarshipAssignments, FeeChargeAssignmentsNoPreview, apply, assign, end stints
StudentFeesNoRead obligations; create generation runs
StudentFeeAdjustmentsNoAdjustments, waivers, reversals, finalization
StudentFeeCancellationNoCancel and reissue
FeePaymentsNoRead payment info; record a manual payment
FeeReceiptsNoDownload 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
FeePaymentCorrectionsYesReverse or correct a manual payment
FeeSyncNoRead the accounting sync inbox
FeeSyncResolutionYesResolve and retry held sync events
FeeMappingsYesStudent and invoice mappings to the accounting system
FeeSettingsNoCalendar, periods, terms, policies, reminders, notification matrix
FeeFinancialSettingsYesRounding, stacking, scholarship cap, payment authority, frequencies
FeeExportsYesCSV export of student fees
FeeIntegrationsYesAPI clients, keys, webhooks for the accounting application (see integration module)
MyFeesNo (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 CodeCondition
FEE_ADJUSTMENT_ALREADY_REVERSEDThe adjustment/waiver line named already has a reversal
FEE_ADJUSTMENT_INVALIDA manual line's amount is zero, or otherwise malformed
FEE_ADJUSTMENT_NOT_FOUNDThe named adjustment line does not exist on this obligation
FEE_ASSIGNMENT_NOT_FOUNDThe named modifier assignment (stint) does not exist
FEE_ASSIGNMENT_OVERLAPA new stint would overlap an existing one for the same (modifier, student)
FEE_ASSIGNMENT_STUDENT_INVALIDThe student is not placed in the modifier's academic session
FEE_BS_DATE_INVALIDA supplied Bikram Sambat date does not convert (out of the supported BS↔AD range or malformed)
FEE_CATEGORY_INACTIVEThe category is not active and cannot be used on a new component/modifier
FEE_CATEGORY_IN_USEThe category is referenced by a component/modifier and cannot be deleted
FEE_CATEGORY_KIND_MISMATCHA fee-kind operation named a scholarship/charge category or vice versa
FEE_CATEGORY_NAME_TAKENAnother category of the same kind already has this name
FEE_CATEGORY_NOT_FOUNDUnknown category id
FEE_CHARGE_SLOT_UNBILLEDA charge-once slot was not billed this period (an earlier period anchors it, or it is already taken)
FEE_COMPONENT_INACTIVEThe component is not active and cannot be added to a new structure line
FEE_COMPONENT_IN_USEThe component is referenced by a structure line and cannot be deleted
FEE_COMPONENT_NAME_TAKENAnother component already has this name
FEE_COMPONENT_NOT_FOUNDUnknown component id
FEE_CONFIRMATION_REQUIREDAn apply-style action was called without confirm: true
FEE_EFFECTIVE_DATE_NOT_PERIOD_STARTA structure's/modifier's effectiveFrom is not a fee period's start date
FEE_END_DATE_BEFORE_FINALIZEDA modifier's chosen end date is before a finalized obligation's live line for it
FEE_EXPORT_TOO_LARGEA CSV export's filter matches too many rows
FEE_FREQUENCY_DISABLEDThe chosen component frequency is not in fee_settings.enabled_frequencies
FEE_IDEMPOTENCY_KEY_INVALIDA supplied Idempotency-Key is not a UUID, or is reused with a different request body
FEE_MAPPING_NOT_FOUNDThe named student/invoice mapping does not exist
FEE_MAPPING_TAKENThe (client, student) or (client, ref) already has an active mapping
FEE_MODIFIER_BASE_INVALIDA modifier's baseScope/baseCategoryId/baseComponentId combination is malformed
FEE_MODIFIER_NAME_TAKENAnother modifier of the same kind in this session already has this name
FEE_MODIFIER_NOT_FOUNDUnknown modifier id, or a scholarship id read through the charges route (and vice versa)
FEE_MODIFIER_RULE_INVALIDThe eligibility rules DSL is malformed or references an unknown id
FEE_MODIFIER_STATUS_TRANSITION_INVALIDThe requested lifecycle transition (activate/end/archive) is not valid from the current status
FEE_NET_NEGATIVEThe calculation's net would be below zero
FEE_NOTIFICATION_SETTING_INVALIDAn audience/channel combination not allowed for that event key
FEE_OBLIGATION_CANCELLEDThe action is not valid on a cancelled obligation
FEE_OBLIGATION_HAS_PAYMENTSCancelling an obligation that has any payment entry
FEE_OBLIGATION_NOT_CANCELLEDReissuing an obligation that is not cancelled
FEE_OBLIGATION_NOT_FOUNDUnknown obligation id
FEE_OBLIGATION_NOT_OPENThe action requires lifecycle = 'open'
FEE_OBLIGATION_SLOT_TAKENA live obligation already exists for this (student, period)
FEE_PAYMENT_ALREADY_REVERSEDThe payment entry named already has a reversal
FEE_PAYMENT_AMOUNT_INVALIDA payment/correction amount is zero, wrong sign for its kind, or out of bounds
FEE_PAYMENT_ENTRY_NOT_FOUNDUnknown payment entry id
FEE_PAYMENT_IDEMPOTENCY_CONFLICTThe same Idempotency-Key was reused with a different request body
FEE_PAYMENT_MANUAL_PORTION_NEGATIVEReversing a manual payment would take the office's manual total, already reduced by a correction, below zero — correct the total instead
FEE_PAYMENT_NOT_REVERSIBLEThe entry is not a manual payment, or is not of this obligation
FEE_PAYMENT_PAID_NEGATIVEA reversal/correction would take paidPaisa below zero
FEE_PERIOD_LIMITToo many periods requested in one prepare call
FEE_PERIOD_NOT_FOUNDUnknown period id
FEE_PERIOD_OWNED_BY_OTHER_SESSIONThe Bikram Sambat month is already owned by a different session
FEE_PLACEMENT_AMBIGUOUSTwo placements tie for the evaluation date
FEE_PLACEMENT_MISSINGThe student has no placement at the evaluation date
FEE_PREVIEW_STALEA rule-mode apply's expectedCount no longer matches the live population
FEE_RECEIPT_NOT_AVAILABLENo receipt can be served for the payment (404; §8.6)
FEE_RECORD_IMMUTABLEAn attempt to change a field a guard trigger has frozen (e.g. a finalized obligation's calculation)
FEE_RUN_ITEM_POISONA run item has failed enough retries to be permanently skipped
FEE_RUN_NOT_CANCELLABLEThe run is already terminal
FEE_RUN_NOT_FOUNDUnknown run id
FEE_RUN_NOT_RETRYABLEThe run has no failed items, or is not terminal
FEE_RUN_SCOPE_EMPTYThe run's scope resolves to zero targets
FEE_RUN_TOO_LARGEThe scope exceeds the configured item ceiling
FEE_SESSION_LOCKEDAn activation/revision holds the exclusive session lock the requested action also needs
FEE_SESSION_NOT_FOUNDUnknown academic session id
FEE_SETTINGS_INVALIDA settings PATCH value fails a domain check (e.g. an unknown rounding mode)
FEE_STRUCTURE_AMBIGUOUSTwo structures tie in specificity and date for one placement
FEE_STRUCTURE_CONFLICTA new/edited target would overlap another structure's target ambiguously
FEE_STRUCTURE_EMPTYActivating a structure with no target or no line
FEE_STRUCTURE_NAME_TAKENAnother structure in this session already has this name
FEE_STRUCTURE_NOT_DRAFTThe action requires status = 'draft'
FEE_STRUCTURE_NOT_FOUNDUnknown structure id
FEE_STRUCTURE_NOT_FOUND_FOR_STUDENTNo structure targets the student's placement at all
FEE_STRUCTURE_STATUS_TRANSITION_INVALIDThe requested lifecycle transition is not valid from the current status
FEE_STRUCTURE_TARGET_INVALIDA target's grade/section combination is malformed or duplicated
FEE_SYNC_EVENT_ID_REUSEDAn eventId was reused by the SAME client with a DIFFERENT payload (not a true idempotent replay)
FEE_SYNC_EVENT_NOT_FOUNDUnknown sync event id
FEE_SYNC_IDENTITY_DISAGREESTwo events on one externalRef resolve to different students/obligations
FEE_SYNC_NOT_RESOLVABLEThe 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_CANCELLEDThe matched obligation is cancelled and cannot receive a payment
FEE_SYNC_PAYLOAD_INVALIDThe posted payment payload fails validation FeeSyncIntakeService performs itself (bypassing the stripped ValidationPipe)
FEE_SYNC_PROCESSING_FAILEDA 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_USEThe term is referenced by a term-frequency line and cannot be deleted
FEE_TERM_NAME_TAKENAnother term of the session already has this name
FEE_TERM_NOT_FOUNDUnknown term id
FEE_TERM_OUTSIDE_SESSIONThe term's dates fall outside its academic session
FEE_TERM_OVERLAPThe term's dates overlap another term of the same session (fee_terms_no_overlap)
FEE_VALUE_INVALIDA flat/percent value's shape, or a manual line's magnitude, is malformed (mirrors calculateFee's own validation)
FEE_VERSION_CONFLICTThe supplied version no longer matches the row (optimistic concurrency)
FEE_VERSION_NOT_FOUNDThe 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