# ApplyDueAppointments

## Permission Scope

`appointment`

## Overview

ApplyDueAppointments carries out the Assignment side effect of future-dated appointments once their `effectiveDate` has arrived. RecordAppointment applies CREATE_* actions and non-future CHANGE/END immediately, but a future-dated CHANGE_ASSIGNMENT or END_ASSIGNMENT is only appended to AppointmentHistory as upcoming (`appliedAt` null); this batch is what actually transfers or ends the assignment on its date. It is meant to run periodically (e.g. daily).

## Business Rules

- Selects pending appointments — `appliedAt` is null and `effectiveDate` is on or before the cutoff (`asOf`, default today) — oldest first, so several scheduled changes to the same assignment apply in chronological order
- A CHANGE_ASSIGNMENT entry is applied via the transfer path (continue the versionOf series at `toPositionId`), then the entry is re-pointed to the newly opened generation and stamped `appliedAt`. Any OTHER still-pending appointment that referenced the now-closed generation is also re-pointed onto the new one, so a later scheduled CHANGE/END on the same assignment series applies against the open generation instead of failing forever on the generation it was recorded against
- An END_ASSIGNMENT entry is applied via the termination path (close the current generation at `effectiveDate`), then stamped `appliedAt`
- Stamping `appliedAt` makes the batch idempotent: an already-applied appointment is never selected or applied again
- Best-effort: an entry whose assignment change is no longer valid (for example the assignment was already ended by an earlier appointment) is left pending and counted as failed rather than aborting the whole batch
- CREATE_* appointments are applied at record time and never pending; a stray one is stamped defensively without a side effect

## Process Flow

```mermaid
flowchart TD
    A[Run batch with asOf cutoff] --> B[Load pending appointments effective on or before asOf, oldest first]
    B --> C{For each entry: action?}
    C -- CHANGE_ASSIGNMENT --> D[transferAssignment to toPositionId on effectiveDate]
    C -- END_ASSIGNMENT --> E[endAssignment at effectiveDate]
    D -- ok --> F[Re-point entry to new generation; set appliedAt; applied++]
    D -- error --> G[Leave pending; failed++]
    E -- ok --> H[Set appliedAt; applied++]
    E -- error --> G
    F --> I[Return applied / failed counts]
    G --> I
    H --> I
```

## External Dependencies

- [workforce::AppointmentHistory](../model/AppointmentHistory.md) model — the upcoming appointments this batch applies and stamps
- [workforce::Assignment](../model/Assignment.md) model — mutated through the transfer/termination paths
- [workforce::AppointmentType](../model/AppointmentType.md) model — supplies each appointment's action

## Error Scenarios

_None — the batch never rejects; an appointment whose change cannot be applied is left pending and reported in the failed count._

## Test Cases

- applies a due future-dated transfer, re-points the appointment to the new generation, and stamps appliedAt
- applies a due future-dated termination and stamps appliedAt
- does not select an appointment whose effectiveDate is still in the future
- does not re-apply an appointment that is already applied
- leaves a due appointment pending and counts it as failed when its assignment change is rejected
- applies multiple due appointments oldest-first
- applies a CHANGE then a due END on the same assignment, the END inheriting the CHANGE's new generation
