Skoolsewa - School Management System Docs
Developer ResourcesLeave

Leave API Reference

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

Leave - API Reference

Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: Admin (office/permission-holder) and mobile-audience (guardian, student, teacher, staff) APIs owned by Leave.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/leave/**/*.controller.ts (9 controllers)Routes, methods, guards, decorators, status codes.
DTOsapps/api/src/modules/leave/**/dto/*.tsRequest, query, response, validation, examples.
Servicesapps/api/src/modules/leave/**/*.service.tsBehavior, side effects, response mapping, errors.
Schemapackages/db/src/schema/school/leave.ts, migration 0022_leave_module.sqlIDs, enums, persisted fields, constraints.
Jobs/outboxpackages/jobs/src/index.ts (QueueName.LEAVE, LeaveJob), apps/api/src/modules/outbox/shared/outbox.service.tsAsync events, the outbox guarantee.
Permissionspackages/db/src/authorization/permission-catalog.ts, packages/db/src/seed/seed-auth.tsThe six Leave* permission modules; which of them no seeded role holds.
Guard allowlistapps/api/src/common/authorization/role.guard.tsThe twelve permission-less portal handlers.
Swagger membershipapps/api/src/config/swagger/swagger-documents.ts, apps/api/src/modules/mobile/mobile.module.tsAdmin-document vs. mobile-document routing.
Error registryapps/api/src/common/types/error-codes.tsAll 21 LEAVE_* codes.
Testsapps/api/src/modules/leave/__tests__/*.spec.tsConfirmed request/response shapes and edge cases.

2. Module Summary

FieldValue
Module nameLeave
Module slugleave
Primary actorspupil (student session), guardian, class teacher, staff member, permission-holding administrator, internal system (worker)
API surfacesadmin (/api/leave-types, /api/student-leaves, /api/staff-leaves), mobile (/api/mobile/{student,guardian,teacher,staff}/leaves)
Base route prefixes/api/leave-types, /api/student-leaves, /api/staff-leaves, /api/mobile/student/leaves, /api/mobile/guardian/leaves, /api/mobile/teacher/leaves, /api/mobile/staff/leaves
Auth modelJwtAuthGuard (global) + RoleGuard. Admin surfaces: @Permissions("Module_ACTION"). Mobile surfaces: no @Permissions() — assertPortalAudience (student, guardian), assertTeacherStaffRow (teacher) or assertStaffRow (staff) instead.
PersistencePostgreSQL only. No Redis, no MongoDB read/write in this module (MongoDB audit logging happens through the global interceptor, not module code).
Runtime source of truthleave_requests/leave_types tables; leave_request_runtime for internal apply/revert state, exposed on no DTO.
Sibling docsBackend (/docs/developer/leave/backend), Features and flows (/docs/developer/leave/feature).

3. Concepts and Terminology

TermMeaningSource FileUsed By
Leave typeThe administrator-managed catalogue entry a request is filed under (sick, casual, annual seeded).leave.ts (leaveTypes)Every create/list DTO.
SubjectThe pupil or staff member the request is about. subjectKind discriminates.leave.ts (leaveSubjectKindEnum)LeaveRequestDto.subjectKind
OriginWho raised the request — decides which consent is sought. self (the subject), guardian, or staff_on_behalf (the office).leave.ts (leaveRequestOriginEnum)LeaveRequestDto.origin
StatusThe request's position in its lifecycle: pending_guardian, pending_school, approved, rejected, cancelled.leave.ts (leaveRequestStatusEnum)LeaveRequestDto.status
DecisionOne approved/rejected answer, permanently logged.leave.ts (leaveDecisionEnum)LeaveDecisionDto.decision
Decision stepWhich stage a decision closed — guardian or school.leave.ts (leaveDecisionStepEnum)LeaveDecisionDto.step
Acted-as capacityIn what capacity the decider acted — guardian, class_teacher, or permission_holder. Distinguishes a parent's own consent from an administrator's override forever.leave.ts (leaveActorCapacityEnum)LeaveDecisionDto.actedAs
ReversalDeciding rejected against an ALREADY-approved request — the only exit from approved, and the trigger for restoring the attendance register.leave-decision.service.tsAdmin decide route only
versionThe row's optimistic-concurrency token, a plain integer counter, sent back as a string on read and required on every write that can conflict.leave.ts (bigint column), row-version.tsEvery LeaveRequestDto/LeaveTypeDto, every decide/update/cancel body
submissionIdClient-generated uuid v7 making a create idempotent. A replay under the same key returns the ORIGINAL row, never a 409.leave-request.dto.tsEvery create DTO
Day countSchool days (pupil paths) or calendar days (staff paths), computed AS AT SUBMISSION — advisory, not necessarily what the register ends up reflecting.leave-resolution.service.tsLeaveRequestDto.dayCount and both portal DTOs

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminGET/api/leave-typesPermission holderJwtAuthGuard, RoleGuardLeaveTypes_READLeaveTypesControllerList the catalogue.
AdminPOST/api/leave-typesPermission holderSameLeaveTypes_CREATELeaveTypesControllerCreate a leave type.
AdminPATCH/api/leave-types/:publicIdPermission holderSameLeaveTypes_UPDATELeaveTypesControllerCorrect a leave type (not code).
AdminDELETE/api/leave-types/:publicIdPermission holderSameLeaveTypes_DELETELeaveTypesControllerRetire (soft delete) a leave type.
AdminPOST/api/leave-types/:publicId/restorePermission holderSameLeaveTypes_RESTORELeaveTypesControllerRestore a retired leave type.
AdminGET/api/student-leavesPermission holderSameStudentLeaves_READStudentLeavesControllerList pupil leave requests.
AdminGET/api/student-leaves/summaryPermission holderSameStudentLeaves_READStudentLeavesControllerStatus counts over the same filter set.
AdminGET/api/student-leaves/:idPermission holderSameStudentLeaves_READStudentLeavesControllerOne pupil request.
AdminGET/api/student-leaves/:id/decisionsPermission holderSameStudentLeaves_READStudentLeavesControllerIts decision trail.
AdminPOST/api/student-leavesPermission holderSameStudentLeaves_CREATEStudentLeavesControllerFile office leave for a pupil.
AdminPATCH/api/student-leaves/:idPermission holderSameStudentLeaves_UPDATEStudentLeavesControllerCorrect type/reason.
AdminDELETE/api/student-leaves/:idPermission holderSameStudentLeaves_DELETEStudentLeavesControllerSoft-delete a non-approved request.
AdminPOST/api/student-leaves/:id/restorePermission holderSameStudentLeaves_RESTOREStudentLeavesControllerRestore a deleted request.
AdminPOST/api/student-leaves/:id/decisionPermission holderSameStudentLeaveDecisions_UPDATEStudentLeavesControllerDecide, or reverse an approved, request.
AdminPOST/api/student-leaves/:id/reapply-attendancePermission holderSameStudentLeaveDecisions_UPDATEStudentLeavesControllerRe-drive a dead-lettered attendance apply.
AdminGET/api/staff-leavesPermission holderSameStaffLeaves_READStaffLeavesControllerList staff leave requests.
AdminGET/api/staff-leaves/summaryPermission holderSameStaffLeaves_READStaffLeavesControllerStatus counts.
AdminGET/api/staff-leaves/:idPermission holderSameStaffLeaves_READStaffLeavesControllerOne staff request.
AdminGET/api/staff-leaves/:id/decisionsPermission holderSameStaffLeaves_READStaffLeavesControllerIts decision trail.
AdminPOST/api/staff-leavesPermission holderSameStaffLeaves_CREATEStaffLeavesControllerFile office leave for a staff member.
AdminPATCH/api/staff-leaves/:idPermission holderSameStaffLeaves_UPDATEStaffLeavesControllerCorrect type/reason.
AdminDELETE/api/staff-leaves/:idPermission holderSameStaffLeaves_DELETEStaffLeavesControllerSoft-delete a non-approved request.
AdminPOST/api/staff-leaves/:id/restorePermission holderSameStaffLeaves_RESTOREStaffLeavesControllerRestore a deleted request.
AdminPOST/api/staff-leaves/:id/decisionPermission holderSameStaffLeaveDecisions_UPDATEStaffLeavesControllerDecide, or reverse an approved, request.
AdminPOST/api/staff-leaves/:id/reapply-attendancePermission holderSameStaffLeaveDecisions_UPDATEStaffLeavesControllerRe-drive a dead-lettered attendance apply.
MobileGET/api/mobile/student/leavesPupilJwtAuthGuard, RoleGuardNone — audience-assertedStudentLeavePortalControllerList my own requests.
MobilePOST/api/mobile/student/leavesPupilSameNoneStudentLeavePortalControllerRaise my own leave.
MobilePOST/api/mobile/student/leaves/:id/cancelPupilSameNoneStudentLeavePortalControllerCancel my own pending request.
MobileGET/api/mobile/guardian/leavesGuardianSameNoneGuardianLeavePortalControllerList my children's requests.
MobilePOST/api/mobile/guardian/leavesGuardianSameNoneGuardianLeavePortalControllerRaise leave for my child.
MobilePOST/api/mobile/guardian/leaves/:id/decisionGuardianSameNoneGuardianLeavePortalControllerConsent to or refuse a child's request.
MobilePOST/api/mobile/guardian/leaves/:id/cancelGuardianSameNoneGuardianLeavePortalControllerCancel a request I raised.
MobileGET/api/mobile/teacher/leavesClass teacherSameNoneTeacherLeavePortalControllerList my class's decision queue.
MobilePOST/api/mobile/teacher/leaves/:id/decisionClass teacherSameNoneTeacherLeavePortalControllerApprove or refuse a pupil's request.
MobileGET/api/mobile/staff/leavesStaff memberSameNoneStaffLeavePortalControllerList my own requests.
MobilePOST/api/mobile/staff/leavesStaff memberSameNoneStaffLeavePortalControllerRaise my own leave.
MobilePOST/api/mobile/staff/leaves/:id/cancelStaff memberSameNoneStaffLeavePortalControllerCancel my own pending request.

37 routes total: 5 leave-type, 10 pupil-admin, 10 staff-admin, 3 student-portal, 4 guardian-portal, 2 teacher-portal, 3 staff-portal. LeaveTypesModule, StudentLeavesModule and StaffLeavesModule are named in ADMIN_MODULES (swagger-documents.ts); the four portal modules are reached via MOBILE_CHILDREN in mobile.module.ts and appear in the mobile document by reflection, never by a second listing in ADMIN_MODULES.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
Admin (types, student-leaves, staff-leaves)@UseGuards(JwtAuthGuard, RoleGuard) + @Permissions("Module_ACTION") per handlerreq.user.id, req.user.activeRoleSee §4NoRoleGuard resolves the caller's grants against the permission catalogue; superadmin bypasses the check.
Mobile — student@UseGuards(JwtAuthGuard, RoleGuard), no @Permissions()activeRole.scopeKind === "student"N/ANoassertPortalAudience(actor, "student") — proves WHO is calling; resolves no row.
Mobile — guardianSameactiveRole.scopeKind === "guardian"N/ANoassertPortalAudience(actor, "guardian").
Mobile — teacherSameactiveRole.name === "teacher", plus a live staff rowN/ANoassertTeacherStaffRow(db, actor) — refuses any other active role with 403 PERMISSION_INSUFFICIENT (a teacher acting as staff switches role first), then resolves and returns the caller's own staff.id.
Mobile — staffSameAny active role that is not guardian/student scoped, plus a live staff rowN/ANoassertStaffRow(db, actor) — the staff-row half of the teacher check, without the role name. Non-teaching staff, and a teacher acting as either staff or teacher, reach their own leave here. staff and teacher both carry scopeKind: "all", so the role name is the only thing that tells the two audiences apart.

Every route in this module requires a valid session. There is no @Public() route and no guest-accessible route anywhere in Leave.

Which permission a caller must hold. LeaveTypes_*, StudentLeaves_*, StaffLeaves_*, StudentLeaveDecisions_UPDATE, StaffLeaveDecisions_UPDATE are all held by superadmin only, among the five seeded system roles (superadmin, staff, teacher, guardian, student). staff/teacher/guardian/student hold LeaveView_READ only (gates a panel screen, zero API routes). A school delegating office/admin leave duties creates a role and grants it the specific codes it needs.

The twelve permission-less mobile handlers and why they are still safe. RoleGuard returns true at the point it checks NO_PERMISSION_ADMIN_HANDLERS, BEFORE it reads request.user or refuses a missing active role — so the ONLY control on these routes is JwtAuthGuard (proves a session exists) plus the handler's own assertPortalAudience/assertTeacherStaffRow/assertStaffRow call (proves WHO is calling) plus the service's own hand-built row predicate (decides WHICH rows). None of the three routes to PeopleAccessService.scopeFor, which would have returned {kind: "all"} for the seeded staff/teacher roles and leaked every pupil's requests to any teacher session.

403 vs. 404, precisely. An audience mismatch (a guardian session calling a teacher route, for instance — not directly possible here since each controller is mounted under its own audience, but relevant if a caller forges the wrong role) is 403. A row outside the caller's own scope — someone else's child, another teacher's class, an already-decided request — is 404, identical to a row that does not exist at all. This is deliberate: an id space over a roll of children must never become an enumeration oracle.

6. DTO and Model Reference

6.1 CreateLeaveTypeDto (request, admin POST /api/leave-types)

FieldTypeRequiredDefaultValidationExampleSource
codestringYesN/A^[a-z][a-z0-9_]*$, max 40"sick"leave-type.dto.ts
labelstringYesN/ATrimmed, then 1–80 chars; whitespace-only is refused with 400 VALIDATION_FAILED"Sick leave"Same
descriptionstringNonullmax 500 chars—Same
appliesToStudentbooleanNotrue——Same
appliesToStaffbooleanNotrue——Same
isActivebooleanNotrue——Same
sortOrdernumberNo0int, 0–1000—Same

6.2 UpdateLeaveTypeDto (request, admin PATCH /api/leave-types/:publicId)

Every field from §6.1 except code, all optional. code is deliberately absent — immutable after create.

description is the one field whose omission and null differ: omitting it leaves the stored description unchanged, while null, an empty string or whitespace clears it to null. Any other value is trimmed. On create, an empty or whitespace-only description is stored as null too.

description is the only nullable field. Sending null for label, appliesToStudent, appliesToStaff, isActive or sortOrder is refused with 400 VALIDATION_FAILED; omit the field to leave it unchanged.

6.3 ListLeaveTypesQueryDto (query, admin GET /api/leave-types)

Extends QueryDto (pagination, page, size, sort, order, search — see §10). Adds:

FieldTypeRequiredDefaultValidationSource
isActivebooleanNo—query-string boolean coercionleave-type.dto.ts
appliesToStudentbooleanNo—SameSame
appliesToStaffbooleanNo—SameSame
includeDeletedbooleanNofalseSameSame

search matches label OR code.

6.4 LeaveTypeDto (response)

FieldTypeNullableNotes
publicIdstring (uuid)NoAddress this row by it.
codestringNo
labelstringNo
descriptionstringYes
appliesToStudent / appliesToStaffbooleanNo
isActivebooleanNo
sortOrdernumberNo
versionstringNoSend back as version on the next update.
createdAt / updatedAtDate (ISO string on the wire)No

6.5 CreateStudentLeaveDto (request, admin POST /api/student-leaves)

Extends CreateLeaveRequestBaseDto (§6.9) and adds:

FieldTypeRequiredValidationExampleSource
studentIdstring (uuid)Yes@IsUUID—student-leave.dto.ts

studentId is the ONLY field beyond the shared base — every other server-derived column (subjectKind, origin, status, classId, enrollmentId, academicSessionId, dayCount, raisedByUserId) is fixed by the service, never accepted here. Naming which pupil is unavoidable on a flat /api/student-leaves collection route with no :studentId path segment.

6.6 UpdateStudentLeaveDto (request, admin PATCH /api/student-leaves/:id)

Identical to UpdateLeaveRequestBaseDto (§6.10) — no pupil-specific field. Dates are immutable after create; only leaveTypeId and reason are accepted.

6.7 ListStudentLeavesQueryDto (query, admin GET /api/student-leaves)

Extends ListLeaveRequestsQueryDto (§6.11) and adds:

FieldTypeRequiredValidationSource
classIdstring (uuid)No@IsUUID — the class's publicId, resolved at submissionstudent-leave.dto.ts

6.8 CreateStaffLeaveDto / UpdateStaffLeaveDto / ListStaffLeavesQueryDto (staff admin equivalents)

CreateStaffLeaveDto extends CreateLeaveRequestBaseDto and adds staffId: string (uuid, required) — the colleague this leave is filed for (staff.id). UpdateStaffLeaveDto is identical to UpdateLeaveRequestBaseDto. ListStaffLeavesQueryDto adds NO field beyond ListLeaveRequestsQueryDto — staff carry no class to filter by. Source: staff-leave.dto.ts.

6.9 CreateLeaveRequestBaseDto (shared base)

FieldTypeRequiredValidationExampleSource
leaveTypeIdstring (uuid)Yes@IsUUID — the type's publicId—leave-request.dto.ts
startDatestring (date)Yes@IsISO8601"2026-08-10"Same
endDatestring (date)Yes@IsISO8601"2026-08-12"Same
reasonstringYes3–500 chars—Same
submissionIdstring (uuid)No@IsUUID — idempotency key—Same

6.10 UpdateLeaveRequestBaseDto (shared base)

FieldTypeRequiredValidationSource
versionstringYesThe version from the record you loadedleave-request.dto.ts
leaveTypeIdstring (uuid)No@IsUUIDSame
reasonstringNo3–500 charsSame

6.11 ListLeaveRequestsQueryDto (shared admin query base)

Extends QueryDto (§10). Adds:

FieldTypeRequiredValidationSource
statusenumNoOne of LEAVE_REQUEST_STATUSES (§7)leave-request.dto.ts
leaveTypeIdstring (uuid)No@IsUUID — the type's publicIdSame
startDateFrom / startDateTostring (date)No@IsISO8601Same
includeDeletedbooleanNo, default falseQuery-string boolean coercionSame

6.12 LeaveRequestDto (response, admin)

FieldTypeNullableNotes
idstring (uuid v7)NoThe public identifier — no separate publicId.
subjectKind"student" | "staff"No
subjectIdstringNostudents.id or staff.id.
subjectNamestringNoDenormalised for display.
subjectCodestringNoAdmission number (student) or employee code (staff).
classIdstring (uuid)YesThe class's publicId, as at submission. Student requests only — always null for staff.
leaveTypeIdstring (uuid)NoThe type's publicId.
leaveTypeCode / leaveTypeLabelstringNo
academicSessionIdstring (uuid)NoThe session's publicId.
startDate / endDatestring (date)No
dayCountnumberNoSchool days (student) or calendar days (staff), as at submission — advisory.
statusenumNoSee §7.
origin"self" | "guardian" | "staff_on_behalf"No
reasonstringNo
submissionIdstring (uuid)Yes
raisedByUserIdstringNo
decidedByUserIdstringYes
decidedAtDateYes
decisionNotestringYes
cancelledByUserIdstringYes
cancelledAtDateYes
attendanceAppliedAtDate (ISO string on the wire)YesWhen the attendance worker finished writing the register for this approved request. null until it has run, and for a request that was never approved. An approved request with this null and attendanceRevertedAt also null is the state reapply-attendance recovers.
attendanceDaysWrittennumberYesHow many register days the worker actually wrote, against dayCount's advisory count. null until the worker has run; 0 is a real answer (the worker ran and wrote nothing).
attendanceRevertedAtDate (ISO string on the wire)YesWhen the attendance write was reversed because the approval was reversed. null when it never was. A reversed request cannot be re-applied.
versionstringNo
createdAt / updatedAtDateNo

The three attendance* fields are present on every response that returns this DTO — list, detail, create (including an idempotent replay by submissionId), update, restore, decision and reapply-attendance — for both pupil and staff requests. They are null together for a request with no runtime row.

6.13 LeaveDecisionDto (response, admin — GET .../:id/decisions)

FieldTypeNullableNotes
idstring (uuid v7)No
step"guardian" | "school"No
decision"approved" | "rejected"No
actedAs"guardian" | "class_teacher" | "permission_holder"No
decidedByUserIdstringYesNULL only in a theoretical raw-SQL path — every service call supplies it.
decidedByNameSnapshotstringNoSnapshot at decision time — outlives the person.
notestringYes
previousStatus / newStatusenumNoAlways differ.
decidedAtDateNo

6.14 LeaveSummaryDto (response, admin GET .../summary)

FieldTypeNotes
totalnumberCount over the same filtered set the list would return.
pendingApprovalnumberpending_guardian + pending_school.
approvednumber
rejectednumber

6.15 DecideLeaveDto (request, admin POST .../:id/decision)

FieldTypeRequiredValidationExampleNotes
decision"approved" | "rejected"Yes@IsIn"approved"
notestringNo1–500 chars"Medical certificate seen."
expectedVersionnumberYesint ≥ 03The row's version as last read.

Carries deliberately NO subject, status, or actor field — the next status is derived from the row's CURRENT status inside the decision transaction, never sent by the caller.

6.16 CreateGuardianLeaveRequestDto (request, mobile POST /api/mobile/guardian/leaves)

FieldTypeRequiredValidationSource
studentIdstring (uuid)Yes@IsUUID — the child's students.idguardian-leave-portal.dto.ts
leaveTypePublicIdstring (uuid)Yes@IsUUIDSame
startDate / endDatestring (date)Yes@IsDateStringSame
reasonstringYesmin 3 charsSame
submissionIdstring (uuid)No@IsUUIDSame

6.17 GuardianLeaveDecisionDto / GuardianCancelLeaveRequestDto (request)

GuardianLeaveDecisionDto: decision ("approved"|"rejected", required), note (optional string), expectedVersion (optional, nullable int — when omitted, the service uses the row's OWN current version rather than refusing). GuardianCancelLeaveRequestDto: expectedVersion only, same optionality.

6.18 GuardianLeaveRequestDto (response, mobile)

FieldTypeNullableNotes
idstring (uuid)No
studentIdstring (uuid)No
studentNamestringNo
leaveType{ id, code, label }NoNested summary, not the full LeaveTypeDto.
startDate / endDatestring (date)No
dayCountnumberNoSchool days.
statusenumNo
origin"self" | "guardian" | "staff_on_behalf"No
reasonstringNoThe guardian is entitled to their own child's stated reason.
decisionNotestringYes
decidedAt / cancelledAtstring (ISO)Yes
versionnumberNoNumeric here — unlike the admin LeaveRequestDto.version, which is a string.
createdAtstring (ISO)No

6.19 CreateStudentLeaveRequestDto / CancelLeaveRequestDto / StudentLeaveRequestDto (mobile, pupil self-serve)

CreateStudentLeaveRequestDto: leaveTypePublicId, startDate, endDate, reason (min 3), submissionId (optional) — no studentId field, since the route resolves the caller's own id. CancelLeaveRequestDto: expectedVersion only. StudentLeaveRequestDto matches GuardianLeaveRequestDto's shape (§6.18) minus studentId/studentName (the caller IS the subject) — reason is present, since this is the pupil's own record. Source: student-leave-portal.dto.ts.

6.20 ListTeacherLeaveRequestsQueryDto / TeacherLeaveDecisionDto / TeacherLeaveRequestDto

ListTeacherLeaveRequestsQueryDto adds nothing beyond QueryDto. TeacherLeaveDecisionDto: decision, note (optional), expectedVersion (optional, nullable). TeacherLeaveRequestDto:

FieldTypeNullableNotes
idstring (uuid)No
leaveType{ id, code, label }No
startDate / endDatestring (date)No
dayCountnumberNo
status"pending_school" | "approved" | "rejected" | "cancelled"Nopending_guardian is impossible here — the query never returns a request still awaiting the guardian step.
studentIdstring (uuid)No
studentNamestringNo
classLabelstringNoe.g. "Grade 5 A (Morning)".
versionnumberNo
createdAtstring (ISO)No

Deliberately carries NO reason field. This is the ONLY teacher-facing leave projection — there is no GET .../:id detail route, so there is nothing else to keep in step. Source: teacher-leave-portal.dto.ts.

6.21 CreateStaffLeaveRequestDto / StaffCancelLeaveRequestDto / StaffLeaveRequestDto

CreateStaffLeaveRequestDto: leaveTypePublicId, startDate, endDate, reason (min 3), submissionId (optional) — no staffId field. StaffCancelLeaveRequestDto: expectedVersion only. StaffLeaveRequestDto matches StudentLeaveRequestDto's shape, with dayCount documented as CALENDAR days (staff are not calendar-gated) and origin restricted to "self" | "staff_on_behalf" (a staff member never sees guardian origin on their own record — that value exists only for student-subject rows). Source: staff-leave-portal.dto.ts.

7. Enum Reference

EnumValueMeaningRuntime EffectSource
leave_subject_kindstudentThe request is about a pupil.Requires student_id, class_id; forbids staff_id.leave.ts
staffThe request is about a staff member.Requires staff_id; forbids student_id, class_id; skips the guardian step.Same
leave_request_statuspending_guardianAwaiting a parent's consent.Only reachable for subject_kind: student; visible to the raising pupil, the addressed guardian, and any permission holder.Same
pending_schoolAwaiting the school's decision.Reachable by every path; visible to the class teacher (student subject) or a permission holder (either subject).Same
approvedDecided in the requester's favor.Arms the attendance apply; cannot be cancelled; can ONLY be exited by an admin reversal.Same
rejectedDecided against, OR an approved request reversed.Terminal.Same
cancelledWithdrawn by the original raiser while pending.Terminal.Same
leave_request_originselfThe subject (pupil or staff member) raised it themselves.Pupil → pending_guardian; staff → pending_school.Same
guardianA guardian raised it for their child.→ pending_school directly — raising IS the consent.Same
staff_on_behalfThe office filed it.Pupil → pending_guardian; staff → pending_school.Same
leave_decisionapprovedThe decider consented/approved.Same
rejectedThe decider refused/rejected, or is reversing an approval.Same
leave_decision_stepguardianThis decision closed the guardian-consent step.Same
schoolThis decision closed the school step (including a reversal).Same
leave_actor_capacityguardianThe decider acted as the subject's guardian.Same
class_teacherThe decider acted as the pupil's class teacher.Same
permission_holderThe decider acted through an admin permission (*LeaveDecisions_UPDATE).Used for both the ordinary school-step decision by an admin AND every reversal.Same

8. Endpoint Reference

8.1 GET /api/leave-types

Purpose

Lists the leave-type catalogue for a select box, a management screen, or any consumer needing to resolve a code/label pair. Supports the reference-table convention: pagination=false is ALLOWED here (bounded at 1000 rows), unlike every leave-request list.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/leave/types/leave-types.controller.ts
DTOapps/api/src/modules/leave/types/dto/leave-type.dto.ts
Serviceapps/api/src/modules/leave/types/leave-types.service.ts
Schemapackages/db/src/schema/school/leave.ts
Testsapps/api/src/modules/leave/__tests__/leave-types.integration.spec.ts

Auth and Permissions

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

Request

PartRequiredDetails
QueryNoListLeaveTypesQueryDto (§6.3) — pagination, page, size, sort (label or default sortOrder), order, search, isActive, appliesToStudent, appliesToStaff, includeDeleted.

Response

{
  "success": true,
  "message": "Leave types fetched.",
  "data": [
    {
      "publicId": "0199a1e0-0000-7000-8000-0000000f5e01",
      "code": "sick",
      "label": "Sick leave",
      "description": null,
      "appliesToStudent": true,
      "appliesToStaff": true,
      "isActive": true,
      "sortOrder": 10,
      "version": "0",
      "createdAt": "2026-08-01T00:00:00.000Z",
      "updatedAt": "2026-08-01T00:00:00.000Z"
    }
  ],
  "count": 3,
  "currentPage": 1,
  "totalPage": 1
}

Side Effects

None — read only.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
401—Missing/invalid JWTLog in again.JwtAuthGuard
403—Missing LeaveTypes_READNot permitted.RoleGuard

Edge Cases

  • Empty catalogue (never happens in a seeded environment — three types always exist unless deleted): empty array, count: 0.
  • ?search= blank: no filter applied (query DTO trims and drops empty strings).
  • pagination=false: allowed, capped at PaginationUtil.UNPAGINATED_HARD_CAP (1000).

Example Requests

curl -X GET "$API_URL/api/leave-types?isActive=true" \
  -H "Authorization: Bearer TOKEN"

8.2 POST /api/leave-types

Purpose

Creates a new leave type. Called from a settings/catalogue management screen.

Source Evidence

EvidencePath
Controllerleave-types.controller.ts
DTOdto/leave-type.dto.ts (CreateLeaveTypeDto, §6.1)
Serviceleave-types.service.ts
Testsleave-types.integration.spec.ts

Auth and Permissions

  • Permission: LeaveTypes_CREATE
  • Idempotency: None — no submissionId on this DTO; a genuine duplicate code is refused by the unique index, not silently deduplicated.

Request

{ "code": "sick", "label": "Sick leave", "appliesToStudent": true, "appliesToStaff": true }

Response

201, same shape as §8.1's array element (one object, not wrapped in an array).

Side Effects

  • leave_types insert.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
400—DTO validation (bad code format, etc.)Fix the request.ValidationPipe
409LEAVE_TYPE_CODE_ALREADY_EXISTS23505 on leave_types_code_uniqueChoose a different code.leave-types.service.ts
409LEAVE_TYPE_NOT_APPLICABLE_TO_SUBJECTBoth appliesToStudent and appliesToStaff explicitly falseA type must apply to somebody.Same

Edge Cases

  • Omitting both appliesTo* flags: defaults both to true (applies to everyone).
  • Reusing a code held by a RETIRED (soft-deleted) type: refused — the partial unique index still counts a live row with that code as taken only while it's live, but a NEW create with a colliding code against a retired one is fine (the retired row is excluded by the partial index); the refusal above is for a collision with a LIVE type.

Example Requests

curl -X POST "$API_URL/api/leave-types" \
  -H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" \
  -d '{"code":"maternity","label":"Maternity leave","appliesToStudent":false,"appliesToStaff":true}'

8.3 PATCH /api/leave-types/:publicId

Purpose

Corrects a leave type's label, description, applicability, active state, or sort order. code is not accepted.

Source Evidence

Same files as §8.2, plus UpdateLeaveTypeDto.

Auth and Permissions

Permission: LeaveTypes_UPDATE.

Request

{ "label": "Sick / medical leave", "isActive": false }

All fields optional; only supplied fields are written.

Response

200, updated LeaveTypeDto — version incremented by the bump_row_version trigger.

Side Effects

  • leave_types update. Trigger bumps version/updated_at.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_TYPE_NOT_FOUNDNo live type with that publicId.
409LEAVE_TYPE_NOT_APPLICABLE_TO_SUBJECTThe update would leave BOTH appliesTo* flags false (checked against the EXISTING row's other flag when only one is supplied).

Edge Cases

  • Sending code: silently stripped by ValidationPipe's whitelist (the DTO has no such field) — never a 400, never applied.
  • Deactivating a type currently in use: succeeds; existing requests are unaffected, only NEW requests against it are refused.

8.4 DELETE /api/leave-types/:publicId

Purpose

Retires a leave type (soft delete). Existing requests filed against it keep displaying it.

Source Evidence

Same files as §8.2.

Auth and Permissions

Permission: LeaveTypes_DELETE.

Request

No body.

Response

{ "success": true, "message": "Leave type retired.", "data": null }

Side Effects

  • leave_types.deleted_at set.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_TYPE_NOT_FOUNDNo live type with that publicId.

Edge Cases

  • Deleting a type currently pending_guardian/pending_school on a live request: succeeds; reads never filter deleted_at on leave_types from the leave_requests join, so those requests still show the type's code/label.

8.5 POST /api/leave-types/:publicId/restore

Purpose

Restores a retired leave type.

Source Evidence

Same files as §8.2.

Auth and Permissions

Permission: LeaveTypes_RESTORE.

Response

200, restored LeaveTypeDto.

Side Effects

  • leave_types.deleted_at cleared.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_TYPE_NOT_FOUNDNo RETIRED type with that publicId (the query requires deleted_at IS NOT NULL).
409LEAVE_TYPE_CODE_ALREADY_EXISTSA different, LIVE type has since taken the same code — the partial-unique index makes the restore itself collide.

Edge Cases

  • The restore-collision case above is the one place remove/restore on this table can surface a conflict a naive reading of "it's just an undelete" would not expect.

8.6 GET /api/student-leaves

Purpose

The office's roll of pupil leave requests — the primary list screen for processing consents and decisions.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/leave/admin/student-leaves/student-leaves.controller.ts
DTOdto/student-leave.dto.ts (§6.7)
Servicestudent-leaves.service.ts
Schemaleave.ts
Testsleave-admin.integration.spec.ts

Auth and Permissions

Permission: StudentLeaves_READ. Pagination CANNOT be disabled (PAGINATION_LIMIT_INVALID if attempted) — this is a roll of children, not a reference table.

Request

PartRequiredDetails
QueryNopage, size (max 100), sort (startDate or default createdAt), order, search, status, leaveTypeId, startDateFrom, startDateTo, includeDeleted, classId.

Response

{
  "success": true,
  "message": "Student leaves fetched.",
  "data": [
    {
      "id": "0199b2c0-1111-7000-8000-000000000001",
      "subjectKind": "student",
      "subjectId": "...",
      "subjectName": "Aarav Sharma",
      "subjectCode": "2026-A-0142",
      "classId": "...",
      "leaveTypeId": "0199a1e0-0000-7000-8000-0000000f5e01",
      "leaveTypeCode": "sick",
      "leaveTypeLabel": "Sick leave",
      "academicSessionId": "...",
      "startDate": "2026-08-10",
      "endDate": "2026-08-12",
      "dayCount": 3,
      "status": "pending_guardian",
      "origin": "self",
      "reason": "Fever, advised bed rest.",
      "submissionId": null,
      "raisedByUserId": "...",
      "decidedByUserId": null,
      "decidedAt": null,
      "decisionNote": null,
      "cancelledByUserId": null,
      "cancelledAt": null,
      "attendanceAppliedAt": null,
      "attendanceDaysWritten": null,
      "attendanceRevertedAt": null,
      "version": "0",
      "createdAt": "2026-08-08T09:00:00.000Z",
      "updatedAt": "2026-08-08T09:00:00.000Z"
    }
  ],
  "count": 1,
  "currentPage": 1,
  "totalPage": 1
}

Side Effects

None.

Error Cases

HTTP StatusError CodeCondition
400PAGINATION_LIMIT_INVALIDpagination=false.
403—Missing StudentLeaves_READ.

Edge Cases

  • ?search= matches the reason OR the pupil's name (an EXISTS subquery, not a join, so the same builder still serves /summary and the count).
  • ?classId= naming a class publicId nobody owns: matches nothing (filtered to -1), never ignored.
  • Rows filed against a RETIRED leave type still show the type's code/label (no deleted_at filter on that join).
  • Default sort is createdAt, not updatedAt — the shared QueryDto's documented default (updatedAt) is NOT honored here; only sort=startDate changes the order column, any other value (including the literal updatedAt) falls through to createdAt.

8.7 GET /api/student-leaves/summary

Purpose

Status tallies (total / pending / approved / rejected) for a dashboard tile or the list screen's header, over the SAME filters the list accepts.

Source Evidence

Same controller/service as §8.6. Declared BEFORE :id in the controller, or Nest would match summary as a request id.

Auth and Permissions

Permission: StudentLeaves_READ.

Request

Same query fields as §8.6 (minus pagination/sort, which are irrelevant to a count).

Response

{
  "success": true,
  "message": "Student leave summary fetched.",
  "data": { "total": 42, "pendingApproval": 5, "approved": 30, "rejected": 7 }
}

Error Cases

None specific beyond the shared auth failures.

Edge Cases

  • pendingApproval sums BOTH pending_guardian and pending_school — there is no separate count per step.
  • Counted over a count(*) FILTER (WHERE ...) against the identical buildWhere predicate the list uses, so the numbers are guaranteed to agree with what the list under the same filter would show.

8.8 GET /api/student-leaves/:id

Purpose

One pupil leave request, in full.

Source Evidence

Same as §8.6.

Auth and Permissions

Permission: StudentLeaves_READ.

Request

PartRequiredDetails
ParamsYesid — the request's uuid v7 (the row's own PK, not a separate public id).

Response

Single LeaveRequestDto (§6.12), wrapped: { "success": true, "message": "Student leave fetched.", "data": { ... } }.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNo row, or subject_kind != student.

Edge Cases

  • Requesting a staff-subject id through this pupil-only route: 404, not a type-mismatch error — the WHERE clause filters subject_kind = student unconditionally.
  • Soft-deleted rows ARE returned here (no deleted_at filter on findOne) — deletion status is visible on data, not hidden.

8.9 GET /api/student-leaves/:id/decisions

Purpose

The full decision trail for one request — every consent/refusal, in capacity and order.

Source Evidence

Same as §8.6, plus leave_request_decisions.

Auth and Permissions

Permission: StudentLeaves_READ.

Response

{
  "success": true,
  "message": "Decisions fetched.",
  "data": [
    {
      "id": "...",
      "step": "guardian",
      "decision": "approved",
      "actedAs": "guardian",
      "decidedByUserId": "...",
      "decidedByNameSnapshot": "Priya Sharma",
      "note": null,
      "previousStatus": "pending_guardian",
      "newStatus": "pending_school",
      "decidedAt": "2026-08-09T10:00:00.000Z"
    }
  ]
}

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNo student-subject row with that id.

Edge Cases

  • A brand-new request: empty array, not an error.
  • Ordered decidedAt DESC, id DESC — a reversal writing two rows in one transaction (identical decided_at) still orders deterministically by the monotonic uuid v7 id.

8.10 POST /api/student-leaves

Purpose

The office files a pupil leave request on someone's behalf — a note received by phone or on paper.

Source Evidence

Same as §8.6, plus LeaveResolutionService.

Auth and Permissions

Permission: StudentLeaves_CREATE.

Request

{
  "studentId": "...",
  "leaveTypeId": "0199a1e0-0000-7000-8000-0000000f5e01",
  "startDate": "2026-08-10",
  "endDate": "2026-08-12",
  "reason": "Family travel, three days.",
  "submissionId": "0199b300-0000-7000-8000-000000000099"
}

subjectKind, origin, status, classId, enrollmentId, academicSessionId, dayCount, raisedByUserId are NEVER accepted — sending any of them is silently stripped (whitelist) rather than a 400, since they are not declared fields on the DTO.

Response

201, LeaveRequestDto. Always status: pending_guardian, origin: staff_on_behalf.

Side Effects

  • leave_requests insert.

Error Cases

HTTP StatusError CodeCondition
400—DTO validation.
404STUDENT_NOT_FOUND / LEAVE_TYPE_NOT_FOUNDBad studentId/leaveTypeId.
409LEAVE_TYPE_INACTIVE / LEAVE_TYPE_NOT_APPLICABLE_TO_SUBJECTRetired type, or a staff-only type against a pupil.
409LEAVE_STUDENT_NOT_ENROLLEDNo live enrolment on startDate.
409LEAVE_RANGE_OUTSIDE_SESSIONRange crosses the enrolment's own session boundary (or the session's own recorded end_date, exclusive).
409LEAVE_CALENDAR_NOT_GENERATEDA date in range has no calendar row.
409LEAVE_NO_SCHOOL_DAYS_IN_RANGEEvery date is a non-school day.
409LEAVE_NO_GUARDIAN_TO_CONSENTPupil has zero live guardians.
409LEAVE_OVERLAPPING_REQUEST_EXISTS23P01 on the exclusion constraint.

Edge Cases

  • Retried POST with the same submissionId: returns the ORIGINAL row, 201-shaped, not a 409.
  • Range partially inside the past: accepted — no "cannot be in the past" rule exists on the admin path either.

8.11 PATCH /api/student-leaves/:id

Purpose

Correct a filed request's leave type or reason. Dates, subject and status are immutable after create.

Source Evidence

Same as §8.6.

Auth and Permissions

Permission: StudentLeaves_UPDATE.

Request

{ "version": "0", "reason": "Fever, doctor's note attached." }

Response

200, updated LeaveRequestDto.

Side Effects

  • leave_requests update (locked FOR UPDATE first); bump_row_version fires.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNo pupil-subject row with that id.
409LEAVE_VERSION_CONFLICTversion mismatch.
404/409LEAVE_TYPE_NOT_FOUND / LEAVE_TYPE_INACTIVE / LEAVE_TYPE_NOT_APPLICABLE_TO_SUBJECTBad or inapplicable leaveTypeId.

Edge Cases

  • Sending startDate/endDate/studentId: stripped by whitelist, never applied — the docblock states plainly why (recomputing day_count/the exclusion window for an already-decided request would desync it from a register the decision may already have rewritten).
  • Omitting leaveTypeId and reason both: succeeds as a no-op WRITE (the updatedAt stamp alone still fires the trigger, bumping version).

8.12 DELETE /api/student-leaves/:id

Purpose

Soft-delete a non-approved pupil leave request.

Source Evidence

Same as §8.6.

Auth and Permissions

Permission: StudentLeaves_DELETE.

Response

{ "success": true, "message": "Student leave deleted.", "data": null }

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNo live pupil-subject row with that id.
409LEAVE_CANNOT_DELETE_APPROVEDstatus = approved.

Edge Cases

  • Deleting a pending_guardian/pending_school/rejected/cancelled row: succeeds — only approved is refused.

8.13 POST /api/student-leaves/:id/restore

Purpose

Restore a soft-deleted pupil leave request.

Source Evidence

Same as §8.6.

Auth and Permissions

Permission: StudentLeaves_RESTORE.

Response

200, restored LeaveRequestDto.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNo DELETED pupil-subject row with that id.
409LEAVE_OVERLAPPING_REQUEST_EXISTSRestoring re-enters the exclusion constraint's live set — a genuinely conflicting request may have been filed and approved while this one was deleted.

Edge Cases

  • Restoring does not revive an approved status's side effects — a delete could not have happened to an approved row in the first place (§8.12).

8.14 POST /api/student-leaves/:id/decision

Purpose

Take the school's decision on a pupil request — OR reverse an already-approved one. This is the only route that can reach the reversal.

Source Evidence

EvidencePath
Controllerstudent-leaves.controller.ts
DTO../../shared/dto/decide-leave.dto.ts (§6.15)
Servicestudent-leaves.service.ts, leave-decision.service.ts
Testsleave-decision.integration.spec.ts

Auth and Permissions

Permission: StudentLeaveDecisions_UPDATE — a SEPARATE permission from StudentLeaves_UPDATE, so correcting a typo never implies the power to decide.

Request

{ "decision": "approved", "expectedVersion": 1 }

Response

200, updated LeaveRequestDto.

Side Effects

  • Approve from pending_school: status → approved; leave_request_decisions insert (step: school, actedAs: permission_holder); leave_request_runtime armed; LeaveJob.APPLY_ATTENDANCE enqueued via the outbox.
  • Approve from pending_guardian: status → pending_school; decision row only, no job.
  • Refuse: status → rejected; decision row only.
  • Refuse an ALREADY-approved row (the reversal): status → rejected; decision row; LeaveJob.REVERT_ATTENDANCE enqueued.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDRow gone or soft-deleted.
409LEAVE_VERSION_CONFLICTStale expectedVersion.
409LEAVE_CANNOT_DECIDE_OWN_REQUESTDecider = raiser.
409LEAVE_ALREADY_DECIDEDRe-approving an approved row, or deciding a rejected/cancelled one.

Edge Cases

  • No status pre-filter on this route (unlike the guardian/teacher portal decide routes) — it accepts ANY id and lets resolveNextStatus decide legality, which is what makes the reversal reachable here and nowhere else.
  • The register write from an approval is ASYNC — this response returns before the attendance job runs; the register is not guaranteed rewritten by the time 200 is returned.

8.15 POST /api/student-leaves/:id/reapply-attendance

Purpose

Re-queue the attendance write for an approved request whose worker attempt dead-lettered — the decision committed and the register never caught up.

Source Evidence

EvidencePath
Controllerstudent-leaves.controller.ts
Servicestudent-leaves.service.ts, leave-attendance.service.ts
Queue contractpackages/jobs/src/index.ts (LeaveJob.APPLY_ATTENDANCE)

Auth and Permissions

Permission: StudentLeaveDecisions_UPDATE.

Request

No body.

Response

200, LeaveRequestDto (unchanged status — this route only re-queues, it does not itself write the register).

Side Effects

  • Outbox enqueue of LeaveJob.APPLY_ATTENDANCE, with a FRESH, timestamped dedupeKey (${jobName}_retry_${id}_${Date.now()}) — reusing the original key would let onConflictDoNothing silently swallow the retry.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDRow gone or soft-deleted.
409LEAVE_ALREADY_REVERTEDNot currently approved, or already reversed.

Edge Cases

  • Calling this against a request whose apply already succeeded: not refused — it re-queues anyway, and the worker's own idempotent claim (attendance_applied_at IS NULL) makes the redundant run a no-op.

8.16–8.25 Staff-leaves admin routes (/api/staff-leaves)

Mirror §8.6–§8.15 exactly, with these differences:

RouteDifference from the pupil equivalent
GET /api/staff-leavesPermission StaffLeaves_READ. Query has NO classId filter. search matches reason only (no name-search subquery — no class to preserve a shared join for).
GET /api/staff-leaves/summaryPermission StaffLeaves_READ. Same counting shape.
GET /api/staff-leaves/:idFilters subject_kind = staff.
GET /api/staff-leaves/:id/decisionsIdentical shape.
POST /api/staff-leavesBody: staffId (not studentId) + the shared base fields (§6.9). Permission StaffLeaves_CREATE. Always status: pending_school, origin: staff_on_behalf — no guardian step. Errors: STAFF_NOT_FOUND in place of STUDENT_NOT_FOUND; LEAVE_RANGE_OUTSIDE_SESSION from resolveSessionForRange (no enrolment involved); NO LEAVE_STUDENT_NOT_ENROLLED, NO LEAVE_CALENDAR_NOT_GENERATED, NO LEAVE_NO_SCHOOL_DAYS_IN_RANGE, NO LEAVE_NO_GUARDIAN_TO_CONSENT — staff are not calendar-gated and carry no guardian step.
PATCH /api/staff-leaves/:idPermission StaffLeaves_UPDATE. Same field set (leaveTypeId, reason).
DELETE /api/staff-leaves/:idPermission StaffLeaves_DELETE. Same LEAVE_CANNOT_DELETE_APPROVED refusal.
POST /api/staff-leaves/:id/restorePermission StaffLeaves_RESTORE.
POST /api/staff-leaves/:id/decisionPermission StaffLeaveDecisions_UPDATE. Identical transition table (§7 of the feature doc) — a staff request's ONLY school-step decider on this route is a permission holder; there is no teacher-portal equivalent for staff subjects.
POST /api/staff-leaves/:id/reapply-attendancePermission StaffLeaveDecisions_UPDATE. Identical mechanics.

Response shape for every one of these ten is the SAME LeaveRequestDto/LeaveDecisionDto/LeaveSummaryDto as the pupil surface — subjectKind: "staff", classId: null always, subjectCode is the employee code rather than the admission number.


8.26 GET /api/mobile/student/leaves

Purpose

A pupil's own leave requests, for their leave screen.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/leave/portal/student/student-leave-portal.controller.ts
DTOdto/student-leave-portal.dto.ts
Servicestudent-leave-portal.service.ts
Testsleave-portal.integration.spec.ts

Auth and Permissions

  • Auth: JWT
  • Guard chain: JwtAuthGuard, RoleGuard
  • Permission: NONE — assertPortalAudience(actor, "student")
  • Guest support: No
  • Rate limit: None specific
  • Idempotency: N/A (read)

Request

ListMyLeaveRequestsQueryDto — QueryDto fields only, no leave-specific filter. pagination=false is REFUSED (PAGINATION_LIMIT_INVALID).

Response

{
  "success": true,
  "message": "Leave requests fetched.",
  "data": [
    {
      "id": "...",
      "leaveType": { "id": "...", "code": "sick", "label": "Sick Leave" },
      "startDate": "2026-09-20",
      "endDate": "2026-09-22",
      "dayCount": 3,
      "status": "pending_guardian",
      "origin": "self",
      "reason": "Fever and viral cold, advised bed rest.",
      "decisionNote": null,
      "decidedAt": null,
      "cancelledAt": null,
      "version": 0,
      "createdAt": "2026-09-18T08:00:00.000Z"
    }
  ],
  "count": 1,
  "currentPage": 1,
  "totalPage": 1
}

Error Cases

HTTP StatusError CodeCondition
403PERMISSION_INSUFFICIENTCaller's activeRole.scopeKind is not student.
400PAGINATION_LIMIT_INVALIDpagination=false.

Edge Cases

  • No results: empty array — a brand-new pupil with no leave history.
  • version here is a NUMBER, not a string (unlike the admin LeaveRequestDto).

8.27 POST /api/mobile/student/leaves

Purpose

A pupil raises their own leave.

Source Evidence

Same as §8.26, plus guardian-leave-portal.resolvers.ts (shared resolution helpers).

Auth and Permissions

None — assertPortalAudience(actor, "student").

Request

{
  "leaveTypePublicId": "0199a1e0-0000-7000-8000-0000000f5e01",
  "startDate": "2026-09-20",
  "endDate": "2026-09-22",
  "reason": "Fever and viral cold, advised bed rest.",
  "submissionId": "0199b300-0000-7000-8000-0000000000aa"
}

No studentId field — the caller's own id is resolved server-side.

Response

201, StudentLeaveRequestDto. Always status: pending_guardian, origin: self.

Side Effects

  • leave_requests insert.

Error Cases

HTTP StatusError CodeCondition
400VALIDATION_FAILEDendDate < startDate (checked before the type/enrolment resolution).
404LEAVE_TYPE_NOT_FOUND / STUDENT_NOT_FOUNDBad type, or caller has no live students row.
422LEAVE_TYPE_INACTIVE / LEAVE_TYPE_NOT_APPLICABLE_TO_SUBJECTRetired or staff-only type.
422LEAVE_STUDENT_NOT_ENROLLEDNo live enrolment as of TODAY (routing only — the range itself may be in the past).
422LEAVE_NO_GUARDIAN_TO_CONSENTZero live guardian links.
422LEAVE_RANGE_OUTSIDE_SESSION / LEAVE_RANGE_TOO_LONGRange not in one session, or > 366 days.
409LEAVE_CALENDAR_NOT_GENERATEDMissing calendar row for a date in range.
422LEAVE_NO_SCHOOL_DAYS_IN_RANGEEvery date is a non-school day.
409LEAVE_OVERLAPPING_REQUEST_EXISTSExclusion constraint.
403STUDENT_GRADUATED_READ_ONLYThe pupil's graduate access state is grace or ended, checked after the submissionId replay so a retry of a request raised while still attending is never turned into this refusal.

Edge Cases

  • Retroactive startDate (in the past): accepted.
  • Retried with the same submissionId: returns the original row.

8.28 POST /api/mobile/student/leaves/:id/cancel

Purpose

Withdraws a request the pupil raised themselves, while it is still pending.

Source Evidence

Same as §8.26.

Auth and Permissions

None — assertPortalAudience(actor, "student").

Request

{ "expectedVersion": 0 }

expectedVersion is OPTIONAL and nullable — omitted, the check is skipped entirely.

Response

200, StudentLeaveRequestDto with status: cancelled.

Side Effects

  • leave_requests update: status, cancelled_at, cancelled_by_user_id.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNot raised by this caller, not in {pending_guardian, pending_school}, or genuinely absent — all identical.
409LEAVE_VERSION_CONFLICTStale expectedVersion, when supplied.

Edge Cases

  • Cancelling a request the pupil's OWN guardian raised about them: refused (404) — the predicate is raised_by_user_id = actor.id, and the guardian, not the pupil, raised it.
  • Cancelling after approval: refused (404) — approved is outside the cancellable-status set.

8.29 GET /api/mobile/guardian/leaves

Purpose

A guardian's view of every one of their children's leave requests.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/leave/portal/guardian/guardian-leave-portal.controller.ts
Mapperguardian-leave-portal.mapper.ts
Serviceguardian-leave-portal.service.ts
Testsleave-portal.integration.spec.ts

Auth and Permissions

None — assertPortalAudience(actor, "guardian").

Request

ListGuardianLeaveRequestsQueryDto — QueryDto fields only. pagination=false refused.

Response

Array of GuardianLeaveRequestDto (§6.18) — includes studentId/studentName per row, since one guardian can have several children.

Error Cases

HTTP StatusError CodeCondition
403PERMISSION_INSUFFICIENTNot a guardian session.
400PAGINATION_LIMIT_INVALIDpagination=false.

Edge Cases

  • Own-children predicate filters deleted_at ONLY (on guardians/students/users), never record_status — the identity layer's rule (Backend §11, and CLAUDE.md's own worked example).
  • Multiple children: rows for all of them, interleaved by createdAt DESC.

8.30 POST /api/mobile/guardian/leaves

Purpose

A guardian files leave for their own child. Raising it IS the consent.

Source Evidence

Same as §8.29, plus guardian-leave-portal.resolvers.ts.

Auth and Permissions

None — assertPortalAudience(actor, "guardian"), then the child is resolved through ownChildrenWhere.

Request

{
  "studentId": "...",
  "leaveTypePublicId": "0199a1e0-0000-7000-8000-0000000f5e01",
  "startDate": "2026-09-20",
  "endDate": "2026-09-22",
  "reason": "Family travel, pre-arranged with the class teacher.",
  "submissionId": "0199b300-0000-7000-8000-0000000000bb"
}

Response

201, GuardianLeaveRequestDto. Always status: pending_school, origin: guardian — skips pending_guardian entirely.

Side Effects

  • leave_requests insert.

Error Cases

HTTP StatusError CodeCondition
404STUDENT_NOT_FOUNDstudentId does not resolve through ownChildrenWhere — a stranger's child 404s before any other field is considered.
400VALIDATION_FAILEDendDate < startDate.
404/422/409LEAVE_TYPE_*, LEAVE_STUDENT_NOT_ENROLLED, LEAVE_RANGE_OUTSIDE_SESSION, LEAVE_RANGE_TOO_LONG, LEAVE_CALENDAR_NOT_GENERATED, LEAVE_NO_SCHOOL_DAYS_IN_RANGE, LEAVE_OVERLAPPING_REQUEST_EXISTSSame resolution failures as §8.27.
403STUDENT_GRADUATED_READ_ONLYThe resolved child's graduate access state is grace or ended, checked after the submissionId replay and after the ward resolution — so a foreign studentId still 404s first.

Note: NO LEAVE_NO_GUARDIAN_TO_CONSENT on this path — the caller IS a guardian, filing.

Edge Cases

  • A non-ward studentId: 404, never a 403 — an id space over a roll of children must not become an enumeration oracle.
  • A child whose graduate access has ENDED (not merely grace) never resolves as a ward at all — ownChildrenWhere excludes them, so the request 404s as STUDENT_NOT_FOUND rather than reaching the graduate-access check.

8.31 POST /api/mobile/guardian/leaves/:id/decision

Purpose

The guardian's consent (or refusal) on a child's self-raised or office-filed request — the ONE decision this flow exists for.

Source Evidence

Same as §8.29, plus leave-decision.service.ts.

Auth and Permissions

None — assertPortalAudience(actor, "guardian"), row re-resolved through own-children + pending_guardian + the addressed-guardian precedence (isAddressedGuardian).

Request

{ "decision": "approved", "note": "Consented — travel dates confirmed.", "expectedVersion": 0 }

expectedVersion OPTIONAL — omitted, the service uses the row's own current version (never refuses on that basis alone).

Response

200, GuardianLeaveRequestDto.

Side Effects

  • Approve: status → pending_school; decision row (step: guardian, actedAs: guardian). NO attendance job yet — that only arms on the SCHOOL step's approval.
  • Refuse: status → rejected (terminal); decision row. No permission was needed to reach this.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNot the caller's own child's request, not pending_guardian, not the addressed guardian, or genuinely absent — all four look identical.
409LEAVE_VERSION_CONFLICTStale version.
409LEAVE_CANNOT_DECIDE_OWN_REQUESTOnly reachable in practice if the guardian is somehow also the raiser of record.

Edge Cases

  • A stranger's child's request: 404, never 403.
  • The addressed-guardian precedence: legally flagged guardian first, then primary, then any live guardian — computed in JS over a small row set, not a correlated SQL subquery.

8.32 POST /api/mobile/guardian/leaves/:id/cancel

Purpose

Withdraws a request the guardian raised themselves, while pending.

Source Evidence

Same as §8.29.

Auth and Permissions

None — assertPortalAudience(actor, "guardian").

Request

{ "expectedVersion": 0 }

Response

200, GuardianLeaveRequestDto with status: cancelled.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNot raised by this caller, not pending, or absent.
409LEAVE_VERSION_CONFLICTStale version, when supplied.

Edge Cases

  • Cancelling AFTER guardian consent moved it to pending_school: still cancellable — the predicate accepts either pending status, and raised_by_user_id is still the guardian regardless of which step it's now at.

8.33 GET /api/mobile/teacher/leaves

Purpose

The class teacher's decision queue — pupil requests routed to a class they are the class teacher of, awaiting the school step.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/leave/portal/teacher/teacher-leave-portal.controller.ts
DTOdto/teacher-leave-portal.dto.ts
Serviceteacher-leave-portal.service.ts
Testsleave-portal.integration.spec.ts

Auth and Permissions

None — assertTeacherStaffRow(db, actor): the active role must be teacher, and the caller must have a live staff row.

Request

ListTeacherLeaveRequestsQueryDto — QueryDto fields only. pagination=false refused.

Response

Array of TeacherLeaveRequestDto (§6.20) — no reason field.

Error Cases

HTTP StatusError CodeCondition
403PERMISSION_INSUFFICIENTThe active role is not teacher (a teacher acting as staff included), or no live staff row.
400PAGINATION_LIMIT_INVALIDpagination=false.

Edge Cases

  • A class with class_teacher_id IS NULL: leaks to nobody — the predicate is never conditionally omitted.
  • Superadmin calling this route: 403 — the active role is superadmin, not teacher; handled by the panel as an empty state rather than an error surface.

8.34 POST /api/mobile/teacher/leaves/:id/decision

Purpose

The class teacher's approve/refuse decision — the terminal decision for a pupil's leave (subject to the admin-only reversal, §8.14).

Source Evidence

Same as §8.33, plus leave-decision.service.ts.

Auth and Permissions

None — assertTeacherStaffRow, row re-resolved through classes.class_teacher_id = staffId + pending_school + subject_kind = student.

Request

{ "decision": "approved", "note": "Approved — informed of the missed unit test.", "expectedVersion": 0 }

Response

200, TeacherLeaveRequestDto.

Side Effects

  • Approve: status → approved; decision row (actedAs: class_teacher); leave_request_runtime armed; LeaveJob.APPLY_ATTENDANCE enqueued.
  • Refuse: status → rejected; decision row.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNot this teacher's class, not pending_school, not subject_kind: student, or absent.
409LEAVE_VERSION_CONFLICTStale version.
409LEAVE_CANNOT_DECIDE_OWN_REQUESTThe class teacher raised the request themselves.

Edge Cases

  • This route can NEVER reach approved a second time or reverse it — the WHERE clause requires pending_school, so an approved row is invisible to it, not merely refused inside the transaction.
  • A staff (non-student) request: never matches — filtered out before the row is even locked.

8.35 GET /api/mobile/staff/leaves

Purpose

A staff member's own leave requests.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/leave/portal/staff/staff-leave-portal.controller.ts
DTOdto/staff-leave-portal.dto.ts
Servicestaff-leave-portal.service.ts
Testsleave-portal.integration.spec.ts

Auth and Permissions

None — assertStaffRow(db, actor): any active role that is not guardian/student scoped, plus a live staff row. The teacher role is not required — non-teaching staff use this route, and so does a teacher acting as either staff or teacher.

Request

ListMyStaffLeaveRequestsQueryDto — QueryDto fields only. pagination=false refused.

Response

Array of StaffLeaveRequestDto (§6.21) — reason present (the caller IS the subject); dayCount documented as CALENDAR days.

Error Cases

HTTP StatusError CodeCondition
403PERMISSION_INSUFFICIENTNo live staff row, or acting as a guardian/student portal role.
400PAGINATION_LIMIT_INVALIDpagination=false.

Edge Cases

  • A class teacher using this route sees only THEIR OWN staff-subject requests — never their pupils' (that's /api/mobile/teacher/leaves, a completely separate query).

8.36 POST /api/mobile/staff/leaves

Purpose

A staff member raises their own leave — no guardian step, no calendar gate.

Source Evidence

Same as §8.35.

Auth and Permissions

None — assertStaffRow(db, actor), as §8.35.

Request

{
  "leaveTypePublicId": "0199a1e0-0000-7000-8000-0000000f5e02",
  "startDate": "2026-09-20",
  "endDate": "2026-09-22",
  "reason": "Family emergency, need three days.",
  "submissionId": "0199b300-0000-7000-8000-0000000000cc"
}

Response

201, StaffLeaveRequestDto. Always status: pending_school, origin: self.

Side Effects

  • leave_requests insert.

Error Cases

HTTP StatusError CodeCondition
400VALIDATION_FAILEDendDate < startDate.
404LEAVE_TYPE_NOT_FOUNDBad type.
422LEAVE_TYPE_INACTIVE / LEAVE_TYPE_NOT_APPLICABLE_TO_SUBJECTRetired, or student-only type.
422LEAVE_RANGE_OUTSIDE_SESSION / LEAVE_RANGE_TOO_LONGSession-boundary or length failure.
409LEAVE_OVERLAPPING_REQUEST_EXISTSExclusion constraint.

Note: NO LEAVE_STUDENT_NOT_ENROLLED, LEAVE_CALENDAR_NOT_GENERATED, or LEAVE_NO_SCHOOL_DAYS_IN_RANGE — none of these questions apply to a staff subject.

Edge Cases

  • A leave request spanning a public holiday or exam block: accepted and counted in full — staff work those days and the register needs to describe them.

8.37 POST /api/mobile/staff/leaves/:id/cancel

Purpose

Withdraws a staff member's own request while it is pending.

Source Evidence

Same as §8.35.

Auth and Permissions

None — assertStaffRow(db, actor), as §8.35.

Request

{ "expectedVersion": 0 }

Response

200, StaffLeaveRequestDto with status: cancelled.

Error Cases

HTTP StatusError CodeCondition
404LEAVE_REQUEST_NOT_FOUNDNot raised by this caller, not pending_school, or absent.
409LEAVE_VERSION_CONFLICTStale version, when supplied.

Edge Cases

  • The cancellable-status set here is {pending_school} ONLY — narrower than the pupil/guardian portals' {pending_guardian, pending_school}, because a staff request never enters pending_guardian in the first place.

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence — decide (any surface)

9.3 Error Branch

EndpointPagination TypeDefault SizeMax SizeSort FieldsFiltersResult Cap
GET /api/leave-typespage/size, pagination=false ALLOWED20100 (paginated)label, else sortOrderisActive, appliesToStudent, appliesToStaff, includeDeleted, search (code/label)1000 unpaginated
GET /api/student-leaves, GET /api/staff-leavespage/size, pagination=false REFUSED20100startDate, else createdAtstatus, leaveTypeId, startDateFrom/startDateTo, includeDeleted, classId (pupil only), search (reason + name for pupils, reason only for staff)N/A — always paginated
Every /api/mobile/*/leaves listpage/size, pagination=false REFUSED20100Fixed — createdAt DESC, id DESC on every portal list; no sort parameter has any effectNone beyond the implicit own-scope predicateN/A

PaginationUtil.normalize/getDrizzleParams/buildMetadata are the shared implementation — see docs/BACKEND_RULES_DIGEST.md's pagination rules for the mechanism itself. The sort field's documented QueryDto default is updatedAt, but no leave list orders by it — the admin lists special-case sort=startDate only and fall through to createdAt for everything else (including the literal string "updatedAt"); every portal list ignores sort entirely.

11. Caching, Jobs, and External Integrations

IntegrationUsed?DetailsSource
Redis cacheNoNo cache key is built or read anywhere in this module.—
BullMQYesQueueName.LEAVE, two job names (LeaveJob.APPLY_ATTENDANCE, LeaveJob.REVERT_ATTENDANCE), always enqueued through the transactional outbox, never a direct queue.add().leave-decision.service.ts, student-leaves.service.ts/staff-leaves.service.ts (reapplyAttendance), leave-queue.processor.ts
External APINo—

13. Mandatory Deep API Documentation Pack

13.1 Route-by-Route Completeness Matrix

RouteController MethodDTOsService MethodGuardsPermissionsCacheJobsDB TouchesErrorsTestsDocumented?
GET /api/leave-typesLeaveTypesController.findAllListLeaveTypesQueryDtoLeaveTypesService.findAllJwt, RoleLeaveTypes_READN/AN/Aleave_types—YesYes
POST /api/leave-types.createCreateLeaveTypeDto.createJwt, RoleLeaveTypes_CREATEN/AN/Aleave_typesLEAVE_TYPE_CODE_ALREADY_EXISTS, LEAVE_TYPE_NOT_APPLICABLE_TO_SUBJECTYesYes
PATCH /api/leave-types/:publicId.updateUpdateLeaveTypeDto.updateJwt, RoleLeaveTypes_UPDATEN/AN/Aleave_typesLEAVE_TYPE_NOT_FOUND, LEAVE_TYPE_NOT_APPLICABLE_TO_SUBJECTYesYes
DELETE /api/leave-types/:publicId.remove—.removeJwt, RoleLeaveTypes_DELETEN/AN/Aleave_typesLEAVE_TYPE_NOT_FOUNDYesYes
POST /api/leave-types/:publicId/restore.restore—.restoreJwt, RoleLeaveTypes_RESTOREN/AN/Aleave_typesLEAVE_TYPE_NOT_FOUND, LEAVE_TYPE_CODE_ALREADY_EXISTSYesYes
GET /api/student-leavesStudentLeavesController.findAllListStudentLeavesQueryDto.findAllJwt, RoleStudentLeaves_READN/AN/Aleave_requests joinedPAGINATION_LIMIT_INVALIDYesYes
GET /api/student-leaves/summary.summarySame query.summaryJwt, RoleStudentLeaves_READN/AN/Aleave_requests—YesYes
GET /api/student-leaves/:id.findOne—.findOneJwt, RoleStudentLeaves_READN/AN/Aleave_requests joinedLEAVE_REQUEST_NOT_FOUNDYesYes
GET /api/student-leaves/:id/decisions.findDecisions—.findDecisionsJwt, RoleStudentLeaves_READN/AN/Aleave_request_decisionsLEAVE_REQUEST_NOT_FOUNDYesYes
POST /api/student-leaves.createCreateStudentLeaveDto.createJwt, RoleStudentLeaves_CREATEN/AN/Aleave_requests9 codes, listed §8.10YesYes
PATCH /api/student-leaves/:id.updateUpdateStudentLeaveDto.updateJwt, RoleStudentLeaves_UPDATEN/AN/Aleave_requestsLEAVE_REQUEST_NOT_FOUND, LEAVE_VERSION_CONFLICT, LEAVE_TYPE_*YesYes
DELETE /api/student-leaves/:id.remove—.removeJwt, RoleStudentLeaves_DELETEN/AN/Aleave_requestsLEAVE_REQUEST_NOT_FOUND, LEAVE_CANNOT_DELETE_APPROVEDYesYes
POST /api/student-leaves/:id/restore.restore—.restoreJwt, RoleStudentLeaves_RESTOREN/AN/Aleave_requestsLEAVE_REQUEST_NOT_FOUND, LEAVE_OVERLAPPING_REQUEST_EXISTSYesYes
POST /api/student-leaves/:id/decision.decideDecideLeaveDto.decide → LeaveDecisionService.decideJwt, RoleStudentLeaveDecisions_UPDATEN/ALeaveJob.APPLY_ATTENDANCE/REVERT_ATTENDANCEleave_requests, leave_request_decisions, leave_request_runtime, outbox_events4 codes, §8.14YesYes
POST /api/student-leaves/:id/reapply-attendance.reapplyAttendance—.reapplyAttendanceJwt, RoleStudentLeaveDecisions_UPDATEN/ALeaveJob.APPLY_ATTENDANCEoutbox_eventsLEAVE_REQUEST_NOT_FOUND, LEAVE_ALREADY_REVERTEDYesYes
GET/POST/PATCH/DELETE/:id/restore/:id/decision/:id/reapply-attendance /api/staff-leaves*StaffLeavesController.*Staff equivalentsStaffLeavesService.*Jwt, RoleStaffLeaves_*/StaffLeaveDecisions_UPDATEN/ASame two jobsleave_requests etc.Staff-equivalent set, §8.16–§8.25YesYes
GET/POST/:id/cancel /api/mobile/student/leaves*StudentLeavePortalController.*Portal DTOsStudentLeavePortalService.*Jwt, RoleNone (audience)N/AN/Aleave_requests§8.26–§8.28YesYes
GET/POST/:id/decision/:id/cancel /api/mobile/guardian/leaves*GuardianLeavePortalController.*Portal DTOsGuardianLeavePortalService.*Jwt, RoleNone (audience)N/ABoth jobs (via decide, on approve/reversal — reversal not reachable here in practice)leave_requests, decision/runtime/outbox on decide§8.29–§8.32YesYes
GET/:id/decision /api/mobile/teacher/leaves*TeacherLeavePortalController.*Portal DTOsTeacherLeavePortalService.*Jwt, RoleNone (audience)N/ALeaveJob.APPLY_ATTENDANCE (approve only)leave_requests, decision/runtime/outbox on decide§8.33–§8.34YesYes
GET/POST/:id/cancel /api/mobile/staff/leaves*StaffLeavePortalController.*Portal DTOsStaffLeavePortalService.*Jwt, RoleNone (audience)N/AN/Aleave_requests§8.35–§8.37YesYes

13.2 Request/Response Exhaustiveness

Example TypeRequired?Notes
Minimal valid requestProvided for every create/decide/cancel endpoint (§8)Smallest valid payload shown per route.
Full valid requestProvided where the DTO has meaningful optional fields (leave-type create with all flags, decide with note).
Authenticated requestEvery example — no public/guest route exists in this module.
Admin requestEvery §8.1–§8.25 example.Permission requirement stated per route.
Success responseEvery route, §8.Full envelope shown.
Empty-list responseCovered in Edge Cases per list route (§8.1, §8.6, §8.26, §8.29, §8.33, §8.35).
Validation errorCovered in Error Cases, every write route.
Domain errorCovered exhaustively, every route — this module's error surface IS mostly domain errors.
Rate-limit/auth/permission error401/403 covered per route (§5, §8). No 429 — no rate limit specific to this module.

13.3 API Diagram Pack

DiagramPresent
Route ownership graph§9.1
Sequence diagram per major endpoint family§9.2 (decide, the most complex family); create/list/cancel are simpler variants of the same controller→service→DB shape.
Activity diagram per write/action flowFeature doc §5, §6 (per-flow branch tables serve this role at API-doc depth).
Error decision tree§9.3
Auth and permission flow§5
Data contract map§6 (DTO reference) doubles as this — request DTO → service command → response DTO is 1:1 named per endpoint.
Cache flowN/A — no cache.
Async/job flow§11, Backend doc §9.
Realtime/event flowN/A — no realtime event.

13.4 Consumer Integration Notes

ConsumerRequired KnowledgeFailure HandlingContract Stability
Web/admin panel (office screens)Module_ACTION permissions per route (§4); version is a STRING on admin DTOs; pagination cannot be disabled on request lists.Render errorCode from the envelope; 404 on a row and 409 on a domain rule need different UI treatment (retry vs. reload).Stable — these routes back the existing office leave screens.
Web/admin panel (portal screens)No permission check to render around — gate navigation on LeaveView_READ instead, since the routes themselves are audience-asserted, not permission-gated; version is a NUMBER on portal DTOs (inconsistent with the admin DTOs — read the type, do not assume).A 404 on decide/cancel means "not yours, wrong status, or gone" — show a generic "this request is no longer available" rather than trying to distinguish.Stable.
QAFixtures need a live guardian/enrolment/calendar for MOST create paths to succeed — LEAVE_NO_GUARDIAN_TO_CONSENT/LEAVE_CALENDAR_NOT_GENERATED are the two most common setup gaps in a fresh test.Reproduce via the exact query filters documented per list endpoint.—
Internal serviceN/A today — nothing outside this module calls into leave services directly; consumption is exclusively via HTTP or the queue payload (LeaveAttendancePayload).The queue payload carries only {leaveRequestId, requestId, v} — a consumer must re-fetch current state, never trust a snapshot.Stable — v: 1 is the only version this payload has ever had.

13.5 API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
Pagination stylePage/size, alwaysCursor pagination (exists elsewhere in this codebase as a dead utility)Consistency with every other list in this module family; leave lists are not high-volume enough to need keyset paging.Deep-page performance degrades like any offset scheme.MAX_PAGE_SIZE caps size at 100; admin/portal lists never allow unbounded reads.
Auth mode on portal routesAudience assertion, no @Permissions()Grant *LeaveDecisions_*/*Leaves_* to staff/teacher/guardian/student roles directlyWould hand every teacher the power to decide any pupil's leave, unscoped (TEACHER_PERMISSIONS = STAFF_PERMISSIONS by reference).A permission-less route is one omission away from a 403-for-everyone outage if its allowlist entries are dropped.Mirrored in role.guard.ts AND route-permissions.spec.ts — two independent gates.
Error shape on row-scope mismatch404 uniformly, never 403Distinguish "not found" from "not yours" from "wrong status" with distinct codesPrevents an id space over a roll of children becoming an enumeration oracle.A legitimate caller cannot tell WHY their request failed from the status code alone.The message string differs per condition even when the code does not; four codes remain reserved for a future, deliberate narrowing (§16.8 backend risk).
Async attendance writeApprove/reverse return before the register updatesSynchronous write inside the decide transactionKeeps decide latency constant regardless of leave length.A caller polling the register immediately after approval may see it unchanged.leave_request_runtime (internal) tracks completion; reapply-attendance is the manual recovery path if it never catches up.
version type: string (admin) vs. number (portal)Both exist, differ by surfaceNormalize to one type everywhereNot attempted — the portal DTOs pass through the raw bigint-typed column as a JS number; the admin mapper explicitly String()s it.A client sharing code between the two surfaces must not assume the type.Documented here and per-DTO in §6; not treated as a bug to silently "fix" in this pass.

13.6 API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
Adding a new leave_request_status value (e.g. a formal "escalated" step)All 37 routes' status-enum surfaces; every status CHECK on leave_requestsLeaveDecisionService.resolveNextStatus needs a new branch; the implication-style CHECKs (§5.2 backend doc) are specifically shaped to make ALTER TYPE ... ADD VALUE safeNew enum label; no existing row affectedYes — enum alterationAdditive; existing consumers reading status as a closed set need to be told before the value can appear on a live row
Adding a decision permission split for a currently-implicit path (e.g. giving staff a peer-review step)role.guard.ts allowlist, permission-catalog.ts, seed-auth.tsNew permission module; new grant decisionNonePossibly — a new PERMISSION_MODULES entry needs permissions:syncNew capability, opt-in per role
Wiring the 'leave' notification categoryNone of the 37 routes directly — a new outbox emission inside LeaveDecisionService/portal create pathsNew NotificationService call inside existing transactionsNew notification_event rowsNo — the category and its two CHECK constraints already existAdditive; no existing consumer's contract changes

14. Zero-Omission API Checklist

  • Every controller route is documented (§4, §8 — 37 routes).
  • Every parent route prefix and runtime URL is documented (§4).
  • Every DTO field, nested field, enum, default, transform, and validator is documented (§6, §7).
  • Every response field, nullable field, generated field is documented (§6).
  • Every auth, guard, permission, and guest identity branch is documented (§5).
  • Every success, validation, auth, permission, not-found, conflict, and rate-limit (none) branch is documented (§8 per route).
  • Every database read/write, queue job, and audit consequence is documented (§8 Side Effects, §11).
  • Every route has examples for minimal request, success response, and representative failures (§8).
  • Every endpoint family has route, sequence, and error diagrams (§9).
  • Every tradeoff and compatibility risk is documented (§13.5, §13.6).
  • The API doc links to backend and features/flows docs (below).

15. Integration Checklist

  • Every route from controllers is documented — 37/37.
  • Every DTO field is documented (§6).
  • Every enum value is documented (§7).
  • Every response envelope is documented (§8, ResponseDto shape).
  • Every error code is documented (§8 per route; all 21 LEAVE_* codes accounted for, 4 explicitly marked unused).
  • Every auth guard and permission (or its deliberate absence) is documented (§5).
  • Every queue job is documented (§11, Backend §9); no cache key or external call exists.
  • Every diagram matches the current code (§9).
  • The API doc links to backend and features/flows docs.

See Also

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

On this page

Leave - API Reference1. Documentation Evidence2. Module Summary3. Concepts and Terminology4. API Surface Map5. Auth, Identity, and Permissions6. DTO and Model Reference6.1 CreateLeaveTypeDto (request, admin POST /api/leave-types)6.2 UpdateLeaveTypeDto (request, admin PATCH /api/leave-types/:publicId)6.3 ListLeaveTypesQueryDto (query, admin GET /api/leave-types)6.4 LeaveTypeDto (response)6.5 CreateStudentLeaveDto (request, admin POST /api/student-leaves)6.6 UpdateStudentLeaveDto (request, admin PATCH /api/student-leaves/:id)6.7 ListStudentLeavesQueryDto (query, admin GET /api/student-leaves)6.8 CreateStaffLeaveDto / UpdateStaffLeaveDto / ListStaffLeavesQueryDto (staff admin equivalents)6.9 CreateLeaveRequestBaseDto (shared base)6.10 UpdateLeaveRequestBaseDto (shared base)6.11 ListLeaveRequestsQueryDto (shared admin query base)6.12 LeaveRequestDto (response, admin)6.13 LeaveDecisionDto (response, admin — GET .../:id/decisions)6.14 LeaveSummaryDto (response, admin GET .../summary)6.15 DecideLeaveDto (request, admin POST .../:id/decision)6.16 CreateGuardianLeaveRequestDto (request, mobile POST /api/mobile/guardian/leaves)6.17 GuardianLeaveDecisionDto / GuardianCancelLeaveRequestDto (request)6.18 GuardianLeaveRequestDto (response, mobile)6.19 CreateStudentLeaveRequestDto / CancelLeaveRequestDto / StudentLeaveRequestDto (mobile, pupil self-serve)6.20 ListTeacherLeaveRequestsQueryDto / TeacherLeaveDecisionDto / TeacherLeaveRequestDto6.21 CreateStaffLeaveRequestDto / StaffCancelLeaveRequestDto / StaffLeaveRequestDto7. Enum Reference8. Endpoint Reference8.1 GET /api/leave-typesPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.2 POST /api/leave-typesPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge CasesExample Requests8.3 PATCH /api/leave-types/:publicIdPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.4 DELETE /api/leave-types/:publicIdPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.5 POST /api/leave-types/:publicId/restorePurposeSource EvidenceAuth and PermissionsResponseSide EffectsError CasesEdge Cases8.6 GET /api/student-leavesPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.7 GET /api/student-leaves/summaryPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases8.8 GET /api/student-leaves/:idPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases8.9 GET /api/student-leaves/:id/decisionsPurposeSource EvidenceAuth and PermissionsResponseError CasesEdge Cases8.10 POST /api/student-leavesPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.11 PATCH /api/student-leaves/:idPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.12 DELETE /api/student-leaves/:idPurposeSource EvidenceAuth and PermissionsResponseError CasesEdge Cases8.13 POST /api/student-leaves/:id/restorePurposeSource EvidenceAuth and PermissionsResponseError CasesEdge Cases8.14 POST /api/student-leaves/:id/decisionPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.15 POST /api/student-leaves/:id/reapply-attendancePurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.16–8.25 Staff-leaves admin routes (/api/staff-leaves)8.26 GET /api/mobile/student/leavesPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases8.27 POST /api/mobile/student/leavesPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.28 POST /api/mobile/student/leaves/:id/cancelPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.29 GET /api/mobile/guardian/leavesPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases8.30 POST /api/mobile/guardian/leavesPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.31 POST /api/mobile/guardian/leaves/:id/decisionPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.32 POST /api/mobile/guardian/leaves/:id/cancelPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases8.33 GET /api/mobile/teacher/leavesPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases8.34 POST /api/mobile/teacher/leaves/:id/decisionPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.35 GET /api/mobile/staff/leavesPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases8.36 POST /api/mobile/staff/leavesPurposeSource EvidenceAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.37 POST /api/mobile/staff/leaves/:id/cancelPurposeSource EvidenceAuth and PermissionsRequestResponseError CasesEdge Cases9. Flow Diagrams9.1 Route Ownership9.2 Request Sequence — decide (any surface)9.3 Error Branch10. 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