Skip to content

Class-register assignments and lesson content

This chapter is the canonical backend contract for homework, upcoming tests, and the optional “what we did today” course text. The approved designs are 2026-08-24-class-register-assignments-design.md, its lesson-content iteration, and the approved 2026-08-25-class-register-assignments-iteration-2-design.md CRUD/Admin iteration, and the approved 2026-08-25-class-register-assignments-iteration-3-design.md files/links iteration. The latest iteration is normative where it extends or reverses an earlier V1 boundary.

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, status, or notification model.

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, restrictive FKs, Class-S ENABLE/FORCE RLS, tenant-reset coverage, and raw non-blank checks. 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:

F <= schoolToday
AND (U IS NULL OR F < U)
AND (U IS NULL OR A < U)

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 (2026-09-03). 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 — the membership cutoff rules above are unchanged. Design: 2026-09-03 spec.

4. API

Fourteen protected routes exist:

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, hidden course/student anchors use generic 404. Missing, foreign, archived-year, hidden, or deleted assignment ids use ASSIGNMENT_NOT_FOUND. 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; principal/secretary and qualified department/curriculum management read their course reach. HR and generic staff receive no assignment scope.

Teacher creation requires an active-year active Teacher profile and a non-empty teaching episode effective on assignedDate. Teacher update/delete requires a teaching episode 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 the episode 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 episode on the parent stored assigned date. There is no new RBAC primitive. 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.created
  • assignment.updated with complete before/after business snapshots
  • assignment.deleted with the complete pre-delete business snapshot
  • assignment.attachments_added for one post-create batch
  • assignment.attachment_updated with public before/after link metadata
  • assignment.attachment_deleted with public removed-item metadata
  • course_day_lesson_content.created
  • course_day_lesson_content.updated
  • course_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. V1 boundary and deployment

There is no rich text, file replacement/rename/reorder, link preview/fetch, malware/content scanner, completion, overdue badge, reminder, restore, revision/history endpoint, target-course update, per-period content, or CombinedClass/activity/homeroom/grade target. Deploy both assignment migrations plus 20260825150000_assignment_attachments in order and regenerate Prisma before enabling attachment clients. No role reseed is required for the iteration-3 child routes because they reuse the existing record scope.