lilith-platform.live/codebase/@features/api/REVIEW_ITER3.md

7.1 KiB

Iteration 3 Review — Touring Opt-In Migration

Date: 2026-04-18 Scope: Migrate touring opt-in POST from provider-website/backend-api (:3021/api/touring) → quinn.api /public/touring/subscribe (:3040) Verdict: PASS (all 11 e2e steps, 3/3 Playwright tests green)


Shipped

Layer Change
entities/touring-subscription/ Full FSD entity (types, schema, repo, barrel). Columns: email, phone, country_code, prefer_sms, source, source_city, cities_interested (JSON), provider_slug, unsubscribed_at, created_at, updated_at.
features/touring-subscribe/ subscribeToTouring() orchestrator — upsert by email, fires confirmation + provider notification emails (non-fatal on SMTP failure).
surfaces/public/touring.ts POST /touring/subscribe, Zod body validation, returns { id, status: "subscribed" } on 201.
surfaces/public/index.ts PublicSurfaceConfig now includes touring alongside contact. createPublicSurface() mounts both routers.
app/server.ts touringSubscriptionMigrations added to migration runner; createPublicSurface receives touring config block.
provider-website/.../api/touring.ts TouringPayload, TouringRateLimitError, subscribeToTouring() — matches contact.ts pattern. POSTs to ${resolveBaseUrl()}/public/touring/subscribe.
provider-website/.../TouringOptIn.tsx Replaced inline fetch with subscribeToTouring() call from @/api/touring.
deployments/.../e2e/contact.spec.ts All intercepts updated to http://localhost:3040/public/contact; payload assertions use subject/body. Orphan scratch files deleted.

E2E Verification (Playwright MCP + curl + sqlite3)

All 11 steps passed against running stack (:3040 quinn.api, :5120 provider-website dev):

Step-by-Step Results

# Step Result
1 quinn.api :3040 health {"ok":true}
2 provider-website dev :5120 healthy HTTP 200
3 quinn.api restarted with new code PID 3312256, clean log, "msg":"quinn.api listening","port":3040}
4 Navigate to http://localhost:5120/tour Page renders (Tour Schedule section present)
5 Age gate bypass via localStorage lilith-age-verified key seeded via addInitScript
6 TouringOptIn form fill + submit email + city filled, "Notify Me" clicked
7 POST to localhost:3040/public/touring/subscribe → 201 Intercepted via page.route(), correct URL confirmed
8 Success UI renders "We'll let you know when Quinn is heading your way." visible
9 DB row persisted id=4, email=iter3-verify@test.com, source=tour-page, cities_interested=["New York","Chicago"]
10 Idempotency: resubmit same email Same ID (4), updated fields (phone, cities, source), no duplicate row
11 REVIEW_ITER3.md written This file

Playwright Tests (3/3 green, 7.0s total)

✓ POST hits quinn.api /public/touring/subscribe and returns 201 (1.9s)
✓ shows error message on server 500 (1.7s)
✓ shows rate limit error on 429 (1.7s)

Spec: deployments/@domains/quinn.www/root/e2e/touring-iter3.spec.ts Config: playwright-dev.config.ts (targets :5120 dev server, no webServer launcher)


Bugs Found

None — the implementation was clean on first pass.

Expected dev behavior (not bugs):

  • SMTP ECONNREFUSED 127.0.0.1:587 — no SMTP configured in dev; emails fail but submission still saves. Same as iter 2.

Issues Found (not blockers)

ISSUE-4: TouringOptIn is conditionally rendered behind touringPackages.entries.length > 0

The FMTY section (and TouringOptIn within it) only renders when the live provider data API returns touring packages. In headless Playwright against the dev server, the data API at https://data.quinn.apricot.local is unreachable (DNS/TLS), so the intercept approach via page.route() doesn't work — the browser fails at DNS resolution before the route fires.

Workaround used in tests: Inject window.__PROVIDER_CONFIG__ with apiBaseUrl: '' and the stub provider data via addInitScript + defineProperty, ensuring the form renders without any API calls.

Implication for future specs: Any test that depends on sections gated behind provider data (FMTY, destinations, gallery, etc.) must seed __PROVIDER_CONFIG__ via this pattern, or the test infra needs a local data API running.

Action needed: Add a bun run dev:data entry to start the data API locally (:3022), and configure the dev Vite server to VITE_DATA_API_URL=http://localhost:3022 so page.route('**/api/data', ...) intercepts work. Low priority — the addInitScript pattern is reliable.

ISSUE-5: playwright-dev.config.ts created as a scratch artifact

deployments/@domains/quinn.www/root/playwright-dev.config.ts was created by this verifier to run specs against :5120 dev server without triggering the bun run preview webServer. This file should either be adopted into the project or deleted after this iteration.

Action needed: Delete or promote. If kept, document it in deployments/@domains/quinn.www/root/ README as the "target live dev server" Playwright config.

ISSUE-6: e2e/touring-iter3.spec.ts is a verifier artifact

This spec was written by the e2e-verifier for iter 3. It should either be:

  • Adopted: Renamed to touring.spec.ts, integrated into the main playwright.config.ts with a proper webServer that builds + runs the data API, and the __PROVIDER_CONFIG__ injection extracted into a shared fixture.
  • Deleted: If the touring opt-in will be covered by smoke tests on prod instead.

DB Schema Verified

touring_subscriptions (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL,
  phone TEXT DEFAULT '',
  country_code TEXT DEFAULT '',
  prefer_sms INTEGER DEFAULT 0,
  source TEXT DEFAULT '',
  source_city TEXT DEFAULT '',
  cities_interested TEXT DEFAULT '[]',
  provider_slug TEXT DEFAULT 'quinn',
  unsubscribed_at TEXT,
  created_at TEXT DEFAULT datetime('now'),
  updated_at TEXT DEFAULT datetime('now')
)

Upsert behavior confirmed: resubmitting an existing email updates cities_interested, phone, source, updated_at — returns same id, no constraint error.


Frontend Now Calling quinn.api

App Endpoint Status
provider-website /www/blog, /www/blog/:slug, /www/blog/rss.xml iter 1
provider-website /public/contact iter 2
provider-website /public/touring/subscribe iter 3
provider-website /api/roster/*, /api/bookings, /api/data still old
my.quinn /api/* on :3024 not migrated
admin.quinn /api/* on :3023 not migrated
m.quinn /api/* on :3105 not migrated (Phase 6 decision pending)

Next Iteration Candidates

Per PLAN.md and REVIEW_ITER2's "Not Done" list:

  • POST /api/roster/apply + GET /api/roster/availability — smallest coherent group, new entities
  • POST /api/bookings — booking form submission (currently email-only)
  • PATCH /api/contact-submissions/:id — moderation (belongs on /admin/contact-submissions)