Skoolsewa - School Management System Docs
Developer ResourcesAssignments

Assignments API Reference

Complete API contracts for Assignments, including routes, auth, DTOs, responses, errors, examples, and integration notes.

Assignments - API Reference

Audience: Frontend engineers (the admin panel), mobile/portal engineers, backend engineers, QA. Scope: Admin, teacher-portal, student-portal, guardian-portal, and file-download surfaces owned by the Assignments module. 76 routes total (GET /api/subjects/{publicId}/assignments and POST /api/subjects/{publicId}/assignments are owned by the unrelated ClassSubjects module — teacher-to-subject assignment, not homework — and are out of scope here).

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/assignments/**/*.controller.ts (22 files)Routes, methods, guards, decorators, status codes
DTOsapps/api/src/modules/assignments/**/dto/*.tsRequest, query, response, validation
Servicesapps/api/src/modules/assignments/**/*.service.tsBehavior, side effects, response mapping, errors
Schemapackages/db/src/schema/school/assignments.tsIDs, enums, persisted fields, constraints
Error registryapps/api/src/common/types/error-codes.ts (grep ASSIGNMENT_)62 ASSIGNMENT_* error codes (§12)
Jobspackages/jobs/src/index.tsQueueName.ASSIGNMENTS, QueueName.ASSIGNMENT_FILE_SCAN, job enums, payloads
Route baselineapps/api/test/structure/structure.baseline.jsonThe authoritative 78-route list this module contributes 76 routes to

2. Module Summary

FieldValue
Module nameAssignments
Module slugassignments
Primary actorsadmin, teacher (audience), student (audience), guardian (audience)
API surfacesAdmin (/api/assignments*, /api/assignment-*), mobile/portal (/api/mobile/teacher/*, /api/mobile/student/*, /api/mobile/guardian/*)
Base route prefixes/api/assignments, /api/assignment-categories, /api/assignment-settings, /api/assignment-notification-rules, /api/assignment-transfer-reviews, /api/assignment-statistics, /api/assignment-files, /api/mobile/teacher/assignments*, /api/mobile/teacher/assignment-files, /api/mobile/student/assignments*, /api/mobile/student/assignment-files, /api/mobile/guardian/children/:studentId/assignments*, /api/mobile/guardian/children/:studentId/assignment-files
Auth modelJwtAuthGuard (global) + RoleGuard; admin routes carry @Permissions("Module_ACTION"); portal routes carry none and are policed by RoleGuard's NO_PERMISSION_ADMIN_HANDLERS allowlist plus the module's own AssignmentAudienceResolver calls
PersistencePostgreSQL (Drizzle), file bytes via @skoolsewa/storage, async work via BullMQ (@skoolsewa/jobs)
Runtime source of truthassignments.status and assignment_students.state/assignment_submission_attempts.status (stored); "active"/"due today"/"overdue" are derived per request, never stored
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
publicIdExternal UUIDv7 identifier for an assignment, category, file, or transfer review — never the internal integer PKassignments.publicId etc.Every route param named id/fileId/event uses this, except assignment-notification-rules/:event which is keyed by the event string itself
statusThe stored assignment lifecycle: draft/scheduled/published/closed/cancelledassignmentStatusEnumList/detail responses, lifecycle routes
displayStatusThe DERIVED status shown to a viewer: draft/scheduled/active/due_today/overdue/closed/cancelledASSIGNMENT_DISPLAY_STATUSList/detail responses; computed per-request from status+dueAt+latePolicy, never stored
stateThe pupil's own progress on one assignment: assigned/in_progress/submitted/under_review/returned/completed/not_applicableassignmentStudentStateEnumSubmission roster, pupil detail
studentDisplayStatusA richer, pupil-facing label derived from state+timing: not_started/in_progress/submitted/submitted_late/resubmitted/under_review/returned/returned_closed/marked/completed/overdue/not_submitted/not_applicable/cancelledASSIGNMENT_STUDENT_DISPLAY_STATUSStudent/guardian list and detail views
bucketA roster/dashboard grouping: all/upcoming/due_today/overdue/in_progress/awaiting_review/returned/completed/not_submittedASSIGNMENT_STUDENT_BUCKETPupil dashboard tile counts, guardian summary counts (overall and per subject), list filters. A cancelled or no-longer-applicable row has no bucket and is left out of every one of those counts.
attemptOne row of assignment_submission_attempts — a single try at the work, draft through completed/supersededassignmentAttemptStatusEnumSubmission record, review actions
versionAn opaque optimistic-concurrency token, sent back exactly as last read — never re-parsed as a number by the clientbump_row_version() triggerEvery write route that targets an existing row
classSubjectIdThe id of one class teaching one subject (class_subjects.id) — what an assignment actually targets, not a bare class idclass_subjects schemaCreate/update classSubjectIds, class-stat rows
rowIdassignment_students.id — one pupil's record for one assignmentschemaAll :id/submissions/:rowId* routes
attemptIdassignment_submission_attempts.id, always bound to rowId server-side before any writeschemaReview request DTOs
clientRequestId/clientSubmissionId/clientUploadIdClient-generated idempotency keys for create/submit/uploadschema unique constraintsReplay-safe create, submit, upload

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminGET/api/assignments/optionsAdminJWT+RoleAssignments_CREATEAssignmentsControllerCreate-form catalogue
AdminPOST/api/assignments/previewAdminJWT+RoleAssignments_CREATEAssignmentsControllerPreview candidate audience
AdminGET/api/assignmentsAdminJWT+RoleAssignments_READAssignmentsControllerList assignments
AdminPOST/api/assignmentsAdminJWT+RoleAssignments_CREATEAssignmentsControllerCreate a draft
AdminGET/api/assignments/:idAdminJWT+RoleAssignments_READAssignmentsControllerOne assignment
AdminGET/api/assignments/:id/optionsAdminJWT+RoleAssignments_UPDATEAssignmentsControllerEdit-form catalogue
AdminPATCH/api/assignments/:idAdminJWT+RoleAssignments_UPDATEAssignmentsControllerUpdate
AdminDELETE/api/assignments/:idAdminJWT+RoleAssignments_DELETEAssignmentsControllerDelete draft/scheduled
AdminPUT/api/assignments/:id/teacherAdminJWT+RoleAssignments_UPDATEAssignmentsControllerReassign teacher
AdminGET/api/assignments/:id/classesAdminJWT+RoleAssignments_READAssignmentsControllerPer-class stats
AdminGET/api/assignments/:id/historyAdminJWT+RoleAssignments_READAssignmentsControllerEvent history
AdminPOST/api/assignments/:id/publishAdminJWT+RoleAssignmentPublish_CREATEAssignmentLifecycleControllerPublish now/schedule
AdminPOST/api/assignments/:id/unscheduleAdminJWT+RoleAssignmentPublish_CREATEAssignmentLifecycleControllerReturn to draft
AdminPOST/api/assignments/:id/closeAdminJWT+RoleAssignmentPublish_UPDATEAssignmentLifecycleControllerClose
AdminPOST/api/assignments/:id/reopenAdminJWT+RoleAssignmentPublish_UPDATEAssignmentLifecycleControllerReopen
AdminPOST/api/assignments/:id/cancelAdminJWT+RoleAssignmentCancel_UPDATEAssignmentLifecycleControllerCancel
AdminPOST/api/assignments/:id/archiveAdminJWT+RoleAssignments_DELETEAssignmentLifecycleControllerArchive
AdminPOST/api/assignments/:id/unarchiveAdminJWT+RoleAssignments_RESTOREAssignmentLifecycleControllerUnarchive
AdminPOST/api/assignments/:id/attachmentsAdminJWT+Role+IP throttleAssignments_UPDATEAssignmentAttachmentsControllerUpload attachment
AdminDELETE/api/assignments/:id/attachments/:fileIdAdminJWT+Role+IP throttleAssignments_UPDATEAssignmentAttachmentsControllerRemove attachment
AdminGET/api/assignments/:id/submissionsAdminJWT+RoleAssignmentSubmissions_READAssignmentSubmissionsControllerSubmissions roster
AdminGET/api/assignments/:id/submissions/:rowIdAdminJWT+RoleAssignmentSubmissions_READAssignmentSubmissionsControllerOne pupil's record
AdminPUT/api/assignments/:id/submissions/:rowId/reviewAdminJWT+RoleAssignmentReview_UPDATEAssignmentSubmissionsControllerSave review (hold)
AdminPOST/api/assignments/:id/submissions/:rowId/completeAdminJWT+RoleAssignmentReview_UPDATEAssignmentSubmissionsControllerComplete/mark
AdminPOST/api/assignments/:id/submissions/:rowId/returnAdminJWT+RoleAssignmentReview_UPDATEAssignmentSubmissionsControllerReturn for correction
AdminPOST/api/assignments/:id/release-marksAdminJWT+RoleAssignmentReview_UPDATEAssignmentSubmissionsControllerBulk-release marks
AdminGET/api/assignment-categoriesAdminJWT+RoleAssignmentSettings_READAssignmentCategoriesControllerList categories
AdminPOST/api/assignment-categoriesAdminJWT+RoleAssignmentSettings_CREATEAssignmentCategoriesControllerCreate category
AdminPATCH/api/assignment-categories/:idAdminJWT+RoleAssignmentSettings_UPDATEAssignmentCategoriesControllerUpdate category
AdminDELETE/api/assignment-categories/:idAdminJWT+RoleAssignmentSettings_DELETEAssignmentCategoriesControllerDelete category
AdminGET/api/assignment-settingsAdminJWT+RoleAssignmentSettings_READAssignmentSettingsControllerRead module settings
AdminPATCH/api/assignment-settingsAdminJWT+RoleAssignmentSettings_UPDATEAssignmentSettingsControllerUpdate module settings
AdminGET/api/assignment-notification-rulesAdminJWT+RoleAssignmentNotificationSettings_READAssignmentNotificationRulesControllerList rules
AdminPUT/api/assignment-notification-rules/:eventAdminJWT+RoleAssignmentNotificationSettings_UPDATEAssignmentNotificationRulesControllerUpdate one rule
AdminGET/api/assignment-transfer-reviewsAdminJWT+RoleAssignments_READAssignmentTransferReviewsControllerList open reviews
AdminPOST/api/assignment-transfer-reviews/:id/resolveAdminJWT+RoleAssignments_UPDATEAssignmentTransferReviewsControllerResolve a review
AdminGET/api/assignment-statistics/dashboardAdminJWT+RoleAssignmentStats_READAssignmentStatsControllerDashboard summary
AdminGET/api/assignment-statistics/analyticsAdminJWT+RoleAssignmentStats_READAssignmentStatsControllerAnalytics + at-risk
AdminGET/api/assignment-files/:fileIdAdminJWT+Role+IP throttleAssignments_READAssignmentFilesController (admin)Download a file
AdminPOST/api/assignment-files/:fileId/rescanAdminJWT+RoleAssignmentSettings_UPDATEAssignmentFilesController (admin)Re-queue a failed scan
TeacherGET/api/mobile/teacher/assignments/optionsTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerCreate-form catalogue (own classes)
TeacherPOST/api/mobile/teacher/assignments/previewTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerPreview audience
TeacherGET/api/mobile/teacher/assignments/dashboardTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerDashboard tiles
TeacherGET/api/mobile/teacher/assignmentsTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerList visible assignments
TeacherPOST/api/mobile/teacher/assignmentsTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerCreate as self
TeacherGET/api/mobile/teacher/assignments/:idTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerOne assignment
TeacherPATCH/api/mobile/teacher/assignments/:idTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerUpdate
TeacherDELETE/api/mobile/teacher/assignments/:idTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerDelete draft/scheduled
TeacherGET/api/mobile/teacher/assignments/:id/classesTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerPer-class stats
TeacherGET/api/mobile/teacher/assignments/:id/historyTeacherJWT+Role, audiencenoneTeacherAssignmentsPortalControllerEvent history
TeacherPOST/api/mobile/teacher/assignments/:id/publishTeacherJWT+Role, audiencenoneTeacherAssignmentActionsPortalControllerPublish now/schedule
TeacherPOST/api/mobile/teacher/assignments/:id/unscheduleTeacherJWT+Role, audiencenoneTeacherAssignmentActionsPortalControllerReturn to draft
TeacherPOST/api/mobile/teacher/assignments/:id/closeTeacherJWT+Role, audiencenoneTeacherAssignmentActionsPortalControllerClose
TeacherPOST/api/mobile/teacher/assignments/:id/reopenTeacherJWT+Role, audiencenoneTeacherAssignmentActionsPortalControllerReopen
TeacherPOST/api/mobile/teacher/assignments/:id/cancelTeacherJWT+Role, audiencenoneTeacherAssignmentActionsPortalControllerCancel (if allowed)
TeacherPOST/api/mobile/teacher/assignments/:id/archiveTeacherJWT+Role, audiencenoneTeacherAssignmentActionsPortalControllerArchive
TeacherPOST/api/mobile/teacher/assignments/:id/unarchiveTeacherJWT+Role, audiencenoneTeacherAssignmentActionsPortalControllerUnarchive
TeacherPOST/api/mobile/teacher/assignments/:id/attachmentsTeacherJWT+Role+IP, audiencenoneTeacherAssignmentActionsPortalControllerUpload attachment
TeacherDELETE/api/mobile/teacher/assignments/:id/attachments/:fileIdTeacherJWT+Role+IP, audiencenoneTeacherAssignmentActionsPortalControllerRemove attachment
TeacherPOST/api/mobile/teacher/assignments/:id/release-marksTeacherJWT+Role, audiencenoneTeacherAssignmentActionsPortalControllerRelease marks
TeacherGET/api/mobile/teacher/assignments/:id/submissionsTeacherJWT+Role, audiencenoneTeacherAssignmentReviewPortalControllerReviewable roster
TeacherGET/api/mobile/teacher/assignments/:id/submissions/:rowIdTeacherJWT+Role, audiencenoneTeacherAssignmentReviewPortalControllerOne pupil's record
TeacherPUT/api/mobile/teacher/assignments/:id/submissions/:rowId/reviewTeacherJWT+Role, audiencenoneTeacherAssignmentReviewPortalControllerSave review
TeacherPOST/api/mobile/teacher/assignments/:id/submissions/:rowId/completeTeacherJWT+Role, audiencenoneTeacherAssignmentReviewPortalControllerComplete/mark
TeacherPOST/api/mobile/teacher/assignments/:id/submissions/:rowId/returnTeacherJWT+Role, audiencenoneTeacherAssignmentReviewPortalControllerReturn for correction
TeacherGET/api/mobile/teacher/assignment-files/:fileIdTeacherJWT+Role+IP, audiencenoneTeacherAssignmentFilesPortalControllerDownload a file
StudentGET/api/mobile/student/assignments/dashboardPupilJWT+Role, audiencenoneStudentAssignmentsPortalControllerDashboard buckets
StudentGET/api/mobile/student/assignmentsPupilJWT+Role, audiencenoneStudentAssignmentsPortalControllerOwn list
StudentGET/api/mobile/student/assignments/:idPupilJWT+Role, audiencenoneStudentAssignmentsPortalControllerOne assignment
StudentPUT/api/mobile/student/assignments/:id/submissionPupilJWT+Role+IP, audiencenoneStudentAssignmentSubmissionPortalControllerSave draft
StudentPOST/api/mobile/student/assignments/:id/submission/filesPupilJWT+Role+IP, audiencenoneStudentAssignmentSubmissionPortalControllerAttach a file
StudentDELETE/api/mobile/student/assignments/:id/submission/files/:fileIdPupilJWT+Role+IP, audiencenoneStudentAssignmentSubmissionPortalControllerRemove a file
StudentPOST/api/mobile/student/assignments/:id/submission/submitPupilJWT+Role+IP, audiencenoneStudentAssignmentSubmissionPortalControllerSubmit
StudentGET/api/mobile/student/assignment-files/:fileIdPupilJWT+Role+IP, audiencenoneStudentAssignmentFilesPortalControllerDownload a file
GuardianGET/api/mobile/guardian/children/:studentId/assignments/summaryGuardianJWT+Role, audiencenoneGuardianAssignmentsPortalControllerChild's summary
GuardianGET/api/mobile/guardian/children/:studentId/assignmentsGuardianJWT+Role, audiencenoneGuardianAssignmentsPortalControllerChild's list
GuardianGET/api/mobile/guardian/children/:studentId/assignments/:idGuardianJWT+Role, audiencenoneGuardianAssignmentsPortalControllerOne assignment
GuardianGET/api/mobile/guardian/children/:studentId/assignment-files/:fileIdGuardianJWT+Role+IP, audiencenoneGuardianAssignmentFilesPortalControllerDownload an attachment only

Note on Nest route resolution: AssignmentsController, AssignmentLifecycleController and AssignmentSubmissionsController all share the base path assignments. Static segments (options, preview) are declared before :id in AssignmentsController, and every lifecycle/submissions route has a literal segment after :id (:id/publish, :id/submissions/:rowId), so no controller's route is ever shadowed by another's :id catch-all.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
AdminJwtAuthGuard, RoleGuard, @Permissions("Module_ACTION")req.user.id, active role's permission grantsPer-route (see §4)NoStandard admin permission model.
Teacher portalJwtAuthGuard, RoleGuard (no @Permissions())req.user resolved to a live staff row via resolveTeacherN/A — AssignmentView_READ gates the panel screen onlyNoRoleGuard allows the handler through NO_PERMISSION_ADMIN_HANDLERS; the actual control is AssignmentAudienceResolver.resolveTeacher plus AssignmentAccessService row scoping.
Student portalsamereq.user resolved to a live students row via resolveStudentN/ANoIdentity layer filters deleted_at only; row-scope layer additionally filters record_status='active' and assignment status.
Guardian portalsamereq.user resolved + studentId bound to a linked child via resolveGuardianChildN/ANoThe child binding happens before any row is read — an unlinked studentId answers 404, not 403.
File download (all)JwtAuthGuard, RoleGuard, IpThrottlerGuardPer-surface actor as aboveAssignments_READ (admin only)NoScope check happens inside AssignmentFileDownloadService.open, keyed to the resolved actor.

Every endpoint requires a valid session (JwtAuthGuard is global with no @Public() opt-out anywhere in this module). No guest/anonymous access exists on any assignments route.

6. DTO and Model Reference

6.1 AssignmentCreateBaseDto (shared by AdminCreateAssignmentDto and TeacherCreateAssignmentDto)

FieldTypeRequiredDefaultValidationExampleSource
clientRequestIduuidNo—@IsUUID()—replay key
academicSessionIduuidYes—@IsUUID("all")—
subjectIduuidYes—@IsUUID("all")—
categoryIduuid | nullNonull@IsUUID("all")—
titlestringYes—trimmed, ≤200 chars, no leading/trailing/control whitespace"Algebra worksheet 4"
instructionsHtmlstringNo""sanitised server-side"<p>Complete exercises 1-10.</p>"
classSubjectIdsuuid[]Yes—≤60 entries, unique, each a uuid of any version—
linksAssignmentLinkInputDto[]No—≤10 entries—see 6.4
dueAtISO-8601 datetimeYes—@IsISO8601()—
latePolicyenumNo"allowed"not_allowed|allowed|until_final_deadline—
finalDeadlineAtISO-8601 | nullNo—required iff latePolicy='until_final_deadline'; must be after dueAt (server)—
allowTextResponsebooleanNotrue——
allowFileUploadbooleanNotrue——at least one of the two must end up true
maxFilesintNo51..20—
submissionFileTypesenum[]No7-type defaulteach ∈ the 14-type closed list—
resubmissionPolicyenumNo"on_request"none|unlimited|limited|on_request—
maxAttemptsint | nullNo—required iff resubmissionPolicy='limited'; 2..20—
isGradedbooleanNofalse——
maxMarksint | nullNo—required iff isGraded; 1..1000—
passMarksint | nullNo—0..maxMarks—
marksReleaseenumNo"on_review"on_review|manual—

AdminCreateAssignmentDto adds one field:

FieldTypeRequiredNotes
teacherIduuid | nullNoA live staff row; a mismatch with target classes is a warning, not a rejection.

TeacherCreateAssignmentDto adds no fields — teacherId is implicitly the caller and forbidNonWhitelisted (global ValidationPipe) rejects the field if sent.

6.1a AdminUpdateAssignmentDto / TeacherUpdateAssignmentDto

Both are the same class body (AssignmentUpdateBaseDto, authoring/core/dto/update-assignment.dto.ts:47); neither accepts teacherId — PUT :id/teacher is the only writer of that column. Every field is optional except version; a field that is absent keeps the stored value.

FieldTypeRequiredStatus restrictionNotes
versionstringYes—The assignment's version.
academicSessionId, subjectIduuidNoA different value only while draft/scheduled, else 409 ASSIGNMENT_SUBJECT_LOCKEDResending the current value is accepted in any editable status, so a full edit form can post both.
categoryIduuid | nullNo—An inactive category is accepted only when it is already the stored one.
title, instructionsHtmlstringNo—Same rules as create.
classSubjectIdsuuid[]Nodraft/scheduled only, else 409 ASSIGNMENT_SUBJECT_LOCKEDReplaces the whole target set.
addClassSubjectIdsuuid[]Nopublished/closed only, else 409 ASSIGNMENT_SUBJECT_LOCKEDRe-adding a removed class clears its removed_at and revives applicable pupil rows.
removeClassSubjectIdsuuid[]Nopublished/closed only, else 409 ASSIGNMENT_SUBJECT_LOCKEDMust leave at least one live class, else 409 ASSIGNMENT_NO_CLASSES. Unsubmitted pupil rows of a removed class become not_applicable.
linksAssignmentLinkInputDto[]No—Each URL normalised; 400 ASSIGNMENT_LINK_INVALID on failure.
dueAt, latePolicy, finalDeadlineAt, allowTextResponse, allowFileUpload, maxFiles, submissionFileTypes, resubmissionPolicy, maxAttemptsas createNo—Validated against the merged row (below).
isGraded, maxMarks, passMarks, marksReleaseas createNoLocked once any attempt has been reviewed: 409 ASSIGNMENT_GRADING_LOCKED
notifyStudentsbooleanNo (default false)—When true, the assignment is published, and content or dueAt changed, the updated notification is sent (assignment-update.service.ts:434-445). Otherwise no notification is sent.

The update service overlays the request on the locked row and validates the merged result (validateMergedAssignmentFields, assignment-merge-validation.util.ts:79-188) before writing. That is where the cross-field codes come from:

Rule on the merged rowError
dueAt changes on a published/closed assignment and is less than 5 minutes away400 ASSIGNMENT_DUE_IN_PAST
dueAt changes on a scheduled assignment and no longer leaves publish lead plus sweep slack after publishAt400 ASSIGNMENT_PUBLISH_AT_INVALID
finalDeadlineAt present without latePolicy='until_final_deadline', absent with it, or not after dueAt400 ASSIGNMENT_FINAL_DEADLINE_INVALID
Both submission methods off; maxFiles outside 1..20; empty or unknown submissionFileTypes; maxAttempts present without resubmissionPolicy='limited', absent with it, or outside 2..20400 ASSIGNMENT_SUBMISSION_SETTINGS_INVALID
maxMarks present without isGraded, absent with it, or outside (0, 1000]; passMarks without maxMarks or outside [0, maxMarks]400 ASSIGNMENT_GRADING_INVALID
The new attempt allowance (limited → maxAttempts, unlimited → 100, otherwise 1) is below the highest attempt_count any pupil already has409 ASSIGNMENT_MAX_ATTEMPTS_BELOW_USED

6.2 AssignmentDetailDto (response — admin/teacher detail)

FieldTypeNotes
iduuidpublicId
title, instructionsHtml, instructionsTextstring
status, displayStatusenum, enumstored vs derived
academicSession{id, name}
subject{id, name}
category{id, name} | null
teacher{id, name, employeeCode} | null
dueAt, previousDueAt, dueChangedAt, publishAt, publishedAt, closedAt, cancelledAt, archivedAtISO datetime | null
cancelReasonstring | null
latePolicy, finalDeadlineAtenum, ISO | null
allowTextResponse, allowFileUpload, maxFiles, submissionFileTypesmixed
resubmissionPolicy, maxAttemptsenum, int | null
isGraded, maxMarks, passMarks, marksRelease, marksReleasedAtmixed
links{label, url}[]
attachmentsAssignmentFileDto[]
classes{classSubjectId, className, subjectName, isRemoved}[]
versionstringopaque token
grant{canManage, canDelete, canCancel, canReassignTeacher, canReadSubmissions, canReview}per-caller capability flags, never trust-worthy for authorization on the client — the server enforces independently on every write
academicSessionLockedbooleantrue once the assignment's academic session has locked_at set; every write to the assignment and everything under it (classes, roster rows, submission attempts, links, files) then answers 409 ACADEMIC_SESSION_LOCKED
createdAt, updatedAtISO datetime

6.3 AssignmentFileDto

FieldTypeNotes
iduuidpublicId
kind"attachment" | "submission"
originalName, mimeType, sizeBytesmixed
scanStatus"pending"|"clean"|"rejected"|"failed"Only "clean" files are downloadable.
uploadedAt, uploadedByNamemixed
sortOrderint

6.4 AssignmentLinkInputDto

FieldTypeRequiredValidationSource
labelstringYes≤120 chars, trimmed, no control chars
urlstringYeshttps:// only, no userinfo, ≤2048 chars

6.5 SaveDraftDto / SubmitAssignmentDto (pupil submission)

FieldTypeRequiredNotes
version (SaveDraftDto)stringNoAbsent creates the draft if none exists; present on an existing draft or 409 ASSIGNMENT_DRAFT_VERSION_CONFLICT.
textHtmlstring | nullYes (may be null)≤400,000 raw chars pre-sanitisation; sanitised/derived text is separately bounded by assignment_settings.max_submission_text_chars and ASSIGNMENT_SUBMISSION_TEXT_PLAIN_MAX/_HTML_MAX.
clientSubmissionId (SubmitAssignmentDto)uuidYesIdempotency key — a replay with the same key and the same latest attempt is a no-op success.
draftVersion (SubmitAssignmentDto)stringNoRequired if a draft exists; absent only for a files-less first submit with no draft.

6.6 SaveReviewDto / CompleteReviewDto / ReturnSubmissionDto

FieldTypeRequiredValidationApplies To
attemptIduuidYesBound server-side to rowIdall three
versionstringYesThe ATTEMPT's version, not the row'sall three
marksnumber | nullConditional2 decimal places max, ≥0, required iff isGradedsave/complete
gradestring | nullNo^[A-Za-z0-9+-]{1,8}$save/complete
feedbackHtmlstring | nullNo (save/complete) / Yes (return)≤100,000 charsall three
resubmitDueAtISO datetime | nullNoMust be in the future when given; required when the base window is already closedreturn only

6.7 AssignmentAudiencePreviewRequestDto / AssignmentAudiencePreviewDto

Request: { academicSessionId: uuid; subjectId: uuid; classSubjectIds: uuid[] } (same shape/limits as the create DTO's targeting fields). Response: { totalPupils: number; byClass: { classSubjectId, className, pupilCount }[] } — a read-only projection, no persistence.

6.8 ListAssignmentsQueryDto / ListAssignmentSubmissionsQueryDto / ListStudentAssignmentsQueryDto

All extend the shared QueryDto (page, size, pagination, sort, search). Assignment-specific additions:

FieldTypeDefaultNotes
status/displayStatus filterenum—Filters on the derived status where present
subjectId, categoryId, teacherId, academicSessionIduuid—admin/teacher list only
filter (submissions roster)enum ASSIGNMENT_ROSTER_FILTER"all"all|not_submitted|submitted|late|pending_review|marked|returned|resubmitted|not_applicable
classId (submissions roster)uuid—Narrows the roster to one target class
bucket (student list)enum ASSIGNMENT_STUDENT_BUCKET"all"See §3

6.9 AssignmentTransferReviewDto / ResolveTransferReviewDto

FieldTypeNotes
iduuidThe review's publicId.
student{id, name, studentCode}
assignment{id, title, subject: {id, name}}
rowIduuidThe pupil's assignment_students.id.
rowStateenum (AssignmentStudentState)The row's current state at the time the review is read.
fromClass{id, label}The class the pupil left.
toClass{id, label} | nullThe pupil's current class, when they still have a live target one.
detectedAtISO datetimeWhen the roster sweep raised the review.
resolution"keep" | "not_applicable" | nullnull while open.
decidedAutomaticallybooleantrue for a decision the roster sweep made on its own: a keep/not_applicable opened already-resolved by transfer_mode, or an open review the sweep later auto-resolves keep (reason: "already_submitted" in its history entry) once the pupil has submitted work while the question sat open (shared/assignment-roster-decisions.service.ts) — in every case it only shows up once status=resolved; false for one an administrator resolved through this endpoint.
resolvedAtISO datetime | null
resolvedByNamestring | null
notestring | nullThe admin's free-text note, if any.
schoolTimezonestringThe school's configured timezone (for example "Asia/Kathmandu"), so the client can render detectedAt/resolvedAt in local time without a second lookup.

ResolveTransferReviewDto (request body for POST :id/resolve): resolution ("keep" | "not_applicable", required) and note (string, optional, ≤500 chars).

7. Enum Reference

EnumValueMeaningRuntime EffectSource
AssignmentStatusdraft|scheduled|published|closed|cancelledStored lifecycleGates every lifecycle transitionassignment.constants.ts
AssignmentDisplayStatusdraft|scheduled|active|due_today|overdue|closed|cancelledDerived, time-sensitive lifecycle shown to viewersComputed per request, never persistedsame
AssignmentLatePolicynot_allowed|allowed|until_final_deadlineWhether/how late submission is acceptedDrives baseOpen/canSubmitsame
AssignmentResubmissionPolicynone|unlimited|limited|on_requestHow many further attempts are allowed after a first submitDrives supersedeAllowedsame
AssignmentMarksReleaseon_review|manualWhen a completed mark becomes pupil-visibleDrives result_published_at timingsame
AssignmentTransferModekeep|follow_class|flag_for_adminSchool policy for a pupil who leaves a target classDrives roster sweep decisionssame
AssignmentStudentStateassigned|in_progress|submitted|under_review|returned|completed|not_applicableStored per-pupil stateSee feature doc §7.2same
AssignmentStudentDisplayStatus14 values (§3)Derived pupil-facing labelPresentation onlysame
AssignmentStudentBucketall|upcoming|due_today|overdue|in_progress|awaiting_review|returned|completed|not_submittedDashboard/list groupingFilter valuesame
AssignmentNotApplicableReasontransferred|withdrawn|class_removedWhy a row was excludedAudit/history detailsame
AssignmentAttemptStatusdraft|submitted|under_review|returned|completed|supersededPer-attempt stateSee feature doc §7.3same
AssignmentFileKindattachment|submissionWhich parent a file belongs toDetermines the FK that must be setsame
AssignmentScanStatuspending|clean|rejected|failedFile safety verdictOnly clean is downloadablesame
AssignmentFileTypepdf,doc,docx,txt,rtf,xls,xlsx,csv,ppt,pptx,jpg,png,webp,gifAllowed extensionsCross-checked against sniffed MIMEsame
AssignmentNotificationEventpublished,updated,submitted,reminder_3d,reminder_1d,due_today,overdue,returned,marked,cancelledNotification rule keysOne assignment_notification_rules row eachsame
AssignmentRosterFilterall,not_submitted,submitted,late,pending_review,marked,returned,resubmitted,not_applicableRoster query filterListAssignmentSubmissionsQueryDto.filtersame
AssignmentHistoryEvent30 values (see backend doc §5.2)Audit event namesassignment_history.eventsame
AssignmentScanRejectCode13 values (see backend doc §6.7)Internal reject reasonNever shown to the uploader; only the generic ASSIGNMENT_FILE_SECURITY_MESSAGE issame

8. Endpoint Reference

The endpoints below are grouped by capability; every route from §4 is covered either here in full detail or, for the many structurally-identical lifecycle/CRUD siblings, in the route-by-route matrix in §13.1. Every admin route has an exact teacher-portal mirror with the same request/response shape and no @Permissions(); rather than repeat each pair twice, the teacher-portal differences are called out inline where the behavior differs (scope, not shape).

8.1 POST /api/assignments (and POST /api/mobile/teacher/assignments)

Purpose

Creates a new assignment, always as a draft. Used by the admin panel's and the teacher portal's assignment-creation form. The admin route accepts an optional teacherId; the teacher route always uses the caller as the teacher and rejects the field if sent.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/assignments/authoring/admin/assignments.controller.ts, apps/api/src/modules/assignments/authoring/portal/teacher-assignments-portal.controller.ts
DTOapps/api/src/modules/assignments/authoring/core/dto/create-assignment.dto.ts
Serviceapps/api/src/modules/assignments/authoring/core/assignment-write.service.ts
Schemapackages/db/src/schema/school/assignments.ts (assignments, assignment_classes)
Testsapps/api/src/modules/assignments/__tests__/assignment-authoring.integration.spec.ts

Auth and Permissions

  • Auth: JWT required.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: Assignments_CREATE (admin route only; teacher route carries none — resolveTeacher is the control).
  • Guest support: none.
  • Rate limit: none on this route (upload routes are throttled; this is JSON-only).
  • Idempotency: clientRequestId, unique per (createdByUserId, clientRequestId). Both the admin and the teacher route answer 201 with "Assignment created." for a genuine insert and 200 with "Assignment already created." when a replay returns the existing assignment.

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>
BodyYesAdminCreateAssignmentDto or TeacherCreateAssignmentDto (§6.1)
{
  "academicSessionId": "01973a...",
  "subjectId": "01973b...",
  "title": "Algebra worksheet 4",
  "classSubjectIds": ["01973c..."],
  "dueAt": "2026-09-22T10:00:00.000Z"
}

Full request with optional fields:

{
  "clientRequestId": "01973d...",
  "academicSessionId": "01973a...",
  "subjectId": "01973b...",
  "categoryId": "01973e...",
  "title": "Algebra worksheet 4",
  "instructionsHtml": "<p>Complete exercises 1-10.</p>",
  "classSubjectIds": ["01973c...", "01973f..."],
  "links": [{ "label": "Reference", "url": "https://example.com/ref" }],
  "dueAt": "2026-09-22T10:00:00.000Z",
  "latePolicy": "until_final_deadline",
  "finalDeadlineAt": "2026-09-24T10:00:00.000Z",
  "allowTextResponse": true,
  "allowFileUpload": true,
  "maxFiles": 3,
  "submissionFileTypes": ["pdf", "docx"],
  "resubmissionPolicy": "limited",
  "maxAttempts": 3,
  "isGraded": true,
  "maxMarks": 20,
  "passMarks": 10,
  "marksRelease": "manual",
  "teacherId": "01973g..."
}

Response

Both routes: 201 with "Assignment created." on genuine creation, 200 with "Assignment already created." when a clientRequestId replay returned the existing row.

{
  "success": true,
  "message": "Assignment created.",
  "data": {
    "id": "01973h...",
    "status": "draft",
    "displayStatus": "draft",
    "version": "0"
  }
}

(Full shape: AssignmentDetailDto, §6.2.)

Side Effects

  • Database writes: assignments (insert), assignment_classes (one row per target).
  • Cache: none.
  • BullMQ jobs: none.
  • Realtime events: none.
  • Notifications: none (a draft is invisible to anyone but its author/managers).
  • Audit logs: ActivityAuditInterceptor records the mutating request; assignment_history gets a created entry.
  • External API calls: none.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
400class-validator errorsMissing/invalid fieldFix the request bodyglobal ValidationPipe
400ASSIGNMENT_LINK_INVALIDA links[].url is not a parseable URL, is not https:, carries a username/password, or exceeds 2048 characters after normalisationCorrect the linkshared/assignment-html.util.ts:126-158 (normaliseLinkUrl), called at assignment-write.service.ts:251
400ASSIGNMENT_FINAL_DEADLINE_INVALIDA final deadline is given without latePolicy='until_final_deadline' (or vice versa), or it is not after dueAtFix the late-policy fields togetherauthoring/core/assignment-merge-validation.util.ts:58-70, called from assignment-write.service.ts:211-215
400ASSIGNMENT_SUBMISSION_SETTINGS_INVALIDNeither submission method allowed, maxFiles outside 1..20, an empty or unknown submissionFileTypes, or maxAttempts inconsistent with resubmissionPolicyFix the submission settings togethersame
400ASSIGNMENT_GRADING_INVALIDmaxMarks/passMarks inconsistent with isGraded, or out of rangeFix the grading fields togethersame
404ACADEMIC_SESSION_NOT_FOUND / SUBJECT_NOT_FOUNDUnknown session or subject public id (codes owned by the academic-structure modules)Re-pick the session or subjectassignment-write.service.ts:120-137
404ASSIGNMENT_CATEGORY_NOT_FOUNDcategoryId unknown or inactiveRe-pick the categoryassignment-write.service.ts:152
404ASSIGNMENT_TEACHER_NOT_FOUND(admin only) teacherId is not a live staff rowPick another teacherassignment-write.service.ts:169
404ASSIGNMENT_CLASS_NOT_TAUGHT(teacher only) A target class-subject is not taught by the callerChoose a class you teachAssignmentAccessServiceImpl.assertClassSubjectsUsable (shared/assignment-access.service.ts:287)
422ASSIGNMENT_CLASS_SUBJECT_INVALID(admin) A target class-subject is inactive, in another session, or teaches a different subject; or (both) a duplicate id was sentFix the class selectionsame
422ASSIGNMENT_AUDIENCE_TOO_LARGEMore than 60 target classes selectedReduce the class selectionsame

Edge Cases

  • Empty classSubjectIds: allowed at create (a draft may have zero targets); publishing without any is blocked separately (ASSIGNMENT_NO_CLASSES).
  • Duplicate class-subject in the array: rejected (422).
  • Replayed clientRequestId: idempotent, 200, no second row.
  • Cross-field rules on create. AssignmentWriteService.create builds the merged field set as a draft with dueAtChanged: false and publishAt: null and runs it through the same validateMergedAssignmentFields that PATCH uses (authoring/core/assignment-write.service.ts:211-215), before the insert. A body that breaks one of those rules — finalDeadlineAt without latePolicy='until_final_deadline', both submission methods off, or isGraded: true without maxMarks — returns the same named 400 code create-time as it does on update: ASSIGNMENT_FINAL_DEADLINE_INVALID, ASSIGNMENT_SUBMISSION_SETTINGS_INVALID, or ASSIGNMENT_GRADING_INVALID (§12). The due-date lead-time check (ASSIGNMENT_DUE_IN_PAST/ASSIGNMENT_PUBLISH_AT_INVALID) is not part of this call — a draft's due date is only checked when it is later published or scheduled.

Example Requests

POST /api/assignments HTTP/1.1
Authorization: Bearer TOKEN
Content-Type: application/json
curl -X POST "$API_URL/api/assignments" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"academicSessionId":"...","subjectId":"...","title":"Algebra worksheet 4","classSubjectIds":["..."],"dueAt":"2026-09-22T10:00:00.000Z"}'

8.2 POST /api/assignments/:id/publish (and teacher mirror)

Purpose

Publishes an assignment immediately, or schedules a future publish. Publishing now materialises the pupil roster and notifies; scheduling defers both to the publish-due sweep.

Source Evidence

EvidencePath
Controllerassignment-lifecycle.controller.ts, teacher-assignment-actions-portal.controller.ts
DTOauthoring/core/dto/assignment-lifecycle.dto.ts (PublishAssignmentDto)
Serviceauthoring/core/assignment-lifecycle.service.ts, authoring/core/assignment-publish-guards.util.ts
Schemaassignments, assignment_students, assignment_history
Testsassignment-authoring.integration.spec.ts

Auth and Permissions

  • Auth: JWT required.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: AssignmentPublish_CREATE (admin); none (audience) for teacher.
  • Guest support: none.
  • Rate limit: none.
  • Idempotency: calling publish-now again on an already-published assignment, or schedule again with the same publishAt, is a no-op returning the current detail. The already-in-target-state check runs before the version check, so this no-op succeeds even with a stale version — every other lifecycle route (unschedule, close, reopen, cancel, archive, unarchive) follows the same ordering, which is what makes a double-click safe rather than a race to a 409.

Request

{ "version": "0", "mode": "now" }

or

{ "version": "0", "mode": "schedule", "publishAt": "2026-09-20T06:00:00.000Z" }

Response

200 with AssignmentDetailDto, status now published or scheduled.

Side Effects

  • Database writes: assignments (status/publish fields), assignment_students (insert, publish-now only), assignment_history (published/scheduled).
  • Jobs: none directly (the sweep that later publishes a scheduled assignment is cron-driven, not enqueued here).
  • Notifications: published event to newly-materialised pupils and their guardians (publish-now only).

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404ASSIGNMENT_NOT_FOUNDRow not found or out of the actor's manage scopeAssignment not foundlifecycle service
409ASSIGNMENT_VERSION_CONFLICTversion mismatchReload and retrylifecycle service
409ASSIGNMENT_NOT_EDITABLEAssignment is archivedUnarchive firstlifecycle service
409ASSIGNMENT_INVALID_TRANSITIONNot currently draft/scheduledAction not available in this statelifecycle service
400ASSIGNMENT_DUE_IN_PAST(mode now) due date less than 5 minutes awayDue date too soon to publish nowlifecycle service
400ASSIGNMENT_PUBLISH_AT_INVALID(mode schedule) missing/too-soon/leaves-no-room publishAtFix the schedule timelifecycle service
422ASSIGNMENT_NO_CLASSESNo live target classesAdd at least one classassignment-publish-guards.util.ts:60-65
422ASSIGNMENT_AUDIENCE_TOO_LARGEMore than 60 live target classes, or more than 3000 distinct live pupils across themNarrow the target classesassignment-publish-guards.util.ts:66-71, :128-133
404ASSIGNMENT_CLASS_NOT_TAUGHT(teacher only) The caller no longer teaches every live target classAsk an administrator to publish, or remove that classassignment-publish-guards.util.ts:73-83
422ASSIGNMENT_TEACHER_REQUIREDassignment_settings.require_teacher is on and no teacher is assignedAssign a teacher (PUT :id/teacher)assignment-publish-guards.util.ts:90-95
409ASSIGNMENT_HAS_REJECTED_ATTACHMENTSAn attachment that is not removed has scan_status='rejected'Remove or replace the rejected attachment, then publishassignment-publish-guards.util.ts:97-112

The guards run in the order listed and stop at the first failure. The publish-due sweep re-runs the same guards; a failure there does not reach any HTTP caller — the assignment returns to draft and the creator receives a publish_failed notification.

Edge Cases

  • Already published (mode now): no-op, 200, unchanged detail.
  • Already scheduled at the same publishAt (mode schedule): no-op.
  • The scheduled sweep itself hits the same guard set at fire time; a failure there reverts to draft rather than returning an error to any caller (no caller is waiting — it is a background tick).

8.3 PUT /api/mobile/student/assignments/:id/submission/submit

Purpose

Submits the pupil's current work — text and/or previously-attached files — for review. Called after the pupil has saved a draft and/or attached files, or directly for a files-less first submit.

Source Evidence

EvidencePath
Controllerportal/student/student-assignment-submission-portal.controller.ts
DTOsubmissions/core/dto/assignment-submission-request.dto.ts (SubmitAssignmentDto)
Servicesubmissions/core/assignment-submission.service.ts
Schemaassignment_submission_attempts, assignment_students
Testsassignment-submission.integration.spec.ts

Auth and Permissions

  • Auth: JWT required.
  • Guard chain: JwtAuthGuard, RoleGuard, IpThrottlerGuard.
  • Permission: none — resolveStudent is the control.
  • Guest support: none.
  • Rate limit: RATE_LIMITS.CUSTOMER_WRITE.
  • Idempotency: clientSubmissionId; a replay matching the current latest non-draft attempt is a no-op success.

Request

{ "clientSubmissionId": "01973i...", "textHtml": "<p>My answer...</p>" }

Minimal (files-only, no draft yet):

{ "clientSubmissionId": "01973i...", "textHtml": null }

Response

200 with the pupil's StudentAssignmentDetailDto (own record + attempt history), re-read after commit.

Side Effects

  • Database writes: prior pending attempt (if any) → superseded; target attempt → submitted; assignment_students counters/state; assignment_history (submitted).
  • Notifications: submitted to the pupil (confirmation), guardians (low priority), and the teacher.

Error Cases

HTTP StatusError CodeCondition
404ASSIGNMENT_NOT_FOUNDNo visible assignment or no pupil row
400ASSIGNMENT_SUBMISSION_EMPTYNo text and no non-rejected file
400ASSIGNMENT_TEXT_NOT_ALLOWEDallowTextResponse=false but text given
400ASSIGNMENT_TEXT_TOO_LONGText exceeds the school's/module's max
409ASSIGNMENT_SUBMISSION_HAS_REJECTED_FILESA rejected file is still attached
409ASSIGNMENT_SUBMISSION_FILE_LIMIT_EXCEEDEDToo many files on the draft
409ASSIGNMENT_FILES_NOT_ALLOWEDallowFileUpload=false but files attached
409ASSIGNMENT_ALREADY_COMPLETEDRow already completed
409ASSIGNMENT_NOT_ACCEPTINGRow under_review, or window closed with no resubmission path
409ASSIGNMENT_RESUBMISSION_NOT_ALLOWEDAttempt budget exhausted
409ASSIGNMENT_VERSION_CONFLICTStale replay of a clientSubmissionId that is no longer the latest
409ASSIGNMENT_DRAFT_VERSION_CONFLICTdraftVersion mismatch, or a draft exists but none was expected
403STUDENT_GRADUATED_READ_ONLYThe pupil's graduate access is grace or ended — checked against the resolved studentId, after resolveStudent, before the write
409ACADEMIC_SESSION_LOCKEDThe assignment's academic session is locked

Edge Cases

  • Submitting with no draft and no files, only textHtml: allowed — creates the first attempt directly.
  • Every attached file rejected and no text: treated specially — this is ASSIGNMENT_SUBMISSION_EMPTY only if it is also the FIRST submission; if it is a resubmission scenario the content-rejection logic (pendingContentRejectedSql) instead grants an extra chance to fix it.
  • Class removed since assignment: window closes even mid-composition; submit answers 409 ASSIGNMENT_NOT_ACCEPTING.
  • Save draft, file upload/removal and submit all run the same two checks in the same order — resolve the pupil, then assertNotGraduatedReadOnly — so a graduated pupil's session and file routes on this controller answer the same 403 STUDENT_GRADUATED_READ_ONLY as submit.

8.4 PUT /api/assignments/:id/submissions/:rowId/review (and teacher mirror), POST .../complete, POST .../return

Purpose

The three review actions a teacher/admin takes on one pupil's latest attempt: hold marks/feedback privately (review), finalise and release per policy (complete), or send back for another try (return).

Source Evidence

EvidencePath
Controllersubmissions/admin/assignment-submissions.controller.ts, submissions/portal/teacher-assignment-review-portal.controller.ts
DTOsubmissions/core/dto/assignment-submission-request.dto.ts
Servicesubmissions/core/assignment-review.service.ts
Schemaassignment_submission_attempts, assignment_students
Testsassignment-submission.integration.spec.ts

Auth and Permissions

  • Auth: JWT required.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: AssignmentReview_UPDATE (admin); none (teacher, scoped by reviewRow).
  • Rate limit: none.
  • Idempotency: none beyond the version optimistic check — a duplicate call with a stale version is rejected, not silently accepted.

Request (all three share attemptId + version)

{ "attemptId": "01973j...", "version": "0", "marks": 15, "grade": "A", "feedbackHtml": "<p>Good work.</p>" }

Return requires feedbackHtml and accepts an optional deadline:

{ "attemptId": "01973j...", "version": "1", "feedbackHtml": "<p>Please redo question 3.</p>", "resubmitDueAt": "2026-09-25T10:00:00.000Z" }

Response

200 with AssignmentSubmissionRecordDto — the full pupil record including attempt history.

Side Effects

  • review: attempt/row → under_review; history review_saved; no notification (nothing pupil-visible yet).
  • complete: attempt/row → completed, finalMarks/finalGrade set; result_published_at set immediately when marksRelease='on_review' or the assignment already has a marksReleasedAt from an earlier bulk release; history completed; marked notification iff visible immediately.
  • return: attempt → returned, row → returned, result cleared, resubmit_due_at set/cleared, extra_attempts_granted incremented (unless policy unlimited); history returned; returned notification.

Error Cases

HTTP StatusError CodeApplies To
404ASSIGNMENT_NOT_FOUNDall three
404ASSIGNMENT_SUBMISSION_NOT_FOUNDall three (row or attempt not found/out of scope, or attemptId not bound to rowId)
409ASSIGNMENT_VERSION_CONFLICTall three
409ASSIGNMENT_ATTEMPT_NOT_REVIEWABLEall three (not the latest attempt, or wrong status)
400ASSIGNMENT_MARKS_INVALIDreview, complete
400ASSIGNMENT_FEEDBACK_REQUIREDreturn with feedback that carries no text (also raised for over-length feedback on any of the three)
409ASSIGNMENT_NOT_EDITABLEall three (assignment not published/closed, or archived)
409ASSIGNMENT_RESUBMISSION_NOT_ALLOWEDreturn (policy none, or extra-attempt budget exhausted)
400ASSIGNMENT_RESUBMIT_DUE_REQUIREDreturn (no deadline given and the base window is already closed)
400ASSIGNMENT_RESUBMIT_DUE_IN_PASTreturn (deadline not in the future)

Edge Cases

  • return on an assignment that is only closed (not published): rejected — returnSubmission requires strictly published, unlike review/complete which also accept closed.
  • complete with marksRelease='manual' and no prior release on this assignment: the mark is saved but held; a subsequent POST :id/release-marks is required to make it visible. Once that release has happened once, every later complete on the same assignment publishes immediately regardless of the policy.
  • return when the pupil is already at the extra-attempt ceiling (policy not unlimited and extraAttemptsGranted >= ASSIGNMENT_MAX_EXTRA_ATTEMPTS): 409 ASSIGNMENT_RESUBMISSION_NOT_ALLOWED, same code the pupil's own submit uses at the attempt ceiling.
  • Visibility is checked before status: an assignment outside the reviewer's visibleAssignment scope is 404 ASSIGNMENT_NOT_FOUND even when the row/attempt would otherwise be a valid target — a reviewer never learns an out-of-scope assignment's current status from the shape of the error.
  • Reviewing an attempt superseded by a newer submission mid-review: 409 ASSIGNMENT_ATTEMPT_NOT_REVIEWABLE.

8.5 File download — GET .../assignment-files/:fileId (all four audiences)

Purpose

Streams a file's bytes: an assignment attachment (any audience whose scope covers the assignment) or a pupil's own submitted file (teacher review, or the pupil themselves — never a guardian, and never a pupil's still-draft attempt even for staff: a submission-file lookup explicitly excludes status='draft').

Source Evidence

EvidencePath
Controllerfiles/admin/assignment-files-admin.controller.ts, files/portal/teacher-assignment-files-portal.controller.ts, .../student-assignment-files-portal.controller.ts, .../guardian-assignment-files-portal.controller.ts
Servicefiles/assignment-file-download.service.ts, files/assignment-file-download-headers.ts
Schemaassignment_files
Testsassignment-scan-worker.integration.spec.ts

Auth and Permissions

  • Auth: JWT required.
  • Guard chain: JwtAuthGuard, RoleGuard, IpThrottlerGuard (RATE_LIMITS.UPLOAD_READ).
  • Permission: Assignments_READ (admin only); none (portal, scoped by actor kind + file kind).
  • Guardian scope: attachments only — a submission file filePublicId for a guardian actor is refused as 404, not a kind-mismatch error, so the response gives no signal about whether the id even exists.

Response

Not a JSON envelope — a binary stream. setAssignmentFileDownloadHeaders (files/assignment-file-download-headers.ts:21-27) sets Content-Type (the stored MIME), X-Content-Type-Options: nosniff, Cache-Control: private, no-store, Content-Security-Policy: sandbox; default-src 'none', Content-Length, and Content-Disposition. Error responses before the stream starts use the normal JSON error envelope.

Error Cases

Scope is checked before scan status, so an id the caller may not see is always 404, whatever its status.

HTTP StatusError CodeConditionConsumer ActionSource
404ASSIGNMENT_FILE_NOT_FOUNDFile not found, outside the actor's scope, or (guardian) a submission fileTreat as missing; do not retryassignment-file-download.service.ts:32-36
409ASSIGNMENT_FILE_NOT_READYscan_status='pending'Show "checking file" and retry after a short delayassignment-file-download.service.ts:102-108
409ASSIGNMENT_FILE_SCAN_FAILEDscan_status='failed'Show "file unavailable"; an administrator can rescan it (POST /api/assignment-files/:fileId/rescan)assignment-file-download.service.ts:109-114
410ASSIGNMENT_FILE_REJECTEDscan_status='rejected', or the attachment was removed, or its bytes were purgedShow the security-check message; the file will never become available — upload a replacementassignment-file-download.service.ts:115-124

Edge Cases

  • Requesting a pending file mid-scan: 409 ASSIGNMENT_FILE_NOT_READY, retry shortly.
  • Requesting a rejected file: 410 — rejected bytes are retained for the quarantine window but never streamed to anyone, including an administrator.

8.6 File upload and removal — attachments and submission files

Routes: POST /api/assignments/:id/attachments, DELETE /api/assignments/:id/attachments/:fileId, their teacher mirrors under /api/mobile/teacher/assignments/:id/attachments*, and the pupil's POST /api/mobile/student/assignments/:id/submission/files and DELETE /api/mobile/student/assignments/:id/submission/files/:fileId.

Request

multipart/form-data with one file part and an optional clientUploadId (uuid) field. Replaying the same clientUploadId from the same uploader returns the file already recorded, as long as it belongs to the same assignment (or the same pupil draft) — including when two requests race: the losing insert hits the clientUploadId unique index, and the loser then looks up and returns the winner's row instead of erroring. The uploaded filename is re-decoded as UTF-8 where the raw bytes support it, stripped of control and bidirectional-control characters, and capped at 255 code points before it is stored — never trusted verbatim. Each upload type stages bytes under its own private prefix (uploads/assignment-attachment/staging, uploads/assignment-submission/staging); the generic GET /api/upload/file/* route refuses any key under either prefix with 404 UPLOAD_FILE_KEY_INVALID, so an assignment file is only ever reachable through this module's own download endpoints.

Validation order

Checks run in this order and stop at the first failure (files/assignment-file-upload-attachment.ts:123-141, files/assignment-file-upload-submission.ts:148-166):

  1. The assignment is visible (and, for attachments, manageable) to the caller.
  2. (Submission only) allowFileUpload is on.
  3. The extension maps to a known file type.
  4. The file is not empty.
  5. The file is within assignment_settings.max_file_size_mb.
  6. The type is in assignment_settings.attachment_file_types (attachments) or the assignment's submission_file_types (submission files).
  7. The uploader has fewer than 20 files still pending a scan (checked once outside the transaction, then re-checked inside it under a per-uploader pg_advisory_xact_lock so two concurrent uploads from the same person cannot both slip past the outer check).
  8. The storage layer accepts the bytes and sniffs their MIME; the sniffed MIME must match what the declared file type requires (a fixed MIME for txt/csv) or the upload is purged and refused as a security-check failure, even though it already passed the extension/size/allowlist checks above.
  9. Inside the write transaction: the attachment count, or the pupil draft's file count and total bytes (summed over non-superseded attempts only — a superseded attempt's files stop counting the moment a newer attempt replaces it), is still within its limit.

Error Cases

HTTP StatusError CodeRoutesConditionConsumer ActionSource
404ASSIGNMENT_NOT_FOUNDall uploadsAssignment not visible/manageable by the callerTreat as missingassignment-file-upload-attachment.ts:65, :177; assignment-file-upload-submission.ts:74
409ASSIGNMENT_FILES_NOT_ALLOWEDpupil uploadThe assignment's allowFileUpload is offHide the upload control; submit text insteadassignment-file-upload-submission.ts:77-81
409ASSIGNMENT_VERSION_CONFLICTall uploadsThe clientUploadId was already used for a different assignment or draftGenerate a fresh clientUploadId and upload againassignment-file-upload-attachment.ts:116-120; assignment-file-upload-submission.ts:141-145
400ASSIGNMENT_FILE_TYPE_NOT_ALLOWEDall uploadsUnknown extension, or a type the attachment/submission allowlist does not includeConvert the file to an allowed typeassignment-file-upload.validation.ts:17-28, :51-61
400ASSIGNMENT_FILE_EMPTYall uploadsZero-byte filePick the file again; it was emptyassignment-file-upload.validation.ts:30-37
413ASSIGNMENT_FILE_TOO_LARGEall uploadsLarger than the school's max_file_size_mbCompress or split the fileassignment-file-upload.validation.ts:39-49
429ASSIGNMENT_TOO_MANY_PENDING_FILESall uploadsThe uploader already has 20 or more files awaiting a scanWait for earlier files to finish checking, then retryassignment-file-upload.validation.ts:63-74
400ASSIGNMENT_FILE_SECURITY_CHECK_FAILEDall uploadsThe storage layer's own validation refused the bytes (StorageValidationError), or storedMimeFor finds the sniffed MIME does not match what the declared type requires — for txt/csv specifically, this is any upload whose bytes sniff as a real binary signature instead of text (a PDF or image renamed .txt)Show the security-check message; do not retry the same fileassignment-file-upload.validation.ts:76-81, raised at assignment-file-upload-attachment.ts:153, :195-201 and assignment-file-upload-submission.ts:178
409ASSIGNMENT_ATTACHMENT_LIMIT_EXCEEDEDattachment uploadThe assignment already carries max_attachments attachmentsRemove an attachment firstassignment-file-upload-attachment.ts:192-196
409ASSIGNMENT_NOT_ACCEPTINGpupil uploadThe pupil can no longer edit a draft (window closed, row under review, class removed)Refresh the assignment; the upload control should be hiddenshared/assignment-draft.service.ts:112-116
409ASSIGNMENT_SUBMISSION_FILE_LIMIT_EXCEEDEDpupil uploadThe draft already holds the assignment's max_filesRemove a file firstassignment-file-upload-submission.ts:228-232
409ASSIGNMENT_SUBMISSION_STORAGE_LIMITpupil uploadThis pupil's submission files for the assignment would exceed 200 MiB in totalRemove or shrink filesassignment-file-upload-submission.ts:252-256
404ASSIGNMENT_FILE_NOT_FOUNDboth removalsNo such attachment, no current draft, or the file is not on itRefresh the file listassignment-file-upload-attachment.ts:287-291; assignment-file-upload-submission.ts:330-353

A file accepted here is only the start: its scan verdict arrives later. A file that is later rejected blocks publishing (ASSIGNMENT_HAS_REJECTED_ATTACHMENTS) or submitting (ASSIGNMENT_SUBMISSION_HAS_REJECTED_FILES) until it is removed, and its download answers 410 ASSIGNMENT_FILE_REJECTED.

8.7 PATCH /api/assignments/:id (and PATCH /api/mobile/teacher/assignments/:id)

Purpose

Edits an assignment. The request shape and the per-status restrictions are in §6.1a.

Auth and Permissions

  • Permission: Assignments_UPDATE (admin); none for the teacher mirror, which is scoped by the manage predicate.
  • Idempotency: version optimistic concurrency.

Error Cases

HTTP StatusError CodeConditionConsumer ActionSource
404ASSIGNMENT_NOT_FOUNDNot found, or outside the caller's manage scopeTreat as missingassignment-update.service.ts:102
409ASSIGNMENT_VERSION_CONFLICTversion mismatchReload and re-apply the editassignment-update.service.ts:103
409ASSIGNMENT_NOT_EDITABLEAssignment is archived or cancelledUnarchive first; a cancelled assignment cannot be editedassignment-update.service.ts:104-105
409ASSIGNMENT_SUBJECT_LOCKEDsubjectId/academicSessionId changed, or classSubjectIds sent, for a published/closed assignment, or addClassSubjectIds/removeClassSubjectIds sent for a draft/scheduled oneUse the field that matches the statusassignment-merge-validation.util.ts:197-203, raised at assignment-update.service.ts:134, :273, :301, :328
404SUBJECT_NOT_FOUND / ACADEMIC_SESSION_NOT_FOUNDUnknown or inactive subject, unknown sessionRe-pickassignment-update.service.ts:127-150
404ASSIGNMENT_CATEGORY_NOT_FOUNDUnknown category, or an inactive one that is not already the stored categoryRe-pick the categoryassignment-update.service.ts:168-173
409ASSIGNMENT_GRADING_LOCKEDisGraded/maxMarks/passMarks/marksRelease sent after any attempt has been reviewedLeave grading unchangedassignment-merge-validation.util.ts:205-211, raised at assignment-update.service.ts:186
409ASSIGNMENT_MAX_ATTEMPTS_BELOW_USEDNew attempt allowance is below a pupil's existing attempt_countChoose a higher limitassignment-merge-validation.util.ts:213-218, raised at assignment-update.service.ts:204
400ASSIGNMENT_DUE_IN_PASTChanged dueAt on a live assignment is less than 5 minutes awayPick a later due dateassignment-merge-validation.util.ts:87-93
400ASSIGNMENT_PUBLISH_AT_INVALIDChanged dueAt on a scheduled assignment leaves no room after publishAtMove the due date later or unscheduleassignment-merge-validation.util.ts:94-106
400ASSIGNMENT_FINAL_DEADLINE_INVALIDFinal deadline and late policy disagree, or the final deadline is not after the due dateFix the late-policy fields togetherassignment-merge-validation.util.ts:108-124
400ASSIGNMENT_SUBMISSION_SETTINGS_INVALIDNo submission method, maxFiles out of range, bad submissionFileTypes, or maxAttempts inconsistent with the resubmission policyFix the submission settings togetherassignment-merge-validation.util.ts:126-164
400ASSIGNMENT_GRADING_INVALIDmaxMarks/passMarks inconsistent with isGraded or out of rangeFix the grading fields togetherassignment-merge-validation.util.ts:166-187
404ASSIGNMENT_CLASS_NOT_TAUGHT(teacher) A new target class is not taught by the callerChoose a class you teachshared/assignment-access.service.ts:287 (assertClassSubjectsUsable)
422ASSIGNMENT_CLASS_SUBJECT_INVALIDDuplicate, invalid, or wrong-session/wrong-subject class-subject; or an addClassSubjectIds entry naming a class-subject that does not existFix the class selectionshared/assignment-access.service.ts:244, :294; AssignmentRosterServiceImpl.reAddClass
422ASSIGNMENT_AUDIENCE_TOO_LARGEMore than 60 classes in one class list, or adding a new target would take the live classes past 60Reduce the selectionshared/assignment-access.service.ts:250; AssignmentRosterServiceImpl.reAddClass
409ASSIGNMENT_NO_CLASSESremoveClassSubjectIds would leave no live classKeep at least one classassignment-update.service.ts:314-318
400ASSIGNMENT_LINK_INVALIDA link fails URL normalisationCorrect the linkshared/assignment-html.util.ts:126-158, called at assignment-update.service.ts:347

8.8 Remaining route families — error cases

The routes below reuse the DTOs in §6 and are listed with every error code they can return.

Route(s)HTTP StatusError CodeConditionConsumer ActionSource
GET :id, GET :id/options, GET :id/classes, GET :id/history (admin and teacher)404ASSIGNMENT_NOT_FOUNDNot found or not visibleTreat as missingassignment-detail.service.ts:56, assignment-class-stats-history.service.ts:37
DELETE :id (admin and teacher)404 / 409 / 409ASSIGNMENT_NOT_FOUND / ASSIGNMENT_VERSION_CONFLICT / ASSIGNMENT_DELETE_NOT_ALLOWEDMissing; stale version query param; status is not draft/scheduledTreat as missing / reload / close or cancel insteadassignment-write.service.ts:43, :50, :296-302
PUT :id/teacher (admin)404 / 409 / 409 / 404ASSIGNMENT_NOT_FOUND / ASSIGNMENT_VERSION_CONFLICT / ASSIGNMENT_NOT_EDITABLE / ASSIGNMENT_TEACHER_NOT_FOUNDMissing; stale version; archived or cancelled; teacherId not a live staff rowTreat as missing / reload / unarchive / pick another teacherassignment-write.service.ts:346, :366
POST :id/unschedule, close, reopen, archive, unarchive (admin and teacher)404 / 409 / 409 / 409ASSIGNMENT_NOT_FOUND / ASSIGNMENT_VERSION_CONFLICT / ASSIGNMENT_INVALID_TRANSITION / ASSIGNMENT_NOT_EDITABLEMissing; stale version; not in a status the action accepts; archived (close/reopen)Treat as missing / reload / refresh the action menu / unarchiveassignment-lifecycle.service.ts:37-61
POST :id/cancel (admin and teacher)403ASSIGNMENT_CANCEL_NOT_ALLOWED(teacher) assignment_settings.teachers_can_cancel is offAsk an administrator to cancelassignment-lifecycle.service.ts:333-345
POST :id/release-marks (admin and teacher)404 / 409 / 409ASSIGNMENT_NOT_FOUND / ASSIGNMENT_MARKS_RELEASE_NOT_APPLICABLE / ASSIGNMENT_VERSION_CONFLICTNot manageable; not graded or not marksRelease='manual'; stale versionTreat as missing / hide the action / reloadassignment-review-release.service.ts:60-78
GET :id/submissions (admin and teacher)404 / 400ASSIGNMENT_NOT_FOUND / PAGINATION_LIMIT_INVALIDNot visible; pagination=false requestedTreat as missing / always paginateassignment-roster-read.service.ts:88-97, :426-429
GET :id/submissions/:rowId (admin and teacher)404ASSIGNMENT_SUBMISSION_NOT_FOUNDRow not on this assignment or outside review scopeTreat as missingassignment-roster-read.service.ts:253-256
PUT /mobile/student/assignments/:id/submission404 / 409 / 409 / 400 / 400ASSIGNMENT_NOT_FOUND / ASSIGNMENT_NOT_ACCEPTING / ASSIGNMENT_DRAFT_VERSION_CONFLICT / ASSIGNMENT_TEXT_NOT_ALLOWED / ASSIGNMENT_TEXT_TOO_LONGNo pupil row; draft not editable; stale or missing draft version; text sent where text is off; text over the limitTreat as missing / refresh / reload the draft / hide the text box / shortenshared/assignment-draft.service.ts:60-116, assignment-submission.service.ts:113-153, :485-494
GET /mobile/student/assignments, GET .../guardian/children/:studentId/assignments400PAGINATION_LIMIT_INVALIDpagination=false requestedAlways paginateportal/student/student-assignments-portal.service.ts:130-136
GET /mobile/student/assignments/:id, guardian summary/list/detail404ASSIGNMENT_NOT_FOUNDNo live pupil for the session, studentId not a uuid or not the guardian's child, or assignment not visibleTreat as missingshared/assignment-audience.resolver.ts:52-110, portal/student/student-assignment-detail.service.ts:66, :83, portal/guardian/guardian-assignments-portal.service.ts:301, :318
GET /assignment-categories—none beyond validation———
POST /assignment-categories, PATCH /assignment-categories/:id409 / 409ASSIGNMENT_CATEGORY_NAME_TAKEN / ASSIGNMENT_CATEGORY_CODE_TAKENAn active category already has this name (case-insensitive) / this codeChoose another name or code, or reactivate the existing oneassignment-categories.service.ts:55-67
PATCH /assignment-categories/:id404 / 409ASSIGNMENT_CATEGORY_NOT_FOUND / ASSIGNMENT_VERSION_CONFLICTUnknown id; stale versionRefresh / reloadassignment-categories.service.ts:145-155, :176-180
DELETE /assignment-categories/:id404 / 409 / 409ASSIGNMENT_CATEGORY_NOT_FOUND / ASSIGNMENT_VERSION_CONFLICT / ASSIGNMENT_CATEGORY_IN_USEUnknown id; stale version; referenced by an assignmentRefresh / reload / deactivate it insteadassignment-categories.service.ts:201-227
PATCH /assignment-settings409ASSIGNMENT_VERSION_CONFLICTStale versionReload and re-applyassignment-settings.service.ts:93-98
PUT /assignment-notification-rules/:event404 / 400 / 409ASSIGNMENT_NOTIFICATION_RULE_INVALID / ASSIGNMENT_NOTIFICATION_RULE_INVALID / ASSIGNMENT_VERSION_CONFLICT:event is not one of the ten events (or its row is missing); notifyTeacher: true on an event other than submitted/overdue; stale versionUse a listed event / leave the teacher toggle off / reloadassignment-notification-rules.service.ts:66-97
GET /assignment-transfer-reviews400PAGINATION_LIMIT_INVALIDpagination=false requestedAlways paginateassignment-transfer-reviews.service.ts:187-192
POST /assignment-transfer-reviews/:id/resolve404ASSIGNMENT_TRANSFER_REVIEW_NOT_FOUNDNo such review, or it is already resolvedRefresh the listAssignmentRosterServiceImpl.resolveDecision (shared/assignment-roster.service.ts), assignment-transfer-reviews.service.ts:259-262
same409ASSIGNMENT_TRANSFER_NO_LONGER_APPLICABLEThe pupil's enrolment changed since the review opened: they are back in a live target class, or no longer have an active enrolment. The stale review is deleted (and a withdrawn pupil's row marked not_applicable) and committed before the 409 is returnedRefresh the list; nothing further is needed for this pupilAssignmentRosterServiceImpl.resolveDecision
same409ASSIGNMENT_TRANSFER_ALREADY_SUBMITTEDresolution: "not_applicable" for a pupil whose row is past in_progressResolve keep insteadAssignmentRosterServiceImpl.resolveDecision
POST /assignment-files/:fileId/rescan404 / 409ASSIGNMENT_FILE_NOT_FOUND / ASSIGNMENT_FILE_NOT_RESCANNABLEUnknown file; scan_status is not failed (checked before and again inside the write)Refresh / only failed scans can be retriedfiles/admin/assignment-file-rescan.service.ts:44-57, :92-97

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence (representative: submit)

9.3 Error Branch (representative: lifecycle action)

EndpointPagination TypeDefault SizeMax SizeSort FieldsFiltersResult Cap
GET /api/assignmentspage/size (pagination=false allowed)20100status, dueAt, createdAt, titlesubjectId, categoryId, teacherId, academicSessionId, status/displayStatus, search1000 unpaginated
GET /mobile/teacher/assignmentspage/size20100samesame (implicitly scoped to the teacher)1000 unpaginated
GET /mobile/student/assignmentspage/size (pagination=false refused with 400 PAGINATION_LIMIT_INVALID)20100dueAtbucket—
GET /mobile/guardian/children/:studentId/assignmentspage/size (same service; pagination=false refused)20100dueAtbucket—
GET :id/submissionspage/size (pagination=false refused with 400 PAGINATION_LIMIT_INVALID)20100name, rollNumber, lastSubmittedAt, status (falls back to name for anything unrecognised)filter (roster filter), classId—
GET /assignment-transfer-reviewspage/size (pagination=false refused with 400 PAGINATION_LIMIT_INVALID)20100———
GET :id/historypage/size20100— (chronological)—1000 unpaginated
GET /assignment-categoriespage/size20100sortOrder ascending (lower numbers first) by default; sort=name honours orderincludeInactive1000 unpaginated
GET /assignment-notification-rulesnone (all 10 rows)—————

All list/roster/history endpoints use the shared PaginationUtil (common/utils/pagination.util.ts) — offset pagination, {count, page, size} metadata envelope, no hand-rolled offset math anywhere in this module. The admin/teacher assignment list orders by the requested sort column in the requested direction with NULLS LAST, then by assignments.id as a tiebreaker — so two rows with an equal or null value on the sort column, or a page boundary landing between them, always resolve in a stable, repeatable order.

11. Caching, Jobs, and External Integrations

IntegrationUsed?DetailsSource
Redis cacheNoNo CacheKeyUtil usage anywhere in this module.grep of apps/api/src/modules/assignments/
BullMQYesQueueName.ASSIGNMENTS (4 sweep jobs, cron-produced) and QueueName.ASSIGNMENT_FILE_SCAN (1 job, produced on upload via the outbox and by the recovery sweep). See the backend doc §9 for the full contract.packages/jobs/src/index.ts, workers/*
External APINoFile storage goes through the first-party @skoolsewa/storage package, not a third-party API called directly from this module.files/assignment-file-upload.service.ts

12. Error Code Reference

Every ASSIGNMENT_* code in apps/api/src/common/types/error-codes.ts (lines 1494-1562), with the HTTP status it is returned with, the routes that can return it, and what a consumer should do. Paths in the Source column are relative to apps/api/src/modules/assignments/. "Admin and teacher" means the admin route and its /api/mobile/teacher mirror.

Three codes are never returned over HTTP — ASSIGNMENT_FILE_PURGE_FAILED and ASSIGNMENT_FILE_SCAN_WORKER_FAILED are written to the server log only, and ASSIGNMENT_SETTINGS_INVALID is defined but not referenced anywhere. They are listed at the end of this table and described in the backend doc §13.

Generic codes owned by other modules also appear on these routes: 400 validation errors from the global ValidationPipe, 400 PAGINATION_LIMIT_INVALID on the lists that refuse pagination=false (§10), 404 SUBJECT_NOT_FOUND and 404 ACADEMIC_SESSION_NOT_FOUND on create and update, 401/403 from the guards, and 429 from IpThrottlerGuard on throttled routes.

Two more shared codes cover every write under a locked academic session or a graduated pupil, on every table this module owns — assignments, assignment_classes, assignment_students, assignment_submission_attempts, assignment_links, and assignment_files for insert and delete (not update, so a pending scan can still finish after a lock):

Error CodeHTTP StatusRoutesConditionConsumer ActionSource
ACADEMIC_SESSION_LOCKED409Every write route on an assignment, its classes, roster, submission attempts, links and file insert/deleteA Postgres trigger refuses the write because the assignment's academic session has locked_at setUnlock the year to make the correction, or wait for a new onepackages/db/src/migrations/0038_year_lock_records.sql, surfaced by AllExceptionsFilter's GUARD_MESSAGES
STUDENT_GRADUATED_READ_ONLY403Pupil portal writes (saveDraft, file upload/removal, submit)The pupil's graduate access state is grace or endedNothing can be submitted for a graduated pupil; read-only access continues until the window endsapps/api/src/modules/people/shared/graduate-access.query.ts:82-95, called from student-assignment-submission-portal.controller.ts
Error CodeHTTP StatusRoutesConditionConsumer ActionSource
ASSIGNMENT_ALREADY_COMPLETED409Pupil submitThe pupil's row is already completedShow the result; no further submissionsubmissions/core/assignment-submission.service.ts:313
ASSIGNMENT_ATTACHMENT_LIMIT_EXCEEDED409Attachment upload (admin and teacher)The assignment already has max_attachments attachmentsRemove an attachment firstfiles/assignment-file-upload-attachment.ts:195
ASSIGNMENT_ATTEMPT_NOT_REVIEWABLE409Review save, complete, return (admin and teacher)The attempt is not the latest non-draft attempt or is in a status the action does not accept; or return on resubmissionPolicy='none'Reload the pupil record and act on the current attemptsubmissions/core/assignment-review.service.ts:240, :449
ASSIGNMENT_AUDIENCE_TOO_LARGE422Create, update, preview, publish (admin and teacher)More than 60 target classes, or more than 3000 live pupils at publishNarrow the class selectionshared/assignment-access.service.ts:250, authoring/core/assignment-publish-guards.util.ts:69, :131, AssignmentRosterServiceImpl.reAddClass
ASSIGNMENT_CANCEL_NOT_ALLOWED403Teacher cancelteachers_can_cancel is offAsk an administrator to cancelauthoring/core/assignment-lifecycle.service.ts:342
ASSIGNMENT_CATEGORY_CODE_TAKEN409Category create, updateAnother active category has this codeChoose another code or reactivate the existing categorysettings/categories/assignment-categories.service.ts:65
ASSIGNMENT_CATEGORY_IN_USE409Category deleteAn assignment still references the categoryDeactivate it instead of deletingsettings/categories/assignment-categories.service.ts:226
ASSIGNMENT_CATEGORY_NAME_TAKEN409Category create, updateAnother active category has this name (case-insensitive)Choose another namesettings/categories/assignment-categories.service.ts:59
ASSIGNMENT_CATEGORY_NOT_FOUND404Assignment create, update; category update, deleteUnknown category, or an inactive one being newly chosenRe-pick from the active listauthoring/core/assignment-write.service.ts:152, authoring/core/assignment-update.service.ts:172, settings/categories/assignment-categories.service.ts:147, :203
ASSIGNMENT_CLASS_NOT_TAUGHT404Teacher create, update, preview, publishA target class-subject is not taught by the calling teacherChoose a class you teach, or ask an administratorshared/assignment-access.service.ts:287, authoring/core/assignment-publish-guards.util.ts:80
ASSIGNMENT_CLASS_SUBJECT_INVALID422Create, update, preview (admin and teacher)Duplicate id; class-subject inactive, in another session, teaching another subject, or nonexistentFix the class selectionshared/assignment-access.service.ts:244, :294, AssignmentRosterServiceImpl.reAddClass
ASSIGNMENT_DELETE_NOT_ALLOWED409Delete (admin and teacher)Status is not draft/scheduledClose or cancel insteadauthoring/core/assignment-write.service.ts:301
ASSIGNMENT_DRAFT_VERSION_CONFLICT409Pupil save draft, submit, submission-file uploadDraft version stale or missing when a draft already exists; a draft could not be createdReload the assignment, then save or submit with the current draft versionshared/assignment-draft.service.ts:187, submissions/core/assignment-submission.service.ts:118, :124, :152, :249, :255, :399
ASSIGNMENT_DUE_IN_PAST400Publish now; updateDue date less than 5 minutes awayPick a later due dateauthoring/core/assignment-lifecycle.service.ts:65, authoring/core/assignment-merge-validation.util.ts:47
ASSIGNMENT_FEEDBACK_REQUIRED400Review save, complete, returnReturn with feedback that carries no text; feedback over 40,000 HTML or 20,000 text characters after sanitisingAdd or shorten the feedbacksubmissions/core/assignment-review.service.ts:273, :279, :286, :519, :526
ASSIGNMENT_FILE_EMPTY400All uploadsZero-byte filePick the file againfiles/assignment-file-upload.validation.ts:34
ASSIGNMENT_FILE_NOT_FOUND404All downloads; attachment and submission-file removal; rescanUnknown file, out of scope, or (guardian) a submission fileRefresh; do not retryfiles/assignment-file-download.service.ts:35, files/assignment-file-upload-attachment.ts:290, files/assignment-file-upload-submission.ts:333, :352, files/admin/assignment-file-rescan.service.ts:48
ASSIGNMENT_FILE_NOT_READY409All downloadsscan_status='pending'Retry after a short delayfiles/assignment-file-download.service.ts:100
ASSIGNMENT_FILE_NOT_RESCANNABLE409Rescanscan_status is not failedOnly failed scans can be rescannedfiles/admin/assignment-file-rescan.service.ts:54, :96
ASSIGNMENT_FILE_REJECTED410All downloadsFile rejected by the scan, removed, or purgedUpload a replacement; this file never becomes availablefiles/assignment-file-download.service.ts:118
ASSIGNMENT_FILE_SCAN_FAILED409All downloadsscan_status='failed'Show "file unavailable"; an administrator can rescanfiles/assignment-file-download.service.ts:106
ASSIGNMENT_FILE_SECURITY_CHECK_FAILED400All uploadsThe storage layer's validation refused the bytes, or (any type, including txt/csv) the sniffed content does not match the required MIME for the declared typeShow the security-check message; use a different filefiles/assignment-file-upload.validation.ts:79, files/assignment-file-upload.validation.ts:81-92 (storedMimeFor)
ASSIGNMENT_FILES_NOT_ALLOWED409Pupil submission-file upload, submitThe assignment's allowFileUpload is offHide file upload; submit textfiles/assignment-file-upload-submission.ts:80, submissions/core/assignment-submission.service.ts:306
ASSIGNMENT_FILE_TOO_LARGE413All uploadsOver assignment_settings.max_file_size_mbCompress or split the filefiles/assignment-file-upload.validation.ts:46
ASSIGNMENT_FILE_TYPE_NOT_ALLOWED400All uploadsUnknown extension, or type not in the attachment/submission allowlistConvert to an allowed typefiles/assignment-file-upload.validation.ts:24, :58
ASSIGNMENT_FINAL_DEADLINE_INVALID400UpdateFinal deadline and late policy disagree, or final deadline not after the due dateFix the late-policy fields togetherauthoring/core/assignment-merge-validation.util.ts:61
ASSIGNMENT_GRADING_INVALID400UpdatemaxMarks/passMarks inconsistent with isGraded or out of rangeFix the grading fields togetherauthoring/core/assignment-merge-validation.util.ts:75
ASSIGNMENT_GRADING_LOCKED409UpdateGrading fields sent after any attempt was reviewedLeave grading unchangedauthoring/core/assignment-merge-validation.util.ts:209
ASSIGNMENT_HAS_REJECTED_ATTACHMENTS409Publish (admin and teacher)A non-removed attachment is rejectedRemove or replace it, then publishauthoring/core/assignment-publish-guards.util.ts:110
ASSIGNMENT_INVALID_TRANSITION409Publish, unschedule, close, reopen, cancel, archiveThe current status does not accept the actionRefresh the action menuauthoring/core/assignment-lifecycle.service.ts:53
ASSIGNMENT_LINK_INVALID400Create, updateA link is unparseable, not https:, carries credentials, or exceeds 2048 charactersCorrect the linkshared/assignment-html.util.ts:134, :141, :147, :153
ASSIGNMENT_MARKS_INVALID400Review save, completeMarks missing on a graded assignment, outside [0, maxMarks], or present on an ungraded oneCorrect the markssubmissions/core/assignment-review.service.ts:471, :479, :485
ASSIGNMENT_MARKS_RELEASE_NOT_APPLICABLE409Release marks (admin and teacher)Assignment not graded, or marksRelease is not manualHide the actionsubmissions/core/assignment-review-release.service.ts:70
ASSIGNMENT_MAX_ATTEMPTS_BELOW_USED409UpdateNew attempt allowance below a pupil's existing attempt countChoose a higher limitauthoring/core/assignment-merge-validation.util.ts:216
ASSIGNMENT_NO_CLASSES422 / 409Publish (422); update removing every live class (409)No live target classAdd or keep at least one classauthoring/core/assignment-publish-guards.util.ts:63, authoring/core/assignment-update.service.ts:322
ASSIGNMENT_NOT_ACCEPTING409Pupil save draft, submission-file upload, submitThe submission window is closed, the row is under review, or the class was removedRefresh; the assignment no longer takes workshared/assignment-draft.service.ts:115, submissions/core/assignment-submission.service.ts:92, :319, :357
ASSIGNMENT_NOT_EDITABLE409Update, reassign teacher, publish, close, reopen, review save/complete/returnArchived or cancelled (update/reassign); archived (lifecycle); not published/closed or archived (review; return needs published)Unarchive, or leave the assignment as it isauthoring/core/assignment-lifecycle.service.ts:59, authoring/core/assignment-merge-validation.util.ts:193, authoring/core/assignment-write.service.ts:389, submissions/core/assignment-review.service.ts:390
ASSIGNMENT_NOT_FOUND404Every route taking an assignment id; pupil and guardian routes when no live pupil/child resolvesMissing, out of scope, or no identity matchTreat as missing; do not retryshared/assignment-audience.resolver.ts:56, :85, :109; authoring/core/assignment-lifecycle.service.ts:40; authoring/core/assignment-write.service.ts:43; and the other throw sites listed in §8
ASSIGNMENT_NOTIFICATION_RULE_INVALID404 / 400Notification rule update:event not one of the ten events or row missing (404); notifyTeacher on an event other than submitted/overdue (400)Use a listed event; leave the teacher toggle offsettings/admin/assignment-notification-rules.service.ts:67, :76, :89
ASSIGNMENT_PUBLISH_AT_INVALID400Schedule; update of a scheduled assignment's due datepublishAt missing, under a minute away, or leaving no room before the due datePick another timeauthoring/core/assignment-lifecycle.service.ts:71, authoring/core/assignment-merge-validation.util.ts:54
ASSIGNMENT_RESUBMISSION_NOT_ALLOWED409Pupil submit; returnAttempt budget exhausted (submit); 50 extra attempts already granted on a non-unlimited policy (return)Hide resubmit / stop returning this pupil's worksubmissions/core/assignment-submission.service.ts:356, submissions/core/assignment-review.service.ts:249
ASSIGNMENT_RESUBMIT_DUE_IN_PAST400ReturnresubmitDueAt not in the futurePick a future deadlinesubmissions/core/assignment-review.service.ts:292
ASSIGNMENT_RESUBMIT_DUE_REQUIRED400ReturnNo resubmitDueAt and the assignment's own window is closedSupply a resubmission deadlinesubmissions/core/assignment-review.service.ts:299
ASSIGNMENT_SUBJECT_LOCKED409UpdateSubject/session/target-set field not allowed for the current statusUse the field that matches the status (§6.1a)authoring/core/assignment-merge-validation.util.ts:201
ASSIGNMENT_SUBMISSION_EMPTY400Pupil submitNo text and no non-rejected fileAdd text or a filesubmissions/core/assignment-submission.service.ts:288
ASSIGNMENT_SUBMISSION_FILE_LIMIT_EXCEEDED409Pupil submission-file upload, submitDraft holds max_files already, or more than max_files at submitRemove a filefiles/assignment-file-upload-submission.ts:231, submissions/core/assignment-submission.service.ts:300
ASSIGNMENT_SUBMISSION_HAS_REJECTED_FILES409Pupil submitA draft file is rejectedRemove the rejected filesubmissions/core/assignment-submission.service.ts:294
ASSIGNMENT_SUBMISSION_NOT_FOUND404Submission record; review save, complete, returnRow not on the assignment or out of review scope; attempt not bound to the rowTreat as missingsubmissions/core/assignment-roster-read.service.ts:255, submissions/core/assignment-review.service.ts:408, :425
ASSIGNMENT_SUBMISSION_SETTINGS_INVALID400UpdateNo submission method, maxFiles out of range, bad file types, or maxAttempts inconsistent with policyFix the submission settings togetherauthoring/core/assignment-merge-validation.util.ts:68
ASSIGNMENT_SUBMISSION_STORAGE_LIMIT409Pupil submission-file uploadPupil's submission files for this assignment would exceed 200 MiBRemove or shrink filesfiles/assignment-file-upload-submission.ts:255
ASSIGNMENT_TEACHER_NOT_FOUND404Admin create, reassign teacherteacherId is not a live staff rowPick another teacherauthoring/core/assignment-write.service.ts:169, :346
ASSIGNMENT_TEACHER_REQUIRED422Publish (admin and teacher)require_teacher is on and no teacher is assignedAssign a teacherauthoring/core/assignment-publish-guards.util.ts:93
ASSIGNMENT_TEXT_NOT_ALLOWED400Pupil save draft, submitText sent while allowTextResponse is offHide the text boxsubmissions/core/assignment-submission.service.ts:135, :266
ASSIGNMENT_TEXT_TOO_LONG400Pupil save draft, submitSanitised text over the school limit or module maximumShorten the responsesubmissions/core/assignment-submission.service.ts:487, :493
ASSIGNMENT_TOO_MANY_PENDING_FILES429All uploadsThe uploader has 20 or more files awaiting scanWait for scans to finish, then retryfiles/assignment-file-upload.validation.ts:69
ASSIGNMENT_TRANSFER_ALREADY_SUBMITTED409Resolve transfer reviewnot_applicable for a pupil past in_progressResolve keepAssignmentRosterServiceImpl.resolveDecision
ASSIGNMENT_TRANSFER_NO_LONGER_APPLICABLE409Resolve transfer reviewPupil's enrolment changed since the review opened; the review is deleted before the responseRefresh the listAssignmentRosterServiceImpl.resolveDecision
ASSIGNMENT_TRANSFER_REVIEW_NOT_FOUND404Resolve transfer reviewUnknown or already resolvedRefresh the listAssignmentRosterServiceImpl.resolveDecision, settings/transfers/assignment-transfer-reviews.service.ts:261
ASSIGNMENT_VERSION_CONFLICT409Every write carrying a version; replayed clientUploadId/clientSubmissionId that no longer matchesStale concurrency token or conflicting replayReload, re-apply, and send the new versionauthoring/core/assignment-lifecycle.service.ts:46, authoring/core/assignment-update.service.ts:60, authoring/core/assignment-write.service.ts:50, settings/admin/assignment-settings.service.ts:97, settings/admin/assignment-notification-rules.service.ts:96, submissions/core/assignment-review.service.ts:84, and the other sites in §8
ASSIGNMENT_FILE_PURGE_FAILEDnot returned— (log only)Deleting purged bytes from storage failed after commitNone; see backend doc §13files/assignment-file-purge.service.ts:42
ASSIGNMENT_FILE_SCAN_WORKER_FAILEDnot returned— (log only)A scan job failed terminally in the workerNone; see backend doc §13workers/assignment-file-scan.processor.ts:96
ASSIGNMENT_SETTINGS_INVALIDnot returned— (defined, unused)No code throws itNoneapps/api/src/common/types/error-codes.ts:1541

13. Mandatory Deep API Documentation Pack

13.1 Route-by-Route Completeness Matrix

RouteController MethodService MethodGuardsPermissionErrors (primary)Documented?
GET /api/assignments/optionsAssignmentsController.optionsAssignmentOptionsService.catalogueJWT, RoleAssignments_CREATE—Yes
POST /api/assignments/previewAssignmentsController.previewAssignmentPreviewService.previewJWT, RoleAssignments_CREATEvalidationYes
GET /api/assignmentsAssignmentsController.findAllAssignmentListService.listJWT, RoleAssignments_READ—Yes
POST /api/assignmentsAssignmentsController.createAssignmentWriteService.createJWT, RoleAssignments_CREATEASSIGNMENT_CLASS_*, ASSIGNMENT_AUDIENCE_TOO_LARGEYes (§8.1)
GET /api/assignments/:idAssignmentsController.findOneAssignmentDetailService.detailJWT, RoleAssignments_READASSIGNMENT_NOT_FOUNDYes
GET /api/assignments/:id/optionsAssignmentsController.optionsForAssignmentAssignmentOptionsService.catalogueJWT, RoleAssignments_UPDATEASSIGNMENT_NOT_FOUNDYes
PATCH /api/assignments/:idAssignmentsController.updateAssignmentUpdateService.updateJWT, RoleAssignments_UPDATEsee §8.7 (ASSIGNMENT_SUBJECT_LOCKED, ASSIGNMENT_GRADING_LOCKED, ASSIGNMENT_MAX_ATTEMPTS_BELOW_USED, ASSIGNMENT_FINAL_DEADLINE_INVALID, ASSIGNMENT_SUBMISSION_SETTINGS_INVALID, ASSIGNMENT_GRADING_INVALID, ASSIGNMENT_LINK_INVALID, …)Yes (§8.7)
DELETE /api/assignments/:idAssignmentsController.removeAssignmentWriteService.removeJWT, RoleAssignments_DELETEASSIGNMENT_NOT_FOUND, ASSIGNMENT_VERSION_CONFLICT, ASSIGNMENT_DELETE_NOT_ALLOWEDYes (§8.8)
PUT /api/assignments/:id/teacherAssignmentsController.reassignTeacherAssignmentWriteService.reassignTeacherJWT, RoleAssignments_UPDATEASSIGNMENT_TEACHER_NOT_FOUND, ASSIGNMENT_VERSION_CONFLICTYes
GET /api/assignments/:id/classesAssignmentsController.classesAssignmentClassStatsHistoryService.classStatsJWT, RoleAssignments_READASSIGNMENT_NOT_FOUNDYes
GET /api/assignments/:id/historyAssignmentsController.historyAssignmentClassStatsHistoryService.historyJWT, RoleAssignments_READASSIGNMENT_NOT_FOUNDYes
POST /api/assignments/:id/publishAssignmentLifecycleController.publishAssignmentLifecycleService.publishJWT, RoleAssignmentPublish_CREATEsee §8.2Yes (§8.2)
POST /api/assignments/:id/unscheduleAssignmentLifecycleController.unscheduleAssignmentLifecycleService.unscheduleJWT, RoleAssignmentPublish_CREATEASSIGNMENT_INVALID_TRANSITION, version conflictYes
POST /api/assignments/:id/closeAssignmentLifecycleController.closeAssignmentLifecycleService.closeJWT, RoleAssignmentPublish_UPDATEASSIGNMENT_INVALID_TRANSITION, ASSIGNMENT_NOT_EDITABLEYes
POST /api/assignments/:id/reopenAssignmentLifecycleController.reopenAssignmentLifecycleService.reopenJWT, RoleAssignmentPublish_UPDATEsameYes
POST /api/assignments/:id/cancelAssignmentLifecycleController.cancelAssignmentLifecycleService.cancelJWT, RoleAssignmentCancel_UPDATEASSIGNMENT_CANCEL_NOT_ALLOWED, ASSIGNMENT_INVALID_TRANSITIONYes
POST /api/assignments/:id/archiveAssignmentLifecycleController.archiveAssignmentLifecycleService.archiveJWT, RoleAssignments_DELETEASSIGNMENT_INVALID_TRANSITIONYes
POST /api/assignments/:id/unarchiveAssignmentLifecycleController.unarchiveAssignmentLifecycleService.unarchiveJWT, RoleAssignments_RESTOREversion conflictYes
POST /api/assignments/:id/attachmentsAssignmentAttachmentsController.uploadAssignmentFileUploadService.uploadAttachmentJWT, Role, IPAssignments_UPDATEsee §8.6 (ASSIGNMENT_FILE_TYPE_NOT_ALLOWED, ASSIGNMENT_FILE_EMPTY, ASSIGNMENT_FILE_TOO_LARGE, ASSIGNMENT_TOO_MANY_PENDING_FILES, ASSIGNMENT_FILE_SECURITY_CHECK_FAILED, ASSIGNMENT_ATTACHMENT_LIMIT_EXCEEDED, …)Yes (§8.6)
DELETE /api/assignments/:id/attachments/:fileIdAssignmentAttachmentsController.removeAssignmentFileUploadService.removeAttachmentJWT, Role, IPAssignments_UPDATEASSIGNMENT_FILE_NOT_FOUNDYes
GET /api/assignments/:id/submissionsAssignmentSubmissionsController.listAssignmentRosterReadService.listRosterJWT, RoleAssignmentSubmissions_READASSIGNMENT_NOT_FOUNDYes
GET /api/assignments/:id/submissions/:rowIdAssignmentSubmissionsController.recordAssignmentRosterReadService.getRecordJWT, RoleAssignmentSubmissions_READASSIGNMENT_SUBMISSION_NOT_FOUNDYes
PUT /api/assignments/:id/submissions/:rowId/reviewAssignmentSubmissionsController.saveReviewAssignmentReviewService.saveReviewJWT, RoleAssignmentReview_UPDATEsee §8.4Yes (§8.4)
POST /api/assignments/:id/submissions/:rowId/completeAssignmentSubmissionsController.completeReviewAssignmentReviewService.completeReviewJWT, RoleAssignmentReview_UPDATEsee §8.4Yes (§8.4)
POST /api/assignments/:id/submissions/:rowId/returnAssignmentSubmissionsController.returnSubmissionAssignmentReviewService.returnSubmissionJWT, RoleAssignmentReview_UPDATEsee §8.4Yes (§8.4)
POST /api/assignments/:id/release-marksAssignmentSubmissionsController.releaseMarksAssignmentReviewOperations.releaseMarksJWT, RoleAssignmentReview_UPDATEASSIGNMENT_MARKS_RELEASE_NOT_APPLICABLEYes
GET /api/assignment-categoriesAssignmentCategoriesController.listAssignmentCategoriesService.listJWT, RoleAssignmentSettings_READ—Yes
POST /api/assignment-categoriesAssignmentCategoriesController.createAssignmentCategoriesService.createJWT, RoleAssignmentSettings_CREATEASSIGNMENT_CATEGORY_NAME_TAKEN, ASSIGNMENT_CATEGORY_CODE_TAKENYes (§8.8)
PATCH /api/assignment-categories/:idAssignmentCategoriesController.updateAssignmentCategoriesService.updateJWT, RoleAssignmentSettings_UPDATEASSIGNMENT_CATEGORY_NAME_TAKEN, ASSIGNMENT_CATEGORY_CODE_TAKEN, ASSIGNMENT_CATEGORY_NOT_FOUND, ASSIGNMENT_VERSION_CONFLICTYes (§8.8)
DELETE /api/assignment-categories/:idAssignmentCategoriesController.removeAssignmentCategoriesService.removeJWT, RoleAssignmentSettings_DELETEASSIGNMENT_CATEGORY_NOT_FOUND, ASSIGNMENT_VERSION_CONFLICT, ASSIGNMENT_CATEGORY_IN_USEYes (§8.8)
GET /api/assignment-settingsAssignmentSettingsController.getAssignmentSettingsService.getJWT, RoleAssignmentSettings_READ—Yes
PATCH /api/assignment-settingsAssignmentSettingsController.updateAssignmentSettingsService.updateJWT, RoleAssignmentSettings_UPDATEASSIGNMENT_VERSION_CONFLICT (field validation is DTO-only)Yes (§8.8)
GET /api/assignment-notification-rulesAssignmentNotificationRulesController.listAssignmentNotificationRulesService.listJWT, RoleAssignmentNotificationSettings_READ—Yes
PUT /api/assignment-notification-rules/:eventAssignmentNotificationRulesController.updateAssignmentNotificationRulesService.updateJWT, RoleAssignmentNotificationSettings_UPDATEASSIGNMENT_NOTIFICATION_RULE_INVALID (404 unknown event, 400 teacher not applicable), ASSIGNMENT_VERSION_CONFLICTYes (§8.8)
GET /api/assignment-transfer-reviewsAssignmentTransferReviewsController.listAssignmentTransferReviewsService.listJWT, RoleAssignments_READPAGINATION_LIMIT_INVALIDYes
POST /api/assignment-transfer-reviews/:id/resolveAssignmentTransferReviewsController.resolveAssignmentTransferReviewsService.resolveJWT, RoleAssignments_UPDATEASSIGNMENT_TRANSFER_REVIEW_NOT_FOUND, ASSIGNMENT_TRANSFER_NO_LONGER_APPLICABLE, ASSIGNMENT_TRANSFER_ALREADY_SUBMITTEDYes (§8.8)
GET /api/assignment-statistics/dashboardAssignmentStatsController.dashboardAssignmentAdminStatsService.summaryJWT, RoleAssignmentStats_READ404 ACADEMIC_SESSION_NOT_FOUNDYes
GET /api/assignment-statistics/analyticsAssignmentStatsController.analyticsAssignmentAdminStatsService.analyticsJWT, RoleAssignmentStats_READ404 ACADEMIC_SESSION_NOT_FOUNDYes
GET /api/assignment-files/:fileIdAssignmentFilesController.download (admin)AssignmentFileDownloadService.openJWT, Role, IPAssignments_READsee §8.5Yes (§8.5)
POST /api/assignment-files/:fileId/rescanAssignmentFilesController.rescan (admin)AssignmentFileRescanService.rescanJWT, RoleAssignmentSettings_UPDATEASSIGNMENT_FILE_NOT_FOUND, ASSIGNMENT_FILE_NOT_RESCANNABLEYes (§8.8)
All 25 /api/mobile/teacher/assignments* routesTeacherAssignmentsPortalController, TeacherAssignmentActionsPortalController, TeacherAssignmentReviewPortalControllerSame core services as the admin mirrorsJWT, Role (+IP on upload)none — audienceSame error sets as the admin mirrors, plus 403 (non-teacher session) and 404 (out-of-teach-scope row)Yes
GET /api/mobile/teacher/assignment-files/:fileIdTeacherAssignmentFilesPortalController.downloadAssignmentFileDownloadService.openJWT, Role, IPnonesee §8.5Yes
GET /mobile/student/assignments/dashboardStudentAssignmentsPortalController.dashboardStudentAssignmentsPortalService.dashboardJWT, Rolenone—Yes
GET /mobile/student/assignmentsStudentAssignmentsPortalController.listStudentAssignmentsPortalService.listJWT, Rolenone—Yes
GET /mobile/student/assignments/:idStudentAssignmentsPortalController.detailStudentAssignmentDetailService.detailJWT, RolenoneASSIGNMENT_NOT_FOUNDYes
PUT /mobile/student/assignments/:id/submissionStudentAssignmentSubmissionPortalController.saveDraftAssignmentSubmissionService.saveDraftJWT, Role, IPnoneASSIGNMENT_NOT_FOUND, ASSIGNMENT_NOT_ACCEPTING, ASSIGNMENT_DRAFT_VERSION_CONFLICT, ASSIGNMENT_TEXT_NOT_ALLOWED, ASSIGNMENT_TEXT_TOO_LONGYes (§8.8)
POST .../submission/filesStudentAssignmentSubmissionPortalController.uploadFileAssignmentFileUploadService.uploadSubmissionFileJWT, Role, IPnonesee §8.6 (adds ASSIGNMENT_FILES_NOT_ALLOWED, ASSIGNMENT_NOT_ACCEPTING, ASSIGNMENT_SUBMISSION_FILE_LIMIT_EXCEEDED, ASSIGNMENT_SUBMISSION_STORAGE_LIMIT)Yes (§8.6)
DELETE .../submission/files/:fileIdStudentAssignmentSubmissionPortalController.removeFileAssignmentFileUploadService.removeSubmissionFileJWT, Role, IPnoneASSIGNMENT_FILE_NOT_FOUNDYes
POST .../submission/submitStudentAssignmentSubmissionPortalController.submitAssignmentSubmissionService.submitJWT, Role, IPnonesee §8.3Yes (§8.3)
GET /mobile/student/assignment-files/:fileIdStudentAssignmentFilesPortalController.downloadAssignmentFileDownloadService.openJWT, Role, IPnonesee §8.5Yes
GET .../assignments/summaryGuardianAssignmentsPortalController.summaryGuardianAssignmentsPortalService.summaryJWT, Rolenone403/404 on child resolutionYes
GET .../assignmentsGuardianAssignmentsPortalController.listStudentAssignmentsPortalService.listJWT, RolenonesameYes
GET .../assignments/:idGuardianAssignmentsPortalController.detailGuardianAssignmentsPortalService.detailJWT, Rolenonesame + ASSIGNMENT_NOT_FOUNDYes
GET .../assignment-files/:fileIdGuardianAssignmentFilesPortalController.downloadAssignmentFileDownloadService.openJWT, Role, IPnoneASSIGNMENT_FILE_NOT_FOUND (incl. submission-file attempts)Yes

13.2 Request/Response Exhaustiveness

Covered per-endpoint in §8 for the representative routes (create, publish, submit, review actions, download) and via the shared DTO tables in §6 for every other route, since the remaining lifecycle/CRUD/settings routes reuse those exact shapes. Every route's success envelope follows the repo-standard {success, message, data} (or {success, message, data, meta} for paginated lists) via ResponseDto/ApiPaginatedResponseDto.

13.3 API Diagram Pack

Covered in §9. Additional cache/async-flow diagrams are unnecessary here: this module has no cache layer (§11), and the one async flow (file scan) is diagrammed in the backend doc §7.4.

13.4 Consumer Integration Notes

ConsumerRequired KnowledgeFailure HandlingContract Stability
Admin panel (web frontend)Permission codes per route (§4/§5); version tokens must be round-tripped exactly; grant flags on AssignmentDetailDto are UI hints only, not an authorization decisionDisplay the errorCode from every non-2xx response; a 409 ASSIGNMENT_VERSION_CONFLICT should trigger a reload-and-retry promptStable
Mobile/portal (same panel, /api/mobile prefix)No permission codes on portal routes — a 403 there means "wrong audience", not "missing grant"; guest/anonymous access does not existSame envelope handling; a 404 on a row the UI just listed usually means it left scope between list and detail fetch (soft-deleted, class removed)Stable
Admin panelDestructive mutations (DELETE, cancel, archive) all require the current versionConfirm before sending; surface the exact reason code from validationStable
QAFixtures live in apps/api/src/modules/assignments/__tests__/fixtures/Reproduce via the integration specs, which seed real rows through the schemaStable
Internal serviceNone — no other backend module calls into this one's HTTP surface; cross-module reads go through Drizzle table imports directly (e.g. class_subjects, students)N/AStable

13.5 API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
Pagination styleOffset (page/size), shared PaginationUtilCursor paginationConsistency with every other list endpoint in this repo; assignment lists are not high-volume enough to need cursor semanticsDeep pages are O(n)MAX_PAGE_SIZE=100 cap
Auth modeJWT always; audience assertion instead of permission codes on portal routesGrant portal audiences real Assignment* permission codesPortal codes would need per-row scoping logic duplicated in the permission layer; the audience+row-scope split keeps one control surfaceA portal handler could theoretically be added without registering it in NO_PERMISSION_ADMIN_HANDLERSMirrored CI check (route-permissions.spec.ts) fails the build if missed
Error shapeExisting {success, message, data} envelope + errorCodeA problem-details (RFC 7807) shapeConsistency with the rest of the APINone—
Response DTO shapegrant flags embedded in AssignmentDetailDtoA separate /permissions sub-resourceOne round trip instead of two for the detail screenA stale grant could be cached client-side past a permission changegrant is recomputed on every detail fetch, never cached server-side

13.6 API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
Adding a new assignment_status valueAdmin panel, mobile portal (every status-branching UI)New CHECK constraint value, new lifecycle transition logicNone to existing rowsYes (migration to widen the enum/CHECK)Ship backend + both consumer repos together; the status enum is embedded in three CHECK constraints
Adding a new notification eventNotification settings screenNew row in assignment_notification_rules, new template kindMigration seeds the default rowYesBackward compatible — an unrecognised event is simply absent from the settings list until seeded
Renaming a DTO fieldAny consumer reading that fieldDTO + mapper changeNoneNoRequires coordinated frontend deploy; this repo has no API versioning segment to stage it behind

14. Zero-Omission API Checklist

  • Every controller route is documented (§4 surface map + §13.1 completeness matrix — all 76 in-scope routes).
  • Every parent route prefix and runtime URL is documented (§2, §4).
  • Every DTO field, nested field, enum, default, and validator is documented (§6, §7).
  • Every response field is documented for the representative DTOs (§6.2–6.7); list/roster DTOs reuse the same field vocabulary.
  • Every auth, guard, permission, and guest-identity branch is documented (§5).
  • Every success, validation, auth, permission, not-found, conflict, and rate-limit branch is documented (§8, §13.1).
  • Every database read/write, queue job, and notification path is documented (§8 side effects, backend doc §9).
  • Representative routes have minimal/full/success/failure examples (§8.1–8.5); the remaining routes share those exact shapes per §6.
  • Route, sequence, and error diagrams are included (§9).
  • Tradeoffs and compatibility risk are documented (§13.5, §13.6).
  • This doc links to backend and features/flows.

15. Integration Checklist

  • Every route from controllers is documented.
  • Every DTO field is documented.
  • Every enum value is documented (§7).
  • Every response envelope is documented.
  • Every error code is documented (all 62 ASSIGNMENT_* codes in §12, with per-route lists in §8; the registry is apps/api/src/common/types/error-codes.ts:1494-1562).
  • Every auth guard and permission is documented.
  • Every cache key (none), queue job, and external call is documented (§11).
  • Every diagram matches the current code.
  • This doc links to backend and features/flows.

See Also

On this page

Assignments - API Reference1. Documentation Evidence2. Module Summary3. Concepts and Terminology4. API Surface Map5. Auth, Identity, and Permissions6. DTO and Model Reference6.1 AssignmentCreateBaseDto (shared by AdminCreateAssignmentDto and TeacherCreateAssignmentDto)6.1a AdminUpdateAssignmentDto / TeacherUpdateAssignmentDto6.2 AssignmentDetailDto (response — admin/teacher detail)6.3 AssignmentFileDto6.4 AssignmentLinkInputDto6.5 SaveDraftDto / SubmitAssignmentDto (pupil submission)6.6 SaveReviewDto / CompleteReviewDto / ReturnSubmissionDto6.7 AssignmentAudiencePreviewRequestDto / AssignmentAudiencePreviewDto6.8 ListAssignmentsQueryDto / ListAssignmentSubmissionsQueryDto / ListStudentAssignmentsQueryDto6.9 AssignmentTransferReviewDto / ResolveTransferReviewDto7. Enum Reference8. Endpoint Reference8.1 POST /api/assignments (and POST /api/mobile/teacher/assignments)PurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.2 POST /api/assignments/:id/publish (and teacher mirror)PurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.3 PUT /api/mobile/student/assignments/:id/submission/submitPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.4 PUT /api/assignments/:id/submissions/:rowId/review (and teacher mirror), POST .../complete, POST .../returnPurposeSource EvidenceAuth and PermissionsRequest (all three share attemptId + version)ResponseSide EffectsError CasesEdge Cases8.5 File download — GET .../assignment-files/:fileId (all four audiences)PurposeSource EvidenceAuth and PermissionsResponseError CasesEdge Cases8.6 File upload and removal — attachments and submission filesRequestValidation orderError Cases8.7 PATCH /api/assignments/:id (and PATCH /api/mobile/teacher/assignments/:id)PurposeAuth and PermissionsError Cases8.8 Remaining route families — error cases9. Flow Diagrams9.1 Route Ownership9.2 Request Sequence (representative: submit)9.3 Error Branch (representative: lifecycle action)10. Pagination, Sorting, Filtering, and Search11. Caching, Jobs, and External Integrations12. Error Code Reference13. Mandatory Deep API Documentation Pack13.1 Route-by-Route Completeness Matrix13.2 Request/Response Exhaustiveness13.3 API Diagram Pack13.4 Consumer Integration Notes13.5 API Tradeoffs and Rationale13.6 API Change Impact14. Zero-Omission API Checklist15. Integration ChecklistSee Also