# Product Requirements Document

**Spec ID:** 049
**Feature:** Configurable Break Policy System
**Owner:** Scheduling / Timesheet team
**Status:** Approved for scoping — implementation pending
**Origin:** May 2026 customer reports that unpaid breaks were leaking into payroll totals, plus draft schedules surfacing to staff. Hot-fixes for both shipped (already in master). Root-cause analysis surfaced a deeper gap: break paid/unpaid classification is hardcoded and inflexible.

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Problem statement is specific and measurable
- [x] Problem is validated by evidence (customer reports + code audit)
- [x] Context → Problem → Solution flow makes sense
- [x] Every persona has at least one user journey
- [x] All MoSCoW categories addressed (Must/Should/Could/Won't)
- [x] Every feature has testable acceptance criteria
- [x] Every metric has corresponding tracking events
- [x] No feature redundancy
- [x] No contradictions between sections
- [x] No technical implementation details included (deferred to SDD)
- [x] A new team member could understand this PRD

---

## Product Overview

### Vision
Replace BuyerKiosk's hardcoded break classification with a per-store, rules-driven break policy that decides paid/unpaid from punch facts, ships with state-aware presets, and surfaces compliance violations — so payroll is accurate, store operators meet labor law without becoming labor lawyers, and employees no longer hold a classification lever they shouldn't.

### Problem Statement
Today, BuyerKiosk treats every break the same way regardless of store, state, or labor law:

- The `TimePunch` model has `BREAK_TYPE_PAID` / `BREAK_TYPE_UNPAID` constants, but every employee-facing surface defaults to unpaid: workspace JS hardcodes `type: 2`, `MobileClockService.php:569` falls back to `BREAK_TYPE_UNPAID` when not supplied. Only the manager Add-Punch modal exposes the dropdown.
- There is no store-level configuration, no rules engine, and no compliance reporting.
- There is no state-law awareness: CA premium pay for missed meals, FLSA <20-min rule, NY mid-day meal window, etc. are all invisible to the system.

Concrete consequences observed:

1. **Payroll incidents (May 2026):** unpaid breaks were being included in payroll totals. Hot-fixed by re-shaping `TimesheetController::buildDayBreakdown` to net unpaid breaks out of `workedHours` and expose `grossWorkedHours` / `unpaidBreakHours` separately. The hot-fix solves the symptom; the underlying classification model is still wrong.
2. **Compliance risk:** stores in CA / NY / OR / WA / CO / IL operate under specific meal and rest rules. The system cannot tell a manager that a CA shift missed a meal break or owes a 1-hour premium pay.
3. **Employee abuse surface:** any employee surface that exposes break type lets staff self-classify a break as paid.

### Value Proposition
A store operator picks their state preset once. From that moment forward:

- Paid vs unpaid is decided by the policy engine, not by the employee at clock-out.
- The timesheet flags missed / late / short breaks visibly, with premium-pay amounts when state law requires it.
- Yearly labor-law updates ship as new preset versions ("CA-2026") with an in-app "Update available" banner — no code change required by the store.
- Stores that don't care about state rules stay on the FLSA-default preset, which exactly replicates current hardcoded behavior — no operational disruption.

---

## User Personas

### Primary Persona: Multi-Store Operator (Manager / Owner)
- **Demographics:** Owner-operator or regional manager of one to several BuyerKiosk stores. Limited HR / legal training. Uses the admin and workspace surfaces daily.
- **Goals:** Run payroll accurately. Avoid wage-and-hour claims. Spend zero time researching labor law.
- **Pain Points:** Has to manually verify break payouts. No visibility when a shift violated state rules. Cannot trust employee-entered break types.

### Secondary Persona: Hourly Employee
- **Demographics:** Front-line staff using the mobile app and workspace clock to punch in/out. Wide range of technical comfort.
- **Goals:** Clock in/out reliably. Be paid correctly. Not be blamed for a clerical error.
- **Pain Points:** Sometimes asked to remember whether their break should be paid or unpaid. Has no consistent guidance about when to take a break. Currently can (in theory) classify their own break.

### Secondary Persona: BuyerKiosk Platform Engineer
- **Demographics:** BK internal engineering. Ships preset library updates yearly as labor law changes.
- **Goals:** Push a new preset (e.g., `CA-2026`) without touching application code or coordinating with every CA store.
- **Pain Points:** Today, "the break rules" live in PHP. Changing them requires a release and risks regressions on every store.

---

## User Journey Maps

### Primary User Journey: Store operator adopts a state preset
1. **Awareness:** Operator visits Store Settings → Break Policy. Sees their current policy (default: `FLSA-default`).
2. **Consideration:** Operator's store is in California. They click "Change preset" and select `CA-2025`. A diff preview shows what rules will change vs current.
3. **Adoption:** Operator commits. All preset rules are cloned into their store as `storeBreakPolicyRule` records. The next punch processed uses CA rules.
4. **Usage:** Operator opens any timesheet. Day rows now show a "Compliance" indicator. A CA shift with a meal break taken at hour 5:15 shows `lateMeal` and a 1-hour premium-pay flag on that day's total.
5. **Retention:** A year later, BK ships `CA-2026`. The operator sees an "Update available" banner. One click + diff preview adopts the new version; their custom overrides are preserved.

### Primary User Journey: Employee takes a break
1. **Pre-change baseline (today):** Employee taps Start Break. Workspace clock sends `type: 2` (unpaid). Mobile API accepts an optional `breakType` field but defaults to unpaid if not supplied. Some surfaces let the employee pick, some don't.
2. **Post-change:** Employee taps Start Break. There is no type picker. The policy engine classifies the break at evaluation time using the store's adopted policy. If the current time matches a scheduled-break rule window, an informational hint is shown ("Your store schedules a 30-min unpaid meal around now") — informational only.
3. **End:** Employee taps End Break. Manager-side reports show the classification driven by policy, not employee choice.

### Secondary User Journey: Manager reviews compliance dashboard
1. Operator opens Compliance Dashboard for their store, week-of view.
2. Filters by violation type (`missingMeal`), employee, date range.
3. Sees a row per shift with violation, ruleId, premium-pay hours owed.
4. Marks resolved with a note (writes `resolvedByUserId`, `resolvedAt`).
5. Exports CSV for inclusion in payroll.

### Secondary User Journey: Platform engineer publishes a yearly preset update
1. Engineer authors `CA-2026` migration under `userfrosting/migrations/input/break-policies/`.
2. Migration inserts a new `breakPolicyPreset` row with bumped `version` and sets `supersededByPresetKey` on `CA-2025`.
3. Runs `php userfrosting/conductor run`.
4. Existing CA stores see "Update available" in Store Settings → Break Policy on next page load.
5. Each store opts in independently; no automatic rewrite of adopted policies.

---

## Feature Requirements

### Must Have Features

#### Feature M1: Preset library (BK-maintained)
- **User Story:** As BuyerKiosk, I want a shared library of named, versioned break policies (FLSA-default, CA-2025, NY-2025, OR-2025, WA-2025, CO-2025, IL-2025) so stores can adopt one without bespoke configuration.
- **Acceptance Criteria:**
  - [ ] All seven v1 presets are seeded via migrations in the shared DB.
  - [ ] Each preset has metadata (`presetKey`, `name`, `description`, `jurisdiction`, `jurisdictionCode`, `effectiveDate`, `version`) and a rule list.
  - [ ] A new preset version can be shipped via migration without modifying existing adopted policies.

#### Feature M2: Per-store policy adoption with preset linkage
- **User Story:** As a store operator, I want to adopt a preset for my store and customize any rule, while keeping a link to the preset so I see updates when the preset changes.
- **Acceptance Criteria:**
  - [ ] On adoption, all preset rules are cloned into the store's policy (`storeBreakPolicy` + `storeBreakPolicyRule`).
  - [ ] After adoption, preset changes do **not** silently mutate the store's policy.
  - [ ] Editing a cloned rule sets `isOverride = true` on that rule.
  - [ ] An "Update available" banner appears when `basedOnPresetVersion` < the current preset version OR the preset is superseded.
  - [ ] A diff preview is shown before any preset change commits.

#### Feature M3: Rules engine — `BreakPolicyEvaluator`
- **User Story:** As the system, I want to classify each break as paid/unpaid and detect violations using the store's adopted policy, without consulting employee-entered classification.
- **Acceptance Criteria:**
  - [ ] Given a shift, a punch list, and a store policy, the evaluator returns per-break classification and a shift-level compliance report.
  - [ ] Classification follows the priority-sorted rule list with the 10-step algorithm (rule match → `isPaid` → late check → tolerance check → FLSA cutoff → default → required-rule walk → autoDeduct → premium sum).
  - [ ] FLSA `<20 min` carve-out always applies as a final classifier.
  - [ ] Unmatched breaks fall back to the policy's `defaultBreakType`.
  - [ ] Required rules with no matching break produce `missingMeal` / `missingRest` violations.
  - [ ] Implementation is pure (no I/O); inputs are arrays/DTOs; output is deterministic.
  - [ ] Unit-test coverage ≥ 95 % on the evaluator.

#### Feature M4: Eliminate employee-side classification
- **User Story:** As an employee, I want to see Start Break / End Break only — not be asked to classify the break — so I can't be blamed for a clerical mistake.
- **Acceptance Criteria:**
  - [ ] The workspace clock no longer sends `type: 2`; sends only that a break started.
  - [ ] The mobile API continues to accept a `breakType` field for backward compatibility but ignores it (and logs a warning) when policy is enabled.
  - [ ] Manager Add-Punch modal retains the Paid/Unpaid dropdown (manual override is a manager-only function).

#### Feature M5: Manager — Store Settings → Break Policy page
- **User Story:** As a store operator, I want a single page to view, adopt, edit, and reset my break policy rules.
- **Acceptance Criteria:**
  - [ ] Page route: `/admin/<typeNum>/scheduling/break-policy`.
  - [ ] Page shows current preset, version, "Update available" banner when applicable.
  - [ ] Rule list shows: type, trigger, duration, paid/unpaid, required, premium.
  - [ ] Each rule has Edit and Reset-to-preset actions.
  - [ ] Default break type (`paid` / `unpaid`) for unmatched breaks is configurable.
  - [ ] "Change preset" action with diff preview before commit.
  - [ ] Permission: `uri_schedule_manage` (existing).

#### Feature M6: Manager — Timesheet compliance indicators
- **User Story:** As a manager, I want to see at-a-glance which days had break violations so I can resolve them before payroll.
- **Acceptance Criteria:**
  - [ ] New Compliance column on the timesheet day row.
  - [ ] Red indicator if any violation occurred that day.
  - [ ] Expanded row shows violation list + premium-pay accruals.
  - [ ] Premium pay shown as a separate line in weekly totals (flag only; not auto-added to `regularHours` in v1).

#### Feature M7: Manager — Compliance Dashboard
- **User Story:** As a multi-store operator, I want one screen to see all open compliance issues across employees this week.
- **Acceptance Criteria:**
  - [ ] Page route: `/admin/<typeNum>/scheduling/compliance`.
  - [ ] Filters: violation type, employee, date range.
  - [ ] Acknowledge / resolve flow writes `resolvedByUserId`, `resolvedAt`.
  - [ ] CSV export for payroll handoff.

#### Feature M8: Feature flag + phased rollout
- **User Story:** As BuyerKiosk, I want to ship this without breaking any store on day one.
- **Acceptance Criteria:**
  - [ ] A `breakPolicyEnabled` feature flag exists and is off by default.
  - [ ] With the flag off, all current behavior is preserved (no DB writes to compliance log; existing classification logic runs).
  - [ ] With the flag on for a store, the evaluator runs against all new punches; compliance log writes happen at timesheet save/recompute time.
  - [ ] Feature can be enabled per store for pilot rollout.

#### Feature M9: Historical-safe rollout
- **User Story:** As a store operator, I don't want my pre-cutover payroll history rewritten by this change.
- **Acceptance Criteria:**
  - [ ] At Phase 3 enablement, every existing store is auto-backfilled to `FLSA-default` with `defaultBreakType = unpaid` (matches today's hardcoded behavior).
  - [ ] No historical punches are reclassified automatically.
  - [ ] A manager-triggered "Reclassify period under current policy" tool exists for stores that explicitly request retro analysis.

### Should Have Features

#### Feature S1: Scheduled-break informational hints (employee surfaces)
- **User Story:** As an employee, I want to know when my store schedules a break around the current time so I can take it.
- **Acceptance Criteria:**
  - [ ] When current time falls within a rule's `triggerStartTimeWindow`, mobile + workspace surfaces show an informational message ("Your store schedules a 30-min unpaid meal around now"). Info only, no action required.

#### Feature S2: "No-state-rules" presets for TX, FL, GA
- **User Story:** As a TX/FL/GA store operator, I want a preset that says "follow federal rules" so I'm not confused by an "FLSA-default" name.
- **Acceptance Criteria:**
  - [ ] Presets exist that inherit `FLSA-default` semantically but are named clearly for the state.

### Could Have Features

#### Feature C1: City-level preset inheritance
- City presets (SF, NYC, Chicago, Berkeley) using `jurisdiction='city'` with stacked inheritance (federal → state → city, narrowest wins). Data model supports it; presets and inheritance logic deferred to v2.

#### Feature C2: Per-store opt-in for premium-pay auto-add
- Currently flag-only. Per-store opt-in to auto-add CA-style missed-meal premium pay to payroll totals is deferred to v2.

#### Feature C3: Per-role policies (managers vs hourly vs minors)
- Deferred to v2.

### Won't Have (This Phase)

- City ordinances (SF, NYC, Chicago, Berkeley, Philadelphia, DC) — data model supports `jurisdiction='city'` but no city presets ship in v1.
- Per-role policies (managers vs hourly vs minors).
- Union CBA / collective bargaining rules.
- Auto-end break after configured duration (separate feature).
- Auto-add of premium pay to payroll totals (v1 is flag-only).
- Historical reclassification of pre-cutover punches (default behavior — manual tool only on request).
- **WIW-integrated stores are out of scope for v1.** Stores whose punches originate from When I Work continue under current classification behavior. The policy engine ships for BK-native punches only. A WIW handoff design will be addressed in v2.

---

## Detailed Feature Specifications

### Feature: M3 — Rules engine (`BreakPolicyEvaluator`)

**Description:** Pure-PHP service that, given a shift and its punches and the store's adopted policy, returns per-break classification (paid/unpaid + compliance) and a shift-level compliance report. The evaluator is the load-bearing piece of the system; everything else is plumbing.

**Inputs:**
- Shift (start, end, duration in hours)
- Ordered punch list (clock-in, clock-out, break-start, break-end)
- Store policy with its rule list

**Outputs:**
- Per break punch pair: `BreakClassification { isPaid, ruleApplied, durationMinutes, isCompliant, violations[] }`
- Per shift: `ShiftComplianceReport { missingBreaks[], lateBreaks[], premiumPayHours, totalUnpaidBreakMinutes, totalPaidBreakMinutes }`

**Business Rules (priority-sorted):**

1. Sort rules by `priority` ascending.
2. For each break punch pair, find the first matching rule by `triggerShiftMinHours` / `triggerShiftMaxHours`, `triggerStartTimeWindow`, and `ruleType` vs duration.
3. Apply `isPaid` from the matched rule.
4. Check `mustStartBeforeHourOfShift` — record `lateMeal` violation if late.
5. Check `durationToleranceMinutes` — record `shortMeal` if short.
6. The FLSA cutoff rule (`ruleType = flsaCutoff`) is always the final classifier: any break shorter than `classifyShortBreaksUnderMinutesAsPaid` is paid.
7. Breaks that match no rule fall back to the policy's `defaultBreakType`.
8. After processing all punches, walk `isRequired` rules — if no break of that rule type appears, record `missingMeal` / `missingRest`.
9. `ruleType = autoDeduct`: if no meal break and shift exceeds the rule's threshold, synthesize a virtual unpaid deduction (does not create a punch record; affects payroll totals + compliance log).
10. Sum `premiumPayHours` across all violations.

**Edge Cases:**
- Two breaks back-to-back: treated as separate breaks; each evaluated independently.
- Break that spans the `mustStartBeforeHourOfShift` boundary: late if it *started* after the boundary.
- Shift shorter than any required-rule trigger: no required-rule violation.
- Punch list missing a clock-out: shift is open; evaluator must not throw — return partial classification with a note.
- Rule that is both `isRequired = true` and `isPaid = true` (e.g., CA rest break): missing rest break is a violation but does not flag premium pay unless `premiumPayOnViolation = true`.
- A punch annotated with `breakType` (legacy data): ignored by the evaluator when policy is enabled.

### Feature: M5 — Store Settings → Break Policy

**Description:** A single admin page where a store operator views, adopts, edits, and resets their break policy.

**User Flow:**
1. Operator navigates to `/admin/<typeNum>/scheduling/break-policy`.
2. System loads the store's `storeBreakPolicy` (creating one defaulted to FLSA-default if none exists), then shows preset header card + cloned rule list.
3. Operator clicks Edit on a rule → in-place editor → save sets `isOverride = true` on that rule.
4. Operator clicks "Change preset" → modal lists available presets → selection shows diff preview → confirm commits the new preset clone.
5. If an "Update available" banner is present, operator clicks Update → diff preview against the current adopted version → confirm migrates customized rules where field-level overrides allow.

**Business Rules:**
- Permission gate: `uri_schedule_manage`.
- Adopting a preset clones every rule; preset changes do not retroactively modify adopted policies.
- Reset rule action restores the rule from its preset source and clears `isOverride`.
- All page-rendered modals follow the wrapper-relocation pattern (CLAUDE.md requirement).

**Edge Cases:**
- Operator adopts a preset, edits a rule, then re-adopts the same preset version: confirm dialog warns about overrides being reset.
- Preset is superseded mid-page-load: banner reflects the new state on next reload.

---

## Success Metrics

### Key Performance Indicators

- **Adoption (operational):** At least one BK pilot store using a non-`FLSA-default` preset within two weeks of Phase 3 cutover. Target: 90% of CA stores adopt `CA-2025` within 90 days of GA.
- **Engagement (manager):** Compliance Dashboard viewed ≥ 1× per week per active store after Phase 5.
- **Quality (data):** Zero payroll regressions in stores still on `FLSA-default` post-cutover (paid/unpaid totals identical to pre-cutover for the same punch data). Measured by replaying historical punches through the evaluator under `FLSA-default` and diffing against the pre-cutover hot-fixed totals.
- **Business impact:** Reduction in support tickets tagged `payroll / break` by ≥ 50 % in the first quarter post-GA.

### Tracking Requirements

| Event | Properties | Purpose |
|-------|------------|---------|
| `break_policy.preset_adopted` | `typeNum`, `presetKey`, `presetVersion`, `userId`, `previousPresetKey` | Measure preset adoption velocity. |
| `break_policy.rule_overridden` | `typeNum`, `policyId`, `ruleId`, `ruleType`, `field`, `userId` | Detect which preset rules stores customize most (informs preset improvements). |
| `break_policy.violation_logged` | `typeNum`, `employeeId`, `violationType`, `ruleId`, `premiumPayHours`, `shiftDate` | Drive compliance reporting + quantify legal exposure. |
| `break_policy.violation_resolved` | `typeNum`, `violationLogId`, `resolvedByUserId`, `note` | Measure manager engagement with compliance flow. |
| `break_policy.update_available_viewed` | `typeNum`, `currentPresetKey`, `availablePresetKey` | Measure yearly-refresh funnel. |
| `break_policy.feature_flag_toggled` | `typeNum`, `enabled`, `userId` | Pilot tracking. |

---

## Constraints and Assumptions

### Constraints
- Must not modify pre-cutover historical payroll behavior for any store.
- Must use existing conductor migration system (`userfrosting/migrations/input/`). No manual SQL.
- Must respect the multi-store architecture: preset library in `kiosk_buykiosk` (shared); per-store policy + compliance log in store DBs.
- All UI must use Bootstrap 5 design tokens. Any modal must follow the wrapper-relocation pattern (CLAUDE.md).
- PHPStan must remain clean on all modified files.
- Must integrate cleanly with existing permission system (`uri_schedule_manage`).
- Must respect that the `users` / `userStoreAssignments` tables are canonical (the legacy store-level `employees` table is deprecated except for WhenIWork-integrated stores).

### Assumptions
- The current `TimePunch` schema (with `clockIn`, `clockOut`, `breakType`) is sufficient — we do not need to add new punch-level columns. Compliance is recorded in a separate log table.
- Store operators are willing to adopt their state's preset themselves; we do not auto-assign by store state without operator confirmation.
- Yearly state-law updates can be published as preset versions by BK internal engineering without coordinating with each store.
- The pre-existing hot-fix that nets unpaid breaks out of `workedHours` will be preserved by the evaluator (it does not regress).

---

## Risks and Mitigations

| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Payroll regression on cutover for stores not on a state preset | High | Medium | Phase 2 read-path preview + Phase 3 backfill every store to `FLSA-default` which exactly replicates current behavior. Diff replay tests before flipping the flag. |
| Engine misclassifies edge-case break (e.g., second meal at hour 10:01) | Medium | Medium | Heavy unit-test coverage on the evaluator (≥ 95 %); integration test matrix covering every shipped preset against representative shift scenarios. |
| WIW-sourced punches conflict with engine classification | High | Medium | Resolve the WIW open question before Phase 3 cutover; if WIW owns classification for integrated stores, scope the engine to non-WIW stores only in v1. |
| Manager misconfigures a rule and breaks their own payroll | Medium | Low | Reset-to-preset action on every rule. Diff preview on preset change. Compliance log is append-only — misconfiguration cannot rewrite history. |
| Yearly preset update silently changes an adopted store's behavior | High | Low | Adoption clones rules — preset changes never auto-mutate adopted policies. Operator must explicitly Update via the banner workflow. |
| Employee surfaces still allow break-type submission via API for backward compat | Low | High | Mobile API ignores the field with a warning log when policy is enabled. Workspace clock stops sending it entirely. |

## Open Questions

These five questions were called out in the project brief and need explicit decisions before / during Phase 1.

- [ ] **Q1 — Premium pay handling:** Flag-only in v1 (recommended), or opt-in per-store auto-add to payroll totals? *v1 recommendation:* flag-only; manager adds via existing timesheet edit flow with audit trail. Auto-add becomes a per-store opt-in in v2.
- [ ] **Q2 — Compliance scope for FLSA-default stores:** Do we flag "9 h shift with no meal break" for a TX store on `FLSA-default` (which doesn't require meal breaks)? *Recommendation:* no — only flag what the active policy declares `isRequired = true`. Stores wanting visibility switch to a state preset.
- [ ] **Q3 — WIW integration:** When I Work pushes punches for some integrated stores. Does our evaluator run on WIW-sourced punches, or does WIW own classification for those stores? *Must be confirmed before Phase 3 cutover.*
- [ ] **Q4 — City presets in v1:** SF / NYC / Chicago / Berkeley have stricter rules than their states. *Recommendation:* state-only v1; data model already supports `jurisdiction = 'city'` for v2.
- [ ] **Q5 — Yearly preset refresh workflow:** Ship `CA-2026` migration → existing CA stores see "Update available" → one-click adopt with diff preview → old policy version preserved for historical reporting. *Confirm pattern.*

---

## Supporting Research

### Competitive Analysis
- **When I Work, Deputy, Homebase, 7shifts:** All offer state-specific break rules and meal-period violation reporting. BK is currently behind on this dimension for any multi-state operator.
- **ADP, Gusto, Justworks:** Payroll-side; report violations only when fed structured data. Most BK customers do not have a separate WFM stack, so the burden falls on us.

### User Research
- Customer reports (May 2026) of unpaid breaks landing in payroll prompted the hot-fix and this initiative.
- Manager interviews during scoping consistently asked for "set it once and forget it" — i.e., adopt a state preset and let the system warn when something's off.

### Market Data
- Of active BK stores, the highest-risk jurisdictions are CA (PAGA exposure), NY (DOL meal-period audits), and OR / WA (paid rest break enforcement). These four states cover the majority of compliance-driven demand for v1.
