Skoolsewa - Ecommerce Docs
Developer ResourcesSchool

School Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for departments, designations, and the school profile.

School Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Backend doc/docs/developer/school/backend and the services it documents (lookups.service.ts, person-classifications.service.ts, school-profile.service.ts)Business behavior, cache behavior, and delete-guard side effects.
API doc/docs/developer/school/api and the controllers it documentsRoute surface, actors, permissions, and response-visible behavior.
Schemapackages/db/src/schema/school/lookups.ts, packages/db/src/schema/school/school-profile.ts, packages/db/src/schema/school/people.ts, packages/db/src/schema/identity.tsConstraints that drive the edge cases and error branches below.

2. Feature Summary

FieldValue
ModuleSchool (departments, designations, person classifications, school profile)
SubmoduleDepartments, Designations, PersonClassifications, SchoolProfile — four independently permissioned areas served by two backend modules
Primary user valueGives the office a school-owned vocabulary for who works where and as what, a shared national reference for the caste/ethnic group and mother tongue a pupil or employee is recorded against, and a single place to declare the facts (name, address, branding, currency, timezone, academic year) every other screen in the product assumes about the school.
ActorsAdmin for every mutation and for departments/designations/school-profile reads; staff and teachers additionally hold read-only access to the two person-classification lists, because both the pupil and the staff people-forms render them as select boxes. No guest or unauthenticated actor, and no mobile-facing route, touches this module.
Main entry pointsGET/POST/PATCH/DELETE /api/departments, GET/POST/PATCH/DELETE /api/designations, GET/POST/PATCH/DELETE /api/lookups/ethnicities, GET/POST/PATCH/DELETE /api/lookups/mother-tongues, GET/PATCH /api/school-profile.
Main outputsPersisted departments/designations/ethnicities/mother_tongues/school_profile rows; the response DTOs consumed by department/designation admin screens, the Teachers screen's filter, staff-creation dropdowns, the pupil and staff demographic forms' ethnicity/mother-tongue selects, and every screen that displays the school's identity.
Related docsAPI, Backend.

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
Guest (unauthenticated)Nothing.Everything in this module.None — and none is accepted.Every route requires a valid JWT; there is no @Public() route anywhere in LookupsModule or SchoolProfileModule.
Logged-in user without the relevant permissionNothing module-specific beyond whatever their own role already grants elsewhere.List, create, update, or delete departments/designations/classifications; read or update the school profile.JWT + active role, but lacking Departments_*/Designations_*/PersonClassifications_*/SchoolProfile_*.Refused with 403 PERMISSION_INSUFFICIENT (or 403 AUTH_ACTIVE_ROLE_REQUIRED if they hold several roles and have not chosen one).
Staff or teacher (any administrative role, not necessarily an "admin" screen operator)List and search the ethnicity and mother-tongue lists — nothing else in this module.Create, rename, retire, or delete a classification entry; anything on departments, designations, or the school profile unless separately granted.JWT + active role with PersonClassifications_READ, seeded onto both staff and teacher roles.The only _READ permission in this module granted outside an explicit admin role — see 12.3 for why. Both the pupil and staff demographic forms render these two lists as select boxes, so a colleague without this permission would see two empty boxes with no way to tell it is a permissions problem.
Admin holding the relevant _READ permissionList and search departments/designations/classifications; read the school profile.Create, update, or delete anything in this module.JWT + active role with Departments_READ/Designations_READ/PersonClassifications_READ/SchoolProfile_READ.Read access is granted independently per entity — an admin could hold SchoolProfile_READ without Departments_READ.
Admin holding _CREATE/_UPDATE/_DELETEThe corresponding mutation.Anything outside the permissions actually granted — e.g. Departments_UPDATE alone does not permit Departments_DELETE.JWT + active role with the specific action permission.Every action is its own permission code — there is no single "manage departments" grant that implies all four. Unlike _READ, every PersonClassifications write code is administrator-only.
SuperadminEverything in this module, unconditionally.Nothing is withheld.JWT + a role flagged is_superadmin.The bypass is keyed on the boolean flag, never on a role's display name.
Worker/systemNo automated actor touches this module.Everything, since none exists.N/ANo job, scheduler, or event consumer reads or writes departments, designations, classifications, or the school profile — verified against the backend doc's Sections 9-10 (BullMQ and Realtime, both "not applicable").

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
List/search departmentsAdminAdmin (Departments_READ)GET /api/departmentsdepartments, correlated designations count8.1
Create a departmentAdminAdmin (Departments_CREATE)POST /api/departmentsdepartments (name pre-check)departments8.2
Rename/re-describe/retire a departmentAdminAdmin (Departments_UPDATE)PATCH /api/departments/:publicIddepartmentsdepartments8.3
Delete a departmentAdminAdmin (Departments_DELETE)DELETE /api/departments/:publicIddepartments, designations (existence check)departments8.4
List/search/filter designations (including "teaching only")AdminAdmin (Designations_READ)GET /api/designationsdesignations joined to departments; correlated staff count8.5
Create a designation under a departmentAdminAdmin (Designations_CREATE)POST /api/designationsdepartments (parent), designations (name pre-check)designations8.6
Rename/retoggle-teaching/retire a designationAdminAdmin (Designations_UPDATE)PATCH /api/designations/:publicIddesignationsdesignations8.7
Delete a designationAdminAdmin (Designations_DELETE)DELETE /api/designations/:publicIddesignations, staff (existence check)designations8.8
List/search caste and ethnic groupsAdmin/Staff/TeacherStaff or teacher (PersonClassifications_READ)GET /api/lookups/ethnicitiesethnicities (cache or table)8.11
Add a caste or ethnic groupAdminAdmin (PersonClassifications_CREATE)POST /api/lookups/ethnicitiesethnicities (name pre-check)ethnicities8.12
Rename/retire an ethnicity entryAdminAdmin (PersonClassifications_UPDATE)PATCH /api/lookups/ethnicities/:publicIdethnicitiesethnicities8.13
Delete an unused ethnicity entryAdminAdmin (PersonClassifications_DELETE)DELETE /api/lookups/ethnicities/:publicIdethnicities, users (existence check)ethnicities8.14
List/search mother tonguesAdmin/Staff/TeacherStaff or teacher (PersonClassifications_READ)GET /api/lookups/mother-tonguesmother_tongues (cache or table)8.15
Add a mother tongueAdminAdmin (PersonClassifications_CREATE)POST /api/lookups/mother-tonguesmother_tongues (name pre-check)mother_tongues8.16
Rename/retire a mother-tongue entryAdminAdmin (PersonClassifications_UPDATE)PATCH /api/lookups/mother-tongues/:publicIdmother_tonguesmother_tongues8.17
Delete an unused mother-tongue entryAdminAdmin (PersonClassifications_DELETE)DELETE /api/lookups/mother-tongues/:publicIdmother_tongues, users (existence check)mother_tongues8.18
Read the school profileAdminAdmin (SchoolProfile_READ)GET /api/school-profileCache, or school_profile on a missCache (miss only)8.9
Update the school profileAdminAdmin (SchoolProfile_UPDATE)PATCH /api/school-profileschool_profileschool_profile, cache invalidation8.10
Filter the Teachers screenAdminAdmin (Designations_READ + Students/Staff permissions elsewhere)GET /api/designations?isTeaching=true, then the staff list filtered on the matching designation idsdesignations.is_teaching3
Populate a department/designation dropdownAdmin (indirect)Any admin creating/editing a staff recordGET /api/departments?pagination=false&isActive=true, then GET /api/designations?departmentId=...&pagination=false&isActive=trueSame as the list endpoints above8.1, 8.5
Populate a pupil/staff form's ethnicity and mother-tongue selectsStaff/Teacher (indirect)Any staff or teacher creating/editing a person recordGET /api/lookups/ethnicities?pagination=false&isActive=true, then GET /api/lookups/mother-tongues?pagination=false&isActive=trueSame as the list endpoints above8.11, 8.15

5. User-Facing Flows

5.1 List and filter departments

Summary

An admin opens the department management screen, which loads every department (or a search-filtered, paginated subset) to display in a table, with an active/retired toggle.

Preconditions

  • Valid JWT with an active role holding Departments_READ.
  • No feature flag or prior state required — the list works identically on an empty table (returns an empty array) and a populated one.

Main Flow

StepActor/SystemActionResultSource
1AdminOpens the department screen, optionally types a search term or toggles active/retired.GET /api/departments?search=...&isActive=... fires.Controller.
2BackendEscapes the search term, builds the WHERE clause, orders by name or updatedAt with an id tie-breaker.Rows selected, each with a live designationCount.Service.
3BackendReturns the page plus count/currentPage/totalPage when paginated.Table renders.Backend doc §7.1.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Empty searchsearch=""Filter dropped entirely — QueryDto's transform turns an empty string into undefined.Full unfiltered list, not zero rows.
Wildcard-looking searchsearch contains % or _Escaped before entering the ILIKE pattern.Matches the literal characters, not a wildcard.
No departments existEmpty tabledata: [], count: 0.Empty state, not an error.
Unrecognized sort valuee.g. sort=codeSilently falls back to updatedAt.No error — just a different (likely unintended) order.
Large page sizesize=500Clamped to 100.No error — smaller page than requested, silently.
Missing permissionRole lacks Departments_READRefused before any query runs.403 PERMISSION_INSUFFICIENT.

5.2 Create a department

Summary

An admin adds a new department from the management screen's "add" action, supplying a name and optionally a short code and description.

Preconditions

  • Departments_CREATE permission.
  • No existing department with the same name, case-insensitively.

Main Flow

StepActor/SystemActionResultSource
1AdminSubmits the "add department" form.POST /api/departments.Controller.
2BackendTrims input, checks the name is free.Proceeds or rejects with 409.Service.
3BackendInserts the row, clears both lookup cache prefixes.New department persisted and immediately visible to the next matching GET.Service.
4BackendReturns the created DepartmentDto with designationCount: 0.Screen shows the new row.Backend doc §7.2.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Validation failurename missing/too long, or an unknown field presentRejected before the service runs.400 VALIDATION_FAILED.
Duplicate nameCase-insensitive clash caught by the pre-checkRejected with a named reason.409 DEPARTMENT_NAME_TAKEN.
Concurrent duplicateTwo identical creates race past the pre-checkThe loser hits the database's own unique index.409 RESOURCE_ALREADY_EXISTS — a different, generic code from the one above, purely because of timing.
Double-submit (no idempotency key)Admin double-clicks "create"Two departments are created unless the name collides.No dedicated protection — see the tradeoff noted in the API doc.

5.3 Retire or reactivate a department

Summary

An admin sets isActive on a department without deleting it — the recommended way to remove a department from active use while any staff or designation still references it.

Preconditions

  • Departments_UPDATE permission.
  • The department exists.

Main Flow

StepActor/SystemActionResultSource
1AdminToggles the department's active switch.PATCH /api/departments/:publicId {"isActive": false}.Controller.
2BackendUpdates only isActive and updatedAt.Row retired; every existing designation and staff reference is untouched.Service.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Retiring with active childrenDepartment still has active designationsAllowed — no cascade, no block.Success; children remain untouched and still resolve.
ReactivatingisActive: true on a retired departmentAllowed unconditionally.Success — the department reappears in active-only pick lists.
Department not foundpublicId deleted or never existed404 DEPARTMENT_NOT_FOUND.

5.4 Delete a department

Summary

An admin permanently removes a department created in error. Blocked while any designation — active or retired — still belongs to it.

Preconditions

  • Departments_DELETE permission.
  • No designation currently has department_id pointing at this department.

Main Flow

StepActor/SystemActionResultSource
1AdminChooses "delete" on a department row.DELETE /api/departments/:publicId.Controller.
2BackendChecks for any referencing designation.Blocks or proceeds.Service.
3BackendDeletes the row if unblocked.Department removed permanently.Service.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Referenced by a designationAt least one designations row points hereRefused with an actionable reason.409 DEPARTMENT_HAS_DESIGNATIONS.
Referenced only by staff.department_id directly (no designation)A staff member is assigned to the department with no designation setNot caught by this check — the guard only inspects designations. The delete proceeds to the database, where the staff.department_id ON DELETE restrict foreign key is the only remaining backstop and would surface as an unmapped 500/23503 rather than a friendly 409.See the risk noted in the backend doc — this is a real gap in the service-level guard, not a documentation gap.
Not foundAlready deleted or invalid publicId404 DEPARTMENT_NOT_FOUND.
Retired departmentisActive: falseDeletable exactly like an active one — retirement status has no bearing here.Same rules as above apply.

5.5 List, filter, and search designations (including the Teachers screen's query)

Summary

An admin (or the Teachers screen, indirectly) lists designations, optionally scoped to one department, filtered to teaching-only titles, and/or filtered to active-only.

Preconditions

  • Designations_READ permission.

Main Flow

StepActor/SystemActionResultSource
1Admin/Teachers screenRequests the list, typically with isTeaching=true for the Teachers screen.GET /api/designations?....Controller.
2BackendJoins departments for the name, computes a live staffCount per row.Rows returned with department context.Service.
3ConsumerUses the returned designation ids to filter the staff list — there is no separate Teachers table or endpoint.Teachers screen shows staff whose designation is a teaching one.Concepts §3.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
staffCount vs. deletability disagreementA designation's only holders have all been soft-deleted from staffstaffCount reports 0 (the count excludes soft-deleted staff), but the designation cannot be deleted (the delete-guard does not exclude soft-deleted staff).No error on the list — the disagreement only surfaces if the admin then tries to delete a designation the list showed as "unused."
departmentId for a nonexistent departmentFilter value does not match any real departmentEmpty result, not an error.data: [].
Designation under a retired departmentParent department has isActive: falseStill listed normally — the list does not hide or flag designations whose parent is retired.No special handling.

5.6 Create a designation

Summary

An admin adds a new designation under a specific department, deciding at creation time whether it counts as a teaching title.

Preconditions

  • Designations_CREATE permission.
  • The named departmentId exists (retired departments are accepted too — see edge cases).
  • No existing designation with the same name in the same department, case-insensitively.

Main Flow

StepActor/SystemActionResultSource
1AdminSubmits the "add designation" form, choosing a department, a name, and whether it is teaching.POST /api/designations.Controller.
2BackendConfirms the department exists, then checks the name is free within that department.Proceeds or rejects.Service.
3BackendInserts the row.New designation persisted, staffCount: 0.Service.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Department missingdepartmentId does not existRejected before the name check runs.404 DEPARTMENT_NOT_FOUND.
Name taken in that departmentCase-insensitive clash scoped to departmentIdRejected with a named reason.409 DESIGNATION_NAME_TAKEN.
Same name, different departmente.g. "Coordinator" in both Science and ArtsAllowed — uniqueness is per department, not global.Success in both.
Department is retireddepartmentId points to an isActive: false departmentAllowed — the existence check does not filter on isActive.Success; the designation is created under a department nobody can currently pick fresh in most dropdowns (which typically filter isActive: true), a state worth flagging to the admin in the UI even though the API permits it.
isTeaching omittedNo value suppliedDefaults to false.The designation will not surface staff on the Teachers screen until explicitly retoggled.

5.7 Rename, retoggle, or retire/reactivate a designation

Summary

An admin edits an existing designation's name, flips its teaching flag, or retires/reactivates it. The department it belongs to can never be changed through this flow.

Preconditions

  • Designations_UPDATE permission.
  • The designation exists.

Main Flow

StepActor/SystemActionResultSource
1AdminEdits the designation's name and/or teaching flag and/or active flag.PATCH /api/designations/:publicId.Controller.
2BackendRe-checks the name only if it actually changed case-insensitively; applies the rest unconditionally.Row updated.Service.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Attempting to move departmentBody includes departmentIdRejected before the service even runs — the field does not exist on the update DTO, and the global validator forbids unknown fields.400 VALIDATION_FAILED (a generic message, not the more specific DESIGNATION_DEPARTMENT_IMMUTABLE code, which is declared but never thrown).
Turning off isTeaching for a currently-staffed designationStaff already hold the designationAllowed instantly — no cascade to staff.Those staff simply stop appearing on the Teachers screen going forward; their staff row is untouched.
Retiring a staffed designationisActive: false while staff hold itAllowed — retirement never requires reassignment first.Success; only hard delete is blocked while referenced.
Renaming to the same name, different casee.g. "Teacher""TEACHER"Treated as a no-op rename, not a clash.Success.

5.8 Delete a designation

Summary

An admin permanently removes a designation created in error. Blocked while any staff row — including a soft-deleted (former) staff member — still references it.

Preconditions

  • Designations_DELETE permission.
  • No staff row, active or soft-deleted, has this designation_id.

Main Flow

StepActor/SystemActionResultSource
1AdminChooses "delete" on a designation row.DELETE /api/designations/:publicId.Controller.
2BackendChecks for any referencing staff row, active or soft-deleted.Blocks or proceeds.Service.
3BackendDeletes the row if unblocked.Designation removed permanently.Service.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Held by active staffAt least one non-deleted staff row references itRefused.409 DESIGNATION_IN_USE.
Held only by soft-deleted staffEvery referencing staff row is soft-deleted (a former employee)Still refused — the check deliberately does not filter on staff.deletedAt, so a former staff member's historical record can never be left pointing at a deleted designation.409 DESIGNATION_IN_USE, even though the list endpoint's staffCount may show 0 for the same designation — see 5.5.
Not foundAlready deleted or invalid publicId404 DESIGNATION_NOT_FOUND.

5.9 Read the school profile

Summary

Any admin screen that needs the school's identity, contact details, branding, currency, or timezone reads the single profile row — cached for up to an hour after the first read following any change.

Preconditions

  • SchoolProfile_READ permission.

Main Flow

StepActor/SystemActionResultSource
1Admin/screenLoads a screen that displays school details.GET /api/school-profile.Controller.
2BackendChecks the cache first.Returns the cached value, or falls through to the database on a miss and repopulates the cache.Service.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Cache hitValue present and TTL not expiredNo database round trip.Fast path.
Cache miss (TTL expired, or just invalidated by a recent write)No valueDatabase read, cache repopulated.Correct but one extra query.
Redis unavailableConnection error on read or writeFail-soft — treated as a miss on read, and a write failure is logged but does not fail the request.The request still succeeds with correct data either way.
Row 1 missingDatabase not fully migratedDocumented as unreachable in a normally-migrated database.404 SCHOOL_PROFILE_NOT_INITIALIZED.

5.10 Update the school profile

Summary

The office edits any subset of the school's facts — name, contact details, branding colors, currency, academic year start month, or timezone — from a settings screen.

Preconditions

  • SchoolProfile_UPDATE permission.

Main Flow

StepActor/SystemActionResultSource
1AdminEdits one or more fields on the settings form.PATCH /api/school-profile {...}.Controller.
2BackendApplies exactly the supplied fields to row 1.Row updated.Service.
3BackendClears the cache.The very next read is guaranteed fresh.Service.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Partial updateOnly one field suppliedEvery other field is left untouched.Success — no accidental resets.
Invalid timezone stringe.g. a misspelled IANA zone nameAccepted — neither the DTO nor the column validates it is a real timezone.No error at write time; admission/employee number allocation silently misdates going forward. This is a real product risk, not a documentation gap — see the backend doc's risk register.
Invalid currency codeAny string ≤ 8 charactersAccepted — not validated against ISO 4217.Same category of silent-acceptance risk as timezone.
Invalid academic year start monthOutside 1-12Rejected by the DTO before it can reach the database's own matching CHECK.400 VALIDATION_FAILED.
Invalid hex colorNot a valid hex stringRejected.400 VALIDATION_FAILED.
Attempt to clear a field to emptySends "" for one of the twelve nullable text fields (logoUrl, registrationNumber, principalName, address, city, state, pinCode, phone, email, website, brandPrimaryColor, brandSecondaryColor)Trimmed and written as NULL — this is the intended, and only, way to clear one of these fields back to "not set."Success; the field renders as unset (e.g. the "no logo" placeholder) rather than an empty string.
Attempt to clear name, currencyCode, or timezone to emptySends "" (or all-whitespace) for one of these three NOT NULL columnsThe blank is dropped from the write entirely — the existing value is left untouched, exactly as if the field had been omitted.Success on the request, but the field is silently unchanged — this is deliberate: a blank timezone would break admission/employee-number allocation, which resolves the academic year through it.
Read landing in the invalidation gapA GET arrives between the UPDATE commit and the DELCould observe the pre-update cached value.Bounded by the (very short, in practice near-immediate) gap between the two statements — not the full TTL.

5.11 List and filter caste/ethnic groups or mother tongues

Summary

Any staff member or teacher — not only an administrator — opens a pupil or staff demographic form (or, less often, the classification maintenance screen) and the ethnicity and mother-tongue selects populate from these two lists.

Preconditions

  • Valid JWT with an active role holding PersonClassifications_READ — currently seeded onto staff and teacher in addition to any administrative role.
  • No feature flag or prior state required.

Main Flow

StepActor/SystemActionResultSource
1Staff/Teacher/AdminOpens a form needing the ethnicity or mother-tongue select, or the maintenance screen.GET /api/lookups/ethnicities and/or GET /api/lookups/mother-tongues, typically pagination=false&isActive=true for a select box.Controller.
2BackendChecks the cache for this exact query first.Returns the cached page, or falls through to the database and repopulates the cache.Service.
3BackendOrders by name ascending by default (not updatedAt, and not descending like every other list in this module).Rows render alphabetically, ready for a select box.Backend doc §6.3, §7.7.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Staff/teacher without PersonClassifications_READ (a custom role that excludes it)Role lacks the permissionRefused before any query runs — same as any other permission failure.403 PERMISSION_INSUFFICIENT.
Empty searchsearch=""Filter dropped entirely, same as departments/designations.Full unfiltered list.
No entries active-only filteredisActive=true on a list where every remaining entry has been retireddata: [].Empty state, not an error.
order=desc explicitly requestedCaller overrides the asc defaultHonored — the asc default only applies when order is omitted.List renders Z-to-A.
Large unpaginated readpagination=false on a list that has grown past 1000 rows (not realistic today at 56/42 seeded rows)Capped at UNPAGINATED_HARD_CAP (1000), not unbounded.Fewer rows than exist, silently, same as any other reference-table read.

5.12 Add a caste/ethnic group or mother tongue

Summary

An administrator adds an entry the seeded national list is missing — the seeded list is deliberately the broad Central Bureau of Statistics classification, not an exhaustive one, and a real school roll regularly contains someone it does not name.

Preconditions

  • PersonClassifications_CREATE permission — administrator only; staff/teacher _READ access does not extend to this.
  • No existing entry in the same list (ethnicities or mother tongues, never both) with the same name, case-insensitively.

Main Flow

StepActor/SystemActionResultSource
1AdminSubmits the "add entry" form on the classification maintenance screen.POST /api/lookups/ethnicities or .../mother-tongues.Controller.
2BackendTrims the name (before validating it is non-empty), checks it is free within the target list.Proceeds or rejects with 409.Service.
3BackendInserts the row, invalidates only that list's cache prefix.New entry persisted and immediately visible to the next GET.Service.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Name of only whitespacename: " "Trimmed before the not-empty check runs, so the blank is visible to the validator.400 VALIDATION_FAILED — never a blank row silently inserted.
Duplicate name, same listCase-insensitive clashRejected with a named reason.409 PERSON_CLASSIFICATION_NAME_TAKEN.
Same name, different liste.g. a language named the same as an ethnic groupAllowed — the two lists are entirely independent.Success in both.
Concurrent duplicateTwo identical creates race past the pre-checkThe loser hits the database's own unique index.409 RESOURCE_ALREADY_EXISTS, same timing-dependent shape as departments/designations.
Attempted by staff/teacher (read-only role)Role holds PersonClassifications_READ but not _CREATERefused before the DTO is evaluated.403 PERMISSION_INSUFFICIENT.

5.13 Rename or retire/reactivate a caste/ethnic group or mother tongue

Summary

An administrator corrects a spelling, or retires an entry the school does not use — retirement is the recommended action for "remove from new picks" rather than a hard delete, exactly as with departments/designations.

Preconditions

  • PersonClassifications_UPDATE permission.
  • The entry exists.

Main Flow

StepActor/SystemActionResultSource
1AdminEdits the entry's name and/or active flag.PATCH /api/lookups/ethnicities/:publicId or .../mother-tongues/:publicId.Controller.
2BackendRe-checks the name only if it actually changed case-insensitively; applies the rest unconditionally.Row updated.Service.

Branches and Edge Cases

BranchConditionBehaviorError/Result
Rename that only changes casinge.g. "newar""Newar"Treated as a no-op rename, not a self-clash.Success.
Retiring an entry people are recorded againstisActive: false while pupils/staff hold itAllowed — retirement never requires clearing references first, exactly like departments/designations.Success; every existing record continues to resolve.
Renaming an entry people are recorded againstAny name change, retired or notAllowed, with a real consequence: every pupil or employee already recorded against this row is retroactively understood to carry the new name, including inside a government return already filed under the old spelling.Success — this is a deliberate, documented tradeoff, not a bug; see 12.3.
Not foundpublicId deleted or never existed404 PERSON_CLASSIFICATION_NOT_FOUND.

5.14 Delete a caste/ethnic group or mother tongue

Summary

An administrator permanently removes an entry created in error. Blocked while any person — pupil or employee — is recorded against it.

Preconditions

  • PersonClassifications_DELETE permission.
  • No users row has ethnicity_id/mother_tongue_id (matching the list) pointing at this entry.

Main Flow

StepActor/SystemActionResultSource
1AdminChooses "delete" on an entry row.DELETE /api/lookups/ethnicities/:publicId or .../mother-tongues/:publicId.Controller.
2BackendChecks users for any row pointing at this entry, on the column matching the list.Blocks or proceeds.Service.
3BackendDeletes the row if unblocked.Entry removed permanently.Service.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
At least one person recorded against itAny users row with a matching foreign keyRefused — this is what stops ethnicity_id's ON DELETE set null from silently blanking a real person's recorded classification the moment the row they point at is removed.409 PERSON_CLASSIFICATION_IN_USE, with a message recommending retirement instead.
Not foundAlready deleted or invalid publicId404 PERSON_CLASSIFICATION_NOT_FOUND.
Retired entryisActive: falseDeletable exactly like an active one — retirement status has no bearing on delete eligibility, same as departments/designations.Same rules as above apply.

6. Admin Flows

6.1 Create

Departments and designations: see 5.2 and 5.6. Person classifications: see 5.12. All three require the corresponding _CREATE permission (administrator-only, even for classifications where _READ is broader), write one row, and invalidate the affected cache prefix — every list in this module is now cache-aside, so the very next matching GET reflects the write.

6.2 List / Read Detail

There is no single-resource "read detail" route for a department, a designation, or a classification entry — only the list endpoints (GET /api/departments, GET /api/designations, GET /api/lookups/ethnicities, GET /api/lookups/mother-tongues), which return every matching row including the one an admin might want to inspect individually. The school profile has no list — it is a singleton read via GET /api/school-profile.

6.3 Update

See 5.3, 5.7, 5.13, and 5.10. Every update on this module is a PATCH, every field is independently optional, and cache invalidation follows every successful write.

6.4 Reorder

Not applicable. No ordering/position column exists on departments, designations, ethnicities, mother_tongues, or school_profile — lists are sorted by name or updatedAt (classifications: name only), never by a stored manual order.

6.5 Activate/deactivate

Covered by the isActive field on departments, designations, ethnicities, and mother_tongues — see 5.3, the retire/reactivate branch of 5.7, and 5.13. This is the module's substitute for soft delete: it hides a row from active-only pick lists without breaking any existing reference, and — unlike a real soft delete — it never blocks a subsequent hard delete on its own (only an existing reference does).

6.6 Soft delete

Not applicable. None of departments, designations, ethnicities, or mother_tongues has a deleted_at column — see 7. Lifecycle and State Transitions for why that is a deliberate design choice, not an oversight. school_profile cannot be deleted at all (no DELETE route exists on it).

6.7 Restore

Not applicable, for the same reason as 6.6 — there is nothing to restore. Note that the permission catalog still generates a _RESTORE code for every module in this document, including PersonClassifications_RESTORE (every module gets every action by the catalog's own generation rule), but no route in this module ever checks any of them.

6.8 Hard delete

See 5.4, 5.8, and 5.14. All three are the module's only true deletion path for their respective entity, all are reference-guarded, and all name the exact blocking condition in the error rather than surfacing an unmapped foreign-key failure or a silent data loss (the classification case is the sharpest version of this: ethnicity_id/mother_tongue_id are ON DELETE set null, so without the guard a delete would succeed and silently blank a real person's recorded classification).

6.9 Export/import

Not owned by this module directly, but relevant: the data-transfer module's bulk import resolves department/designation names against these tables and produces IMPORT_UNKNOWN_DEPARTMENT/IMPORT_UNKNOWN_DESIGNATION when a name in an import file does not match an existing row here. This module does not implement that import — it is only the vocabulary the import validates against.

6.10 Moderation

Not applicable. Nothing in this module requires approval, review, or a moderation queue — every mutation an admin with the right permission makes takes effect immediately.

6.11 Manual retry

Not applicable. No job, queue, or async operation exists in this module to retry — every operation is a single synchronous request/response.

6.12 Settings (school profile)

See 5.9 and 5.10. This is the one "settings-shaped" flow in the module: a single row, read-cached, write-invalidated, with no create/delete lifecycle at all.

7. Lifecycle and State Transitions

Departments, designations, and both classification lists do not carry a multi-state lifecycle — isActive is a boolean flag, not a state machine, and that is a deliberate schema choice (see the backend doc's tradeoffs): a soft-deleted row never fires ON DELETE restrict/set null correctly, so it would keep resolving through every existing reference while disappearing from admin pick lists — "gone to the eye, present to the query." isActive avoids that by never touching referential integrity at all; only hard delete does, and hard delete is blocked while referenced.

EntityFromEvent/ActionToGuard ConditionSide Effects
Department/Designation/Ethnicity/MotherTongueisActive: truePATCH isActive: falseisActive: falseNone — always allowed, even with active children/references.Removed from active-only pick lists; every existing reference (designations, staff, or a person's recorded classification) still resolves normally.
Department/Designation/Ethnicity/MotherTongueisActive: falsePATCH isActive: trueisActive: trueNone — always allowed.Reappears in active-only pick lists. Deliberately not automatic on a seed re-run — seedReferenceData() never reactivates a row it finds retired.
Department/Designation/Ethnicity/MotherTongueEither isActive valueDELETERow no longer existsNo referencing row (designation for a department; staff, including soft-deleted, for a designation; any users row for a classification entry).Permanent removal; cache invalidated.

The school profile has no lifecycle at all — it is a single row that exists from the moment migration 0002 runs, is never created or deleted through the API, and only ever transitions field-by-field via PATCH.

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
List departments/designationsNone (cache hit); departments/designations (cache miss, read-only)Populates that exact query's cache entry under lookups:departments:*/lookups:designations:* on a missNoneNoneNoneNone
Create/update/delete a departmentdepartmentsClears both lookups:departments:* and lookups:designations:* — every cached page of both listsNoneNoneNoneNone
Create/update/delete a designationdesignationsClears the same two prefixesNoneNoneNoneNone
List ethnicities/mother tonguesNone (cache hit); ethnicities/mother_tongues (cache miss, read-only)Populates that exact query's cache entry under lookups:classifications:<kind>:* on a missNoneNoneNoneNone
Create/update/delete a classification entryethnicities or mother_tongues (never both)Clears only that list's lookups:classifications:<kind>:* prefixNoneNoneNoneNone
Read the school profileNone (cache hit); school_profile (cache miss, read-only)Populates school:profile on a missNoneNoneNoneNone
Update the school profileschool_profileDeletes school:profileNoneNoneNoneNone

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Name already takenCreating/renaming to a name that collides case-insensitively409 with a named error code and a human-readable message.Choose a different name.LookupsService.
Name-uniqueness raceTwo concurrent creates for the identical nameThe winner succeeds normally; the loser gets a generic 409 RESOURCE_ALREADY_EXISTS instead of the friendlier named code.Refresh and retry with a different name, or confirm the existing one is what was intended.Global unique-violation fallback in AllExceptionsFilter.
Delete blocked by a referenceDeleting a department with designations, or a designation with staff (including soft-deleted)409 naming exactly what is blocking it.Retire instead of deleting, or clear the blocking references first.LookupsService.
Classification delete blocked by a referenceDeleting an ethnicity/mother-tongue entry a person is still recorded against409 PERSON_CLASSIFICATION_IN_USE, recommending retirement instead.Retire the entry (isActive: false) rather than delete it.PersonClassificationsService.
Classification name already takenCreating/renaming to a name that collides case-insensitively within the same list409 PERSON_CLASSIFICATION_NAME_TAKEN.Choose a different name, or confirm the existing entry is the one meant.PersonClassificationsService.
Cache/Redis outageRedis unreachable during a school-profile read or writeFail-soft on every path — the request still succeeds, served from (or written straight to) the database.Automatic once Redis recovers; no manual intervention needed.RedisCacheService.
Row 1 of school_profile missingAn incompletely-migrated databaseEvery school-profile route returns 404 SCHOOL_PROFILE_NOT_INITIALIZED.Run pending migrations — the row is created by migration 0002, not a seed script.SchoolProfileService.
Permission deniedActive role lacks the route's required permission403 with a specific code distinguishing "no permission" from "no role selected" from "no role assigned."Select or request the correct role/permission.RoleGuard.

11. Diagrams Required Per Module

  • Actor capability diagram: 3. Actor Matrix and 4. Capability Matrix.
  • High-level module flow diagram: the route-ownership diagram in the API doc §9.1.
  • Sequence diagram for each major flow: provided per-flow in 5.
  • State machine diagram for the one modeled lifecycle: 7.
  • Data side-effect diagram for write flows: 9.
  • Error branch diagram: 6.8 and the API doc's §9.3.

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
List departmentsTie-break by id on every sortAdminAny GET /api/departmentsStable pagination even with duplicate updatedAt valuesNone — read-only ordering detaillookups.service.ts
List departmentsSilent fallback for an unrecognized sort valueAdminsort= anything other than nameSorted by updatedAt instead, with no errorNonelookups.service.ts
List designationsstaffCount excludes soft-deleted staffAdmin/Teachers screenAny GET /api/designationsA designation held only by former staff shows staffCount: 0None — read-onlylookups.service.ts
Create departmentEmpty code/description stored literally, not normalizedAdminPOST with code: ""Stored as "", not nullRow written with an empty stringlookups.service.ts
Update departmentEmpty code/description normalized to nullAdminPATCH with code: ""Field cleared to null — different from the create-path behavior aboveRow written with nulllookups.service.ts
Update department/designationRename to same name, different case, is a no-opAdminPATCH name: "SCIENCE" on "Science"Accepted, not rejected as a self-clashRow updated with new casinglookups.service.ts
Create designationParent department existence checked before the name checkAdminPOST /api/designations with a bad departmentId404, never reaches the name-uniqueness checkNonelookups.service.ts
Create designationAccepted under a retired departmentAdminPOST with departmentId pointing to isActive: falseSucceeds silentlyRow writtenlookups.service.ts
Update designationstaffCount in the response is a stale 0 placeholder, not re-queriedAdminAny PATCH /api/designations/:publicIdClient sees 0 even for an already-staffed designationNone — display-only mismatch until the next fresh GETlookups.service.ts
Delete designationSoft-deleted staff still block deletionAdminDELETE on a designation whose only holders have leftRefused with DESIGNATION_IN_USE despite the list showing staffCount: 0 for the same rowNone — the delete never runslookups.service.ts
Delete departmentDirect staff.department_id references (no designation) are not checkedAdminDELETE on a department with a directly-assigned staff member and no designationsThe check passes and the delete is attempted at the database, which then enforces its own FKPotential unmapped 500/23503 instead of a friendly 409lookups.service.ts + schema FK
List departments/designationsCache-aside, keyed on the full query — a filter/page/size change is a different cache entry, not a different read of the same entryAdminAny GETRepeat identical queries are served without a database round tripCache populated on the first request for a given query, cleared on any writelookups.service.ts
List person classificationsSorted by name ascending by default — the one list in this module that defaults ascendingStaff/Teacher/AdminGET /api/lookups/ethnicities or .../mother-tongues with no orderAlphabetical by default, suited to a select boxNone — read-only ordering detailperson-classification.dto.ts
Create person classificationName trimmed before the not-empty check, not afterAdminPOST with name: " "400, not a blank row silently insertedNoneperson-classification.dto.ts
Delete person classificationReferential check against users.ethnicity_id/users.mother_tongue_id, on the column matching kindAdminDELETE on an entry a person is recorded againstRefused with PERSON_CLASSIFICATION_IN_USE rather than silently blanking that person's record via the column's ON DELETE set nullNone — the delete never runsperson-classifications.service.ts
Read person classificationsPersonClassifications_READ is granted to staff and teacher roles, unlike every other _READ permission in this documentStaff/TeacherAny GET on either classification listSucceeds for a non-administrative role that would be refused on every other list in this moduleNone — read-onlyseed-auth.ts
Read school profileCache-aside with no lock — a burst of misses can each redundantly re-read and re-writeAny adminConcurrent GETs immediately after a PATCHAll correct, just briefly redundant workMultiple identical SELECT/SETEX pairsschool-profile.service.ts
Update school profileBlank on one of twelve nullable text fields clears it to NULL; blank on name/currencyCode/timezone is silently ignored insteadAdminPATCH with logoUrl: "" vs. PATCH with timezone: ""Field cleared vs. field unchanged — the same-shaped input produces two different outcomes depending which field it targetsRow written with NULL, or the patch key dropped entirelyschool-profile.service.ts
Update school profileNo validation that timezone/currencyCode are real valuesAdminPATCH with a bad timezone/currency stringAccepted; silently wrong going forwardRow written with the bad valueschool-profile.dto.ts

Rules from the format are honored above: no restore/retry/fallback/cache-miss/duplicate-action/permission-failure behavior has been grouped away, even where the underlying rule (e.g. "empty search is dropped") might look too small to mention on its own.

12.2 Business Process Diagram Pack

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
A designation is not a role.A vice-principal who still teaches, or a senior teacher granted admin access, breaks the assumption that employment title implies system capability.Admins must set both a designation and a role explicitly; there is no automatic mapping.Design invariant — no constraint links designations to roles.Neither the designation nor the staff API returns or accepts a role value.See the backend doc's invariant catalog.None.
Both tables are school-editable, not a fixed enum.Every institution carries designations an outsider would not predict; a fixed list guarantees somebody is eventually filed as "Other."Any admin with _CREATE can add a title unique to their school.Plain text columns with uniqueness constraints, not a Postgres enum type.POST bodies accept any string within length limits.departments/designations schema.None.
isTeaching decides who appears on the Teachers screen.There is no Teachers entity — building one would duplicate staff for no benefit.A designation created without isTeaching: true produces staff who teach but never appear among "teachers" until an admin retoggles it.designations.is_teaching column, GET /designations?isTeaching=true filter.The Teachers screen is a composition of this filter plus the staff module's list, not its own endpoint.designations_is_teaching_idx.None.
Name uniqueness is case-insensitive; departments globally, designations per department.Free text otherwise yields "Teacher"/"teacher"/"Sr. Teacher" as indistinguishable-but-distinct rows, breaking any staff report built on top.An admin cannot create a near-duplicate by casing alone.Expression UNIQUE indexes on lower(name), mirrored by a service-level pre-check for a friendly error.409 DEPARTMENT_NAME_TAKEN/DESIGNATION_NAME_TAKEN on a case-insensitive clash.departments_name_unique, designations_department_name_unique.None.
A designation's department is immutable once created.Moving it would violate the composite foreign key from every staff row referencing it at once.An admin who assigned the wrong department must retire and recreate, not edit in place.UpdateDesignationDto omits departmentId; global forbidNonWhitelisted rejects it if sent.PATCH with departmentId in the body always 400s.staff_designation_in_department_fk.None.
Neither lookup table soft-deletes.A soft-deleted row would keep resolving through references while vanishing from pick lists — inconsistent with what "deleted" should mean when ON DELETE restrict exists specifically to prevent silent dangling references.Deletion always requires clearing references first; retirement (isActive) is the "hide but keep" tool instead.Absence of a deleted_at column; hard-delete guards in the service.DELETE always either succeeds permanently or is refused with a named reason.Schema design choice, documented in the backend doc.None.
PersonClassifications_READ is granted to staff and teacher roles, not just administrators.Both the pupil and staff demographic forms render the ethnicity and mother-tongue lists as select boxes; a colleague who cannot read them sees two empty boxes with no way to tell it is a permissions problem rather than an empty table.Any staff member or teacher can populate these two selects; every write action stays administrator-only.Seeded explicitly onto STAFF_PERMISSIONS/TEACHER_PERMISSIONS in seed-auth.ts, the only _READ code in this document seeded that way.GET /api/lookups/ethnicities/.../mother-tongues succeed for a non-administrative role that every other list in this module would refuse.Permission catalog + seed grants.None.
Ethnicities and mother tongues are seeded national data, not a school's own invented vocabulary.Every government return (IEMIS, scholarship and free-textbook allocations, reservation quotas) is aggregated against the same national classification; two schools spelling a group differently produce two rows the ministry cannot add together.A school starts with a complete, correct baseline (56 ethnicities, 42 mother tongues) rather than typing every entry in from nothing.seedReferenceData(), called from both seed.ts and seed-prod.ts, idempotent and non-reactivating.The lists are populated from the very first GET after migration, with no empty-state onboarding flow needed.seed-reference-data.ts.None.
Renaming a classification entry retroactively changes what every already-filed government return meant.The rename rewrites the recorded classification of every pupil or employee already pointing at the row — there is no versioning of "what this row meant as of a given date."A typo correction is safe and encouraged; repurposing a row to mean a different group is not, and the documented remedy is to retire the row and create a new one instead.Not enforced by any constraint — a design invariant relying on administrator judgment, backed only by the write permission being administrator-only.PATCH accepts any rename unconditionally; the API cannot distinguish "fixing a typo" from "repurposing this row."See the backend doc's risk register.None.
The school profile is a singleton.Typed, redeployable-free school configuration in one row, replacing both environment variables and a removed key-value config table.Admins edit school facts without a deploy; there is exactly one profile per database.CHECK (id = 1) plus the PK on id.No POST/DELETE route exists on /school-profile at all.school_profile_singleton.None.
An empty string clears a nullable school-profile field to NULL; the same empty string on a NOT NULL column with a default is a no-op instead.class-validator's @IsOptional() treats an actual null in the request body as absent, so a "clear this field" UI control has only the empty string available to send; but name/currencyCode/timezone are NOT NULL with defaults, and blanking timezone specifically would break admission/employee-number allocation.A client can genuinely clear a logo, address, or contact field; the same gesture on the three protected fields is silently absorbed rather than corrupting them.SchoolProfileService.toPatch — see the backend doc's 6.2.PATCH with logoUrl: "" clears it; PATCH with timezone: "" leaves the existing timezone untouched, with no error distinguishing the two outcomes.school-profile.service.spec.ts.school-profile.service.spec.ts.
The school's timezone decides year numbering for admission/employee numbers.Asia/Kathmandu is UTC+05:45; a UTC clock would misdate numbers issued near midnight local time.An admin who mis-sets the timezone silently corrupts future numbering, with no validation to catch it.SchoolProfileService.getTimezone() is the documented read path for consumers.PATCH /school-profile accepts any string for timezone.See the backend doc's risk register.None.

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
isActive flag instead of soft delete on lookup tablesAn admin can hide a department/designation from new picks without losing history on existing staffNo deleted_at filtering needed anywhere these tables are joinedSoft delete like staffHard delete requires clearing references first — a longer pathLow — this is the safer default, not a shortcut.
Hard delete guarded by an explicit existence check, not left to the database FK aloneAn admin sees "this department still has designations," not a raw constraint-violation errorA named error code is easier to branch on client-side than parsing a Postgres errorLet ON DELETE restrict surface as an unmapped 500The department-delete guard has a known gap for direct staff.department_id references — see 12.1Moderate — a rare shape (staff assigned to a department with no designation) can still hit an unfriendly error.
Cache-aside for every list in this module — the school profile, departments, designations, and both classification listsEvery list is read on a high-traffic form (staff creation, pupil/staff demographic sections) and rarely writtenThe full query (filters, sort, page, size) is folded into the cache key via CacheKeyUtil.build, so no two distinct requests can ever share a cache entryInvalidation-only, with lists always querying live (the prior shape for departments/designations)Correctness risk of a stale read is bounded by the TTL (1 hour) and by every write clearing the relevant prefix(es) immediatelyLow — every write path already invalidates synchronously; the only residual risk is a lost invalidation during a Redis fault, which is logged.
PersonClassifications_READ granted to staff and teacher roles; every other _READ in this module stays administrator-onlyThe pupil/staff demographic forms every staff member and teacher fills out need these two selects populatedA dedicated seed grant onto STAFF_PERMISSIONS/TEACHER_PERMISSIONS, while the four write codes stay administrator-onlyKeep every code in this module administrator-only, requiring a broader role just to fill out a demographic formA non-administrative role can read (never write) national reference data it has no reason to be blocked from seeingLow — read-only, and the data is not sensitive; the write side is unaffected.
No idempotency key on create endpointsSimpler client integrationNo dedup infrastructure neededIdempotency-Key header + server-side dedupA double-click can create a duplicate department/designation/classification entry unless the name collidesLow — these are low-frequency, human-reviewed admin actions.
No format validation on timezone/currencyCodeSimpler DTO, no dependency on a timezone/currency data setLess validation code to maintainValidate against Intl.supportedValuesOf("timeZone") / ISO 4217 listA typo silently corrupts year numbering or displays a wrong currency symbolModerate — this is a real, documented risk in the backend doc's risk register, not a hypothetical.

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
List departments/designationsEmpty stateNo rows in the table yetdata: [], count: 0Empty list rendered, no errorlookups.service.ts
List departments/designationsFirst useVery first department/designation ever createdAppears on the very next list call for that exact query — a differently-filtered request cannot have a stale cache entry to serve, since no query populates another query's keyImmediate visibilityBackend doc §8 (cache keyed per query)
List person classificationsEmpty state after filteringisActive=true when every remaining entry has been retireddata: [], count: 0Empty list rendered, no errorperson-classifications.service.ts
Create department/designation/classification entryDuplicate action (double-submit)No idempotency keyTwo rows created unless names collideTwo entries appear; admin must manually clean uplookups.service.ts / person-classifications.service.ts
Create department/designation/classification entryConcurrent actionTwo admins create the same name simultaneouslyOne succeeds; the other gets a generic conflict code instead of the friendly one409 RESOURCE_ALREADY_EXISTS vs. 409 *_NAME_TAKEN, purely by timingAllExceptionsFilter
Delete classification entryDuplicate actionTwo admins delete the same entry simultaneouslyOne succeeds; the other's row is already gone404 PERSON_CLASSIFICATION_NOT_FOUND for the loserperson-classifications.service.ts
Update school profileExpired cache entryTTL elapsed since the last readTransparent — a fresh read repopulates itNo user-visible difference from a hitschool-profile.service.ts
Update school profilePermission mismatchRole lacks SchoolProfile_UPDATERefused before the DTO is even parsed against business rules403 PERMISSION_INSUFFICIENTRoleGuard
Delete departmentMissing dependency checkStaff directly assigned with no designationGuard passes, DB FK is the real (unfriendly) backstopPotential unmapped 500 instead of 409lookups.service.ts
List classificationsRead permission mismatchStaff/teacher role lacking PersonClassifications_READ (a custom role that excludes it)Refused before any query runs403 PERMISSION_INSUFFICIENTRoleGuard
List departments/designations/classificationsCache stale/missTTL elapsed, or the very first request for a given queryFresh database read, cache repopulatedNo user-visible difference from a hit, just one extra querylookups.service.ts / person-classifications.service.ts
Delete designationQueue failureN/A — no queue involved anywhere in this moduleN/AN/ABackend doc §9 ("Not applicable")
List departments/designationsUnsupported sort optionsort=nonexistentFalls back to updatedAt, no errorSilently different order than expectedlookups.service.ts
List classificationsUnsupported sort optionN/A — classification lists take no sort field at all, only orderAlways sorted by nameNo error — there is no sort field to be wrong aboutperson-classification.dto.ts

12.6 Flow-to-Data Trace

FlowReadsWritesCacheJobs/EventsResponse Fields
List departmentsCache, or departments + correlated designations count on a misslookups:departments:* (read/populate for this exact query)Noneid, publicId, name, code, description, isActive, designationCount, createdAt, updatedAt
Create/update/delete departmentdepartments, designations (delete only)departmentslookups:departments:*, lookups:designations:* (both cleared entirely)NoneSame fields as list, or null on delete
List designationsCache, or designations + departments (join) + staff (count) on a misslookups:designations:* (read/populate)Noneid, publicId, departmentId, departmentName, name, isTeaching, isActive, staffCount, createdAt, updatedAt
Create/update/delete designationdepartments, designations, staff (delete only)designationsSame two prefixes clearedNoneSame fields as list, or null on delete
List ethnicities/mother tonguesCache, or ethnicities/mother_tongues on a misslookups:classifications:<kind>:* (read/populate)Noneid, publicId, name, isActive
Create/update/delete classification entryethnicities/mother_tongues (name pre-check, lookup); users (delete only, reference check)ethnicities or mother_tonguesOnly that kind's lookups:classifications:<kind>:* prefix clearedNoneid, publicId, name, isActive, or null on delete
Read school profileCache, or school_profile on a miss— (or the cache, on a miss)school:profile (read/populate)NoneEvery SchoolProfileDto field except id/createdAt
Update school profileschool_profileschool_profileschool:profile (deleted)NoneSame as read

12.7 Experience Quality Checklist

  • The doc explains what the actor is trying to accomplish (organize staff into departments/designations; keep the school's own facts current).
  • The doc explains what the backend does that the actor does not see (name pre-checks, cache-aside on the profile, the staffCount/deletability disagreement, the direct-staff delete gap).
  • The doc covers every minor flow and branch, including ones a casual read of the controllers would group away (empty search, case-only renames, retired-department designation creation).
  • The doc includes admin and (by composition) Teachers-screen flows; there is no guest, worker, or system flow to include, and that absence is stated explicitly rather than left silent.
  • The doc explains business logic, tradeoffs, and rationale (§12.3, §12.4).
  • The doc maps every flow to API routes and backend side effects (§12.6, and every flow's Main Flow table).
  • The doc includes diagrams appropriate to each flow type (sequence per flow, one state diagram, one error-branch flowchart, one flow-to-data diagram).
  • The doc covers edge cases and failure recovery (§10, §12.5).

13. Completion Checklist

  • Every feature, minor action, and submodule capability is listed (§4, §12.1).
  • Every actor has allowed and forbidden behavior listed (§3) — including the actors that do not exist for this module (guest, worker/system), stated explicitly rather than omitted.
  • Every major and minor flow includes steps, branches, and diagrams (§5).
  • The one real lifecycle (isActive) has a transition table and state diagram; the absence of a lifecycle for the school profile is stated explicitly (§7).
  • Every flow links to the API and backend docs.
  • No claim in this document is unverified against the current source files cited in the backend and API docs.

See Also

On this page

School Features and Flows1. Documentation Evidence2. Feature Summary3. Actor Matrix4. Capability Matrix5. User-Facing Flows5.1 List and filter departmentsSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.2 Create a departmentSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.3 Retire or reactivate a departmentSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.4 Delete a departmentSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.5 List, filter, and search designations (including the Teachers screen's query)SummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.6 Create a designationSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.7 Rename, retoggle, or retire/reactivate a designationSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.8 Delete a designationSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.9 Read the school profileSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.10 Update the school profileSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.11 List and filter caste/ethnic groups or mother tonguesSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.12 Add a caste/ethnic group or mother tongueSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.13 Rename or retire/reactivate a caste/ethnic group or mother tongueSummaryPreconditionsMain FlowBranches and Edge Cases5.14 Delete a caste/ethnic group or mother tongueSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases6. Admin Flows6.1 Create6.2 List / Read Detail6.3 Update6.4 Reorder6.5 Activate/deactivate6.6 Soft delete6.7 Restore6.8 Hard delete6.9 Export/import6.10 Moderation6.11 Manual retry6.12 Settings (school profile)7. Lifecycle and State Transitions9. Data and Side Effects by Flow10. Error and Recovery Flows11. Diagrams Required Per Module12. Mandatory Feature and Flow Deep-Dive Pack12.1 Feature Inventory With Minor Behaviors12.2 Business Process Diagram Pack12.3 Business Rules and Policy Traceability12.4 Tradeoffs and Product Rationale12.5 Flow Edge-Case Matrix12.6 Flow-to-Data Trace12.7 Experience Quality Checklist13. Completion ChecklistSee Also