2 Commits

Author SHA1 Message Date
metatroncubeswdev
5b2207a397 feat: Phase 7 — POS + Checkout UI extensions
Some checks failed
CI / Lint, Unit & Integration Tests (push) Has been cancelled
Also fixes the [events] gate that was blocking ALL extension generation
(discovered while starting this phase).

- shopify.app.toml: this org appears enrolled in Shopify's "Next
  Generation Events" developer preview, which the CLI now treats as a
  REQUIRED top-level [events] section even though nothing in this app
  actually uses it (real webhook handling is entirely classic [webhooks],
  unaffected). Iteratively discovered the required shape from the CLI's
  own field-by-field validation errors, then found the real docs (Events
  is optional/developer-preview, api_version pinned to "unstable") to
  confirm rather than keep guessing. Added a functionally-inert
  [[events.subscription]] placeholder + a stub handler
  (webhooks.events.placeholder.tsx) solely to satisfy the gate.

- app/services/availability-request.server.ts +
  app/services/hold-request.server.ts: extracted the resolution logic that
  used to live directly in apps.scheduling.availability.tsx/hold.tsx into
  shared functions. This is what actually makes "same capacity pool feeds
  every surface" (CLAUDE.md) true by construction rather than by
  convention — the storefront, POS, and checkout routes now call the exact
  same code, not three copies that could quietly drift apart.

- extensions/pos-datetime (generated via `shopify app generate extension
  --template=pos_smart_grid` — pos_action's flavor requirement contradicted
  the CLI's own global --flavor validator, so smart_grid was used instead):
  a home-screen tile opening a modal where staff pick method -> date -> time
  against the same availability/hold endpoints (pos.scheduling.*.tsx,
  session-token authenticated), writing the same dd_* cart properties via
  CartApi.addCartProperties — booking.server.ts needed zero changes to
  handle POS-originated orders. Several API-shape guesses (toast isError
  option, ChoiceList's `value`/`label` props, a nonexistent
  action.dismissModal(), shopify.cart.cart.current) were wrong and caught
  by typechecking directly against @shopify/ui-extensions' own bundled
  .d.ts files (`npm run typecheck:pos`, now also in CI) — none of this was
  verified against a live POS session, which isn't possible in this
  environment.

- extensions/checkout-datetime (generated via `--template=checkout_ui`):
  the Plus-only native picker in checkout itself
  (purchase.checkout.block.render) plus a Thank You confirmation block
  (purchase.thank-you.block.render). Went looking for an order-status
  target too ("all plans show confirmed slot on thank-you/order-status" is
  the Phase 7 accept criterion) and confirmed via the installed package's
  own type definitions that purchase.order-status.block.render does not
  exist in this API version — checkout UI extensions' thank-you/order-status
  surfaces are Plus-only regardless. The actual "all plans" mechanism is
  extensions/datetime-widget/blocks/order-confirmation.liquid — a new Theme
  App Extension block reading order.note_attributes, which works on every
  plan since it's plain Liquid, not checkout extensibility.

Both new UI extensions share one real unverified assumption, called out in
code comments: process.env.APP_URL is expected to be substituted at build
time by the Shopify CLI to the app's backend origin, since these run in a
different origin than the app and need an absolute URL, unlike the
storefront widget's relative /apps/scheduling/* path. Needs confirming
against a live dev session.

Verified: lint, typecheck (root + both new extensions'
`npm run typecheck:pos`/`typecheck:checkout`, all now in CI), 102 unit +
18 integration tests (unchanged — this phase didn't touch pure business
logic, only added thin auth wrappers around already-tested services), both
admin/widget builds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:11:34 -04:00
metatroncubeswdev
7a340ad135 feat: Phase 4 — enforcement Functions + slot-holds
The core competitive moat (PRODUCT_STRATEGY.md §3.1): checkout can no
longer complete without a valid, still-available slot, on any Shopify
plan. User confirmed writing Functions in JS rather than Rust — no Rust
toolchain was available in this environment, and IMPLEMENTATION_PLAN.md §1
explicitly allows JS as a fallback ("Rust preferred, JS acceptable").

- app/services/holds.server.ts: Redis-backed soft slot-holds with TTL.
  Hold creation is a Lua script (EVAL) — the "does this slot have room"
  check and the reservation itself have to be one atomic Redis operation,
  or two concurrent requests can both read "one spot left" and both
  succeed. Outstanding holds per slot live in a sorted set scored by expiry
  (so eviction is just ZREMRANGEBYSCORE, no separate expiry job needed to
  read a correct count), and a repeat request from the same cart renews
  its own hold instead of competing against the capacity gate again.
- app/routes/apps.scheduling.hold.tsx: public app-proxy endpoint the widget
  calls the moment a shopper picks a slot — reserves capacity BEFORE the
  cart attribute is written, since the attribute alone is just two
  shoppers racing to write the same field.
- extensions/datetime-widget: now fetches the cart token, requests a hold
  first, and only writes cart attributes on success; shows an error and
  refreshes the slot list if it loses the race.
- app/services/booking.server.ts + webhooks.orders.create.tsx: converts an
  order's dd_* cart attributes into a confirmed Booking (idempotent on
  orderId — webhooks redeliver), releases the matching hold, and writes a
  `delivery_datetime.booking` order metafield so the slot is visible on the
  order record natively (IMPLEMENTATION_PLAN.md §5.2/§2). Deliberately does
  NOT re-check capacity and reject at this point — by the time an order
  exists, payment has happened; that's the Function's job, earlier.
- webhooks.orders.cancelled.tsx: marks the Booking cancelled, freeing its
  capacity.
- extensions/validation-slot (Cart/Checkout Validation Function): blocks
  checkout when the cart's dd_* attributes are missing or incomplete — a
  shopper who bypasses the widget entirely (clears the attribute, calls the
  cart API directly) still cannot check out, because this runs inside
  Shopify's own checkout, not the browser. Scope note documented in
  evaluate.js: this doesn't yet re-validate against a live capacity
  snapshot at the moment checkout completes ("has since been taken" in
  §3.1) — Functions can't call our DB, and a metafield-snapshot refresh
  pipeline for that is unscoped work; the 10-minute hold TTL is the interim
  mitigation for that specific race.
- extensions/delivery-customization: relabels every delivery option to the
  shopper's actual chosen method + date ("Pickup — Aug 25" instead of a
  generic carrier label), directly fixing the "estimated delivery date on a
  pickup order" complaint §3.1 names. payment-customization is deliberately
  NOT built yet — there's no configurable payment-method rule for it to
  enforce until later phases add one; shipping a no-op Function serves
  nothing.
- Prisma: added Booking (no SlotHold table — Redis is the sole source of
  truth for holds, per §4's own "(Redis-backed)" annotation; mirroring it
  into Postgres would just be a sync-consistency burden with no benefit).
- Both Function extensions are hand-scaffolded from Shopify's documented
  JS Function structure (same interactive-login limitation as the Theme
  App Extension) — README.md flags that `shopify app function schema`
  should be run before deploying to confirm the input queries still match
  the live schema.

New tests/integration/ suite (separate vitest config, needs live
Redis+Postgres — `docker compose up -d` locally, a services: block in CI)
holds the tests that can't be meaningfully mocked: the slot-hold
concurrency test CLAUDE.md calls out as non-negotiable (20 concurrent
requests for 1 unit of capacity → exactly 1 succeeds; 30 for 5 → exactly 5;
release-then-retry; TTL expiry; same-cart renewal) and the full
hold-to-booking lifecycle including idempotency on webhook redelivery.
Both Functions' pure decision logic is separately unit-tested (11 tests).
60 total tests now pass (52 unit + 8 integration).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 18:22:51 -04:00