diff --git a/CONTEXT.md b/CONTEXT.md index 10c07e3..be4eced 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -13,8 +13,7 @@ people (or two devices for the same person) can both resume the same session. The checkout session moves through a single state machine (`status` field): -- **created** — session just made for a selected listing; inventory hold placed. -- **active** — fan is viewing/resuming the session; inventory still held, price/inventory +- **active** — session created with inventory held; fan is viewing/resuming; price/inventory can still change underneath it. - **pending_payment** — fan submitted payment; awaiting provider result. This is the window where a second device resuming the same session is the actual duplicate-order hazard the @@ -22,8 +21,8 @@ The checkout session moves through a single state machine (`status` field): - **completed** — order placed successfully. Terminal. - **expired** — inventory hold lapsed before completion. Terminal unless the fan starts a fresh session (re-priced/re-held). -- **failed** — payment or completion failed. Fan may retry (transitions back to **active**) - or the session expires. +- **failed** — payment or completion failed. Fan may retry (claims again through + **pending_payment**) or the session expires. ## Price Reconfirmation @@ -50,3 +49,8 @@ own `expiresAt` implies the hold is still good — the hold can lapse independen shorter TTL than the session, or inventory reclaimed for other reasons). A session can be unexpired but reference inventory that is no longer held; that combination surfaces to the fan as a distinct "listing no longer available" state, not the same as "session expired." +Conversely, when the session clock itself lapses, checkout also releases the hold +(request-time via `expireIfNeeded`, or in the background via `expireLapsedSessions` / +`SessionExpirySweeper`) so inventory isn't stranded — `expiryReason` stays +`session_lapsed` (why the session died), not `hold_released` (hold disappeared while the +session was still live). diff --git a/README.md b/README.md index 9f5383d..5de3d27 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,50 @@ # Notes about the demo -- Cross-surface resume in this demo is **web ↔ web** (copy the link into another - tab/browser) and **mobile → web** (native share/copy hands the fan a web - checkout URL). I did not build a seamless **web → mobile** handoff: a browser - cannot detect whether an iOS/Android simulator (or Expo) is running on the - reviewer’s machine, and custom-scheme deep links (`mobileweb://…`) only work - when that OS has registered the scheme. An earlier **Open in app** control on - web (navigate / `window.open` to `mobileweb://…`) proved too volatile — it - either navigated the checkout page away or failed silently depending on the - browser and whether Expo had claimed the scheme — so it was removed. Use - `xcrun simctl openurl` (below) for local deep-link demos instead. -- I also wanted to pressure-test a quickly built React Native demo alongside - shared UI and a monorepo with multiple apps. This started from a GitHub - template I maintain, with a decent amount of cleanup so the shared packages - actually show code reuse between the React Native app and the web app. -- The core sharing/resume design came together quickly; I spent extra time on - presentation so the demo feels attractive and demonstrates mobile ↔ web - parity, not just a working handoff. -- **Why go past a “minimal UI” slice?** The prompt’s time box (~2–3 hours) - prefers a focused end-to-end continuity path over a broad checkout clone. I - still invested past that floor on purpose: a bare curl-and-JSON demo proves - the state machine, but a product-shaped shell (selection → checkout, shared - tokens/UI, Gametime-like visual hierarchy) is a better signal of how I attack - _complicated_ product + systems problems — domain boundaries, cross-surface - consistency, and reviewer-friendly UX — not only whether `resume` returns - `200`. Treat the polish as intentional flex, not scope creep that replaced the - continuity core. - -**Time trade-offs (kept the continuity slice honest, deferred production depth):** - -- In-memory session store and deterministic payment/inventory fakes — fine per - the prompt, zero reviewer setup, and the CAS race is still real in one - process. No Prisma session model, no real Stripe/inventory SDKs, no auth. -- Surfaces discover changes by acting (resume / complete), not via push — with - one demo exception: **Sec 118 · Row 8 · 10s price demo** (`listing_3`) ages its _held_ - price after 10 seconds so you can watch reconfirmation live. Catalog price - stays at the seed if you leave and browse again; a new session resets the - timer. Other failure modes (decline, sold-out) still need tests / - `forceOutcome` / `releaseListing`. -- No automated create-on-web / complete-on-mobile E2E; unit tests cover the - state machine and conflict path, and the handoff is verified by hand. -- Fixed 10-minute session TTL, two-tap price confirm then buy (not one-tap), - and share via opaque session id only — deliberate scope cuts, not unfinished - stubs. - -**Beyond the basic ask (to raise demo quality):** - -- Ticket landing with listing selection and a static stadium map so the flow - starts like a product, not a bare “create session” curl. -- Shared design tokens + `@repo/ui` checkout/listings so web (Next SSR) and - native actually share components — not two parallel UIs with the same API. -- Visual shell aligned to Gametime-style mocks (layout, hierarchy, share UX) - while keeping payment/promo chrome decorative and non-functional. -- Domain write-up (`CONTEXT.md`), decision log (`docs/decisions.md`), and - structured instrumentation so the _why_ of the state machine is reviewable. - -**AI-assisted engineering trail:** session notes under [`docs/`](./docs/) -(`session__