12 Commits

Author SHA1 Message Date
metatroncubeswdev
e6b8b710c4 feat(phase-8): implement GDPR compliance webhook handlers
Some checks failed
CI / Lint, Unit & Integration Tests (push) Has been cancelled
Replace the customers/data_request, customers/redact, and shop/redact
stubs with real logic in app/services/gdpr.server.ts: compile a
customer's Booking history, anonymize customerEmail/customerPhone on
redact, and purge every shopDomain-scoped row on shop uninstall
(Booking before Location, relying on Location's cascade for
SlotTemplate/SlotOverride/BlackoutDate/Zone/Rate). Covered by a new
tests/integration/gdpr.test.ts against live Postgres to verify the
FK deletion order actually works, not just typechecks.

Still commented out in shopify.app.toml pending Protected customer
data access approval in the Partner Dashboard — unrelated to code
readiness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:20:16 -04:00
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
c5ec8f368c feat: Phase 6 — ops/dispatch dashboard
- Prisma: Booking.totalPriceCents (parsed from the order webhook's
  total_price string), populated in booking.server.ts so "revenue by
  method" is real data, not a placeholder.
- app/services/dashboard.server.ts: pure aggregation over injected booking
  data (CLAUDE.md — no DB calls in the math) — bucketByLocalDate (each
  booking grouped under its own location's local calendar day, not a
  shared UTC day), revenueByMethod (confirmed/fulfilled only),
  utilizationByDateLocation (booked vs. summed SlotTemplate capacity for
  that weekday, capped at 100%), upcomingFulfillments, and a CSV
  writer/formatter with proper quote-escaping.
- /app/dashboard: filterable (date range, location, method, status) view
  with revenue-by-method cards, a capacity-utilization list (color-coded
  by load), an upcoming-fulfillments table with one-click
  confirmed->fulfilled/no_show status transitions, and a full by-day
  booking list.
- /app/dashboard/export: a resource route (loader only, no component)
  streaming the same filtered bookings as a downloadable CSV — kept
  separate from the dashboard route specifically so it can import
  dashboard.server.ts freely without the client-bundling constraint the
  main route has to respect (see below).

Fixed the same class of server/client bundling bug from the Phase 5 commit
before it could ship, this time by construction: dashboard.server.ts's
aggregation functions are called only inside app.dashboard.tsx's `loader`,
never referenced by the default-exported component (which only reads
useLoaderData() output) — verified this holds by actually running
`npm run build`, not just tsc/vitest, which both stay silent about this
class of error. Also hit (and fixed) the same "loader Dates arrive as
strings on the client" issue from Phase 1: swapped DateTime.fromJSDate for
DateTime.fromISO in the two places the component formats a booking's
slotStart.

Verified: lint, typecheck, 102 unit tests (+16 new for dashboard.server.ts,
+2 new for totalPriceCents parsing), 18 integration tests, both builds, and
a live script exercising the full aggregation pipeline (revenue exclusion
of cancelled bookings, utilization math, CSV output) against the Postgres
container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 03:33:21 -04:00
metatroncubeswdev
6598691372 feat: Phase 5 — multi-location, zones, rates, auto-assignment
- Prisma: Zone (postal-code list or radius), Rate (zone- or distance-band
  keyed), GeocodeCache (permanent address->lat/lng cache per
  IMPLEMENTATION_PLAN.md §9), Location.shopifyLocationId (maps to
  Shopify's own Location resource for inventory checks), Booking.zoneId
  (needed for per-zone delivery-density counts, not just per-location).
- app/lib/geo.ts: pure haversine distance + postal-code matching, unit
  tested against known city-to-city distances.
- app/services/zones.server.ts: geocoding (Google Maps Geocoding API,
  cached — never re-geocodes the same address twice), zone eligibility,
  nearest-location auto-assignment ranked by distance, delivery-density
  threshold checks (a sparse zone doesn't unlock until minOrders bookings
  have already routed through it), and inventory-based location exclusion
  via Shopify's InventoryLevel API (locations without a mapped
  shopifyLocationId are left in rather than false-negative excluded).
- app/services/rates.server.ts: pure rate resolution by zone or distance
  band, cheapest-match-wins when bands overlap.
- apps.scheduling.availability.tsx: LOCAL_DELIVERY requests with a
  postalCode/address now auto-assign to the nearest eligible,
  density-qualified zone/location instead of the shop's default location;
  response includes the matched rate. Also fixed a real gap left over from
  Phase 4: this route never actually read Booking counts into
  getAvailability's `consumed` map, so capacity always showed as fully
  available regardless of existing bookings — now it does.
- extensions/datetime-widget: LOCAL_DELIVERY now asks for a postal code
  before showing dates; PICKUP shows a Google Maps pin for the location
  (both gated on an optional Maps API key — a block setting in the theme
  editor, since it needs to be public/client-side, not an app secret);
  confirmation display and cart attributes (dd_zone_id, dd_rate_label)
  carry the resolved zone/rate through to checkout.
- extensions/delivery-customization: now appends the resolved rate to the
  relabeled delivery option ("Local delivery — Aug 25 ($5.99)") when one's
  configured — real Cart Transform-based fee *charging* stays deferred to
  v2 per IMPLEMENTATION_PLAN.md §5.4, this is display-only.
- Admin: /app/zones and /app/rates (Polaris CRUD, mirroring Phase 1's
  patterns), plus shopifyLocationId and auto-geocode-on-save added to the
  location edit form.

Fixed one real bug caught only by `npm run build` (not tsc/vitest, which
both passed clean): app.rates._index.tsx's component called
formatPriceLabel from rates.server.ts, and Remix correctly refuses to
bundle anything imported from a .server.ts path for the client. Moved the
pure (no I/O, no Prisma) formatter to app/lib/currency.ts.

Verified: lint, typecheck, 86 unit tests (+21 new: geo, zones, rates,
delivery-customization's rate-label case with a real WASM fixture run),
16 integration tests against live Postgres (+8 new: geocode caching,
postal/radius zone matching, nearest-first ranking, density thresholds),
both builds, and a live script exercising the full
zone-match -> density-check -> rate-resolve -> availability pipeline
together against the Postgres container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 03:27:21 -04:00
metatroncubeswdev
cc3303d318 fix: disable GDPR compliance webhooks pending Protected Customer Data access
shopify app dev refused to push the 3 mandatory compliance-topic webhooks
(customers/data_request, customers/redact, shop/redact) with "This app is
not approved to subscribe to webhook topics containing protected customer
data" — subscribing to these requires the org to first request and be
granted Protected Customer Data Access in the Partner Dashboard, a manual
approval step outside the CLI/config entirely.

Commented out the three subscription blocks in shopify.app.toml (handlers
are untouched and still fully wired) so dev can proceed now. README.md
gets a new "Before public launch" section as the reminder to re-enable
them once access is granted — this is a hard requirement for BfS/public
submission per CLAUDE.md, not something to forget once dev is unblocked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 23:19:26 -04:00
metatroncubeswdev
e116a6c87a fix: Theme App Extension only allows assets/blocks/snippets/locales dirs
`shopify app dev` failed dev preview with "Only assets, blocks, snippets,
locales directories are allowed" — the widget's TypeScript source lived in
extensions/datetime-widget/src/, which isn't one of the four directories a
Theme App Extension may contain. Moved it to widget-src/datetime-widget/
(a plain, non-extension folder outside extensions/) and updated
build:widget's esbuild input path accordingly; the bundled output still
lands in the same place (extensions/datetime-widget/assets/).

Also fixed a theme-check warning surfaced during the same run:
`script_tag` renders a parser-blocking <script> with no way to defer it —
switched to a manual <script defer> tag for the widget's JS asset.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 21:51:54 -04:00
metatroncubeswdev
381c52d01a fix: replace hand-scaffolded Functions with real CLI-generated ones
The user linked the app to a real Partner org and hit two live errors
running `shopify app dev`, which is exactly the verification the earlier
hand-scaffolded Functions couldn't get in this environment. Root-caused and
fixed both, then went further: regenerated both Functions from scratch via
`shopify app generate extension` (now possible — the user's session had
authenticated) instead of patching the guesses.

What broke and why:
- `shopify app config link` pulled a fresh app's (empty) remote config and
  overwrote shopify.app.toml, dropping the webhook subscriptions and
  app_proxy block — restored both, keeping the real client_id/name/scopes
  the CLI set.
- `[extensions.build.watch]` as a nested table was invalid TOML for this
  field — it's a plain `watch = [...]` array directly under
  `[extensions.build]`.
- The real failure ("doesn't have a build command or it's empty") turned
  out to be a red herring pointing at a stale filename
  (shopify.function.extension.toml, not the current shopify.extension.toml)
  — the actual problem was that `@shopify/shopify_function` was never
  installed for these extensions (confirmed: no node_modules), because
  hand-writing package.json doesn't run the install step
  `shopify app generate extension` does automatically.

Rather than keep guessing at the toolchain, regenerated both Functions for
real:
- `shopify app generate extension --template=cart_checkout_validation` and
  `--template=delivery_customization` (--flavor=vanilla-js), which produces
  a working vite/vitest-based build+test setup, real
  `@shopify/shopify-function-test-helpers` fixture testing (builds actual
  WASM and runs it via function-runner), and a generated GraphQL type file
  per extension.
- This surfaced several concrete corrections to what was hand-written
  before: the real target names are `cart.validations.generate.run` and
  `cart.delivery-options.transform.run` (not `purchase.validation.run` /
  `purchase.delivery-customization.run`), current api_version is 2026-07
  (not 2025-01), the validation output wraps errors in
  `operations: [{ validationAdd: { errors } }]` with a plain `message`
  field (not top-level `errors` with `localizedMessage`), and the rename
  operation is `deliveryOptionRename: { deliveryOptionHandle, title }` (not
  `rename: { deliveryOptionHandle, title }`).
- Rewrote each extension's `.graphql` input query to request our actual
  dd_* cart attributes (plus delivery option handles for the rename case),
  regenerated types via `npm run typegen` in each, and ported the pure
  evaluate.js decision logic (same exported function names/behavior as
  before, now proven correct against the live schema) into the adapter
  file the generator expects.
- Replaced each extension's demo fixture with ones matching our real
  logic; `npm test` inside each extension now compiles real WASM and runs
  function-runner against them — this is strictly stronger verification
  than the previous pure-JS-only unit tests (which are kept too, unchanged,
  since the evaluate.js files kept the same interface).

Repo-wide wiring: extensions/*/generated and extensions/*/dist are not
committed (matches the CLI's own per-extension .gitignore) — added
`npm run typegen:functions` (runs automatically before `npm run typecheck`
via a pretypecheck hook) and `npm run test:functions`, both now also in CI.
Root `npm install` picked these two folders up as proper npm workspace
members (they already have their own package.json from generation).

Verified: lint, typecheck, all 52 unit tests, all 8 integration tests, both
extensions' real WASM/function-runner test suites (5 fixtures total), and
both `npm run build` / `npm run build:widget` all pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 20:14:21 -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
metatroncubeswdev
039b1539ab docs: update README for Phase 3 completion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 18:02:47 -04:00
metatroncubeswdev
24980b8e03 docs: update README status for Phase 2 completion
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 17:53:49 -04:00
metatroncubeswdev
a35a4f89be docs: update README for Postgres-based local dev and Phase 1 status
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 17:48:19 -04:00
metatroncubeswdev
0303eba07a feat: scaffold Phase 0 — Remix app template, CI, Redis/BullMQ, test harness
Bootstraps from Shopify's official shopify-app-template-remix (cloned
directly rather than via `shopify app init`, which requires an interactive
Partner login unavailable in this session):

- Prisma (SQLite dev / Postgres-ready) with baseline Session model + migration
- Vitest configured for unit tests, Playwright configured for E2E
- Redis client + BullMQ worker skeleton (app/lib/redis.server.ts, jobs/worker.ts)
- shopify.app.toml: minimal scopes, GDPR + orders webhooks wired (stub handlers),
  app proxy config for the future storefront widget
- Stripped template-repo-only meta files (CLA, issue templates, demo product
  page) and replaced CI with a lint+typecheck+test workflow
- Bumped @shopify/shopify-app-session-storage-prisma to resolve a duplicate
  @shopify/shopify-api install that broke typecheck
- Dropped the Jest-only ESLint config (template default) since the project
  standardizes on Vitest per IMPLEMENTATION_PLAN.md

Verified: npm install, lint, typecheck, unit tests, prisma migrate dev, and
npm run build all pass on Node 22 LTS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 15:45:28 -04:00