Class-register assignments and lesson content¶
Homework, upcoming tests and course-day lesson content are owned by src/assignments/. Start with assignments.service.ts for writes, assignments-school.service.ts and assignments-family.service.ts for read audiences, and assignments-attachments.service.ts for child resources. Controllers/DTOs define the public surface; assignments.queries.ts, assignments.visibility.ts and assignments.policy.ts own persistence and access rules.
1. Storage and ownership¶
Assignment is a mutable informational row for one SubjectGroup in one
academic year. It stores HOMEWORK or TEST, trimmed details, selected
assignedDate, inclusive dueDate, transaction-time createdAt, updatedAt,
frozen course/subject/curriculum/department/grade/homeroom labels, and original
author snapshots. teacherId is a soft nullable UUID: Teacher-created items
expose their creating Teacher, while Admin-created items expose teacher: null
and retain the actual Admin in recordedBy* and audit. There is no recipient,
completion, overdue, submission, or status model. Only creating an assignment notifies its course's students; see Creation notification.
AssignmentAttachment is the tenant-bearing mixed child wrapper. A FILE
wrapper references one unique File row with usage
ASSIGNMENT_ATTACHMENT and owner (ASSIGNMENT, assignmentId); a LINK
wrapper stores an absolute HTTP/HTTPS URL and optional label. Its database
CHECK makes those branches exclusive. Assignment deletion cascades wrappers,
while File deletion is restricted until its wrapper has been removed. File
blobs retain the shared post-commit, fail-soft cleanup contract.
CourseDayLessonContent is one shared last-commit-wins value per
(tenantId, academicYearId, subjectGroupId, date). It stores frozen
course/subject labels and soft nullable first/latest Teacher snapshots plus
the actual user/name snapshots. Null text means
cleared; the stable row remains as an audit anchor. It is owned by
src/assignments/, not attendance: the attendance frontend composes this API
with its register call, and no attendance record or frozen day slot stores the
text.
All three tenant-bearing tables carry required tenant anchors, explicit foreign keys, Class-S ENABLE/FORCE RLS, tenant-reset coverage and raw non-blank checks. Domain anchors are restrictive; owned attachment wrappers cascade with their assignment. Assignment
also has dueDate >= assignedDate at the database boundary.
2. Time rules¶
All business dates are YYYY-MM-DD school-calendar dates.
active on T ⇔ assignedDate <= T AND T <= dueDate
assigned on D ⇔ assignedDate = D
due on D ⇔ dueDate = D
Due-today remains active. Passage of time never mutates a row; retained past
items stay in historical day views. Create and update target the active
academic year, forbid a future assigned date, require both dates inside the
course department's calendar, and require dueDate >= assignedDate. Updating
the dates immediately changes active/day projections and can reactivate an
expired item. Explicit hard delete removes the item from every projection.
Neither date must be a timetable occurrence or instructional day.
Lesson content is readable on any admitted course/family day but writable only
when the path date equals tenant schoolToday. A trimmed non-blank string saves
or replaces; null/blank clears; same-value and clear-of-absent requests are
successful no-ops without timestamp or audit noise.
3. Temporal audience¶
Assignments store no recipient rows. Active family visibility uses non-empty
SubjectGroupAssignment effectiveOn(schoolToday) episodes. Once a course is
admitted, all active work is returned even when it predates the membership.
Closed memberships never contribute active work.
For historical assignments, an assignment made on A is admitted by episode
[F,U) when:
There is deliberately no F <= A: after a membership begins, a late joiner
sees the earlier course archive. Multiple episodes union; an open begun episode
dominates closed cutoffs. A due-day may remain visible after exit when the
assignment itself was made before the exclusive cutoff.
Lesson content applies the same retrospective rule to its own date D:
U IS NULL OR D < U. Cleared values are omitted from the family array.
Student sessions are self-only. Referents additionally need the current
non-temporal StudentReferentLink; canWrite is irrelevant. Linking exposes
the student's permitted archive immediately, and unlinking removes it.
Participation. GET /assignments/family/active enumerates
the caller's children, so its access predicate is the family link ∧ the
live fence status = ENROLLED (participatingStudentWhere): a child who has
left, or one not yet enrolled, contributes no active work. The by-id day read
and the attachment download keep the bare link, so an exited child's archive
stays readable under the membership cutoff rules above.
4. API¶
Protected routes:
| Verb | Route | Contract |
|---|---|---|
| POST | /assignments |
Create one assignment from JSON, or atomically create it with repeated files and JSON-encoded links multipart parts |
| GET | /assignments/:assignmentId |
Read one active-year school-side assignment |
| PATCH | /assignments/:assignmentId |
Change type, details, assigned date, and/or due date |
| DELETE | /assignments/:assignmentId |
Hard-delete one assignment; 204 empty response |
| GET | /assignments/subject-groups/:subjectGroupId/days/:date |
Course header, nullable lessonContent, assigned[], due[] |
| PUT | /assignments/subject-groups/:subjectGroupId/days/:date/lesson-content |
Replace/clear today's shared course text |
| GET | /assignments/family/active?studentId= |
{ asOf, items[] }, each item explicitly names its student |
| GET | /assignments/family/students/:studentId/days/:date |
Student header, lessonContents[], assigned[], due[] |
| POST | /assignments/:assignmentId/attachments/files |
Append one atomic multipart file batch |
| POST | /assignments/:assignmentId/attachments/links |
Append one atomic JSON link batch |
| PATCH | /assignments/:assignmentId/attachments/links/:attachmentId |
Correct a link URL and/or label |
| DELETE | /assignments/:assignmentId/attachments/:attachmentId |
Hard-delete one FILE or LINK wrapper; 204 empty response |
| GET | /assignments/:assignmentId/attachments/:attachmentId/download |
School-side signed URL for a FILE wrapper |
| GET | /assignments/family/students/:studentId/assignments/:assignmentId/attachments/:attachmentId/download |
Student/Referent signed URL after the family parent fence is re-proved |
AssignmentDto exposes id, type, details, frozen course/subject/homeroom refs,
a nullable original Teacher ref, assigned/due dates, createdAt, and
updatedAt, and required attachments[]. FILE items expose wrapper id,
name, normalized MIME, byte size, and wrapper timestamps. LINK items expose
wrapper id, URL, nullable label, and wrapper timestamps. The DTO exposes no
internal File id, storage key, uploader, or signed URL. It exposes no internal actor, organizational snapshots,
recipients, or derived state. PATCH accepts a non-empty subset of type,
details, assignedDate, and dueDate; course and original authorship are
immutable. A normalized no-op preserves updatedAt. A same-day assignment
appears once in each day bucket. Day buckets order by createdAt, then id;
active items order by due date, assigned date, student last/first name, then
assignment id. Lesson contents order by subject name, course name, then id.
One Assignment may contain at most ten FILE/LINK items combined. Each file is
at most 3,145,728 bytes and persisted files total at most 15,728,640 bytes;
links consume an item but no byte budget. The exact case-insensitive file
extensions are .pdf, .docx, .xls, .xlsx, .csv, .jpg, .jpeg, and
.png; persisted MIME is normalized from the extension (.jpg/.jpeg to
image/jpeg and .png to image/png). Links are inert data: absolute
HTTP/HTTPS, maximum 2048 characters, with a nullable trimmed/nonblank label of
at most 120 characters. Duplicates are allowed. Wrappers order by createdAt,
then id.
Direct multipart create validates the course, dates, authority, every file,
and aggregate caps before storage. It pre-mints the Assignment id, stages only
blobs, then creates Assignment, File rows, wrappers, links, and the one
assignment.created audit event in the aggregate transaction. Failure
compensation-deletes staged blobs. Every post-create child mutation locks the
parent and re-counts the persisted aggregate before writing. A material child
change touches parent updatedAt; a normalized link no-op does not. Passed-due
items remain correctable through their historical parent authority.
Missing, foreign or hidden course/student anchors use generic 404. Missing, foreign, hidden or deleted assignment IDs use ASSIGNMENT_NOT_FOUND. School-side assignment detail/download and mutations target the active year; course-day reads use the course's year, and family day/download reads use the student's year and retrospective audience. Other semantic errors are
ASSIGNMENT_NOT_AUTHORIZED_FOR_COURSE,
ASSIGNMENT_ASSIGNED_DATE_IN_FUTURE,
ASSIGNMENT_DATE_OUTSIDE_CALENDAR, ASSIGNMENT_DUE_BEFORE_ASSIGNED,
ASSIGNMENT_ATTACHMENT_NOT_FOUND, ASSIGNMENT_ATTACHMENT_LIMIT,
ASSIGNMENT_ATTACHMENT_TOTAL_TOO_LARGE,
LESSON_CONTENT_NOT_AUTHORIZED_FOR_COURSE, and
LESSON_CONTENT_DATE_NOT_TODAY.
5. Authorization and audit¶
The flat assignments entity has descriptor scope record. create,
delete, and set_lesson_content each require record:WRITE; PATCH uses
record:WRITE directly. Teacher holds WRITE and all three actions. Admin
inherits the same command capabilities and is tenant-wide within the active
academic year without needing a Teacher profile. Student/referent hold READ;
the Director and Front Office function roles, SEN Manager, and qualified department/curriculum management read their course reach. Function role sets live in src/common/constants/management-role-keys.ts; read reach does not provide mutation authority. HR and generic staff receive no assignment scope.
Teacher creation requires an active-year active Teacher profile and either a non-empty teaching episode or qualifying lesson assistance on assignedDate. Teacher update/delete
requires the same dated teaching or assistance relationship on the stored assigned date; a changed assigned
date also requires authority on the proposed date. Co-teachers satisfying the
same course/date predicate may mutate the shared item. Teacher lesson-content
writes require that relationship today. Admin bypasses only these Teacher predicates
and may create, update, delete, or set today's lesson content for any
active-year SubjectGroup. Dated school reads admit whole-school management,
qualified immutable course anchors, or a Teacher episode effective on the
relevant date.
Attachment create/update/delete reuses assignments.record:WRITE: Admin is
active-year tenant-wide and Teacher must have the same effective teaching/assistance relationship on the parent stored assigned date.
Metadata inherits every existing parent projection. School download reuses the
school parent READ fence; family download additionally names the relevant
student and re-proves that student's retrospective audience before minting.
Wrong-kind, wrong-parent, foreign, and hidden attachment ids use the same
existence-hiding 404 posture.
Assignment creation, material correction, deletion, and every material lesson-content transition append audit inside the owning transaction:
assignment.createdassignment.updatedwith complete before/after business snapshotsassignment.deletedwith the complete pre-delete business snapshotassignment.attachments_addedfor one post-create batchassignment.attachment_updatedwith public before/after link metadataassignment.attachment_deletedwith public removed-item metadatacourse_day_lesson_content.createdcourse_day_lesson_content.updatedcourse_day_lesson_content.cleared
Reads, deadline passage, membership changes, and idempotent writes add no event. Delete is physical and has no restore API; its audit row survives.
6. Boundaries and relationship authority¶
The API has no completion/submission state, deadline reminders, restore/history endpoint, target-course update or per-period lesson text. Links are inert HTTP/HTTPS values: the backend does not fetch previews. File/link metadata and explicit download routes share parent access checks.
Creation notification¶
AssignmentsService.create notifies the course's students with assignment.created (Notifications §5). Inside the creation transaction, once the row and its attachments exist, one post-commit job is registered; after commit it runs findAssignmentNotificationRecipients in its own tenant-scoped read, which resolves the students who are members of the assignment's course on school-today, ENROLLED, with an active user, excluding the author's own user: the same students who see the item in the family active list. One intent per student is sent by that job (sendAfterCommit with a resolver), so a rollback sends nothing and a lookup or delivery failure never fails the request. The payload carries the assignment, course and subject name, the recipient's student ID, type and both dates, never the text. Update, delete, attachment and lesson-content writes do not notify, and there is no reminder as the due date approaches.
SEN Manager reads¶
sen_manager has assignments.record:READ and school-wide school-facing assignment, course-day lesson-content and attachment-download visibility. It is admitted only by the school read policy/service; author and lesson-content mutation policies require their own qualified grants. Create/update/delete, attachment mutations and lesson-content writes are denied without another independently qualified author role, whose existing teaching-episode fence remains effective. See Permissions.
Assistance-derived course teachers¶
findAuthorizedTeacherOn and the existence-only findTeachingEpisodeOn accept an ordinary dated episode or a timetable assistance relationship for that course/date. The latter is valid throughout the effective version and course teaching/calendar window, independent of the shift weekday; combined parents grant the relationship to each dated contributor. The operation's date supplies relationship context. Course-day uses its requested date; assignment detail/download and changes use assignedDate, and a changed assignedDate must qualify again. All file/link and multipart mutations retain their parent authority checks and existing ownership rules. Write lookups use findAuthorizedTeacherForWrite under the AY-day lock; lesson-content rechecks inside its transaction before no-op/set/clear handling. Assistance supplies a dated relationship; it never supplies the required scope/action grant.