# ExpireLeaveGrants

## Permission Scope

`grant`

## Overview

Daily batch job that sweeps all `LeaveGrant` records past their `expirationDate`, zeroing any unused remainder and stamping `expiredAt`, forfeiting the remainder while preserving its history.

## Business Rules

- Runs daily; selects grants where `expirationDate < today`, `remainingDays > 0`, and `expiredAt` is not yet set
- Sets `remainingDays` to 0 and stamps `expiredAt` = run date; the grant row itself is never deleted
- Once expired (`expiredAt` set), a grant participates in neither balance calculation nor FIFO consumption allocation
- Idempotent: a grant already expired (`expiredAt` set) is left untouched by subsequent runs
- A grant already fully consumed (`remainingDays = 0`) before its expiration date is not stamped `expiredAt` by the sweep — it was consumed, not forfeited

## Process Flow

```mermaid
flowchart TD
    A[Daily batch trigger] --> B[Select LeaveGrants where expirationDate < today]
    B --> C{remainingDays > 0 and expiredAt not set?}
    C -- No --> D[Leave grant untouched - already expired or already exhausted]
    C -- Yes --> E[Set remainingDays = 0]
    E --> F[Stamp expiredAt = today]
    F --> G[Grant excluded from balance and FIFO allocation going forward]
```

## External Dependencies

- [leave-management::LeaveGrant](../model/LeaveGrant.md) - the grants swept and stamped expired by this batch

## Error Scenarios

- **EXPIRATION_SWEEP_PARTIAL_FAILURE**: an individual grant update fails during the sweep and is retried on the next run without blocking other grants (batch job; no user-facing error scenarios otherwise)

## Test Cases

- days past their expiration date stop counting after the nightly sweep and are recorded as expired
- a grant already expired is left unchanged by a subsequent run
- a grant with `remainingDays = 0` before its expiration date is not stamped `expiredAt` by the sweep
- continues processing remaining grants and reports the failure count when one update fails

