# GetLeaveBalance

## Overview

Computes a worker's leave balance for a given `leaveTypeKey` as of a date — a derived aggregate, not a stored entity. There is no `LeaveBalance` model: the balance is reconstructed from the ledger for the as-of date. For each LeaveGrant whose validity window contains the as-of date and that had not lapsed by then, the remaining amount is recomputed from `grantedDays` minus the reserving consumption that had started on or before the as-of date — never read from the current stored `remainingDays` — so a past as-of date yields the balance as it stood then, not today's. It also returns the soonest upcoming expiration among the contributing grants.

## Business Rules

- Balance is reconstructed from the ledger as of the `asOf` date (default: today): for each LeaveGrant for the given `workerId` and `leaveTypeKey` whose `[grantedDate, expirationDate]` window contains `asOf` and that had not lapsed by `asOf` (`expiredAt` unset, or set to a moment after `asOf`), remaining = `grantedDays` − reserving consumption started on or before `asOf`; the balance is the SUM of the positive remainings
- The remaining is derived, never read from the stored `remainingDays` column — so a grant that has since been consumed or expired still reports its *as-of* remaining for a past `asOf`, rather than today's value
- Reserving consumption = `LeaveConsumption` lines whose `LeaveRequest` is APPROVED, PENDING, or CANCEL_PENDING and whose `startDate` is on or before `asOf`; REJECTED / CANCELLED requests no longer reserve and are excluded
- Excludes grants outside the as-of validity window — a grant not yet started or already past its `expirationDate` on that date does not contribute
- Also returns the soonest upcoming `expirationDate` among the contributing grants that still have a positive remaining as of that date, so the caller can prompt use-before-lapse
- Never reads a cached/denormalized balance column — this query is the only way to know a balance, by design (ADR-005)

## Process Flow

```mermaid
flowchart TD
    A[Caller requests balance for worker + leaveTypeKey + asOf date] --> B[Load grants for workerId and leaveTypeKey granted on or before asOf]
    B --> C[Keep grants whose validity window contains asOf and that had not lapsed by asOf]
    C --> D[Sum reserving consumption started on or before asOf per grant]
    D --> E[remaining = grantedDays - consumed; balance = sum of positive remainings]
    C --> F[Find soonest expirationDate among grants with positive remaining]
    E --> G[Return balance + next expiration]
    F --> G
```

## External Dependencies

- [leave-management::LeaveGrant](../model/LeaveGrant.md) model — the grant ledger this query aggregates; there is deliberately no LeaveBalance model (ADR-005)
- [leave-management::LeaveConsumption](../model/LeaveConsumption.md) model — the consumption ledger joined to reconstruct the as-of remaining
- [leave-management::LeaveRequest](../model/LeaveRequest.md) model — supplies each consumption's `startDate` and reserving status
- [leave-management::LeaveType](../model/LeaveType.md) model — referenced by `leaveTypeKey`
- [workforce::Worker](../../../workforce/docs/model/Worker.md) model — referenced by `workerId`

## Error Scenarios

- **LEAVE_TYPE_NOT_FOUND**: no LeaveType exists for the given id/key
- **WORKER_NOT_FOUND**: no workforce Worker exists for the given id

## Test Cases

- returns a balance reconstructed from grantedDays minus reserving consumption as of the date
- a grant that has since lapsed still contributes its as-of remaining for a past asOf within its window
- restricts reconstruction to reserving consumption started on or before asOf
- excludes grants outside the asOf validity window
- returns the soonest upcoming expiration among contributing grants
- returns a zero balance with no upcoming expiration when the worker has no active grants
- returns LEAVE_TYPE_NOT_FOUND for an unknown leaveTypeKey

> Note: `returns WORKER_NOT_FOUND for an unknown workerId` is a documented error scenario above,
> but is a KNOWN GAP not yet implemented in `getLeaveBalance.ts` (workerId references workforce's
> Worker cross-module, with no query injection wired into module.ts yet — referential integrity is
> left to the DB-level FK constraint for now). It is intentionally omitted from this Test Cases
> list until implemented; re-add it here once the check and its test land.
