# LeaveGrant

## Description

LeaveGrant is the **grant ledger** — one record per grant event and the single source of truth for leave balances (ADR-005). There is no denormalized balance cache anywhere in this module: a worker's balance for a leave type is always computed on demand by summing `remainingDays` over that worker's non-expired grants whose validity window contains the as-of date. This is the core of ADR-005, carried forward and generalized from the legacy `PaidLeaveGrant`: the twin bug source of the old design — a cached `Employee.paidLeave` column that drifted from the ledger and needed repair scripts — is structurally removed by never storing a balance at all.

Each grant records who it belongs to (`workerId` — the ledger is keyed to the **Worker**, the person, so entitlement survives transfers and re-assignment; ADR-016), the leave type (`leaveTypeKey`), its entitlement basis (`grantType`: STATUTORY for law-based annual leave, COMPENSATORY for compensatory leave, MANUAL for a discretionary administrative grant), its provenance (`grantSource`: HIRE front-load, ANNIVERSARY batch, MANUAL administrator action), the originally granted amount (`grantedDays`), the still-available amount (`remainingDays`), its validity window (`grantedDate` → `expirationDate`), and an `expiredAt` stamp once the nightly expiration sweep forfeits an unused remainder.

Day amounts are decimal, so half-day (0.5) consumption is natural. Consumption and restoration are never performed directly on a grant; they flow exclusively through the `LeaveRequest` lifecycle, which draws from the soonest-expiring grant first (FIFO by `expirationDate`) and records each draw as a `LeaveConsumption` line so days can be restored to the exact grant they came from.

## Domain Model Definitions

### Model type

Standard

### Command Definitions

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

- grantLeave — administrator manual grant (STATUTORY adjustment, COMPENSATORY (compensatory leave) fixed 1.0 day / 30-day expiry, or MANUAL discretionary days)
- runAnniversaryLeaveGrants — daily batch creating anniversary grants for eligible employments per the effective AccrualPlan
- expireLeaveGrants — daily batch zeroing remainders past `expirationDate` and stamping `expiredAt`
- (cross-feature, via LeaveRequest) requestLeave / rejectLeave / withdrawLeave / approveLeaveCancel — decrement or restore `remainingDays` through the ledger, recording LeaveConsumption lines

### Query Definitions

- getLeaveGrant — retrieve a single grant by id
- getLeaveBalance — sums `remainingDays` of a worker's non-expired, in-window grants per `leaveTypeKey` as of a date, plus the soonest upcoming expiration (balance-by-query; there is no LeaveBalance model)
- listLeaveGrantsByWorker — a worker's grant history for a leave type, soonest-expiring first, paginated
- listExpiringLeaveGrants — grants with remaining days approaching expiration, for use-before-lapse prompts
### Models

- LeaveGrant

### Invariants

- Balances are derived, never stored: there is no denormalized balance column anywhere; the balance for a (worker, `leaveTypeKey`) is the SUM of `remainingDays` over non-expired grants whose `[grantedDate, expirationDate]` window contains the as-of date (ADR-005)
- `grantType` is one of STATUTORY, COMPENSATORY, MANUAL; `grantSource` is one of HIRE, ANNIVERSARY, MANUAL
- HIRE and ANNIVERSARY `grantSource` imply STATUTORY `grantType` and are produced from an effective AccrualPlan; COMPENSATORY and MANUAL `grantType` always have `grantSource` MANUAL (there is no automated compensatory or discretionary batch)
- `grantedDays` is a positive decimal; half-day (0.5) increments are valid
- `remainingDays` is always between 0 and `grantedDays` inclusive
- `expirationDate` is strictly after `grantedDate`
- A batch STATUTORY grant (HIRE / ANNIVERSARY) has `expirationDate` = `grantedDate` + the effective AccrualPlan's `expirationMonths` (24 months for statutory annual leave); a manual STATUTORY grant (via grantLeave) defaults its expiration from the same `expirationMonths` but may carry an administrator-supplied override for a correction. A COMPENSATORY (compensatory leave) grant is granted in 1.0-day units expiring `grantedDate` + 30 days
- While unexpired (`expiredAt` unset): `remainingDays` decreases only through leave consumption and increases only by restoring previously recorded consumption — never above `grantedDays`
- Once expired (`expiredAt` set): `remainingDays` is 0 and the grant participates in neither balance calculation nor FIFO allocation
- FIFO allocation: consumption always draws from the unexpired grant with the soonest `expirationDate` first, splitting across grants when one is insufficient
- The unrestored consumption recorded against a grant always equals `grantedDays` minus `remainingDays` (ledger consistency with its consumption lines)
- At most one batch grant exists per (`workerId`, `leaveTypeKey`, `grantedDate`, `grantSource`) — the batch's idempotency boundary keyed by `grantSource`, so a HIRE and an ANNIVERSARY grant on the same date are distinct, not duplicates; a MANUAL grant on the same date is likewise not a duplicate

### Relationships

- **Belongs to Worker** (workforce, cross-module): `workerId` owns the ledger; entitlement follows the person across employment and assignment changes (ADR-016), unlike a request's Assignment-scoped "whose"
- **References LeaveType** (by `leaveTypeKey`): the balance bucket this grant contributes to
- **Driven by AccrualPlan**: HIRE and ANNIVERSARY grants derive days and expiration from the plan generation effective on `grantedDate`
- **Consumed through LeaveConsumption**: each consumption line links this grant to a LeaveRequest, recording days drawn and, when returned, a `restoredAt` stamp
- **Balance is a query, not a model**: there is deliberately no LeaveBalance entity; `getLeaveBalance` aggregates this ledger (ADR-005)
