Disciplinary Notes¶
Disciplinary notes are student-centred records of a behavioural incident: one student, one occurrence day, and objective free text. The API stores free text rather than a category/severity workflow; request-size limits still apply.
Canonical implementation: src/disciplinary-notes/disciplinary-notes.service.ts, disciplinary-notes.queries.ts, disciplinary-notes.policy.ts, and disciplinary-notes-notifier.service.ts.
1. Lifecycle¶
create ──> PENDING ──grace+sweep──> DELIVERED ──explicit referent ACK──> ACKNOWLEDGED
▲ │
└──────── edit/re-arm ───────┘
PENDING or DELIVERED ──withdraw──> WITHDRAWN
PENDING: authorized school-side callers can read it; family cannot.armAtuses the shared family-alert grace (30 minutes by default).DELIVERED: the sweeper has claimed it for an alert to currently linked active referent accounts. Zero reachable accounts still completes delivery.ACKNOWLEDGED: one currently linked referent explicitly confirmed it. The acknowledgement is shared for the family and permanently locks edit/withdraw.WITHDRAWN: a tombstone, never a deleted row. Text and a pending arm are cleared. School-side detail returns 410. A currently linked referent gets 410 only if the note had previously been delivered; otherwise existence remains hidden by 404.
Every successful edit returns the row to PENDING, increments revision, sets a fresh armAt, clears notifiedAt, and retains firstNotifiedAt. A later alert is therefore an UPDATED notification.
A note's kind is fixed at creation. STANDARD (the default when kind is omitted, and the column default) follows the lifecycle above. INTERNAL is staff only: armAt stays NULL on create and edit (the CHECK kind = 'INTERNAL' ⇒ arm_at IS NULL enforces it), so its recipients stay PENDING for their whole life, it is never notified, and its acked is always false. Edits still increment revision. To change visibility, withdraw the note and create a new one.
2. Visibility¶
All access is tenant-scoped and based on current school-day relationships, even when reading notes from an older academic year.
| Caller | Visible students |
|---|---|
| Platform admin, admin, Front Office function, Director function, SEN Manager | Whole tenant for reads; mutation grants are separate |
| Department head or another DEPARTMENT-parametric role | Students currently placed in assigned departments |
| Curriculum coordinator or another CURRICULUM-parametric role | Students with a current selection in assigned curricula |
| Teacher | Canonical current taught set, including dated lesson assistance, direct course, combined-class sibling roster, tutored homeroom and supervised-activity audience |
| Referent | Currently linked children, STANDARD notes only; only DELIVERED and ACKNOWLEDGED rows appear in lists |
| Student, HR, generic staff | No access |
Authorship is not a visibility limb: a teacher may see a colleague's note when the student is in their taught set.
The referent branch of DisciplinaryNotesPolicy carries note.kind = STANDARD, so list, detail and acknowledgement all answer an INTERNAL note as missing (404, never 410). recipientLifecycleVisibility repeats the limb for the family side. School-side readers see both kinds. A caller holding both a school-side role and a referent link reads through the union of their roles.
The optional cohort filter (cohortType + cohortId) uses today's roster and the live ENROLLED fence. Note creation/date edits require placement on the occurrence day and reject PRE_ENROLLED students with DISCIPLINARY_NOTE_STUDENT_NOT_PLACED. Existing notes about students who have left remain reachable through authorized policy-only reads.
Front Office and Director role sets come from src/common/constants/management-role-keys.ts; their function membership supplies record reach, while route scopes/actions determine permitted operations. See Permissions.
3. Mutation authority¶
Management callers with independently qualified WRITE/action grants may create notes and mutate unacknowledged notes within that grant's record reach. A teacher may create for a currently taught student but may edit or withdraw only a note they authored. Referents cannot author or change school content; their only operation is acknowledge, and StudentReferentLink.canWrite is irrelevant to it.
The response's canEdit and canDelete flags incorporate row visibility, role/parameter authority, teacher authorship, lifecycle, and the acknowledgement lock. Commands recheck authority at write time.
DisciplinaryNotesReadPolicy admits school-wide SEN reads, including pending notes. DisciplinaryNotesPolicy supplies mutation reach and excludes SEN. Mutation contexts are rebuilt from qualified WRITE/action assignments, so wider SEN reads cannot lend authority to a Teacher's writes. SEN alone receives both capability flags as false.
4. REST contract¶
Routes are under /disciplinary-notes:
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Paginated visible list |
GET |
/:id |
Detail, including authorized 410 tombstones |
POST |
/ |
Create { studentIds, date, text, source?, kind? } |
PATCH |
/:id |
Edit date and/or text; empty bodies are invalid and kind is rejected |
DELETE |
/:id |
Withdraw an unacknowledged note |
PATCH |
/:id/ack |
Idempotent explicit shared family acknowledgement |
The list accepts page, limit, academicYearId?, studentId?, q?, from?, to?, acked?, kind?, plus the both-or-neither pair cohortType? and cohortId?. Cohort types are HOMEROOM, GRADE_GROUP, SUBJECT_GROUP, and COMBINED_CLASS; they always use today's roster. Omitting the pair returns the caller's entire visible set. The academic year defaults to ACTIVE.
Ordering is fixed: occurrence date DESC, then createdAt DESC, then id ASC. No sort parameters are accepted. Multiple notes for one student and day are valid.
Every response carries kind. acked=false also matches INTERNAL notes; combine it with kind=STANDARD for notes still awaiting a family. Each handler declares its own @AppliesPolicy: RolesGuard reads handler metadata only.
5. Delivery and acknowledgement¶
DisciplinaryNotesNotifierService is a per-tenant scheduled sweeper (a TenantSweeper) using the same ATTENDANCE_ARM_GRACE_MINUTES (default 30 minutes) and ATTENDANCE_SWEEP_INTERVAL_SECONDS settings as attendance. It scans due STANDARD notes (armAt IS NOT NULL AND armAt <= now AND kind = STANDARD; the kind limb is a second guard next to the CHECK) and claims each with a state/deadline/revision CAS in its own committed transaction before calling NOTIFICATION_PORT. This is multi-process safe and intentionally at-most-once; a crash between claim and send can lose an alert, while the delivered note remains visible on the family list.
The discipline.note payload is exactly { noteId, studentId, studentName, date, changeType }. It never contains note text because inbox/email copies outlive edits and withdrawals. There are no reminders or follow-ups.
Reading an inbox notification and acknowledging the note are independent operations. Only PATCH /disciplinary-notes/:id/ack changes acked.
6. Errors¶
| Code | HTTP | Meaning |
|---|---|---|
DISCIPLINARY_NOTE_NOT_FOUND |
404 | Missing, outside current row scope, or not family-visible yet |
DISCIPLINARY_NOTE_WITHDRAWN |
410 | Authorized caller follows a withdrawn note |
DISCIPLINARY_NOTE_ALREADY_ACKNOWLEDGED |
409 | Edit/withdraw attempted after acknowledgement |
DISCIPLINARY_NOTE_NOT_AUTHORIZED |
403 | Visible row, but teacher is not its author |
DISCIPLINARY_NOTE_FUTURE_DATE |
422 | Occurrence date is after the school's current day |
DISCIPLINARY_NOTE_STUDENT_NOT_PLACED |
422 | Student was not enrolled and effectively placed on the occurrence day |
CONFLICT |
409 | A competing edit/withdraw won the version CAS; refresh and retry |
7. Audit¶
Create, update, withdraw, and acknowledge append disciplinary_note.created, .updated, .withdrawn, and .acknowledged inside the same transaction as the mutation. Delivery transitions are machine state and are not audit events; notifiedAt, firstNotifiedAt, and revision are their evidence.
8. Recipes¶
To add a list filter, add DTO validation in dto/list-disciplinary-notes-query.dto.ts, compose a separate AND limb in disciplinary-notes.queries.ts, and add query/service/API documentation coverage. Never merge independent student or OR clauses by object spread.
To change the grace or cadence, configure the shared attendance-named environment variables. ATTENDANCE_REMINDER_DELAY_MINUTES remains attendance-only.
When a referent cannot see a note, check in order: the note is STANDARD; lifecycle is not pending; row is not withdrawn; the referent is currently linked; the referent has an active user account if investigating alert delivery. An account is not required for the note to reach DELIVERED.
9. Relationship and delivery boundaries¶
Teacher contexts load date-bound assistance through the same owner projection used for student/course discovery. This supplies ordinary teacher reach for list/detail/create throughout the effective course/version window. It does not waive occurrence-date validation, author-only edits, acknowledgement locks or capability checks.
Acknowledgement is shared, rather than a receipt for each referent. Delivery has no notification retry/outbox or reminder workflow; the delivered note remains the authoritative family-visible record even if an alert fails.