Skip to content

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. armAt uses 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.