# Break Policy Yearly Refresh Runbook

**Spec:** 049 — Configurable Break Policy System
**Section:** T5.5.4 / T5.7.2

---

## Overview

Each calendar year (or when a jurisdiction updates its requirements) the Break Policy
preset library needs a new version. The yearly refresh is an **opt-in per-store flow**:
stores continue running on their adopted version until a manager chooses to update
(ADR-8).

---

## Preset-Update Process

### 1. Author the migration

Create a new JSON file in `userfrosting/migrations/input/` following the
`049_NNN_preset_<key>_<year>.json` naming convention.

Use `userfrosting/migrations/input/049_099_preset_ca_2026_example.json.example`
as a template. The migration must:

1. **Mark the old preset as superseded** — `UPDATE breakPolicyPreset SET supersededByPresetKey = 'CA-2026'` for the old `CA-2025` row. Use `check_query` targeting `supersededByPresetKey IS NOT NULL` so the operation is idempotent.
2. **Insert the new preset row** — new `presetKey`, same `jurisdiction`/`jurisdictionCode`, incremented `version`, new `effectiveDate`.
3. **Insert preset rules** — copy or modify rules as required by the updated legislation.

Key invariants:
- `presetKey` for the new year must be distinct (e.g., `CA-2026`).
- `version` on the new row is the incremented integer (e.g., `2`).
- `supersededByPresetKey` on the OLD row points to the new key.

### 2. Run `conductor run`

```bash
php userfrosting/conductor run
```

The conductor applies all unapplied migrations. After this completes:
- The new preset rows exist in `kiosk_buykiosk.breakPolicyPreset` and
  `breakPolicyPresetRule`.
- Existing store policies are **not changed** — they still reference `CA-2025`
  with their own cloned rule set.

### 3. Stores see the Update banner

The next time a manager loads Settings → Break Policy for a CA store, the
`BreakPolicyController::getPolicy` endpoint returns `updateAvailable: true`
because `checkUpdateAvailable()` detects:

- **(a) Higher version**: the latest preset for `CA-2025` key now reports
  `supersededByPresetKey = 'CA-2026'`, **OR**
- **(b) Newer version of same key**: if you bump the version on an existing key
  rather than issuing a new key, `currentPreset->version > policy->basedOnPresetVersion`.

Either condition sets `updateAvailable: true` and the frontend renders the
"Update available" banner.

### 4. Manager opt-in via diff preview

The manager clicks the banner, reviews the diff (added/removed/modified rules),
and confirms. `POST /admin/:typeNum/scheduling/break-policy/preset` with
`{ "presetKey": "CA-2026" }` clones the new preset rules into the store policy.

### 5. Historical data is unaffected

Pre-existing `breakComplianceLog` rows are never modified. Each row stores a
`details.ruleSnapshot` JSON blob (written by the evaluator at evaluation time,
T3.2.2a). The compliance dashboard renders historical violations using the
snapshot, so old rows remain meaningful even after the store adopts CA-2026.

---

## Schema Invariants (ADR-2)

**Adoption clones rules, it does not live-bind them.**

When a store adopts a preset:
1. `BreakPolicyRepository::adoptPreset()` clones each `breakPolicyPresetRule` row
   into a new `storeBreakPolicyRule` row in the **per-store DB**.
2. Each cloned rule carries `sourcePresetRuleId` pointing back to the library rule
   it was cloned from (for diff/reset UX).
3. Future changes to `breakPolicyPresetRule` in the shared DB have **zero effect**
   on existing store policies — stores must explicitly opt in to the new version.

This means:
- A store on CA-2025 runs its own rule copies forever, regardless of preset updates.
- A store that adopts CA-2026 gets new rule copies; the CA-2025 copies are deleted
  and replaced by `adoptPreset()`.
- Historical `breakComplianceLog` rows continue to reference the **rule ID that
  existed at evaluation time** via `ruleId` + `details.ruleSnapshot`.

---

## Compliance Log Preservation

`breakComplianceLog.details` is a JSON column. The evaluator writes a
`ruleSnapshot` key containing a copy of the rule's scalar fields at the time
of evaluation (T3.2.2a):

```json
{
  "ruleSnapshot": {
    "ruleType": "meal",
    "durationMinutes": 30,
    "isRequired": true,
    "premiumPayOnViolation": true,
    "premiumPayHours": 1.0
  }
}
```

The compliance dashboard uses `ruleSnapshot` to display rule details for
historical violations even after the store adopts a newer preset and the
original `storeBreakPolicyRule` row is replaced. **Never delete or compact
`details` on existing log rows.**

---

## Testing in Dev

### Option A — Insert fake v2 via the targeted migration skill

1. Author the new migration JSON.
2. Use the `buyerkiosk-conductor-targeted-migration` skill to apply it against
   a single dev store without running all 150+ other migrations.
3. Reload Settings → Break Policy — the Update banner should render.

### Option B — Direct SQL (dev only, never on prod)

```sql
-- Mark CA-2025 as superseded (dev DB: kiosk_buykiosk)
UPDATE breakPolicyPreset
SET supersededByPresetKey = 'CA-2026', updatedAt = NOW()
WHERE presetKey = 'CA-2025';
```

Then reload Break Policy for any CA pilot store and confirm `updateAvailable: true`
in the JSON response at
`GET /admin/pc00/scheduling/break-policy/policy`.

### Using apply-049-store-policy-schema.php

The dev helper at `scripts/apply-049-store-policy-schema.php` can seed a test
store with a CA policy. After running it, the store's `storeBreakPolicy` row
will have `basedOnPresetKey = 'CA-2025'` and `basedOnPresetVersion = 1`. A
superseded-preset or version-bump in the library will then make the Update
banner appear on next page load.

---

## Rollback

Because adoption is **opt-in and per-store**, there is no fleet-wide rollback
concern. If a store accidentally adopts an incorrect preset:

1. Re-adopt the previous preset key via the diff/preview flow.
2. The store's `storeBreakPolicyRule` rows are replaced with the previous
   preset's values; `basedOnPresetKey` / `basedOnPresetVersion` revert.
3. Historical `breakComplianceLog` rows are unaffected (they carry snapshots).

To suppress the Update banner globally (e.g., a bad migration was deployed),
revert the `supersededByPresetKey` column on the old preset row:

```sql
-- Revert supersession (dev DB only — use migration in prod)
UPDATE breakPolicyPreset
SET supersededByPresetKey = NULL, updatedAt = NOW()
WHERE presetKey = 'CA-2025';
```
