Skoolsewa - School Management System Docs
Developer ResourcesAssignments

Assignments Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for Assignments.

Assignments Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Schemapackages/db/src/schema/school/assignments.tsTables, enums, constraints, lifecycle rules and their rationale
Migrationspackages/db/src/migrations/0027_assignment_module.sql, 0028_assignment_module_triggers_and_seeds.sqlAppend-only history trigger, version-bump triggers, seeded categories and notification rules
Controllersapps/api/src/modules/assignments/**/*.controller.tsRoute surface, actors, guard chain
Servicesapps/api/src/modules/assignments/**/*.service.tsBusiness rules, side effects, state transitions
Constantsapps/api/src/modules/assignments/shared/assignment.constants.tsEnum vocabulary and numeric limits
Notification templatesapps/api/src/modules/notification/templates/assignment-templates.tsNotification events, recipients, channels, copy

2. Feature Summary

FieldValue
ModuleAssignments
Submoduleauthoring, submissions, files, settings, stats, portal (mobile)
Primary user valueA teacher (or an administrator) sets homework for one or more classes of one subject; each pupil does the work, submits it, and gets it marked; a guardian watches their child's assignments.
ActorsAdministrator, teacher (a live staff row), pupil (a live students row), guardian (linked to a pupil), the scheduler/worker system.
Main entry points/api/assignments* (admin), /api/mobile/teacher/assignments*, /api/mobile/student/assignments*, /api/mobile/guardian/children/:studentId/assignments*, the assignments BullMQ queue, the assignment_file_scan queue, four crons.
Main outputsAssignment records, per-pupil rows and submission attempts, files (attachments and submissions) with a virus/structural scan verdict, in-app/push/email/SMS notifications, an append-only history log, school-wide statistics.
Related docsBackend, API

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
AdministratorCreate/edit/publish/schedule/close/reopen/cancel/archive any assignment in the school; reassign the teacher; read and review any pupil's submission; release marks; manage categories, settings, notification rules, and transfer reviews; rescan a failed file.Nothing is scoped away from an administrator holding the relevant Assignments_*/AssignmentReview_* codes.JWT + Assignments_* / AssignmentPublish_* / AssignmentCancel_* / AssignmentSubmissions_READ / AssignmentReview_UPDATE / AssignmentStats_READ / AssignmentSettings_* / AssignmentNotificationSettings_* permission codes, per route.The admin surface is /api/assignments*, /api/assignment-categories*, /api/assignment-settings, /api/assignment-notification-rules*, /api/assignment-transfer-reviews*, /api/assignment-statistics/*, /api/assignment-files/*.
TeacherCreate/edit/publish/close/reopen/cancel (if teachers_can_cancel)/archive an assignment they manage (see §12.3 for the exact rule); review and mark work for classes they teach; upload/remove their own attachments; see their own dashboard.Manage or review an assignment none of whose live target classes they teach; reassign the teacher; see the admin categories/settings/notification-rules/statistics screens; act on a class-subject they do not teach.JWT only — no @Permissions(). resolveTeacher resolves a live staff row (staff.user_id, deleted_at IS NULL — staff_user_id_unique guarantees at most one). Every route then scopes rows through AssignmentAccessService.Reached at /api/mobile/teacher/assignments*. AssignmentView_READ gates the panel screen, not any route.
Pupil (student)Read their own visible assignments (published/closed/cancelled with a live row); save a draft, upload/remove draft files, submit; download attachments of their visible assignments and their own submission files.See another pupil's row; see a draft/scheduled assignment; write outside their own row; download another pupil's submission file.JWT only. resolveStudent resolves students.user_id, deleted_at IS NULL (identity layer) — no record_status filter, so a suspended pupil still sees their own list; row-scope predicates then filter record_status = 'active'.Reached at /api/mobile/student/assignments*, /api/mobile/student/assignment-files/*.
GuardianRead a linked child's assignment summary, list and detail; download the child's assignment attachments (never submission files).Read/act for a non-linked child; download a submission file; write anything.JWT only. resolveGuardianChild(actor, studentId) binds the child before any row read; a non-owned child answers 404.Reached at /api/mobile/guardian/children/:studentId/assignments*.
Worker/systemEnqueue and run the publish sweep, reminder sweep, roster sync sweep and file-scan-recovery sweep on a cron; run one file scan per queued file; auto-decide most transfer reviews.Bypass any lifecycle guard the human paths enforce — the sweeps re-check the same guards.Internal (BullMQ), no HTTP surface.AssignmentScheduler enqueues; AssignmentsQueueProcessor (queue assignments) and the file-scan processor (queue assignment_file_scan) consume.

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
Create assignment (draft)Admin/teacherAdmin, teacherPOST /api/assignments, POST /api/mobile/teacher/assignmentsSession, subject, categories, class-subjectsassignments row8.1
Preview audienceAdmin/teacherAdmin, teacherPOST /api/assignments/previewclass_subjects, student_class_enrollmentsnone8.2
Update assignmentAdmin/teacherAdmin, teacher (manage)PATCH /api/assignments/:idassignmentsassignments, target-class deltas8.x
Delete draft/scheduledAdmin/teacherAdmin, teacher (manage)DELETE /api/assignments/:idassignmentscascade-deletes targets, links, files, history8.x
Reassign teacherAdminAdminPUT /api/assignments/:id/teacherstaffassignments.teacher_id8.x
Publish now / scheduleAdmin/teacherAdmin, teacher (manage)POST /assignments/:id/publishassignments, publish guardsassignments.status, roster materialisation9.1
UnscheduleAdmin/teacherAdmin, teacher (manage)POST /assignments/:id/unscheduleassignmentsassignments.status='draft'9.x
Close / reopenAdmin/teacherAdmin, teacher (manage)POST /assignments/:id/close, /reopenassignmentsassignments.status, closed_at9.x
CancelAdmin/teacherAdmin, teacher (if teachers_can_cancel)POST /assignments/:id/cancelassignments, assignment_settingsassignments.status='cancelled'9.x
Archive / unarchiveAdmin/teacherAdmin, teacher (manage)POST /assignments/:id/archive, /unarchiveassignmentsassignments.archived_at9.x
Upload/remove attachmentAdmin/teacherAdmin, teacher (manage)POST/DELETE :id/attachments*assignment_settings, assignment_filesassignment_files row + storage bytes + scan queue enqueue10.x
Save draftPupilPupil (own row)PUT /mobile/student/assignments/:id/submissionassignments, assignment_students, draft assignment_submission_attemptsdraft attempt text11.x
Upload/remove draft filePupilPupil (own row)POST/DELETE .../submission/files*assignment_settingsassignment_files (kind=submission)11.x
SubmitPupilPupil (own row)POST .../submission/submitwindow/eligibility rulesnew/updated attempt submitted, assignment_students.state11.x
Review roster / recordAdmin/teacherAdmin (AssignmentSubmissions_READ), teacher (reviewRow)GET :id/submissions*assignment_students, assignment_submission_attemptsnone12.x
Save review (hold)Admin/teacherAdmin (AssignmentReview_UPDATE), teacherPUT :id/submissions/:rowId/reviewlatest non-draft attemptattempt → under_review12.x
Complete review (mark)Admin/teachersamePOST :id/submissions/:rowId/completesameattempt/row → completed, marks release per policy12.x
Return for correctionAdmin/teachersamePOST :id/submissions/:rowId/returnsame, resubmission policyattempt → returned, row → returned12.x
Release marks (bulk)Admin/teachersamePOST :id/release-marksgraded rows with a held resultresult_published_at set for every graded, un-released row12.x
Resolve transfer reviewAdminAdminPOST /assignment-transfer-reviews/:id/resolveassignment_transfer_reviews, live enrolmentrow keep/not_applicable13.x
Categories CRUDAdminAdmin/assignment-categories*assignment_categoriescreate/update/soft-retire14.x
Settings read/updateAdminAdmin/assignment-settingsassignment_settingspolicy singleton14.x
Notification rules read/updateAdminAdmin/assignment-notification-rules*assignment_notification_rulesone row per event14.x
Dashboard/analyticsAdminAdmin/assignment-statistics/*derived aggregatesnone15.x
File downloadAllscoped per audienceGET .../assignment-files/:fileIdassignment_files, scan statusnone16.x
File rescanAdminAdminPOST /assignment-files/:fileId/rescanassignment_files.scan_status='failed'scan_status='pending', re-enqueue16.x
Publish-due sweepWorkerSystemCron, every minuteassignments.status='scheduled', duepublish or revert-to-draftBackend §9
Reminder sweepWorkerSystemCron, every 15 mindue windows, assignment_reminder_dispatchesone dispatch row + notificationsBackend §9
Roster sync sweepWorkerSystemCron, every 5 minlive targets, enrolmentsroster add/move/not_applicable/reviveBackend §9
File-scan recovery sweepWorkerSystemCron, every 10 minstuck pending scansre-enqueue or mark failedBackend §9
File scanWorkerSystemassignment_file_scan queue, on uploadstaged bytesscan_status, storage bytes committed or purgedBackend §9

5. User-Facing Flows

5.1 Teacher creates and publishes an assignment

Summary

A teacher opens the create form, picks a subject and one or more of their own class-subjects, writes instructions, sets a due date and submission rules, and publishes — either immediately or on a schedule. Publishing materialises one row per enrolled pupil and notifies them.

Preconditions

  • The teacher is acting as the role named teacher and has a live staff row (assertTeacherStaffRow).
  • Every targeted class_subject_id is active, belongs to an active class of the chosen academic session, teaches the chosen subject, and is taught by the teacher (teachesForCreate).
  • dueAt is far enough in the future to publish now (ASSIGNMENT_PUBLISH_MIN_LEAD_MS = 5 minutes) or, for a schedule, publishAt is at least a minute away and leaves at least publish-lead-plus-slack room before dueAt.
  • At least one of allowTextResponse / allowFileUpload is true.

Main Flow

StepActor/SystemActionResultSource
1TeacherPOST /api/mobile/teacher/assignments with title, instructions, due date, target class-subjectsassignments row created, status='draft', assignment_classes rows for each targetAssignmentWriteService.create
2TeacherPOST :id/publish with mode: "now"Publish guards re-checked; status='published', published_at setAssignmentLifecycleService.publish
3BackendMaterialise rosterOne assignment_students row per pupil actively enrolled in a live target classAssignmentRosterService.materialise
4BackendWrite history, notifypublished history entry; published notification to the new rows' pupils and their guardiansAssignmentHistoryWriter, AssignmentNotifier
5BackendReturn detailFull AssignmentDetailDtoAssignmentDetailService

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Class not taughtA target class-subject's teacher_id is not the callerReject before insert404 ASSIGNMENT_CLASS_NOT_TAUGHT
No classes selectedEmpty classSubjectIdsPublish is refused; create accepts an empty list422 ASSIGNMENT_NO_CLASSES at publish; 409 ASSIGNMENT_NO_CLASSES when an update would remove every live class
Too many classesMore than ASSIGNMENT_MAX_CLASSES (60)Reject before any DB read422 ASSIGNMENT_AUDIENCE_TOO_LARGE
Audience too largeLive pupil count across targets exceeds ASSIGNMENT_MAX_AUDIENCE (3000)Publish guard rejects422 ASSIGNMENT_AUDIENCE_TOO_LARGE
Due too soondueAt less than 5 minutes awayReject publish-now400 ASSIGNMENT_DUE_IN_PAST
Idempotent createSame clientRequestId from the same creator replayedReturns the existing draft, 200 instead of 201created:false in the response
Already publishedpublish called again on an already-published assignmentNo-op, returns current detail — checked before version, so a double-click succeeds even with a stale version in hand200, no state change
Version conflictversion sent does not match the row's current version, on an action that WOULD change somethingReject409 ASSIGNMENT_VERSION_CONFLICT

5.2 Pupil does and submits an assignment

Summary

A pupil opens an assigned piece of work, writes a response and/or attaches files, then submits. The system decides whether the submission is late and whether a further submission is allowed at all.

Preconditions

  • The assignment is published, not archived, and its class is still live (or the pupil's assignment_students.state is returned with an open resubmission window).
  • The pupil has a live assignment_students row for the assignment (created by roster materialisation).
  • At least one non-empty content item exists at submit time: non-blank text, or a non-rejected file.

Main Flow

StepActor/SystemActionResultSource
1PupilPUT .../submission to save draft textA draft attempt is created or updated; row moves assigned → in_progress on first writeAssignmentSubmissionService.saveDraft, AssignmentDraftService.lockRowAndDraft
2PupilPOST .../submission/files to attach a fileFile row pending scan; queued to assignment_file_scanAssignmentFileUploadService.uploadSubmissionFile
3WorkerScan runsscan_status becomes clean, rejected or failedAssignmentFileScanRunner / AssignmentsQueueProcessor sibling processor
4PupilPOST .../submission/submit with clientSubmissionIdDraft (or a fresh attempt) becomes submitted; row state='submitted', attemptCount incremented, isLate computedAssignmentSubmissionService.submit
5BackendNotifysubmitted notification to pupil, guardians (low priority) and teacherAssignmentNotifier

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Empty submissionNo text and no non-rejected fileReject400 ASSIGNMENT_SUBMISSION_EMPTY
Rejected file still attachedA draft file's scan_status='rejected'Reject; pupil must remove it first409 ASSIGNMENT_SUBMISSION_HAS_REJECTED_FILES
Too many filesDraft file count exceeds assignments.max_filesReject409 ASSIGNMENT_SUBMISSION_FILE_LIMIT_EXCEEDED
Files when not allowedallowFileUpload=false but files attachedReject409 ASSIGNMENT_FILES_NOT_ALLOWED
Already completedassignment_students.state='completed'Reject409 ASSIGNMENT_ALREADY_COMPLETED
Pending reviewstate='under_review'Reject — nothing to resubmit onto409 ASSIGNMENT_NOT_ACCEPTING
Resubmission window closedstate='submitted' and no attempts remain and no content-rejected pending attemptReject409 ASSIGNMENT_RESUBMISSION_NOT_ALLOWED
Resubmission window closed (never submitted)Past due, late not allowed, no resubmit deadlineReject409 ASSIGNMENT_NOT_ACCEPTING
Duplicate submitSame clientSubmissionId replayed and it is still the latest non-draft attemptNo-op success (idempotent replay)Returns current detail, no duplicate attempt
Stale replaySame clientSubmissionId but a newer attempt now existsReject409 ASSIGNMENT_VERSION_CONFLICT
Content-rejected pending attemptThe pupil's only submitted content was files that all got rejected, and no replacement text existsTreated as effectively unsubmitted — allowed to submit again even without spare attemptssupersedeAllowed special case
Class removed mid-submissionThe pupil's target class was removed from the assignmentclassLive=false; submission window closes409 ASSIGNMENT_NOT_ACCEPTING

5.3 Teacher reviews and marks a submission

Summary

A teacher (or administrator) opens the submissions roster for an assignment, opens one pupil's record, and either saves in-progress marking (under_review, held from the pupil), completes it (releasing the result per the assignment's marks-release policy), or returns it for correction.

Preconditions

  • The assignment is published or closed (returning requires strictly published), and not archived.
  • The caller may review the row: admin always; teacher when they manage the assignment OR teach the class-subject the row's assignment_class_id targets (even if that class has since been removed).
  • The attempt being acted on is the pupil's latest non-draft attempt and in an allowed status for the action.

Main Flow

StepActor/SystemActionResultSource
1TeacherGET :id/submissionsRoster of pupil rows, filterable/sortableAssignmentRosterReadService.listRoster
2TeacherPUT :id/submissions/:rowId/reviewMarks/grade/feedback saved on the attempt; attempt/row → under_review; nothing visible to the pupil yetAssignmentReviewService.saveReview
3TeacherPOST :id/submissions/:rowId/completeAttempt/row → completed; result visible immediately (and a marked notification fires) when marksRelease='on_review', or when marks have already been released once for this assignmentAssignmentReviewService.completeReview
3'TeacherPOST :id/submissions/:rowId/returnAttempt → returned; row → returned, resubmit_due_at set or window reopened; a returned notification firesAssignmentReviewService.returnSubmission

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Not the latest attemptA newer attempt superseded the one referencedReject409 ASSIGNMENT_ATTEMPT_NOT_REVIEWABLE
Marks requiredisGraded=true and no marks suppliedReject400 ASSIGNMENT_MARKS_INVALID
Marks out of rangemarks outside [0, maxMarks]Reject400 ASSIGNMENT_MARKS_INVALID
Marks on ungraded assignmentisGraded=false but marks suppliedReject400 ASSIGNMENT_MARKS_INVALID
Return without feedbackfeedbackHtml carries no text (an empty editor's <p></p> counts as empty)Reject400 ASSIGNMENT_FEEDBACK_REQUIRED
Return not allowedresubmissionPolicy='none'Reject409 ASSIGNMENT_ATTEMPT_NOT_REVIEWABLE
Return budget exhaustedresubmissionPolicy not unlimited and extra_attempts_granted >= 50Reject409 ASSIGNMENT_RESUBMISSION_NOT_ALLOWED
Resubmit deadline requiredNo resubmitDueAt supplied and the base due window is already closedReject400 ASSIGNMENT_RESUBMIT_DUE_REQUIRED
Resubmit deadline in the pastresubmitDueAt <= nowReject400 ASSIGNMENT_RESUBMIT_DUE_IN_PAST
Assignment not open for reviewstatus not in published/closed, or archivedReject409 ASSIGNMENT_NOT_EDITABLE

6. Admin Flows

6.1 Cancel an assignment

Terminal state. Requires AssignmentCancel_UPDATE for an admin, or assignment_settings.teachers_can_cancel = true for a teacher. A non-empty reason is required and stored. Clears any pending schedule and closes-state, sets status='cancelled', and notifies every pupil and (at low priority) every guardian.

6.2 Archive / unarchive

Archiving is only valid from closed or cancelled and freezes the row from further lifecycle actions (ASSIGNMENT_NOT_EDITABLE on any attempt to publish/close/reopen/cancel a non-null archived_at). Unarchiving clears the flag with no other state change. Both are idempotent no-ops when already in the target state.

6.3 Reassign teacher (admin only)

PUT /api/assignments/:id/teacher changes assignments.teacher_id. Reassigning to a teacher who does not teach any live target class is accepted but surfaced as a warning by the write path (the FK's ON DELETE SET NULL and the manage predicate mean a mismatched teacher simply cannot manage the assignment through the teacher portal afterward).

6.4 Resolve a transfer review

When the roster sweep cannot automatically decide what happens to a pupil who left a target class (the school's transfer_mode='flag_for_admin', or an ambiguous move), it opens an assignment_transfer_reviews row. An administrator resolves it keep (row stays, re-targeted to the new class if the pupil is still in the assignment's classes) or not_applicable (row is excluded from the assignment). Resolving after the pupil's enrolment has changed again (moved back into a live target, or lost their active enrolment) is refused with 409 ASSIGNMENT_TRANSFER_NO_LONGER_APPLICABLE; before that response the stale review row is deleted (and, for a pupil with no active enrolment, the row is marked not_applicable with reason withdrawn) and the change is committed, so it is not left open forever. Resolving not_applicable on a pupil who already submitted work is refused with 409 ASSIGNMENT_TRANSFER_ALREADY_SUBMITTED.

6.5 Manage categories

Categories (assignment_categories) are school-defined, not an enum, so a school adds its own without a migration. AssignmentSettings_CREATE/_UPDATE/_DELETE gate create/update/delete; name and code uniqueness are partial on is_active, so retiring a category frees its name and code for reuse. Deleting a category still referenced by an assignment is refused (409 ASSIGNMENT_CATEGORY_IN_USE).

6.6 Manage module settings and notification rules

assignment_settings is a singleton (id=1). It governs file limits, default submission types, the maximum submission text length, the transfer_mode policy, whether a non-school-day due date warns, whether an assignment requires an assigned teacher, whether teachers may cancel, and the reminder hour. assignment_notification_rules has one row per event and can only subtract recipients/channels from the module default — it can disable an event, or narrow notify_student/notify_guardians/notify_teacher, never widen beyond what AssignmentNotifier would otherwise send.

6.7 Rescan a failed file

POST /api/assignment-files/:fileId/rescan (AssignmentSettings_UPDATE) re-queues a file whose scan_status='failed' for another pass, resetting it to pending. Refused with 409 ASSIGNMENT_FILE_NOT_RESCANNABLE for any other status (a clean, rejected or already-pending file is not eligible).

7. Lifecycle and State Transitions

7.1 Assignment status

EntityFromEvent/ActionToGuard ConditionSide Effects
assignmentsdraftPublish nowpublishedDue ≥ 5 min away; publish guards passRoster materialised, published notification
assignmentsdraftSchedulescheduledpublishAt ≥ 1 min away, leaves room before due; publish guards passscheduled history entry
assignmentsscheduledUnscheduledraft—unscheduled history entry
assignmentsscheduledSweep fires at publishAtpublishedGuards re-checkedRoster materialised; on guard failure, reverted to draft with publish_failed history + notification to the creator
assignmentspublishedCloseclosed—closed_at set
assignmentsclosedReopenpublishedNot archivedclosed_at cleared
assignmentsscheduled/published/closedCancelcancelledTeacher requires teachers_can_cancelNotifies pupils + guardians
assignmentsclosed/cancelledArchive(same status) + archived_at set—Freezes further lifecycle actions
assignments(archived)Unarchivearchived_at cleared——

7.2 Pupil row state (assignment_students.state)

EntityFromEvent/ActionToGuard ConditionSide Effects
assignment_students(none)Roster materialiseassignedLive enrolment in a live target classNew row, added_to_roster history
assignment_studentsassignedSave first draftin_progress—Draft attempt created
assignment_studentsassigned/in_progressSubmitsubmittedWindow open, non-empty contentAttempt submitted, counters updated
assignment_studentssubmittedTeacher saves reviewunder_reviewLatest attempt—
assignment_studentssubmitted/under_reviewComplete reviewcompleted—final_marks/final_grade set; result_published_at per policy
assignment_studentssubmitted/under_review/completedReturnreturnedAssignment strictly published; resubmission allowedResult cleared, resubmit_due_at set/cleared
assignment_studentsreturnedResubmitsubmittedWithin resubmit windowNew/updated attempt
assignment_studentsassigned/in_progressClass removed / withdrawn / transferrednot_applicable—Excluded from active work
assignment_studentsnot_applicablePupil re-enters a live targetassigned or in_progressHas a draft or notrevived history

7.3 Submission attempt status (assignment_submission_attempts.status)

draft → submitted → under_review → completed, with returned reachable from submitted/under_review/completed, and superseded applied to any prior submitted/under_review attempt when a fresh one is accepted. At most one draft and one pending (submitted/under_review) attempt exist per pupil row at a time (assignment_attempts_one_draft_unique, assignment_attempts_one_pending_review_unique).

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
Createassignments, assignment_classesnonenonenonenonenone
Publish nowassignments, assignment_students (insert), assignment_historynonenonenonenonepublished to new rows' pupils + guardians
Scheduleassignments, assignment_historynonenonenonenonenone
Publish-due sweepassignments, assignment_students, assignment_historynoneJob: assignment.publish_duenonenonepublished or publish_failed
Updateassignments, assignment_classes (add/remove), assignment_students (not_applicable on removal), assignment_historynonenonenonenoneupdated when the pupil-visible shape changes
Cancelassignments, assignment_historynonenonenonenonecancelled
Attachment uploadassignment_files, storage stagingnoneJob: assignment.scan_filenonenonenone directly (rejection later notifies)
Save draftassignment_submission_attemptsnonenonenonenonenone
Submitassignment_submission_attempts, assignment_students, assignment_historynonenonenonenonesubmitted to pupil, guardians, teacher
File scanassignment_files.scan_status, storage commit/purgenoneconsumes assignment.scan_file; may notifynonenonefile_rejected on a rejected verdict
Save reviewassignment_submission_attempts, assignment_students, assignment_historynonenonenonenonenone (held)
Complete reviewsame + result_published_atnonenonenonenonemarked when visible on completion
Returnsamenonenonenonenonereturned
Release marksassignment_students.result_published_at (bulk)nonenonenonenonemarked per released row
Roster sync sweepassignment_students, assignment_transfer_reviews, assignment_historynoneJob: assignment.roster_syncnonenonepublished for newly-added rows
Reminder sweepassignment_reminder_dispatchesnoneJob: assignment.reminder_sweepnonenonereminder_3d/reminder_1d/due_today/overdue
Scan-recovery sweepassignment_files.scan_status/scan_attemptsnoneJob: assignment.scan_recovery, re-enqueues assignment.scan_filenonenonenone directly

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Rate limitedToo many upload/write requests from one IP429 responseRetry after the windowIpThrottlerGuard with RATE_LIMITS.UPLOAD_WRITE/CUSTOMER_WRITE
Version conflictConcurrent edit or stale version sent409 with the conflicting resource's code (ASSIGNMENT_VERSION_CONFLICT, ASSIGNMENT_DRAFT_VERSION_CONFLICT)Reload and retryOptimistic-concurrency triggers + matchesVersion
Scheduled publish fails its guardsClass removed, teacher lost the class, audience now empty, etc.Assignment silently reverts to draft; creator is notifiedFix the issue, republish or rescheduleAssignmentPublisher.publishScheduled
File fails structural scanMalformed office file, embedded macro, archive bomb, etc.Pupil/teacher sees the file rejected, generic security messageRemove and re-upload a clean filestructural-file-scanner.ts, ASSIGNMENT_FILE_SECURITY_MESSAGE
Scan stuck pendingWorker crash, queue delayFile shows as still scanning past ASSIGNMENT_SCAN_STALE_MS (15 min)The 10-minute recovery sweep re-enqueues or marks failed after ASSIGNMENT_SCAN_ATTEMPTS (3)assignment-file-scan-recovery.ts
Roster reconciliation contentionTwo sweeps target the same assignment at onceThe second sweep is skipped (pg_try_advisory_xact_lock fails)Picked up on the next 5-minute tickAssignmentRosterServiceImpl.reconcile
Transfer review stale by resolution timePupil's enrolment changed again between flagging and resolvingResolve is refused, review auto-closesAdmin re-checks the pupil's current state; a fresh review opens if still ambiguousAssignmentRosterServiceImpl.resolveDecision
Academic session lockedAny write to a locked year's assignment, its classes, roster, submission attempts, links, or a file insert/delete409 ACADEMIC_SESSION_LOCKED: "That academic year is locked, so its records can no longer change."An administrator unlocks the year to make the correctionpackages/db/src/migrations/0038_year_lock_records.sql
Pupil has graduatedDraft save, file upload/removal, or submit by a pupil whose graduate access is grace or ended403 STUDENT_GRADUATED_READ_ONLY: nothing can be submitted for themNone — the portal stays read-only for that pupil until an administrator reopens accessstudent-assignment-submission-portal.controller.ts, graduate-access.query.ts

11. Diagrams Required Per Module

Covered above: actor capability (§3), high-level create→publish→submit→review flow (§5), sequence diagrams per major flow (§5), state machines for assignment status and pupil row state (§7), data side-effect table (§9), error branches for cancel (§6.1) and submit/review edge cases (§5.2, §5.3).

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
CreateIdempotent replay by clientRequestIdAdmin/teacherDuplicate POST with the same key200 with created:false, same assignment returnedNo second row insertedassignments_client_request_unique
CreateInstructions HTML sanitisedAdmin/teacherAny create/update with instructionsHtmlStored HTML is stripped of disallowed tags; a plain-text mirror is derivedinstructions_text kept in syncassignment-html.util.ts
Audience previewExcludes pupils already excluded (not_applicable)Admin/teacherPOST /assignments/previewCount reflects only live, eligible pupilsRead-onlyAssignmentPreviewService
PublishTeacher requirementAdmin/teacherPublish while require_teacher is on and no teacher is assigned422 ASSIGNMENT_TEACHER_REQUIRED; an admin-chosen teacherId that is not a live staff row is refused earlier, at create or reassign, with 404 ASSIGNMENT_TEACHER_NOT_FOUNDPublish blocked until a teacher is assignedassignment-publish-guards.util.ts:90-95, assignment-write.service.ts:169, :346
UpdateNon-school-day due dateAdmin/teacherwarnNonSchoolDayDue=true and the new dueAt falls on a day the school calendar marks non-teachingNon-blocking warning surfaced by the write servicenoneassignment_settings.warn_non_school_day_due
UpdateDue-date change bannerAny viewerdueAt changed after publishpreviousDueAt/dueChangedAt retained and surfaced for ASSIGNMENT_DUE_CHANGED_BANNER_MS (7 days)updated notification with old/new due dateassignments.previous_due_at, constants
Attachment uploadDuplicate upload replayAdmin/teacherSame clientUploadId from the same uploaderReturns the existing file row instead of duplicatingassignment_files_client_upload_uniqueassignment-file-upload-attachment.ts
Attachment uploadPending-scan throttleAny uploaderMore than ASSIGNMENT_MAX_PENDING_SCANS_PER_USER (20) files awaiting scanReject429 ASSIGNMENT_TOO_MANY_PENDING_FILESfiles/assignment-file-upload.validation.ts:63-74
SubmissionEmpty draft save allowedPupilSave with no text and no files yetDraft row exists with null textnoneAssignmentDraftService
SubmissionLate flagPupilSubmit after dueAt under latePolicy='allowed' or within the final deadlineisLate=true recorded, surfaced to reviewerassignment_students.last_is_latesubmission-window.util.ts
SubmissionContent-rejected resubmitPupilEvery file of the last submission was rejected and no text was givenTreated as not-yet-submitted for resubmission purposespendingContentRejectedSqlassignment-status.sql.ts
ReviewHistory-only saveTeacher/adminsaveReviewNo notification (nothing visible to the pupil)review_saved history entry onlyAssignmentReviewService.saveReview
ReviewMarks release timingTeacher/adminmarksRelease='manual' and no marks have ever been released for this assignmentResult held until an explicit release-marks callresult_published_at stays null on completeassignment-review-release.service.ts
ReviewMarks release stays open after one releaseTeacher/adminmarksRelease='manual' but the assignment already has a marksReleasedAt from an earlier bulk releaseEvery later complete on that assignment publishes its result immediatelyresult_published_at set on complete, no further release-marks call neededassignment-review.service.ts
TransferAutomatic keepSystemtransfer_mode='keep'Row stays on its old target with no review openednoneassignment-roster-decisions.service.ts
TransferAutomatic followSystemtransfer_mode='follow_class' and the new class is already a live targetRow silently re-targetedmoved_class historysame
TransferAutomatic self-resolveSystemA review sits open while its pupil's row leaves assigned/in_progress (they submitted, or were reviewed)Review auto-resolves keep; the submitted work is never overwrittenhistory reason: "already_submitted", decidedAutomatically: trueassignment-roster-decisions.service.ts
TransferRevival respects the windowSystemA not_applicable row's pupil re-enters a live target class after the assignment's own submission window has already closedRow stays not_applicable rather than reviving into a window the pupil can no longer submit againstnoneassignment-roster.service.ts
CategoriesRetire and reuseAdminDeactivate a category then create a new one with the same name/codeAllowed — partial unique indexes are scoped to is_activeassignment_categories_name_unique/_code_uniqueschema
FilesQuarantine retentionSystemA file is rejectedBytes kept (never served) for ASSIGNMENT_QUARANTINE_RETENTION_MS (30 days), then purgedstorage_key/purged_at set togetherassignment-file-purge.service.ts
NotificationsTeacher-applicable events onlyAdminAttempt to enable notify_teacher on an event other than submitted/overdueRejected in the service before the CHECK constraint is reached400 ASSIGNMENT_NOTIFICATION_RULE_INVALID (an unknown event is 404 with the same code)assignment-notification-rules.service.ts:66-82, assignment_notification_rules_teacher_applicable

12.2 Business Process Diagram Pack

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
A teacher manages an assignment only when they own its teacher slot AND teach at least one live target, or every live target is theirsPrevents one teacher silently editing another's homework while co-teaching classesTeacher sees 404 (never 403) for out-of-scope assignmentsAssignmentAccessServiceImpl.manageExprOverAssignmentsAll manage-gated routesRow-level WHERE clauseassignment-authoring.integration.spec.ts
Publishing needs a live target with a non-empty, bounded audienceAn assignment nobody can see is a defect, not a draftPublish blocked with a clear reasonassignment-publish-guards.util.tsPOST :id/publishASSIGNMENT_NO_CLASSES/ASSIGNMENT_AUDIENCE_TOO_LARGEintegration specs
A soft-deleted or record-inactive pupil is invisible to every readData integrity: a withdrawn pupil's homework should not clutter live rostersPupil disappears from lists, not 403visibleAssignment/roster queries filter record_status='active', deleted_at IS NULLlist/detail routesWHERE clauseassignment-display-status-parity.integration.spec.ts
A guardian never sees a submission fileSubmission content is the pupil's own work; a guardian sees outcomes, not raw files404 on submission-file download attempts by a guardianAssignmentFileDownloadServiceGET guardian/.../assignment-files/:fileIdkind checkfile download service
Staff never see a pupil's draft fileA teacher/admin reviews handed-in work, not a pupil's in-progress composition404 when a staff download targets a file on a still-draft attemptAssignmentFileDownloadServiceGET .../assignment-files/:fileId (admin/teacher)attempt-status filterfile download service
Marks release policy decides when a result becomes visibleSome schools want results held for moderation before parents see themPupil/guardian see "reviewed" without a mark until releasedmarksRelease on assignments, enforced in completeReview/releaseMarksPOST :id/release-marksresult_published_at timingreview integration spec
History is append-onlyAn audit trail that can be edited is not an audit trailNone visibleassignment_history_append_only triggerGET :id/historyDB-level refusalmigration 0028, roster/authoring specs
Notification rules can only subtractPrevents an administrator accidentally emailing every parent about something the school intentionally keeps low-keyFewer notifications than the module default, never moreAssignmentNotifier.notify reads the rule before sendingnone directlyrule read gates every sendnotifier tests
Teachers cancelling is a school-level toggleSome schools want cancellation restricted to admins even though teachers own their assignmentsTeacher gets 403 ASSIGNMENT_CANCEL_NOT_ALLOWED when disabledassignment_settings.teachers_can_cancelPOST :id/cancel (teacher route)settings lookup inside the transactionlifecycle spec

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
Status is stored; "active"/"due today"/"overdue" are derivedThe stored lifecycle never goes staleOne SQL expression (assignment-status.sql.ts) is the single source, reused by list, stats and portal readsStore a computed display statusSlightly heavier queriesLow — indexed on (session, status, due_at)
Notification rules subtract onlyPredictable, safe defaults; a school cannot accidentally over-notify by editing one rowSimple mental model: module default ∩ rule ∩ per-user preferenceRules could add recipients tooLess flexible for a school wanting to widen a specific eventLow, by design
Soft transfer resolution (keep/follow/flag) instead of always deleting the rowA pupil who changes class does not lose their homework historyOne reconciliation sweep handles the common cases automaticallyAlways require manual resolutionMore sweep complexityContained by the advisory lock and idempotent predicates
File quarantine retention (30 days) instead of immediate purgeGives a security reviewer time to inspect a rejected file if neededSimple storage lifecyclePurge immediatelySlightly more storage heldBounded by the sweep and retention constant
assignment_files splits attachment vs submission by a CHECK, not two tablesOne download/scan pipeline serves both kindsLess code duplicationSeparate tablesA stricter split would be more self-documentingLow — the CHECK is exhaustive

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
PublishEmpty stateNo live target classesBlocked422 ASSIGNMENT_NO_CLASSESpublish guards
Roster materialiseFirst useFirst publish of a brand-new assignmentEvery actively-enrolled pupil of every live target class gets a rowadded_to_roster history per rowroster service
SubmitLast attemptattemptCount equals the resolved max allowedFurther submit blocked unless a content-rejected exception applies409 ASSIGNMENT_RESUBMISSION_NOT_ALLOWEDsupersedeAllowedSql
SubmitDuplicate actionSame clientSubmissionId sent twiceSecond call is a no-op200, same resultidempotency check
Roster syncConcurrent actionTwo sweep ticks for the same assignment overlapSecond is skipped, not queued{skipped:true}advisory lock
ReturnExpired stateResubmission window's base due date has already passed and no explicit deadline givenReject, force an explicit deadline400 ASSIGNMENT_RESUBMIT_DUE_REQUIREDreview service
FilesPermission mismatchA teacher who does not teach the row's class tries to open a submission fileRefused404 (never 403)reviewRow scope
Guardian readGuest/guardian limitationGuardian requests a submission fileRefused regardless of ownership404download service
PublishMissing dependencyA teacher stops teaching one of the target classes between creating and publishingPublish guard re-checks teaching and refuses404 ASSIGNMENT_CLASS_NOT_TAUGHT (teacher publish only)assignment-publish-guards.util.ts:73-83
File scanCache stale/missN/A — no cache layer in this module———
ScanQueue failureScan job repeatedly errorsRecovery sweep retries up to ASSIGNMENT_SCAN_ATTEMPTS then marks failedfile shows failed, admin can rescanscan-recovery
ListUnsupported filter/sortAn unrecognised sort value on the submissions rosterFalls back to nameNo errorresolveSubmissionRosterSort

12.6 Flow-to-Data Trace

FlowReadsWritesCacheJobs/EventsResponse Fields
Submitassignments, assignment_students, assignment_submission_attempts, assignment_filesassignment_submission_attempts, assignment_students, assignment_historynonesubmitted notificationfull detail incl. state, attempt history
Review completeassignments, assignment_students, assignment_submission_attemptssame tables + result_published_atnonemarked notification (conditional)finalMarks, finalGrade, resultPublishedAt
Publish-due sweepassignments (status='scheduled')assignments, assignment_students, assignment_historynonepublished/publish_failed notificationn/a (background)

12.7 Experience Quality Checklist

  • The doc explains what the actor is trying to accomplish (set/do/review homework).
  • The doc explains what the backend does that the actor does not see (roster materialisation, scan pipeline, reminder/publish sweeps).
  • The doc covers every minor flow and branch (idempotent replays, quarantine retention, content-rejected resubmission).
  • The doc includes user, admin, worker, and system flows.
  • The doc explains business logic, tradeoffs, and rationale.
  • The doc maps every flow to API routes and backend side effects.
  • The doc includes diagrams appropriate to each flow type.
  • The doc covers edge cases and failure recovery.

13. Completion Checklist

  • Every feature, minor action, and submodule capability is listed.
  • Every actor has allowed and forbidden behavior.
  • Every major and minor flow includes steps, branches, and diagrams.
  • Every lifecycle has a transition table and state diagram.
  • Every flow links to the API and backend docs.
  • No TDD sibling doc exists for this module; none is referenced as required.

See Also