# UpdateAccrualPlan

## Permission Scope

`accrualPlan`

## Overview

Records a rule change to an `AccrualPlan` (eligibility delay, base/tiers, cap, expiration, applicability, grant condition, or proportional tiers) as a new effective-dated generation, closing the prior generation without overwriting it (ADR-013).

## Business Rules

- Effective-dated generation model (ADR-013): update closes the current open generation (`effectiveEnd` = day before the change) and inserts a new generation sharing the same `versionOf`
- Generations sharing a `versionOf` must never overlap in their effective ranges
- Past grants remain explained by the generation effective on their grant date; no historical grant is retroactively affected by the change
- A future-dated `effectiveStart` on the new generation schedules it ahead without governing grants until that date
- Only mutable rule fields (`accrualMethod`, `grantType`, `eligibilityDelayMonths`, `baseGrantDays`, `tenureTiers`, `annualCapDays`, `expirationMonths`, `appliesToEmploymentTypeId`, `grantCondition`) may change between generations; the `leaveTypeKey` binding is preserved across generations of the same plan
- `accrualMethod` is optional: omitted = keep the current generation's method; when supplied it must be one of the recognized accrual methods (`FRONT_LOAD_TENURE` today) and carries into the new generation
- `grantType` is optional: omitted = keep the current generation's provenance; when supplied it must be one of `STATUTORY` / `MANUAL` and carries into the new generation
- New-generation values are validated as in CreateAccrualPlan: `eligibilityDelayMonths` a non-negative integer; `baseGrantDays` a non-negative half-day-increment decimal; `tenureTiers` monotonic non-decreasing with each `grantDays` within `annualCapDays`; `annualCapDays` optional (null = uncapped); `expirationMonths` a positive integer, required (non-null in v1)
- `grantCondition` is optional (ADR-026 C): null = clear (type NONE = unconditional grant), omitted = keep the current generation's gate. The generic `MIN_WORKED_DAYS` condition requires a positive integer `minWorkedDays`; `referenceMonths`, when present, must be a positive integer; an unrecognized `type` is rejected — a jurisdiction-specific gate is expressed as an AccrualPlan custom field read by the application eligibility policy, not as an unknown enum value
- Custom fields configured through `defineModule({ accrualPlan: { fields } })` are mutable. Omitted custom fields are inherited from the current generation; supplied values replace them in the new generation. The consuming application owns semantic validation of jurisdiction-specific condition fields
- Cannot update a plan whose current generation has already been retired (no open generation to close)
- When this update sets `appliesToEmploymentTypeId` to a concrete id, it is validated against the workforce catalog (must exist and be ACTIVE); an untouched or cleared (null = applies to all) value is not re-checked

## Process Flow

```mermaid
flowchart TD
    A[Admin submits rule change for an AccrualPlan effective on date D] --> B{Plan has an open generation?}
    B -- No --> R1[Reject: PLAN_ALREADY_RETIRED]
    B -- Yes --> C{New effectiveStart overlaps current open generation?}
    C -- Yes --> R2[Reject: OVERLAPPING_GENERATION]
    C -- No --> D2[Validate new eligibilityDelayMonths / baseGrantDays / tenureTiers / annualCapDays / expirationMonths / grantCondition]
    D2 -- Invalid --> R3[Reject: validation error]
    D2 -- Valid --> V{appliesToEmploymentTypeId set and ACTIVE in workforce catalog?}
    V -- No --> R4[Reject: EMPLOYMENT_TYPE_NOT_FOUND / EMPLOYMENT_TYPE_NOT_ACTIVE]
    V -- Yes/unchanged --> E[Close current generation: effectiveEnd = D - 1]
    E --> F[Insert new generation sharing versionOf: effectiveStart = D, effectiveEnd = null]
    F --> G[Past grants keep resolving to the generation effective on their grant date]
```

## External Dependencies

- [leave-management::AccrualPlan](../model/AccrualPlan.md) - closes the current generation and inserts a new one sharing `versionOf`
- workforce `EmploymentType` - a concrete `appliesToEmploymentTypeId` in this update is validated via the injected `getEmploymentType` query (must exist and be ACTIVE); it also still constrains the anniversary batch after the change (cross-module)

## Error Scenarios

- **ACCRUAL_PLAN_NOT_FOUND**: no AccrualPlan exists for the given id
- **PLAN_ALREADY_RETIRED**: the plan has no open generation to close
- **OVERLAPPING_GENERATION**: the new generation's `effectiveStart` would overlap the currently open generation
- **INVALID_ELIGIBILITY_DELAY_MONTHS**: `eligibilityDelayMonths` is negative or not an integer
- **INVALID_BASE_GRANT_DAYS**: `baseGrantDays` is negative or not a valid half-day increment
- **INVALID_TENURE_TIERS**: the tier table is malformed, not monotonic non-decreasing, or a tier's `grantDays` exceeds `annualCapDays`
- **INVALID_EXPIRATION_MONTHS**: `expirationMonths` is missing/null, zero, or negative
- **INVALID_GRANT_CONDITION**: the generic `grantCondition` is malformed — `type` must be NONE or MIN_WORKED_DAYS, MIN_WORKED_DAYS requires a positive integer `minWorkedDays`, and `referenceMonths` when present must be a positive integer
- **INVALID_ACCRUAL_METHOD**: accrualMethod is not a recognized accrual method
- **INVALID_GRANT_TYPE**: grantType is not one of STATUTORY / MANUAL
- **EMPLOYMENT_TYPE_NOT_FOUND**: the workforce EmploymentType referenced by `appliesToEmploymentTypeId` does not exist
- **EMPLOYMENT_TYPE_NOT_ACTIVE**: the workforce EmploymentType referenced by `appliesToEmploymentTypeId` is not ACTIVE

## Test Cases

- editing a plan closes the prior generation and inserts a new one with no range overlap
- a future-dated plan generation does not govern grants until its effective date
- grants made before the change remain explained by the prior generation
- updating a retired plan is rejected
- updating a non-existent AccrualPlan id is rejected
- a new effectiveStart that does not come after the current generation's effectiveStart is rejected
- revising tenureTiers beyond annualCapDays is rejected
- revising eligibilityDelayMonths to a negative value is rejected
- revising baseGrantDays to a negative value is rejected
- revising expirationMonths to a non-positive value is rejected
- revising grantCondition to type MIN_WORKED_DAYS with minWorkedDays 240 creates a new generation carrying the gate
- revising grantCondition to type MIN_WORKED_DAYS without minWorkedDays is rejected
- custom fields are inherited when omitted and replaced when supplied
- the previous generation's built-in columns are never carried over as custom fields
- revising accrualMethod and grantType carries the new values into the new generation
- omitting accrualMethod and grantType keeps the current generation's values
- revising accrualMethod to an unrecognized value is rejected
- revising grantType to an unrecognized value is rejected
- revising appliesToEmploymentTypeId to an id that does not resolve to a workforce EmploymentType is rejected
- revising appliesToEmploymentTypeId to an INACTIVE EmploymentType is rejected
