# Time punches

Employee self-clock and manager corrections, ported from BuyerKiosk Scheduling / MobileScheduling. This file is the punch-specific companion to the Scheduling README.

## API and essential contract clarification

Routes are in `punch-routes.php`, narrowly registered in `routes/api.php`. Fragment: `contracts/fragments/punches.yaml` (parent merges canonical contract).

- `GET /v1/stores/{store}/my/clock`: real active membership only, resolved by StoreViewer; no synthetic admin clock identity.
- `POST .../my/clock`: `{action: clock|break, state_revision, break_type?: paid|unpaid}` plus required `Idempotency-Key`.
- `GET .../punches`: punches.manage; required `store_membership_id`, inclusive store-local `from`, `to`; date difference <=31 days.
- `POST .../punches`: manager add, required membership/type/time/note, optional assigned shift and break type.
- `PATCH .../punches/{punch}`: required revision and note; optional type/time/shift/break type; membership cannot be changed.
- `DELETE .../punches/{punch}`: required revision and note; soft delete with manager attribution.

**Deviation from initial coupling contract:** self-clock requires `state_revision` from the last GET. Initial state token is `empty`, otherwise latest punch's revision. A toggle action alone cannot distinguish a stale clock-in intent with a new idempotency key from a deliberate clock-out. Frontend must pass the token unchanged and refetch on 409. Parent has been informed and directed the timekeeping frontend agent to use it.

## Safety invariants

- Every mutation takes Store then membership SQL row locks inside a transaction. Self time is captured inside the lock, UTC, whole-second precision; no employee-supplied identity, timestamp, punch type, or shift is accepted.
- Server derives `clock_in -> break_start -> break_end -> clock_out`. Clock-out on break rejects rather than silently creating a paired closure. Break start requires an explicit paid/unpaid type; self break-end inherits it, rejecting a supplied mismatch. Manual break events require explicit matching types.
- Full chronological history is validated after corrections, so edits/deletes cannot orphan clock-outs, overlap sessions, or corrupt break pairs. An open final session remains visible. No midnight reset, stranded-session auto-close, or future manager punches.
- UUIDs cannot provide meaningful same-second chronology. Self opposing actions within the same second return 409; corrections require distinct increasing event seconds. SQL additionally enforces unique membership/type/second and real foreign keys (including shifts).
- Dedupe is not idempotency. `clock_actions` durably stores membership/key, SHA256 hash of canonical validated request, and immutable response. Replay occurs before mutable punch/shift/payroll reads, but after live membership authorization. Same key/different hash returns 409. No TTL silently permits an old key to toggle again.
- A manager correction may make an old replay snapshot stale. **Always refetch GET after any mutation or replay.** The replay response is a historical acknowledgement, never the current authority.
- Published, assigned shift clock-in window is required. Zero early/late settings are restrictive. Clock-out requires its original assigned shift and no later than configured end+late allowance; otherwise a clear 422 `require_manager_override` error directs the user to manager correction. No unscheduled fallback.
- Manager-supplied memberships, punches and assigned shifts are store-scoped (404 on mismatch). Historical corrections remain possible after employee termination or shift soft deletion/unassignment; omitted shift preserves history, explicit null removes association.
- Every manual mutation requires a nonblank note, fresh opaque UUID revision on edits/deletes, before/after activity audit, and created/updated/deleted actor fields. Deleted rows retain unique-key tombstones: duplicate manual type/second returns 409 rather than creating another event or audit.
- GET state is derived from complete history, but payload is bounded to at most 500 of today's store-local punches plus active clock-in/break-start. Old closed history is not returned. `allowed_actions` reflects lifecycle/windows; frozen payroll or concurrent changes can still reject a submission.

## Payroll integration

`PunchPeriodGuard` directly depends on Payroll's Timesheet model/table and Scheduling's StoreSettings/WorkWeek; **no schema-exists fail-open path** is present. Missing dependencies fail the write. Approved/exported weeks are checked under membership and timesheet locks. Guard range includes surrounding sessions, not just edited timestamps: cross-week sessions, moving a punch out of frozen history, and deleting a clock-out that would reopen a session across a later frozen week all reject. Checks occur both before and after corrections. Guards are intentionally conservative around adjacent/open sessions; unlock affected approved history first. Exported history remains immutable.

Every actual self/manual create/update/delete calls `TimesheetRecalculationService::recalculateAffected($member)` in the same transaction, before audit and clock replay snapshot. It recalculates existing pending timesheets; creation of missing week rows remains Payroll's week/recalculate endpoint responsibility. Replay does not recalculate. SQLite trigger-induced recalculation failure is tested to roll back punch, audit and replay record atomically.

Payroll approval locks membership; export locks Store then memberships; these shared locks serialize with punch writes. Production engine contention/deadlock behavior still requires deployment-stack concurrency testing (the feature suite executes real SQLite, not a mocked PDO or a claim of MySQL verification).

## Source fidelity / intentional exclusions

- MobileClockService: identity-derived self-service, timestamp-inside-lock, assigned-window gating, session-scoped breaks.
- TimePunchRepository: chronological state, no midnight reset, exclude future self-state events, lock and dedupe semantics.
- Rebuilt Laravel Requests/Controllers/Resources/services; no copied legacy HTTP/SQL plumbing.
- Deferred: GPS/geofence, kiosk/device clock mode, realtime/workbook dual writes, break-policy engine, provider plumbing. v1 supports explicit paid/unpaid only.

## Verification

PHP 8.5 (`/opt/homebrew/bin/php`) is required by installed dependencies; PHP 8.3 is not usable. No dependency/vendor changes.

`/opt/homebrew/bin/php vendor/bin/phpunit tests/Feature/Scheduling/TimePunchEndpointsTest.php`

23 feature tests / 150 assertions passed after RED/GREEN vertical implementation slices and supplementary safety regression checks. Real SQLite migrations, factories, HTTP middleware, SQL constraints, activity writes, payroll recalculation and rollback execute. Focused PHPStan: no errors; focused Pint: pass; `git diff --check`: clean. Combined Scheduling + Payroll feature run: 52 tests / 434 assertions passed. OpenAPI fragment parsed with Symfony YAML: 3 paths, 6 operations, 10 schemas; all internal references resolve. Parent owns final full-suite and frontend E2E integration. No commits or pushes.
