# paid-leave-ledger

## Overview

The Paid Leave Ledger owns the `LeaveGrant` records — the single source of truth for every leave balance (ADR-005). Days enter the ledger through three paths: a front-loaded hire grant, a daily anniversary batch driven by the effective `AccrualPlan`, and manual grants by administrators (statutory top-ups, compensatory leave at a fixed 1.0 day / 30-day expiry, or discretionary MANUAL days). Days leave the ledger through leave consumption (owned by the request lifecycle, allocating FIFO by soonest expiration) and through a daily expiration sweep that forfeits remainders past their expiration date.

Balances are **never cached**: the balance query sums the remaining days of unexpired, in-window grants per leave type on demand. This is the whole point of ADR-005 — the legacy design kept a denormalized `Employee.paidLeave` column that drifted from the ledger and needed repair scripts; removing the cache entirely removes that class of bug. There is deliberately no `LeaveBalance` model.

## Business Purpose

- **Drift-free balances**: the ledger-only design (ADR-005) structurally eliminates the denormalized balance-cache bugs and manual repair scripts that plagued the legacy system
- **Auditability without an audit module**: expired grants keep their history (`expiredAt`), and every consumption is a restorable, permanent line item — the ledger reconciles end to end (ADR-013)
- **Half-day precision**: decimal `remainingDays` represents 0.5-day consumption naturally
- **Use-before-lapse transparency**: the balance query surfaces the soonest upcoming expiration so workers consume days before forfeiture
- **Administrator discretion for compensatory leave**: compensatory leave is granted manually and never auto-linked to holiday-work approval (legacy parity — the decision stays with the administrator)

## Process Flow

Days enter the ledger through three grant paths.

```mermaid
flowchart TD
    A[Eligibility date = hire + eligibilityDelayMonths] --> B[Front-load STATUTORY baseGrantDays, expiration = grantDate + expirationMonths]
    C[Daily anniversary batch] --> D{Eligible employment and anniversary today and no grant yet for this date?}
    D -- No --> E[Skip - idempotent]
    D -- Yes --> F["Grant min(applicable tier total ?? baseGrantDays, annualCapDays) STATUTORY days per effective AccrualPlan"]
    G[Admin grantLeave] --> H{grantType}
    H -- STATUTORY --> I[Arbitrary positive days; expiration defaults from plan.expirationMonths unless overridden]
    H -- COMPENSATORY compensatory leave --> J[Fixed 1.0 day, expiration = grantDate + 30 days]
    H -- MANUAL --> K[Discretionary days with explicit expiration]
```

Days leave the ledger through FIFO consumption (via the request lifecycle) and nightly expiration.

```mermaid
flowchart TD
    A[LeaveGrant ledger] --> B[requestLeave allocates FIFO by soonest expiration, decrements remainingDays, records LeaveConsumption]
    B --> C{Request outcome}
    C -- Approved --> D[Days remain consumed]
    C -- Rejected / withdrawn / cancellation approved --> E[Days restored to the exact same grants]
    A --> F[Daily expiration sweep]
    F --> G{expirationDate < today and remainingDays > 0 and not yet expired?}
    G -- Yes --> H[Set remainingDays to 0 and stamp expiredAt]
    G -- No --> I[Leave grant untouched]
```

Balances are derived on demand — there is no cached balance column.

```mermaid
flowchart TD
    A[getLeaveBalance for worker + leaveTypeKey + asOf date] --> B[Load unexpired grants whose validity window contains the date]
    B --> C[Sum remainingDays]
    B --> D[Find soonest upcoming expiration among grants with remaining days]
    C --> E[Return balance + next expiration info]
    D --> E
```

## Scenario Patterns

- **Hire grant**: an eligible new hire's front-load grant (at hire + `eligibilityDelayMonths`, 0 for the current front-load policy) is written STATUTORY/HIRE, expiring per the plan's `expirationMonths`
- **Anniversary grant**: the daily batch grants STATUTORY/ANNIVERSARY days to matching employments on their hire anniversary, capped at the plan's `annualCapDays` (20 for statutory annual leave)
- **Idempotent batch re-run**: re-running the anniversary batch on the same day skips workers who already received their grant for that date (at most one ANNIVERSARY grant per worker, leave type, and grant date)
- **Manual statutory adjustment**: an admin grants arbitrary positive STATUTORY days (0.5 increments allowed) to top up or correct
- **Compensatory grant**: an admin manually grants exactly 1.0 COMPENSATORY day expiring 30 days later; deliberately not auto-linked to holiday-work approval
- **FIFO consumption and restoration (cross-feature)**: a request draws soonest-expiring grants first and records consumption lines; rejection, withdrawal, or approved cancellation restores the exact grants drawn from (see [leave-request-approval](leave-request-approval.md))
- **Split allocation**: when the soonest-expiring grant has fewer remaining days than requested, the draw spans two grants, each its own consumption line
- **Expiration sweep**: the daily batch zeroes remainders past expiration and stamps `expiredAt`, preserving the forfeiture history
- **Balance inquiry**: `getLeaveBalance` sums unexpired, in-window remainders and reports the soonest upcoming expiration — no cached column is read

## Test Cases

- an eligible hire shows a balance equal to the plan's `baseGrantDays` at the eligibility date (hire + `eligibilityDelayMonths`), expiring per `expirationMonths`
- the anniversary batch grants the correct tiered amount and never grants twice for the same worker, leave type, and date
- a manually granted compensatory day is always exactly 1.0 day and lapses 30 days after the grant date
- the balance query is derived purely from the ledger with no denormalized column anywhere
- a consumption splits across grants when the soonest-expiring grant is insufficient alone
- days past their expiration date stop counting after the nightly sweep and are recorded as expired
- rejected, withdrawn, or cancelled leave restores days to the exact grants they were drawn from

## Reference Links

- Data-model design research (ledger as source of truth, no balance cache): https://github.com/tailor-sandbox/Omakase-ERP-attendance/issues/7
- Enterprise gap analysis (legacy dual-management of balance vs ledger): https://github.com/tailor-sandbox/Omakase-ERP-attendance/issues/9
- [Labor Standards Act Article 39 (Annual Paid Leave) — e-Gov Law Search](https://laws.e-gov.go.jp/law/322AC0000000049) — statutory paid-leave entitlement the ledger guarantees (ADR-009)
- [KING OF TIME Support Center — Managing remaining paid-leave days](https://support.ta.kingoftime.jp/hc/ja) — balance/grant ledger management benchmarked per ADR-009
- [Jobcan Attendance Management Help — Paid-leave balance and grant history](https://jobcan.zendesk.com/hc/ja) — grant history and derived balance benchmarked per ADR-009
