# LeaveRequest

## Description

LeaveRequest represents one worker-initiated leave request and its entire lifecycle, from filing through approval, rejection, withdrawal, and the cancellation of an already-approved leave. It records the subject (`workerId`), the organizational "whose" (`assignmentId` — the workforce Assignment effective on the leave date, which resolves department/site/employment; ADR-016), the leave type (`leaveTypeKey`), the `startDate` / `endDate` requested, a mandatory `reason`, and the resolution trail (`resolvedBy`, `resolvedAt`).

> **Implementation note (issue #39).** The db model (`db/leaveRequest.ts`) has no `specialSubtype` field and no persisted `approverComment` field. A rejection's approver comment is passed to the bundled `approval` module as a command input, not stored on this row. A per-request `specialSubtype` was never built (it would pair with the dropped `LeaveType.specialSubtypeSet`). Both are documented here as absent rather than implied.

**Unification decision (ADR-011).** The legacy attendance module modeled this as two separate stateful entities — `TimeOffRequest` (PENDING → APPROVED/REJECTED/CANCELLED) and a distinct `TimeOffCancelRequest` (its own PENDING → APPROVED/REJECTED) for taking back an approved leave. The redesign folds both into **one** `LeaveRequest` lifecycle by introducing an intermediate `CANCEL_PENDING` state: withdrawing a still-PENDING request is unilateral (`withdrawLeave` → CANCELLED, no approval), while cancelling an already-APPROVED leave routes through a second approval leg (`requestLeaveCancel` → CANCEL_PENDING, then `approveLeaveCancel` → CANCELLED or `rejectLeaveCancel` → back to APPROVED). One model owns the whole story, so the consumption ledger has a single lifecycle to hang off of rather than two entities to keep consistent.

Approval routing is delegated to the bundled `approval` module as a wrapper (ADR-003): each pending decision is mirrored by a direct-mode approval request (one step, leave-approver role, quorum ANY), linked by `targetEntityId`. The mirroring is wired — `requestLeave` / `requestLeaveCancel` create the approval request through the injected approval seam and `approveLeave` / `rejectLeave` / `approveLeaveCancel` / `rejectLeaveCancel` locate and resolve it, so `targetEntityId` holds the real mirroring ApprovalRequest id (see the invariant below). Cross-module failures are re-wrapped as this module's errors (NOT_ASSIGNEE / SELF_APPROVAL / APPROVAL_STEP_FAILED). Balance-backed leave reserves its days from the `LeaveGrant` ledger at filing time; on any reversal the reserved days are restored to their originating grants. By design, leave-management does not write into `time-tracking`: time-tracking calculates actual worked time from punches and has no reverse dependency on leave-management (a leave day is simply the absence of worked blocks), so nothing is emitted into the attendance record — `LeaveType.timeEntryCodeKey` is the mapping a downstream payroll/reporting consumer uses to render or pay an approved leave, keeping the dependency one-directional (leave-management → time-tracking, pull-based).

## Domain Model Definitions

### Model type

Stateful

#### State Transitions

```mermaid
stateDiagram-v2
    [*] --> PENDING: requestLeave
    PENDING --> APPROVED: approveLeave
    PENDING --> REJECTED: rejectLeave
    PENDING --> CANCELLED: withdrawLeave
    APPROVED --> CANCEL_PENDING: requestLeaveCancel
    CANCEL_PENDING --> CANCELLED: approveLeaveCancel
    CANCEL_PENDING --> APPROVED: rejectLeaveCancel
    APPROVED --> [*]
    REJECTED --> [*]
    CANCELLED --> [*]
```

### Command Definitions

Command docs are out of scope for this design phase (ADR-011). Anticipated commands:

- requestLeave — file a leave request; reserve ledger days for balance-backed types; create the mirroring approval request
- approveLeave — approver approves a PENDING request; resolve the mirroring approval request as approved
- rejectLeave — approver rejects a PENDING request; restore any reserved days
- withdrawLeave — requester unilaterally withdraws their own PENDING request; restore any reserved days
- requestLeaveCancel — requester files a cancellation of an APPROVED leave; move to CANCEL_PENDING and create a mirroring approval request
- approveLeaveCancel — approver approves the cancellation; restore reserved days to their originating grants and resolve the mirroring cancellation approval request
- rejectLeaveCancel — approver rejects the cancellation; the leave stays APPROVED with no ledger or time-tracking change

### Query Definitions

- getLeaveRequest — retrieve a single request by id, any status
- listLeaveRequestsByWorker — a worker's requests, paginated
- listPendingLeaveApprovals — requests awaiting a decision (PENDING or CANCEL_PENDING) for an approver's inbox, joined to the mirroring approval request
### Models

- LeaveRequest

### Invariants

- `status` is one of PENDING, APPROVED, REJECTED, CANCELLED, CANCEL_PENDING; REJECTED and CANCELLED are terminal (normalized naming — CANCELLED, per ADR-006)
- `leaveTypeKey` references an active LeaveType; `reason` is required and non-empty
- `assignmentId` must reference a workforce Assignment effective on the requested dates and belonging to the same `workerId`'s employment (the request's "whose")
- At most one non-terminal request (PENDING, APPROVED, or CANCEL_PENDING) exists per `workerId` for any given calendar date: a new filing is rejected when its `startDate`–`endDate` range overlaps an existing non-terminal request's range (overlap = `existing.startDate <= new.endDate` AND `existing.endDate >= new.startDate`); REJECTED and CANCELLED do not count toward this constraint
- For a `requiresBalance` LeaveType, the days required (1.0 per full day, 0.5 per half day, over the target dates) are reserved from the ledger at filing and recorded as LeaveConsumption rows; a non-terminal balance-backed request always has consumption rows summing to its required days, and non-deducting types (SPECIAL, MENSTRUAL) have zero rows
- APPROVED and REJECTED are reachable only from PENDING; the resolver is never the requester (self-approval blocked, ADR-003)
- The APPROVED → CANCELLED path is reachable **only** through CANCEL_PENDING via `approveLeaveCancel`; there is no direct APPROVED → CANCELLED transition. CANCEL_PENDING is reachable only from APPROVED
- `withdrawLeave` (PENDING → CANCELLED) is unilateral and needs no approval; `approveLeaveCancel` (CANCEL_PENDING → CANCELLED) requires an approval decision
- Every transition into CANCELLED, and every transition into REJECTED, restores all non-restored LeaveConsumption for this request in the same transaction; approving leave keeps the consumption
- One-directional time-tracking boundary: leave-management never emits or reverts a day status into `time-tracking`. time-tracking calculates worked time from punches and has no reverse dependency on leave-management (a leave day is simply the absence of worked blocks), so approval and cancellation change only this module's state and ledger; the LeaveType's `timeEntryCodeKey` is a mapping a downstream payroll/reporting consumer reads to render or pay an approved leave (pull-based)
- `resolvedBy` and `resolvedAt` are set on any resolution (approve / reject / cancel decision); a REJECTED or rejected-cancel decision requires a non-empty approver comment, which is passed to the `approval` module rather than persisted on this row
- **Approval seam (ADR-003, issue #39):** `targetEntityId` links each pending decision to exactly one active `approval` request. Approval routing is wired: `requestLeave` / `requestLeaveCancel` create the mirroring approval request through the injected approval seam and store its real ApprovalRequest id in this column, and `approveLeave` / `rejectLeave` / `approveLeaveCancel` / `rejectLeaveCancel` locate that request by the LeaveRequest id and resolve it. Filing is rejected when the sole eligible approver is the requester (NOT_ASSIGNEE)

### Relationships

- **Belongs to Worker** (workforce, cross-module): `workerId` is the subject and the ledger owner whose grants are drawn from
- **References Assignment** (workforce, cross-module): `assignmentId` establishes the organizational "whose" (department/site/employment) effective on the leave date
- **References LeaveType** (by `leaveTypeKey`): drives balance deduction; its `timeEntryCodeKey` is the mapping downstream payroll/reporting consumers use to render or pay the leave
- **Has many LeaveConsumption**: each ledger allocation for this request, retained for audit even after restoration
- **Resolved by User** (user-management, cross-module): `resolvedBy` references the approver who approved or rejected
- **Mirrored by an approval request** (approval, bundled): an `ApprovalRequest` whose `targetEntityId` points back to this row (ADR-003)
- **Mapped to time-tracking** (cross-module, one-directional): leave-management does not emit into `time-tracking`; the LeaveType's `timeEntryCodeKey` maps an approved leave to a work-rules TimeEntryCode that downstream payroll/reporting consumers read to render or pay it, keeping the dependency pull-based (leave-management → time-tracking)
