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 mainplaywright.config.tswith 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 entitiesPOST /api/bookings— booking form submission (currently email-only)PATCH /api/contact-submissions/:id— moderation (belongs on/admin/contact-submissions)