# CreateAccrualPlan

## Permission Scope

`accrualPlan`

## Overview

Defines a new effective-dated grant rule (`AccrualPlan`) for a balance-backed `LeaveType`, establishing the initial generation used to compute HIRE and ANNIVERSARY grants written into the `LeaveGrant` ledger. Per ADR-025 the grant timing is expressed as configuration (eligibility delay, tiers, cap, expiration) rather than hard-coded behaviour.

## Business Rules

- May only reference a `LeaveType` whose `requiresBalance` is true (only balance-backed leave accrues)
- Creates the first generation with `effectiveStart` set and `effectiveEnd` null (the current, open generation)
- `accrualMethod` is optional and defaults to `FRONT_LOAD_TENURE` (the only method implemented today: front-load at the eligibility date + tenure-tier escalation on each anniversary); it is the discriminator the anniversary grant batch dispatches on, and must be one of the recognized accrual methods
- `grantType` is optional and defaults to `STATUTORY`; it is the provenance stamped on every grant this plan produces and must be one of `STATUTORY` / `MANUAL`
- `eligibilityDelayMonths` must be a non-negative integer (0 = day one/current policy, 6 = statutory, 12 = CN/CA); the first grant's `grantedDate` = hire date + `eligibilityDelayMonths`
- `baseGrantDays` must be a non-negative decimal; half-day increments are valid — it is the front-loaded tier-0 amount granted at the eligibility date
- `tenureTiers` must be an ordered tenure-to-entitlement mapping (`{ yearsOfService, grantDays }`) where `grantDays` is the **total** entitlement at that tenure (e.g. 11 at 1y, 12 at 2y, 14 at 3y, 16 at 4y, 18 at 5y, 20 at 6y+), monotonic non-decreasing in both `yearsOfService` and `grantDays`; each tier's `grantDays` must not exceed `annualCapDays` when a cap is set
- `annualCapDays` is optional; when set it bounds the granted amount — `min(applicable tier total ?? baseGrantDays, annualCapDays)`; null = uncapped. Replaces the hard-coded 20-day cap
- `expirationMonths` is required (non-null in v1) and must be a positive integer (24 for statutory annual leave 2-year statute of limitations); it determines a STATUTORY grant's `expirationDate` = grant date + `expirationMonths`, so every STATUTORY grant has an expiration
- `appliesToEmploymentTypeId` constrains which workforce `WorkerEmployment` employment types this plan governs (referenced by EmploymentType catalog id); the anniversary batch only grants to matching employments. When present it is validated against the workforce catalog: the EmploymentType must exist and be ACTIVE (null/omitted = applies to all, needs no lookup)
- `grantCondition` is optional (ADR-026 C): null/omitted = type NONE = unconditional grant. 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 accepted and persisted by this command. Jurisdiction- or company-specific conditions such as attendance rate belong in these fields; their semantic validation is owned by the consuming application
- At most one plan generation may be effective per (`leaveTypeKey`, employment type) at any given date — non-overlap is enforced across ALL generations, not only open ones: the new open-ended plan is rejected if any existing generation for the same combination is still open OR ends on/after the new `effectiveStart` (a closed past generation overlapping the new start is caught)
- A future-dated `effectiveStart` schedules the generation ahead of time without governing grants until that date
- **Backward compatibility**: the pre-ADR-025 front-load policy is exactly `eligibilityDelayMonths=0` / `annualCapDays=20` / `expirationMonths=24`

## Process Flow

```mermaid
flowchart TD
    A[Admin submits leaveTypeKey, eligibilityDelayMonths, baseGrantDays, tenureTiers, annualCapDays, expirationMonths, appliesToEmploymentTypeId, grantCondition] --> B{leaveTypeKey requiresBalance?}
    B -- No --> R1[Reject: LEAVE_TYPE_NOT_BALANCE_BACKED]
    B -- Yes --> C{Open generation already exists for leaveTypeKey + employment type?}
    C -- Yes --> R2[Reject: DUPLICATE_ACTIVE_PLAN]
    C -- No --> V{appliesToEmploymentTypeId set and ACTIVE in workforce catalog?}
    V -- No --> R4[Reject: EMPLOYMENT_TYPE_NOT_FOUND / EMPLOYMENT_TYPE_NOT_ACTIVE]
    V -- Yes/null --> D[Validate eligibilityDelayMonths, baseGrantDays, tenureTiers, annualCapDays, expirationMonths, grantCondition]
    D -- Invalid --> R3[Reject: validation error]
    D -- Valid --> E[Insert generation: effectiveStart = given date, effectiveEnd = null]
    E --> F[Plan available to HIRE/ANNIVERSARY grant events effective on or after effectiveStart]
```

## External Dependencies

- [leave-management::LeaveType](../model/LeaveType.md) - the referenced `leaveTypeKey` must resolve to an active leave type with `requiresBalance = true`
- workforce `EmploymentType` - `appliesToEmploymentTypeId` is validated at write time via the injected `getEmploymentType` query (must exist and be ACTIVE); it also constrains which employment types the anniversary batch grants for (cross-module)

## Error Scenarios

- **LEAVE_TYPE_NOT_FOUND**: no LeaveType exists for the given id/key
- **LEAVE_TYPE_NOT_BALANCE_BACKED**: the referenced leave type has `requiresBalance = false`
- **DUPLICATE_ACTIVE_PLAN**: an existing generation (open, or closed but ending on/after the new `effectiveStart`) already overlaps the new plan for the (`leaveTypeKey`, employment type) combination
- **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
- **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

## Test Cases

- creating a plan with a balance-backed leaveTypeKey succeeds and returns effectiveStart set, effectiveEnd null
- creating a plan for a leaveTypeKey with requiresBalance = false is rejected
- creating a plan for a leaveTypeKey with no matching LeaveType is rejected
- creating a plan for an employment type that already has an active generation is rejected
- creating a plan that overlaps a closed past generation is rejected (M11)
- creating a plan whose appliesToEmploymentTypeId does not resolve to a workforce EmploymentType is rejected
- creating a plan whose appliesToEmploymentTypeId resolves to an INACTIVE EmploymentType is rejected
- creating a plan with a future effectiveStart schedules it without affecting current grants
- creating a plan with eligibilityDelayMonths = 6 (statutory waiting period) succeeds
- creating a plan with a negative eligibilityDelayMonths is rejected
- creating a plan with negative baseGrantDays is rejected
- creating a plan whose tenureTiers exceed annualCapDays is rejected
- creating a plan with annualCapDays = null grants uncapped tiers
- creating a plan with a non-positive expirationMonths is rejected
- creating a plan with grantCondition type MIN_WORKED_DAYS and minWorkedDays 240 succeeds
- creating a plan with grantCondition type MIN_WORKED_DAYS and no minWorkedDays is rejected
- creating a plan persists configured custom fields
- creating a plan without accrualMethod or grantType applies the FRONT_LOAD_TENURE and STATUTORY defaults
- creating a plan with explicit accrualMethod and grantType stores them
- creating a plan with an unrecognized accrualMethod is rejected
- creating a plan with an unrecognized grantType is rejected
