Disciplinary Notes¶
Disciplinary notes are student-centred records of a behavioural incident: one student, one occurrence day, and objective free text. V1 deliberately has no category, severity, context, title, attachments, follow-up workflow, or arbitrary text-length cap.
Design history: 2026-08-11-disciplinary-notes-design.md.
1. Lifecycle¶
create ──> PENDING ──grace+sweep──> DELIVERED ──explicit referent ACK──> ACKNOWLEDGED
▲ │
└──────── edit/re-arm ───────┘
PENDING or DELIVERED ──withdraw──> WITHDRAWN
PENDING: staff can read it; family cannot.armAtuses the shared family-alert grace.DELIVERED: the sweeper has claimed it and notified 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.
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, secretary, principal | Whole tenant |
| 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: direct course, combined-class sibling roster, tutored homeroom, or supervised-activity audience |
| Referent | Currently linked children; 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.
Participation (2026-09-03). The optional cohort filter (cohortType + cohortId) is anchored on today, so every cohort shape is ANDed with the live fence status = ENROLLED (participatingStudentWhere): only enrolled students are a cohort's members in the list. The writable-date gate (findStudentPlacedOn) carries the dated limb status ≠ PRE_ENROLLED beside the placement interval — the 2026-08-11 design meant it to refuse pre-enrolment, but a pre-enrolled student holds an open placement, so the interval alone never did. A note about a pre-enrolled student now answers 422 DISCIPLINARY_NOTE_STUDENT_NOT_PLACED (copy widened to "not enrolled and placed on that date"); post-exit notes were already refused. Existing notes of a student who has since left stay reachable through the policy-only routes. Design: 2026-09-03 spec.
3. Mutation authority¶
School-wide management and scoped department/curriculum management may create notes for and mutate any unacknowledged note within their visible rows. 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 are authoritative. They incorporate row visibility, role/parameter authority, teacher authorship, lifecycle, and the acknowledgement lock.
4. REST contract¶
Routes are under /disciplinary-notes:
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Paginated visible list |
GET |
/:id |
Detail, including authorized 410 tombstones |
POST |
/ |
Create { studentId, date, text } |
PATCH |
/:id |
Edit date and/or text; empty bodies are invalid |
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?, 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.
5. Delivery and acknowledgement¶
DisciplinaryNotesNotifierService is a per-tenant scheduled sweeper using the same ATTENDANCE_ARM_GRACE_MINUTES and ATTENDANCE_SWEEP_INTERVAL_SECONDS settings as attendance. It scans due PENDING rows 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 had no effective placement on that 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: 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. Deferred¶
Per-referent receipts, reminders, follow-ups, categories, severity, context, attachments, comments, appeal/escalation workflows, bulk operations, per-note history endpoints, and notification retry/outbox semantics are outside v1.