Skoolsewa - School Management System Docs
Developer ResourcesAcademic Calendar

Academic Calendar API Reference

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

Academic Calendar - API Reference

Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: Admin calendar generation/read/correction, admin holiday CRUD, admin week configuration, and the one mobile-audience calendar read owned by Academic Calendar.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/academic-calendar/days/academic-calendar.controller.ts, apps/api/src/modules/academic-calendar/holidays/school-holiday.controller.ts, apps/api/src/modules/academic-calendar/config/school-week-config.controller.ts, apps/api/src/modules/attendance/portal/calendar/calendar-portal.controller.tsRoutes, methods, guards, decorators, status codes.
DTOsapps/api/src/modules/academic-calendar/dto/calendar-day.dto.ts, holiday.dto.ts, week-config.dto.ts, apps/api/src/modules/attendance/portal/calendar/dto/portal-calendar.dto.tsRequest, query, response, validation, examples.
Servicescalendar-read.service.ts, calendar-generate.service.ts, calendar-day-write.service.ts, school-holiday.service.ts, holiday-day-apply.service.ts, school-week-config.service.ts, calendar-portal.service.tsBehavior, side effects, response mapping, errors.
Schemapackages/db/src/schema/school/calendar.tsIDs, enums, persisted fields, constraints.
Calendar vocabularypackages/db/src/calendar/calendar-contract.ts, packages/db/src/calendar/bikram-sambat.tsClassification/source enums, attendance-required set, holiday precedence, BS range.
Route mountingapps/api/src/modules/academic-calendar/academic-calendar.module.ts, apps/api/src/modules/mobile/mobile.module.ts, apps/api/src/config/swagger/swagger-documents.tsWhich routes are admin (/api/...) vs. mobile (/api/mobile/...), and which Swagger document each lands in.
Permissionspackages/db/src/authorization/permission-catalog.ts, packages/db/src/seed/seed-auth.tsAcademicCalendar/AcademicCalendarView codes, and which of the five seeded roles hold which.
Error registryapps/api/src/common/types/error-codes.tsEvery error code this module raises and its condition.
Shared envelope/paginationapps/api/src/common/dto/response-dto.ts, apps/api/src/common/dto/query.dto.ts, apps/api/src/common/utils/pagination.util.tsResponseDto shape, pagination defaults and caps.
Testsapps/api/src/modules/academic-calendar/__tests__/academic-calendar.integration.spec.tsConfirmed request/response behavior and edge cases.

2. Module Summary

FieldValue
Module nameAcademic Calendar
Module slugacademic-calendar
Primary actorsadmin/superadmin (full CRUD), staff/teacher/guardian/student (view screen only, via a separate permission code that gates zero API routes), any authenticated user (the portal read)
API surfacesadmin (/api/academic-sessions/:id/calendar, /api/holidays, /api/school/week-config, /api/calendar/today), mobile (/api/mobile/calendar)
Base route prefixes/api/academic-sessions/:sessionPublicId/calendar, /api/holidays, /api/school/week-config, /api/calendar/today, /api/mobile/calendar
Auth modelJWT for every route (JwtAuthGuard, RoleGuard); admin routes additionally require @Permissions("AcademicCalendar_..."); the mobile route requires only a valid JWT and asserts no role or permission at all
PersistencePostgreSQL — school_week_config, school_holidays, academic_calendar_days, school_holiday_days. Redis is used only by GET /api/calendar/upcoming (a 60-second read cache). No BullMQ or realtime dependency.
Runtime source of truthacademic_calendar_days.classification for whether a date is a school day; school_week_config row existence for whether the school has configured its week at all
Sibling docsBackend, features/flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
Academic sessionThe school year/term a calendar belongs to; resolved by academic_sessions.public_id in every calendar/holiday route's path.packages/db/src/schema/school/academic-sessions.ts (not itself part of this module)Every route under /calendar and /holidays.
Calendar day classificationschool_day, public_holiday, school_holiday, weekend, special_school_day, emergency_holiday, other — how a date behaves.packages/db/src/calendar/calendar-contract.ts:17-25CalendarDayDto.classification, SchoolHolidayDto.classification, query/body DTOs.
Calendar day sourceauto (generated from the weekend rule), seed (platform-populated holiday), school (admin-created holiday), admin_modified (an operator's own edit, protected from regeneration).calendar-contract.ts:37-42CalendarDayDto.source, SchoolHolidayDto.source.
Attendance-required classificationschool_day and special_school_day — the only classifications on which attendance is expected.calendar-contract.ts:62-65CalendarDayDto.attendanceExpected, PortalCalendarDayDto.attendanceExpected, derived, never stored.
Holiday precedenceemergency_holiday > public_holiday > school_holiday > special_school_day — which classification a day takes when two holidays cover it.calendar-contract.ts:78-83HolidayDayApplyService.recomputeDay.
Bikram Sambat (BS) dateThe Nepali calendar date stored on every calendar day; supported range BS 2000–2090.packages/db/src/calendar/bikram-sambat.ts:47-49CalendarDayDto.bsYear/bsMonth/bsDay, the month-view route, the portal route's query params.
Configured (week)Whether school_week_config has ever been saved. There is no seeded default; row existence is this flag.packages/db/src/schema/school/calendar.ts:85-99SchoolWeekConfigDto.configured.
Staleness (coverage)auto-sourced rows whose generated_from_weekend_days no longer matches the live school_week_config.weekend_days.calendar-read.service.ts:316-361CalendarCoverageDto.staleDays.
Admin-modifiedA day whose source was set to admin_modified by a PATCH; excluded from regeneration's delete predicate and from precedence recomputation.calendar-day-write.service.ts:101-105, holiday-day-apply.service.ts:106Day-detail/list responses.

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminPOST/api/academic-sessions/:sessionPublicId/calendar/generateAdmin (superadmin only, by seed)JwtAuthGuard, RoleGuardAcademicCalendar_CREATEAcademicCalendarControllerGenerate/repair the calendar.
AdminGET/api/academic-sessions/:sessionPublicId/calendarAdminJwtAuthGuard, RoleGuardAcademicCalendar_READAcademicCalendarControllerPaginated day list plus coverage.
AdminGET/api/academic-sessions/:sessionPublicId/calendar/months/:bsYear/:bsMonthAdminJwtAuthGuard, RoleGuardAcademicCalendar_READAcademicCalendarControllerOne BS month grid, with holiday names.
AdminGET/api/academic-sessions/:sessionPublicId/calendar/days/:dateAdminJwtAuthGuard, RoleGuardAcademicCalendar_READAcademicCalendarControllerSingle-day detail, with full holiday rows.
AdminPATCH/api/academic-sessions/:sessionPublicId/calendar/days/:dateAdminJwtAuthGuard, RoleGuardAcademicCalendar_UPDATEAcademicCalendarControllerCorrect one day.
AdminGET/api/academic-sessions/:sessionPublicId/holidaysAdminJwtAuthGuard, RoleGuardAcademicCalendar_READSchoolHolidayControllerList the holidays of a session.
AdminPOST/api/academic-sessions/:sessionPublicId/holidaysAdminJwtAuthGuard, RoleGuardAcademicCalendar_CREATESchoolHolidayControllerCreate a holiday.
AdminPATCH/api/holidays/:holidayIdAdminJwtAuthGuard, RoleGuardAcademicCalendar_UPDATESchoolHolidayControllerUpdate a holiday.
AdminDELETE/api/holidays/:holidayIdAdminJwtAuthGuard, RoleGuardAcademicCalendar_DELETESchoolHolidayControllerDelete a non-seed holiday.
AdminGET/api/school/week-configAdminJwtAuthGuard, RoleGuardAcademicCalendar_READSchoolWeekConfigControllerRead the school week configuration.
AdminPUT/api/school/week-configAdminJwtAuthGuard, RoleGuardAcademicCalendar_UPDATESchoolWeekConfigControllerConfigure the school week.
MobileGET/api/mobile/calendarAny authenticated user (pupil, guardian, teacher, staff)JwtAuthGuard, RoleGuardNoneCalendarPortalControllerShared audience read of one BS month.
AdminGET/api/calendar/todayAny authenticated userJwtAuthGuard, RoleGuardNoneCalendarTodayControllerToday in Asia/Kathmandu with its BS date and Devanagari rendering.
AdminGET/api/calendar/upcomingAny authenticated userJwtAuthGuard, RoleGuardNoneCalendarUpcomingControllerThe next N days of the school calendar, with multi-day events.

Every path above is read directly from the controller's @Controller() base plus its method decorator: AcademicCalendarController declares @Controller("academic-sessions/:sessionPublicId/calendar") (days/academic-calendar.controller.ts:35); SchoolHolidayController declares a bare @Controller() and states its own full path per method (holidays/school-holiday.controller.ts:42-112), which is why the list/create routes are session-scoped while update/delete are holiday-id-scoped with no common prefix; SchoolWeekConfigController declares @Controller("school/week-config") (config/school-week-config.controller.ts:23); CalendarPortalController declares @Controller("calendar") (calendar-portal.controller.ts:35), and the /api/mobile prefix is added by RouterModule.register() in apps/api/src/modules/mobile/mobile.module.ts:64-80, never by the controller decorator itself. The global prefix /api is applied in apps/api/src/main.ts:86.

AcademicCalendarModule is registered in ADMIN_MODULES (apps/api/src/config/swagger/swagger-documents.ts:88) and therefore appears in the admin Swagger document; CalendarPortalModule is registered as a MOBILE_CHILDREN leaf (apps/api/src/modules/mobile/mobile.module.ts:69) and therefore appears only in the mobile Swagger document, which is filtered by isMobilePath/isAdminPath in apps/api/src/config/swagger/swagger.shared.ts:29-38.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
Admin (calendar days)JwtAuthGuard, RoleGuardreq.user.idAcademicCalendar_CREATE/_READ/_UPDATENoEvery route is annotated with @Permissions(...).
Admin (holidays)JwtAuthGuard, RoleGuardreq.user, injected via @CurrentAdmin() on create for createdByUserIdAcademicCalendar_CREATE/_READ/_UPDATE/_DELETENoSame guard chain, per-method permission codes.
Admin (week config)JwtAuthGuard, RoleGuardreq.user, injected via @CurrentAdmin() on update for configuredByUserIdAcademicCalendar_READ/_UPDATENo—
Mobile (portal calendar)JwtAuthGuard, RoleGuardreq.user.id (unused by the handler itself)None declaredNo (JWT still required)Explained in detail below.
Today (/api/calendar/today)JwtAuthGuard, RoleGuardNone readNone declaredNo (JWT still required)Allowlisted as "CalendarTodayController.today"; the response is only today's date and its BS equivalent.
Upcoming (/api/calendar/upcoming)JwtAuthGuard, RoleGuardNone readNone declaredNo (JWT still required)Allowlisted as "CalendarUpcomingController.upcoming"; the service selects fields by hand and never reads the administrator note.

Which roles actually hold the admin permission codes: only superadmin is granted fullAccessPermissions, which includes every AcademicCalendar_* CRUD code (packages/db/src/seed/seed-auth.ts:214). The other four seeded roles — staff, teacher, guardian, student — hold only AcademicCalendarView_READ (seed-auth.ts:110,162,195), a code that gates the panel's calendar screen and appears on zero API routes (packages/db/src/authorization/permission-catalog.ts:234-243). In practice this means every admin route in this module is reachable only by superadmin out of the box, and a school that wants a non-superadmin operator to manage the calendar must grant AcademicCalendar_* to a custom role explicitly.

CalendarPortalController.monthView carries no @Permissions() decorator at all (calendar-portal.controller.ts:36-49) — the calendar names no person, so unlike every other portal route in the attendance module, it asserts no audience. Because RoleGuard otherwise treats a non-mobile-looking handler with no @Permissions() as an admin surface and throws, this handler is explicitly allowlisted in NO_PERMISSION_ADMIN_HANDLERS under the key "CalendarPortalController.monthView" (apps/api/src/common/authorization/role.guard.ts:228) and mirrored in test/structure/route-permissions.spec.ts. What actually makes the route safe with no permission check is the response shape: PortalCalendarDayDto enumerates every field by hand and is never built by spreading a row, so a column such as note (administrator-internal text) can never reach this response even if added to academic_calendar_days later (apps/api/src/modules/attendance/portal/calendar/dto/portal-calendar.dto.ts:71-93).

6. DTO and Model Reference

6.1 SchoolWeekConfigDto (response, GET/PUT school/week-config)

FieldTypeRequiredDefaultValidationExampleSource
configuredbooleanYes——falseweek-config.dto.ts:30
weekendDaysnumber[]Yes[] when unconfigured0–6, no repeats (service-checked on write)[5, 6]week-config.dto.ts:41-46
schoolDayCutoffMinutenumberYes1020 when unconfigured0..14401020week-config.dto.ts:49-50
versionnumber | nullYesnull when unconfigured—3week-config.dto.ts:52-54

weekendDays and schoolDayCutoffMinute are always present, even when configured is false — the service returns { configured: false, weekendDays: [], schoolDayCutoffMinute: 1020, version: null } rather than omitting them (school-week-config.service.ts:38-49). This is a deliberate fix for a real defect: the settings screen crashed on weekendDays is not iterable the first time it met an unconfigured school, which is every school, exactly once (week-config.dto.ts:33-40).

6.2 UpdateSchoolWeekConfigDto (request, PUT school/week-config)

FieldTypeRequiredDefaultValidationExampleSource
weekendDaysnumber[]Yes—@IsArray, max 6 items, each @IsInt @Min(0) @Max(6)[5, 6]week-config.dto.ts:73-78
schoolDayCutoffMinutenumberYes—@IsInt @Min(0) @Max(1440)1020week-config.dto.ts:80-83
expectedVersionnumber | nullYes (but may be null)—@IsOptional @IsInt @Min(0)3, or null on first saveweek-config.dto.ts:86-94

weekendDays may be an empty array — a seven-day residential school has no weekend, and this is a legal, saved configuration, not an incomplete one. Distinctness (no repeated day) is checked server-side, not by class-validator and not by a CHECK — an array CHECK cannot express "no duplicates" without a subquery (school-week-config.service.ts:143-149).

6.3 CalendarDayHolidayDto (response, nested)

FieldTypeRequiredDefaultValidationExampleSource
idstringYes——uuidcalendar-day.dto.ts:34
namestringYes——"Dashain"calendar-day.dto.ts:35
classificationenumYes—one of CALENDAR_DAY_CLASSIFICATION"public_holiday"calendar-day.dto.ts:36-37

6.4 CalendarDayDto (response, base shape)

FieldTypeRequiredDefaultValidationExampleSource
idstringYes——uuidcalendar-day.dto.ts:42
gregorianDatestringYes—YYYY-MM-DD"2026-04-15"calendar-day.dto.ts:43
bsYearnumberYes——2083calendar-day.dto.ts:44
bsMonthnumberYes—1..121calendar-day.dto.ts:45
bsDaynumberYes—1..321calendar-day.dto.ts:46
dayOfWeeknumberYes—0..6, 0 = Sunday3calendar-day.dto.ts:47-48
classificationenumYes—one of CALENDAR_DAY_CLASSIFICATION"school_day"calendar-day.dto.ts:49-50
sourceenumYes—one of CALENDAR_DAY_SOURCE"auto"calendar-day.dto.ts:51-52
eventNamestring | nullYesnull—"Indra Jatra" or nullcalendar-day.dto.ts:53-54
notestring | nullYesnullAdministrator-internal"closed, staff hearing" or nullcalendar-day.dto.ts:55-60
needsReviewbooleanYes——truecalendar-day.dto.ts:61
attendanceExpectedbooleanYes—Derived from classification; never storedtruecalendar-day.dto.ts:62-63
attendanceExistsbooleanYes—Whether attendance was already recorded for the datefalsecalendar-day.dto.ts:64-68
versionnumberYes——0calendar-day.dto.ts:69

6.5 MonthCalendarDayDto (response, GET .../calendar/months/:bsYear/:bsMonth)

CalendarDayDto plus:

FieldTypeRequiredDefaultValidationExampleSource
holidayNamesstring[]Yes[]—["Dashain", "School closed"]calendar-day.dto.ts:73-75

6.6 CalendarDayDetailDto (response, GET/PATCH .../calendar/days/:date)

CalendarDayDto plus:

FieldTypeRequiredDefaultValidationExampleSource
holidaysCalendarDayHolidayDto[]Yes[]Every holiday currently linked to this daysee §6.3calendar-day.dto.ts:77-81

6.7 CalendarCoverageDto (response, nested)

FieldTypeRequiredDefaultValidationExampleSource
expectedDaysnumberYes—Days in [startDate, endDate) — the session end is exclusive240calendar-day.dto.ts:84-87
presentDaysnumberYes—Actual row count240calendar-day.dto.ts:88
staleDaysnumberYes—auto/weekend rows generated under a weekend configuration no longer live0calendar-day.dto.ts:89-93

6.8 CalendarDayListDto (response, GET .../calendar)

FieldTypeRequiredDefaultValidationExampleSource
daysCalendarDayDto[]Yes[]——calendar-day.dto.ts:97
coverageCalendarCoverageDtoYes—Session-wide, independent of the list's own filters—calendar-day.dto.ts:98

6.9 GenerateCalendarDto (response, POST .../calendar/generate)

FieldTypeRequiredDefaultValidationExampleSource
generatednumberYes—Rows freshly inserted this run240calendar-day.dto.ts:102
repairednumberYes—Holiday day-links rebuilt/recomputed this run12calendar-day.dto.ts:103
deletednumberYes—Unlinked auto rows removed before reinsertion240calendar-day.dto.ts:104
coverageCalendarCoverageDtoYes—Post-generation coverage—calendar-day.dto.ts:105

6.10 ListCalendarDaysQueryDto (query, GET .../calendar)

Inherits pagination, page, size, sort, order, search from QueryDto (apps/api/src/common/dto/query.dto.ts:14-64) — sort/order/search are accepted but ignored, since the list is always ordered by gregorianDate.

FieldTypeRequiredDefaultValidationExampleSource
bsYearnumberNo—@IsInt2083calendar-day.dto.ts:114-118
bsMonthnumberNo—@IsInt @Min(1) @Max(12)1calendar-day.dto.ts:120-126
classificationenumNo—one of CALENDAR_DAY_CLASSIFICATION"weekend"calendar-day.dto.ts:128-131
fromstringNo—@IsISO8601"2026-04-15"calendar-day.dto.ts:133-136
tostringNo—@IsISO8601"2026-05-15"calendar-day.dto.ts:138-141

6.11 UpdateCalendarDayDto (request, PATCH .../calendar/days/:date)

FieldTypeRequiredDefaultValidationExampleSource
classificationenumNo—one of CALENDAR_DAY_CLASSIFICATION"special_school_day"calendar-day.dto.ts:150-153
eventNamestring | nullNo—@IsString @MaxLength(120)"Sports Day"calendar-day.dto.ts:155-163
notestring | nullNo—@IsString @MaxLength(500)"Reviewed"calendar-day.dto.ts:165-173
needsReviewbooleanNo—@IsBooleanfalsecalendar-day.dto.ts:175-178
expectedVersionnumberYes—@IsInt @Min(0)0calendar-day.dto.ts:180-185

Setting any field other than expectedVersion on an auto/seed day moves it to admin_modified, protecting it from the next regeneration and from holiday precedence recomputation.

6.12 SchoolHolidayDto (response)

FieldTypeRequiredDefaultValidationExampleSource
idstringYes——uuidholiday.dto.ts:23
namestringYes——"School closed"holiday.dto.ts:24
classificationenumYes—one of CALENDAR_DAY_CLASSIFICATION, never school_day in practice"school_holiday"holiday.dto.ts:25-26
fromDatestringYes——"2026-09-19"holiday.dto.ts:27
toDatestringYes——"2026-09-21"holiday.dto.ts:28
reasonstring | nullYesnullAdministrator-internal"Maintenance" or nullholiday.dto.ts:29-34
sourceenumYes—one of CALENDAR_DAY_SOURCE"school"holiday.dto.ts:35-36
versionnumberYes——0holiday.dto.ts:37

6.13 CreateSchoolHolidayDto (request, POST .../holidays)

FieldTypeRequiredDefaultValidationExampleSource
namestringYes—@IsString @MaxLength(120), backstopped by a no-pad/no-control-char CHECK"School closed"holiday.dto.ts:47-50
classificationenumYes—one of CALENDAR_DAY_CLASSIFICATION; school_day refused by the service"school_holiday"holiday.dto.ts:52-54
fromDatestringYes—@IsISO8601"2026-09-19"holiday.dto.ts:56-57
toDatestringYes—@IsISO8601; must be >= fromDate (service + CHECK)"2026-09-21"holiday.dto.ts:59
reasonstring | nullNonull@IsString @MaxLength(500)"Maintenance"holiday.dto.ts:61-69

Server-generated/overwritten fields not accepted from the client: id, source (always written as "school" for this route), createdByUserId (from @CurrentAdmin()), version (always 0 on insert).

6.14 UpdateSchoolHolidayDto (request, PATCH holidays/:holidayId)

Every field optional except expectedVersion.

FieldTypeRequiredDefaultValidationExampleSource
namestringNo—@IsString @MaxLength(120)"Renamed holiday"holiday.dto.ts:74-78
classificationenumNo—one of CALENDAR_DAY_CLASSIFICATION; school_day refused"public_holiday"holiday.dto.ts:80-83
fromDatestringNo—@IsISO8601"2026-09-20"holiday.dto.ts:85-88
toDatestringNo—@IsISO8601"2026-09-22"holiday.dto.ts:90-93
reasonstring | nullNo—@IsString @MaxLength(500)"Updated reason"holiday.dto.ts:95-103
expectedVersionnumberYes—@IsInt @Min(0)2holiday.dto.ts:105-108

6.15 PortalCalendarDayDto (response, GET /api/mobile/calendar)

FROZEN — enumerated by hand at CalendarPortalService.monthView's mapper, never a spread.

FieldTypeRequiredDefaultValidationExampleSource
gregorianDatestringYes——"2026-04-15"portal-calendar.dto.ts:84
bsYearnumberYes——2083portal-calendar.dto.ts:85
bsMonthnumberYes——1portal-calendar.dto.ts:86
bsDaynumberYes——1portal-calendar.dto.ts:87
dayOfWeeknumberYes—0..6, 0 = Sunday3portal-calendar.dto.ts:88
classificationenumYes—one of CALENDAR_DAY_CLASSIFICATION"school_day"portal-calendar.dto.ts:89-90
attendanceExpectedbooleanYes—Derived, never storedtrueportal-calendar.dto.ts:91
eventNamestring | nullNonull—"Indra Jatra" or nullportal-calendar.dto.ts:92

Deliberately absent: note, reason, source, needsReview, any holiday id, createdByUserId, configuredByUserId, version, generatedFromWeekendDays — every one of them either administrator-internal, an internal-provenance field, or a concurrency/staleness detail the audience side has no use for (portal-calendar.dto.ts:71-82).

6.16 CalendarPortalQueryDto (query, GET /api/mobile/calendar)

FieldTypeRequiredDefaultValidationExampleSource
sessionPublicIdstringNoThe session containing today@IsUUIDuuidportal-calendar.dto.ts:36-38
bsYearnumberNo (but see below)The current BS year@IsInt @Min(2000) @Max(2090)2083portal-calendar.dto.ts:54-60
bsMonthnumberNo (but see below)The current BS month@IsInt @Min(1) @Max(12)1portal-calendar.dto.ts:62-68

Both bsYear and bsMonth must be supplied together or omitted together — supplying only one is refused with CALENDAR_BS_DATE_INVALID rather than silently defaulting the missing half to the current year (calendar-portal.service.ts:38-55).

6.17 CalendarTodayDto (response, GET /api/calendar/today)

The six bs*/*Np fields are null together when the date is outside the converter's range. date and dayOfWeek are always present.

FieldTypeRequiredDefaultValidationExampleSource
datestringYes—Today in Asia/Kathmandu, YYYY-MM-DD"2026-09-29"calendar-today.dto.ts
bsstring | nullYes (nullable)—BS date, YYYY-MM-DD, zero-padded month and day"2083-06-13"calendar-today.dto.ts
bsYearnumber | nullYes (nullable)——2083calendar-today.dto.ts
bsMonthnumber | nullYes (nullable)—1-based, 1 to 126calendar-today.dto.ts
bsDaynumber | nullYes (nullable)——13calendar-today.dto.ts
dayOfWeeknumberYes—0..6, 0 = Sunday2calendar-today.dto.ts
bsDisplayNpstring | nullYes (nullable)—Devanagari BS date"आश्विन १३, २०८३"calendar-today.dto.ts
weekdayNpstring | nullYes (nullable)—Devanagari weekday"मंगलबार"calendar-today.dto.ts

6.18 Upcoming DTOs (GET /api/calendar/upcoming)

Source: dto/calendar-upcoming.dto.ts.

CalendarUpcomingQueryDto (query):

FieldTypeRequiredDefaultValidationExample
daysnumberNo7@IsInt @Min(1) @Max(31); a non-integer or out-of-range value is 400 VALIDATION_FAILED14

CalendarUpcomingDto (response):

FieldTypeRequiredNotes
todaystringYesThe school-local date, YYYY-MM-DD.
academicSessionNamestring | nullYes (nullable)The current session's name; null when none is current.
daysCalendarUpcomingDayDto[]YesExactly days consecutive dates; the first is today.
eventsCalendarUpcomingEventDto[]YesRuns of notable days; may be empty.

CalendarUpcomingDayDto:

FieldTypeNotes
datestringYYYY-MM-DD.
dayOfWeeknumber0–6, 0 = Sunday.
bsYear, bsMonth, bsDaynumber | nullThe BS date: the stored value when a calendar row exists, otherwise converted from the Gregorian date. null together when the date is outside the converter's range.
classificationCalendarDayClassification | nullSee §7. null when the calendar has no row for the date.
isOperatingDayboolean | nullWhether attendance is expected (school_day and special_school_day). null when there is no row; a date with no row is neither a school day nor a holiday.
eventNamestring | nullThe row's event name.

CalendarUpcomingEventDto:

FieldTypeNotes
namestring | nullThe shared event name; null for an unnamed run of a non-ordinary classification.
classificationCalendarDayClassificationThe shared classification.
startDate, endDatestringFirst and last date of the run, inclusive.
startBs, endBs{ year, month, day } | nullBS dates of the first and last day; null when either has no BS date.

The administrator note column is never selected and appears in no field.

7. Enum Reference

EnumValueMeaningRuntime EffectSource
CalendarDayClassificationschool_dayAn ordinary teaching day.Attendance expected.calendar-contract.ts:18
CalendarDayClassificationpublic_holidayA national/public holiday.Attendance not expected; outranks school_holiday/special_school_day in precedence.calendar-contract.ts:19
CalendarDayClassificationschool_holidayA school-specific closure.Attendance not expected.calendar-contract.ts:20
CalendarDayClassificationweekendA configured non-operating weekday.Attendance not expected; derived from school_week_config.weekend_days at generation time.calendar-contract.ts:21
CalendarDayClassificationspecial_school_dayA school day with different rules (e.g. sports day).Attendance is expected — the one non-school_day classification that requires it.calendar-contract.ts:22
CalendarDayClassificationemergency_holidayAn unplanned closure (e.g. weather).Attendance not expected; outranks every other holiday classification.calendar-contract.ts:23
CalendarDayClassificationotherAnything not covered above.Attendance not expected.calendar-contract.ts:24
CalendarDaySourceautoGenerated from the weekend rule.Rewritten by the next regeneration if still unlinked from any holiday.calendar-contract.ts:38
CalendarDaySourceseedPopulated by the platform's reference holiday data.Not deletable via DELETE /holidays/:id; reclassify instead.calendar-contract.ts:39
CalendarDaySourceschoolCreated by an administrator.Deletable.calendar-contract.ts:40
CalendarDaySourceadmin_modifiedAn operator's own edit to a day.Protected from regeneration and from precedence recomputation.calendar-contract.ts:41

classification = school_day on a school_holidays row is refused by SchoolHolidayService.assertNotSchoolDay, not by the database enum itself — see the backend doc's §16.6 for why this is a service rule rather than a CHECK.

8. Endpoint Reference

8.1 POST /api/academic-sessions/:sessionPublicId/calendar/generate

Purpose

Generates or repairs the calendar for one academic session. Call it once after a session is created and its week is configured, and again any time the week configuration changes, a holiday was created before the calendar existed, or the coverage response reports staleDays > 0. It is additive-with-repair and safe to call repeatedly.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/days/academic-calendar.controller.ts:43-56
Serviceapps/api/src/modules/academic-calendar/days/calendar-generate.service.ts
Schemapackages/db/src/schema/school/calendar.ts
Testsapps/api/src/modules/academic-calendar/__tests__/academic-calendar.integration.spec.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: AcademicCalendar_CREATE.
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: Result-idempotent, not diff-idempotent — every call deletes and reinserts every unlinked auto row, so a stale weekend snapshot self-heals on every run.

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYessessionPublicId — the academic session's uuid.
QueryNoNone.
BodyNo—

Response

{
  "message": "Academic calendar generated.",
  "data": {
    "generated": 240,
    "deleted": 0,
    "repaired": 0,
    "coverage": {
      "expectedDays": 240,
      "presentDays": 240,
      "staleDays": 0
    }
  }
}

Side Effects

  • Database writes: deletes unlinked auto rows of academic_calendar_days, inserts every missing date, re-links and recomputes every existing holiday of the session via HolidayDayApplyService.reapplyAllForSession.
  • No cache, job, realtime, notification, or external call.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404ACADEMIC_SESSION_NOT_FOUNDsessionPublicId does not resolve.The session does not exist.calendar-read.service.ts:74-79
409CALENDAR_WEEKEND_NOT_CONFIGUREDschool_week_config has no row.Configure the school week first.calendar-generate.service.ts:58-63
422CALENDAR_BS_DATE_OUT_OF_RANGEAny date in the session falls outside BS 2000–2090.The session dates are outside the supported range.calendar-generate.service.ts:74-79
422CALENDAR_BS_DATE_INVALIDAn internal BS-day validation failed during enumeration.Report as a bug — should be unreachable for a well-formed session.calendar-generate.service.ts:80-85

Edge Cases

  • A session with zero calendar rows and no existing holidays generates every day as auto, generated = expectedDays, deleted = 0, repaired = 0.
  • Calling generate a second time with nothing changed reports generated = presentDays, deleted = presentDays (every previously-inserted auto row is unlinked and gets rewritten) — the response's coverage is identical both times; only the diff counters differ (confirmed by the integration spec's "generates a full session and is idempotent (in RESULT) on a second run" case).
  • An admin_modified day is excluded from the delete predicate entirely — it is never deleted or reinserted, and generated/deleted both drop by exactly one relative to a run with no such day.
  • A holiday created before the first generate() call is picked up on the next run through reapplyAllForSession, not lost.

Example Requests

curl -X POST "$API_URL/api/academic-sessions/018f2f3e-.../calendar/generate" \
  -H "Authorization: Bearer TOKEN"

8.2 GET /api/academic-sessions/:sessionPublicId/calendar

Purpose

The paginated, filterable day list for one session, plus a session-wide coverage summary that is always computed independently of the list's own filters. Used to power a table/list view of the calendar, and to detect staleness after a week-configuration change.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/days/academic-calendar.controller.ts:58-74
Serviceapps/api/src/modules/academic-calendar/days/calendar-read.service.ts:150-205
DTOapps/api/src/modules/academic-calendar/dto/calendar-day.dto.ts:96-142
Schemapackages/db/src/schema/school/calendar.ts
Testsapps/api/src/modules/academic-calendar/__tests__/academic-calendar.integration.spec.ts

Auth and Permissions

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

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYessessionPublicId.
QueryNobsYear, bsMonth (1–12), classification, from/to (YYYY-MM-DD), plus pagination/page/size inherited from QueryDto (sort/order/search are accepted but ignored — the list is always ordered by gregorianDate).
BodyNo—

Response

{
  "message": "Calendar days fetched.",
  "data": {
    "days": [
      {
        "id": "018f2f40-...",
        "gregorianDate": "2026-04-15",
        "bsYear": 2083,
        "bsMonth": 1,
        "bsDay": 1,
        "dayOfWeek": 3,
        "classification": "public_holiday",
        "source": "seed",
        "eventName": "Nepali New Year",
        "note": null,
        "needsReview": false,
        "attendanceExpected": false,
        "attendanceExists": false,
        "version": 0
      }
    ],
    "coverage": {
      "expectedDays": 240,
      "presentDays": 240,
      "staleDays": 0
    }
  }
}

Side Effects

None — read-only.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404ACADEMIC_SESSION_NOT_FOUNDsessionPublicId does not resolve.The session does not exist.calendar-read.service.ts:74-79

Edge Cases

  • coverage is computed independently of every filter on the list — filtering to classification=weekend still returns the session's full expectedDays/presentDays/staleDays, not a filtered subset.
  • An empty filtered result returns days: [] with a populated, non-empty coverage block.
  • staleDays is scoped to source = 'auto' rows only — a holiday-covered day is never counted as stale, since its classification does not come from the weekend rule.

Example Requests

curl -X GET "$API_URL/api/academic-sessions/018f2f3e-.../calendar?bsYear=2083&bsMonth=1" \
  -H "Authorization: Bearer TOKEN"

8.3 GET /api/academic-sessions/:sessionPublicId/calendar/months/:bsYear/:bsMonth

Purpose

One Bikram Sambat month rendered as a grid, with each day carrying the names of every holiday covering it. This is the admin calendar screen's primary month view.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/days/academic-calendar.controller.ts:76-95
Serviceapps/api/src/modules/academic-calendar/days/calendar-read.service.ts:207-262
DTOapps/api/src/modules/academic-calendar/dto/calendar-day.dto.ts:72-75

Auth and Permissions

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

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYessessionPublicId, bsYear (2000–2090), bsMonth (1–12).
QueryNoNone.
BodyNo—

Response

{
  "message": "Calendar month fetched.",
  "data": [
    {
      "id": "018f2f40-...",
      "gregorianDate": "2026-04-15",
      "bsYear": 2083,
      "bsMonth": 1,
      "bsDay": 1,
      "dayOfWeek": 3,
      "classification": "public_holiday",
      "source": "seed",
      "eventName": "Nepali New Year",
      "note": null,
      "needsReview": false,
      "attendanceExpected": false,
      "attendanceExists": false,
      "version": 0,
      "holidayNames": ["Nepali New Year"]
    }
  ]
}

Side Effects

None — read-only.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
422CALENDAR_BS_DATE_OUT_OF_RANGEbsYear outside 2000–2090.Choose a year inside the supported range.calendar-read.service.ts:366-380
422CALENDAR_BS_DATE_INVALIDbsMonth outside 1–12.Fix the request.calendar-read.service.ts:380-385
404ACADEMIC_SESSION_NOT_FOUNDsessionPublicId does not resolve.The session does not exist.calendar-read.service.ts:74-79

Edge Cases

  • A BS month with zero generated rows returns an empty array [], never a 404 — the calendar simply has not been generated that far, or the session does not cover that month.
  • holidayNames is [] for a day covered by no holiday, never omitted.
  • The range check on bsYear/bsMonth runs before session resolution, so an out-of-range year is refused even for a session id that would itself not resolve.

Example Requests

curl -X GET "$API_URL/api/academic-sessions/018f2f3e-.../calendar/months/2083/1" \
  -H "Authorization: Bearer TOKEN"

8.4 GET /api/academic-sessions/:sessionPublicId/calendar/days/:date

Purpose

The single-day detail view — used by the day drawer/dialog when an administrator clicks a date, showing the full holiday rows (not just names) currently covering it.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/days/academic-calendar.controller.ts:97-109
Serviceapps/api/src/modules/academic-calendar/days/calendar-read.service.ts:118-148
DTOapps/api/src/modules/academic-calendar/dto/calendar-day.dto.ts:77-81

Auth and Permissions

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

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYessessionPublicId, date (YYYY-MM-DD).
QueryNoNone.
BodyNo—

Response

{
  "message": "Calendar day fetched.",
  "data": {
    "id": "018f2f40-...",
    "gregorianDate": "2026-04-15",
    "bsYear": 2083,
    "bsMonth": 1,
    "bsDay": 1,
    "dayOfWeek": 3,
    "classification": "public_holiday",
    "source": "seed",
    "eventName": "Nepali New Year",
    "note": null,
    "needsReview": false,
    "attendanceExpected": false,
    "attendanceExists": false,
    "version": 0,
    "holidays": [
      { "id": "018f2f50-...", "name": "Nepali New Year", "classification": "public_holiday" }
    ]
  }
}

Side Effects

None — read-only.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404ACADEMIC_SESSION_NOT_FOUNDsessionPublicId does not resolve.The session does not exist.calendar-read.service.ts:74-79
404CALENDAR_DAY_NOT_FOUNDNo row for that date in that session.Generate the calendar, or check the date.calendar-read.service.ts:102-116

Edge Cases

  • A date never generated for the session returns 404 CALENDAR_DAY_NOT_FOUND, never a synthesized default row — a missing row is never treated as a school day.
  • holidays is [] for a day covered by no holiday.
  • attendanceExists reflects real recorded attendance regardless of attendanceExpected — a day where attendance was recorded despite not being expected still reports attendanceExists: true.

Example Requests

curl -X GET "$API_URL/api/academic-sessions/018f2f3e-.../calendar/days/2026-04-15" \
  -H "Authorization: Bearer TOKEN"

8.5 PATCH /api/academic-sessions/:sessionPublicId/calendar/days/:date

Purpose

Corrects one calendar day — reclassifying it, adding an event name or an internal note, or flagging it for review. Every write moves an auto/seed day to admin_modified, which is what protects it from the next regeneration and from holiday precedence recomputation.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/days/academic-calendar.controller.ts:111-128
DTOapps/api/src/modules/academic-calendar/dto/calendar-day.dto.ts:144-186
Serviceapps/api/src/modules/academic-calendar/days/calendar-day-write.service.ts
Testsapps/api/src/modules/academic-calendar/__tests__/academic-calendar.integration.spec.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: AcademicCalendar_UPDATE.
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: Not idempotent by design — expectedVersion is required and a repeat with the same stale version is refused with 409.

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYessessionPublicId, date.
QueryNoNone.
BodyYesUpdateCalendarDayDto — see §6.11.
{
  "classification": "special_school_day",
  "eventName": "Sports Day",
  "expectedVersion": 0
}

Response

Same shape as §8.4's response — the freshly re-read CalendarDayDetailDto.

Side Effects

  • Database write: academic_calendar_days UPDATE, version-guarded; moves source to admin_modified when the existing row was auto or seed.
  • No cache, job, realtime, notification, or external call.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
422CALENDAR_TEXT_FORMAT_INVALIDeventName/note empty, padded, or control-character-bearing.Fix the payload.calendar-day-write.service.ts:143-154
404ACADEMIC_SESSION_NOT_FOUNDsessionPublicId does not resolve.The session does not exist.calendar-read.service.ts:74-79
404CALENDAR_DAY_NOT_FOUNDNo row for that date in that session.Reload the list.calendar-read.service.ts:102-116
409ATTENDANCE_VERSION_CONFLICTexpectedVersion does not match the stored version.Reload and retry.calendar-day-write.service.ts:125-131

Edge Cases

  • A body carrying only expectedVersion (no other field) still succeeds and still bumps version by exactly one — the service assigns needsReview to its own current value so the UPDATE is non-trivial and the trigger fires (calendar-day-write.service.ts:107-112).
  • Patching a day that is already admin_modified keeps it admin_modified — the source transition is one-directional through this route.
  • note and eventName can each be cleared by sending null.
  • The re-read after the write is inside the same transaction — the response is never an echo of the request body.

Example Requests

curl -X PATCH "$API_URL/api/academic-sessions/018f2f3e-.../calendar/days/2026-04-15" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"classification":"special_school_day","expectedVersion":0}'

8.6 GET /api/academic-sessions/:sessionPublicId/holidays

Purpose

Lists the holidays of one academic session, for the holiday management screen.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/holidays/school-holiday.controller.ts:46-63
Serviceapps/api/src/modules/academic-calendar/holidays/school-holiday.service.ts:92-137
DTOapps/api/src/modules/academic-calendar/dto/holiday.dto.ts:22-38

Auth and Permissions

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

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYessessionPublicId.
QueryNopagination, page, size (inherited from QueryDto; sort/order/search are accepted by the DTO but the list is always ordered by fromDate, then id).
BodyNo—

Response

The array is the top-level data; count/currentPage/totalPage are top-level envelope fields, not nested a second time inside data — the service deliberately returns { items, count, page, size } rather than { data, count }, because nesting a second data broke a screen that called holidays.map on an object (school-holiday.service.ts:88-91).

{
  "message": "Holidays fetched.",
  "data": [
    {
      "id": "018f2f50-...",
      "name": "Nepali New Year",
      "classification": "public_holiday",
      "fromDate": "2026-04-14",
      "toDate": "2026-04-14",
      "reason": null,
      "source": "seed",
      "version": 0
    }
  ],
  "count": 1,
  "currentPage": 1,
  "totalPage": 1
}

Side Effects

None — read-only.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404ACADEMIC_SESSION_NOT_FOUNDsessionPublicId does not resolve.The session does not exist.calendar-read.service.ts:74-79

Edge Cases

  • A session with no holidays returns data: [], count: 0, currentPage: 1, totalPage: 0.
  • pagination=false returns every holiday of the session up to PaginationUtil.UNPAGINATED_HARD_CAP (1000), with count computed from the returned row count rather than a separate COUNT(*).

Example Requests

curl -X GET "$API_URL/api/academic-sessions/018f2f3e-.../holidays" \
  -H "Authorization: Bearer TOKEN"

8.7 POST /api/academic-sessions/:sessionPublicId/holidays

Purpose

Creates a holiday and immediately applies it: every calendar day in [fromDate, toDate] is linked and recomputed in the same transaction. The range must already be fully generated.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/holidays/school-holiday.controller.ts:65-82
DTOapps/api/src/modules/academic-calendar/dto/holiday.dto.ts:46-70
Serviceapps/api/src/modules/academic-calendar/holidays/school-holiday.service.ts:139-192
Apply serviceapps/api/src/modules/academic-calendar/holidays/holiday-day-apply.service.ts:34-74
Testsapps/api/src/modules/academic-calendar/__tests__/academic-calendar.integration.spec.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: AcademicCalendar_CREATE.
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: Not idempotent — every call creates a new holiday row.

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYessessionPublicId.
QueryNoNone.
BodyYesCreateSchoolHolidayDto — see §6.13.
{
  "name": "School closed",
  "classification": "school_holiday",
  "fromDate": "2026-04-16",
  "toDate": "2026-04-18",
  "reason": "Maintenance"
}

Response

{
  "message": "Holiday created.",
  "data": {
    "id": "018f2f60-...",
    "name": "School closed",
    "classification": "school_holiday",
    "fromDate": "2026-04-16",
    "toDate": "2026-04-18",
    "reason": "Maintenance",
    "source": "school",
    "version": 0
  }
}

Side Effects

  • Database writes: school_holidays insert; school_holiday_days insert for every covered day; academic_calendar_days update for every day whose classification/source actually changes (via HolidayDayApplyService).
  • createdByUserId is set from the authenticated caller (@CurrentAdmin()).
  • No cache, job, realtime, notification, or external call.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
400CALENDAR_HOLIDAY_RANGE_INVALIDclassification = "school_day".A holiday cannot be a school day.school-holiday.service.ts:370-378
422CALENDAR_HOLIDAY_RANGE_INVALIDtoDate < fromDate (service or CHECK).Fix the date range.school-holiday.service.ts:381-388
422CALENDAR_TEXT_FORMAT_INVALIDname/reason empty, padded, or control-character-bearing.Fix the payload.school-holiday.service.ts:407-418
404ACADEMIC_SESSION_NOT_FOUNDsessionPublicId does not resolve.The session does not exist.calendar-read.service.ts:74-79
422CALENDAR_RANGE_OUTSIDE_SESSIONRange not wholly inside [startDate, endDate).Narrow the range to fit the session.school-holiday.service.ts:390-405
409ACADEMIC_CALENDAR_NOT_GENERATEDFewer calendar rows exist in the range than the range spans.Generate the calendar for that range first.holiday-day-apply.service.ts:50-60
500WRITE_RETURNED_NO_ROWDefensive — insert returned no row.Report as a bug.school-holiday.service.ts:176-181

Edge Cases

  • A single-day holiday is created with fromDate === toDate.
  • Creating a holiday whose range overlaps an existing holiday of a different classification does not error — both link to the shared days, and HOLIDAY_PRECEDENCE decides which classification the overlapping days show.
  • Creating a holiday over a range that includes an admin_modified day still links it (school_holiday_days gets the row), but recomputeDay leaves that day's classification/source untouched — the admin's edit wins.

Example Requests

curl -X POST "$API_URL/api/academic-sessions/018f2f3e-.../holidays" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"School closed","classification":"school_holiday","fromDate":"2026-04-16","toDate":"2026-04-18","reason":"Maintenance"}'

8.8 PATCH /api/holidays/:holidayId

Purpose

Updates a holiday's name, classification, range, or reason. Narrowing the range reverts the days that fall out; widening applies to the days that fall in; a classification change rewrites every day the holiday still owns.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/holidays/school-holiday.controller.ts:84-100
DTOapps/api/src/modules/academic-calendar/dto/holiday.dto.ts:72-108
Serviceapps/api/src/modules/academic-calendar/holidays/school-holiday.service.ts:194-324

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: AcademicCalendar_UPDATE.
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: Not idempotent — expectedVersion is required and a repeat with a stale version is refused with 409.

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYesholidayId.
QueryNoNone.
BodyYesUpdateSchoolHolidayDto — see §6.14.
{
  "toDate": "2026-04-20",
  "expectedVersion": 0
}

Response

Same shape as §8.7's response, reflecting the updated row.

Side Effects

  • Database writes: school_holidays update (version-guarded); every existing school_holiday_days link for this holiday deleted; new links inserted for the new range; recomputation of every day newly covered and every day that fell out.
  • No cache, job, realtime, notification, or external call.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404SCHOOL_HOLIDAY_NOT_FOUNDholidayId does not resolve.Verify the id.school-holiday.service.ts:213-218
400CALENDAR_HOLIDAY_RANGE_INVALIDclassification = "school_day".A holiday cannot be a school day.school-holiday.service.ts:370-378
422CALENDAR_HOLIDAY_RANGE_INVALIDtoDate < fromDate.Fix the date range.school-holiday.service.ts:220-223
422CALENDAR_TEXT_FORMAT_INVALIDname/reason invalid.Fix the payload.school-holiday.service.ts:407-418
422CALENDAR_RANGE_OUTSIDE_SESSIONNew range not wholly inside the session.Narrow the range.school-holiday.service.ts:241-245
409ATTENDANCE_VERSION_CONFLICTexpectedVersion stale.Reload and retry.school-holiday.service.ts:278-286
500WRITE_RETURNED_NO_ROWDefensive — update returned no row despite an affected row.Report as a bug.school-holiday.service.ts:286-292
409ACADEMIC_CALENDAR_NOT_GENERATEDThe widened range is not fully generated.Generate the calendar for the new range first.holiday-day-apply.service.ts:50-60

Edge Cases

  • Narrowing toDate reverts every day that fell out of the range to whatever classification the remaining holidays (or the weekend rule) dictate — never left classified as the old holiday.
  • Changing only classification (same range) rewrites every currently-linked day's classification/source, without touching the link set itself.
  • A patch body that changes nothing meaningful still sends name = existing.name internally so the UPDATE is non-trivial and the version bumps (school-holiday.service.ts:255).

Example Requests

curl -X PATCH "$API_URL/api/holidays/018f2f60-..." \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"toDate":"2026-04-20","expectedVersion":0}'

8.9 DELETE /api/holidays/:holidayId

Purpose

Deletes a holiday and recomputes every day it covered from whichever holidays remain. Refused for seeded holidays — reclassify instead of deleting, so the record of what the platform populated is preserved.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/holidays/school-holiday.controller.ts:102-112
Serviceapps/api/src/modules/academic-calendar/holidays/school-holiday.service.ts:332-366
Testsapps/api/src/modules/academic-calendar/__tests__/academic-calendar.integration.spec.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: AcademicCalendar_DELETE.
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: The second delete of the same id 404s — not naturally idempotent.

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsYesholidayId.
QueryNoNone.
BodyNo—

Response

{
  "message": "Holiday deleted.",
  "data": null
}

Side Effects

  • Database writes: school_holidays delete, cascading school_holiday_days via the foreign key; recomputation of every day that was linked.
  • No cache, job, realtime, notification, or external call.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
404SCHOOL_HOLIDAY_NOT_FOUNDholidayId does not resolve.Verify the id.school-holiday.service.ts:339-344
409SCHOOL_HOLIDAY_NOT_DELETABLEsource = 'seed'.Reclassify instead of deleting.school-holiday.service.ts:345-350

Edge Cases

  • Deleting a holiday that is the only one covering a set of days reverts each to weekend/school_day (against the live week configuration) and source: 'auto'.
  • Deleting a holiday that shares days with another still-active holiday leaves those days classified by whichever remaining holiday wins under HOLIDAY_PRECEDENCE.
  • Deleting a holiday that covers an admin_modified day leaves that day's classification/source untouched.

Example Requests

curl -X DELETE "$API_URL/api/holidays/018f2f60-..." \
  -H "Authorization: Bearer TOKEN"

8.10 GET /api/school/week-config

Purpose

Reads the school-wide weekend and attendance-cutoff configuration. Called before offering calendar generation, to decide whether to show the "configure your week first" prompt.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/config/school-week-config.controller.ts:27-40
Serviceapps/api/src/modules/academic-calendar/config/school-week-config.service.ts:32-51
DTOapps/api/src/modules/academic-calendar/dto/week-config.dto.ts:20-55
Testsapps/api/src/modules/academic-calendar/__tests__/academic-calendar.integration.spec.ts

Auth and Permissions

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

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsNoNone.
QueryNoNone.
BodyNo—

Response — unconfigured (no row exists)

{
  "message": "School week configuration fetched.",
  "data": {
    "configured": false,
    "weekendDays": [],
    "schoolDayCutoffMinute": 1020,
    "version": null
  }
}

Every field is present even though the school has never saved a configuration — weekendDays is [], schoolDayCutoffMinute is the shipped default (1020 = 17:00), and version is null so the client knows to send expectedVersion: null on its first save. This shape exists because the settings screen previously crashed with weekendDays is not iterable when the array was omitted entirely (school-week-config.service.ts:38-49).

Response — configured

{
  "message": "School week configuration fetched.",
  "data": {
    "configured": true,
    "weekendDays": [5, 6],
    "schoolDayCutoffMinute": 1020,
    "version": 2
  }
}

Side Effects

None — read-only.

Error Cases

None. This endpoint never errors; the unconfigured state is a valid response, not a failure.

Edge Cases

  • weekendDays: [] is returned both when the school is genuinely unconfigured and when a configured school has deliberately chosen a seven-day week — configured is the only field that distinguishes the two.

Example Requests

curl -X GET "$API_URL/api/school/week-config" \
  -H "Authorization: Bearer TOKEN"

8.11 PUT /api/school/week-config

Purpose

Upserts the single school-week-configuration row. Must be called successfully at least once before any session's calendar can be generated.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/config/school-week-config.controller.ts:42-58
DTOapps/api/src/modules/academic-calendar/dto/week-config.dto.ts:57-95
Serviceapps/api/src/modules/academic-calendar/config/school-week-config.service.ts:64-139
Testsapps/api/src/modules/academic-calendar/__tests__/academic-calendar.integration.spec.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: AcademicCalendar_UPDATE.
  • Guest support: No.
  • Rate limit: None module-specific.
  • Idempotency: Not idempotent by design — expectedVersion is required after the first save, so a repeat with a stale version is a 409.

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsNoNone.
QueryNoNone.
BodyYesUpdateSchoolWeekConfigDto — see §6.2.

Minimal valid request, first configuration:

{
  "weekendDays": [6],
  "schoolDayCutoffMinute": 1020,
  "expectedVersion": null
}

Full valid request, a later update:

{
  "weekendDays": [5, 6],
  "schoolDayCutoffMinute": 990,
  "expectedVersion": 2
}

Response

SchoolWeekConfigDto with configured: true — see §8.10's "configured" example.

Side Effects

  • Database write: school_week_config insert (first save) or version-guarded update.
  • configuredByUserId is set from the authenticated caller (@CurrentAdmin()) on every save.
  • No cache, job, realtime, notification, or external call.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
422CALENDAR_WEEKEND_INVALIDweekendDays contains a repeated day.Fix the payload.school-week-config.service.ts:68,143-149
409ATTENDANCE_VERSION_CONFLICTFirst save with a non-null expectedVersion, or a later save with a stale/mismatched one.Reload and retry, or send null if this is genuinely the first save.school-week-config.service.ts:77-84,113-129

Edge Cases

  • weekendDays: [] is accepted and saved — a seven-day school is a legal, complete configuration.
  • Sending expectedVersion: null when a row already exists is refused with the same ATTENDANCE_VERSION_CONFLICT a stale numeric version would produce — the ?? -1 fallback guarantees null can never accidentally match a real version.
  • schoolDayCutoffMinute accepts the full 0..1440 range, including 1440 (midnight-exclusive end of day) and 0 (a school with no cutoff grace at all).

Example Requests

curl -X PUT "$API_URL/api/school/week-config" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"weekendDays":[6],"schoolDayCutoffMinute":1020,"expectedVersion":null}'

8.12 GET /api/mobile/calendar

Purpose

The one calendar read every audience shares — pupil, guardian, teacher, and staff alike. Renders one Bikram Sambat month, defaulting to the session containing today and the BS month containing today when no query parameters are supplied. This is the mobile/portal counterpart of §8.3, with a deliberately narrower response shape and no permission check.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/attendance/portal/calendar/calendar-portal.controller.ts
Serviceapps/api/src/modules/attendance/portal/calendar/calendar-portal.service.ts
DTOapps/api/src/modules/attendance/portal/calendar/dto/portal-calendar.dto.ts
Mountingapps/api/src/modules/mobile/mobile.module.ts:64-80

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard — but no @Permissions() decorator.
  • Permission: None. Allowlisted in NO_PERMISSION_ADMIN_HANDLERS under "CalendarPortalController.monthView" (apps/api/src/common/authorization/role.guard.ts:228) so RoleGuard does not treat the missing decorator as a misconfigured admin route.
  • Guest support: No — a valid JWT is still required; there is simply no role/permission check beyond it.
  • Rate limit: None module-specific.
  • Idempotency: N/A (read).

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsNoNone.
QueryNosessionPublicId (defaults to the session containing today), bsYear+bsMonth together (default to the BS month containing today) — see §6.16.
BodyNo—

Response

{
  "message": "Calendar fetched.",
  "data": [
    {
      "gregorianDate": "2026-04-15",
      "bsYear": 2083,
      "bsMonth": 1,
      "bsDay": 1,
      "dayOfWeek": 3,
      "classification": "public_holiday",
      "attendanceExpected": false,
      "eventName": "Nepali New Year"
    }
  ]
}

Note the smaller field set relative to the admin day/month responses (§8.3, §8.4): no id, note, source, needsReview, attendanceExists, or version.

Side Effects

None — read-only.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
400CALENDAR_BS_DATE_INVALIDOnly one of bsYear/bsMonth supplied.Supply both, or neither.calendar-portal.service.ts:44-49
404ACADEMIC_SESSION_NOT_FOUNDsessionPublicId does not resolve, or no session contains today.Verify the id, or that a current session exists.calendar-portal.service.ts:105-134
409ACADEMIC_CALENDAR_NOT_GENERATEDThe resolved session/month has zero calendar rows.The calendar for that month has not been generated yet.calendar-portal.service.ts:83-88

Edge Cases

  • Omitting both bsYear and bsMonth resolves "today" through SchoolClockService, in the school's own timezone, and converts to BS via the same wrapper the admin side uses.
  • A pupil, parent, teacher, or staff member all receive the identical response shape for the same query — there is no audience-conditional field.
  • Requesting a month that is technically covered by the session but was never generated (e.g. the session exists but generate() was never called) returns 409 ACADEMIC_CALENDAR_NOT_GENERATED, not an empty array.

Example Requests

curl -X GET "$API_URL/api/mobile/calendar?bsYear=2083&bsMonth=1" \
  -H "Authorization: Bearer TOKEN"

8.13 GET /api/calendar/today

Purpose

Today's date in Asia/Kathmandu, its Bikram Sambat equivalent, and a Devanagari rendering of the BS date and the weekday. Every audience's page header renders it. It is pure computation: no database, cache, queue, Redis or MongoDB is touched.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/today/calendar-today.controller.ts
DTOapps/api/src/modules/academic-calendar/dto/calendar-today.dto.ts
Computationapps/api/src/utils/calendar/calendar-today.util.ts
Devanagari formatterpackages/db/src/calendar/bikram-sambat.ts (formatBsDateNepali)

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard — but no @Permissions() decorator.
  • Permission: None. Allowlisted in NO_PERMISSION_ADMIN_HANDLERS under "CalendarTodayController.today". The response holds only today's date in Asia/Kathmandu and its BS equivalent — no user, school or row data — so a session is the whole requirement.
  • Guest support: No — a valid JWT is required.
  • Rate limit: None module-specific.
  • Idempotency: N/A (read).

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsNoNone.
QueryNoNone.
BodyNo—

Response

{
  "message": "Today fetched.",
  "data": {
    "date": "2026-09-30",
    "bs": "2083-06-14",
    "bsYear": 2083,
    "bsMonth": 6,
    "bsDay": 14,
    "dayOfWeek": 3,
    "bsDisplayNp": "आश्विन १४, २०८३",
    "weekdayNp": "बुधबार"
  },
  "errorCode": null
}

Field semantics are in §6.17.

Side Effects

None — read-only. A GET is not recorded by the audit interceptor.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
401—No valid session.Sign in again.JwtAuthGuard

A date outside the converter's range is not an error; see the edge cases.

Edge Cases

  • The date is taken in Asia/Kathmandu (UTC+05:45), not in the server's timezone. The day rolls at Nepal midnight, which is 18:15 UTC.
  • dayOfWeek is computed from the Nepal date string through UTC arithmetic, so the server's timezone cannot move it. 0 is Sunday.
  • When the Nepal date is outside the converter's supported range (BS 2000–2090), the response is still 200. bs, bsYear, bsMonth, bsDay, bsDisplayNp and weekdayNp are all null together; date and dayOfWeek are present.
  • A BS day below 10 renders zero-padded in bsDisplayNp (for example आश्विन ०५, २०८३).

Example Requests

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

8.14 GET /api/calendar/upcoming

Purpose

The next N days of the school calendar, starting today, with each day's Bikram Sambat date, classification and whether attendance is expected, plus the multi-day events inside the window. Every audience's dashboard renders it.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/academic-calendar/upcoming/calendar-upcoming.controller.ts
Serviceapps/api/src/modules/academic-calendar/upcoming/calendar-upcoming.service.ts
DTOapps/api/src/modules/academic-calendar/dto/calendar-upcoming.dto.ts
Cache helperapps/api/src/common/utils/cache-race.util.ts

Auth and Permissions

  • Auth: JWT.
  • Guard chain: JwtAuthGuard, RoleGuard — but no @Permissions() decorator.
  • Permission: None. Allowlisted in NO_PERMISSION_ADMIN_HANDLERS under "CalendarUpcomingController.upcoming". The response names no person, enumerates its fields and never includes the administrator note, so a session is the whole requirement.
  • Guest support: No.
  • Idempotency: N/A (read).

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
ParamsNoNone.
QueryNodays — integer 1 to 31, default 7.
BodyNo—

Response

{
  "message": "Upcoming calendar days fetched.",
  "data": {
    "today": "2026-10-06",
    "academicSessionName": "2082/83",
    "days": [
      {
        "date": "2026-10-06",
        "dayOfWeek": 2,
        "bsYear": 2083,
        "bsMonth": 6,
        "bsDay": 20,
        "classification": "school_day",
        "isOperatingDay": true,
        "eventName": null
      },
      {
        "date": "2026-10-07",
        "dayOfWeek": 3,
        "bsYear": 2083,
        "bsMonth": 6,
        "bsDay": 21,
        "classification": "public_holiday",
        "isOperatingDay": false,
        "eventName": "Dashain"
      }
    ],
    "events": [
      {
        "name": "Dashain",
        "classification": "public_holiday",
        "startDate": "2026-10-07",
        "endDate": "2026-10-07",
        "startBs": { "year": 2083, "month": 6, "day": 21 },
        "endBs": { "year": 2083, "month": 6, "day": 21 }
      }
    ]
  },
  "errorCode": null
}

The example shows two of the requested days. Field semantics are in §6.18.

How the window is built

  • today is the school-local date, not the server's. The window is days consecutive dates from it, enumerated by UTC arithmetic on the date string, so it cannot fail near the edge of the Bikram Sambat range.
  • One calendar row is chosen per date from active academic sessions only. When two active sessions hold the same date, the current session's row wins, then the lower session id. Archived (inactive) sessions never contribute.
  • A date with no row has classification, isOperatingDay and eventName set to null; its BS fields are converted from the Gregorian date.
  • events groups consecutive days that share a classification and an event name. A day is notable when it has an event name, or its classification is neither school_day nor weekend. A day that is not notable, or has no row, ends the current run.

Side Effects

  • Database reads: the current session and the calendar rows between the first and last date of the window.
  • Cache: reads and writes calendar:upcoming:; see Caching. No writes to the database.

Caching

  • Key: calendar:upcoming: with the school-local date and the requested days; the value is the whole response.
  • TTL 60 seconds. No calendar write invalidates the key: a holiday create, update or delete, a day correction or a regeneration 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 a miss, so the route reads PostgreSQL. The write is fire-and-forget (cacheSetFast).
  • The response is the same for every caller, so one entry serves all audiences.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
400VALIDATION_FAILEDdays is not an integer, is below 1 or is above 31.Send a whole number from 1 to 31.CalendarUpcomingQueryDto
401AUTH_UNAUTHENTICATEDNo valid session.Sign in again.JwtAuthGuard

Edge Cases

  • Omitting days returns seven days; days=1 returns only today.
  • The response always holds exactly days entries, even when the calendar has not been generated: every day then carries null classification and events is empty.
  • A session with no current flag gives academicSessionName: null; calendar rows are still returned for active sessions.

Example Requests

curl -X GET "$API_URL/api/calendar/upcoming?days=14" \
  -H "Authorization: Bearer TOKEN"

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence — Generate

9.3 Error Branch — Holiday Create

9.4 Auth and Permission Flow — Admin vs. Portal

EndpointPagination TypeDefault SizeMax SizeSort FieldsFiltersResult Cap
GET .../calendarpage/size (QueryDto)20100Fixed: gregorianDate ascending (sort/order accepted but ignored)bsYear, bsMonth, classification, from, to1000 when pagination=false
GET .../holidayspage/size (QueryDto)20100Fixed: fromDate ascending, then id (sort/order accepted but ignored)None beyond session scope1000 when pagination=false
GET .../calendar/months/:bsYear/:bsMonthNone — whole monthN/AN/AFixed: bsDay ascendingPath params onlyBounded by month length (≤32)
GET /api/mobile/calendarNone — whole monthN/AN/AFixed: bsDay ascendingsessionPublicId, bsYear+bsMonthBounded by month length (≤32)

No endpoint in this module supports free-text search — the inherited QueryDto.search field is accepted (it is part of the shared base class) but has no effect on any query built by these services. No relevance scoring exists. Only GET /api/calendar/upcoming is cached; see §11.

11. Caching, Jobs, and External Integrations

IntegrationUsed?DetailsSource
Redis cacheOnly GET /api/calendar/upcomingcalendar:upcoming: keys (school-local date and days), 60-second TTL. The read goes through cacheGetFast (250 ms budget; a slow or unavailable Redis is a miss and the route reads PostgreSQL); the write is fire-and-forget. No calendar writer invalidates the key, so an edit is visible within 60 seconds. Every other route reads PostgreSQL directly.upcoming/calendar-upcoming.service.ts; common/utils/cache-race.util.ts
BullMQNoNo queue, job, or scheduled task.—
External APINonepali-date-converter is a local npm dependency, not a network call.packages/db/src/calendar/bikram-sambat.ts

13. Mandatory Deep API Documentation Pack

13.1 Route-by-Route Completeness Matrix

RouteController MethodDTOsService MethodGuardsPermissionsCacheJobsDB TouchesErrorsTestsDocumented?
POST /api/academic-sessions/:id/calendar/generateAcademicCalendarController.generateGenerateCalendarDto (response)CalendarGenerateService.generateJwtAuthGuard, RoleGuardAcademicCalendar_CREATEN/AN/Aacademic_calendar_days, school_holiday_days, school_week_config, academic_sessionsACADEMIC_SESSION_NOT_FOUND, CALENDAR_WEEKEND_NOT_CONFIGURED, CALENDAR_BS_DATE_OUT_OF_RANGE, CALENDAR_BS_DATE_INVALIDacademic-calendar.integration.spec.tsYes
GET /api/academic-sessions/:id/calendarAcademicCalendarController.listListCalendarDaysQueryDto, CalendarDayListDtoCalendarReadService.listJwtAuthGuard, RoleGuardAcademicCalendar_READN/AN/Aacademic_calendar_days, student_attendance_sessions/_records, school_week_configACADEMIC_SESSION_NOT_FOUNDacademic-calendar.integration.spec.tsYes
GET /api/academic-sessions/:id/calendar/months/:bsYear/:bsMonthAcademicCalendarController.monthViewMonthCalendarDayDto[]CalendarReadService.monthViewJwtAuthGuard, RoleGuardAcademicCalendar_READN/AN/Aacademic_calendar_days, school_holiday_days/school_holidaysCALENDAR_BS_DATE_OUT_OF_RANGE, CALENDAR_BS_DATE_INVALID, ACADEMIC_SESSION_NOT_FOUND—Yes
GET /api/academic-sessions/:id/calendar/days/:dateAcademicCalendarController.dayDetailCalendarDayDetailDtoCalendarReadService.dayDetailJwtAuthGuard, RoleGuardAcademicCalendar_READN/AN/Aacademic_calendar_days, school_holiday_days/school_holidays, attendance-existsACADEMIC_SESSION_NOT_FOUND, CALENDAR_DAY_NOT_FOUNDacademic-calendar.integration.spec.tsYes
PATCH /api/academic-sessions/:id/calendar/days/:dateAcademicCalendarController.patchDayUpdateCalendarDayDto, CalendarDayDetailDtoCalendarDayWriteService.patchJwtAuthGuard, RoleGuardAcademicCalendar_UPDATEN/AN/Aacademic_calendar_daysCALENDAR_TEXT_FORMAT_INVALID, ACADEMIC_SESSION_NOT_FOUND, CALENDAR_DAY_NOT_FOUND, ATTENDANCE_VERSION_CONFLICTacademic-calendar.integration.spec.tsYes
GET /api/academic-sessions/:id/holidaysSchoolHolidayController.listQueryDto, SchoolHolidayDto[]SchoolHolidayService.listJwtAuthGuard, RoleGuardAcademicCalendar_READN/AN/Aschool_holidaysACADEMIC_SESSION_NOT_FOUND—Yes
POST /api/academic-sessions/:id/holidaysSchoolHolidayController.createCreateSchoolHolidayDto, SchoolHolidayDtoSchoolHolidayService.createJwtAuthGuard, RoleGuardAcademicCalendar_CREATEN/AN/Aschool_holidays, school_holiday_days, academic_calendar_daysCALENDAR_HOLIDAY_RANGE_INVALID, CALENDAR_TEXT_FORMAT_INVALID, CALENDAR_RANGE_OUTSIDE_SESSION, ACADEMIC_CALENDAR_NOT_GENERATED, WRITE_RETURNED_NO_ROWacademic-calendar.integration.spec.tsYes
PATCH /api/holidays/:holidayIdSchoolHolidayController.updateUpdateSchoolHolidayDto, SchoolHolidayDtoSchoolHolidayService.updateJwtAuthGuard, RoleGuardAcademicCalendar_UPDATEN/AN/Aschool_holidays, school_holiday_days, academic_calendar_daysSCHOOL_HOLIDAY_NOT_FOUND, CALENDAR_HOLIDAY_RANGE_INVALID, CALENDAR_RANGE_OUTSIDE_SESSION, ATTENDANCE_VERSION_CONFLICT, WRITE_RETURNED_NO_ROW, ACADEMIC_CALENDAR_NOT_GENERATED—Yes
DELETE /api/holidays/:holidayIdSchoolHolidayController.remove—SchoolHolidayService.removeJwtAuthGuard, RoleGuardAcademicCalendar_DELETEN/AN/Aschool_holidays, school_holiday_days (cascade), academic_calendar_daysSCHOOL_HOLIDAY_NOT_FOUND, SCHOOL_HOLIDAY_NOT_DELETABLEacademic-calendar.integration.spec.tsYes
GET /api/school/week-configSchoolWeekConfigController.getSchoolWeekConfigDtoSchoolWeekConfigService.getJwtAuthGuard, RoleGuardAcademicCalendar_READN/AN/Aschool_week_confignoneacademic-calendar.integration.spec.tsYes
PUT /api/school/week-configSchoolWeekConfigController.updateUpdateSchoolWeekConfigDto, SchoolWeekConfigDtoSchoolWeekConfigService.updateJwtAuthGuard, RoleGuardAcademicCalendar_UPDATEN/AN/Aschool_week_configCALENDAR_WEEKEND_INVALID, ATTENDANCE_VERSION_CONFLICTacademic-calendar.integration.spec.tsYes
GET /api/mobile/calendarCalendarPortalController.monthViewCalendarPortalQueryDto, PortalCalendarDayDto[]CalendarPortalService.monthViewJwtAuthGuard, RoleGuardNone (allowlisted)N/AN/Aacademic_calendar_days, academic_sessionsCALENDAR_BS_DATE_INVALID, ACADEMIC_SESSION_NOT_FOUND, ACADEMIC_CALENDAR_NOT_GENERATED—Yes
GET /api/calendar/upcomingCalendarUpcomingController.upcomingCalendarUpcomingQueryDto, CalendarUpcomingDtoCalendarUpcomingService.upcomingJwtAuthGuard, RoleGuardNone (allowlisted)calendar:upcoming: (60 s)N/Aacademic_calendar_days, academic_sessionsVALIDATION_FAILEDupcoming/__tests__Yes

13.2 Request/Response Exhaustiveness

Every applicable example type is present in §8 per endpoint: minimal/full request bodies (holiday create, week-config update), public/guest behavior (not applicable — every route requires a JWT), authenticated and admin requests (all endpoints), success responses with full envelopes, empty-list responses (§8.2, §8.6, §8.3), validation errors, domain errors, and version-conflict errors. 401/403 are not shown as JSON bodies because they are produced by the shared guard stack, not by any service in this module, and their shape is documented once in the platform's own auth documentation rather than duplicated per endpoint here.

13.3 API Diagram Pack

See §9 for the route-ownership graph, the generate sequence, the holiday-create error decision tree, and the admin-vs-portal auth flow. A cache-flow diagram is omitted — no route in this module reads or writes a cache. An async/job-flow diagram is omitted for the same reason regarding BullMQ. A realtime/event-flow diagram is omitted — no event is ever emitted.

13.4 Consumer Integration Notes

ConsumerRequired KnowledgeFailure HandlingContract Stability
Admin panel (calendar screen)Must call GET /school/week-config before offering generation; must read coverage.staleDays to prompt regeneration; must send expectedVersion on every PATCH/PUT.Show CALENDAR_WEEKEND_NOT_CONFIGURED as a first-run prompt, not a generic error; show ATTENDANCE_VERSION_CONFLICT as a reload prompt.Stable.
Mobile app (calendar tab)Uses GET /api/mobile/calendar with no query params for "this month"; must supply bsYear+bsMonth together if the user navigates to a different month.ACADEMIC_CALENDAR_NOT_GENERATED should render as "not yet available" rather than a hard error, since it is reachable in ordinary use (a school that has not generated far enough ahead).Stable — the response shape is explicitly frozen (PortalCalendarDayDto's own docblock).
QAThe integration spec (academic-calendar.integration.spec.ts) is the canonical source of confirmed edge cases: additive-with-repair idempotency, admin_modified preservation, BS-boundary refusal, two-holiday precedence, seed-delete refusal, and staleness reporting.Reproduce any reported discrepancy against that spec's fixtures first.Stable.
Internal serviceNo other module currently calls into this module's services directly except through HTTP; the attendance module reads academic_calendar_days/calendar-contract.ts directly at the database/TypeScript level, not through this API.N/AStable at the schema/contract level (calendar-contract.ts), which is the intended integration surface for other backend modules.

13.5 API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
Pagination stylePage/size (QueryDto) on both list endpointsCursor paginationMatches every other list endpoint in this codebase; the calendar's natural page count (one academic year, ≤365 rows) never approaches cursor-pagination's motivating scale.None significant at this table size.The UNPAGINATED_HARD_CAP (1000) still bounds an unpaginated request.
Auth mode on the portal routeJWT required, but no @Permissions()Require AcademicCalendarView_READ explicitlyThe calendar names no person; requiring a specific permission would need granting it to every audience role, which is exactly what AcademicCalendarView_READ already models for the panel screen — duplicating that check on the API route adds a second place to keep in sync with no additional safety, since the DTO shape is the actual control.A future column added to academic_calendar_days could leak if a mapper is ever changed to spread the row instead of enumerating fields.PortalCalendarDayDto's docblock states the constraint explicitly; a reviewer must catch a future spread.
Error shapeThe shared { success, message, data, errorCode } envelope (ResponseDto)A calendar-specific envelopeConsistency with every other module in the codebase; frontend/mobile error handling is already written against this shape.None.—
Reusing ATTENDANCE_VERSION_CONFLICT rather than a calendar-specific version-conflict codeShared code across CalendarDayWriteService, SchoolHolidayService, and SchoolWeekConfigServiceA dedicated CALENDAR_VERSION_CONFLICT codeThe condition and client action are identical to the attendance module's own version conflicts — a stale optimistic-concurrency token — and a second code for the same meaning would only add an entry to memorize.A client that branches on error code text (rather than HTTP status) for calendar-specific messaging would need to know the code is shared.Documented here and in the backend doc's error table.

13.6 API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
Adding a field to academic_calendar_daysAdmin panel (if the field should be admin-visible)CalendarDayDto/CalendarDayDetailDto/MonthCalendarDayDto must add the field explicitly (no spread exists to add it automatically)New column, likely nullableYes, a schema migrationThe portal DTO (PortalCalendarDayDto) must not automatically gain the field — a reviewer must decide inclusion explicitly, since its mapper enumerates by hand.
Wiring seedHolidaysForRange into generationAdmin panel (holiday list would show new seed rows after the next generate)CalendarGenerateService.generate or a new call site would need to invoke it and insert school_holidays rows with source: 'seed'New school_holidays/school_holiday_days/academic_calendar_days rows for previously-unseeded sessionsNo — no schema change, only a new call siteBackward compatible; existing sessions would gain seed holidays on their next generate() call. See the backend doc §16.6 and the report accompanying this documentation set.
Changing HOLIDAY_PRECEDENCE orderEvery session with overlapping holidaysHolidayDayApplyService.recomputeDay's output changes for any day currently covered by more than one holidayNo schema change; existing rows are only correct after the next generate()/holiday-write recomputationNoWould require re-running recomputation across every existing session to bring stored classifications in line with the new order — not automatic.

14. Zero-Omission API Checklist

  • Every controller route is documented (§4, §8).
  • Every parent route prefix and runtime URL is documented, including the mobile-mounting distinction (§4, §9.1).
  • Every DTO field, nested field, enum, default, transform, and validator is documented (§6, §7).
  • Every response field, nullable field, generated field, and omitted raw entity field is documented, including the portal DTO's deliberate omissions (§6.15).
  • Every auth, guard, permission, role, and the one no-permission branch is documented (§5, §9.4).
  • Every success, validation, auth, permission, not-found, conflict, and server-error branch is documented (§8, §13.1).
  • Every database read/write is documented; no cache, queue, realtime, notification, or external call exists to document (§11).
  • Every route has examples for minimal request, full request (where applicable), success response, and representative failures (§8).
  • Every endpoint family has route, sequence, activity, and error diagrams (§9).
  • Every tradeoff and compatibility risk is documented (§13.5, §13.6).
  • The API 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, including the holiday list's top-level (not double-nested) pagination fields.
  • Every error code is documented.
  • Every auth guard and permission is documented, including the one deliberately permission-less route.
  • The only cache key is calendar:upcoming:; no queue job or realtime event exists in this module.
  • Every diagram matches the current code.
  • The API doc links to backend and features/flows.

See Also

On this page

Academic Calendar - API Reference1. Documentation Evidence2. Module Summary3. Concepts and Terminology4. API Surface Map5. Auth, Identity, and Permissions6. DTO and Model Reference6.1 SchoolWeekConfigDto (response, GET/PUT school/week-config)6.2 UpdateSchoolWeekConfigDto (request, PUT school/week-config)6.3 CalendarDayHolidayDto (response, nested)6.4 CalendarDayDto (response, base shape)6.5 MonthCalendarDayDto (response, GET .../calendar/months/:bsYear/:bsMonth)6.6 CalendarDayDetailDto (response, GET/PATCH .../calendar/days/:date)6.7 CalendarCoverageDto (response, nested)6.8 CalendarDayListDto (response, GET .../calendar)6.9 GenerateCalendarDto (response, POST .../calendar/generate)6.10 ListCalendarDaysQueryDto (query, GET .../calendar)6.11 UpdateCalendarDayDto (request, PATCH .../calendar/days/:date)6.12 SchoolHolidayDto (response)6.13 CreateSchoolHolidayDto (request, POST .../holidays)6.14 UpdateSchoolHolidayDto (request, PATCH holidays/:holidayId)6.15 PortalCalendarDayDto (response, GET /api/mobile/calendar)6.16 CalendarPortalQueryDto (query, GET /api/mobile/calendar)6.17 CalendarTodayDto (response, GET /api/calendar/today)6.18 Upcoming DTOs (GET /api/calendar/upcoming)7. Enum Reference8. Endpoint Reference8.1 POST /api/academic-sessions/:sessionPublicId/calendar/generatePurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.2 GET /api/academic-sessions/:sessionPublicId/calendarPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.3 GET /api/academic-sessions/:sessionPublicId/calendar/months/:bsYear/:bsMonthPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.4 GET /api/academic-sessions/:sessionPublicId/calendar/days/:datePurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.5 PATCH /api/academic-sessions/:sessionPublicId/calendar/days/:datePurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.6 GET /api/academic-sessions/:sessionPublicId/holidaysPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.7 POST /api/academic-sessions/:sessionPublicId/holidaysPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.8 PATCH /api/holidays/:holidayIdPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.9 DELETE /api/holidays/:holidayIdPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.10 GET /api/school/week-configPurposeSource EvidenceAuth and PermissionsRequestResponse — unconfigured (no row exists)Response — configuredSide EffectsError CasesEdge CasesExample Requests8.11 PUT /api/school/week-configPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.12 GET /api/mobile/calendarPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.13 GET /api/calendar/todayPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.14 GET /api/calendar/upcomingPurposeSource EvidenceAuth and PermissionsRequestResponseHow the window is builtSide EffectsCachingError CasesEdge CasesExample Requests9. Flow Diagrams9.1 Route Ownership9.2 Request Sequence — Generate9.3 Error Branch — Holiday Create9.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