Subjects Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the school's subject catalogue and its per-class teaching assignments.
Subjects Features and Flows
1. Documentation Evidence
| Source Type | Files or Docs | What Was Extracted |
|---|---|---|
| Backend doc | /docs/developer/subjects/backend and the services it documents (subjects-read.service.ts, subjects-write.service.ts, class-subjects-read.service.ts, class-subjects-write.service.ts) | Business behavior, transaction boundaries, cache invalidation, and row-scoping. |
| API doc | /docs/developer/subjects/api and the controllers it documents | Route surface, actors, permissions, and response-visible behavior. |
| Schema | packages/db/src/schema/school/subjects.ts, migrations packages/db/src/migrations/0017_subject_module.sql and 0018_subject_permission_grants.sql | Constraints, indexes and permission grants that drive the edge cases and error branches below. |
| Consumer surfaces | apps/api/src/modules/mobile/student/student-portal.controller.ts, student-portal.service.ts, apps/api/src/modules/mobile/teacher/teacher-portal.controller.ts, teacher-portal.service.ts | The pupil's and teacher's own-subjects reads, audience-scoped and outside the admin permission catalogue. |
| Authorization | apps/api/src/modules/people/shared/people-access.service.ts, apps/api/src/modules/mobile/shared/portal-audience.ts, packages/db/src/seed/seed-auth.ts | Row scope for ClassSubjects_READ, portal audience assertions, and per-role permission grants. |
2. Feature Summary
| Field | Value |
|---|---|
| Module | Subjects (the school-wide subject catalogue) and ClassSubjects (one class teaching one subject) — two permission modules composed by AcademicStructureModule. |
| Submodule | ClassSubjects is the per-class assignment layer over Subjects; the mobile "my subjects" reads are a third, audience-scoped surface over the same two tables. |
| Primary user value | Lets the school define what it teaches once ("Mathematics", with a default mark scheme) and then assign that same subject to as many classes as it actually runs, each with its own teacher and its own pass/max marks — so Mr Sharma's Grade 5A Mathematics and Ms Rai's Grade 5B Mathematics are two configurations of one entity, not two subjects, and every report that groups by subject groups by something real. |
| Actors | Superadmin for every write on both tables — no seeded admin role exists in this database, so Subjects_CREATE/_UPDATE/_DELETE and ClassSubjects_CREATE/_UPDATE/_DELETE are superadmin-only until an operator hand-builds a custom role and grants them. Staff and teachers hold Subjects_READ and ClassSubjects_READ. Pupils hold Subjects_READ alone and read their own subjects through a dedicated audience-scoped route, never through the admin list. Guardians hold neither code. No guest or unauthenticated actor reaches any part of this module. |
| Main entry points | GET/POST/PATCH/DELETE /api/subjects, GET /api/subjects/options; GET/POST /api/subjects/:publicId/assignments, GET /api/classes/:classPublicId/subjects, PATCH/DELETE /api/class-subjects/:id; GET /api/mobile/student/subjects, GET /api/mobile/teacher/subjects. |
| Main outputs | Persisted subjects and class_subjects rows; the response DTOs behind the admin subject catalogue screen, the per-class assignment grid, a pupil's "my subjects" screen, and a teacher's "my subjects" screen. |
| Related docs | API, Backend. |
3. Actor Matrix
| Actor | Can Do | Cannot Do | Auth Requirement | Notes |
|---|---|---|---|---|
| Guest (unauthenticated) | Nothing. | Everything in this module. | None — and none is accepted. | Every route sits behind the global JwtAuthGuard; there is no @Public() route anywhere subjects or class_subjects are touched. |
| Logged-in user without the relevant permission | Nothing module-specific. | List, read, create, update or delete a subject or an assignment. | JWT + active role, but lacking Subjects_*/ClassSubjects_*. | Refused with 403 PERMISSION_INSUFFICIENT (or 403 AUTH_ACTIVE_ROLE_REQUIRED if several roles are held and none is selected). |
Pupil (seeded student role) | Read Subjects_READ-gated endpoints on the admin surface if reached directly; in practice reads their own subjects through GET /api/mobile/student/subjects, which needs no permission at all. | List a subject's or a class's assignments (ClassSubjects_READ is not granted), see any teacher's employee code or employment status, see any classmate's teacher-identity data. | JWT + active role with scopeKind: "student". | The consumer route is audience-scoped by assertPortalAudience, not by the permission catalogue — see Section 5.10. |
Guardian (seeded guardian role) | Nothing on this module. | Read the subject catalogue, read any assignment, read a ward's subjects. | JWT + active role with scopeKind: "guardian". | Deliberately withheld — see 12.3: a guardian holding Subjects_READ alone would be routed by the panel to the pupil-only consumer endpoint and refused there, permanently, since grants are never revoked. |
Staff or teacher (seeded staff/teacher role) | List/read the subject catalogue and subject options (Subjects_READ); list a subject's assignments and a class's assigned subjects (ClassSubjects_READ); a teacher additionally reads their own teaching load through GET /api/mobile/teacher/subjects. | Create, update, retire, reactivate or delete a subject; create, reconfigure, suspend, revive or delete an assignment. | JWT + active role holding the read codes above. | ClassSubjects_READ is safe for this audience specifically because Classes_READ (already held by both roles) already exposes every class's teacher name and employee code — see the grant's own rationale in seed-auth.ts. |
| Superadmin | Everything 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, and is the ONLY seeded role holding Subjects_CREATE/_UPDATE/_DELETE or any ClassSubjects_CREATE/_UPDATE/_DELETE code today. |
| Custom role holding a write code | Whatever action the granted code names — e.g. Subjects_CREATE alone permits creating a subject and nothing else. | Anything outside the specific code granted. | JWT + active role with that exact permission code. | An operator builds this through the Roles/Permissions module; nothing in the seed produces it. |
| Worker/system | No automated actor touches this module. | Everything, since none exists. | N/A | No job, scheduler or queue reads or writes subjects or class_subjects — the backend doc's BullMQ and Realtime sections are both "not applicable". |
4. Capability Matrix
| Capability | Surface | Actor | Route/Trigger | State Read | State Written | Linked API Section |
|---|---|---|---|---|---|---|
| List/search the subject catalogue | Admin/Staff/Teacher | Subjects_READ | GET /api/subjects | subjects, correlated class_subjects count | — | 8.1 |
| Subject options for pickers | Admin/Staff/Teacher | Subjects_READ | GET /api/subjects/options | subjects | — | 8.2 |
| Read one subject | Admin/Staff/Teacher | Subjects_READ | GET /api/subjects/:publicId | subjects, correlated count | — | 8.3 |
| Create a subject | Superadmin | Subjects_CREATE | POST /api/subjects | — | subjects | 8.4 |
| Update/retire/reactivate a subject | Superadmin | Subjects_UPDATE | PATCH /api/subjects/:publicId | subjects (locked read, reactivation collision check) | subjects | 8.5 |
| Delete a subject | Superadmin | Subjects_DELETE | DELETE /api/subjects/:publicId | subjects, class_subjects (existence/FK check) | subjects | 8.6 |
| List a subject's class assignments | Admin/Staff/Teacher | ClassSubjects_READ | GET /api/subjects/:publicId/assignments | class_subjects joined to classes/grades/sections/staff/users/designations | — | 8.7 |
| Assign a subject to one or more classes (batch) | Superadmin | ClassSubjects_CREATE | POST /api/subjects/:publicId/assignments | subjects, classes, staff+designations, existing class_subjects rows | class_subjects (insert or revive) | 8.8 |
| List a class's assigned subjects | Admin/Staff/Teacher | ClassSubjects_READ | GET /api/classes/:classPublicId/subjects | classes (scoped resolve), class_subjects joined as above | — | 8.9 |
| Update a class subject assignment | Superadmin | ClassSubjects_UPDATE | PATCH /api/class-subjects/:id | class_subjects (locked), subjects (revival precondition), sibling class_subjects (label collision) | class_subjects | 8.10 |
| Delete a class subject assignment | Superadmin | ClassSubjects_DELETE | DELETE /api/class-subjects/:id | class_subjects | class_subjects | 8.11 |
| A pupil views their own subjects | Mobile/consumer | Pupil, scopeKind: "student" | GET /api/mobile/student/subjects | student_class_enrollments, class_subjects, subjects, classes, grades, sections, staff, users, academic_sessions | — | 8.12 |
| A teacher views their own subjects | Mobile/consumer | Teacher, live staff row | GET /api/mobile/teacher/subjects | class_subjects, subjects, classes, grades, sections, academic_sessions | — | 8.13 |
5. User-Facing Flows
5.1 List/search the subject catalogue
Summary
An administrator, staff member or teacher opens a subject picker or the subject-management screen, and the school's full or filtered subject list loads with each row's live count of active class assignments.
Preconditions
- Valid JWT with an active role holding
Subjects_READ.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Admin/Staff/Teacher | Opens the subject list, optionally with search, type or isActive. | GET /api/subjects. | Controller. |
| 2 | Backend | Checks the Redis cache for this exact query shape. | Cache hit returns immediately; a miss falls through. | SubjectsReadService.findAll. |
| 3 | Backend | Reads subjects, filtered, ordered by case-folded name then id, with classCount as a correlated subquery. | Rows mapped to SubjectDto. | Service. |
| 4 | Backend | Writes the result to cache for 300 seconds. | Next identical query is served from cache. | Service. |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
search supplied | e.g. ?search=g% | Matched against name and code with ILIKE, with the pattern escaped — a literal % or _ in the search text filters as a literal, never as a wildcard. | Rows whose name or code contains the term. |
type/isActive filters | e.g. ?type=optional&isActive=false | ANDed into the WHERE clause. | Narrowed result set. |
pagination=false | Reading the whole catalogue for a select box | Allowed, capped at PaginationUtil.UNPAGINATED_HARD_CAP (1000) — the catalogue carries no person data. | Full (capped) list in one response, no count/currentPage/totalPage in the envelope. |
| Cache hit | Same query shape within 300 seconds | Served without touching Postgres. | Identical response, including a classCount that may be up to 300 seconds stale relative to a concurrent assignment write. |
| Redis unavailable | Cache read/write throws | Fails soft — treated as a miss on read, a no-op on write, both logged as a WARN. | Request still succeeds, served from Postgres. |
| Missing permission | Role lacks Subjects_READ | Refused before any query runs. | 403 PERMISSION_INSUFFICIENT. |
5.2 Subject options for assignment pickers
Summary
The class-assignment grid needs a lightweight, person-data-free list of subjects to populate its "add a subject" dropdown, always unpaginated and always capped.
Preconditions
Subjects_READ.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Admin/Staff/Teacher | Opens the assignment grid's subject picker. | GET /api/subjects/options. | Controller. |
| 2 | Backend | Reads active subjects (or all, with includeInactive=true) ordered by case-folded name, fetching one row past the 1000 cap. | Up to 1000 rows returned; truncated flags whether more exist. | SubjectsReadService.findOptions. |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
includeInactive omitted | Default | Active subjects only. | Retired subjects excluded from the picker. |
includeInactive=true | Explicit | Every subject, active or archived. | Used by a screen that must show a retired subject's historical label. |
| Over 1000 rows exist | A very large catalogue | Rows beyond 1000 are silently withheld from data, but truncated: true names the fact rather than hiding it. | Caller can decide to narrow the request; no error is raised. |
Route declared before :publicId | N/A — routing order | options is a literal segment declared ahead of the parameterised GET /api/subjects/:publicId in the controller, so it can never be swallowed as a subject whose id is the string options. | Consistent 200, never a confusing 404. |
5.3 Read one subject
Summary
An operator opens a single subject's detail screen by its publicId.
Preconditions
Subjects_READ.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Admin/Staff/Teacher | Opens a subject's detail view. | GET /api/subjects/:publicId. | Controller. |
| 2 | Backend | Looks up the row by publicId, including the live classCount. | SubjectDto, or a 404. | SubjectsReadService.loadDto. |
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
Unknown publicId | No row with that id | Not found. | 404 SUBJECT_NOT_FOUND. |
| Archived subject | isActive: false | Still readable by id — retirement does not hide a row from a direct lookup. | 200, isActive: false in the payload. |
5.4 Create a subject
Summary
Superadmin defines a new subject the school teaches: its display name, its stable machine code, whether it is compulsory or optional by default, and the default mark scheme every new assignment inherits.
Preconditions
Subjects_CREATE.- No active subject already holds the same name (case-insensitive) or the same code.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Superadmin | Submits name, code, type, and optionally defaultMaxMarks/defaultPassMarks/isActive. | POST /api/subjects. | Controller. |
| 2 | Backend | Trims the name; defaults marks to 100/40 when omitted. | Validated payload. | SubjectsWriteService.create. |
| 3 | Backend | Checks passMarks <= maxMarks on the two now-known values. | Passes, or refuses. | Service. |
| 4 | Backend | Inserts the row inside a transaction. | New subjects row, publicId generated server-side (UUIDv7). | Service. |
| 5 | Backend | Invalidates the entire academic-structure:subjects:* cache prefix. | Every cached list/options/detail response for this module is dropped. | Service. |
| 6 | Backend | Re-reads the just-written row inside the same connection and returns it. | 201 with the full SubjectDto. | Service. |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
| Duplicate name | An active subject already has this name, case-insensitively | Refused by the partial unique index. | 409 SUBJECT_NAME_TAKEN. |
| Duplicate code | An active subject already has this code | Refused by the partial unique index. | 409 SUBJECT_CODE_TAKEN. |
passMarks > maxMarks | Cross-column check | Refused pre-write, before the CHECK constraint would fire. | 409 SUBJECT_MARKS_INVALID. |
| Padded/control-character name | e.g. " Math" or embedded newline | Refused by the DTO's regex validators before the request reaches the service. | 400 from the global pipe. |
| Same name/code as an archived subject | The retired row's name is free again | Allowed — the unique indexes are partial on is_active. | 201, two rows now share one label, one active and one archived. |
isActive: false on create | Explicit | A subject can be created already retired. | 201, classCount: 0, no assignment possible until reactivated. |
5.5 Update, retire, and reactivate a subject
Summary
Superadmin edits a subject's name, code, type, default marks, or active state. Reactivating a previously retired subject is the one transition with its own precondition.
Preconditions
Subjects_UPDATE.- The
versionlast read must still match the row's currentversion. - Reactivation only: neither the merged name nor the merged code may belong to a different currently-active subject.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Superadmin | Submits a partial patch plus the version they last read. | PATCH /api/subjects/:publicId. | Controller. |
| 2 | Backend | Reads the existing row inside a transaction. | 404 if absent. | SubjectsWriteService.update. |
| 3 | Backend | Merges the patch onto the existing row for the passMarks <= maxMarks check. | Passes, or refuses. | Service. |
| 4 | Backend | If isActive flips false → true, checks whether the merged name or code now belongs to a different active subject. | Passes, or refuses naming the holder's code. | Service. |
| 5 | Backend | Runs UPDATE ... WHERE public_id = ... AND version = dto.version. | 0 rows affected means gone-or-stale; re-reads to tell which. | Service. |
| 6 | Backend | The bump_row_version trigger advances version and stamps updated_at. | New version. | Database trigger. |
| 7 | Backend | Invalidates the cache prefix and returns the freshly re-read row. | 200 with the updated SubjectDto. | Service. |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
Stale version | Row changed since the caller last read it | Zero rows affected, row still exists. | 409 SUBJECT_VERSION_CONFLICT. |
| Row deleted mid-flight | Zero rows affected, row no longer exists | Reported as absent, not as a conflict. | 404 SUBJECT_NOT_FOUND. |
| Reactivating onto a taken name | isActive: false → true, merged name matches another active subject | Refused before the write, naming the holder's code. | 409 SUBJECT_NAME_TAKEN_ON_REACTIVATE. |
| Reactivating onto a taken code | Same, for code | As above. | 409 SUBJECT_CODE_TAKEN_ON_REACTIVATE. |
Empty patch, version only | Caller sends only { version } | The service self-assigns name = existing.name so the UPDATE statement is non-empty and the trigger still fires — version still advances. | 200, no field actually changed except version/updatedAt. |
| Partial update crossing the marks rule | Request sets defaultPassMarks: 120, row's existing defaultMaxMarks is 100 | Merged check catches what no single-field DTO validator could. | 409 SUBJECT_MARKS_INVALID. |
| Retiring a subject with active assignments | isActive: true → false on a subject several classes still teach | Allowed — retirement does not touch class_subjects. | 200; those classes keep teaching a now-archived subject until reassigned. |
5.6 Delete a subject
Summary
Superadmin permanently removes a subject that was never actually assigned to a class, or has had every assignment removed first.
Preconditions
Subjects_DELETE.- Zero
class_subjectsrows reference this subject — active or suspended.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Superadmin | Requests deletion. | DELETE /api/subjects/:publicId. | Controller. |
| 2 | Backend | Resolves the row; 404 if absent. | — | SubjectsWriteService.remove. |
| 3 | Backend | Counts total and active class_subjects rows referencing it. | Any total > 0 refuses. | Service. |
| 4 | Backend | Deletes the row. | Hard delete, no soft-delete flag on this table. | Service. |
| 5 | Backend | Invalidates the cache prefix. | — | Service. |
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
| Any assignment exists, even suspended | total > 0 | Refused, message names both the total and the active count so the operator is not refused on the strength of a number the screen never showed them. | 409 SUBJECT_IN_USE. |
| All assignments were already removed | total === 0 | Delete proceeds. | 200, row gone, null data. |
| Race: an assignment is created between the pre-check and the delete | Concurrent POST .../assignments | ON DELETE restrict on the foreign key refuses the delete at the database, mapped by name. | 409 SUBJECT_IN_USE (same code, database-level backstop). |
5.7 Assign a subject to one or more classes (batch)
Summary
Superadmin assigns one already-created subject to any number of classes in a single request — the assignment grid's "save row" and "save all" actions both go through this one endpoint, since a single-item assign is a one-element batch.
Preconditions
ClassSubjects_CREATE.- The subject exists and is active.
- 1-100 items per request.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Superadmin | Submits { items: [...] } against a subject's publicId. | POST /api/subjects/:publicId/assignments. | Controller. |
| 2 | Backend | Resolves the subject once; refuses if missing or retired. | — | ClassSubjectsWriteService.assign step 1. |
| 3 | Backend | Resolves every classId in one query; collects CLASS_NOT_FOUND, CLASS_INACTIVE, and in-batch duplicate class errors per item, deduped on the resolved integer id. | — | Steps 2-4. |
| 4 | Backend | Resolves every non-null teacherId in one query joined to designations; collects "not found" and "not a teaching designation" errors per item. | — | Step 5. |
| 5 | Backend | Fills any absent type/maxMarks/passMarks from the subject's defaults, then validates passMarks <= maxMarks per item after defaulting. | — | Steps 5 cont., 7. |
| 6 | Backend | Checks for a label collision — the class already actively teaches a different subject row sharing this name or code — and checks a stale version against any existing row for (class, this subject). | — | Step 6. |
| 7 | Backend | If ANY item failed any of the above, throws with every failure attached. Nothing is written. | — | Step 8. |
| 8 | Backend | Upserts every surviving item on (classId, subjectId): insert if absent, merge-update if present — absent fields keep the existing value, explicit null on teacherId clears it. A suspended existing row not explicitly asked to revive (isActive absent) is left untouched and reported as skipped. | class_subjects rows created or updated. | Step 9. |
| 9 | Backend | Commits, then invalidates the subjects cache prefix (assignment count changed). | — | Step 10. |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
Empty items array | Zero-length batch | Refused before the transaction opens. | 400 CLASS_SUBJECT_BATCH_INVALID. |
| Subject archived | subjects.is_active = false | Whole request refused, no items are validated further. | 422 SUBJECT_INACTIVE. |
| Same class appears twice in the batch | Two items name the same resolved class | Second occurrence collected as an error. | Item-level CLASS_SUBJECT_DUPLICATE_CLASS inside 422 CLASS_SUBJECT_BATCH_INVALID. |
| Teacher id does not resolve, or resolves to a soft-deleted staff row | Bad or stale teacherId | Item-level failure. | CLASS_SUBJECT_TEACHER_NOT_FOUND. |
| Teacher's designation is not a teaching one | designations.is_teaching = false | Item-level failure. | CLASS_SUBJECT_TEACHER_NOT_TEACHING. |
Post-defaulting passMarks > maxMarks | Item omitted marks that, once defaulted from the subject, violate the bound | Item-level failure. | CLASS_SUBJECT_MARKS_INVALID. |
| Class already actively teaches a different subject with the same name/code | Two subject rows sharing one retired-then-recreated label, both assigned to one class | No database constraint can express this (it spans two tables); the service guard is the only enforcement. | CLASS_SUBJECT_DUPLICATE_LABEL. |
version supplied and stale against an existing row | Concurrent edit | Item-level failure, folded into the batch rather than partially committing. | CLASS_SUBJECT_VERSION_CONFLICT. |
Existing row is suspended, item omits isActive | A prior suspension, re-assign without asking to revive | Left untouched, its id reported in skippedSuspended — never silently reactivated. | 201, no write to that row. |
Existing row is suspended, item sets isActive: true | Explicit revival | Treated as a normal upsert; other omitted fields keep their existing values. | 201, row revived. |
| Absent field on an existing row | e.g. item omits maxMarks on a re-assign | The existing value is kept, never reset to the subject's current default. | Distinguishes a partial edit from a full re-send. |
| One item fails validation among many valid ones | Mixed batch | The WHOLE batch is refused — nothing partially commits. | 422 CLASS_SUBJECT_BATCH_INVALID naming every failing index. |
5.8 List a subject's class assignments
Summary
An operator opens a subject's detail screen and sees every class currently (or previously) teaching it.
Preconditions
ClassSubjects_READ.- Row scope resolved via
PeopleAccessService.scopeFor(actor, "classSubjects", …)— in practiceallfor staff/teacher/superadmin and refused (empty) for anyone else, since pupils and guardians are never granted this code.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Staff/Teacher/Admin | Opens a subject's assignments tab. | GET /api/subjects/:publicId/assignments. | Controller. |
| 2 | Backend | Resolves the subject by publicId; 404 if absent. | — | ClassSubjectsReadService.findForSubject. |
| 3 | Backend | Resolves the caller's scope, applies it plus subjectId = ... and the optional isActive filter. | Ordered by grade sort order, section sort order, shift, id. | Service. |
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
pagination=false requested | Query explicitly disables pagination | Refused — every row embeds a ClassTeacherDto (employee code, employment status). | 400 PAGINATION_LIMIT_INVALID. |
isActive=false | Filter to suspended assignments only | Narrowed result. | Only suspended rows returned. |
| Unknown subject | No row with that publicId | 404 before any assignment query runs. | 404 SUBJECT_NOT_FOUND. |
5.9 List a class's assigned subjects
Summary
An operator opens a class's detail screen and sees every subject it currently (or previously) taught, each with its teacher and its effective mark scheme.
Preconditions
ClassSubjects_READ.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Staff/Teacher/Admin | Opens a class's subjects tab. | GET /api/classes/:classPublicId/subjects. | Controller. |
| 2 | Backend | Resolves the class through the caller's scope predicate, not after. | An out-of-scope class reads identically to an absent one. | ClassSubjectsReadService.findForClass. |
| 3 | Backend | Applies the same scope again to the assignment rows, plus the optional isActive filter. | Ordered as in 5.8. | Service. |
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
| Class outside the caller's scope | A narrower-than-all scope existed and excluded it | Indistinguishable from a genuinely absent class — never a 403. | 404 CLASS_NOT_FOUND. |
pagination=false | As in 5.8 | Refused for the same staff-identity reason. | 400 PAGINATION_LIMIT_INVALID. |
5.10 A pupil views their own subjects
Summary
A pupil opens their own "My Subjects" screen and sees exactly what their currently-enrolled class is being taught this academic session — subject name, code, type, teacher's name only, and the pass/max marks — with no way to reach any other class's data through this route.
Preconditions
- Active session with
scopeKind: "student". - No permission code is checked —
STUDENT_PERMISSIONSis deliberately[]beyondSubjects_READ, and this route needs none at all.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Pupil | Opens their subjects screen. | GET /api/mobile/student/subjects. | StudentSubjectsPortalController. |
| 2 | Backend | Asserts scopeKind === "student". | 403 otherwise. | assertPortalAudience. |
| 3 | Backend | Resolves the target academic session — the query param if given, otherwise the current session. | null if no current session is configured. | StudentPortalService.resolveSessionId. |
| 4 | Backend | Builds a subquery of the pupil's own actively-enrolled class ids (student_class_enrollments.status = 'active', pupil not soft-deleted, record_status = 'active') and reads class_subjects rows whose classId is in that set, active, in the target session. | Every subject their current class teaches. | Service. |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
| Session acting as a non-student role | e.g. staff, teacher, guardian | Refused before any query runs. | 403 PERMISSION_INSUFFICIENT. |
| No active role selected | Multi-role session, none chosen | Refused. | 403 AUTH_ACTIVE_ROLE_REQUIRED. |
| Pupil has no active enrolment | Not currently placed in a class | Matches nothing — not an error. | 200 { data: [], truncated: false }. |
| No current academic session configured | Nothing marked is_current | Resolved session is null, short-circuits before any subject query. | 200 { data: [], truncated: false }. |
Pupil record soft-deleted or record_status not active | Withdrawn or on leave | Excluded by the enrolment subquery's own filters. | Empty result, not an error. |
| Assignment has no teacher yet | class_subjects.teacher_id IS NULL | teacherName: null. | Visible in the payload as a gap, not an error. |
| Over 1000 rows | A pupil enrolled in an improbably large number of active assignments | Truncated at the cap, truncated: true. | Same withholding pattern as SubjectOptionsPayloadDto. |
5.11 A teacher views their own subjects
Summary
A teacher opens their own "My Subjects" screen and sees every class they are the assigned teacher of, for the current academic session — their own name filled in as teacherName on every row, since the query is already scoped to their own teacherId.
Preconditions
- Active session with a live
staffrow, not acting as guardian or student. - No permission code is checked — the same reasoning as 5.10, restated for staff: holding
Students_READwithscopeKind: "all"would make a permission gate here no gate at all.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Teacher | Opens their subjects screen. | GET /api/mobile/teacher/subjects. | TeacherSubjectsPortalController. |
| 2 | Backend | Resolves the caller's own live staff.id; refuses a session not acting as the role named teacher or with no live staff row. | — | assertTeacherStaffRow. |
| 3 | Backend | Resolves the target academic session, as in 5.10. | — | TeacherPortalService.resolveSessionId. |
| 4 | Backend | Reads class_subjects rows where teacherId is the caller's own resolved staff id, active, in the target session. | Every class they teach this subject in. | Service. |
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
| Session acting as guardian or student | Wrong audience for this route | Refused. | 403 PERMISSION_INSUFFICIENT. |
No live staff row | Dismissed staff member who kept another live profile | Refused — staff.deleted_at is filtered explicitly. | 403 PERMISSION_INSUFFICIENT. |
No class_subjects row names this teacher | Subject teacher who is nobody's assigned teacher yet | Matches nothing. | 200 { data: [], truncated: false }. |
| No current academic session | As in 5.10 | Short-circuits. | 200 { data: [], truncated: false }. |
teacherName field | Always the caller's own name | Derived from the decoded session (actor.name), never re-selected from users — no join needed since teacherId in the WHERE clause already IS the caller. | Never null for this route. |
6. Admin Flows
Repeat for every admin flow:
- Create: 5.4 (subject), 5.7 (assignment).
- List: 5.1, 5.8, 5.9.
- Read detail: 5.3.
- Update: 5.5 (subject), assignment
PATCH(covered inline below). - Reorder: Not applicable — neither table carries a
sortOrdercolumn; the catalogue sorts by case-folded name and the assignment lists sort by grade/section/shift. - Activate/deactivate:
isActiveon both tables — retire/reactivate a subject (5.5), suspend/revive an assignment (below). - Soft delete: Not applicable — both tables hard-delete only; there is no
deleted_atcolumn on either. - Restore: Not reachable —
Subjects_RESTOREandClassSubjects_RESTOREcodes exist in the generated permission catalogue (every module gets all five actions), but nothing in either controller implements a restore route, because there is nothing soft-deleted to restore. - Export/import: Not applicable — neither table is wired into the
DataImport/DataExportmodule. - Moderation: Not applicable — no content moderation surface exists on either table.
- Manual retry: Not applicable — no queue or async job touches either table.
6.1 Update a class subject assignment (reconfigure, suspend, revive)
Route, permission, mutation side effects, cache invalidation and failure branches:
- Route:
PATCH /api/class-subjects/:id, permissionClassSubjects_UPDATE. versionis required and applied in theWHEREclause — the same optimistic-concurrency shape as the subjectPATCH.- A
false → truetransition onisActiveruns the SAME preconditionsassign()runs: the subject must still be active, and the class must not already actively teach a different subject sharing the same name or code. - On success, the subjects cache prefix is invalidated (
classCounton the parent subject may have moved). - No audit call beyond the global
ActivityAuditInterceptor— every mutating request on this route is already recorded.
6.2 Delete a class subject assignment
DELETE /api/class-subjects/:id, permission ClassSubjects_DELETE. Hard delete, no pre-check against a dependent table today — CLASS_SUBJECT_IN_USE is registered but unreachable, because nothing references class_subjects yet. Invalidates the subjects cache prefix on success, since the parent subject's classCount moves.
7. Lifecycle and State Transitions
Subjects
| Entity | From | Event/Action | To | Guard Condition | Side Effects |
|---|---|---|---|---|---|
subjects | (none) | POST /api/subjects | active (or archived if isActive: false was sent) | Name/code not taken by another active subject; passMarks <= maxMarks | Cache prefix invalidated. |
subjects | active | PATCH { isActive: false } | archived | None beyond the version check — retiring never checks assignments. | Cache prefix invalidated; existing assignments untouched. |
subjects | archived | PATCH { isActive: true } | active | Merged name/code must not belong to a different active subject. | Cache prefix invalidated. |
subjects | active or archived | DELETE | (gone) | Zero class_subjects rows reference it, active or suspended. | Row removed, cache prefix invalidated. |
Class subject assignments
| Entity | From | Event/Action | To | Guard Condition | Side Effects |
|---|---|---|---|---|---|
class_subjects | (no row for this class+subject) | POST .../assignments (insert branch) | active (or isActive: false if explicit) | Subject active; class active; no label collision; teacher (if given) is a live, teaching staff member | Row created, subjects cache prefix invalidated. |
class_subjects | existing row, any state | POST .../assignments (upsert branch, any field supplied) | Fields merge-updated; state changes only if isActive explicitly supplied | Same preconditions as insert, plus a supplied version must match | Row updated. |
class_subjects | suspended | POST .../assignments, item omits isActive | Unchanged — explicitly left suspended | N/A | Reported in skippedSuspended, nothing written to that row. |
class_subjects | active | PATCH { isActive: false } | suspended | Version match. | Cache prefix invalidated. |
class_subjects | suspended | PATCH { isActive: true } | active | Subject still active; no label collision on the class. | Cache prefix invalidated. |
class_subjects | any | DELETE | (gone) | None today (no dependent table yet). | Row removed, cache prefix invalidated. |
9. Data and Side Effects by Flow
| Flow | DB Writes | Cache Effects | Jobs | Realtime | Analytics | Notifications |
|---|---|---|---|---|---|---|
| List/read subjects, options | — | Read/write academic-structure:subjects:* (300s TTL) | None | None | None | None |
| Create/update/delete a subject | subjects | Invalidates academic-structure:subjects:* | None | None | None | None |
| List assignments (per subject/class) | — | None (scope-dependent reads are never cached) | None | None | None | None |
| Assign / update / delete an assignment | class_subjects | Invalidates academic-structure:subjects:* (parent's classCount) | None | None | None | None |
| Pupil/teacher own-subjects read | — | None | None | None | None | None |
10. Error and Recovery Flows
| Scenario | Trigger | User/System Experience | Recovery | Source |
|---|---|---|---|---|
| Rate limited | Not applicable | This module applies no route-specific rate limit beyond whatever a global guard enforces elsewhere in the application. | N/A | Controllers carry no throttling decorator. |
| Queue failure | Not applicable | No BullMQ job is produced or consumed by this module. | N/A | Backend doc, Section 9. |
| Cache failure | Redis unavailable during a list/options/detail read | Falls through to Postgres transparently; response is unaffected, only slower. | Automatic — getSoft treats any Redis error as a cache miss. | RedisCacheService.getSoft. |
| Cache invalidation failure | Redis unavailable right after a successful write | The write already committed; the stale cached list/options response persists until its 300-second TTL expires. | Automatic expiry; a WARN log line is the only signal in the interim. | RedisCacheService.delPatternSoft. |
| Optimistic concurrency lost | Two operators edit the same subject or assignment concurrently | The second writer's request fails cleanly rather than silently overwriting the first. | Caller reloads the row and retries with the fresh version. | SUBJECT_VERSION_CONFLICT / CLASS_SUBJECT_VERSION_CONFLICT. |
| Batch assignment partially invalid | Any one item in a 1-100 item batch fails validation | The entire batch is refused; nothing is written. | Caller fixes every named index and resubmits — no partial state to reconcile. | CLASS_SUBJECT_BATCH_INVALID, per-index errors[]. |
11. Diagrams Required Per Module
- Actor capability diagram: Section 3, Section 12.2.
- High-level module flow diagram: Section 12.2.
- Sequence diagram for each major user/admin/system flow: present under every Section 5.x flow that has one, plus 6.1.
- State machine diagram for every lifecycle: Section 7, two diagrams (subject, assignment).
- Data side-effect diagram for write flows: Section 12.6.
- Error branch diagram for critical flows: Section 6.1.
12. Mandatory Feature and Flow Deep-Dive Pack
12.1 Feature Inventory With Minor Behaviors
| Feature | Minor Behavior | Actor | Trigger | User/System Result | Backend Side Effect | Source |
|---|---|---|---|---|---|---|
| List subjects | Search escapes %/_ in the term | Admin/Staff/Teacher | ?search=... | A literal percent sign in a subject name filters literally, never as a wildcard | escapeLikePattern applied before ILIKE | subjects-read.service.ts |
| List subjects | pagination=false capped, not refused | Admin/Staff/Teacher | ?pagination=false | Full catalogue in one response, up to 1000 rows | UNPAGINATED_HARD_CAP | subjects-read.service.ts |
| List subjects | classCount never joins classes | Admin/Staff/Teacher | Any list/detail read | A subject's assignment count is stable regardless of a class being deactivated | Correlated subquery scoped to class_subjects.is_active only | subjects-read.service.ts |
| Subject options | Truncation is reported, not silent | Admin/Staff/Teacher | Catalogue exceeds 1000 rows | truncated: true in the payload | One extra row fetched, sliced back | subjects-read.service.ts |
| Create subject | Name is trimmed server-side | Superadmin | POST /api/subjects | Leading/trailing whitespace from a copy-paste is silently removed | dto.name.trim() | subjects-write.service.ts |
| Create subject | Marks default independently | Superadmin | Marks omitted | defaultMaxMarks defaults to 100, defaultPassMarks to 40, even if only one is supplied | Nullish-coalescing per field | subjects-write.service.ts |
| Update subject | No-op patch still advances version | Superadmin | PATCH { version } only | The token changes even though nothing else did | Self-assignment keeps the UPDATE non-empty so the trigger fires | subjects-write.service.ts |
| Update subject | Reactivation collision names the holder | Superadmin | Reactivating onto a taken label | Error message includes the other subject's code | Pre-write SELECT against active rows excluding self | subjects-write.service.ts |
| Delete subject | Refusal message states both counts | Superadmin | Any dependent assignment exists | Message shows total AND active counts, since classCount only shows active | Two aggregate counts in one query | subjects-write.service.ts |
| Assign to classes | Absent field keeps existing value on re-assign | Superadmin | Partial item in a batch against an existing row | Untouched fields are not reset to the subject's current defaults | Object.hasOwn per field drives the SET clause | class-subjects-write.service.ts |
| Assign to classes | Explicit null clears the teacher | Superadmin | teacherId: null | The assignment becomes unassigned rather than keeping the prior teacher | ValidateIf distinguishes absent from explicit null | class-subject.dto.ts |
| Assign to classes | Suspended row is never silently revived | Superadmin | Bulk re-assign across a grade, one class suspended | That row stays suspended and its id is reported back | skippedSuspended array | class-subjects-write.service.ts |
| Assign to classes | Duplicate class in one batch is caught before any write | Superadmin | Two items name the same class (by resolved integer id, not uuid text) | Second occurrence flagged, nothing written | seenClassIntIds set | class-subjects-write.service.ts |
| List assignments | Pagination cannot be disabled | Admin/Staff/Teacher | ?pagination=false | Refused — every row carries an employee code | PAGINATION_LIMIT_INVALID | class-subjects-read.service.ts |
| List for class | Out-of-scope class reads as absent | Non-all-scope caller | GET /classes/:id/subjects on an out-of-scope class | 404, never 403 | Class resolved THROUGH the scope predicate | class-subjects-read.service.ts |
| Pupil subjects | Empty is 200, not 404 | Pupil | No active enrolment | { data: [], truncated: false } | No pre-resolve-or-404 of a students row | student-portal.service.ts |
| Pupil subjects | Defaults to the current session | Pupil | academicSessionId omitted | Only the current year's subjects, never the pupil's whole history | resolveSessionId reads academic_sessions.is_current | student-portal.service.ts |
| Teacher subjects | teacherName is always the caller's own name | Teacher | Any read | No extra join needed, and no risk of showing a stale name | actor.name, not re-selected | teacher-portal.service.ts |
12.2 Business Process Diagram Pack
12.3 Business Rules and Policy Traceability
| Rule | Business Reason | Actor Impact | Enforced In | API Impact | Backend Impact | Tests |
|---|---|---|---|---|---|---|
| A subject is defined once, school-wide; a class-subject assignment is a separate row per class | Mathematics is taught by different teachers in different classes — collapsing the two would force one "Mathematics" row per class and break every report that groups by subject | Every actor works with one subject entity regardless of how many classes teach it | subjects vs class_subjects tables | Two distinct route families | subjects.ts schema docblock | subjects-write.service.integration.spec.ts |
type, maxMarks, passMarks are COPIED onto the assignment at assignment time, never referenced | Editing a subject's defaults must never retroactively change a class whose marks were already recorded | An operator changing a subject's default pass mark does not silently alter any class's grading | class_subjects.type/max_marks/pass_marks are plain columns, not a join | AssignSubjectItemDto defaults from the subject only when a field is absent | class-subjects.ts schema docblock | class-subjects-write.service.integration.spec.ts |
subjects name/code uniqueness is partial on is_active | A retired subject's name must be reusable, or archiving becomes a dead end | An operator can retire "Sanskrit" and create a fresh "Sanskrit" | subjects_name_unique, subjects_code_unique | SUBJECT_NAME_TAKEN/SUBJECT_CODE_TAKEN only fire against active rows | Partial uniqueIndex ... WHERE is_active | subjects-write.service.integration.spec.ts |
| Reactivating a subject re-checks the name/code against currently-active rows | The partial index cannot catch this — a reactivating row does not yet violate anything at the moment it is checked | An operator reactivating a retired subject onto a now-taken label is refused with the holder's name, not a generic conflict | Application-only pre-write check | SUBJECT_NAME_TAKEN_ON_REACTIVATE/SUBJECT_CODE_TAKEN_ON_REACTIVATE | subjects-write.service.ts reactivation branch | subjects-write.service.integration.spec.ts |
class_subjects identity (class_id, subject_id) is TOTALLY unique, including while suspended | Two live rows for one identity would make every mark, attendance record and timetable slot pointing at one of them unresolvable | An operator cannot accidentally create two rows for one class-subject pair, even across a suspend/revive cycle | class_subjects_identity_unique (non-partial) | The assign endpoint upserts rather than inserts | onConflictDoUpdate targeting the identity index | class-subjects-write.service.integration.spec.ts |
| Assigning to a class holding a suspended row REVIVES it, but only when the caller explicitly asks | A bulk re-assign must not silently un-suspend something an administrator deliberately suspended | An operator's routine "re-run this term's assignments" cannot accidentally undo a deliberate suspension | ClassSubjectsWriteService.assign step 9 | AssignSubjectItemDto.isActive | Explicit isActive: true required, else reported in skippedSuspended | class-subjects-write.service.integration.spec.ts |
Optimistic concurrency via a server-owned version, bumped by a trigger | Prevents a lost update between two operators editing the same row | The second of two concurrent editors is told to reload rather than silently overwritten | bump_row_version BEFORE UPDATE trigger on both tables | version required on every UpdateSubjectDto/UpdateClassSubjectDto | WHERE ... AND version = ... in every UPDATE | Both write-service integration specs |
ClassSubjects_READ is withheld from pupils and guardians | Object-level scope restricts which ROWS a caller sees, never which FIELDS — a pupil holding this code would read every teacher's employee code and employment status for their own class | Pupils use a name-only consumer endpoint instead | packages/db/src/seed/seed-auth.ts (STUDENT_PERMISSIONS, GUARDIAN_PERMISSIONS) | GET /subjects/:id/assignments and GET /classes/:id/subjects are unreachable for these roles | class-subjects.dto.ts, ClassTeacherDto | class-subjects-authorization.integration.spec.ts |
Subjects_READ alone is granted to pupils; ClassSubjects_READ alone is granted to staff/teacher, never both to guardians | A guardian holding Subjects_READ would be routed by the panel to a pupil-only route and refused there permanently | Guardians hold neither code today, by deliberate choice, not oversight | GUARDIAN_PERMISSIONS = [] | No guardian-facing subjects route exists yet | seed-auth.ts commentary | portal-authorization.integration.spec.ts |
Pupils and teachers read their own subjects through an audience-scoped route, never through PeopleAccessService.scopeFor | scopeFor can return {kind:"all"} for a broadly-scoped role, which would leak the entire pupil roll through a portal named for one audience | A pupil's "my subjects" screen cannot be reached by a teacher or staff session, and vice versa | assertPortalAudience / assertTeacherStaffRow | Consumer routes carry no @Permissions() at all | portal-audience.ts | portal-subjects.integration.spec.ts |
| Both consumer subject reads default to the current academic session | class_subjects carries no session column and nothing prunes it; without a bound, a long-serving pupil or teacher's list grows without limit | A pupil or teacher sees only this year's subjects unless they explicitly ask for another session | resolveSessionId in both portal services | MySubjectsQueryDto.academicSessionId optional | academic_sessions.is_current lookup | portal-subjects.integration.spec.ts |
| The subjects list/options/detail reads are cached; the assignment reads are not | Assignment reads are scope-dependent (row-scoped by actor), so a shared cache key would serve one audience's rows to another | List/options/detail responses may lag up to 300 seconds behind a concurrent write; assignment reads are always current | SubjectsReadService uses getSoft/setSoft; ClassSubjectsReadService does not | N/A | academic-structure:subjects:* prefix | subjects-read.service.integration.spec.ts |
12.4 Tradeoffs and Product Rationale
| Product Decision | User Benefit | Engineering Benefit | Alternative | Tradeoff | Risk |
|---|---|---|---|---|---|
| Batch assignment is all-or-nothing in one transaction | A 100-row grid save either fully succeeds or fully fails, never leaving a half-applied grid an operator has to reconcile row by row | One write path, one set of invariants, no partial-commit recovery logic | Per-item commit, collecting successes and failures independently | A single bad row in a large batch blocks 99 good ones until fixed | Low — errors carry a per-index list so the fix is immediate |
| Assignment defaults are copied, not referenced | A class's grading never silently drifts when someone edits the subject catalogue later | No join needed on every mark calculation; the assignment row is self-contained | A foreign-key reference to the subject's live defaults | Two rows must be kept mentally distinct — "the subject's default" vs "this class's actual" | Low — the DTOs and docs both label the field as effective/copied |
ClassSubjects_READ granted to staff/teacher but not pupils/guardians | Protects staff identity (employee code, employment status) from a non-staff audience | One permission code instead of a field-level redaction layer on this DTO | Field-omitting a pupil-safe variant of ClassSubjectDto | Pupils/guardians need an entirely separate route (already built) instead of a filtered view of the same one | Low — the separate route already exists and is audience-scoped |
Consumer "my subjects" routes bypass PeopleAccessService.scopeFor entirely | Closes a real, previously-found hole where a broadly-scoped staff session could read another audience's portal data | The row-scope IS the query's own join condition, impossible to widen by a permission grant elsewhere | Reuse scopeFor with a classSubjects/students entity | A second, portal-specific authorization pattern to maintain alongside the admin one | Low — documented at length in portal-audience.ts, and covered by its own authorization integration spec |
Suspension (isActive: false) instead of deletion for a class-subject assignment | Configuration (teacher, marks) is preserved for a quick, safe revival | Avoids re-entering marks and re-picking a teacher on a temporary pause | Hard delete and re-create | A suspended row still occupies the identity slot — a genuinely new assignment for the same pair must revive it, never insert a second row | Low — the identity index is total specifically to make this the only path |
| Marks upper bound of 1000 on both tables | Catches an obvious typo (10000) without constraining real assessment schemes | One shared, generous ceiling instead of per-school configuration | No ceiling at all | A school running a genuinely unusual scale above 1000 would need a schema change | Very low — stated explicitly as a fat-finger guard, not a domain rule |
12.5 Flow Edge-Case Matrix
| Flow | Edge Case | Trigger | Expected Behavior | User/System Feedback | Source |
|---|---|---|---|---|---|
| List subjects | Empty state | No subjects created yet | { data: [], totalCount: 0 } | Empty list, no error | subjects-read.service.ts |
| List subjects | First use | Cache cold, first request after boot | Full DB round trip, then cached | Same response, slightly slower first call | subjects-read.service.ts |
| Assign to classes | Last item in a batch fails | 99 valid + 1 invalid item | Whole batch refused | 422, every failing index named | class-subjects-write.service.ts |
| Assign to classes | Duplicate action | Same batch submitted twice, unchanged | Second call is idempotent for unchanged fields (self-assignment no-op on the SET clause) | 201 both times, version may advance on the second if the trigger fires | class-subjects-write.service.ts |
| Assign to classes | Concurrent action | Two operators assign the same subject to the same class simultaneously | The identity unique index serializes them; the loser's onConflictDoUpdate still succeeds since it is an upsert, not a plain insert | Both requests succeed, last write wins on overlapping fields (bounded by any supplied version) | class_subjects_identity_unique |
| Update subject | Expired/stale state | version sent is older than current | Refused | 409 SUBJECT_VERSION_CONFLICT | subjects-write.service.ts |
| Update subject | Permission mismatch | Caller holds Subjects_READ but not _UPDATE | Refused before the handler runs | 403 PERMISSION_INSUFFICIENT | RoleGuard |
| Pupil subjects | Guest limitation | N/A — route requires a JWT | No guest path exists | 401 from the global guard | JwtAuthGuard |
| Pupil subjects | Missing dependency | No current academic session configured anywhere | Empty result rather than an error | 200 { data: [], truncated: false } | student-portal.service.ts |
| List subjects | Cache stale/miss | Redis down | Falls through to DB | No visible error, response unaffected | redis.service.ts getSoft |
| Assign to classes | Queue failure | Not applicable | No queue is involved anywhere in this module | N/A | Backend doc, Section 9 |
| List subjects | Unsupported filter/sort | e.g. sort=name on the subject list | ListSubjectsQueryDto extends QueryDto, which accepts sort/order, but SubjectsReadService.findAll uses a FIXED orderBy clause that never reads either field | Always sorted case-folded name then id, regardless of what was requested | subjects-read.service.ts |
12.6 Flow-to-Data Trace
| Flow | Reads | Writes | Cache | Jobs/Events | Response Fields |
|---|---|---|---|---|---|
| List subjects | subjects, correlated class_subjects count | — | academic-structure:subjects:list:* | None | publicId, name, code, type, defaultMaxMarks, defaultPassMarks, isActive, classCount, version, createdAt, updatedAt |
| Create subject | subjects (uniqueness via index) | subjects | Prefix invalidated | None | Full SubjectDto |
| Assign to classes | subjects, classes, staff+designations, class_subjects (collisions, existing) | class_subjects | Prefix invalidated | None | assigned: ClassSubjectDto[], skippedSuspended: string[] |
| List a subject's assignments | class_subjects joined to classes/grades/sections/staff/users/designations | — | Never cached (scope-dependent) | None | ClassSubjectDto[] |
| Pupil own subjects | student_class_enrollments, class_subjects, subjects, classes/grades/sections, staff/users, academic_sessions | — | Never cached | None | MySubjectDto[] (no ids, teacher name only) |
12.7 Experience Quality Checklist
- The doc explains what the actor is trying to accomplish — define what the school teaches and who teaches it where.
- The doc explains what the backend does that the actor does not see — copy-not-reference defaults, cache invalidation, scoped row resolution.
- The doc covers every minor flow and branch — suspension skip, reactivation collision, batch partial failure, cache fail-soft.
- The doc includes user, admin, worker, and system flows where applicable — no worker/system flow exists on this module, stated explicitly rather than omitted.
- The doc explains business logic, tradeoffs, and rationale — 12.3, 12.4.
- The doc maps every flow to API routes and backend side effects — 4, 12.6.
- The doc includes diagrams appropriate to each flow type — sequence, state, flowchart.
- The doc covers all edge cases and failure recovery — 12.5, 10.
13. Completion Checklist
- Every feature, minor action, and submodule capability is listed.
- Every actor has allowed and forbidden behavior.
- Every major and minor flow includes steps, branches, and diagrams.
- Every lifecycle has a transition table and state diagram.
- Every flow links to the API and backend docs.
- No TDD document exists for this module — there is no
tdd.mdxpage and none is referenced; the schema file's own extensive docblocks serve as the technical design record.
See Also
- API doc:
/docs/developer/subjects/api - Backend doc:
/docs/developer/subjects/backend
Classes API Reference
Complete API contracts for grades, sections, rooms, classes, and student class enrolments, including routes, auth, DTOs, responses, errors, and examples.
Subjects Backend Documentation
Backend architecture, data model, services, cache, runtime rules, and operational behavior for the school's subject catalogue and its per-class teaching assignments.