Skoolsewa - School Management System Docs
Developer ResourcesFees

Fees Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the fee management module.

Fees Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Schemapackages/db/src/schema/school/fees-*.ts, packages/db/src/migrations/0044_fee_management_integrity.sqlEvery table, enum, constraint, and the guard triggers behind each lifecycle
Calculation engineapps/api/src/utils/fees/fee-calculation.util.ts, fee-charge-once.util.ts, fee-rounding.util.tsThe fixed calculation order, rounding, caps, charge-once anchor rule
Resolverapps/api/src/modules/fees/shared/fee-resolution.service.tsPlacement, structure/version resolution, charge-once eligibility
Controllersapps/api/src/modules/fees/**/*.controller.ts (22 files)Every admin and portal route, its permission, and its purpose
Permission catalogpackages/db/src/authorization/permission-catalog.tsThe 25 fee permission modules and their default grants
Notification matrixpackages/db/src/schema/school/fees-contract.ts, apps/api/src/modules/notification/templates/fee-templates.tsThe 26 fee notification events, their audiences, channels, and templates
Queue and schedulerpackages/jobs/src/index.ts, apps/api/src/modules/fees/workers/*.tsFeeJob, the five-minute sweep, and its six passes
Worked examplesapps/api/src/utils/fees/fee-calculation.util.spec.ts, apps/api/src/modules/fees/__tests__/fee-calculation.integration.spec.tsExact numbers for every calculation rule below
Sibling docsThis module's backend.mdx, api.mdxCross-referenced throughout

2. Feature Summary

FieldValue
Modulefees
Submodulescatalog, structures, modifiers, obligations, runs, payments, sync, portal, reports, settings, workers, shared
Primary user valueA school bills every pupil the correct amount for every month, term, and one-time charge, tracks what is paid, and lets a linked accounting application read and reconcile the same figures without re-entering them.
ActorsAdmin/office staff (with fee permissions), pupils, guardians, the accounting application (via the integration module), the daily sweep and BullMQ workers
Main entry points/api/fees/** admin routes, /api/mobile/fees/** portal routes, /api/integration/fees/** (documented in the integration module), the five-minute cron sweep, BullMQ jobs on QueueName.FEES
Main outputsStudent fee obligations and their line breakdowns, payment records, generation runs, the fee change feed (consumed by integration), in-app/push/email/SMS notifications
Related docsBackend, API

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
Office / accountant (granted Fee* codes)Manage the catalogue, structures, scholarships/charges, generate and recalculate fees, record and correct payments, resolve accounting sync exceptionsAnything a code they were not granted covers — every fee code is withheld from staff/teacher by defaultJWT + RoleGuard + the specific Fee*_ACTION codeNo fee code is part of the automatic admin grant except FeeDashboard/FeeCategories/FeeComponents/FeeStructures/FeeScholarships/FeeCharges/StudentFees/FeePayments/FeeSync/FeeSettings-family read/write; money-moving and disclosure codes (FeePaymentCorrections, FeeSyncResolution, FeeMappings, FeeFinancialSettings, FeeExports, FeeIntegrations) are superadmin-only and must be granted to a named role
Pupil (student role)Read their own fees list and one fee's detailSee any other pupil's fee, make a payment, see internal reasons/notesJWT + RoleGuard + MyFees_READ + the route's own assertPortalAudience(actor, "student") check/api/mobile/fees/me, /api/mobile/fees/me/:obligationId
Guardian (guardian role)Read each linked child's fees list and detailRead a child not linked to them, make a paymentJWT + RoleGuard + MyFees_READ + assertPortalAudience(actor, "guardian") + the child resolved against the guardian's own links before anything is read/api/mobile/fees/children, /api/mobile/fees/children/:studentId, /api/mobile/fees/children/:studentId/:obligationId
Accounting applicationRead the catalogue, structures, scholarships/charges, students, obligations; report payments; manage student/invoice mappings; poll the change feed; receive webhooksWrite to anything but payments and mappings; read another accounting client's private mapping eventsAuthorization: Bearer ssk_… (API key) + scope (fees:read, students:read, payments:write, mappings:write) — see the integration module's docsEvery route is @Public() to the staff JwtAuthGuard; the API-key guard is its own authentication
Sweep / BullMQ workerFinalize started periods, auto-generate and re-price fees, send reminders, apply new rule matches, plan and run generation runs, process payment sync events, fan out change eventsAnything requiring a human decision (a manual adjustment, a cancellation reason)Internal — no HTTP surfaceFeeQueueProcessor on QueueName.FEES, concurrency: 2

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
Manage categories/componentsAdminOffice/api/fees/categories, /api/fees/componentsfee_categories, fee_componentsSameCatalogue
Prepare/edit fee periodsAdminOfficePOST /api/fees/periods/prepare, PATCH /api/fees/periods/:idacademic_sessions, fee_settingsfee_periods, cascades due-date edits to every non-cancelled obligation of the periodCatalogue
Manage termsAdminOffice/api/fees/termsacademic_sessionsfee_terms (no-overlap EXCLUDE constraint)Catalogue
Draft/edit/activate a fee structureAdminOffice/api/fees/structures/**, /api/fees/structures/:id/activatefee_categories, fee_componentsfee_structures, fee_structure_targets, fee_structure_lines (draft) → fee_structure_versions, fee_structure_version_lines (on activate)Structures
Revise an active structureAdminOfficePOST /api/fees/structures/:id/revisions (preview then commit)Current versionA new fee_structure_versions row, a structure_revision generation run to re-price open obligationsStructures
Create/activate/end a scholarship or chargeAdminOffice/api/fees/scholarships/**, /api/fees/charges/**fee_categoriesfee_modifiersModifiers
Preview/apply rule-mode eligibilityAdminOfficePOST /api/fees/{scholarships,charges}/:id/eligibility-preview, /applyLive placements against eligibilityRulesfee_modifier_assignments, a modifier_apply runModifiers
Assign/end a manual stintAdminOfficePOST /api/fees/{scholarships,charges}/:id/assignments, .../end—fee_modifier_assignments, a modifier_end run on endingModifiers
Generate fees for a scopeAdminOfficePOST /api/fees/generation-runsPlacements, structure versions, modifier stintsfee_generation_runs, items, fee_obligations, fee_obligation_linesRuns
Retry/cancel a runAdminOfficePOST /api/fees/generation-runs/:id/{retry,cancel}Run/item stateSameRuns
Adjust/waive/reverseAdminOfficePOST /api/fees/student-fees/:id/{adjustments,waivers}, .../reverseObligationfee_obligation_lines, fee_obligation_eventsObligations
Finalize/cancel/reissueAdminOfficePOST /api/fees/student-fees/:id/{finalize,cancel,reissue}Obligationfee_obligations, releases charge-once lines, may create a reissued obligationObligations
Record/reverse/correct a paymentAdminOffice (corrections: superadmin-granted)/api/fees/student-fees/:id/payments, .../reverse, .../payment-correctionObligationfee_payment_entries, fee_obligations.paid_paisa (via FeeLedgerService)Payments
Read/resolve sync exceptionsAdminOffice (resolution: superadmin-granted)/api/fees/sync-events/**fee_payment_sync_eventsSame, plus applies or rejects onto an obligationPayments
Manage student/invoice mappingsAdminOffice (superadmin-granted)/api/fees/student-mappings, /api/fees/invoice-mappings—fee_external_student_mappings, fee_external_invoice_mappingsPayments
Configure calendar/policies/notificationsAdminOffice/api/fees/settings, /api/fees/settings/financial, /api/fees/notification-settings—fee_settings, fee_notification_settingsSettings
Export student fees to CSVAdminOffice (superadmin-granted)GET /api/fees/exports/student-feesFiltered obligations— (streams a file)Reports
View dashboard/change logAdminOffice/api/fees/dashboard, /api/fees/change-logAggregates over obligations/events—Reports
Read own feesPortalPupil/guardian/api/mobile/fees/**fee_obligations scoped to the pupil—Portal
Auto-generate and repriceWorkerSystemFive-minute sweep, auto_generate pass, once per Nepal dayEnabled sessions/periodsauto and placement_review runsRuns
Finalize started periodsWorkerSystemSweep, finalize_started_periods pass, every tickOpen obligations whose period has startedfee_obligations.lifecycle = 'finalized'Obligations
Send remindersWorkerSystemSweep, reminders passOpen/unpaid obligations by due datefee_reminder_log, notification sends—
Apply new rule matchesWorkerSystemSweep, rule_new_matches pass, once per Nepal dayActive rule-mode modifiers with auto_apply_new_matchesfee_modifier_assignments, modifier_apply runsModifiers
Re-match held paymentsWorkerSystemSweep, sync_rematch passfee_external_transactions in held_unmatched/held_conflictApplies or reclassifiesPayments
Process a payment sync eventWorkerSystemFeeJob.PAYMENT_SYNCfee_payment_sync_eventsfee_external_transactions, fee_payment_entries, fee_obligations.paid_paisaPayments
Fan a change event outWorkerSystemFeeJob.CHANGE_FANOUTintegration_change_eventsintegration_webhook_deliveries (see integration module)—

5. User-Facing Flows

5.1 A pupil reads their own fees

Summary

A pupil or a guardian opens the fees screen and sees every fee owed for the current or a chosen academic session, then drills into one fee's full breakdown.

Preconditions

  • A valid JWT with an active role of student or guardian.
  • The role holds MyFees_READ (seeded to both roles by default).
  • For a guardian, the pupil must be one of their linked children.

Main Flow

StepActor/SystemActionResultSource
1Pupil/guardianGET /api/mobile/fees/me or /children/:studentIdRoute resolves the pupil (self, or the guardian's link)fee-student-portal.controller.ts, fee-guardian-portal.controller.ts
2BackendassertPortalAudience(actor, "student"|"guardian")Confirms the caller's role scope kind matches; a mismatch is 403portal-audience.ts
3BackendReads fee_obligations filtered to the pupil, optionally the sessionA list of fee summariesfee-student-portal.service.ts, fee-guardian-portal.service.ts
4BackendMaps to FeePortalFeeDto/FeePortalFeeListDtoNo internal reason, note, or actor identityfee-portal.mapper.ts
5ClientRenders the list; the pupil taps one feeGET /api/mobile/fees/me/:obligationId—

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Wrong audienceA staff/teacher token calls the routeRefused before any row is read403, audience mismatch
Guardian, unlinked childstudentId is not one of the guardian's childrenRow never resolved404 — never discloses that the child exists
No fees yetThe pupil has no obligation for the sessionEmpty list200 with items: []
Cancelled feeAn obligation was cancelledStill returned, paymentStatus: "cancelled"Visible, not hidden
Detail of another pupil's feeA pupil requests an obligationId that is not theirsRow-scoped query returns nothing404

5.2 The office generates fees for a period

Summary

An accountant asks the system to bill a period (or a filtered scope of students) against the active fee structure; a background run does the actual per-student work so the request returns immediately.

Preconditions

  • The period exists (fee_periods, prepared from the academic session).
  • An active fee structure targets every grade/section the run should cover.
  • The academic session is not locked (lockStructureActivation).

Every screen that narrows work to a grade or a section — the generation-run scope, the student-fee filters, a structure's targets and a scholarship or charge's eligibility rules — offers the classes the chosen session ACTUALLY runs, read from the classes module (GET /api/classes/options, keyed by the session's integer id). The grade and section reference tables hold every grade from Nursery to 12 and every section letter the school might ever use; offering those would let an operator scope a run, a target or a rule to a class that does not exist. A caller without Classes_READ sees no grade or section filter rather than an empty one.

Main Flow

StepActor/SystemActionResultSource
1OfficePOST /api/fees/generation-runs with a scope (periods, optional grade/section/student filter)A fee_generation_runs row, status: "queued", idempotent on requestKeyfee-runs-admin.service.ts, fee-run-launcher.service.ts
2BackendEnqueues FeeJob.GENERATION_PLAN in the same transaction (outbox)The plan job resolves target (student, period) pairs into fee_generation_run_items, batchedfee-run-worker.service.ts
3WorkerFeeJob.GENERATION_CHUNK per batchFor each item: resolve placement/structure/modifiers, calculateFee, write the obligationfee-run-worker.service.ts, fee-obligation-writer.service.ts
4BackendRecounts the run's counters from its items; finalizes when planning is complete and nothing is pendingstatus: "completed" or "completed_with_exceptions"fee-run-worker.service.ts
5BackendChange-feed events, notificationsintegration_change_events, fees.bulk_completed/.adminfee-change-feed.service.ts, fee-notifier.service.ts

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Empty scopeNo student matches the filterRun completes with totalItems: 0FEE_RUN_SCOPE_EMPTY refused at creation if the scope resolves to nothing up front
No placementA targeted student has no placement at the evaluation dateItem outcome not_applicableCounted, not an item failure
Ambiguous structureTwo grade-wide (or two section-level) structures both target the pupil on the same dateItem fails with FEE_STRUCTURE_AMBIGUOUSItem failed, run completed_with_exceptions
Slot already takenAn annual/term/one-time component's charge-once slot is already billed elsewhereLine outcome slot_unbilled — the item still succeeds, just without that lineDocumented in the obligation's calc detail
Already existsA live obligation already exists for the (student, period)Outcome already_exists, no duplicate writeIdempotent
Cancelled existsThe (student, period) slot holds a cancelled obligationOutcome cancelled_exists — the slot stays free unless explicitly reissuedNo new obligation
Session lockedlockStructureActivation is held exclusively (an activation/revision in progress)The run is refused or waits, never races the structure changeFEE_SESSION_LOCKED
Run retriedPOST .../retry on a failed/completed_with_exceptions runOnly failed items re-run, under a bumped leaseGenerationNew chunk jobs, same run row
Run cancelledPOST .../cancel on a non-terminal runPending items stop being claimedstatus: "cancelled"

5.3 The office corrects a mistake

Summary

Adjustments, waivers, reversals, cancellations and reissues are the manual escape hatches over the otherwise system-computed fee — used for a scholarship applied late, a billing error, or a withdrawal.

Main Flow

StepActor/SystemActionResultSource
1OfficePOST /api/fees/student-fees/:id/adjustments (or /waivers) with an amount, label, reason, and the version last readA manual fee_obligation_lines row, origin: "manual"fee-student-fee-actions.service.ts
2BackendRecomputes totals from the LIVE line set, checked at commit by the deferred constraint triggerfee_obligations totals updated; net < 0 is refusedfee_obligations_guard, FEE_NET_NEGATIVE
3BackendWrites a fee_obligation_events row with before/after amountsAudit trailrecordObligationEvent
4BackendNotifies the family and/or office per the notification matrixfees.fee_adjusted, .adminfee-notifier.service.ts

Branches and Edge Cases

BranchConditionBehaviorError/Result
Stale versionThe version in the request no longer matchesRefused before any writeFEE_VERSION_CONFLICT
Finalized obligationThe obligation already finalizedAdjustments and waivers are still allowed (finalization freezes the CALCULATION, not manual lines) — see backend doc—
Zero amountamountPaisa === 0RefusedFEE_ADJUSTMENT_INVALID
Reversal target already reversed.../adjustments/:lineId/reverse on an already-reversed lineRefused, one reversal per line (unique index)FEE_ADJUSTMENT_ALREADY_REVERSED
Cancel with payments.../cancel on an obligation with any payment entryRefused — a cancellation only ever applies to nothing-paid obligationsFEE_OBLIGATION_HAS_PAYMENTS
Cancel without paymentsCancel on an unpaid obligationLifecycle → cancelled, its charge-once lines released, and the released once-only charge is re-offered — see belowfee_obligations_cancel_complete requires a 3–1000 character reason
Reissue.../reissue on a cancelled obligationA brand-new obligation for the same (student, period), reissuedFromObligationId setSlot re-taken

Cancelling a fee that held a charge-once (annual/term/one-time) line releases that line, then looks for somewhere the released charge can land. If the student has another OPEN fee in the same academic session, that fee is re-priced so it picks the charge up. If not, and a later period of the session still has no live fee for the student, nothing is recorded — the anchor rule means that later period takes the charge when it is generated, and recording slot_unbilled here would invite an office user to bill the family a second time. Only when neither of those is true — no open fee can take it, and no later period is still pending — does the cancellation record a slot_unbilled event for an administrator to resolve by hand.

6. Admin Flows

6.1 Activating a fee structure

Route: POST /api/fees/structures/:id/activate. Permission: FeeStructureActivation_UPDATE.

Side effects: an integration_change_events row (fee_structure.activated), an admin notice (fee_structure_changed), and the structure's dates and session become fixed by the header guard trigger — nothing returns to draft.

6.2 Revising an active structure

Route: POST /api/fees/structures/:id/revisions/preview then POST /api/fees/structures/:id/revisions. Permission: FeeStructureActivation_UPDATE.

The preview shows what would change without writing anything; the commit creates a new immutable version (versionNo + 1, its own effectiveFrom) and launches a structure_revision generation run that re-prices every OPEN obligation the new version now governs. Already-finalized obligations are untouched — this is why finalization exists.

6.3 Ending a scholarship or charge

Route: POST /api/fees/{scholarships,charges}/:id/end. Permission: FeeScholarships_UPDATE/FeeCharges_UPDATE.

Applicability is by date, not by status — a period whose evaluation date is on or before effective_until still receives the modifier even after it is ended.

6.4 Recording a manual payment

Route: POST /api/fees/student-fees/:id/payments. Permission: FeePayments_CREATE.

Idempotent on an Idempotency-Key header (stored as idempotency_key + request_hash); a retried request with the same key and the same body returns the original result rather than double-recording. FeeLedgerService is the only writer of fee_payment_entries and of fee_obligations.paid_paisa, and a deferred trigger proves the two agree at commit.

6.5 Downloading a payment receipt

Routes: GET /api/fees/payments/:paymentId/receipt (office, FeeReceipts_READ), GET /api/mobile/fees/children/:studentId/payments/:paymentId/receipt (guardian) and GET /api/mobile/fees/me/payments/:paymentId/receipt (pupil), the last two under MyFees_READ.

When the accounting application reports a payment, it issues a receipt. A payment entry whose receiptAvailable is true can be downloaded by the office, and by the pupil or guardian it belongs to, as a PDF the school fetches from accounting on request and relays; the school stores no copy. A client shows the download action only when receiptAvailable is true. An entry the office recorded by hand, a correction, a reversal, a cancelled fee, or any entry while receipt download is switched off has no receipt. A guardian can download only their own children's receipts; a pupil only their own; anything else answers 404, indistinguishable from "no receipt". If accounting is down, slow or answers something other than a PDF, the caller gets a 503 and can try again later.

7. Lifecycle and State Transitions

7.1 Fee structure

FromEvent/ActionToGuard ConditionSide Effects
draftActivateactive≥1 target, ≥1 lineVersion 1 snapshot
activeDeactivateinactive—No new obligations resolve against it (existing ones unaffected)
inactiveReactivateactive——
active/inactiveArchivearchivedNot referenced by an open obligation's CURRENT structure (existing obligations keep their version reference regardless)Terminal
activeReviseactive (new version)—New fee_structure_versions row, structure_revision run

7.2 Scholarship / charge (modifier)

FromEvent/ActionToGuard ConditionSide Effects
draftActivateactiveValue shape, base scope, eligibility rules valid—
activeEndendedNo finalized line dated after the chosen end dateeffective_until set, modifier_end run
endedArchivearchivedNo open obligation still carries a live line for itTerminal
draftDelete(removed)Draft onlyHard delete

7.3 Student fee obligation

FromEvent/ActionToGuard ConditionSide Effects
openPeriod starts (sweep)finalizedfinalized_at stampedCalculation freezes; payments/adjustments/cancellation still allowed
openCancelcancelledNo payment entries, reason 3–1000 charsCharge-once lines released, slot freed
finalizedCancelcancelledSameSame
cancelledReissuea NEW open obligation—reissued_from_obligation_id links them

payment_status is a GENERATED column, never written directly: cancelled (lifecycle) → waived (net 0, paid 0, a waiver exists) → nothing_due (net 0, paid 0, no waiver) → unpaid → partially_paid → paid → overpaid.

7.4 Generation run

A run's counters are always RECOUNTED from its items, never incremented blindly, and it finalizes only when planning_completed_at is set and no item is pending — whichever of the last plan batch, the last chunk, or the watchdog observes that state last.

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeNotifications
Generate feesfee_generation_runs, items, fee_obligations, fee_obligation_lines, fee_obligation_events—GENERATION_PLAN, GENERATION_CHUNK—fee_generated (family), bulk_completed/bulk_exception (admin)
Activate/revise structurefee_structures, fee_structure_versions(_lines)—structure_revision run on a revision—fee_structure_changed (admin)
Apply a scholarship/chargefee_modifiers, fee_modifier_assignments—modifier_apply/modifier_end run—scholarship_applied/charge_applied (family), scholarship_review_required (admin, not-evaluated pupils)
Record a paymentfee_payment_entries, fee_obligations.paid_paisa—CHANGE_FANOUT (change feed)—payment_received/payment_partial/payment_full (family), payment_status_changed (admin)
Adjust/waivefee_obligation_lines, fee_obligation_events—CHANGE_FANOUT—fee_adjusted (family)
Cancelfee_obligations, releases lines—CHANGE_FANOUT—fee_cancelled (family)
Payment sync intakefee_payment_sync_events, fee_external_transactions, fee_payment_entries—PAYMENT_SYNC—sync_failed/payment_unmatched/sync_conflict (admin)
Reminders (sweep)fee_reminder_log———reminder_before_due/reminder_due/reminder_overdue (family)

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Ambiguous placementTwo structures or two placements tie on specificity/dateItem fails, run completed_with_exceptionsOffice fixes the structure targeting, retries the runfee-resolution.service.ts
Session lockedAn activation/revision holds the exclusive session lock while a run tries to startFEE_SESSION_LOCKEDRetry after the lock releasesfee-locks.util.ts
Payment sync failureThe final attempt of FeeJob.PAYMENT_SYNC throwsEvent marked failed, office notified (sync_failed) — never left pending foreverOffice resolves manually via /api/fees/sync-events/:id/resolve or retriesFeeQueueProcessor.onFailed
Held unmatched paymentexternalRef matches no mappingfee_external_transactions.state = held_unmatchedThe daily sync_rematch pass, or a mapping created later, resolves it automaticallyfee-sync-rematch.service.ts
Receipt unavailableA receipt download while accounting is unreachable, slow, or returns something other than a PDF503 ACCOUNTING_RECEIPT_UNAVAILABLE; receiptAvailable is unchanged because it reflects the school's records, not accounting's livenessTry again lateraccounting-receipt.client.ts
No receipt for a paymentA receipt request for an office-recorded entry, a correction, a cancelled fee, another family's payment, or while receipt download is off404 FEE_RECEIPT_NOT_AVAILABLENone; do not request a receipt when receiptAvailable is falsefee-receipt-resolver.service.ts
Net would go negativeAn adjustment/waiver would take netPaisa below 0Refused before any writeFEE_NET_NEGATIVE — the office enters a smaller amountfee-calculation.util.ts

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
Fee period preparationGlobal uniqueness of (bsYear, bsMonth) across every sessionOfficePOST /api/fees/periods/prepareA period can never be billed by two sessionsComposite unique constraintfee-periods.ts
Due date editEditing a period's due date cascadesOfficePATCH /api/fees/periods/:idEvery non-cancelled obligation of the period gets the new due dateFeeObligationActionsService.propagateDueDate, in the same transactionfee-periods.service.ts
Term overlapTwo terms of one session can never overlapOfficePOST /api/fees/termsA second overlapping term is refusedfee_terms_no_overlap EXCLUDE (btree_gist)0044_fee_management_integrity.sql
Percentage component baseA percentage component is a percentage of the FLAT subtotal only, never of another percentageSystemEvery calculationPrevents compoundingfee-calculation.util.ts
Scholarship on an absent componentA scholarship scoped to a component the pupil does not haveSystemCalculationA zero, capped: true line — explainable, not silently missingfee-calculation.util.ts
Charge on an absent componentSame, for a chargeSystemCalculationA zero line, capped: false (charges are never capped)fee-calculation.util.ts
Charge-once anchorAn annual/term/one-time component bills on the EARLIEST eligible period, regardless of generation orderSystemResolutionA period generated out of order does not double-bill or skipfee-charge-once.util.ts
Mid-period joinA pupil placed mid-periodSystemResolutioncharge_period: billed from the join date. next_period: nothing owed this periodfee_settings.mid_period_join_policy
WithdrawalA pupil's placement endsWorker (sweep, placement_review)Recalculationcancel_unpaid_open: unpaid open obligations for periods after withdrawal are cancelled. flag: left for manual reviewfee_settings.withdrawal_policy
Section changeA pupil moves sections mid-sessionWorkerRecalculationkeep: existing obligations keep their old placement snapshot. recalculate_open: open obligations are re-priced against the new section's structurefee_settings.section_change_policy
RoundingEvery computed (percentage) amount rounds to the configured unit and modeSystemCalculationFlat amounts never round — entered exactlyfee-rounding.util.ts
Scholarship stackingstack: every non-grouped scholarship applies. highest_only: only the single largest scholarship across ALL candidates appliesSystemCalculationChanges which scholarships a pupil actually receivesfee_settings.scholarship_stacking
Stack groupScholarships sharing a stackGroup (scholarships only)SystemCalculationOnly the largest candidate in the group survives; the rest are recorded as dropped_scholarships with reason stack_groupfee-calculation.util.ts
Gross capmaxScholarshipBp bounds total scholarships as a percentage of grossSystemCalculationKept candidates are granted amount-DESC, id-ASC, each capped by its own remaining scope AND the shared remaining gross capfee-calculation.util.ts
Idempotent paymentAn Idempotency-Key header on a manual paymentOfficePOST .../paymentsA retried identical request returns the original result, never double-recordsfee_payment_entries_idempotency_unique
Idempotent sync eventAn accounting client's own eventIdAccounting appPOST /api/integration/fees/paymentsA replayed event returns replayed: true, no second writefee_payment_sync_events_client_event_unique
Reminder de-duplicationOne reminder slot (before/due/after/repeat:<k>) per obligation, everWorkerSweep reminders passA re-run sweep can never send the same slot twicefee_reminder_log primary key
Consolidated notificationsfee_settings.consolidate_notificationsSystemEvery family noticeA guardian with several children gets one message per event, not one per child, when enabledfee-notifier.service.ts
Public CSV export capFEE_EXPORT_TOO_LARGEOfficeGET /api/fees/exports/student-feesA filter matching too many rows is refused rather than streaming an unbounded filefee-exports.service.ts

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
A student fee obligation's net can never be negativeA school never owes a family credit through this systemAn adjustment/waiver that would do so is refusedcalculateFeeFEE_NET_NEGATIVENo write occursfee-calculation.util.spec.ts
A percentage component is of the flat subtotal onlyPrevents compounding percentages into an unpredictable totalThe fee is easy to explain to a parentfee-calculation.util.ts step 1basePaisa on the response line shows what it was taken of—§"percentage component" tests
Scholarships are measured before charges, charges never after scholarships are subtractedCharges are punitive/administrative and should not shrink because a scholarship was grantedA late fee is the same whether or not a pupil has a scholarshipfee-calculation.util.ts step 5 comment——"charges are measured on gross" test
Charge-once components bill on the earliest eligible periodA component marked annual/term/one_time must bill exactly once, however runs are orderedThe family is billed once, predictablyfee-charge-once.util.ts, fee-resolution.service.tsManifests as slot_unbilled on a later periodUnique partial indexes make a double-bill a DB-level impossibility"generated in order P3, P1, P2 charged on P1 only"
A cancellation is only allowed with zero paymentsCancelling a paid fee would leave the ledger inconsistent (money received for nothing owed)Money already paid must go through a payment reversal first, not a cancellationfee-student-fee-actions.service.tsFEE_OBLIGATION_HAS_PAYMENTS——
A finalized obligation's calculation never recalculatesOnce a period has started, the bill is the bill — a later structure edit should not silently change a bill already sentManual adjustments/waivers still work; nothing else recomputesfee_obligations_guard trigger—23514 on any attempt—
record_status is never a fee eligibility signalIt is a record-keeping toggle on students/student_enrollments, not the enrolment lifecycleAn administrative toggle can never silently exempt or re-bill a pupilfee-resolution.service.ts placement query———

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
GenerateSame run requested twiceSame requestKeySecond request returns the first run, no duplicate201 with the same runfee-run-launcher.service.ts
GenerateStructure has no line for the pupil's gradeNo matching targetItem not_applicableItem counted, not a failurefee-resolution.service.ts
AdjustConcurrent adjustment from two tabsTwo requests read the same versionSecond write refusedFEE_VERSION_CONFLICTfee-locks.util.ts
PaymentOverpaymentpaidPaisa > netPaisaAllowed, payment_status: "overpaid"Shown to the family as an overpayment, not an errorfee-calculation.util.ts mirror
ReminderObligation already fully paid before the due-date reminder firesSweep reads balance at send timeNo reminder sent (nothing owed)Silent skipfee-reminders.service.ts
SyncTwo payment events name the same externalRef with conflicting amountsSecond event's identity disagrees with the first's resolutionheld_conflict, admin notifiedsync_conflict notice, requires manual resolutionfee-sync-matcher.ts
PortalA guardian's child is later unlinkedGuardian re-requests the child's feeRow-scope query returns nothing404fee-guardian-portal.service.ts

13. Completion Checklist

  • Every catalogue, structure, modifier, obligation, run, payment, sync, portal, settings, and reporting capability is listed.
  • Every actor (office, pupil, guardian, accounting application, worker) has allowed and forbidden behavior stated.
  • Major user, admin, and worker flows include steps, branches, and diagrams.
  • Every lifecycle (structure, modifier, obligation, run) has a transition table and state diagram.
  • Flows link to the API and backend docs.
  • The calculation engine's business rules are traced to enforcing code and tests.

See Also

  • Backend doc: /docs/developer/fees/backend
  • API doc: /docs/developer/fees/api