Academic Calendar Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for Academic Calendar.
Academic Calendar Features and Flows
1. Documentation Evidence
| Source Type | Files or Docs | What Was Extracted |
|---|---|---|
| Backend | Sibling backend doc, calendar-generate.service.ts, holiday-day-apply.service.ts, school-week-config.service.ts | Generation algorithm, precedence recomputation, the unconfigured-state contract. |
| API | Sibling API doc, academic-calendar.controller.ts, school-holiday.controller.ts, school-week-config.controller.ts, calendar-portal.controller.ts | Route surface, actors, auth. |
| Schema | packages/db/src/schema/school/calendar.ts, packages/db/src/calendar/calendar-contract.ts | Classification/source vocabulary, holiday precedence, attendance-required set. |
| Tests | apps/api/src/modules/academic-calendar/__tests__/academic-calendar.integration.spec.ts | Confirmed flow behavior and edge cases. |
2. Feature Summary
| Field | Value |
|---|---|
| Module | Academic Calendar |
| Submodule | Week configuration, day generation/read/correction, holidays, audience read |
| Primary user value | An administrator states the school's weekend once, generates a full academic year's calendar in one click, corrects individual days by hand, and manages holidays — while every pupil, parent, and staff member can see the same calendar with no setup at all. |
| Actors | Admin (superadmin by default seed), staff, teacher, guardian, student — the last four only through the read-only screen and the shared mobile read. |
| Main entry points | POST .../calendar/generate, the calendar/holiday admin panel screens, GET /api/mobile/calendar, GET /api/calendar/today. |
| Main outputs | A materialised academic_calendar_days row per date, school_holidays records, the school-wide school_week_config row, and the derived attendanceExpected flag every downstream attendance flow reads. |
| Related docs | API, backend. |
3. Actor Matrix
| Actor | Can Do | Cannot Do | Auth Requirement | Notes |
|---|---|---|---|---|
Admin (superadmin) | Configure the week, generate/repair the calendar, correct any day, create/update/delete holidays. | Delete a seeded holiday (must reclassify instead); classify a holiday as school_day. | Admin JWT + AcademicCalendar_* permission codes. | Only superadmin holds these codes by default seed — staff/teacher/guardian/student do not. |
| Staff / Teacher | View the calendar screen. | Any write to the calendar, holidays, or week config — they hold no AcademicCalendar_* code. | JWT + AcademicCalendarView_READ (gates the panel screen only). | AcademicCalendarView_READ appears on zero API routes; the actual data comes from the same no-permission mobile route every audience uses. |
| Guardian | View the calendar screen; read GET /api/mobile/calendar for their household's context. | Any write. | JWT + AcademicCalendarView_READ. | Same projection as every other audience — the calendar names no person, so there is no per-child scoping to apply. |
| Student | View the calendar screen; read GET /api/mobile/calendar. | Any write. | JWT + AcademicCalendarView_READ. | Same as guardian. |
| Any authenticated user (mobile) | GET /api/mobile/calendar — one BS month, defaulting to "now". | See any administrator-internal field (note, reason, source, needsReview, any id, version). | Valid JWT only — no permission or role check at all. | Safety comes from the frozen, hand-enumerated response DTO, not from a guard. |
| Any authenticated user (header) | GET /api/calendar/today — today's date in Nepal, its Bikram Sambat date, and a Devanagari rendering of the BS date and the weekday. | Read anything beyond the date: the route touches no table. | Valid JWT only — no permission or role check. | Every audience's header shows it. When the date is outside the converter's range (BS 2000–2090) the response is still successful and the BS and Devanagari fields are null and only the Gregorian date and weekday number remain. |
4. Capability Matrix
| Capability | Surface | Actor | Route/Trigger | State Read | State Written | Linked API Section |
|---|---|---|---|---|---|---|
| Configure the school week | Admin | Admin | PUT /api/school/week-config | Existing config row, if any | school_week_config insert/update | API §8.11 |
| Read the school week configuration | Admin | Admin | GET /api/school/week-config | school_week_config | — | API §8.10 |
| Generate/repair the calendar | Admin | Admin | POST /api/academic-sessions/:id/calendar/generate | academic_sessions, school_week_config, existing calendar/holiday rows | academic_calendar_days, school_holiday_days | API §8.1 |
| List calendar days with coverage | Admin | Admin | GET /api/academic-sessions/:id/calendar | academic_calendar_days, attendance existence, school_week_config | — | API §8.2 |
| View a BS month grid | Admin | Admin | GET /api/academic-sessions/:id/calendar/months/:y/:m | academic_calendar_days, school_holiday_days/school_holidays | — | API §8.3 |
| View one day's detail | Admin | Admin | GET /api/academic-sessions/:id/calendar/days/:date | academic_calendar_days, holidays, attendance existence | — | API §8.4 |
| Correct one day | Admin | Admin | PATCH /api/academic-sessions/:id/calendar/days/:date | Existing day row | academic_calendar_days | API §8.5 |
| List holidays of a session | Admin | Admin | GET /api/academic-sessions/:id/holidays | school_holidays | — | API §8.6 |
| Create a holiday | Admin | Admin | POST /api/academic-sessions/:id/holidays | academic_sessions, academic_calendar_days in range | school_holidays, school_holiday_days, academic_calendar_days | API §8.7 |
| Update a holiday | Admin | Admin | PATCH /api/holidays/:id | Existing holiday, academic_sessions, calendar days | Same three tables | API §8.8 |
| Delete a holiday | Admin | Admin | DELETE /api/holidays/:id | Existing holiday, its links | school_holidays, school_holiday_days (cascade), academic_calendar_days | API §8.9 |
| View this month's calendar | Any audience | Any authenticated user | GET /api/mobile/calendar | academic_calendar_days | — | API §8.12 |
| Show today's date in the page header | Any audience | Any authenticated user | GET /api/calendar/today | The clock only — no table | — | API §8.13 |
| See the next days of the calendar | Any audience | Any authenticated user | GET /api/calendar/upcoming?days=N | academic_calendar_days, academic_sessions (60 s cache) | — | API §8.14 |
5. User-Facing Flows
5.1 Configure the school week (first time)
Summary
Before anything else can happen, an administrator states which days the school does not operate and, optionally, adjusts the attendance cutoff time. There is deliberately no default — every school does this once, explicitly.
Preconditions
- Caller holds
AcademicCalendar_UPDATE. - No preconditions on data — this is legitimately the very first action available in a fresh school.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Admin | Opens the week-configuration screen. | GET .../school/week-config returns configured: false with the shipped defaults. | API §8.10 |
| 2 | Admin | Selects weekend days (e.g. Saturday only, or Sunday+Saturday) and a cutoff time. | Local form state; nothing persisted yet. | — |
| 3 | Admin | Saves. | PUT .../school/week-config with expectedVersion: null. | API §8.11 |
| 4 | Backend | Validates no duplicate weekend day; inserts the single row. | configured: true, version: 0. | Backend §6.4 |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
| Duplicate weekend day | e.g. [6, 6] | Reject | CALENDAR_WEEKEND_INVALID |
| Seven-day school | weekendDays: [] | Accepted as a legal, complete configuration | configured: true, weekendDays: [] |
| First save claims a version | expectedVersion is a number, not null, on the first save | Reject — the caller cannot have read a version that never existed | ATTENDANCE_VERSION_CONFLICT |
| Read before any save | GET with no row present | Never errors; returns every field with shipped defaults | configured: false, weekendDays: [], schoolDayCutoffMinute: 1020, version: null |
5.2 Generate the academic calendar
Summary
Once the week is configured, an administrator generates the full calendar for a session in one action — every Gregorian date from the session's start up to (but not including) its end becomes a row, classified as weekend or school_day from the configured weekend, with its Bikram Sambat date computed and stored.
Preconditions
- Caller holds
AcademicCalendar_CREATE. school_week_confighas a saved row (§5.1).- The academic session exists and every date in its range falls within Bikram Sambat 2000–2090.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Admin | Opens a session's calendar screen for the first time. | Empty list; coverage.presentDays = 0. | API §8.2 |
| 2 | Admin | Clicks "Generate calendar". | POST .../calendar/generate. | API §8.1 |
| 3 | Backend | Reads the week config; refuses if absent. | CALENDAR_WEEKEND_NOT_CONFIGURED, or continue. | Backend §6.2 |
| 4 | Backend | Enumerates every date, converts each to BS, classifies by weekend membership, and inserts. | academic_calendar_days fully populated. | Backend §6.2 |
| 5 | Backend | Reapplies every already-existing holiday of the session onto the freshly-inserted rows. | Holidays created before generation are not lost. | Backend §6.6 |
| 6 | Admin | Sees the full grid, with coverage.presentDays === coverage.expectedDays. | — | API §8.2 |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
| Week not configured | school_week_config has no row | Refuse | CALENDAR_WEEKEND_NOT_CONFIGURED |
| Session dates beyond BS 2090 | Any enumerated date converts out of range | Refuse the whole operation, nothing partially written | CALENDAR_BS_DATE_OUT_OF_RANGE |
| Repeat generation, nothing changed | Called a second time with no admin edits | Every unlinked auto row is deleted and reinserted; result is the same coverage, different diff counters | generated/deleted both equal the previous run's row count |
| Repeat generation after week change | school_week_config changed since the last generate | Every auto row is rewritten against the new weekend rule; staleDays in the pre-generation coverage read drops to 0 after | — |
An admin_modified day exists | An operator previously corrected a day | Excluded from the delete predicate entirely — never touched by generation | generated/deleted are one less than a run with no such day |
| Holiday created before first generation | A holiday exists for a session with zero calendar rows | Picked up on the next generate() call via the holiday-reapply step, not lost | repaired > 0 on that run |
5.3 Correct a calendar day by hand
Summary
An administrator overrides one day's classification, adds an event name visible to every audience, or writes an internal note. This is how a school marks an unplanned event, a special school day, or corrects a seeded holiday date that turned out wrong for their calendar — without waiting for a data update.
Preconditions
- Caller holds
AcademicCalendar_UPDATE. - The calendar for that session/date has already been generated (
CALENDAR_DAY_NOT_FOUNDotherwise). - Caller has the current
versionof the day, from a prior read.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Admin | Opens a day's detail drawer. | GET .../calendar/days/:date returns the current row plus its version. | API §8.4 |
| 2 | Admin | Changes classification and/or adds an event name/note. | Local form state. | — |
| 3 | Admin | Saves. | PATCH .../calendar/days/:date with expectedVersion. | API §8.5 |
| 4 | Backend | Moves the day's source to admin_modified if it was auto/seed. | Protected from the next regeneration and from holiday recomputation. | Backend §6.3 |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
| Stale version | A second admin already edited this day | Reject | ATTENDANCE_VERSION_CONFLICT |
| Invalid free text | eventName/note empty, padded, or with control characters | Reject | CALENDAR_TEXT_FORMAT_INVALID |
| No-op patch | Only expectedVersion sent | Still succeeds; version still bumps by one | 200, source unchanged if already not auto/seed |
Day already admin_modified | Editing an already-corrected day | Stays admin_modified | — |
| Day not yet generated | Date outside what generate() has produced | Reject | CALENDAR_DAY_NOT_FOUND |
5.4 Manage holidays (create, narrow/widen, reclassify, delete)
Summary
An administrator records a holiday — a single lunar festival, a multi-day school closure, or an emergency — and the platform applies it to every covered calendar day immediately. Editing the range or classification later re-applies the change; deleting reverts the covered days to whatever the remaining holidays (or the plain weekend rule) say they should be.
Preconditions
- Caller holds the relevant
AcademicCalendar_CREATE/_UPDATE/_DELETEcode. - Every date in the holiday's range must already be generated (
ACADEMIC_CALENDAR_NOT_GENERATEDotherwise). - The range must be wholly inside the session (
CALENDAR_RANGE_OUTSIDE_SESSIONotherwise).
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Admin | Creates a holiday with a name, classification, and date range. | POST .../holidays. | API §8.7 |
| 2 | Backend | Links every covered calendar day and recomputes each. | Days show the holiday's classification and its source. | Backend §6.6 |
| 3 | Admin | Later narrows, widens, or reclassifies the holiday. | PATCH /holidays/:id. | API §8.8 |
| 4 | Backend | Unlinks every day, relinks from the new range, recomputes both the newly-covered and the newly-dropped days. | — | Backend §6.6 |
| 5 | Admin | Deletes a school-sourced holiday no longer needed. | DELETE /holidays/:id. | API §8.9 |
| 6 | Backend | Cascades the link deletion, recomputes each formerly-covered day from the holidays that remain. | — | Backend §6.6 |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
Holiday classified school_day | classification: "school_day" on create or update | Reject — a contradiction, not a variant | CALENDAR_HOLIDAY_RANGE_INVALID |
| Range not generated | A date in the range has no calendar row | Reject the whole holiday, never a partial link | ACADEMIC_CALENDAR_NOT_GENERATED |
| Range outside session | Any date falls outside [startDate, endDate) | Reject | CALENDAR_RANGE_OUTSIDE_SESSION |
| Two holidays cover one day | A school closure overlaps a public holiday | Both link; HOLIDAY_PRECEDENCE decides the visible classification | public_holiday outranks school_holiday, which outranks special_school_day |
| Delete the higher-precedence holiday | The winning holiday of an overlapping pair is deleted | The day falls back to the remaining, lower-precedence holiday | Day's classification changes to the remaining holiday's |
| Delete the last holiday on a day | No holiday remains linked | The day falls back to weekend/school_day against the live week config, source: auto | — |
| Delete a seeded holiday | source: "seed" | Reject — reclassify instead | SCHOOL_HOLIDAY_NOT_DELETABLE |
Holiday covers an admin_modified day | Create, update, or delete touches a day an admin already corrected | The link is created/removed, but the day's classification/source is left untouched | — |
| Stale version on update | Two admins edit the same holiday | Reject | ATTENDANCE_VERSION_CONFLICT |
5.5 Read the calendar as any audience (mobile)
Summary
A pupil, guardian, teacher, or staff member opens the calendar tab and sees the current Bikram Sambat month — with no configuration, no permission, and no per-audience difference in what is shown.
Preconditions
- Caller holds a valid JWT (any role).
- The requested session/month has already been generated by an administrator (§5.2) — otherwise the read is refused, not silently empty.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | User | Opens the calendar tab. | GET /api/mobile/calendar with no query params. | API §8.12 |
| 2 | Backend | Resolves "today's" session and BS month via SchoolClockService. | — | Backend §6.7 |
| 3 | Backend | Reads the month's rows, maps each by hand to the reduced audience DTO. | note, source, ids, and version never leave the server. | Backend §6.7 |
| 4 | User | Navigates to another month. | GET /api/mobile/calendar?bsYear=...&bsMonth=.... | API §8.12 |
Sequence Diagram
Branches and Edge Cases
| Branch | Condition | Behavior | Error/Result |
|---|---|---|---|
| Half a month specified | Only bsYear OR only bsMonth in the query | Reject rather than half-default | CALENDAR_BS_DATE_INVALID |
| No session contains today | Every session has ended or not yet started | Reject | ACADEMIC_SESSION_NOT_FOUND |
| Month not generated | The resolved session/month has zero rows | Reject, never an empty-but-200 response | ACADEMIC_CALENDAR_NOT_GENERATED |
| Same request from any role | Pupil vs. parent vs. teacher vs. staff | Byte-identical response shape | No audience branch exists in the handler at all |
5.6 See the next days of the calendar (any audience)
Summary
A dashboard shows the next few days: which are school days, which are holidays, and any multi-day events such as a festival break.
Main Flow
| Step | Actor/System | Action | Result | Source |
|---|---|---|---|---|
| 1 | Client | GET /api/calendar/upcoming?days=N, N from 1 to 31 (default 7) | The request. | CalendarUpcomingController.upcoming |
| 2 | System | Builds N consecutive school-local dates from today and picks one calendar row per date. | days, each with its BS date, classification and isOperatingDay. | CalendarUpcomingService |
| 3 | System | Merges consecutive notable days that share a classification and event name. | events, each with start and end dates in both calendars. | CalendarUpcomingService |
Branches and Edge Cases
| Branch | Condition | Behavior | Result |
|---|---|---|---|
| Day with no calendar row | The calendar has not decided that date | Classification and isOperatingDay are null; shown as undecided, not as a school day or a holiday. | 200 |
Invalid days | Not an integer, below 1 or above 31 | Rejected. | 400 VALIDATION_FAILED |
| Redis slow or down | Cache read exceeds its 250 ms budget or fails | Read from the database. | 200 |
| Calendar edited moments ago | A holiday or day correction was just saved | The previous result is served until the 60-second cache expires. | 200 |
6. Admin Flows
6.1 Create (holiday)
See §5.4, step 1. Route: POST /api/academic-sessions/:id/holidays. Permission: AcademicCalendar_CREATE.
6.2 List / Read detail (calendar days and holidays)
See §5.2 step 6, §5.3 step 1, and API §8.2/§8.3/§8.4/§8.6. Every list is paginated (QueryDto defaults: page size 20, cap 100, unpaginated hard cap 1000); every detail read is by exact key (date, or holiday id).
6.3 Update (holiday, calendar day, week config)
See §5.3 (calendar day) and §5.4 step 3 (holiday). Both require expectedVersion and are refused with ATTENDANCE_VERSION_CONFLICT on a stale token. Week-config update is covered in §5.1.
6.4 Delete (holiday only)
See §5.4 steps 5–6. There is no delete for calendar days or the week configuration — a calendar day is corrected via PATCH, never removed, and the week-config row is never deleted once created.
6.5 Reorder
Not applicable. No orderable list exists in this module — calendar days are ordered by date and holidays by fromDate, both fixed sort orders with no user-controlled reordering.
6.6 Activate/deactivate
Not applicable. Neither a calendar day nor a holiday nor the week configuration has an active/inactive flag; a day's classification is the closest analogue and is covered under Update.
6.7 Soft delete / Restore
Not applicable. Every table in this module (school_week_config, school_holidays, academic_calendar_days, school_holiday_days) uses hard delete only, and only school_holidays (non-seed) is ever deleted at all — see §6.4.
6.8 Export/import
Not applicable. No export or import endpoint exists in this module.
6.9 Moderation
Not applicable. There is no moderation queue or approval workflow in this module — every admin write takes effect immediately.
6.10 Manual retry
Not applicable in the queue-retry sense — there is no async job in this module to retry. The closest analogue is re-running POST .../calendar/generate, which is designed to be safely repeatable (§5.2) and functions as the module's own "repair" action.
7. Lifecycle and State Transitions
7.1 Calendar day classification/source lifecycle
| Entity | From | Event/Action | To | Guard Condition | Side Effects |
|---|---|---|---|---|---|
academic_calendar_days | (absent) | generate() inserts | weekend/school_day, source: auto | Date not already present | generated_from_weekend_days stamped with the live config |
academic_calendar_days (source: auto) | weekend/school_day | Holiday create/reapply covers the day | Holiday's classification, source = holiday's own source (school/seed) | Holiday range includes the day | school_holiday_days link created |
academic_calendar_days (source: school/seed) | Holiday classification | Holiday deleted, another still covers the day | Remaining holiday's classification per HOLIDAY_PRECEDENCE | At least one holiday still linked | Link removed for the deleted holiday |
academic_calendar_days (source: school/seed) | Holiday classification | Last covering holiday deleted | weekend/school_day against the live week config, source: auto | No holiday remains linked | generated_from_weekend_days re-stamped |
academic_calendar_days (any source except admin_modified) | any | Admin PATCH | Whatever the PATCH set, source: admin_modified | expectedVersion matches | Excluded from every future regeneration/recomputation |
academic_calendar_days (source: admin_modified) | any | Regeneration or holiday recomputation attempts to touch it | Unchanged | — | The row is skipped entirely — never deleted, never recomputed |
Diagram:
7.2 School week configuration lifecycle
| Entity | From | Event/Action | To | Guard Condition | Side Effects |
|---|---|---|---|---|---|
school_week_config | (no row) | First PUT .../week-config | Row exists, configured: true | expectedVersion is null/omitted | Calendar generation becomes possible |
school_week_config | exists | Later PUT .../week-config | Updated weekendDays/schoolDayCutoffMinute, version + 1 | expectedVersion matches stored version | Every auto day's generated_from_weekend_days becomes stale until the next generate() |
Diagram:
9. Data and Side Effects by Flow
| Flow | DB Writes | Cache Effects | Jobs | Realtime | Analytics | Notifications |
|---|---|---|---|---|---|---|
| Configure the school week | school_week_config | None | None | None | None | None |
| Generate the calendar | academic_calendar_days (delete + insert), school_holiday_days (via reapply) | None | None | None | None | None |
| Correct a day (PATCH) | academic_calendar_days | None | None | None | None | None |
| Create/update/delete a holiday | school_holidays, school_holiday_days, academic_calendar_days | None | None | None | None | None |
| Read the calendar (admin or mobile) | None | None | None | None | None | None |
No flow in this module writes to a cache, enqueues a job, emits a realtime event, records an analytics event, or sends a notification. The attendance module separately reads academic_calendar_days.classification to decide whether to expect a register and whether to alert a guardian, but those side effects belong to the attendance module's own flows, not to this one.
10. Error and Recovery Flows
| Scenario | Trigger | User/System Experience | Recovery | Source |
|---|---|---|---|---|
| Week not configured | Generation attempted with no school_week_config row | 409 on generate | Save a week configuration, then retry generate | CalendarGenerateService.generate |
| Version conflict | Two admins edit the same day/holiday/week-config concurrently | 409 on the second writer | Reload the row and retry | ATTENDANCE_VERSION_CONFLICT throughout |
| Holiday range not generated | Holiday create/update targets dates the calendar has not produced yet | 409 on the holiday write | Generate the calendar for that range, then retry | ACADEMIC_CALENDAR_NOT_GENERATED |
| BS range exceeded | A session's dates fall outside BS 2000–2090 | 422 on generate | Correct the session's Gregorian dates | CALENDAR_BS_DATE_OUT_OF_RANGE |
| Portal month not generated | A pupil/parent opens a month an admin has not generated yet | 409 on the mobile read | An administrator must generate that far ahead | ACADEMIC_CALENDAR_NOT_GENERATED |
| Seed holiday delete attempted | An admin tries to delete a platform-populated holiday | 409, with the reclassify path suggested | PATCH the holiday's classification instead | SCHOOL_HOLIDAY_NOT_DELETABLE |
No queue or cache exists in this module, so there is no queue-failure or cache-failure recovery flow to document — the "Recovery" column above is always a direct retry of the same HTTP call after fixing the stated condition.
11. Diagrams Required Per Module
- Actor capability diagram: §3, §4 (tables; the actor set is small enough that a table is clearer than a diagram).
- High-level module flow diagram: §6.10, §9.1 of the backend doc.
- Sequence diagram for each major flow: §5.1–§5.5.
- State machine diagram for every lifecycle: §7.1, §7.2.
- Data side-effect diagram: §9 (table; no flow has enough distinct side-effect categories to warrant a graph).
- Error branch diagram: §6.10 (generic admin-write branch); each flow's own error table in §5 covers its specific branches.
12. Mandatory Feature and Flow Deep-Dive Pack
12.1 Feature Inventory With Minor Behaviors
| Feature | Minor Behavior | Actor | Trigger | User/System Result | Backend Side Effect | Source |
|---|---|---|---|---|---|---|
| Week configuration | Read before any save | Admin | GET .../week-config | Full shape with shipped defaults, never a partial object | None | school-week-config.service.ts:38-49 |
| Week configuration | Seven-day school | Admin | weekendDays: [] on save | Accepted as complete, not incomplete | Row saved with empty array | school-week-config.service.ts |
| Week configuration | Duplicate day rejected | Admin | weekendDays with a repeat | 422 | None | school-week-config.service.ts:143-149 |
| Calendar generation | Repeat call, no changes | Admin | generate() twice | Same coverage, different diff counters | Delete + reinsert every unlinked auto row | calendar-generate.service.ts:31-38 |
| Calendar generation | admin_modified day preserved | Admin | generate() after a PATCH | Day untouched | Excluded from the delete predicate | academic-calendar.integration.spec.ts |
| Calendar generation | Holiday created before first generation | Admin | generate() after an out-of-order holiday create | Holiday applied retroactively | reapplyAllForSession | holiday-day-apply.service.ts:171-176 |
| Calendar generation | BS out-of-range refusal | Admin | Session dated beyond BS 2090 | 422, nothing written | Whole transaction rolled back | calendar-generate.service.ts:74-79 |
| Day correction | No-op patch still bumps version | Admin | PATCH with only expectedVersion | 200, version + 1 | needsReview self-assigned so the trigger fires | calendar-day-write.service.ts:107-112 |
| Day correction | Text trimmed server-side | Admin | eventName/note with leading/trailing space | Stored trimmed | — | calendar-day-write.service.ts:156-161 |
| Day correction | Clearing a field | Admin | eventName: null | Field cleared | — | calendar-day.dto.ts:155-163 |
| Holiday create | Single-day holiday | Admin | fromDate === toDate | Accepted | — | school-holidays_dates_ordered |
| Holiday create | Overlap with a different classification | Admin | Two holidays cover one day | Both link; precedence decides the visible classification | school_holiday_days gets two rows for that day | academic-calendar.integration.spec.ts |
| Holiday update | Narrowing reverts fallen-out days | Admin | Shrinking toDate | Dropped days recompute | recomputeMany on the dropped set | school-holiday.service.ts:294-319 |
| Holiday update | Widening applies to newly-covered days | Admin | Growing the range | Newly-covered days recompute | linkAndRecompute on the new set | school-holiday.service.ts:306-314 |
| Holiday delete | Cascading unlink | Admin | Delete a school-sourced holiday | Links removed via FK cascade | Formerly-covered days recompute | school-holiday.service.ts:352-364 |
| Holiday delete | Seed protection | Admin | Delete a source: seed holiday | 409, no write | None | school-holiday.service.ts:345-350 |
| Portal read | Half-a-month refusal | Any user | Only one of bsYear/bsMonth | 400 | None | calendar-portal.service.ts:44-49 |
| Portal read | Default to "now" | Any user | No query params at all | Current session/month resolved via SchoolClockService | None | calendar-portal.service.ts:52-55 |
| Portal read | Frozen field set | Any user | Any successful call | Identical shape regardless of role | Mapper never spreads a row | portal-calendar.dto.ts:71-93 |
12.2 Business Process Diagram Pack
12.3 Business Rules and Policy Traceability
| Rule | Business Reason | Actor Impact | Enforced In | API Impact | Backend Impact | Tests |
|---|---|---|---|---|---|---|
| No default school week | Nepal's standard week differs from international-curriculum schools; a wrong default silently mis-classifies a year of Fridays. | Admin must configure once before generating | Migration (no seed row) | CALENDAR_WEEKEND_NOT_CONFIGURED on generate | CalendarGenerateService.generate's pre-check | academic-calendar.integration.spec.ts:"refuses generation while school_week_config has no row" |
A holiday is never school_day | A holiday recorded as a school day is a contradiction. | Admin sees a clear rejection rather than a silently-ignored value | SchoolHolidayService.assertNotSchoolDay | 400 CALENDAR_HOLIDAY_RANGE_INVALID | Service guard, not a CHECK | — |
| Holiday precedence is fixed and ordered | Two holidays may cover one day; deletion must recompute, not revert. | Admin sees the correct classification survive a partial deletion | HOLIDAY_PRECEDENCE, HolidayDayApplyService.recomputeDay | Day-detail response reflects the winner | academic_calendar_days classification/source | academic-calendar.integration.spec.ts:"keeps a day classified by the remaining holiday..." |
admin_modified survives everything | An operator's correction must never be silently overwritten. | Admin's edits are durable | CalendarGenerateService's delete predicate; recomputeDay's early return | — | academic_calendar_days.source | academic-calendar.integration.spec.ts:"preserves an admin_modified day across regeneration" |
| Seed holidays are not deletable | Preserves the record of what the platform populated. | Admin must reclassify instead of delete | SchoolHolidayService.remove | 409 SCHOOL_HOLIDAY_NOT_DELETABLE | — | academic-calendar.integration.spec.ts:"refuses to delete a seed holiday" |
| The audience read carries no administrator-internal field | The route has no permission check; the DTO shape is the actual control. | Every pupil/parent/staff sees the same, safe projection | PortalCalendarDayDto's hand enumeration | Response never grows a field by inheritance | — | — |
Only superadmin holds full CRUD by default | The day/holiday rows carry an administrator's free-text notes and identity. | Staff/teacher/guardian/student can view but never write | seed-auth.ts role grants | 403 for any non-superadmin admin route call | — | — |
12.4 Tradeoffs and Product Rationale
| Product Decision | User Benefit | Engineering Benefit | Alternative | Tradeoff | Risk |
|---|---|---|---|---|---|
| No default weekend, ever | A school never silently gets the wrong calendar | No branch to keep in sync with "which country/curriculum" | Ship a Nepal-standard default | Every school configures once, even one that would have wanted the default | An admin who skips this step is blocked at generation, not silently wrong |
| Materialised calendar, not computed on read | Fast, stable reads; per-day edits are possible at all | One row per date is simple to reason about and index | Compute on the fly from a rule + holiday list | Storage cost, explicit staleness tracking | A school that changes its week must remember to regenerate |
| Audience read has no permission check | Every pupil/parent/staff sees the calendar with zero extra setup | One shared implementation instead of four audience-scoped ones | Require AcademicCalendarView_READ on the route itself | The DTO shape becomes the only safety net for this route | A future careless change (spreading a row) would need catching in review |
| Seed holidays are curated for one BS year only | No school is shown a guessed festival date | No temptation to extrapolate incorrect data forward | Extrapolate approximate lunar dates for future years | Administrators must add next year's festivals by hand until the table is extended | A school relying entirely on seed data for a future year gets none |
12.5 Flow Edge-Case Matrix
| Flow | Edge Case | Trigger | Expected Behavior | User/System Feedback | Source |
|---|---|---|---|---|---|
| Week configuration | Empty state | No row ever saved | Full shape with defaults, never a crash | configured: false | school-week-config.service.ts:38-49 |
| Week configuration | First use | First PUT | Insert, configured: true | 200 | school-week-config.service.ts:85-102 |
| Calendar generation | Duplicate action | Called twice with no changes | Result-idempotent | Same coverage both times | academic-calendar.integration.spec.ts |
| Calendar generation | Concurrent action | Two admins generate at once | Both succeed; the transaction serializes the writes | Last-committed state wins; no partial corruption | (structural — one transaction per call) |
| Calendar generation | Missing dependency | No week config | Refused | CALENDAR_WEEKEND_NOT_CONFIGURED | calendar-generate.service.ts:58-63 |
| Day correction | Expired state (stale version) | Two admins edit one day | Second writer refused | ATTENDANCE_VERSION_CONFLICT | calendar-day-write.service.ts:125-131 |
| Holiday create | Permission mismatch | Non-superadmin without AcademicCalendar_CREATE | Refused | 403 | permission-catalog.ts, seed-auth.ts |
| Holiday create | Guest limitation | No unauthenticated caller exists for this route | N/A | 401 | JwtAuthGuard |
| Holiday create | Missing dependency | Range not yet generated | Refused | ACADEMIC_CALENDAR_NOT_GENERATED | holiday-day-apply.service.ts:50-60 |
| Holiday list | Unsupported filter/sort | sort/order/search supplied | Accepted by the DTO, silently ignored — the list is always ordered by fromDate | 200, unaffected ordering | school-holiday.service.ts:114-121 |
| Calendar list | Unsupported filter/sort | Same as above, ordered by gregorianDate | Same | Same | calendar-read.service.ts:183-189 |
| Portal read | Cache stale/miss | N/A — no cache exists | N/A | N/A | — |
| Portal read | Queue failure | N/A — no queue exists | N/A | N/A | — |
| Portal read | Last item | Requesting the final day of a session's last generated month | Returns normally, no special-casing at the boundary | 200 | calendar-portal.service.ts |
12.6 Flow-to-Data Trace
| Flow | Reads | Writes | Cache | Jobs/Events | Response Fields |
|---|---|---|---|---|---|
| Configure week | school_week_config | school_week_config | None | None | configured, weekendDays, schoolDayCutoffMinute, version |
| Generate calendar | academic_sessions, school_week_config, academic_calendar_days, school_holidays, school_holiday_days | academic_calendar_days, school_holiday_days | None | None | generated, deleted, repaired, coverage |
| Correct a day | academic_calendar_days | academic_calendar_days | None | None | Full CalendarDayDetailDto |
| Manage a holiday | school_holidays, academic_calendar_days, academic_sessions | school_holidays, school_holiday_days, academic_calendar_days | None | None | SchoolHolidayDto, and every recomputed day's classification/source (visible on the next day/month read) |
| Read the calendar (admin or mobile) | academic_calendar_days, school_holiday_days/school_holidays (admin month view only) | None | None | None | CalendarDayDto/MonthCalendarDayDto/CalendarDayDetailDto (admin) or PortalCalendarDayDto (mobile) |
12.7 Experience Quality Checklist
- The doc explains what the actor is trying to accomplish (configure once, generate once, correct as needed, view without setup).
- The doc explains what the backend does that the actor does not see (precedence recomputation, staleness tracking, the
admin_modifiedprotection). - The doc covers every minor flow and branch, including no-op patches, seed protection, and unsupported-filter behavior.
- The doc includes admin and shared-audience flows; there is no distinct worker/system-triggered flow in this module (no queue or scheduler exists here).
- The doc explains business logic, tradeoffs, and rationale, including the deliberate absence of a default weekend and the reasoning behind the no-permission portal route.
- The doc maps every flow to API routes and backend side effects.
- The doc includes diagrams appropriate to each flow type.
- The doc covers edge cases and failure recovery, including staleness, version conflicts, and the BS-range boundary.
13. Completion Checklist
- Every feature, minor action, and submodule capability is listed.
- Every actor has allowed and forbidden behavior documented (§3).
- Every major and minor flow includes steps, branches, and diagrams (§5, §12.1, §12.5).
- Every lifecycle has a transition table and state diagram (§7.1, §7.2).
- Every flow links to the API and backend docs.
- No TDD doc exists for this module, so no TDD dependency is called out; the API and backend docs are the complete set of siblings produced.
See Also
- API doc:
/docs/developer/academic-calendar/api - Backend doc:
/docs/developer/academic-calendar/backend
Timetable API Reference
Complete API contracts for Timetable, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Academic Calendar Backend Documentation
Backend architecture, data model, services, cache, queues, runtime rules, and operational behavior for Academic Calendar.