# Spec 051 — Scheduling Onboarding & Activation

**Status:** All three documents reviewed — **ready for implementation** (`/start:implement 051`; T0 decision gates first)
**Owner:** Scheduling team
**Created:** 2026-07-20
**Last reviewed:** 2026-07-21 (Codex plan review, full restructure applied)

## Documents

| Document | Path |
|---|---|
| Product Requirements | [product-requirements.md](./product-requirements.md) |
| Solution Design | [solution-design.md](./solution-design.md) |
| Implementation Plan | [implementation-plan.md](./implementation-plan.md) |

## SDD headline results (2026-07-21 codebase audits)

- **M1-01 entitlement inventory (complete):** the entire premium gate funnels through 4 chokepoints — `PremiumService::isPremiumActive` (:53), Twig `isPremiumActive` (+fallback), `Store::isPremiumActive` (:2541), mobile `getPremiumSchedulingInfo` (:5154). A dedicated `SchedulingEntitlement` predicate (ADR-051-3) is consumed at each call site instead of a single short-circuit; gates stay in code so switch-off restores pre-051 behavior exactly. Chat is decoupled first onto `isChatEntitled()` (Q5 isolation). AI has no PHP gate (template clauses only). `PremiumGateMiddleware` is verified-unwired dead code → deleted (ADR-051-8). **⚠ Billing keeps charging `premiumStatus='active'` stores until Q1 executes** — audit-sharpened launch blocker.
- **DR-7 verdict (ADR-051-7):** `userDeviceTokens.appId` is the only Team-vs-Live discriminator (Live provably sends `'live'`; 30-day `updatedAt` freshness per StickinessRollupJob precedent). Refresh tokens can't attribute the app. v1 formula = device-token per app within 30d; additive fallback = apps send `clientId:'team'|'live'` at login (zero schema change — JwtAuthService already honors it); push-permission deniers covered by manual-eligible step until then.
- **Invite audit bombshell (ADR-051-9):** `users.activationToken` has NO consumer anywhere — the only working set-password page rides the deprecated `employee_invitations`/employeeId flow. We BUILD `ActivationController` + `common/activate.html` on the canonical users token. Also: `sendInvitation()` throws without email (must relax for SMS-only); invite expiry is 24h (one constant if product wants longer).
- **Transports:** email = PHPMailer+SMTP + Twig `templates/mail/` (house invite precedent); SMS = `TextMessageService` + new `SmsCategory::SCHEDULING_INVITE` + existing Twilio delivery webhook for per-message delivered/failed (no new webhook endpoint).
- **New tables (central):** 7 new tables — 5 in `kiosk_buykiosk` (`onboardingProgress`, `onboardingObservations`, `onboardingUserProgress`, `onboardingEventLog`, `onboardingCampaignCohort`) and 2 in `kiosk_users` (`userInvites`, `inviteMessages`) — across 4 conductor migrations.
- **ADRs:** 051-1 generic framework/one flow · 051-2 live detection, terminal-facts-only persistence · 051-3 entitlement seam not deletion · 051-4 event-advanced walkthrough beats · 051-5 invite history wraps LoginAccessService · 051-6 central-DB placement · 051-7 signal strategy · 051-8 delete dead middleware · 051-9 build users-token activation.

## One-line summary

Scheduling moves from the paid Premium add-on (Spec 035) to included-in-all-plans. This spec builds the activation funnel: inventory-driven entitlement ungating + System Alerts announcement (sequenced last), an opt-in Getting Started hub with a contract-defined self-detecting setup checklist (settings → positions → roster → invites → apps → availability → first schedule), real invite delivery by email + SMS (today's invite flow sends nothing), observed-app-use adoption detection, and a driver.js guided walkthrough of the calendar (shift → open shift → AI fill → publish).

## Locked decisions

**From the 2026-07-20 brainstorm (confirmed with product owner via AskUserQuestion):**
- **Activation is opt-in via the hub** — the gate drops for everyone, but each store explicitly clicks "Get Started" (flips `schedulingProvider='buyerkiosk'`). WIW/Homebase stores get an integration-aware page; nothing auto-flips (ADR-5 lineage).
- **UX = persistent checklist hub + interactive walkthroughs**, not a linear wizard.
- **Walkthrough engine = driver.js** (MIT, vendored), subject to the DR-9 compatibility spike.
- **V1 plumbing scope = invite email + SMS delivery.** QR handoff (S2), adoption tracker panel (S3), sample-week data (S4) deferred.

**From the 2026-07-20 Codex review cycle (Decisions Log D-1…D-7 in the PRD):**
- D-1/D-2: Premium infrastructure retained dormant (kill switch, no deletion).
- D-3: Announcement goes to **all roles** (Spec 040 has no role targeting); conversion measured via `source=alert` hub visits, never acknowledgments.
- D-4: Hub persists post-completion as a compact "Setup" nav entry.
- D-5: External-provider switching is **support-assisted only** in v1 (no self-service switch).
- D-6: Invite SMS = new Spec 037 `schedulingInvite` category, billed per existing store SMS policy.
- D-7: "First clock-in" is a tracked milestone, **not** a setup step (stores may clock elsewhere).

**From the 2026-07-21 T0 decision gate — RATIFIED by product at the T0→T1 gate (full rationale in PRD Decisions Log D-8..D-10):**
- **D-8/Q5:** Team Chat **becomes FREE with scheduling** (product overrode the provisional stays-premium default). `isChatEntitled()` stays a separate predicate but delegates to `SchedulingEntitlement::isEntitled()` — flags off ⇒ byte-identical pre-051 chat gating; flags on ⇒ chat ungates with scheduling. Mobile `features.chat` follows.
- **D-9/Q3:** Invite email = existing PHPMailer/SMTP path, sender pinned `noreply@buyerkiosk.com`, `InviteEmailTransport` adapter, fail-fast when SMTP unconfigured outside dev (SendGrid API = T8.6-failure contingency; SPF already includes sendgrid.com; DMARC p=none). Locks at T3.2.
- **D-10/Q4:** Detection thresholds = PRD defaults (lock at T2.1); ticket taxonomy = manual `scheduling-setup` tagging, 60-day retroactive classification, mandatory-storeTypeNum CSV baseline import (no helpdesk API). CSV format locks before T6.2.

## Review history

### 2026-07-21 — Implementation Plan review (Codex, `read-only` sandbox)

- **Verdict:** not implementation-ready as drafted; all findings accepted, plan restructured same day.
- **Key restructure:** new **T0 decisions/spikes phase** (Q5 chat decision BEFORE the code it shapes; driver.js spike before the walkthrough phase, with the fallback article a deliverable regardless of verdict; Q3/Q4 repositioned as entry gates); **all four migrations consolidated into T1B** (the invites detector previously preceded its own tables); T1A entitlement rebuilt test-first; **T3/T4 contract freeze** before parallel branches; walkthrough state ownership split (T2 server / T5 client); new **T7 integration gate** (mocked delivery chain, invite→app ladder, alert-attribution chain, all-flags matrix, criterion-matrix audit) before **T8 launch readiness**.
- **Coverage added:** per-producer analytics tasks with property contracts; privacy/retention tasks (24-month event retention, Privacy-module deletion of invite PII); table-driven endpoint-security test matrix; worker operations (scheduler registration, health checks, alerting, error taxonomy; ambiguous SMS = `sent`/`unconfirmed` reason, not a schema state); **per-detector query-budget table + ≤10-query regression test**; baseline script completed (ticket source ownership, grandfathered median time-to-first-publish, immutable rerun, artifact checksum); UI tasks for every previously-implicit requirement (Setup nav persistence, wage-permission rendering, M5 no-mutation proof, M6 overnight/timezone edges, storeNotify actual delivery, M10/M12 step UIs); criterion matrix now covers every M/S/NFR ID with S2–S5 marked deferred; first-clock-in test corrected to at-or-after first post-activation publish.
- **Process added:** formal per-phase DoD template (TDD evidence, run-twice migrations, flag-off/on verification, artifacts, sign-off); 11-entry risk register with owners/triggers/contingencies; **fixture stores (ob01–ob03) + dev-guarded reset script** solving the latched-pilot-store re-test problem.
- **Final shape:** 10 phases (T0, T1A∥T1B, T2, T3∥T4∥T5∥T6, T7, T8), ~4.5–5.5 weeks single-dev (~3.5–4 with a second dev on the parallel branches).
- **Decision:** ✅ **Ready for implementation** — entry gate is T0 (Q5/Q3/Q4 decisions, driver.js spike, fixture-store setup).

### 2026-07-21 — SDD review (Codex, `read-only` sandbox)

- **Verdict:** conditional no-go — 8 blockers + interface/data-model/test gaps. All accepted; SDD fully rewritten same day.
- **Blockers found → resolution:**
  1. **Entitlement conflation + unsafe default** — `SCHEDULING_FREE` defaulted true (missing prod var would ungate); short-circuiting commercial `isPremiumActive()` conflated billing truth with entitlement and would free chat via delegation → dedicated `SchedulingEntitlement` predicate (`free || commercialPremium`), commercial predicate untouched (CON-10), `isChatEntitled()` bound to commercial premium structurally, **all flags default false** + `SCHEDULING_FREE_PILOT_STORES` allowlist, authoritative route matrix (`/schedule` 302s unactivated stores to get-started — no empty calendar), store-resolver route for the alert's static CTA (ADR-051-3 rewritten).
  2. **Invite delivery not durable** → transactional outbox on `inviteMessages` + TaskEngine `InviteDeliveryJob` (guarded claims), `InviteSweepJob` (stale-lock recovery, retry backoff, dead-letter), `InviteExpiryJob`; bulk = 202 + batchId + polling; numeric caps (200/run, 500/store/day, 3/recipient/day) (ADR-051-10).
  3. **SMS category is a DB enum ×3** (billingSmsUsage, billingSmsCategoryConfig, billingLineItems — the "one PHP constant" claim was factually wrong) → migration 051_003 extends all three enums + default config rows; invoice-pipeline integration test (S12).
  4. **Token/activation security** → invite-scoped hashed tokens (fixes cross-store invalidation + winning-invite ambiguity), session-exchange redemption (302 to tokenless URL; no-store/no-referrer/CSP; ingress redaction), guarded one-winner consumption, object-scope guard on invite endpoints (target must be assigned to route typeNum), opaque `messageRef` channel attribution, **username collection at activation** (SMS-only members previously could never log in) (ADR-051-11).
  5. **M3-08 impossible with terminal-only schema** → `onboardingObservations` snapshot table (last-known non-terminal state, permission-neutral evidence) + 5-minute hub polling.
  6. **Analytics asserted, not designed** → event-producer matrix (producer, tx owner, dedupeKey per event), atomic progress+event writes, `onboardingCampaignCohort` immutable KPI denominators + `scripts/onboarding-baseline-snapshot.php` as the M2-04 gate.
  7. **M10 contract gaps** → `appLoginObserved` (clientId, launch-gated mobile deliverable w/ contract tests) and `pushRegistered` kept as separate payload fields; `COALESCE(lastUsedAt,createdAt)`; `userDeviceTokens` uk fixed to `(userId,deviceId,appId)` (Team+Live on one device previously overwrote each other) + composite index.
  8. **Webhook/compliance contradictions** → do-not-text list ENFORCED for invites (skip idea dropped; matches M9-09 + own test), X-Twilio-Signature validation, durable-write-before-2xx (5xx on transient failure), monotonic state transitions, payload redaction; Vonage invites documented as sent-only.
- **Also fixed:** PRD-alignment gaps designed in (M2-04 baseline script, M7-02 starter-position catalog + endpoint, M11-05 adaptive beats, M11-06 AI failure states, M12-04 low-availability notice); full interface contracts (schemas, CSRF, error envelope, 202/409/422 semantics — activate 409-vs-200 contradiction resolved: 200 idempotent, 409 only provider-conflict); data-model completions (walkthrough `inProgress`, `isCurrent` unique active invite, provider-qualified unique providerMessageId, event dedupeKey, retention); positions correctly identified as store-local (CON-3 fix); valid p95 method (seeded 95th-percentile profile + concurrent load + Server-Timing telemetry); fault/security/flag-matrix test scenarios S8–S13; open items assigned owners. PRD D-1 amended with the middleware-deletion carve-out.
- **Decision:** ✅ **Ready for Implementation Plan.**

### 2026-07-20 — PRD review (Codex, `read-only` sandbox)

- **Verdict:** revision required before SDD sign-off. All six blockers accepted; full revision applied same day.
- **Blockers found → resolution:**
  1. **Entitlement boundary incomplete** (Spec 035 also gates chat, AI, mobile flags; the walkthrough needs AI ungated) → M1 rebuilt around an authoritative entitlement inventory (M1-01), AI explicitly entitled (M1-02), mobile flag validation (M1-03), kill switch (M1-06), chat split into Open Question Q5.
  2. **Checklist state model contradictory** (undefined invite/go-live completion; skip-vs-complete, manual-vs-detected, latching-vs-live conflicts; M9 AI-required-vs-skippable) → authoritative **Step Contract Table** added (step key, actor, permission, prereqs, deterministic completion, denominators, historical handling, manual/skip); latched-achievement + health-indicator semantics (M3-05); required steps unskippable (M3-06); first-clock-in demoted to milestone (D-7); AI beat explicitly skippable (M11-02).
  3. **Activation/provider switching undefined** → provider × premium × config state matrix added; switching made support-assisted only (D-5), resolving the M3/Won't-Have contradiction; idempotent concurrent activation (M4-02); unrecognized provider values default safe-external (M4-03).
  4. **V1 scope undefined** → Release Manifest added (release, launch gate, flag, rollback, rollout order per feature); M1 split (entitlement vs announcement); S5 funnel UI made fast-follow with launch monitoring on raw events; "only new plumbing is invites" claim corrected.
  5. **Permissions + System Alerts conflict** (Spec 040 has no role targeting; acknowledgment can't attribute per-store) → permission matrix (M3-11); D-3 all-roles announcement; conversion re-based on `source=alert` hub visits (M2-02).
  6. **M7/M8 data contracts unresolved** (invite service is user-global, no store context; Spec 037 lacks an invite category; refresh tokens may not distinguish apps) → M9 rewritten with channel semantics, per-channel states, store/member/channel-scoped history (M9-03), idempotent bulk, token security, `schedulingInvite` category (D-6), channel attribution (M9-12); M10 rewritten around **observed app use** with signal validation as an SDD gate + defined fallback contract (M10-03/DR-7).
- **Important findings applied:** hypotheses labeled + pre-launch baseline capture requirement; eligible-cohort definitions; stories split (M1/M2, M4/M5, S2/S3) and employee invite story added; Could-haves relabeled uncommitted Future Opportunities; stable acceptance-criteria IDs (`M9-01` …); Dependency Register (DR-1…DR-11) with owners/fallbacks/launch gates; KPI definition table (numerator/denominator/cohort/clock) + **durable-adoption KPI** (≥ 3 of weeks 3–6); missing tracking events added (`invite.delivery_updated`, `schedule.week_first_published`, AI events, milestone, switch interest) with `flowVersion`/`campaignId`/`inviteId`/`messageId`; NFR section (NFR-1…NFR-8); expanded edge cases (concurrency, zero-denominator, DST/overnight hours, stale tokens, detection failure); validation checklist re-stated honestly.
- **Open-question triage:** Q1 = announcement blocker, not SDD blocker. Old Q2/Q5/Q6 converted to decisions D-1/D-4/D-3. Q3 = SDD decision with launch-gated deliverability. Q4 = threshold + taxonomy sign-off before acceptance tests. New Q5 = Team Chat entitlement (needed before M1 implementation, not before SDD).
- **Feature renumbering:** M1–M12 (entitlement, announcement, hub, activation, external-provider page, settings, positions, roster, invites, apps, walkthrough, availability); S1–S5; F1–F4. Earlier references to the pre-review numbering are obsolete.
- **Decision:** ✅ **Ready for SDD.**

## Readiness checklist (post-review)

- [x] All 6 review blockers resolved in the revised PRD
- [x] User stories split/added per review; all follow role/want/why format
- [x] Acceptance criteria carry stable IDs; checklist semantics defined solely by the Step Contract Table
- [x] Scope pinned by the Release Manifest (v1 / Phase 2 / fast-follow / uncommitted) with launch gates and rollback
- [x] Dependency Register with owners, status, fallbacks, launch gates
- [x] KPIs operationally defined with matching tracking events; baseline capture is a launch gate
- [x] Remaining open questions (Q1, Q3, Q4, Q5) are explicitly non-blocking for SDD start, with owners and needed-by points

## Launch blockers (unchanged in nature, now formally gated)

- **Q1/DR-2 — commercial transition for current premium payers** must be decided before the announcement publishes (product/billing, Spec 036 territory). Dark deploys proceed regardless.
- Announcement (M2) is sequenced **last** per the Release Manifest — a manual action after all launch-gated features are verified live on pilot stores.

## Key codebase facts the PRD is grounded in (July 2026 audit)

- No onboarding/checklist/tour infra exists in the product; only the Flows.sh tour in the demo sandbox.
- Premium gate is enforced inline in `SchedulingPageController::calendar()/settings()/timesheets()` (`PremiumGateMiddleware` exists but is unwired) — but Spec 035's full footprint (chat, AI, mobile flags) is wider, hence the M1-01 inventory. `premium/marketing.html` frees up as the hub URL.
- `LoginAccessService::sendInvitation()` persists an activation token and **sends nothing** — no email/SMS delivery exists; token storage is user-global with no store context (drives M9-03).
- App-use signals: `userDeviceTokens.appId` ('team'/'live') = push-registered per app; `oauthRefreshTokens.lastUsedAt` = logged in but may not distinguish the app (drives M10-03/DR-7). `users.lastLoginAt` is NOT updated by mobile login.
- AI fill errors with `ERROR_NO_OPEN_SHIFTS` on a week with zero open shifts — walkthrough beat order (open shift before AI) is a hard constraint.
- No CSV roster import exists (export only); no QR/app-invite deep links exist.
- Spec 037's SMS category taxonomy has no invite category (drives M9-08/D-6). Spec 040 System Alerts has no role targeting and per-user (not per-store) acknowledgment (drives D-3/M2-02).

## Next step

Implementation in progress — T0 complete 2026-07-21; see implementation-plan.md Phase Summaries.
