# Scheduling port — implementation coordination

User authorized autonomous completion of Port 01 lean core on 2026-09-06, including functional web UI, unit/feature tests and browser E2E against a real isolated API. Existing Port 00 prerequisites are being completed first. Source rules and deferred scope remain in port-01-scheduling-payroll.md. No commits, pushes, deployments or production data resets are authorized by this note.

## Shared conventions / owners

- UUID tenant-scoped models in app/Models; capabilities via StoreViewer only, no role-name gates. `/my` resolves real membership, never client member ID. Every nested body/path ID tenant-checked, 404 on cross-store resources.
- Strict TDD, real SQLite SQL feature tests. Installed Composer dependencies require PHP >=8.4; `/opt/homebrew/bin/php` is the verified PHP8.5 runner. PHP8.3 cannot boot these dependencies. The full-suite SQLite cascade was caused by missing PhpRedis preventing teardown rollback, not an application transaction defect. Broad regression uses a process-local Redis extension and an isolated Redis instance; never run flushdb tests against an existing Redis service. Isolated fixtures only.
- Agent Scheduling API owns settings, WorkWeek, Shift/SchedulePublication, scheduling routes.php and module README, migrations suffix _000100. Agent Clock owns punches routes in `Modules/Scheduling/punch-routes.php`, TimePunch model, punch services/requests/resources and migrations suffix _000200. Agent Payroll owns Payroll module and migrations suffix _000300. All agent edits to shared route/provider files must be narrow patches preserving each other. API contract additions: each agent writes a standalone OpenAPI YAML fragment in api/contracts/fragments/{scheduling,punches,payroll}.yaml containing `paths` and `components.schemas`; parent merges into canonical openapi.yaml and syncs frontend. Avoid simultaneous rewriting canonical contract.
- No source/algorithm invention: inspect BuyerKiosk source and tests, preserve WorkWeek/DST, overtime, copy-week, replay snapshot semantics. Explicit paid/unpaid breaks only; advanced break-policy engine deferred.

## Agreed HTTP interface (all responses standard `{data: ...}`)

Base `/v1/stores/{store}`. Dates Y-m-d; UTC instants ISO8601 including offsets; durations minutes; money integer cents. Pageless week-scoped lists allowed, bound query ranges. Error 422 validation, 403 auth, 404 tenant-child mismatch, 409 stale/conflict.

### Settings and shifts

- GET `/schedule/settings` readable with schedule.view; PATCH owner settings.schedule.
- Settings fields: `timezone` (IANA, default America/Los_Angeles), `work_week_start_day` ('sun'...'sat', default mon), `work_week_start_time` ('HH:MM', default 00:00), `payroll_rounding_mode` (none|nearest|up|down, default none), `payroll_rounding_increment_minutes` (default 1), `clock_in_early_minutes` (default 0), `clock_in_late_minutes` (default 0), `clock_out_late_minutes` (default 0). StoreSettings holds these. Zero clock window is restrictive, owner may configure. Settings revisions must not silently reinterpret approved/exported historical payroll; restrict timezone/work-week changes once relevant history exists or provide safe explicit policy.
- GET `/schedule/week?week_start=Y-m-d` with schedule.view, drafts only schedule.view_drafts. Data `{week_start, starts_at, ends_at, timezone, shifts: Shift[], publication: ...|null}`. Week label normalized by configured start day.
- POST `/schedule/shifts`, PATCH/DELETE `/schedule/shifts/{shift}` with schedule.manage. Input store_membership_id nullable for open, position_id nullable, starts_at, ends_at, notes nullable; PATCH and DELETE require `revision` exact opaque token from Shift. Shift fields id, store_id, store_membership_id, member_name nullable, position_id, position_name nullable, starts_at, ends_at, notes, published_at, revision. Edits clear publication (fail closed) so staff do not see unpublished modifications; preserve published snapshot if implementing snapshot approach instead, document explicitly.
- POST `/schedule/copy-week` with schedule.manage. Input `{source_week_start, target_week_start, preview: boolean}` (explicit required, default never implicit commit). Preview/commit same response with shift candidates and counts; shifts copied as drafts, DST local wall times retained, overlaps reject atomically.
- POST `/schedule/publish` with schedule.publish: `{week_start}`. Transaction stamps week shifts and publication row; audit.
- GET `/my/schedule?week_start=...` store.member, own published shifts only, same week response shape. Staff can also use `/schedule/week` for published team view.
- MembershipTerminated listener unassigns future shifts inside same transaction without Team importing Scheduling.

### Clock / corrections

- TimePunch fields id, store_id, store_membership_id, shift_id nullable, punch_type (clock_in|clock_out|break_start|break_end), punched_at UTC instant, break_type nullable (paid|unpaid), note nullable, revision opaque. UUID, real uniqueness by membership/type/second; soft deletion/edit auditing and manager actor attribution.
- GET `/my/clock` -> `{status: 'clocked_out'|'clocked_in'|'on_break', punches: TimePunch[], allowed_actions: string[], server_time, ...}`.
- POST `/my/clock` input `{action: 'clock'|'break', break_type?: 'paid'|'unpaid'}`. Client expresses intent, server derives next punch type from locked current state; server time only. Action clock toggles in/out (reject clock out while on break unless documented paired closure); break toggles start/end. Honor Idempotency-Key request hash/replay so retries cannot toggle twice. Clock in requires assigned published shift within configured early/late window; clock out handles limits with explicit override errors. Zero windows fail closed. Re-fetch after mutations.
- GET `/punches?store_membership_id=...&from=Y-m-d&to=Y-m-d` punches.manage -> list; POST `/punches` with membership, shift optional, punch_type, punched_at, break_type, note (required); PATCH/DELETE `/punches/{punch}` with revision+note required. Check state sequence, overlap, tenant membership, approved/exported period immutability; parent will integrate payroll recalculation after punch/rate changes.

### Timesheets/payroll

- Timesheet fields id, store_membership_id, member_name, week_start_date, scheduled_minutes, regular_minutes, overtime_minutes, doubletime_minutes, unpaid_break_minutes, total_pay_cents nullable; status pending|approved|exported; calculation_metadata for guards and breakdown. Strip all money from non-pay.view management responses; own /my can see own pay.
- GET `/timesheets?week_start=...` timesheets.approve; POST `/timesheets/recalculate` `{week_start}`; GET `/timesheets/{timesheet}` including punch breakdown; POST `/timesheets/{timesheet}/approve`, `/unlock`. Guard stranded clock-in hard block, exported immutable, oversized soft acknowledgement `acknowledge_oversized_sessions` true. Scope to store always.
- GET `/my/timesheets` store.member read own only.
- Owner-only `payroll.export`: POST `/payroll/exports` `{week_starts: string[], acknowledge_oversized_sessions?:boolean}` + Idempotency-Key, GET `/payroll/exports`, GET `/payroll/exports/{export}/download` CSV. Snapshot/hash/key replay-before-mutable-read, same key/different request 409, mark+row in single transaction, safe CSV escaping including formula injection.
- Overtime rules per store effective date, US-FLSA default; implement typed schema/version plus US-CA/CA-ON or custom as source fidelity permits. Endpoints GET/PATCH `/payroll/rules` owner settings.schedule, readable staff scheduling only if no private data. Daily OT before weekly remaining regular; exempt; seventh-day; UTC elapsed time; historical rates, integer cents/minutes. Never silently guess pay when rate missing.

## Frontend shared seam

Auth store selectedStoreId:string|null, selectStore(id):void, refreshMemberships():Promise<void>. `@/lib/stores/store-context` exports useSelectedMembership(), useSelectedStoreId(). No user.store_id fallback. Capabilities select access. Unknown memberships fail closed; remount tenant subtree on switch. Seller nav gates Schedule=schedule.manage, Timesheets=timesheets.approve, Payroll=payroll.export, schedule settings=settings.schedule; staff own schedule/clock/hours any membership.

This is a coupling contract, not verification evidence. Agents must document any essential deviation and communicate it to the parent; parent resolves cross-agent integration and runs real E2E.
