Skoolsewa - School Management System Docs
Developer ResourcesTimetable

Timetable API Reference

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

Timetable - API Reference

Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: Admin routes for the Timeslot Manager and Timetable Builder, and the mobile "my schedule" and "today" read routes owned by the teacher, student, and guardian portals.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/timetable/timeslots/class-timeslots.controller.ts, apps/api/src/modules/timetable/entries/class-timetable.controller.ts, apps/api/src/modules/mobile/{teacher,student,guardian}/*-schedule-portal.controller.tsRoutes, methods, guards, decorators, permissions, status codes.
DTOsapps/api/src/modules/timetable/dto/timeslot.dto.ts, apps/api/src/modules/timetable/dto/timetable.dto.ts, apps/api/src/modules/mobile/shared/portal-schedule.dto.tsRequest, query, response, validation, defaults.
Servicesapps/api/src/modules/timetable/timeslots/*.ts, apps/api/src/modules/timetable/entries/*.ts, apps/api/src/modules/mobile/shared/portal-schedule.query.tsBehavior, side effects, response mapping, errors.
Schemapackages/db/src/schema/school/timetable.ts, packages/db/src/migrations/0019_timetable_module.sqlIDs, enums, persisted fields, constraints.
Error registryapps/api/src/common/types/error-codes.ts (TIMESLOT_*, TIMETABLE_*)Every error code and its raising condition.
Permissionspackages/db/src/authorization/permission-catalog.tsTimetables, TimetablePublish, Schedule module grants.

2. Module Summary

FieldValue
Module nameTimetable
Module slugtimetable
Primary actorsadmin (Timeslot Manager, Timetable Builder), teacher, student, guardian (read-only "my schedule")
API surfacesadmin, mobile
Base route prefixes/api/classes/:classPublicId/timeslots, /api/classes/:classPublicId/timetable, /api/mobile/teacher/schedule, /api/mobile/teacher/schedule/today, /api/mobile/student/schedule, /api/mobile/student/schedule/today, /api/mobile/guardian/children/:id/schedule, /api/mobile/guardian/children/:id/schedule/today
Auth modelJwtAuthGuard + RoleGuard on every route. Admin routes additionally require @Permissions(...); the six portal routes carry no @Permissions() and assert their audience in code instead.
PersistencePostgreSQL only — class_timeslots, class_timetables, class_timetable_entries, class_timetable_archives. No Redis, BullMQ, or realtime dependency.
Runtime source of truthThe database rows and their constraints — no cache layer exists to disagree with them.
Sibling docsBackend, features/flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
TimeslotOne row of class_timeslots: a day/time-range/type for one class. Structure, not content.packages/db/src/schema/school/timetable.tsClassTimeslotsController, every entry route
Timetable (header)One row of class_timetables: a versioned draft or published state of a class's schedule.sameClassTimetableController
EntryOne row of class_timetable_entries: a lesson (subject + optional teacher/room) placed into one timeslot of one timetable version.sameDraft/publish routes
slotTypeperiod | break | lunch | activity | study_hall | free. Only period may hold an entry.timeslot.dto.tsAll timeslot and entry DTOs
dayOfWeek0–6, 0 = Sunday, matching Postgres EXTRACT(DOW). The admin surface only ever writes 0–5 in practice but the schema and API accept 0–6.timeslot.dto.tsAll timeslot and entry DTOs
startMinute / endMinuteInteger minutes from local midnight, half-open range [start, end). endMinute is at most 1440.timeslot.dto.tsAll timeslot and entry DTOs
versionServer-owned optimistic-concurrency token on class_timeslots and class_timetable_entries. Echo it back on update; it is a bigint in the database, serialized as a plain number.schema, DTOsTimeslot update, entry upsert
versionNoThe integer that identifies one archived or published version of a class's whole timetable — distinct from the row-level version token above.timetable.dto.tsDraft restore, version listing
Draft / PublishedThe two values of class_timetables.status. At most one of each per class, enforced by partial unique indexes.schemaClassTimetableDto.status
Stale referenceA published entry whose teacher was soft-deleted, or whose room/subject assignment was deactivated, after the entry was created. No constraint can detect this at write time.class-timetable-read.service.tsClassTimetableDto.staleReferences, publish

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminGET/api/classes/:classPublicId/timeslotsAdminJwtAuthGuard, RoleGuardTimetables_READClassTimeslotsControllerList every day's timeslots for a class.
AdminPOST/api/classes/:classPublicId/timeslotsAdminsameTimetables_CREATEClassTimeslotsControllerCreate one timeslot.
AdminPATCH/api/classes/:classPublicId/timeslots/:timeslotIdAdminsameTimetables_UPDATE, TimetablePublish_UPDATEClassTimeslotsControllerUpdate a timeslot's day/time/type/label.
AdminDELETE/api/classes/:classPublicId/timeslots/:timeslotIdAdminsameTimetables_DELETE, TimetablePublish_UPDATEClassTimeslotsControllerHard-delete an unoccupied timeslot.
AdminPOST/api/classes/:classPublicId/timeslots/copyAdminsameTimetables_CREATE, Timetables_DELETE, TimetablePublish_UPDATEClassTimeslotsControllerCopy one day's slots onto one or more other days.
AdminGET/api/classes/:classPublicId/timetableAdminsameTimetables_READClassTimetableControllerRead the live grid (draft, published, or an explicit version).
AdminGET/api/classes/:classPublicId/timetable/versionsAdminsameTimetables_READClassTimetableControllerList every version — the currently live one first, then archived versions.
AdminGET/api/classes/:classPublicId/timetable/versions/:versionNoAdminsameTimetables_READClassTimetableControllerRead one version by its number, live or archived.
AdminPOST/api/classes/:classPublicId/timetable/draftAdminsameTimetables_UPDATEClassTimetableControllerCreate or restore a draft.
AdminDELETE/api/classes/:classPublicId/timetable/draftAdminsameTimetables_DELETEClassTimetableControllerDiscard the current draft.
AdminPUT/api/classes/:classPublicId/timetable/draft/entries/:timeslotIdAdminsameTimetables_UPDATEClassTimetableControllerPlace or update a lesson in one slot of the draft.
AdminDELETE/api/classes/:classPublicId/timetable/draft/entries/:timeslotIdAdminsameTimetables_UPDATEClassTimetableControllerClear a slot in the draft.
AdminGET/api/classes/:classPublicId/timetable/slots/:timeslotId/availabilityAdminsameTimetables_READClassTimetableControllerWhich teachers/rooms are free for a slot.
AdminPOST/api/classes/:classPublicId/timetable/publishAdminsameTimetablePublish_UPDATEClassTimetableControllerPublish the current draft.
AdminPOST/api/classes/:classPublicId/timetable/unpublishAdminsameTimetablePublish_UPDATEClassTimetableControllerUnpublish the current timetable.
MobileGET/api/mobile/teacher/scheduleTeacherJwtAuthGuard, RoleGuardNone — audience asserted in codeTeacherSchedulePortalControllerThe caller's own weekly teaching schedule.
MobileGET/api/mobile/teacher/schedule/todayTeachersameNone — audience asserted in codeTeacherTodayPortalControllerThe caller's own periods for today — class periods, exam periods for the class-subjects they teach, and invigilation duties — with live state.
MobileGET/api/mobile/student/scheduleStudentsameNone — audience asserted in codeStudentSchedulePortalControllerThe caller's own class's weekly schedule.
MobileGET/api/mobile/student/schedule/todayStudentsameNone — audience asserted in codeStudentSchedulePortalControllerThe caller's own class's periods for today, with live state.
MobileGET/api/mobile/guardian/children/:id/scheduleGuardiansameNone — audience asserted in codeGuardianSchedulePortalControllerOne ward's weekly class schedule.
MobileGET/api/mobile/guardian/children/:id/schedule/todayGuardiansameNone — audience asserted in codeGuardianSchedulePortalControllerOne ward's periods for today, with live state.

Route prefixes for the two admin controllers come from their own @Controller() decorators (classes/:classPublicId/timeslots, classes/:classPublicId/timetable); TimetableModule adds no further prefix. The four portal controllers share their existing mobile-module @Controller("teacher"|"student"|"guardian") prefixes rather than being declared inside TimetableModule — a class declared in two modules would register its routes twice.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
Admin (read)JwtAuthGuard, RoleGuard, @Permissions("Timetables_READ")req.user.idTimetables_READNoGranted to no seeded role — a school must create a role deliberately to expose staff-wide scheduling data.
Admin (create/update/delete on timeslots that could touch a published grid)same + multi-code @Permissions(...)req.user.idTimetables_UPDATE/_DELETE/_CREATE and TimetablePublish_UPDATENoRoleGuard requires every listed code. TimetablePublish_UPDATE is also in SUPERADMIN_ONLY_MODULES, so an admin role never receives it automatically even though the catalog's default sweep grants everything else.
Admin (publish/unpublish)samereq.user.idTimetablePublish_UPDATENoSuperadmin-only in practice for the reason above.
Mobile (teacher/student/guardian schedule)JwtAuthGuard, RoleGuard, no @Permissions()req.user (AuthUser) resolved via @CurrentAdmin()None — Schedule_READ gates the screen, not the API routeNoEach controller asserts its own audience (assertTeacherStaffRow, assertPortalAudience) and, for the guardian route, additionally resolves ward ownership (GuardianPortalService.resolveOwnChildId) before any schedule row is read. Because these routes carry no @Permissions(), they must be listed in NO_PERMISSION_ADMIN_HANDLERS and in test/structure/route-permissions.spec.ts, or they 403 for every caller including superadmin.

Schedule is the one permission module granted to staff, teacher, student, and guardian roles — the guardian role's first grant of any kind, made safe here because the guardian route takes an explicit ward parameter and asserts ownership of it, unlike an earlier withdrawn attempt to route guardians through a pupil-audience endpoint.

6. DTO and Model Reference

6.1 TimeslotDto (response)

FieldTypeRequiredDefaultValidationExampleSource
idstring (uuid)Yes——"018f..."timeslot.dto.ts
dayOfWeeknumberYes—0–6, 0 = Sunday1timeslot.dto.ts
startMinutenumberYes—0–1440480timeslot.dto.ts
endMinutenumberYes—0–1440540timeslot.dto.ts
slotTypestring enumYes—period | break | lunch | activity | study_hall | free"period"timeslot.dto.ts
labelstring | nullYes (nullable)null—"Tiffin"timeslot.dto.ts
versionnumberYes—server-generated3timeslot.dto.ts

6.2 ClassTimeslotsDto (response, GET .../timeslots)

FieldTypeRequiredDefaultValidationExampleSource
classPublicIdstringYes———timeslot.dto.ts
daysArray<{ dayOfWeek: number; slots: TimeslotDto[] }>Yes—Always exactly 7 entries, dayOfWeek 0–6, in order—timeslot.dto.ts

There is no aggregate/summary endpoint; the admin panel derives its per-day tab counts from this one response.

6.3 CreateTimeslotDto (request, POST .../timeslots)

FieldTypeRequiredDefaultValidationExampleSource
dayOfWeeknumberYes—@IsInt, @Min(0), @Max(6)1timeslot.dto.ts
startMinutenumberYes—@IsInt, @Min(0), @Max(1440)480timeslot.dto.ts
endMinutenumberYes—@IsInt, @Min(0), @Max(1440)540timeslot.dto.ts
slotTypestring enumYes—@IsIn(TIMESLOT_TYPES)"period"timeslot.dto.ts
labelstring | nullNo—@IsString, @MaxLength(64); trimmed server-side before write"Tiffin"timeslot.dto.ts

6.4 UpdateTimeslotDto (request, PATCH .../timeslots/:timeslotId)

Every field is optional except version, which is required.

FieldTypeRequiredDefaultValidationExampleSource
dayOfWeeknumberNounchanged@IsInt, @Min(0), @Max(6)2timeslot.dto.ts
startMinutenumberNounchanged@IsInt, @Min(0), @Max(1440)500timeslot.dto.ts
endMinutenumberNounchanged@IsInt, @Min(0), @Max(1440)560timeslot.dto.ts
slotTypestring enumNounchanged@IsIn(TIMESLOT_TYPES)"break"timeslot.dto.ts
labelstring | nullNounchanged@IsString, @MaxLength(64)nulltimeslot.dto.ts
versionnumberYes—@IsInt, @Min(0)3timeslot.dto.ts

A silently optional concurrency token is a token that is silently not checked, so version is required here even though every other field is optional; omitting it is a 400 from the global validation pipe.

6.5 CopyTimeslotsDto (request, POST .../timeslots/copy)

FieldTypeRequiredDefaultValidationExampleSource
fromDayOfWeeknumberYes—@IsInt, @Min(0), @Max(6)1timeslot.dto.ts
toDaysOfWeeknumber[]Yes—@ArrayNotEmpty, @ArrayMaxSize(7), each @IsInt, @Min(0), @Max(6); deduplicated server-side; must not contain fromDayOfWeek[2, 3, 4]timeslot.dto.ts
replacebooleanYes—@IsBooleantruetimeslot.dto.ts
fromClassPublicIdstring (uuid)Nothe target class@IsUUID—timeslot.dto.ts

fromClassPublicId defaulting to the target class is what makes copying between classes (a session rollover from one year's class to the next) possible in the same endpoint used for same-class day duplication.

6.6 TimetableEntryDto (response)

FieldTypeRequiredDefaultValidationExampleSource
timeslotIdstring (uuid)Yes———timetable.dto.ts
classSubjectIdstring (uuid)Yes———timetable.dto.ts
subjectNamestringYes——"Mathematics"timetable.dto.ts
subjectCodestringYes——"MATH"timetable.dto.ts
teacherIdstring | null (uuid)Yes (nullable)nulla staff uuid — staff has no public_id—timetable.dto.ts
teacherNamestring | nullYes (nullable)null—"Ram Sharma"timetable.dto.ts
roomIdnumber | nullYes (nullable)nullrooms.id, the integer, never a public id—timetable.dto.ts
roomNamestring | nullYes (nullable)null—"Room 204"timetable.dto.ts
notesstring | nullYes (nullable)null——timetable.dto.ts
versionnumberYes—server-generated3timetable.dto.ts

6.7 StaleReferenceDto (response, nested in ClassTimetableDto)

FieldTypeRequiredDefaultValidationExampleSource
timeslotIdstring (uuid)Yes———timetable.dto.ts
kindstring enumYes—teacher_deleted | room_inactive | class_subject_inactive"teacher_deleted"timetable.dto.ts
referenceNamestring | nullYes (nullable)nullThe last-known name of the decayed reference."Ram Sharma"timetable.dto.ts

6.8 ClassTimetableDto (response, GET .../timetable)

FieldTypeRequiredDefaultValidationExampleSource
classPublicIdstringYes———timetable.dto.ts
statusstring enumYes—draft | published — which version this response actually is"draft"timetable.dto.ts
versionNonumber | nullYes (nullable)nullnull only for a brand-new class with neither draft nor published version—timetable.dto.ts
publishedAtstring | null (ISO 8601)Yes (nullable)null——timetable.dto.ts
hasDraftbooleanYes—So the panel can offer "edit" without a second call—timetable.dto.ts
hasPublishedbooleanYes—So the panel can offer "view published" without a second call—timetable.dto.ts
slotsTimeslotDto[]Yes[]Ordered by dayOfWeek, then startMinute—timetable.dto.ts
entriesTimetableEntryDto[]Yes[]Keyed to slots by timeslotId—timetable.dto.ts
staleReferencesStaleReferenceDto[]Yes[]Populated only when status === "published"; always [] for a draft response—timetable.dto.ts

6.9 ReadTimetableQueryDto (query, GET .../timetable)

FieldTypeRequiredDefaultValidationExampleSource
statusstring enumNoresolved server-side (draft, else published, else empty)@IsIn(["draft","published"])"published"timetable.dto.ts

6.10 PutTimetableEntryDto (request, PUT .../draft/entries/:timeslotId)

One body serves both a drag-and-drop (sending classSubjectId alone) and the follow-up lesson-details dialog (sending the rest). A field omitted (undefined) is left untouched on an existing row; only an explicit value or explicit null changes it.

FieldTypeRequiredDefaultValidationExampleSource
classSubjectIdstring (uuid)Yes—@IsUUID—timetable.dto.ts
teacherIdstring | null (uuid)Noleft unchanged if omitted@IsUUID; a staff uuid, not a public id—timetable.dto.ts
roomIdnumber | nullNoleft unchanged if omitted@IsInt; rooms.id—timetable.dto.ts
notesstring | nullNoleft unchanged if omitted@IsString, @MaxLength(500); trimmed server-side—timetable.dto.ts
versionnumberNo—@IsInt, @Min(0); enforced only when supplied, since the same endpoint both creates and updates3timetable.dto.ts

6.11 CreateDraftDto (request, POST .../timetable/draft)

FieldTypeRequiredDefaultValidationExampleSource
fromVersionNonumberNoforks the published version, or creates an empty draft@IsInt, @Min(1)4timetable.dto.ts

6.12 SkippedEntryDto (response, nested in CreateDraftResultDto)

FieldTypeRequiredDefaultValidationExampleSource
timeslotIdstring | nullYes (nullable)———timetable.dto.ts
subjectNamestring | nullYes (nullable)———timetable.dto.ts
reasonstring enumYes—timeslot_missing | timeslot_moved | class_subject_missing"timeslot_moved"timetable.dto.ts

6.13 CreateDraftResultDto (response, POST .../timetable/draft)

FieldTypeRequiredDefaultValidationExampleSource
versionNonumberYes——5timetable.dto.ts
skippedSkippedEntryDto[]Yes[] — non-empty only when fromVersionNo was given and some entries could not resolve——timetable.dto.ts

6.14 TimetableVersionSummaryDto (response, GET .../timetable/versions)

FieldTypeRequiredDefaultValidationExampleSource
versionNonumberYes——4timetable.dto.ts
publishedAtstring (ISO 8601)Yes———timetable.dto.ts
archivedAtstring | nullYes (nullable)—null for the row that is currently live—timetable.dto.ts
publishedByNamestring | nullYes (nullable)null—"Admin User"timetable.dto.ts
entryCountnumberYes—The live row's count is selectEntryRows(...).length; an archived row's is the stored snapshot's entries.length28timetable.dto.ts
isLivebooleanYes—true for exactly one row, or none when nothing is publishedtruetimetable.dto.ts

The live version — the currently published header, not yet archived — is always first when present, ahead of every archived row.

6.15 ArchivedTimetableDto / ArchivedTimetableEntryDto (response, GET .../timetable/versions/:versionNo)

Enumerated from the stored snapshot, never the raw jsonb — this route is Timetables_READ-gated so notes is included here, but internal ids (timeslotId, classSubjectId, teacherId) that exist only to make restore possible are not part of this read shape. versionNo may name either an archived version or the currently live one; the live version's entries are read fresh and mapped through the same projection, so the shape returned is identical either way.

FieldTypeRequiredDefaultValidationExampleSource
versionNonumberYes———timetable.dto.ts
publishedAtstring (ISO 8601)Yes———timetable.dto.ts
archivedAtstring | nullYes (nullable)—null when versionNo names the currently live version—timetable.dto.ts
publishedByNamestring | nullYes (nullable)null——timetable.dto.ts
entriesArchivedTimetableEntryDto[]Yes—See below—timetable.dto.ts

ArchivedTimetableEntryDto: dayOfWeek, startMinute, endMinute (all number, required), slotType (string enum, required), label (string \| null), subjectName (string \| null), teacherName (string \| null), roomName (string \| null), notes (string \| null).

6.16 SlotAvailabilityDto (response, GET .../timetable/slots/:timeslotId/availability)

FieldTypeRequiredDefaultValidationExampleSource
timeslotIdstring (uuid)Yes———timetable.dto.ts
freeTeachersArray<{ id: string; name: string }>Yes[]Active, teaching-designation staff not already committed to a published, overlapping entry—timetable.dto.ts
freeRoomsArray<{ id: number; name: string }>Yes[]Active rooms not already committed to a published, overlapping entry—timetable.dto.ts

6.17 ConflictDetailsDto / ConflictsWithDto (response, embedded in 409 error bodies)

The body both TIMETABLE_TEACHER_CONFLICT and TIMETABLE_ROOM_CONFLICT carry, in the details field of the error response. Obtained by a second query run inside the same transaction, before rollback, so the conflicting row is provably not yet moved.

ConflictDetailsDto: dayOfWeek, startMinute, endMinute (number, required), timeslotId (string, required), conflictsWith (ConflictsWithDto, required).

ConflictsWithDto: classPublicId (string), className (string), subjectName (string), teacherId (string \| null), teacherName (string \| null), roomId (number \| null), roomName (string \| null).

6.18 Portal DTOs

PortalScheduleEntryDto (response, student/guardian schedule entries):

FieldTypeRequiredDefaultValidationExampleSource
dayOfWeeknumberYes—0–61portal-schedule.dto.ts
startMinutenumberYes——480portal-schedule.dto.ts
endMinutenumberYes——540portal-schedule.dto.ts
slotTypestringYes—period | break | lunch | activity | study_hall | free"period"portal-schedule.dto.ts
labelstring | nullYes (nullable)null—"Tiffin"portal-schedule.dto.ts
subjectNamestring | nullYes (nullable)nullOnly set for period slots"Mathematics"portal-schedule.dto.ts
subjectCodestring | nullYes (nullable)null—"MATH"portal-schedule.dto.ts
teacherNamestring | nullYes (nullable)nullA name only — no staff id, employee code, or employment status is ever included"Ram Sharma"portal-schedule.dto.ts
roomNamestring | nullYes (nullable)null—"Room 204"portal-schedule.dto.ts

TeacherScheduleEntryDto extends PortalScheduleEntryDto with four additional fields:

FieldTypeRequiredDefaultValidationExampleSource
classNamestring | nullYes (nullable)nullNeeded only for the teacher view, since one teacher's week spans multiple classes"Grade 5 A"portal-schedule.dto.ts
classPublicIdstring (uuid)Yes—The class's public_id"0198f2c4-6b1e-7c3a-9d52-3a4f1b8e7a10"portal-schedule.dto.ts
subjectIdstring (uuid)Yes—The subject's public_id"0198f2c4-6b1e-7c3a-9d52-3a4f1b8e7a11"portal-schedule.dto.ts
classSubjectIdstring (uuid)Yes—The class_subjects row's id, which is its public identifier"0198f2c4-6b1e-7c3a-9d52-3a4f1b8e7a12"portal-schedule.dto.ts

PortalScheduleDto / TeacherScheduleDto (the envelopes):

FieldTypeRequiredDefaultValidationExampleSource
academicSessionNamestring | nullYes (nullable)nullnull when no session is marked current"2026-27"portal-schedule.dto.ts
entriesPortalScheduleEntryDto[] or TeacherScheduleEntryDto[]Yes[]An empty array is a normal, successful response — never treated as an error—portal-schedule.dto.ts

PupilTodayScheduleDto (response of both schedule/today routes; mobile/shared/portal-today.dto.ts):

FieldTypeRequiredNotes
serverTimestringYesThe instant the response was computed, ISO 8601 UTC. Period states are evaluated against it.
timezonestringYesThe school's IANA timezone, for example "Asia/Kathmandu".
datestringYesThe school-local date, YYYY-MM-DD.
bsDate{ year, month, day} | nullYes (nullable)The Bikram Sambat date (1-based month). null outside the converter's range.
dayOfWeeknumberYes0–6, 0 = Sunday, of the school-local date.
academicSessionNamestring | nullYes (nullable)The current session's name; null when no session is current.
calendarPupilTodayCalendarDtoYesWhy the day looks the way it does; see below.
periodsPupilTodayPeriodDto[]YesThe class's slots for today in start order. Empty unless calendar.status is operating.
currentPeriodIdstring | nullYes (nullable)id of the first period whose state is in_progress.
nextPeriodIdstring | nullYes (nullable)id of the first period whose state is upcoming.
nextTransitionAtstring | nullYes (nullable)The earliest slot start or end later than serverTime, ISO 8601 UTC, so a client can refresh at that moment. null when no boundary remains.
summary{ total, finished, remaining }YesTeaching periods only (slotType = "period"). remaining = total - finished, so it includes an in-progress period. All 0 when periods is empty.

PupilTodayCalendarDto:

FieldTypeNotes
statusenumOne of operating, non_operating, not_set, outside_session, no_session, no_class. See the decision order in §8.23.
classificationstring | nullThe calendar day's classification (for example school_day). null for no_session, outside_session and not_set.
eventNamestring | nullThe calendar day's event name, when it has one.
isOperatingDayboolean | nullWhether attendance is expected. null for no_session, outside_session and not_set.

PupilTodayPeriodDto:

FieldTypeNotes
idstring<startMinute>-<endMinute>-<index>, where index is the slot's position in the day. Stable within a day; the same slot has the same id in both audiences.
state"finished" | "in_progress" | "upcoming"finished once serverTime >= endsAt; in_progress once serverTime >= startsAt; otherwise upcoming.
startMinute, endMinutenumberMinutes from school-local midnight.
startsAt, endsAtstringThe slot boundaries as ISO 8601 UTC instants, built from the school-local date and minute.
slotTypestringperiod, break, lunch, activity, study_hall or free. Only period carries a subject.
label, subjectName, subjectCode, teacherName, roomNamestring | nullAs in PortalScheduleEntryDto; teacherName is a name only.

Fields deliberately absent from every portal DTO, and why: notes (operator free text — "Cover for Mrs Sharma, medical leave" is a staff medical disclosure with a column name that gives no hint of that), teacherId, roomId, timetableId, timeslotId, version, publishedByUserId (internal identity/concurrency plumbing a read-only screen has no use for), and any teacher field beyond a name (no employeeCode, no employmentStatus — the field-level control that keeps this endpoint from telling every parent in a class whether a teacher has been terminated).

7. Enum Reference

EnumValueMeaningRuntime EffectSource
timeslot_type / TIMESLOT_TYPESperiodA teaching period.The only value an entry may occupy.packages/db/src/schema/school/timetable.ts, timeslot.dto.ts
breakA short non-teaching gap.Renders from the timeslot row itself; cannot hold an entry.same
lunchThe midday break.samesame
activityA non-subject activity period (assembly, sport, club).samesame
study_hallUnsupervised or self-study time.samesame
freeAn unstructured free period.samesame
timetable_status / TIMETABLE_STATUSESdraftThe editable working copy. Unconstrained by the two exclusion constraints.Read/write via the draft routes; never visible to portal reads.same, timetable.dto.ts
publishedThe live, school-facing version. Constrained by both exclusion constraints; the only status portal reads ever see.Read via GET .../timetable?status=published and the three weekly portal routes.same
StaleReferenceDto.kindteacher_deletedThe entry's teacher was soft-deleted after assignment.Surfaced in staleReferences; blocks publish if found on the draft.timetable.dto.ts
room_inactiveThe entry's room was deactivated (rooms.is_active = false) after assignment.samesame
class_subject_inactiveThe entry's subject assignment was deactivated (class_subjects.is_active = false).samesame
SkippedEntryDto.reasontimeslot_missingThe archived entry's timeslotId no longer resolves for this class.The entry is omitted from the restored draft.timetable.dto.ts
timeslot_movedThe timeslot still exists but its day/time/type has since changed.samesame
class_subject_missingThe archived classSubjectId no longer resolves for this class.samesame

8. Endpoint Reference

8.1 GET /api/classes/:classPublicId/timeslots

Purpose

Returns every day of the week, 0–6, in one response — including days with no slots at all. There is no aggregate/summary endpoint; the admin panel derives its per-day tab counts directly from this response's days array.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/timetable/timeslots/class-timeslots.controller.ts
Serviceapps/api/src/modules/timetable/timeslots/class-timeslots-read.service.ts
Schemapackages/db/src/schema/school/timetable.ts
Testsapps/api/src/modules/timetable/__tests__/timeslots.integration.spec.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: Timetables_READ.
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: N/A (read).

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYesclassPublicId — the class's uuid.
QueryNoNone.
BodyNo—

Response

{
  "success": true,
  "message": "Timeslots fetched.",
  "data": {
    "classPublicId": "018f2f3e-...-...",
    "days": [
      { "dayOfWeek": 0, "slots": [] },
      {
        "dayOfWeek": 1,
        "slots": [
          {
            "id": "018f2f40-...",
            "dayOfWeek": 1,
            "startMinute": 480,
            "endMinute": 540,
            "slotType": "period",
            "label": null,
            "version": 0
          }
        ]
      }
    ]
  }
}

Side Effects

None — read-only.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404CLASS_NOT_FOUNDclassPublicId does not resolve.The class does not exist.class-timeslots-read.service.ts

Edge Cases

  • A class with no timeslots at all returns all 7 days with empty slots arrays — never a 404.
  • Slots are ordered dayOfWeek then startMinute within each day.

Example Requests

curl -X GET "$API_URL/api/classes/018f2f3e-.../timeslots" \
  -H "Authorization: Bearer TOKEN"

8.2 POST /api/classes/:classPublicId/timeslots

Purpose

Creates one timeslot for a class. Used by the Timeslot Manager to build up a class's daily structure one period, break, or lunch at a time.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/timetable/timeslots/class-timeslots.controller.ts
DTOapps/api/src/modules/timetable/dto/timeslot.dto.ts (CreateTimeslotDto)
Serviceapps/api/src/modules/timetable/timeslots/class-timeslots-write.service.ts
Schemapackages/db/src/schema/school/timetable.ts
Testsapps/api/src/modules/timetable/__tests__/timeslots.integration.spec.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: Timetables_CREATE.
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: Not idempotent — repeated calls create repeated rows unless they would overlap.

Request

PartRequiredDetails
HeadersYesAuthorization, Content-Type: application/json.
ParamsYesclassPublicId.
QueryNo—
BodyYesCreateTimeslotDto — see §6.3.
{ "dayOfWeek": 1, "startMinute": 480, "endMinute": 540, "slotType": "period", "label": null }

Response

201, TimeslotDto (§6.1), wrapped in the standard envelope with message "Timeslot created.".

Side Effects

  • Database write: one class_timeslots insert.
  • No cache, queue, realtime, notification, or external call.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404CLASS_NOT_FOUNDclassPublicId does not resolve.—class-timeslots-write.service.ts
409TIMESLOT_OVERLAPS_EXISTINGOverlaps another slot of the same class/day.Pick a non-overlapping time.class_timeslots_no_overlap
422TIMESLOT_TIMES_INVALIDendMinute <= startMinute, or out of [0,1440].Fix the times.class_timeslots_minutes_bounded
422TIMESLOT_DAY_INVALIDdayOfWeek outside 0..6.Fix the day.class_timeslots_day_bounded
422TIMESLOT_LABEL_INVALIDControl character, or over 64 chars after trim.Fix the label.class_timeslots_label_format

Edge Cases

  • label omitted or null is valid; a leading/trailing-space label is trimmed server-side before validation against the format CHECK, so a pasted " Tiffin " succeeds as "Tiffin".
  • A value for startMinute/endMinute larger than a Postgres smallint (32767) raises 22003 at the database level before any CHECK runs — this is not translated to a module error code and surfaces as an unmapped 500 if the DTO's own @Max(1440) bound is somehow bypassed; the DTO bound is the actual first line of defense.

Example Requests

curl -X POST "$API_URL/api/classes/018f2f3e-.../timeslots" \
  -H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" \
  -d '{"dayOfWeek":1,"startMinute":480,"endMinute":540,"slotType":"period"}'

8.3 PATCH /api/classes/:classPublicId/timeslots/:timeslotId

Purpose

Updates a timeslot's day, time range, type, or label, with optimistic concurrency. Because moving or retyping a slot cascades onto every class_timetable_entries row that occupies it — including published ones — this route requires an additional permission beyond the ordinary update code.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/timetable/timeslots/class-timeslots.controller.ts
DTOtimeslot.dto.ts (UpdateTimeslotDto)
Serviceclass-timeslots-write.service.ts
Schemapackages/db/src/schema/school/timetable.ts (composite key 2 on class_timetable_entries)
Teststimeslots.integration.spec.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: Timetables_UPDATE and TimetablePublish_UPDATE (both required).
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: Effectively idempotent for the same version (a stale version fails on retry).

Request

PartRequiredDetails
HeadersYesAuthorization, Content-Type.
ParamsYesclassPublicId, timeslotId.
QueryNo—
BodyYesUpdateTimeslotDto — all fields optional except version (§6.4).

Minimal valid request (no field changes, only advancing the version by touching one field or resending it):

{ "version": 3 }

Full valid request:

{ "dayOfWeek": 2, "startMinute": 500, "endMinute": 560, "slotType": "break", "label": null, "version": 3 }

Response

200, TimeslotDto, message "Timeslot updated.".

Side Effects

  • Database write: class_timeslots update.
  • Cascading write: if dayOfWeek/startMinute/endMinute/slotType change, every class_timetable_entries row referencing this slot is rewritten via ON UPDATE cascade, including published rows.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404TIMESLOT_NOT_FOUNDSlot absent, or belongs to a different class.—class-timeslots-write.service.ts
409TIMESLOT_VERSION_CONFLICTversion in the body is stale.Reload and retry.same
409TIMESLOT_OVERLAPS_EXISTINGNew day/time overlaps another slot of the class.—class_timeslots_no_overlap
422TIMESLOT_TIMES_INVALID / TIMESLOT_DAY_INVALID / TIMESLOT_LABEL_INVALIDAs in create.—respective CHECKs
409TIMESLOT_TYPE_CHANGE_BLOCKEDConverting an occupied period into a non-teaching type.Clear the lesson from this slot first.cascade onto class_timetable_entries_slot_is_teaching

Edge Cases

  • Omitting every field but version is valid: the service self-assigns the existing value of one column so the UPDATE statement is non-trivial and the bump_row_version trigger still fires, advancing version.
  • label: null explicitly clears an existing label (distinct from omitting label, which leaves it unchanged).

Example Requests

curl -X PATCH "$API_URL/api/classes/018f2f3e-.../timeslots/018f2f40-..." \
  -H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" \
  -d '{"startMinute":500,"endMinute":560,"version":3}'

8.4 DELETE /api/classes/:classPublicId/timeslots/:timeslotId

Purpose

Hard-deletes a timeslot. Refused while any lesson still occupies it.

Source Evidence

EvidencePath
Controllerclass-timeslots.controller.ts
Serviceclass-timeslots-write.service.ts
Schemaclass_timetable_entries_timeslot_fk (ON DELETE restrict)
Teststimeslots.integration.spec.ts

Auth and Permissions

  • Permission: Timetables_DELETE and TimetablePublish_UPDATE.
  • Guest support: No.
  • Idempotency: Not idempotent — a second call 404s.

Request

PartRequiredDetails
ParamsYesclassPublicId, timeslotId.

Response

200, data: null, message "Timeslot deleted.".

Side Effects

Database write: class_timeslots delete, refused by ON DELETE restrict if occupied.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404TIMESLOT_NOT_FOUNDSlot absent, or belongs to a different class.—class-timeslots-write.service.ts
409TIMESLOT_IN_USEA lesson still occupies the slot (23001 on the timeslot FK).Clear the entry first.class_timetable_entries_timeslot_fk

Edge Cases

Deleting a slot that never existed and one that is currently occupied both fail, with different codes — the caller cannot distinguish "wrong id" from "occupied" without reading the error code.

Example Requests

curl -X DELETE "$API_URL/api/classes/018f2f3e-.../timeslots/018f2f40-..." \
  -H "Authorization: Bearer TOKEN"

8.5 POST /api/classes/:classPublicId/timeslots/copy

Purpose

Clones one day's timeslots onto one or more other days in a single transaction — the fast path for building a school's typical five- or six-day-identical week, and, via fromClassPublicId, for rolling a schedule forward into a new class at the start of a session.

Source Evidence

EvidencePath
Controllerclass-timeslots.controller.ts
DTOtimeslot.dto.ts (CopyTimeslotsDto)
Serviceclass-timeslots-write.service.ts
Teststimeslots.integration.spec.ts

Auth and Permissions

  • Permission: Timetables_CREATE, Timetables_DELETE, and TimetablePublish_UPDATE (all three required — copy can both create and, with replace: true, destroy existing timeslots).
  • Guest support: No.
  • Idempotency: Not idempotent with replace: false (re-running duplicates or overlaps); idempotent in effect with replace: true against an unchanged source day.

Request

{ "fromDayOfWeek": 1, "toDaysOfWeek": [2, 3, 4, 5], "replace": true, "fromClassPublicId": null }

See §6.5 for the full field table.

Response

200, ClassTimeslotsDto (the full 7-day listing after the copy), message "Timeslots copied.".

Side Effects

  • Database writes: optional bulk delete of the target days' slots (replace: true), then bulk insert of the copied slots — all in one transaction.
  • No cache, queue, or realtime side effect.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
422TIMESLOT_COPY_SOURCE_IN_TARGETtoDaysOfWeek includes fromDayOfWeek.Self-copy is incoherent — remove it from the target list.pre-check in copy()
422TIMESLOT_COPY_SOURCE_EMPTYThe source day has zero slots.A silent no-op across up to 6 days is refused rather than silently doing nothing.pre-check in copy()
409TIMESLOT_IN_USEWith replace: true, a target day's slot is occupied and cannot be deleted.The whole copy rolls back.cascade from the delete step
409TIMESLOT_OVERLAPS_EXISTINGWith replace: false, a copied slot overlaps an existing one on a target day.—class_timeslots_no_overlap

Edge Cases

  • toDaysOfWeek is deduplicated server-side ([1, 1, 2] behaves as [1, 2]) so a client sending duplicates does not attempt the same insert twice inside one transaction.
  • The entire operation is one transaction: a failure on the third of four target days rolls back all four, never leaving some days copied and others not.
  • fromClassPublicId omitted defaults to the target class itself.

Example Requests

curl -X POST "$API_URL/api/classes/018f2f3e-.../timeslots/copy" \
  -H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" \
  -d '{"fromDayOfWeek":1,"toDaysOfWeek":[2,3,4,5],"replace":true}'

8.6 GET /api/classes/:classPublicId/timetable

Purpose

Reads the live grid. Defaults to the draft if one exists, else the published version, else an empty grid — a brand-new class is not an error. Pass ?status=draft or ?status=published to require that exact version.

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
Query DTOtimetable.dto.ts (ReadTimetableQueryDto)
Serviceclass-timetable-read.service.ts
Teststimetable-write.integration.spec.ts

Auth and Permissions

  • Permission: Timetables_READ.
  • Guest support: No.
  • Idempotency: N/A (read).

Request

PartRequiredDetails
ParamsYesclassPublicId.
QueryNostatus?: "draft" | "published".

Response

{
  "success": true,
  "message": "Timetable fetched.",
  "data": {
    "classPublicId": "018f2f3e-...",
    "status": "draft",
    "versionNo": 5,
    "publishedAt": null,
    "hasDraft": true,
    "hasPublished": true,
    "slots": [ /* TimeslotDto[] */ ],
    "entries": [ /* TimetableEntryDto[] */ ],
    "staleReferences": []
  }
}

Side Effects

None — read-only.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404CLASS_NOT_FOUNDclassPublicId does not resolve.—resolveClass

An explicit ?status= naming a version that does not exist is not an error — see the next row.

Edge Cases

  • No draft, no published, no explicit status: returns status: "draft", versionNo: null, hasDraft: false, hasPublished: false, empty slots/entries/staleReferences. An empty class is a normal state, not a 404.
  • Explicit ?status=published on a class with no published version: same empty-grid shape, status echoed back as "published" — deliberately not a 404 (an earlier design that did 404 here produced a permanent error-retry loop in the admin panel for the ordinary case of an unpublished class).
  • staleReferences is populated only for status === "published"; a draft response's staleReferences is always [] even if the draft's own references have decayed (that check runs only at publish time for drafts).

Example Requests

curl -X GET "$API_URL/api/classes/018f2f3e-.../timetable?status=published" \
  -H "Authorization: Bearer TOKEN"

8.7 GET /api/classes/:classPublicId/timetable/versions

Purpose

Lists every version of a class's timetable for a version-history screen: the currently live version first (if one exists), then every archived version, newest first.

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
Serviceclass-timetable-read.service.ts (listVersions)
Schemaclass_timetables, class_timetable_archives

Auth and Permissions

  • Permission: Timetables_READ.

Response

200, TimetableVersionSummaryDto[] (§6.14), message "Versions fetched.". An empty array is normal for a class that has never published. The read runs inside one read-only REPEATABLE READ transaction over both tables, so a concurrent publish or unpublish cannot duplicate, drop, or hide a version between the live lookup and the archive lookup.

Error Cases

HTTP StatusError CodeCondition
404CLASS_NOT_FOUNDclassPublicId does not resolve.

Example Requests

curl -X GET "$API_URL/api/classes/018f2f3e-.../timetable/versions" -H "Authorization: Bearer TOKEN"

8.8 GET /api/classes/:classPublicId/timetable/versions/:versionNo

Purpose

Reads one version in full, for a read-only "view this version" screen — the version named by versionNo may be archived or the one currently live.

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
Serviceclass-timetable-read.service.ts (readVersion)
Schemaclass_timetables, class_timetable_archives

Auth and Permissions

  • Permission: Timetables_READ.

Response

200, ArchivedTimetableDto (§6.15), message "Version fetched.". An archived match is checked first; if none exists, versionNo is matched against the currently PUBLISHED header on this class — all three of classId, status = 'published' and versionNo must hold, or the request 404s. That predicate is deliberate: a versionNo matching neither branch always 404s, even when the class has a live version under a different number — the screen titles itself from the requested versionNo, so silently substituting the live week would show it under the wrong heading.

Error Cases

HTTP StatusError CodeCondition
404CLASS_NOT_FOUNDclassPublicId does not resolve.
404TIMETABLE_VERSION_NOT_FOUNDNo archive AND no currently-published header with that versionNo for this class.

Example Requests

curl -X GET "$API_URL/api/classes/018f2f3e-.../timetable/versions/4" -H "Authorization: Bearer TOKEN"

8.9 POST /api/classes/:classPublicId/timetable/draft

Purpose

Creates a new draft. With no body, forks the currently-published version (or creates an empty draft if none exists). With fromVersionNo, rebuilds the draft from that archived version instead — resolving each entry against the current schema state, and skipping (never silently dropping, never failing the whole call) any entry whose referenced timeslot or subject assignment no longer matches.

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
DTOtimetable.dto.ts (CreateDraftDto)
Serviceclass-timetable-draft-write.service.ts (createDraft)
Utilclass-timetable-draft.util.ts (resolveArchivedEntry, assertSessionCurrent, nextVersionNo)

Auth and Permissions

  • Permission: Timetables_UPDATE.
  • Idempotency: Not idempotent — a second call while a draft already exists is refused.

Request

{ "fromVersionNo": 4 }

Or an empty body {} to fork the published version.

Response

201, CreateDraftResultDto (§6.13), message "Draft created.".

Side Effects

  • Database writes: one class_timetables insert (status: "draft"), plus a bulk class_timetable_entries insert for every entry that resolved.
  • Locks: both of this class's headers, FOR UPDATE, ORDER BY id — the same order publish locks in, so the two operations cannot deadlock against each other.

Error Cases

HTTP StatusError CodeConditionSource
404CLASS_NOT_FOUNDclassPublicId does not resolve.resolveClass
409TIMETABLE_DRAFT_EXISTSA draft already exists for this class.pre-check + class_timetables_one_draft_per_class
409TIMETABLE_NO_CURRENT_SESSIONNo academic_sessions row is marked current at all.assertSessionCurrent
422TIMETABLE_SESSION_NOT_CURRENTThis class's session is not the current one.assertSessionCurrent
404TIMETABLE_VERSION_NOT_FOUNDfromVersionNo does not resolve to an archive of this class.createDraft

Edge Cases

  • Restoring from an archive whose referenced timeslot has since moved, or whose subject is no longer assigned to the class, skips that entry with a specific reason in the response's skipped[] — the call still succeeds.
  • Forking a published version with zero entries is valid — the new draft simply has zero entries (publishing it later will then fail with TIMETABLE_DRAFT_EMPTY).

Example Requests

curl -X POST "$API_URL/api/classes/018f2f3e-.../timetable/draft" \
  -H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" -d '{}'

8.10 DELETE /api/classes/:classPublicId/timetable/draft

Purpose

Discards the current draft, discarding all its unpublished edits.

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
Serviceclass-timetable-draft-write.service.ts (discardDraft)

Auth and Permissions

  • Permission: Timetables_DELETE.
  • Idempotency: Not idempotent — a second call 409s.

Response

200, data: null, message "Draft discarded.".

Side Effects

Database write: class_timetables delete (status = 'draft'); every class_timetable_entries row of that header is deleted by ON DELETE cascade.

Error Cases

HTTP StatusError CodeCondition
409TIMETABLE_DRAFT_MISSINGNo draft exists for this class.

Example Requests

curl -X DELETE "$API_URL/api/classes/018f2f3e-.../timetable/draft" -H "Authorization: Bearer TOKEN"

8.11 PUT /api/classes/:classPublicId/timetable/draft/entries/:timeslotId

Purpose

Places or updates a lesson in one timeslot of the current draft. An upsert — safe to retry after a timeout — that also serves as the drag-and-drop endpoint (send classSubjectId alone) and the lesson-details dialog (send the rest alongside it).

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
DTOtimetable.dto.ts (PutTimetableEntryDto)
Serviceclass-timetable-draft-write.service.ts (putEntry)
Utilclass-timetable-conflict.util.ts (findConflict)

Auth and Permissions

  • Permission: Timetables_UPDATE.
  • Idempotency: Yes, by design — safe to retry.

Request

Minimal (drop-only):

{ "classSubjectId": "018f2f41-..." }

Full:

{
  "classSubjectId": "018f2f41-...",
  "teacherId": "018f2f42-...",
  "roomId": 12,
  "notes": "Bring lab coats",
  "version": 2
}

Response

200, TimetableEntryDto (§6.6), message "Entry saved.".

Side Effects

  • Database write: class_timetable_entries INSERT ... ON CONFLICT (timetable_id, timeslot_id) DO UPDATE.
  • Read-only conflict lookup (findConflict) before the write, excluding the class being edited from the search.

Error Cases

HTTP StatusError CodeConditionSource
409TIMETABLE_DRAFT_MISSINGNo draft exists for this class.putEntry
404TIMESLOT_NOT_FOUNDtimeslotId absent, or belongs to a different class.putEntry
422TIMETABLE_SLOT_NOT_TEACHINGThe slot is not slotType: "period".pre-check
422TIMETABLE_SUBJECT_NOT_IN_CLASSclassSubjectId does not belong to this class.pre-check + class_timetable_entries_class_subject_fk
404STAFF_NOT_FOUNDSupplied teacherId does not resolve.pre-check
404ROOM_NOT_FOUNDSupplied roomId does not resolve.pre-check
409TIMETABLE_TEACHER_CONFLICTThe teacher is already committed to a published, overlapping lesson elsewhere in this session.findConflict, backstopped by class_timetable_entries_teacher_free
409TIMETABLE_ROOM_CONFLICTThe room is already assigned during an overlapping published lesson.findConflict, backstopped by class_timetable_entries_room_free
409TIMETABLE_VERSION_CONFLICTCaller supplied version, and the row has since changed.setWhere on the upsert
422TIMETABLE_NOTES_INVALIDNotes blank-padded, control characters, or over 500 chars.class_timetable_entries_notes_format

Edge Cases

  • Sending only classSubjectId never clears an existing teacherId/roomId/notes on that slot — only fields present in the body are overwritten.
  • The conflict check excludes the class currently being edited: publishing deletes that class's own published rows before promoting, so its own published entries can never be the thing a draft edit collides with.
  • version omitted entirely (a fresh placement) is always accepted; supplied and stale, it is rejected even though the write would otherwise have succeeded.

Example Requests

curl -X PUT "$API_URL/api/classes/018f2f3e-.../timetable/draft/entries/018f2f40-..." \
  -H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" \
  -d '{"classSubjectId":"018f2f41-..."}'

8.12 DELETE /api/classes/:classPublicId/timetable/draft/entries/:timeslotId

Purpose

Clears a slot in the draft. Idempotent — clearing an already-empty slot is not an error.

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
Serviceclass-timetable-draft-write.service.ts (removeEntry)

Auth and Permissions

  • Permission: Timetables_UPDATE.
  • Idempotency: Yes.

Response

200, data: null, message "Entry cleared.".

Error Cases

HTTP StatusError CodeCondition
409TIMETABLE_DRAFT_MISSINGNo draft exists for this class.

Example Requests

curl -X DELETE "$API_URL/api/classes/018f2f3e-.../timetable/draft/entries/018f2f40-..." \
  -H "Authorization: Bearer TOKEN"

8.13 GET /api/classes/:classPublicId/timetable/slots/:timeslotId/availability

Purpose

Returns which teachers and rooms are free for a given slot's day and minute range — the complement of the conflict check run on every drop, surfaced proactively so the builder UI can grey out or highlight options before the operator even attempts a placement.

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
Serviceclass-timetable-read.service.ts (availability)

Auth and Permissions

  • Permission: Timetables_READ.

Response

200, SlotAvailabilityDto (§6.16), message "Availability fetched.".

Error Cases

HTTP StatusError CodeCondition
404CLASS_NOT_FOUNDclassPublicId does not resolve.
404TIMESLOT_NOT_FOUNDtimeslotId absent, or belongs to a different class.

Edge Cases

Computed among published entries only, scoped to the class's academic session — a draft's own conflicting placements never make a teacher/room appear unavailable here.

Example Requests

curl -X GET "$API_URL/api/classes/018f2f3e-.../timetable/slots/018f2f40-.../availability" \
  -H "Authorization: Bearer TOKEN"

8.14 POST /api/classes/:classPublicId/timetable/publish

Purpose

Publishes the current draft: archives the superseded published version (if any), then promotes the draft. This is the action that makes a schedule visible to every pupil and parent in the class.

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
Serviceclass-timetable-publish.service.ts (publish)
Utilclass-timetable-conflict.util.ts, class-timetable-draft.util.ts
Teststimetable-write.integration.spec.ts

Auth and Permissions

  • Permission: TimetablePublish_UPDATE (superadmin-only in practice — see §5).
  • Idempotency: Idempotent in the useful sense — retried with no draft but a published version already present, returns that version rather than failing.

Request

No body. HttpCode(200) overrides Nest's default 201 for a POST.

Response

200, ClassTimetableDto (§6.8, status: "published"), message "Timetable published.".

Side Effects

  • Database writes: optional archive insert + published-header delete (if a published version existed), then the draft header's status update to "published".
  • Cascading write: the header's status change cascades to every entry via the composite header FK, which is the exact moment the two EXCLUDE constraints are checked.
  • No cache, queue, or realtime side effect.

Error Cases

HTTP StatusError CodeConditionSource
409TIMETABLE_DRAFT_MISSINGNo draft and no published version exists.publish
422TIMETABLE_DRAFT_EMPTYThe draft has zero entries.pre-check
409TIMETABLE_NO_CURRENT_SESSIONNo session is marked current.assertSessionCurrent
422TIMETABLE_SESSION_NOT_CURRENTThis class's session is not current.assertSessionCurrent
422TIMETABLE_STALE_REFERENCESA draft entry's teacher/room/subject has decayed since assignment.computeStaleReferences; response details.staleReferences
409TIMETABLE_TEACHER_CONFLICTA cross-class teacher clash was found among the draft's entries.class_timetable_entries_teacher_free, named via findConflict; response details.conflictsWith
409TIMETABLE_ROOM_CONFLICTA cross-class room clash.class_timetable_entries_room_free; response details.conflictsWith
409TIMETABLE_CONCURRENT_UPDATEA 40001/40P01 deadlock survived one retry.withDeadlockRetry

Edge Cases

  • Retrying a publish that already succeeded (no draft left, published version present) returns that published version with a 200, not an error.
  • A conflict found during promotion is investigated before the surrounding transaction rolls back, so the named conflictsWith row is guaranteed still accurate at the moment it is reported.

Example Requests

curl -X POST "$API_URL/api/classes/018f2f3e-.../timetable/publish" -H "Authorization: Bearer TOKEN"

8.15 POST /api/classes/:classPublicId/timetable/unpublish

Purpose

Removes the currently published timetable after archiving it, returning the class to having no live schedule.

Source Evidence

EvidencePath
Controllerclass-timetable.controller.ts
Serviceclass-timetable-publish.service.ts (unpublish)

Auth and Permissions

  • Permission: TimetablePublish_UPDATE.
  • Idempotency: Not idempotent — a second call 404s.

Response

200, data: null, message "Timetable unpublished.".

Side Effects

Database writes: archive insert, then delete of the published header (entries cascade away).

Error Cases

HTTP StatusError CodeCondition
404TIMETABLE_NOT_FOUNDNo published timetable exists for this class.

Example Requests

curl -X POST "$API_URL/api/classes/018f2f3e-.../timetable/unpublish" -H "Authorization: Bearer TOKEN"

8.16 GET /api/mobile/teacher/schedule

Purpose

Returns the calling teacher's own published weekly teaching schedule for the current academic session.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/mobile/teacher/teacher-schedule-portal.controller.ts
Queryapps/api/src/modules/mobile/shared/portal-schedule.query.ts
DTOapps/api/src/modules/mobile/shared/portal-schedule.dto.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: None — audience asserted by assertTeacherStaffRow, which resolves and returns the caller's own live staff.id (never a boolean), because classes.class_teacher_id and class_timetable_entries.teacher_id are both nullable and a predicate built from an undefined local would otherwise match the whole school.
  • Guest support: No.

Response

200, TeacherScheduleDto, message "Schedule fetched.".

{
  "success": true,
  "message": "Schedule fetched.",
  "data": {
    "academicSessionName": "2026-27",
    "entries": [
      {
        "dayOfWeek": 1,
        "startMinute": 480,
        "endMinute": 540,
        "slotType": "period",
        "label": null,
        "subjectName": "Mathematics",
        "subjectCode": "MATH",
        "teacherName": "Ram Sharma",
        "roomName": "Room 204",
        "className": "Grade 5 A",
        "classPublicId": "0198f2c4-6b1e-7c3a-9d52-3a4f1b8e7a10",
        "subjectId": "0198f2c4-6b1e-7c3a-9d52-3a4f1b8e7a11",
        "classSubjectId": "0198f2c4-6b1e-7c3a-9d52-3a4f1b8e7a12"
      }
    ]
  }
}

Side Effects

None — read-only.

Error Cases

No module-specific error code is raised by this route; an audience-assertion failure returns the shared portal 403.

Edge Cases

  • No current academic session: returns academicSessionName: null, entries: [] — not an error.
  • A teacher with no assigned periods returns entries: [].
  • Entries include className because one teacher's week spans multiple classes.
  • Every entry is a published lesson, so classPublicId, subjectId and classSubjectId are always present; each is a public uuid, never an integer key.

Example Requests

curl -X GET "$API_URL/api/mobile/teacher/schedule" -H "Authorization: Bearer TOKEN"

8.17 GET /api/mobile/student/schedule

Purpose

Returns the calling pupil's own class's published weekly schedule for the current academic session.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/mobile/student/student-schedule-portal.controller.ts
Queryapps/api/src/modules/mobile/shared/portal-schedule.query.ts

Auth and Permissions

  • Permission: None — assertPortalAudience(actor, "student") only confirms the session is a pupil session, never which pupil; the row scope is the WHERE clause built from a live enrollment (students.deleted_at IS NULL, students.record_status = 'active', studentClassEnrollments.status = 'active', scoped to the current academic session).
  • Guest support: No.

Response

200, PortalScheduleDto, message "Schedule fetched." — same entry shape as the teacher route minus className (every row already belongs to the pupil's one enrolled class).

Error Cases

None module-specific.

Edge Cases

  • No current session, or no live enrollment: entries: [].
  • A pupil holding an unclosed prior-year enrollment alongside the current year's is filtered to the current session only — without that filter this route would interleave two classes' weeks.

Example Requests

curl -X GET "$API_URL/api/mobile/student/schedule" -H "Authorization: Bearer TOKEN"

8.18 GET /api/mobile/guardian/children/:id/schedule

Purpose

Returns one of the calling guardian's children's published weekly class schedule for the current academic session.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/mobile/guardian/guardian-schedule-portal.controller.ts
Queryapps/api/src/modules/mobile/shared/portal-schedule.query.ts
ServiceGuardianPortalService.resolveOwnChildId

Auth and Permissions

  • Permission: None — assertPortalAudience(actor, "guardian") confirms audience only; resolveOwnChildId is the actual authorization decision and runs before any schedule row is read.
  • Guest support: No.

Request

PartRequiredDetails
ParamsYesid — the child's students.id.

Response

200, PortalScheduleDto.

Error Cases

HTTP StatusError CodeConditionUser-Facing Meaning
404(from resolveOwnChildId, not a TIMETABLE_* code)id does not resolve to one of the caller's own children.Not 403 — a 403 would confirm the row exists, turning the id space into an enumeration oracle against a roll of children.

Edge Cases

  • The ward check runs first and can 404 before any timetable row is touched; only after it succeeds can an empty week ever be returned. Reversing that order would not itself leak data (an empty week from a foreign child id is indistinguishable from an own child with no timetable), but it would mean authorization ran after the query rather than before it.
  • No current session, or the child has no live enrollment: entries: [], 200.

Example Requests

curl -X GET "$API_URL/api/mobile/guardian/children/018f2f43-.../schedule" -H "Authorization: Bearer TOKEN"

8.19 GET /api/mobile/student/schedule/today

Purpose

The periods of the caller's own class for today, each marked finished, in progress or upcoming against the server clock in the school's timezone, with the next moment the day changes. Feeds the pupil dashboard's "today" card.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/mobile/student/student-schedule-portal.controller.ts (myToday)
Serviceapps/api/src/modules/mobile/shared/portal-today.service.ts
Period stateapps/api/src/modules/mobile/shared/portal-today.ts
Queriesapps/api/src/modules/mobile/shared/portal-today.query.ts
DTOapps/api/src/modules/mobile/shared/portal-today.dto.ts
Cache helperapps/api/src/common/utils/cache-race.util.ts

Auth and Permissions

  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: None — assertPortalAudience(actor, "student"). The handler key StudentSchedulePortalController.myToday is in NO_PERMISSION_ADMIN_HANDLERS.
  • The route takes no id. The pupil is resolved from the token (students.user_id = actor.id, deleted_at only).

Request

No params, query or body.

Response

{
  "message": "Schedule fetched.",
  "data": {
    "serverTime": "2026-10-06T04:30:12.000Z",
    "timezone": "Asia/Kathmandu",
    "date": "2026-10-06",
    "bsDate": { "year": 2083, "month": 6, "day": 20 },
    "dayOfWeek": 2,
    "academicSessionName": "2082/83",
    "calendar": {
      "status": "operating",
      "classification": "school_day",
      "eventName": null,
      "isOperatingDay": true
    },
    "periods": [
      {
        "id": "540-585-0",
        "state": "finished",
        "startMinute": 540,
        "endMinute": 585,
        "startsAt": "2026-10-06T03:15:00.000Z",
        "endsAt": "2026-10-06T04:00:00.000Z",
        "slotType": "period",
        "label": null,
        "subjectName": "Mathematics",
        "subjectCode": "MATH",
        "teacherName": "Ram Sharma",
        "roomName": "Room 204"
      }
    ],
    "currentPeriodId": null,
    "nextPeriodId": null,
    "nextTransitionAt": null,
    "summary": { "total": 1, "finished": 1, "remaining": 0 }
  },
  "errorCode": null
}

Fields are described under Portal DTOs in §6.18.

Calendar status

calendar.status is decided in a fixed order and the first match wins:

OrderstatusCondition
1no_sessionNo academic session is flagged current.
2outside_sessionToday is before the session's start_date or on or after its end_date (the range is half-open).
3not_setThe calendar has no row for today. This is not a holiday: the calendar has not decided.
4non_operatingThe calendar row says attendance is not expected (a weekend or holiday classification).
5no_classAn operating day, but the pupil has no active enrolment in the current session, or the pupil's record is deleted or not active. classification, eventName and isOperatingDay still describe the day.
6operatingAn operating day and an enrolled class. periods holds that class's slots for the weekday.

Every status other than operating returns an empty periods list and a zero summary. An operating day for a class with no published slots on this weekday also returns an empty list.

Side Effects

  • Database reads: the school clock, the current session, the calendar day, the pupil's active enrolment, and the class's published timetable entries merged with its breaks, lunches and activities.
  • Cache: the clock-independent base is cached for 60 seconds; see Caching below. No writes.

Caching

  • Key: portal:today: with the pupil's student id and the school-local date. The cached value is the session name, the calendar block and the day's slot rows. Period states, serverTime and nextTransitionAt are recomputed on every request from the cached rows, so they are never stale.
  • TTL 60 seconds. Nothing invalidates the key: a timetable publish or unpublish, a calendar edit, an enrolment change or a pupil status change is visible within 60 seconds.
  • The read goes through cacheGetFast: the Redis read races a 250 ms budget, and a timeout, an error or an outage is treated as a miss, so the route falls back to the database. The write is fire-and-forget (cacheSetFast).
  • The key is reachable only after identity is resolved, so the pupil route and the guardian route share one entry per pupil and day.

Error Cases

HTTP StatusError CodeCondition
403PERMISSION_INSUFFICIENTActive role's scopeKind is not student.
404STUDENT_NOT_FOUNDThe caller has no live students row (deleted_at is null is the only test here).

Edge Cases

  • An inactive or deleted-profile pupil who still holds a students row passes the identity check and receives 200 with calendar.status = "no_class" on an operating day.
  • Slot instants are built from the school-local date and minute, not from host-local time, so the response does not depend on the server's timezone.
  • currentPeriodId and nextPeriodId refer to any slot type, including breaks; summary counts only period slots.
  • bsDate is null when the date is outside the converter's range; the response is still 200.

Example Requests

curl -X GET "$API_URL/api/mobile/student/schedule/today" \
  -H "Authorization: Bearer TOKEN"

8.20 GET /api/mobile/guardian/children/:id/schedule/today

Purpose

One ward's periods for today, with the same payload as §8.19.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/mobile/guardian/guardian-schedule-portal.controller.ts (childToday)
Ward checkGuardianPortalService.resolveOwnChildId
Serviceapps/api/src/modules/mobile/shared/portal-today.service.ts

Auth and Permissions

  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: None — assertPortalAudience(actor, "guardian"), then resolveOwnChildId as the authorization decision. The handler key GuardianSchedulePortalController.childToday is in NO_PERMISSION_ADMIN_HANDLERS.
  • The ward check runs before the cache or any schedule row is read.

Request

Params: id — the child's student id (uuid; a non-UUID value is a 400 from ParseUUIDPipe). No query or body.

Response

200, PupilTodayScheduleDto — identical to §8.19, with message: "Schedule fetched.". Calendar status, caching and edge cases are those of §8.19.

Error Cases

HTTP StatusError CodeCondition
403PERMISSION_INSUFFICIENTActive role's scopeKind is not guardian.
404STUDENT_NOT_FOUNDid is not one of the caller's own children, or does not exist. Both answer identically.

Example Requests

curl -X GET "$API_URL/api/mobile/guardian/children/018f2f43-.../schedule/today" \
  -H "Authorization: Bearer TOKEN"

8.21 GET /api/mobile/teacher/schedule/today

Purpose

The caller's own periods for today, each marked finished, in progress or upcoming against the server clock in the school's timezone, with the next moment the day changes. Feeds the teacher dashboard's "today" card.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/mobile/teacher/teacher-today-portal.controller.ts (getToday)
Serviceapps/api/src/modules/mobile/teacher/teacher-today-portal.service.ts
Audience checkassertTeacherStaffRow in apps/api/src/modules/mobile/shared/portal-audience.ts

Auth and Permissions

  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: None — assertTeacherStaffRow requires the active role named teacher and resolves the caller's live staff row. The handler key TeacherTodayPortalController.getToday is in NO_PERMISSION_ADMIN_HANDLERS.
  • The route takes no id; the teacher is resolved from the token.

Request

No query, params or body.

Response

200, TeacherTodayScheduleDto, message: "Today's schedule fetched.": serverTime, timezone, date, bsDate, dayOfWeek, calendar (classification, eventName, isOperatingDay), periods, currentPeriodId, nextPeriodId, nextTransitionAt, summary (total, finished, remaining). Each period carries kind (class, exam or exam_invigilation), state (finished, in_progress or upcoming), start and end as minutes and instants, className, subjectName, room, and the public uuids classPublicId, subjectId and classSubjectId (always present, for every kind). Periods are ordered by start time; finished periods are kept.

The periods come from three sources that never suppress one another: the caller's published timetable entries for today, exam routine entries for class-subjects the caller currently teaches, and exam invigilation duties. Exam duties are returned on a non-operating day; isOperatingDay is informational. An exam_invigilation period names the class being invigilated, which the caller may neither teach nor be class teacher of; opening that class's roster through GET /api/mobile/teacher/classes/:publicId/roster then answers 404, so a client must not assume every period's classPublicId is openable.

Error Cases

HTTP StatusError CodeCondition
403PERMISSION_INSUFFICIENTNo active role, a guardian or pupil session, or no live staff row.

Example Requests

curl -X GET "$API_URL/api/mobile/teacher/schedule/today" \
  -H "Authorization: Bearer TOKEN"

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence — Entry Upsert

9.3 Error Branch — putEntry

9.4 Auth and Permission Flow — Admin vs. Portal

No endpoint in this module paginates. GET .../timeslots always returns all 7 days; GET .../timetable always returns the full grid for one version; GET .../timetable/versions returns the live version (if any) plus every archived version for the class (bounded naturally by how many times the class has published/unpublished); the weekly portal routes return one week's entries with no upper bound beyond what a week can hold, and the schedule/today routes return one day's. There is no page/pageSize/cursor parameter anywhere in this module, no sort parameter (ordering is always dayOfWeek then startMinute, which is total because overlapping rows are unrepresentable), and no search/filter parameter.

11. Caching, Jobs, and External Integrations

IntegrationUsed?DetailsSource
Redis cacheOnly the pupil and guardian schedule/today routesportal:today: keys, 60-second TTL, one entry per pupil and school-local date, holding the session name, calendar block and the day's slots; period states are recomputed per request. Reads use cacheGetFast (250 ms budget, a slow or unavailable Redis is a miss); writes are fire-and-forget. Nothing invalidates the key, so a publish, unpublish or calendar edit is visible within 60 seconds. The admin routes, the three weekly schedule routes and the teacher schedule/today route read the database directly.mobile/shared/portal-today.service.ts; common/utils/cache-race.util.ts
BullMQNoNo queue, producer, or processor exists for this module.—
Realtime (Socket.IO)NoNo gateway or emitted event exists for this module.—
External APINo——

13. Mandatory Deep API Documentation Pack

13.1 Route-by-Route Completeness Matrix

RouteController MethodDTOsService MethodGuardsPermissionsCacheJobsDB TouchesErrorsTestsDocumented?
GET /classes/:id/timeslotsClassTimeslotsController.listClassTimeslotsDtoClassTimeslotsReadService.listByClassJwt, RoleTimetables_READN/AN/Aclass_timeslots (read)CLASS_NOT_FOUNDtimeslots.integration.spec.tsYes
POST /classes/:id/timeslots.createCreateTimeslotDto→TimeslotDtoClassTimeslotsWriteService.createJwt, RoleTimetables_CREATEN/AN/Aclass_timeslots (insert)CLASS_NOT_FOUND, TIMESLOT_OVERLAPS_EXISTING, TIMESLOT_TIMES_INVALID, TIMESLOT_DAY_INVALID, TIMESLOT_LABEL_INVALIDtimeslots.integration.spec.tsYes
PATCH /classes/:id/timeslots/:timeslotId.updateUpdateTimeslotDto→TimeslotDtoClassTimeslotsWriteService.updateJwt, RoleTimetables_UPDATE + TimetablePublish_UPDATEN/AN/Aclass_timeslots (update), cascades to class_timetable_entriesTIMESLOT_NOT_FOUND, TIMESLOT_VERSION_CONFLICT, TIMESLOT_OVERLAPS_EXISTING, TIMESLOT_TIMES_INVALID, TIMESLOT_DAY_INVALID, TIMESLOT_LABEL_INVALID, TIMESLOT_TYPE_CHANGE_BLOCKEDtimeslots.integration.spec.tsYes
DELETE /classes/:id/timeslots/:timeslotId.remove—ClassTimeslotsWriteService.removeJwt, RoleTimetables_DELETE + TimetablePublish_UPDATEN/AN/Aclass_timeslots (delete)TIMESLOT_NOT_FOUND, TIMESLOT_IN_USEtimeslots.integration.spec.tsYes
POST /classes/:id/timeslots/copy.copyCopyTimeslotsDto→ClassTimeslotsDtoClassTimeslotsWriteService.copyJwt, RoleTimetables_CREATE + Timetables_DELETE + TimetablePublish_UPDATEN/AN/Aclass_timeslots (bulk delete/insert)TIMESLOT_COPY_SOURCE_IN_TARGET, TIMESLOT_COPY_SOURCE_EMPTY, TIMESLOT_IN_USE, TIMESLOT_OVERLAPS_EXISTINGtimeslots.integration.spec.tsYes
GET /classes/:id/timetable.readReadTimetableQueryDto→ClassTimetableDtoClassTimetableReadService.readJwt, RoleTimetables_READN/AN/Aclass_timetables, class_timeslots, class_timetable_entries (+joins)CLASS_NOT_FOUNDtimetable-write.integration.spec.tsYes
GET /classes/:id/timetable/versions.listVersionsTimetableVersionSummaryDto[]ClassTimetableReadService.listVersionsJwt, RoleTimetables_READN/AN/Aclass_timetables, class_timetable_archives, users (in one REPEATABLE READ transaction)CLASS_NOT_FOUNDtimetable-write.integration.spec.tsYes
GET /classes/:id/timetable/versions/:versionNo.readVersionArchivedTimetableDtoClassTimetableReadService.readVersionJwt, RoleTimetables_READN/AN/Aclass_timetables, class_timetable_archives, users (in one REPEATABLE READ transaction)CLASS_NOT_FOUND, TIMETABLE_VERSION_NOT_FOUNDtimetable-write.integration.spec.tsYes
POST /classes/:id/timetable/draft.createDraftCreateDraftDto→CreateDraftResultDtoClassTimetableDraftWriteService.createDraftJwt, RoleTimetables_UPDATEN/AN/Aclass_timetables (insert), class_timetable_entries (bulk insert), class_timetable_archives (read, if restoring)CLASS_NOT_FOUND, TIMETABLE_DRAFT_EXISTS, TIMETABLE_NO_CURRENT_SESSION, TIMETABLE_SESSION_NOT_CURRENT, TIMETABLE_VERSION_NOT_FOUNDtimetable-write.integration.spec.tsYes
DELETE /classes/:id/timetable/draft.discardDraft—ClassTimetableDraftWriteService.discardDraftJwt, RoleTimetables_DELETEN/AN/Aclass_timetables (delete), cascade to entriesTIMETABLE_DRAFT_MISSINGtimetable-write.integration.spec.tsYes
PUT /classes/:id/timetable/draft/entries/:timeslotId.putEntryPutTimetableEntryDto→TimetableEntryDtoClassTimetableDraftWriteService.putEntryJwt, RoleTimetables_UPDATEN/AN/Aclass_timetable_entries (upsert)TIMETABLE_DRAFT_MISSING, TIMESLOT_NOT_FOUND, TIMETABLE_SLOT_NOT_TEACHING, TIMETABLE_SUBJECT_NOT_IN_CLASS, STAFF_NOT_FOUND, ROOM_NOT_FOUND, TIMETABLE_TEACHER_CONFLICT, TIMETABLE_ROOM_CONFLICT, TIMETABLE_VERSION_CONFLICT, TIMETABLE_NOTES_INVALIDtimetable-write.integration.spec.tsYes
DELETE /classes/:id/timetable/draft/entries/:timeslotId.removeEntry—ClassTimetableDraftWriteService.removeEntryJwt, RoleTimetables_UPDATEN/AN/Aclass_timetable_entries (delete)TIMETABLE_DRAFT_MISSINGtimetable-write.integration.spec.tsYes
GET /classes/:id/timetable/slots/:timeslotId/availability.availabilitySlotAvailabilityDtoClassTimetableReadService.availabilityJwt, RoleTimetables_READN/AN/Aclass_timeslots, class_timetable_entries, staff, rooms, designationsCLASS_NOT_FOUND, TIMESLOT_NOT_FOUNDtimetable-write.integration.spec.tsYes
POST /classes/:id/timetable/publish.publishClassTimetableDtoClassTimetablePublishService.publishJwt, RoleTimetablePublish_UPDATEN/AN/Aclass_timetables, class_timetable_archives, class_timetable_entries (cascade)TIMETABLE_DRAFT_MISSING, TIMETABLE_DRAFT_EMPTY, TIMETABLE_NO_CURRENT_SESSION, TIMETABLE_SESSION_NOT_CURRENT, TIMETABLE_STALE_REFERENCES, TIMETABLE_TEACHER_CONFLICT, TIMETABLE_ROOM_CONFLICT, TIMETABLE_CONCURRENT_UPDATEtimetable-write.integration.spec.tsYes
POST /classes/:id/timetable/unpublish.unpublish—ClassTimetablePublishService.unpublishJwt, RoleTimetablePublish_UPDATEN/AN/Aclass_timetables (delete), class_timetable_archives (insert)TIMETABLE_NOT_FOUNDtimetable-write.integration.spec.tsYes
GET /teacher/scheduleTeacherSchedulePortalController.myScheduleTeacherScheduleDtoreadPublishedScheduleJwt, RoleNone (audience-asserted)N/AN/Aclass_timetable_entries (+joins), academic_sessions(none module-specific)—Yes
GET /student/scheduleStudentSchedulePortalController.mySchedulePortalScheduleDtoreadPublishedScheduleJwt, RoleNone (audience-asserted)N/AN/Aclass_timetable_entries (+joins), student_class_enrollments, students(none module-specific)—Yes
GET /mobile/student/schedule/todayStudentSchedulePortalController.myTodayPupilTodayScheduleDtoPortalTodayService.resolveOwnStudentId, .forStudentJwt, RoleNone (audience-asserted)portal:today: (60 s)N/Astudents, academic_sessions, academic_calendar_days, student_class_enrollments, class_timetable_entries (+joins), class_timeslotsSTUDENT_NOT_FOUNDportal-today specsYes
GET /guardian/children/:id/scheduleGuardianSchedulePortalController.childSchedulePortalScheduleDtoreadPublishedScheduleJwt, RoleNone (audience-asserted)N/AN/Aclass_timetable_entries (+joins), student_class_enrollments, plus GuardianPortalService.resolveOwnChildId's own tables(none module-specific; a ward-ownership failure 404s)—Yes
GET /mobile/teacher/schedule/todayTeacherTodayPortalController.getTodayTeacherTodayScheduleDtoTeacherTodayPortalService.getTodayJwt, RoleNone (audience-asserted)N/AN/Aclass_timetable_entries, exam_routine_entries, exam_invigilators (+joins), academic_calendar_daysPERMISSION_INSUFFICIENTN/AYes
GET /mobile/guardian/children/:id/schedule/todayGuardianSchedulePortalController.childTodayPupilTodayScheduleDtoGuardianPortalService.resolveOwnChildId, PortalTodayService.forStudentJwt, RoleNone (audience-asserted)portal:today: (60 s)N/Aas above, plus the ward check's tablesSTUDENT_NOT_FOUNDportal-today specsYes

13.2 Request/Response Exhaustiveness

Covered per-endpoint in §8: every mutation route includes a minimal and full request example where applicable, every route documents its success envelope, empty-list/empty-grid behavior is documented for the list-shaped and grid-shaped reads, and every validation, domain, and auth/permission error is enumerated per endpoint with its exact code and condition.

13.3 API Diagram Pack

See §9 for the route-ownership graph, the entry-upsert sequence diagram, the putEntry error decision tree, and the auth/permission flow. A cache-flow diagram and an async/job-flow diagram are omitted deliberately — neither a cache nor a queue exists anywhere in this module (§11).

13.4 Consumer Integration Notes

ConsumerRequired KnowledgeFailure HandlingContract Stability
Admin panel (Timeslot Manager)version is mandatory on every PATCH; update/remove/copy need two or three permission codes simultaneously.Display TIMESLOT_VERSION_CONFLICT as "someone else changed this, reload"; TIMESLOT_IN_USE as "clear the lesson first".Stable — route names and handler contracts are treated as frozen.
Admin panel (Timetable Builder)The draft/published resolution order on GET .../timetable; that staleReferences only appears on published reads; that publish can fail with a named conflict carrying details.conflictsWith.Render TIMETABLE_TEACHER_CONFLICT/TIMETABLE_ROOM_CONFLICT using the exact conflictsWith payload rather than a generic message; render TIMETABLE_STALE_REFERENCES's details.staleReferences as an actionable per-entry list.Stable.
Mobile apps (teacher/student/guardian)All three routes return entries: [] on "nothing scheduled" — never treat an empty array as an error state. The guardian route 404s on a foreign child id; a 403 is never returned for that case.Render an empty week as "no schedule published yet", not as a failure.Stable.
QAEvery entries-side write path shares one constraint-translation map (translateTimetableWriteError); every timeslot write path shares another (CONSTRAINT_RULES in class-timeslots-write.service.ts). Reproducing a specific error usually means violating the named constraint directly via two concurrent requests, not via a single malformed payload.——
Internal servicesNone of these routes are called by any other backend module or job.——

13.5 API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
No pagination anywhere in this moduleEvery read returns its full, natural-sized result setCursor/page pagination on versions or the gridA class's timeslots, one version's entries, and one week's schedule are all naturally bounded (at most a few dozen rows); pagination would add client complexity for no real benefit.An unusually large archive history could make GET .../versions slow.Revisit if archive counts per class grow large (see backend doc §16.8).
GET .../timetable never 404s for a missing explicit ?status=Empty grid, echoed status404 TIMETABLE_NOT_FOUNDAn earlier version of this route did exactly that and produced a permanent error-retry loop in the admin panel for the ordinary case of an unpublished class.None currently known.Covered by an integration test asserting the empty-grid shape.
Portal routes return entries: [] instead of 404 when nothing is scheduledEmpty array, 200404 SCHEDULE_NOT_FOUND"Nothing is scheduled" is a normal, common state (a new class, a between-enrollment pupil, a teacher with no periods); treating it as an error would make every fresh mobile install hit an error screen.None.—
Guardian route 404s (not 403) on a foreign child id404403 ForbiddenA 403 confirms the row exists, turning the id space into an enumeration oracle.None additional — this is itself the mitigation.—
Entry upsert (PUT) instead of separate create/update verbsOne idempotent PUTSeparate POST (create) and PATCH (update)The drop-then-dialog UX flow naturally produces two writes to the same slot in quick succession; a single idempotent upsert avoids the client having to track whether the slot was already occupied.A caller cannot distinguish "created" from "updated" from the response alone (both return 200/the same DTO).Acceptable — the builder UI does not need that distinction.
version optional on entry upsert, required on timeslot updateAsymmetric requirementRequire version on bothThe entry route serves both create (no prior version to send) and update; the timeslot route only ever updates an existing row.None.—

13.6 API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
Adding a new slotType valueAdmin panel, all three portal appsNew enum value added to timeslot_type; every place matching on the six current values needs reviewExisting rows unaffectedYes — ALTER TYPE ... ADD VALUECoordinate a mobile app release before any slot is created with the new type, since older clients render an unrecognized slotType as an unstyled fallback.
Renaming a TimetableEntryDto/PortalScheduleEntryDto fieldAdmin panel, all three portal appsDTO and mapper updateNoneNoVersion the DTO or add the new field alongside the old one for a deprecation window; this module currently has no versioning scheme for its DTOs.
Adding a fourth portal audienceNew mobile controllerNew controller + NO_PERMISSION_ADMIN_HANDLERS/route-permissions.spec.ts entriesNoneNoMust reuse readPublishedSchedule/portal-schedule.dto.ts rather than re-deriving the query, per the shared-query rationale in the backend doc.

14. Zero-Omission API Checklist

  • Every controller route is documented (18 routes across 5 controllers).
  • Every parent route prefix and runtime URL is documented.
  • Every DTO field, nested field, enum, default, and validator is documented.
  • Every response field, nullable field, and generated field is documented; portal DTOs' deliberately-omitted fields are documented and why.
  • Every auth, guard, permission, and audience-assertion branch is documented.
  • Every success, validation, auth, permission, not-found, conflict, and server-error branch is documented.
  • Every database read/write and cascade is documented; cache/queue/realtime/notification/audit are documented as absent where absent.
  • Every route has examples for minimal and/or full requests, success responses, and representative failures.
  • Route ownership, sequence, error-decision, and auth-flow diagrams are included.
  • Every tradeoff and compatibility risk is documented.
  • 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.
  • Every response envelope is documented.
  • Every error code is documented.
  • Every auth guard and permission is documented.
  • Cache/queue/realtime/external calls are documented as absent.
  • Every diagram matches the current code.
  • This doc links to backend and features/flows.

See Also

  • Backend doc: /docs/developer/timetable/backend
  • Features and flows doc: /docs/developer/timetable/feature

On this page

Timetable - API Reference1. Documentation Evidence2. Module Summary3. Concepts and Terminology4. API Surface Map5. Auth, Identity, and Permissions6. DTO and Model Reference6.1 TimeslotDto (response)6.2 ClassTimeslotsDto (response, GET .../timeslots)6.3 CreateTimeslotDto (request, POST .../timeslots)6.4 UpdateTimeslotDto (request, PATCH .../timeslots/:timeslotId)6.5 CopyTimeslotsDto (request, POST .../timeslots/copy)6.6 TimetableEntryDto (response)6.7 StaleReferenceDto (response, nested in ClassTimetableDto)6.8 ClassTimetableDto (response, GET .../timetable)6.9 ReadTimetableQueryDto (query, GET .../timetable)6.10 PutTimetableEntryDto (request, PUT .../draft/entries/:timeslotId)6.11 CreateDraftDto (request, POST .../timetable/draft)6.12 SkippedEntryDto (response, nested in CreateDraftResultDto)6.13 CreateDraftResultDto (response, POST .../timetable/draft)6.14 TimetableVersionSummaryDto (response, GET .../timetable/versions)6.15 ArchivedTimetableDto / ArchivedTimetableEntryDto (response, GET .../timetable/versions/:versionNo)6.16 SlotAvailabilityDto (response, GET .../timetable/slots/:timeslotId/availability)6.17 ConflictDetailsDto / ConflictsWithDto (response, embedded in 409 error bodies)6.18 Portal DTOs7. Enum Reference8. Endpoint Reference8.1 GET /api/classes/:classPublicId/timeslotsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.2 POST /api/classes/:classPublicId/timeslotsPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.3 PATCH /api/classes/:classPublicId/timeslots/:timeslotIdPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.4 DELETE /api/classes/:classPublicId/timeslots/:timeslotIdPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.5 POST /api/classes/:classPublicId/timeslots/copyPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.6 GET /api/classes/:classPublicId/timetablePurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.7 GET /api/classes/:classPublicId/timetable/versionsPurposeSource EvidenceAuth and PermissionsResponseError CasesExample Requests8.8 GET /api/classes/:classPublicId/timetable/versions/:versionNoPurposeSource EvidenceAuth and PermissionsResponseError CasesExample Requests8.9 POST /api/classes/:classPublicId/timetable/draftPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.10 DELETE /api/classes/:classPublicId/timetable/draftPurposeSource EvidenceAuth and PermissionsResponseSide EffectsError CasesExample Requests8.11 PUT /api/classes/:classPublicId/timetable/draft/entries/:timeslotIdPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.12 DELETE /api/classes/:classPublicId/timetable/draft/entries/:timeslotIdPurposeSource EvidenceAuth and PermissionsResponseError CasesExample Requests8.13 GET /api/classes/:classPublicId/timetable/slots/:timeslotId/availabilityPurposeSource EvidenceAuth and PermissionsResponseError CasesEdge CasesExample Requests8.14 POST /api/classes/:classPublicId/timetable/publishPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.15 POST /api/classes/:classPublicId/timetable/unpublishPurposeSource EvidenceAuth and PermissionsResponseSide EffectsError CasesExample Requests8.16 GET /api/mobile/teacher/schedulePurposeSource EvidenceAuth and PermissionsResponseSide EffectsError CasesEdge CasesExample Requests8.17 GET /api/mobile/student/schedulePurposeSource EvidenceAuth and PermissionsResponseError CasesEdge CasesExample Requests8.18 GET /api/mobile/guardian/children/:id/schedulePurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge CasesExample Requests8.19 GET /api/mobile/student/schedule/todayPurposeSource EvidenceAuth and PermissionsRequestResponseCalendar statusSide EffectsCachingError CasesEdge CasesExample Requests8.20 GET /api/mobile/guardian/children/:id/schedule/todayPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesExample Requests8.21 GET /api/mobile/teacher/schedule/todayPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesExample Requests9. Flow Diagrams9.1 Route Ownership9.2 Request Sequence — Entry Upsert9.3 Error Branch — putEntry9.4 Auth and Permission Flow — Admin vs. Portal10. Pagination, Sorting, Filtering, and Search11. Caching, Jobs, and External Integrations13. 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