Skoolsewa - Ecommerce Docs
Developer ResourcesAcademic sessions

Academic Sessions API Reference

Complete API contracts for the academic sessions lookup, including routes, auth, DTOs, responses, errors, and the split enforcement of the "one current session" rule.

Academic Sessions - API Reference

Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: Admin-facing API owned by AcademicSessionsModule. No public or mobile-facing route exists.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllerapps/api/src/modules/academic-sessions/academic-sessions.controller.tsRoutes, methods, guards, permissions, decorators.
DTOsapps/api/src/modules/academic-sessions/dto/academic-session.dto.tsRequest, query, response, validation, defaults.
Serviceapps/api/src/modules/academic-sessions/academic-sessions.service.tsBehavior, transactions, side effects, error translation.
Moduleapps/api/src/modules/academic-sessions/academic-sessions.module.tsWiring and dependency on RoleModule.
Schemapackages/db/src/schema/school/academic-sessions.tsColumns, indexes, check constraint.
Testsapps/api/src/modules/academic-sessions/__tests__/academic-sessions.service.integration.spec.tsBehaviours proven against a real database.
Errorsapps/api/src/common/types/error-codes.ts (lines 258-281)Every error code this module can produce.
Permissionspackages/db/src/authorization/permission-catalog.ts (line 95)AcademicSessions declared as a permission module.

2. Module Summary

FieldValue
Module nameAcademicSessionsModule
Module slugacademic-sessions
Primary actorsAdmin (only role verified to hold any AcademicSessions_* permission in this codebase)
API surfaceAdmin only — no @Public() route on the controller
Base route prefix/api/academic-sessions (global prefix api set in apps/api/src/main.ts; controller declares academic-sessions locally)
Auth modelJwtAuthGuard + RoleGuard, applied at the controller class level
PersistencePostgreSQL, table academic_sessions; Redis cache on list reads
Runtime source of truthThe academic_sessions table, served from a 1-hour Redis cache keyed on the exact query and cleared on every write
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
Academic sessionA named school year (e.g. "2026-27") with a start and end date, that dated records hang off.packages/db/src/schema/school/academic-sessions.tsEvery route below.
Current sessionThe one session the office is currently working in.academic-sessions.ts (isCurrent column)isCurrent filter and field.
The "exactly one current" ruleSplit across two layers — see Section 5.academic-sessions.ts, academic-sessions.service.tsCreate, update, delete.
Retirement (isActive)Removes a session from active pick lists without touching any existing reference to it. There is no deleted_at on this table.academic-sessions.tsisActive filter and field.
Public ID (publicId)The UUIDv7 every PATCH/DELETE route addresses a session by. The internal integer id is still returned in the response body.academic-sessions.tsEvery route param below.

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminGET/api/academic-sessionsAdminJwtAuthGuard, RoleGuardAcademicSessions_READAcademicSessionsControllerList/search sessions, paginated.
AdminPOST/api/academic-sessionsAdminJwtAuthGuard, RoleGuardAcademicSessions_CREATEAcademicSessionsControllerCreate a session, optionally as current.
AdminPATCH/api/academic-sessions/:publicIdAdminJwtAuthGuard, RoleGuardAcademicSessions_UPDATEAcademicSessionsControllerUpdate fields, and/or make the session current.
AdminDELETE/api/academic-sessions/:publicIdAdminJwtAuthGuard, RoleGuardAcademicSessions_DELETEAcademicSessionsControllerHard delete — refused for the current session.

There is no separate "make current" route. Making a session current is PATCH /api/academic-sessions/:publicId with { "isCurrent": true } in the body — verified against academic-sessions.controller.ts, which declares only the four routes above.

5. The "Exactly One Current Session" Rule

This rule is split between the database and the service, and the two halves are enforced differently on purpose.

HalfRuleEnforced ByWhy It Cannot Be Enforced Elsewhere
At most one currentNo two sessions may both carry isCurrent = true.The partial unique index academic_sessions_one_current_idx on (is_current) WHERE is_current, packages/db/src/schema/school/academic-sessions.tsA regular unique index over is_current would also forbid every false row from coexisting with another; the partial predicate indexes only rows where the flag is true, so false rows never collide.
At least one currentA school always has a current session; the last one cannot be unset.AcademicSessionsService.update, apps/api/src/modules/academic-sessions/academic-sessions.service.tsNo index or constraint can require that a row exist with a given value — a CHECK or unique index only ever restricts rows that ARE written, never guarantees a write happens. This half can only be enforced in application code, at the point where an unset is attempted.

Both create and update unset the incumbent session inside the same database transaction as the write that sets the new one, and always in that order — unset first, then set. Setting a second isCurrent = true row before clearing the first would violate the partial unique index at the INSERT/UPDATE itself, regardless of which write the caller intended to win.

6. DTO and Model Reference

6.1 AcademicSessionDto (response)

FieldTypeRequiredSource
idnumberYesacademic-session.dto.ts:29 — internal serial id.
publicIdstring (UUID)Yesacademic-session.dto.ts:30.
namestringYesacademic-session.dto.ts:31, e.g. "2026-27".
startDatestring (ISO date)Yesacademic-session.dto.ts:32, e.g. "2026-04-15".
endDatestring (ISO date)Yesacademic-session.dto.ts:33, e.g. "2027-04-14".
isCurrentbooleanYesacademic-session.dto.ts:34-38. Exactly one row carries true — see Section 5.
isActivebooleanYesacademic-session.dto.ts:39.
createdAtDateYesacademic-session.dto.ts:40.
updatedAtDateYesacademic-session.dto.ts:41.

6.2 CreateAcademicSessionDto (body)

FieldTypeRequiredDefaultValidationSource
namestringYes@IsString, @MinLength(1), @MaxLength(64)academic-session.dto.ts:45-49
startDatestringYes@IsISO8601, ISO date YYYY-MM-DDacademic-session.dto.ts:51-53
endDatestringYes@IsISO8601academic-session.dto.ts:55-57
isCurrentbooleanNofalse@IsOptional, @IsBooleanacademic-session.dto.ts:59-65
isActivebooleanNotrue@IsOptional, @IsBooleanacademic-session.dto.ts:67-70

endDate must be strictly after startDate — enforced both by the service (AcademicSessionsService.assertDatesOrdered, a readable pre-check) and by the database CHECK constraint academic_sessions_dates_ordered (the actual guarantee). A duplicate name is rejected case-insensitively.

6.3 UpdateAcademicSessionDto (body, PATCH)

FieldTypeRequiredNotesSource
namestringNoSame validation as create.academic-session.dto.ts:74-79
startDatestringNoChecked against the stored endDate when omitted — see edge cases below.academic-session.dto.ts:81-84
endDatestringNoChecked against the stored startDate when omitted.academic-session.dto.ts:86-89
isCurrentbooleanNotrue makes this session current and unsets the incumbent in the same transaction. false on the only current session is refused.academic-session.dto.ts:91-97
isActivebooleanNoRetire/reactivate.academic-session.dto.ts:99-102

6.4 ListAcademicSessionsQueryDto (query)

Extends the shared QueryDto (search, page, size, sort, order, pagination — inherited, never redeclared) and adds:

FieldTypeRequiredSource
isActivebooleanNoacademic-session.dto.ts:113-116. Query-string boolean, coerced from "true"/"1".
isCurrentbooleanNoacademic-session.dto.ts:118-121. Same coercion.

sort accepts "name" or "endDate"; anything else (including the default) sorts by startDateacademic-sessions.service.ts, findAll. Every list is tie-broken by id ascending after the requested sort column.

7. Endpoint Reference

7.1 GET /api/academic-sessions

Purpose: Paginated, filterable list of sessions.

Auth: JwtAuthGuard, RoleGuard; permission AcademicSessions_READ.

Query: search, isActive, isCurrent, pagination, page, size, sort (name | endDate | default startDate), order.

Response:

{
  "message": "Academic sessions fetched.",
  "data": [
    {
      "id": 3,
      "publicId": "018f2a1e-6b1e-7c3a-9d2e-1a2b3c4d5e6f",
      "name": "2026-27",
      "startDate": "2026-04-15",
      "endDate": "2027-04-14",
      "isCurrent": true,
      "isActive": true,
      "createdAt": "2026-04-01T00:00:00.000Z",
      "updatedAt": "2026-04-01T00:00:00.000Z"
    }
  ],
  "errorCode": null,
  "count": 1,
  "currentPage": 1,
  "totalPage": 1
}

Side effects: A Redis cache read (getSoft) is tried first on a key folding the full query (search, isActive, isCurrent, sort, order, pagination, page, size); on a miss, a SELECT (plus a COUNT(*) when paginated) followed by a setSoft at a 3600-second TTL.

Errors: 401 AUTH_UNAUTHENTICATED, 403 PERMISSION_INSUFFICIENT, 400 VALIDATION_FAILED.

7.2 POST /api/academic-sessions

Purpose: Create a session, optionally making it current immediately.

Auth: permission AcademicSessions_CREATE.

Body: CreateAcademicSessionDto (see 6.2).

Side effects: If isCurrent: true, the existing current session (if any) is unset inside the same transaction before the insert. Always invalidates the list cache (delPatternSoft over academic-sessions:*) after commit.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks AcademicSessions_CREATE.
400VALIDATION_FAILEDInvalid body (bad DTO shape, non-ISO date, name too long, etc.).
409ACADEMIC_SESSION_NAME_TAKENA session with that name already exists (case-insensitive).
409ACADEMIC_SESSION_DATES_INVALIDendDate is not after startDate.
409ACADEMIC_SESSION_CURRENT_CONFLICTTwo concurrent requests both tried to make a session current; the partial unique index refused the loser. Retryable.

7.3 PATCH /api/academic-sessions/:publicId

Purpose: Update any subset of a session's fields. This is also the only way to make a session current — there is no separate "set current" route.

Auth: permission AcademicSessions_UPDATE.

Body: UpdateAcademicSessionDto (see 6.3).

Behavior:

  • { "isCurrent": true } unsets the existing current session (excluding this row) and sets this one, in one transaction.
  • { "isCurrent": false } on the session that is currently the only current one is refused with 409 ACADEMIC_SESSION_CURRENT_REQUIRED — a school always has exactly one current session, and this half of the rule cannot be expressed as a database constraint (see Section 5).
  • A PATCH that supplies only one of startDate/endDate is checked against the stored value of the other field, not just the supplied one — otherwise a caller could invert the range across two separate requests and have the database CHECK constraint report an unmapped 23514.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks AcademicSessions_UPDATE.
404ACADEMIC_SESSION_NOT_FOUNDNo session with that publicId.
400VALIDATION_FAILEDInvalid body.
409ACADEMIC_SESSION_NAME_TAKENRenamed to a name already in use.
409ACADEMIC_SESSION_DATES_INVALIDResulting range (stored, dto-merged) ends before or on its start.
409ACADEMIC_SESSION_CURRENT_REQUIREDisCurrent: false on the only current session.
409ACADEMIC_SESSION_CURRENT_CONFLICTConcurrent handover race, same as create.

7.4 DELETE /api/academic-sessions/:publicId

Purpose: Hard delete. There is no deleted_at on academic_sessions — retire a session with PATCH { "isActive": false } instead, which removes it from pick lists while leaving any existing reference to it intact.

Auth: permission AcademicSessions_DELETE.

Behavior: Refused when the session is the current one.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks AcademicSessions_DELETE.
404ACADEMIC_SESSION_NOT_FOUNDNo session with that publicId.
409ACADEMIC_SESSION_IS_CURRENTThis is the current session; make another one current first.

As of this doc, no other schema references academic_sessions.id, so no foreign-key conflict (23503) can occur on delete yet — the service's own docblock (academic-sessions.service.ts, remove) notes this is expected to change once classes/enrolment land, at which point a 23503 guard is added.

8. Error Code Reference

Error CodeHTTP StatusMeaning
ACADEMIC_SESSION_NOT_FOUND404No session with the given publicId.
ACADEMIC_SESSION_NAME_TAKEN409The case-insensitive unique index on name refused the write.
ACADEMIC_SESSION_DATES_INVALID409endDate is not strictly after startDate.
ACADEMIC_SESSION_IS_CURRENT409Delete refused: this is the current session.
ACADEMIC_SESSION_CURRENT_REQUIRED409PATCH { isCurrent: false } on the only current session.
ACADEMIC_SESSION_CURRENT_CONFLICT409Two operators raced to make different sessions current; the partial unique index refused the second. Retryable.

Source: apps/api/src/common/types/error-codes.ts, lines 258-281.

See Also

  • Backend doc: /docs/developer/academic-sessions/backend
  • Feature and flows doc: /docs/developer/academic-sessions/feature