# Payroll integration seam — stable

`App\Modules\Payroll\Services\TimesheetRecalculationService`:
- `recalculate(StoreMembership $member, string $weekLabel, bool $persist = true): Timesheet` uses Scheduling WorkWeek; creates/updates pending only, approved/exported unchanged.
- `recalculateAffected(StoreMembership $member): void` recalculates all existing pending weeks after punch/rate change. Call INSIDE writer transaction after write.
- `assertRateChangeMutable(StoreMembership $member, CarbonInterface $effectiveAt): void` locks membership then sheets; rejects 409 if ANY approved/exported week ends after effectiveAt. Conservative retroactive rejection, never invalidates approval silently. Unlock approved first; exported cannot unlock.
- Clock now owns `App\Modules\Scheduling\Services\PunchPeriodGuard::assertMutable(member, at, oldAt?)`; do not add a duplicate Payroll punch guard. It inspects the complete affected session interval. Clock calls `recalculateAffected` inside the write transaction.

Parent implemented Team `MembershipPayRateSet` event and Payroll `RecalculateAfterPayRateSet` listener; append, audit, guard and recalculation run together in DB::transaction. `$persist=false` recalculates approved input without saving and is used only to detect stale approved hours/pay during a NEW export; replay never invokes it. Lock StoreMembership before timesheets; same ordering as Clock. Rates resolve per actual paid interval, including intraday effective_at changes, never week-only. Replays never consult rates.

Export supports one CSV row per membership/week; no alternate aggregate layout in v1. Immutable snapshot contains scalar CSV rows/settings; replay lookup precedes mutable domain reads. HTTP matches shared contract. PHP 8.5 tests (installed dependencies reject 8.3).

API fragment is ready at `contracts/fragments/payroll.yaml` (11 operations). Export summary now includes `pay_basis: actual_minutes` and `warnings: [missing_pay_rates]` for blank/unknown pay. Export rounds displayed hour components only; actual earned pay is preserved per inspected BK exporter. No provider submission.

Intentional safety tightening vs BK: ALL open sessions overlapping a requested week (including today/prior-week open clock-ins) block approval/export; oversized sessions overlapping either week are flagged (not solely clock-in week), and failed punch lookup propagates instead of fail-open guards.
