# BuyerKiosk → Alqove Module Porting Framework

How to bring BuyerKiosk (buyerkiosk-web) admin modules into the Alqove platform as
store-owner features, preserving the domain logic and hard-won lessons while landing
code that is native to Alqove's conventions. This is the reusable playbook; each
ported module gets its own plan doc in this folder (see `port-01-scheduling-payroll.md`).

**Source:** `~/Projects/buyerkiosk-web` — legacy Slim 2.6 PHP, raw-PDO repositories,
per-store databases, jQuery + Syncfusion + Twig frontend, session auth.
**Target:** `~/Projects/alqove-api` (Laravel 13 / PHP 8.3 modular monolith, API-only)
+ `~/Projects/alqove-web` (Next.js App Router monorepo, TanStack Query, shadcn/ui).

---

## 1. The prime directive

**We port domain knowledge, not code.** Three things transfer from BuyerKiosk:

1. **Algorithms and business rules** — timezone/DST math, overtime calculation,
   rounding, approval state machines, idempotency schemes. Port these near-verbatim
   (adapted to Laravel idiom) because they encode years of production debugging.
2. **Test cases** — BK's unit tests describe the edge cases (DST weeks, overnight
   shifts, stranded punches, replay attacks). Re-express them as Pest tests. The test
   suite is the second-most-valuable artifact after the algorithms.
3. **Schema semantics** — what the columns *mean*, the state machines, the invariants.
   The physical schema gets redesigned to Alqove conventions.

Everything else — HTTP layer, persistence plumbing, UI — is **rebuilt** on the target
stack. Never copy a BK controller, route file, repository SQL string, or JS file.

## 2. Translation map

The systematic differences. When porting any module, walk each row.

| Dimension | BuyerKiosk | Alqove | Porting rule |
|---|---|---|---|
| **Layering** | God controllers (3–4k lines) mixing HTTP + validation + domain; raw-PDO repositories | Thin Controller + FormRequest + JsonResource + Service (see `app/Modules/Orders/`) | Extract domain logic out of BK controllers into Alqove Services. Validation → FormRequests. Output shaping → Resources. |
| **DB topology** | Per-store DB `kiosk_<typeNum>` + central `kiosk_users` + `kiosk_buykiosk.stores` | Single DB, row-level tenancy | Every per-store BK table → one Alqove table with `foreignUuid('store_id')`. Every BK unique key gains `store_id`. Central-per-typeNum tables (e.g. OT rules) also become `store_id`-scoped. |
| **Primary keys** | Auto-increment ints | UUID via `HasUuid` trait (`app/Support/Traits/HasUuid.php`) | All new tables UUID. |
| **Identity** | `employeeId` = central `kiosk_users.users.id` (half-migrated from a dead `employees` table; stale FK comments everywhere) | Only `users.store_id` (the seller) exists; **no staff concept** | Build a clean staffing domain once (shared models, see §5). Never overload `users.store_id` for staff. |
| **Auth (web)** | Slim session + `checkAccess('uri_schedule*')` permission strings | Sanctum bearer + membership-based `store.can:{capability}` middleware (Port 00); routes shaped `/v1/stores/{store}/...` | Store access = `StoreMembership` row; endpoints gate on `StoreCapability` values (never role names — roles are capability presets, custom roles come later as data). Management: `/v1/stores/{store}/...` behind `store.can:{capability}` per the Port 00 matrix. Self-service: `/v1/stores/{store}/my/...` behind `store.member`. Never gate on `users.store_id` (see Port 00 §5). |
| **Auth (device/kiosk)** | Workbook kiosk panel, session-based | `PosDevice` hashed-token auth (`pos.device` middleware) exists | Kiosk-style surfaces (e.g. a punch clock) ride the existing device-auth pattern, later phase. |
| **Money** | `DECIMAL(10,2)` dollars | Integer **cents**, `unsignedInteger` migration + `'integer'` cast; format via `@alqove/shared` `formatPrice()` | Convert all rates/pay to cents. Splitting across rows: largest-remainder (BK `TimesheetExporter::exportPeriodAggregateCsv` pattern). |
| **Durations** | `DECIMAL(8,4)` hours | No precedent | Store integer **minutes** (or seconds where punch-level); derive decimal hours only at presentation/export. Avoids float drift; matches integer-cents philosophy. |
| **Time / TZ** | Persist UTC; store-local tz authoritative for week/day math; DST handled in `WorkWeek.php` + `ShiftRepository::copyWeek` | No timezone on Store yet | Keep the BK convention exactly: UTC in DB, store tz for all week-boundary and day-offset math. Adding `timezone` to stores/settings is a one-time prerequisite (§5). Port `WorkWeek` near-verbatim. |
| **Config** | Sprawl of columns on central `stores` + JSON flags | `StoreSettings` model exists (`app/Models/StoreSettings.php`) | Module config → columns on `store_settings` (typed, defaulted) rather than JSON blobs. Beware fail-open defaults on anything safety-gating (see traps §6). |
| **Enums** | MySQL enum columns + magic strings (`'wiw'` vs `'wheniwork'` mismatch shipped a real bug) | Backed string enums in `app/Support/Enums/`, cast on models | Every BK enum column and status string → one backed enum, referenced everywhere. No string literals. |
| **Events / jobs** | TaskEngine jobs, Redis queue, ad-hoc dispatch | Laravel queued Jobs (redis), Events past-tense, Listeners; **central registration** in `app/Providers/EventServiceProvider.php` (`shouldDiscoverEvents() = false`) | BK job → module `Jobs/`; BK side-effect hooks → Event + Listener registered centrally. Scheduled work → `bootstrap/app.php` schedule (see `payouts:run-cycle`). |
| **Realtime** | Ably publish + client resync, Firebase push, SMS | None (push channel stubbed; mail + database notifications live) | v1: degrade realtime to TanStack Query `invalidateQueries` + refetch. Notifications via the Notifications module (Listeners dispatch, preference-gated). Do not port Ably plumbing. |
| **Audit** | Bespoke audit tables (`scheduleAuditLog`, `shiftAudit`, …) | spatie/laravel-activitylog, explicit `activity('...')->causedBy(...)->log(...)` at service layer | Generic who-did-what → activitylog. **Exception:** ledgers that are *durability mechanisms* (e.g. the payroll export ledger with idempotency keys and hashes) stay dedicated tables — they are domain state, not audit. |
| **API contract** | None | Hand-written `api/contracts/openapi.yaml` (not generated, not CI-validated) | Every new endpoint is hand-added to the yaml with the standard envelopes; then in alqove-web: `./bin/sync-openapi.sh && npm run build:types`. |
| **Frontend** | jQuery ES5/ES6 classes + Syncfusion EJ2 + Twig | Next.js `(seller)` route group; api-client endpoint factory → `lib/queries/use-*.ts` hooks → thin server `page.tsx` + `*-client.tsx`; manual `useState` forms; plain `<table>`; native date inputs; no toast lib | Full rebuild per the alqove-web feature anatomy (§4 step 8). Syncfusion widgets have no equivalent — complex UI (schedule grid) is designed fresh. |
| **File export** | CSV via Blob + `Idempotency-Key` header client-side | `client.getBlob()` + object-URL download (see `statements.ts` / `seller-statements-client.tsx`); server: `response()->streamDownload` + `fputcsv` (`StoreLedgerController::exportCsv`) | Mirror the Ledger statements CSV pattern end to end. Note api-client already auto-sends `Idempotency-Key` on non-GET (`client.ts:86-90`) — align server semantics with it. |
| **Testing** | PHPUnit with **mocked PDO** → green tests can hide broken SQL (`pdo-mock-green-sql-broken-verify-live`) | Pest + `RefreshDatabase` on sqlite `:memory:` — real SQL executes | This is an upgrade: port BK *test cases*, not test infra. Feature-test idiom (post-Port 00): `Store::factory()->verified()` + `StoreMembership::factory()->owner()/->manager()/->shiftLead()/->sales()` + `actingAs($user,'sanctum')`, seeding `RoleAndPermissionSeeder`. (The older "user with `store_id` + `assignRole('seller')`" idiom in ~15 pre-existing files is legacy.) Always include the cross-store IDOR pair test on nested routes (Port 00 §2). |
| **Docs** | `docs/specs/NNN` spec docs | Module `README.md` as behavioral spec (see `app/Modules/Orders/README.md`); `docs/superpowers/specs` for design docs | Each ported module ships a README stating rules, state machines, and **what was dropped/deferred from BK**. |

## 3. Fidelity classes — what happens to each piece

Classify every piece of the BK module into one of four buckets during inventory:

- **PORT VERBATIM** (logic preserved line-for-line, syntax adapted): pure algorithms
  and safety schemes. Examples: `WorkWeek` DST cutoff math, `OvertimeCalculator`,
  rounding/largest-remainder, copy-week day-offset math, export idempotency
  replay-from-snapshot lifecycle, immutability state machines.
- **TRANSLITERATE** (same behavior, native structure): workflows and validation.
  Repositories → Eloquent-backed Services; approval flows (swap/offer/time-off);
  punch validation and clock windows; publish flow.
- **REBUILD** (same user outcome, new implementation): all HTTP, all UI, notifications,
  realtime, printing/exports UI.
- **DROP** (do not bring over): multi-DB plumbing; WIW/Homebase provider abstraction
  and gating; dual-write legacy tables (e.g. `workbook_punch_log`); env-var store
  allowlists (`NATIVE_PAYROLL_EXPORT_AUTHORITATIVE_STORES` → per-store setting);
  Slim route-ordering hacks; anything the BK map flagged as tech debt.

Default bucket when unsure: **transliterate**, and write the BK test cases first —
they'll tell you if you lost behavior.

## 4. Per-module porting process

1. **Inventory the BK module.** Fan out explorers to map: schema (+ which DB), backend
   classes per concern, business rules with file:line, frontend surfaces, integrations,
   cross-cutting seams, tests. (The scheduling map from 2026-09-02 is the template.)
2. **Classify** every piece into the four fidelity buckets (§3).
3. **Identify prerequisites** — shared models, settings columns, roles — and check
   the once-only foundations (§5). Build missing foundations first, as their own PR.
4. **Design the Alqove data model**: UUID PKs, `store_id` tenancy, cents, integer
   durations, backed enums, `store_settings` config. Write migrations + factories.
5. **Design the API**: endpoints under `/v1/stores/{store}/...`, standard envelopes,
   then hand-write the `openapi.yaml` additions. Get the contract reviewed before
   building — it's the coupling point between the two repos.
6. **Build the module skeleton**: `app/Modules/<Name>/{Controllers,Requests,Resources,
   Services,Jobs,Events,Listeners,Tests,routes.php,README.md}`; add the
   `require app_path('Modules/<Name>/routes.php');` line to `routes/api.php`;
   register events in `EventServiceProvider`.
7. **Port domain services + tests together**: for each PORT-VERBATIM algorithm,
   first port its BK test file to Pest (`tests/Unit/<Module>/`), then the service,
   until green. Feature tests for every endpoint (`tests/Feature/<Module>/`).
8. **Frontend feature** (alqove-web): sync contract + `build:types` →
   `packages/api-client/src/endpoints/<resource>.ts` factory (export in `index.ts`,
   register in `web/src/lib/api.ts`) → `web/src/lib/queries/use-<resource>.ts`
   (`*_KEYS` + hooks with `invalidateQueries`) → `(seller)/seller/<feature>/page.tsx`
   + `<feature>-client.tsx` → nav entry in `(seller)/layout.tsx` → colocated
   `__tests__` (Vitest) → e2e spec (mock-server style for CI; `playwright.qa.config`
   style against the real stack for the money paths).
9. **Close out**: module README (rules + dropped/deferred list), pint + phpstan +
   full test runs in api, typecheck + lint + vitest in web, contract re-synced.

## 5. Foundations built once (shared across all ported modules)

Check these before each port; build missing ones as standalone prerequisite PRs.

1. **Staffing & access domain** — designed in full in
   [`port-00-employees-store-access.md`](port-00-employees-store-access.md)
   (decided 2026-09-02: employees are full Alqove users, multi-store from day one,
   four-tier store roles, mobile-first `(staff)` surface). Core pieces:
   - `StoreMembership` (user ↔ store, `StoreRole` enum owner/manager/shift_lead/sales,
     status, `is_exempt`, default position) — all per-store people-data keys off
     `store_membership_id`.
   - `MembershipPayRate`: append-only cents history, latest-before-date wins.
   - `StorePosition` (per store): schedulable label ≠ access role.
   - `StoreCapability` enum + `EnsureStoreCapability` middleware + the v1 preset
     matrix (Port 00 §2) — the authorization contract every ported module must
     follow. New modules add capabilities to the enum and grant them to presets;
     they never invent their own gating or check role names at endpoints.
2. **Store timezone** — a `timezone` column (IANA name) on `stores` or
   `store_settings`, defaulted sensibly and settable in seller settings. Required by
   any module doing local-time math.
3. **Scheduling settings** on `store_settings` — work-week start day + start time,
   rounding mode/increment, clock windows, policy toggles. Added incrementally per
   module, but always typed columns with **fail-closed defaults** for safety gates.
4. **Store-role authorization** — provided by Port 00 (`EnsureStoreRole` +
   capability matrix). spatie roles remain platform-level only (buyer/seller/admin);
   store-tier access lives on the membership row. New modules extend the capability
   matrix in Port 00's doc rather than inventing their own gating.

## 6. Known traps ledger

Collected from BK production history and this repo pair's skills. Re-read the
matching skill before touching the area.

| Trap | Where it bites | Countermeasure |
|---|---|---|
| DST day-offset drift (`utc-day-offset-dst-drift`) | Copy-week / recurring shifts | Day arithmetic in store-local tz, convert back to UTC. Port BK `ShiftRepository::copyWeek` math. |
| DST week-cutoff double-pay | Week boundaries, payroll | Rebuild the +7d cutoff from configured local wall time; never `modify('+7 days')` a UTC instant. Port BK `WorkWeek`. |
| At-most-once submit (`at-most-once-external-payment-submit-recovery`) | Payroll export, any money movement | Idempotency key + request hash + immutable snapshot; replay answered from snapshot BEFORE reading mutable data; mark+audit in one transaction; stuck-locked beats wrong-release. |
| Rounding drift (`money-split-rounding-largest-remainder`) | Any per-row money/hour split | Round components, derive totals from rounded components; largest-remainder for splits. |
| Fail-open safety defaults (`fail-open-default-on-safety-flag-json-field`) | Settings/flags gating visibility or money | Safety gates default to the restrictive state; verify the API actually returns the field the client filters on. |
| Green-but-broken SQL (`pdo-mock-green-sql-broken-verify-live`) | N/A in Alqove (real DB in tests) — but beware mocking Services in feature tests | Prefer `RefreshDatabase` feature tests hitting real SQL. |
| Provider/flag string drift (`buyerkiosk-wiw-exclusion-source-of-truth`) | Any string-compared discriminator | Backed enums only; one source of truth per fact. |
| Paired-event asymmetry (`reducer-paired-event-asymmetry`) | Frontend state on inverse actions (publish/unpublish, approve/unapprove) | Make inverse handlers mirror forward handlers; prefer refetch-on-invalidate over hand-mutated caches. |
| Swallowed async failure (`riverpod-asyncvalue-guard-false-success` — pattern generalizes) | Mutations followed by success UI | Check mutation result before success UI; TanStack `onSuccess`/`onError` split does this naturally — don't bypass it. |
| Concurrency on assignment (BK advisory locks + microsecond revision token) | Double-assign, double-punch, double-submit | DB transactions + unique constraints first; `updated_at`-style revision token for optimistic concurrency on edits; idempotency keys on submits (api-client already sends them). |

## 7. Open platform decisions (flag per module, decide with Ryan)

- ~~**Staff logins**~~ — DECIDED 2026-09-02: employees are full Alqove users with
  logins, multi-store employment, four store-role tiers. See Port 00.
- **External payroll provider**: BK has **no Everee code** (verified 2026-09-02 —
  the Everee lifecycle in the skills library is design guidance, not portable code).
  If Alqove later submits to a real payroll API, that's a net-new integration built
  on the at-most-once pattern; CSV export is the v1.
- **Realtime**: if/when Alqove adopts a realtime channel, revisit the degraded
  refetch-based UX in ported modules.
- **Schedule grid UI**: no calendar/data-grid library exists in alqove-web. Building
  a bespoke timeline grid vs adopting a library is a per-module design decision.
