# accrual-plans

## Overview

Accrual Plans define the **effective-dated grant schedules** for balance-backed leave. A plan governs the grants written into the `LeaveGrant` ledger — each stamped with the plan's configurable `grantType` provenance (`STATUTORY` / `MANUAL`) — and, per ADR-025, expresses its grant timing as configuration rather than hard-coded behaviour: an `accrualMethod` naming which method the grant batch runs, an eligibility delay (waiting period) before the first grant, a base amount with a tenure escalation table, a data-driven annual cap, and an expiration policy. The only accrual method implemented today is `FRONT_LOAD_TENURE` (front-load at the eligibility date + tenure-tier escalation); naming it explicitly lets one model span both the legacy front-loaded policy and the statutory Labor Standards Act Article 39 waiting period, and leaves room for additional methods to be added — with their own batch handlers, without a schema migration — as other jurisdictions' models come online (ADR-018).

Because a grant rule is exactly the kind of thing that changes with law revisions or policy updates, an AccrualPlan is **effective-dated** (ADR-013): a change closes the current generation and inserts a new one, so any past grant is explainable by the plan generation effective on its grant date, and a future rule change can be scheduled ahead of time.

**Scope**: the `accrualMethod` discriminator exists but has a single implemented method today — `FRONT_LOAD_TENURE`. The plan also carries the generic **grant-condition gate** `grantCondition`, whose `MIN_WORKED_DAYS` condition is evaluated by the anniversary batch via time-tracking `aggregateWorkedDays`. Consumers add jurisdiction-specific condition data, such as attendance-rate configuration or proportional-grant tables, as top-level `AccrualPlan` custom fields through `defineModule({ accrualPlan: { fields } })`; create/update commands preserve them across effective-dated generations. The optional `grantEligibility.evaluate` policy receives those typed fields and the proposed generic amount, so the app can reject, defer, or adjust a grant without copying the batch. The `CONTINUOUS` method, calendar-year anchor, and carryover modes remain deferred behind the ADR-018 global GO gate.

An application-specific condition is composed at the module boundary:

```ts
const leaveManagement = defineLeaveManagementModule({
  accrualPlan: {
    fields: {
      attendanceRateCondition: db.object(
        {
          threshold: db.decimal(),
          referenceMonths: db.int(),
        },
        { optional: true },
      ),
    },
  },
  grantEligibility: {
    evaluate: async (trx, { candidate, accrualPlan, proposedGrantDays }, ctx) => {
      if (!accrualPlan.attendanceRateCondition) return { status: "ELIGIBLE" };

      // The application owns calendar, deemed-attendance, validation, and failure semantics.
      return evaluateAttendanceRate(
        trx,
        { candidate, condition: accrualPlan.attendanceRateCondition, proposedGrantDays },
        ctx,
      );
    },
  },
  // workforce, timeTracking, userManagement, and approval dependencies omitted
});
```

## Business Purpose

- Guarantee at least the Labor Standards Act Article 39 statutory paid-leave entitlement, while letting the early-grant policy (which strictly exceeds the legal minimum) and a strictly-statutory policy be selected as configuration on the same model
- Make the grant timing — waiting period, escalation, cap, expiration — externalised data so rules can be corrected and audited without code changes (ADR-011, ADR-022 pattern)
- Version rules over time so a grant is always explainable by the rule in force when it was made, and law changes can be scheduled with a future effective date (ADR-013)
- Reward tenure via the escalation table (+1 day at 1 year up to +10 from 6 years, capped by `annualCapDays`) to encourage retention
- Reserve structural room for non-Japanese accrual models (waiting-period and continuous-accrual jurisdictions) without committing to global activation yet (ADR-018)

## Process Flow

```mermaid
flowchart TD
    A[Admin defines AccrualPlan for a balance leaveTypeKey] --> B[Generation 1: effectiveStart = D0, effectiveEnd = null]
    B --> C{Rule change e.g. law revision}
    C -- Change on date D --> E[Close current: effectiveEnd = D - 1]
    E --> F[Insert new generation: effectiveStart = D, effectiveEnd = null]
    C -- No change --> G[Current generation stays open]
    F --> H[Grant events read the plan generation effective on the grant date]
    G --> H
    H --> I{First grant or anniversary?}
    I -- First --> P[Eligibility date = hire + eligibilityDelayMonths; grantSource = HIRE]
    I -- Anniversary --> Q[Hire anniversary; grantSource = ANNIVERSARY]
    P --> J
    Q --> J["Grant min(applicable tier total ?? baseGrantDays, annualCapDays) days; expiration = grantDate + expirationMonths"]
    J --> L[Evaluate generic MIN_WORKED_DAYS and optional application grantEligibility policy, then insert LeaveGrant]
```

## Scenario Patterns

- **Front-load company policy (current operation)**: `eligibilityDelayMonths=0` — on hire, an eligible employment receives `baseGrantDays` (e.g. 10) STATUTORY days expiring `expirationMonths` later, day-one entitlement ahead of the statutory schedule
- **Statutory mode (statutory compliance)**: `eligibilityDelayMonths=6` — the first grant lands at the 6-month mark, matching Labor Standards Act Article 39's waiting period. A `grantCondition` of `MIN_WORKED_DAYS` (e.g. 240 over 12 months) is evaluated at grant time via the injected time-tracking worked-days aggregate; below the threshold no grant is made that period
- **Jurisdiction-specific condition**: define a top-level custom field such as `attendanceRateCondition`, validate it in the application, and inject `grantEligibility.evaluate` to calculate the app's working-calendar/deemed-attendance policy. The evaluator receives the effective plan row and proposed amount and returns ELIGIBLE, INELIGIBLE, or NOT_EVALUABLE; an ELIGIBLE amount override must still be a half-day increment within the plan's `annualCapDays`, so an app policy cannot silently exceed the rule it was configured with
- **Waiting-period jurisdictions**: `eligibilityDelayMonths=12` expresses China / Canada's one-year qualifying period on the same model
- **Anniversary escalation**: on the hire anniversary, a FULL_TIME employment receives the applicable tenure tier's total entitlement (11 at 1y, 12 at 2y, up to 20 at 6y+), capped at `annualCapDays`
- **Cap as data**: the annual cap is `annualCapDays` (20 for statutory annual leave), not a code constant; raising it is a versioned rule change, not a deploy
- **Germany approximation**: `FRONT_LOAD` + `eligibilityDelayMonths=6` approximates the German full-entitlement timing; the §5 pro-rata Teilurlaub during the wait is intentionally out of scope (ADR-025 decision C)
- **Employment-type applicability**: a plan with `appliesToEmploymentTypeId=FULL_TIME` grants only to full-time employments; other types are skipped
- **Rule change is versioned**: switching from front-load to statutory mode inserts a new plan generation; grants made before the change remain explained by the prior generation
- **Future-dated rule change**: a plan generation with a future `effectiveStart` is scheduled and does not govern grants until its start date

## Test Cases

- Creating an AccrualPlan with valid fields should succeed
- A plan with `eligibilityDelayMonths=0` reproduces the current front-load behaviour exactly (first grant at hire)
- A plan with `eligibilityDelayMonths=6` places the first grant at the 6-month mark, not at hire
- The first grant (at the eligibility date) is written `grantSource=HIRE`; subsequent hire-anniversary grants are `grantSource=ANNIVERSARY`
- An eligible new hire receives `min(applicable tier total ?? baseGrantDays, annualCapDays)` with expiration = grant date + `expirationMonths`
- A FULL_TIME employment gains 11 days on the first anniversary and reaches `annualCapDays` (20) from the sixth anniversary onward, per the tier table
- The annual cap is read from `annualCapDays` (data), and a grant is never issued above it; a null `annualCapDays` means uncapped
- `createAccrualPlan` rejects a non-monotonic `tenureTiers`, a tier `grantDays` exceeding `annualCapDays`, a negative `eligibilityDelayMonths`, or a non-positive `expirationMonths`
- An employment whose type is not in `appliesToEmploymentTypeId` receives no grant
- 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
- Querying `asOf` a past date returns the plan generation effective then

## Reference Links

- **ADR-025: Generalization of AccrualPlan grant methods** — this feature's governing decision (discriminator + parameters + handler dispatch, v1/v2 split)
- **ADR-026: AccrualPlan v2 first installment — completion of Japan's statutory mode** — grant-condition gate (`MIN_WORKED_DAYS` evaluated in B1) and the time-tracking `aggregateWorkedDays` injection
- **ADR-013: Effective Dating design standard** — versioned generations
- **ADR-018: Global support is future scope (reserving structural room)** — v3 (CONTINUOUS + global) GO gate (the cross-module worked-days gate shipped in v2 per ADR-026; only CONTINUOUS/global remain behind this gate)
- **ADR-022: Model simplification (ComplianceRule generalization)** — the discriminator + nullable-parameter + handler pattern this feature follows
- Data-model design research (rule externalization, effective-dated configuration): https://github.com/tailor-sandbox/Omakase-ERP-attendance/issues/7
- Enterprise gap analysis (legacy accrual numbers hard-coded in a batch): 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 baseline: 10 days after 6 months at 80% attendance, rising to 20 with tenure; the front-load policy exceeds this minimum (lawful, ADR-009)
- Global benchmark (ADR-009, 10 countries): statutory paid-leave grant timing splits into continuous-accrual (UK/AU/FR/NL), waiting-period (JP/DE/CN/CA/IN), and no-statutory (US federal); no surveyed country front-loads the full amount on day one by statute
