Skip to content

Attendance register

Attendance records what the school observed for a student, on a date, at a scheduled occurrence. The cell stores its current value and frozen teaching context; audited edits preserve the value history. Family communications describe what the family announced or justified and do not create attendance cells.

The module has management, teacher, and family surfaces. They share dated schedule resolution and day classification, but have different authority and response contracts. Related chapters: timetables, audit log, notifications, and reports.

1. Cells, projection, and participation

One AttendanceRecord identifies one (tenantId, studentId, date, startTick). startTick counts five-minute steps from midnight, as in a scheduled lesson. Every cell has a tick, including in a DAILY department.

  • PERIOD: each scheduled occurrence shows its own authored cell or remains not recorded.
  • DAILY: authored cells describe changes during the day. Reads project them across the student's scheduled occurrences.

The department supplies attendanceMode; the write freezes it onto the cell. projectDayStatuses in attendance-day-model.ts owns both modes.

Authored status Later unrecorded slots Slots before the earliest authored cell
LATE_ENTRY PRESENT ABSENT
EARLY_EXIT ABSENT PRESENT
Other status Same status Same status

For example, a DAILY student marked ABSENT at 08:00 and LATE_ENTRY at 10:00 shows ABSENT at 09:00 and PRESENT at 11:00. The late-arrival fact still appears in the day summary.

RECORDED identifies a stored cell with an id and provenance. PROJECTED is derived status without an editable cell id. A null source/status means not recorded; it is not absence.

Dated rosters require effective placement and membership and exclude PRE_ENROLLED through participationHistoryStatusWhere. A departed student remains on dates before their exit. Live follow-up and justification worklists instead use participatingStudentWhere (ENROLLED). Do not substitute the live fence in a historical reader.

Participation status itself is not versioned: changing PRE_ENROLLED to ENROLLED makes placement-covered dates eligible. An incorrect enrolment date must be corrected through the placement command. Cohort ownership checks remain status-neutral so an authorized manager can reach an empty cohort.

2. Snapshot and storage contract

At creation the cell snapshots student identity, department/grade/homeroom, the physical teaching anchor, subject context when applicable, room, actual teachers, wall bounds, mode, and optional unit tags. Display these values from the cell when describing the recorded event.

The hard Restrict references are tenant, academic year, and student. Teaching-context ids are soft references with snapshotted names. A later rename or structural change does not rewrite the record.

recordedByUserId, recordedByName, and recordedAt identify the original author. lastModifiedBy* and lastModifiedAt identify the latest edit. A same-anchor rewrite changes values and modification provenance only. A different-anchor assertion at the same student/date/tick replaces the teaching snapshot and audits the prior evidence; first-author provenance remains.

Physical anchors

An anchor contains exactly one of subjectGroupId, combinedClassId, or activityId, plus startTick.

  • A subject-group cell names that course and its subject.
  • A combined cell names the physical combined meeting. Public singular course/subject fields are null even if the student contributes through only one member.
  • An activity cell names the activity and has null course/subject/combined fields.

attendance-anchor.ts normalizes public fields and labels. Public cells do not expose courseAttributions or attributionStatus. Stored historical courseAttributions data remains available to authorized audit presentation through the decoder in course-attributions.ts; ordinary reads do not reconstruct course evidence from current membership.

A nullable cell justificationId is historical provenance, not a current write instruction or a foreign-key guarantee. Justification commands do not set it by creating cells.

Day-shape evidence

AttendanceDayShapeService.freezeDay captures AttendanceDaySlot rows inside a register write transaction. It is idempotent and preserves an existing manifest.

The manifest is write-side evidence, not a read source. Every register read resolves dated versions and relationships through the expected-attendance resolver. Reads neither create manifests nor fall back to one. Preserve stored evidence, including rows whose source is READ, without treating it as the current schedule oracle.

3. Resolving a scheduled day

ExpectedScheduleResolver finds the timetable version whose half-open valid-time interval covers the requested date, then hydrates that version's geometry. An ARCHIVED version can govern a historical date or today's retained schedule; status = PUBLISHED is not a by-date lookup.

Range reads use forRange(db, tenantId, academicYearId, from, to). It loads the versions overlapping the range once (findTimetableVersionsOverlapping) and, per version, the activity graph, the whole-week lessons and the legacy duration cascade once. resolve(date) then picks the covering version in memory (timetableVersionCovers) and builds the day without a query; a date outside the range is rejected. Both paths build days with the pure buildDaySlots (attendance-day.queries.ts), and a combined lesson's sections on a date are the combination's memberships effective on that date, filtered in memory with the half-open predicate. The two paths therefore resolve every date identically. The single-date read costs three Prisma reads (version, activity graph, the weekday's lessons), plus the two cascade reads only when the day has legacy unstamped lessons.

ExpectedAttendanceResolver composes that geometry with placements, course enrolments, teaching episodes, combination membership, homerooms, and selections effective on the same date. It resolves once per operation and passes its result to planners and projections.

On a same-day boundary, effectiveOnOrTailOn includes relevant tail episodes and governsSlot(row, date, startTick) selects the relationship at the occurrence. A student moved at 14:00 remains on the old 09:00 roster. Day-level grouping and activity audience expansion use end-of-day indexes; do not confuse them with the slot-level course roster.

Ordinary lesson teachers are filtered through the version's ALL/SELECTED teacher selection, then assistants are added. Activity supervisors come from that version's weekday-qualified assignments. Teacher names can be current while membership is dated; a cell freezes both when recorded.

A date without a governing version resolves to an empty expected schedule. Reads show nothing owed. Writes reject with ATTENDANCE_NO_PUBLISHED_TIMETABLE. An anchor absent from a resolved day returns its ordinary lesson/activity 404; never fall back to whatever is currently published.

School days

A recordable date must:

  1. Fall inside the department's calendar.
  2. Have an operative weekday for the student's grade, derived from lessons or attendance-bearing activities in the governing version.
  3. Fall outside department CLOSING periods.

The board batches calendar and membership inputs; the write planner keeps a request-local department/grade cache. Both must implement the same predicate. An ACTIVITY cohort has one department but no single grade, so its board school-day calculation is department-based.

Use the school timezone through shared clock helpers. A missing school configuration uses the helper's UTC fallback, never the server's local zone. Calendar dates and instants are distinct inputs.

4. Classification, counters, and inconsistencies

classifyStudentDay reads authored cells, not projected slots:

Classification Meaning
notTaken No cell authored that day
suspended At least one SUSPENDED cell; takes precedence over other buckets
trip FIELD_TRIP or DAY_TRIP, unless suspended; replaces ordinary buckets
absent Every authored cell is ABSENT
lateEntry / earlyExit The corresponding transition appears in the authored day
present PRESENT appears without either transition

The counters overlap: a late arrival followed by an early exit belongs in both. Use numStudents as the denominator, never their sum. notTaken is disjoint.

toRegister counts owed cells for elapsed slots in school-local time. PERIOD owes each elapsed missing occurrence; DAILY owes one take per student, attributed to their first elapsed slot. Future or uncovered dates use null rather than zero. Board totals include only school-day groups; closed group cards can still appear.

inconsistencies counts contradictions, separately from student counters. detectDayInconsistencies walks authored cells through IN_SCHOOL, OUT_LEFT, and OUT_ABSENT. The first cell is not a contradiction; trips and suspension do not advance the machine.

Previous state Status Code
OUT_ABSENT PRESENT PRESENT_AFTER_ABSENCE_WITHOUT_LATE_ENTRY
OUT_ABSENT EARLY_EXIT EARLY_EXIT_WHILE_ABSENT
OUT_LEFT PRESENT PRESENT_AFTER_EARLY_EXIT
OUT_LEFT EARLY_EXIT EARLY_EXIT_AFTER_EARLY_EXIT
IN_SCHOOL ABSENT ABSENT_WITHOUT_EARLY_EXIT
IN_SCHOOL LATE_ENTRY LATE_ENTRY_WHILE_PRESENT

Inconsistency cards carry localized copy and both cells' frozen context. Render the message rather than mapping the code to client-written text.

5. Board, rows, and the shared read pipeline

AttendanceBoardService.assembleGroupDays supplies the board, pending list, rows, and inconsistency projections. Its catalogue and expected slots share one captured date.

Cohorts are HOMEROOM, GRADE_GROUP, SUBJECT_GROUP, COMBINED_CLASS, or ACTIVITY. The management board catalogue is student-derived homerooms and classless grade groups. Explicit teaching-unit/activity reads construct a synthetic group. Existing teaching units can return an empty roster; an empty student-derived class may have no group and return 404.

Structural filters and search select whole groups, keeping their full counts. curriculumId instead narrows students inside each group. A cohort type/id pair scopes a widget to one group. Follow-up search narrows students directly using the shared person-search rule.

Student rows

items[] is the student's schedule strip. register[] is the authored day summary and includes cells that no longer have a matching expected slot. Never reconstruct the latter by filtering the former for RECORDED items.

GET /attendance/rows can filter items[] by startTick; teaching-unit cohorts also require the matching anchor. Day classifications, events, register entries, and the group card remain whole-day facts. Write responses always carry whole-day rows.

Every student row includes department, grade, homeroom, and curriculum ids/names. Department and grade prefer effective placement, with head metadata only as a display fallback. Homeroom and curriculum resolve independently on the requested date. Sorting follows effective department name, grade ordinal, homeroom name (unassigned last), student surname/name, and id.

A RECORDED item and each register cell expose frozen coveredUnitIds/coveredUnitNames. Projected items carry empty arrays: lesson content is an authored observation, not a carried attendance state.

Keep expected-fact loading batched. Query-count specifications in src/attendance/expected/ protect bounded loader composition; they are not latency guarantees.

6. Read admission and field grants

attendance.policy.ts defines distinct admissions:

Policy Surface Required service narrowing
AttendancePolicy Cell writes and history Qualified management departments, record policy, and per-cell write authority
AttendanceBoardPolicy Board, pending groups, follow-up operations, school justification list Qualified management departments
AttendanceCohortReadPolicy Rows, inconsistencies, school justification downloads Management department fence or an independently valid teacher relationship
AttendanceTeacherPolicy Teacher-day Calling teacher's own dated occurrences
AttendanceFamilyPolicy Family routes Linked students for referents; self for students

Aggregate services do not obtain record narrowing merely by applying a policy decorator. Their catalogue, records, and count queries must enforce the resolved scope.

Entity attendance has scope register. take and manage_communications require WRITE; justify requires READ. The scope/action grant and admitted policy branch are both necessary. Family mutation additionally requires referent identity and per-link canWrite; a management role's justify grant does not admit it to family routes.

School-wide READ includes admin, SEN Manager, Director, Communications Manager, Front Office, Administrative Assistant, and HR Manager. School-wide WRITE uses admin and the Front Office/Administrative Assistant functions. SEN, Director, Communications Manager, and HR do not obtain writes from their read breadth.

DEPARTMENT parameters resolve to permitted departments; CURRICULUM parameters resolve to owning departments in the relevant year. Scope/action authority and ids must come from the same qualified assignment. Consult AttendanceAccessService.resolveManagementAttendanceScope and the preset catalogue in permissions; do not flatten ids across independently incomplete grants.

7. Per-cell write authority

canWriteStudentCell accepts a complete management limb or the teacher relationship for the targeted cell:

  • A school-wide writer can write any admitted student cell.
  • A department manager can write within the qualified department set.
  • A related teacher can write a slot they teach/supervise/assist or a student whose dated homeroom they tutor.

Teaching another occurrence on the same date does not grant this occurrence. The homeroom membership is dated to the cell's slot, but the tutor identity is the homeroom's current pointer.

Selected management assignments can also supply a Teacher-anchored relationship limb, independently of their parameter coverage. A Staff-only anchor does not. A role missing the operation's grant cannot lend either ids or a teacher anchor to another role.

For cell edits, frozen ordinary teaching evidence remains authority. Frozen assistant ids alone do not: attendanceTeacherAuthorityIds subtracts the recorded assistant subset and adds a fresh dated assistance witness. This lets a same-day added assistant operate the parent occurrence and prevents a removed assistant retaining that grant solely through old evidence.

Every write also needs a readable response cohort. The service checks cohort ownership/admission before mutation, because its envelope contains a group card, inconsistencies, and follow-ups. A legitimate cell write cannot be used to obtain an unrelated class's aggregate.

Write window

All school-side writes require date <= schoolToday; future dates return ATTENDANCE_FUTURE_DATE_LOCKED. Past corrections require a complete school-wide or matching department-management limb. Related-teacher-only authority returns ATTENDANCE_PAST_DATE_LOCKED. Today permits any complete authorized limb.

Family announcements use their separate temporal rules and do not bypass these gates by creating cells.

7.1 Teacher-day and cohort reads

GET /attendance/teacher-day defaults to school-today and returns:

  • today[]: regular lessons, assisted parent lessons, and supervised activities on the date, each with a server-minted anchor and a cohort selector.
  • duties[]: break/lunch supervision with time, room, other duty teachers, and audience labels. These slots have no attendance anchor or counters.
  • otherGroups[]: other courses related to the teacher on that date, including dated assistance relationships, with roster counts.

A combined meeting appears once. Assistance shows the caller's assistanceIntervals[] on the parent card; its attendance anchor is the parent lesson start. Access is not limited to the shift's wall-clock interval.

A card's needsAttendance expresses duty, not completion. PERIOD requires every occurrence; DAILY requires a slot opening the day for at least one of its students. recorded counts students covered, and inconsistencies counts only contradictions triggered at that card's tick. The latter can be smaller than a whole-cohort drill-down.

A caller without an active-year teacher profile receives empty arrays. Future dates are an agenda preview; the write window still applies.

Teacher cohort reads require a dated taught/assisted SUBJECT_GROUP or COMBINED_CLASS, a tutored HOMEROOM, or an ACTIVITY supervised on the queried weekday. No teacher relationship grants an unrestricted board or a GRADE_GROUP. A mixed session may use a complete teacher limb outside its management departments. Cell-edit response admission uses the record's own start tick; ordinary cohort reads use their day-level gate.

The “what we did today” editor is the SubjectGroup class-register feature in assignments, not attendance state.

8. Status rules

Statuses are PRESENT, ABSENT, EARLY_EXIT, LATE_ENTRY, FIELD_TRIP, DAY_TRIP, and SUSPENDED.

attendance-status-rules.ts permits time only on transitions (EARLY_EXIT/LATE_ENTRY) and note on all statuses except PRESENT/ABSENT. Notes are trimmed; blank becomes null. Invalid combinations return VALIDATION_FAILED.

Batch writes validate submitted values. PATCH merges sparse fields, clears time when changing to a non-transition, and clears note on PRESENT/ABSENT before validation. Explicit null clears time/note. Justification is event/communication state, not a boolean on the cell.

9. Batch writes

POST /attendance/records accepts {date, cohortType, cohortId, unitIds?, entries[]}. Each entry is {studentId, anchor, status, time?, note?}. Echo the anchor returned by the register.

The batch must be nonempty and unique per student/startTick. It is one declared cohort and can span several scheduled anchors.

The service:

  1. Resolves the grant-qualified cohort and write-window authority.
  2. Takes the shared academic-year/day advisory lock inside the write transaction before resolving the expected day.
  3. Requires a governing version and checks every student against the declared cohort.
  4. Plans each distinct anchor once; each keeps its own dated roster, actual teachers, geometry, and curriculum unit facts.
  5. Validates unit ids and all per-entry roster, authority, status, and school-day gates before writing.
  6. Captures day-shape evidence, upserts cells in deterministic student/startTick order, reads exact touched pairs back, and writes audit evidence.
  7. Reclassifies touched student-days through AttendanceLoopService in the same transaction.

The day lock coordinates with timetable publication and temporal changes that take its exclusive side. Do not move expected resolution outside it to shorten the transaction.

Upsert uses the database cell identity. Same-anchor writes preserve the snapshot; reassertion through another physical anchor replaces it and records prior anchor/evidence in audit data. The audit create/update label comes from an in-transaction pre-read; under a competing insert it can describe a cell as created even when the upsert updates it. Cell value evidence still records the mutation.

The response is {records, group, inconsistencies, followUps}. Each written cell includes its whole student-day row, shared with the ordinary row projector. A student touched twice has the same row on both results. group and row can be null if that cohort no longer assembles. Follow-ups are capped by WRITE_FOLLOW_UP_LIMIT (50). Returned records preserve submission order.

Observed units

unitIds records lesson content covered during the session. Validate ids against the subjects of the dated anchor; a combined session stores flat session tags, not inferred per-student course attribution. An activity has no unit tags. Same-anchor rewrites preserve the recorded tags; a new assertion can snapshot a new context.

10. Cell edits and history

PATCH /attendance/records/:id loads a visible cell, prepares its sparse value update, checks the write window and authority, and resolves the response cohort. An optional cohort type/id pair must admit that student on the record's own date/year. A cohort-only or empty body is invalid.

Without an echo, management gets the cell's class/grade group, a lesson teacher or supervisor gets its physical unit/activity, and a tutor-only writer gets the homeroom governing the cell's slot.

Inside the transaction, PATCH takes the shared day lock, locks and reloads the cell, reloads assistance witnesses, and revalidates authority before updating and auditing. Ordinary changes use attendance_record.updated; a school-wide override outside the writer's teacher reach uses attendance_record.admin_override. Loop state is reclassified in the same transaction.

GET /attendance/records/:id/history returns {current, history}, with history projected from audit_logs. A cross-anchor change keeps the replaced context in audit data. Ordinary snapshot renames/reassignments never trigger a refresh.

Clearing cells

"Not taken" is the absence of a cell, not a status, so clearing deletes the attendance_records row. Board counters (notTaken, toRegister) are computed from row absence and count the student as still owed straight away. There is no restore: the register is retaken. Clearing writes no audit row, so it is the one attendance mutation outside the audit trail; the earlier created/updated rows of the deleted id stay readable only through the id.

Route Request Behavior
DELETE /attendance/records/:id Optional cohortType + cohortId echo (both or neither), as on PATCH Clears one cell. A missing, hidden or already cleared id is 404 NOT_FOUND; any other refusal throws its own code
DELETE /attendance/records Body {date, cohortType, cohortId, recordIds}, 1-200 unique ids Clears the cells of one group and day that the caller may clear, and lists the rest

Both answer 200 with {cleared, skipped, group, inconsistencies, followUps}: cleared[] is {id, studentId, startTick, row} with row the student's day after the clear, and group/inconsistencies/followUps refresh the board without a second read. skipped is always empty on the single route, which throws instead.

Per-cell checks reuse the PATCH admission:

  1. Visibility is the PATCH filter; hidden looks like missing (NOT_FOUND).
  2. The date must not be in the future. On the list route a cell recorded on another day than date is skipped as OUTSIDE_COHORT.
  3. A past day needs management authority over the cell's department; otherwise ATTENDANCE_PAST_DATE_LOCKED (PAST_DATE_LOCKED in the list).
  4. The per-student write authority is the PATCH one: ATTENDANCE_NOT_AUTHORIZED_FOR_STUDENT (NOT_AUTHORIZED).
  5. List only: the student must be on the declared cohort's roster on that date, otherwise OUTSIDE_COHORT. A caller who cannot write the cohort at all gets a 403 for the whole request, as on POST.

The list is partial by design: one cell the caller may not clear never fails the others. There is no status-based rule: whatever the caller may overwrite, including SUSPENDED and trip cells, they may clear. Clearing twice changes nothing; a cleared id is indistinguishable from one that never existed.

The transaction takes the shared day lock, locks the named rows in id order, reloads them and reruns checks 3-4 (a row that vanished meanwhile becomes NOT_FOUND), deletes them, and reclassifies the touched student-days through the loop. The response is built after commit.

11. Family communication loop

AttendanceDayEvent is unique per student/date/eventType. Events are ABSENT or LATE_ENTRY; trips, suspension, and early exits do not mint an event. mintForStudentDays reuses authored-day classification.

Existing state Day still warrants event Day stops warranting event
No row Create UNDER_REVIEW with arm deadline No operation
UNDER_REVIEW Keep CANCELLED in grace; otherwise SUPERSEDED
NOTIFIED / FOLLOW_UP Keep SUPERSEDED with armAt = now + grace: a retraction is owed
ACKNOWLEDGED / NOT_REQUIRED Preserve resolved history Preserve: a justified absence is settled, no retraction
SUPERSEDED with a retraction owed Back to NOTIFIED (FOLLOW_UP if a reminder had been sent) with the original notification stamps and armAt = notifiedAt; NOT_REQUIRED if a justification now covers it. Nothing is sent Preserve
CANCELLED / any other SUPERSEDED Reopen with a new grace period and cleared episode metadata Preserve

A covering justification makes a minted/reopened event NOT_REQUIRED with no arm deadline. Cell writes and event changes share a transaction.

The retraction follows any change that stops a notified event being warranted: a clear, or an edit from ABSENT to PRESENT. The grace before it lets a clear followed by a retake restore the event silently, so the family never sees "cancelled" then "absent".

armAt means different things by state. On UNDER_REVIEW it is the grace deadline (the notify scan and family visibility compare it with now). On SUPERSEDED it is the retraction clock, and the sweeper sets it to null when it claims the retraction. A retraction is owed when state = SUPERSEDED AND notifiedAt IS NOT NULL AND armAt > notifiedAt: a notification is stamped only once armAt <= now, so a SUPERSEDED row that never owed one (or whose retraction was claimed) fails the last condition. Reads outside UNDER_REVIEW never use armAt; they filter on state first.

The loop locks the day's event rows (SELECT ... FOR UPDATE) before reading them, on every write. The sweeper's claim then waits for that lock and re-checks its condition: either the loop sees the event still pending and the claim fails, or it sees it already notified and owes the retraction. Without the lock a send could slip between the loop's read and its write.

Open worklists include NOTIFIED, FOLLOW_UP, and armed UNDER_REVIEW. Office day detail includes all states. Family-visible history includes ACKNOWLEDGED and NOT_REQUIRED too, but never CANCELLED, SUPERSEDED, or in-grace UNDER_REVIEW. Predicate helpers contain OR clauses and must compose under AND.

Acknowledgement and notifications

PATCH /attendance/follow-ups/:id/ack can resolve an open event even during grace. It creates a one-day justification with PHONE or IN_PERSON channel, stamps acknowledgement/provenance on the event, and audits both facts. A late-entry time is copied from the earliest matching cell when available. It writes no cell; APP is reserved for family submission. The state change is compare-and-swap: a lost race answers 409 ATTENDANCE_EVENT_ALREADY_RESOLVED and leaves no second justification.

PATCH /attendance/follow-ups/ack with { scope: "all_open", channel, reason? } resolves every open follow-up the caller can see. The selection is the unfiltered Follow-up card's predicate (AttendanceInsightsService.openFollowUpIdsFor), so in-grace UNDER_REVIEW events are not touched and a department-scoped caller gets only their departments; list filters are rejected by validation. Scope and actor are resolved once, then each event runs the shared per-event write in its own transaction, so one failure never rolls back the others. Per-event outcomes are acknowledged, already_acknowledged (not live any more when its turn came, carrying the state found) and failed (the error code, INTERNAL_ERROR for an unexpected exception, which is logged and not exposed); the response also carries counts and the refreshed card summary, and is 200 even when some events failed. The loop is synchronous and uncapped.

AttendanceNotifierService runs tenant-scoped scheduled passes:

  1. Armed UNDER_REVIEW becomes NOTIFIED and requests attendance.event.
  2. NOTIFIED past its escalation deadline becomes FOLLOW_UP and requests attendance.reminder.
  3. SUPERSEDED with a retraction owed and its clock past (notifiedAt < armAt <= now) has armAt set to null and requests attendance.retracted, which carries the same payload and recipients as the absence notice and opens the student's day.

The reminder pass only takes NOTIFIED, so a SUPERSEDED event is never reminded; the retraction pass is its only way out.

Each state/deadline claim is compare-and-swap and commits before delivery. This is at-most-once triggering: process failure between claim and send can lose that send. Worklists remain the operational record. Acknowledgement and coverage remove events from claimable states; notification delivery does not change attendance.

Recipients are linked referents with accounts, through the notification engine. Zero reachable recipients does not undo the state transition.

Configuration Default Meaning
ATTENDANCE_ARM_GRACE_MINUTES 30 Correction grace after the write, and the wait before a retraction
ATTENDANCE_REMINDER_DELAY_MINUTES 60 Delay after initial notification
ATTENDANCE_SWEEP_INTERVAL_SECONDS 60; 0 in test Timer cadence; zero disables the timer

The grace and sweep pair is shared with disciplinary notes through family-alert-timing.ts. The reminder delay is attendance-specific. Validation bounds live in configuration validation.

11.1 Family reads

Family routes require attendance READ and AttendanceFamilyPolicy. familyStudentAccessWhere narrows referents to linked students and students to themselves. A missing, foreign, or unlinked target uses the same hidden 404.

  • GET /family/to-justify: paginated open events across linked active-year students, with optional student/from filters. It includes the earliest matching cell's author name and the justification kind that covers the event.
  • GET /family/students/:studentId/overview: record-derived day statistics and family-visible event history.
  • GET /family/students/:studentId/days/:date: the shared student-day projection with reduced family event details.

Live lists include ENROLLED students. By-id overview/day reads retain link authority for departed students' history.

Overview statistics classify record-bearing days. Trips/suspension are excluded from the school-day denominator. Present hours approximate authored cell intervals, without DAILY projection; sparse DAILY recording therefore underweights unrecorded intervals. The configured department threshold is a comparison value, not proof of a complete hours ledger.

The family day DTO names the authored summary cells[]; a top-level register key would collide with the scope name on an aggregate response. It omits office-only acknowledgement reason/actor and contact detail. Justifications appear independently of events.

11.2 Justification commands

Referents submit dated statements. There is no approval workflow and no office edit/delete route. Family mutations require the justify action plus per-link canWrite; students only read.

Kind Dates Time Reason Attachments
ABSENCE Inclusive range; end required Forbidden Required Up to 5
LATE_ENTRY One day Required Required Up to 5
EARLY_EXIT One day Required Optional None

The range must fit the student's department calendar. validateKindShape owns the shape contract.

Today/future submissions are announcements. A submission covering past dates needs at least one family-visible event in its past portion that the kind can answer; otherwise JUSTIFICATION_DAY_NOT_FLAGGED. A past EARLY_EXIT cannot pass because it has no event type. There is no additional answer deadline while the year is writable.

ABSENCE covers ABSENT events only; LATE_ENTRY covers LATE_ENTRY only; EARLY_EXIT covers none. At submit, matching live events become ACKNOWLEDGED via APP. At event mint, existing coverage produces NOT_REQUIRED. Store the justification id as provenance rather than deriving it again on reads.

Consistency and mutability

Incoming statement Existing overlapping ABSENCE Same single-day kind Other single-day kind
ABSENCE Conflict Supersede Supersede
LATE_ENTRY / EARLY_EXIT Conflict Create overwrites in place; PATCH conflicts Coexist

An in-place resubmission preserves row identity and attachments while replacing substance/submitter. A superseding absence removes swallowed single-day statements and their attachments and updates covered event provenance according to the loop rules. Office-channel statements participate in the same consistency matrix; resubmitting a same-kind single-day statement can replace its channel with APP while the settled event keeps its acknowledgement evidence.

PATCH/DELETE require channel = APP and startDate > schoolToday. The entire range freezes together. PATCH checks both stored and proposed range; kind and studentId cannot change. canEditJustification combines this window with per-link write authority and is the shared family read/write response rule. Office rows do not carry canEdit.

Before withdrawal, reopen events resolved by that APP statement or pre-empted as NOT_REQUIRED, clear episode metadata, and start a new grace period. Do this before deleting the row, because its FK otherwise sets event provenance to null. Office acknowledgements and cancelled/superseded events are preserved. PATCH reopens only events outside surviving coverage.

Attachments are independent evidence and do not use the statement's date mutability window. Authorize ownership and per-link writes, enforce the kind/cap, and use AttendanceJustificationFilesService and FilesService. Attachment controls must not be disabled solely because canEdit is false.

Read projections and register separation

The family archive is paginated by submission recency and supports student/from/kind filters. The school list requires manage_communications and a qualified department fence. AttendanceJustificationsReadService owns family, school, and nested day projections; nested justifications[] includes communications even before a register/event exists.

No justification create, update, withdrawal, acknowledgement, or attachment operation materializes attendance. An announced day remains notTaken until the school records it; its projected strip has no authored cell merely because a statement exists. A subsequent matching school observation produces a covered NOT_REQUIRED event through normal minting.

12. API and extension map

All paths below are under /api/v1/attendance; the controllers and DTOs remain the authoritative detailed contract.

Surface Routes
Management board GET /groups, /groups/pending
Cohort detail GET /rows, /inconsistencies
Teacher agenda GET /teacher-day
Register writes/history POST /records; PATCH /records/:id; DELETE /records/:id and /records (clear); GET /records/:id/history
Follow-up operations GET /follow-ups, /follow-ups/summary; PATCH /follow-ups/:id/ack
Family reads GET /family/to-justify, /family/students/:studentId/overview, /family/students/:studentId/days/:date
Family statements GET/POST /family/justifications; PATCH/DELETE /family/justifications/:id
Family attachments POST /family/justifications/:id/files; GET/DELETE /family/justifications/:id/files/:fileId
School communications GET /justifications, /justifications/:id/files/:fileId

Date inputs use date-only validation; do not truncate timestamps. Cohort type/id pairs must be complete. Server-minted anchor XOR and student/tick uniqueness apply regardless of cohort.

Concern Canonical source
Dated geometry and membership src/attendance/expected/
Board and shared rows attendance-board.service.ts, attendance-day.queries.ts
Projection and contradictions attendance-day-model.ts, attendance-inconsistency.ts
Qualified authority attendance-access.service.ts, attendance-authority.ts, attendance.policy.ts
Register commands and snapshots attendance.service.ts, attendance-planning.ts, attendance-anchor.ts
Teacher agenda attendance-teacher.service.ts
Family/event loop attendance-family.service.ts, attendance-loop.service.ts, attendance-notifier.service.ts
Justifications attendance-justifications.service.ts, attendance-justifications.read.service.ts, attendance-justifications.shared.ts
Evidence attachments attendance-justification-files.service.ts
Bounded report reader and register-taken dates src/attendance/reporting/ (AttendanceReportReader, loadRegisterTakenDates)

When adding a status, update validation, forward/backward projection, day classification, inconsistency behavior, and event coverage together. When adding a cohort, update ownership, roster, admission, projection, and write-envelope behavior together.

Never identify a schedule slot by tick alone: option-block lessons can share a tick. Use tick plus physical anchor. Conversely, the cell identity is student/date/tick regardless of anchor; two overlapping scheduled items can display the same stored fact.

Do not refresh snapshots after a structural edit or infer combined course evidence from today's roster. Preserve deterministic write ordering and the shared day-lock boundary. Keep report classification aligned with the register while respecting report-specific selection and coverage rules in reports. Report-card attendance reads the same reader for a student set and counts a date only when someone in the department has a cell on it; see report cards.