Some checks failed
CI / Lint, Unit & Integration Tests (push) Has been cancelled
Add real Shopify Billing API integration: Free/Starter/Growth/Pro plans (app/lib/billing-plans.ts, priced per PRODUCT_STRATEGY.md §6) wired into shopify.server.ts's billing config, a merchant-facing plan page (app/routes/app.billing.tsx) using billing.request/billing.cancel, and webhooks.app_subscriptions.update.tsx as the durable sync path for Shop.tier (fires even when a merchant cancels from Shopify's own billing page, not just from this app). Gate the features actually built so far in both loader and action (never just hidden in the UI, so a direct POST can't bypass a tier limit): delivery zones/rates require Growth+, the dispatch dashboard requires Starter+, and location count is capped per tier (Free=1, Starter=3, Growth/Pro=unlimited). Split pure tier logic (app/lib/billing-plans.ts) from DB-backed reads/writes (app/services/billing.server.ts) so the client-rendered UpsellState component can import the Tier type without pulling server code into the client bundle — same split as currency.ts. Covered by tests/unit/billing-plans.test.ts (pure tier ranking/mapping) and tests/integration/billing.test.ts (tier persistence and location-limit enforcement against live Postgres). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
146 lines
8.2 KiB
Markdown
146 lines
8.2 KiB
Markdown
# Delivery Date & Time
|
|
|
|
Shopify app: scheduling for Shipping / Local Delivery / Store Pickup, with
|
|
date-time slots, capacity intelligence, and **all-plan checkout enforcement**
|
|
via Cart/Checkout Validation Functions.
|
|
|
|
Read **[PRODUCT_STRATEGY.md](./PRODUCT_STRATEGY.md)** (why) and
|
|
**[IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md)** (how, phased build
|
|
order) before making changes. [CLAUDE.md](./CLAUDE.md) holds the
|
|
non-negotiables for AI-assisted work in this repo.
|
|
|
|
## Getting started
|
|
|
|
```sh
|
|
npm install
|
|
cp .env.example .env # fill in Shopify app credentials after linking
|
|
docker compose up -d # local Postgres (5433) + Redis (6380)
|
|
npx prisma migrate dev
|
|
npm run dev # shopify app dev — requires `shopify auth login` first
|
|
```
|
|
|
|
Dev uses Postgres, same as prod, since the schema relies on Prisma enums and
|
|
(from Phase 5 on) array fields that SQLite can't express. `npm run worker`
|
|
runs the BullMQ worker (jobs/worker.ts) once Phase 4 makes it do anything.
|
|
|
|
## Commands
|
|
|
|
| Command | Purpose |
|
|
|---|---|
|
|
| `npm run dev` | `shopify app dev` — local dev against a dev store |
|
|
| `npm test` | Vitest unit tests |
|
|
| `npm run test:e2e` | Playwright E2E |
|
|
| `npm run lint` / `npm run typecheck` | ESLint / `tsc --noEmit` |
|
|
| `npx prisma migrate dev` | DB migrations |
|
|
| `npm run deploy` | `shopify app deploy` — deploy extensions/functions |
|
|
| `npm run worker` | BullMQ worker (hold-expiry, notifications) |
|
|
| `npm run test:integration` | Redis/Postgres-backed tests (slot-hold concurrency, booking flow) — needs `docker compose up -d` |
|
|
| `npm run test:functions` | Real WASM build + `function-runner` tests for both Shopify Functions, against fixtures |
|
|
| `npm run typegen:functions` | Regenerate `extensions/*/generated/api.ts` from each Function's `schema.graphql` + `.graphql` query (also runs automatically before `npm run typecheck`) |
|
|
|
|
## Before public launch
|
|
|
|
The 3 mandatory GDPR compliance webhooks (`customers/data_request`,
|
|
`customers/redact`, `shop/redact`) are commented out in `shopify.app.toml`
|
|
— Shopify refuses to push them until the org requests and is granted
|
|
**Protected customer data access** in the Partner Dashboard (Apps → this
|
|
app → API access → Protected customer data), which is a manual
|
|
questionnaire/approval step. The handlers are fully implemented against the
|
|
real schema (`app/services/gdpr.server.ts`, used by
|
|
`app/routes/webhooks.customers.*.tsx` and `webhooks.shop.redact.tsx`,
|
|
covered by `tests/integration/gdpr.test.ts`): `customers/data_request`
|
|
compiles and logs the customer's Booking history for the merchant to hand
|
|
off (Shopify's webhook has no response payload — a self-serve export is a
|
|
Phase 9+ notification-system enhancement); `customers/redact` anonymizes
|
|
`customerEmail`/`customerPhone` on matching Bookings while keeping the
|
|
booking rows for the shop's own revenue/utilization history;
|
|
`shop/redact` deletes every shopDomain-scoped row (Booking first, then
|
|
Location — whose cascade removes SlotTemplate/SlotOverride/BlackoutDate/
|
|
Zone/Rate — then Shop and Session; `GeocodeCache` is deliberately excluded,
|
|
it's a shared address-keyed cache with no shopDomain). Once Protected
|
|
customer data access is granted, uncomment the three
|
|
`[[webhooks.subscriptions]]` blocks near the bottom of the webhooks
|
|
section. **Required before any public launch or Built-for-Shopify
|
|
submission** — don't ship without it.
|
|
|
|
## Status
|
|
|
|
Phase 0 (scaffold & CI) through Phase 7 (POS + Checkout UI extensions) are
|
|
complete — that's the full v1 launch scope per §8 of
|
|
`PRODUCT_STRATEGY.md`/§6 of `IMPLEMENTATION_PLAN.md`. Phase 8 (billing +
|
|
Built-for-Shopify hardening) is in progress; Phases 9-10 (v1.x fast-follow,
|
|
v2) are explicitly separate post-launch milestones in the plan, not part of
|
|
v1.
|
|
|
|
**Billing (Phase 8):** real Shopify Billing API integration
|
|
(`app/shopify.server.ts`'s `billing` config, built from
|
|
`app/lib/billing-plans.ts`'s plan/price table — Free/Starter/Growth/Pro per
|
|
`PRODUCT_STRATEGY.md` §6). `app/routes/app.billing.tsx` lets a merchant
|
|
switch plans (`billing.request`) or downgrade to Free (`billing.cancel`);
|
|
`webhooks.app_subscriptions.update.tsx` is the durable sync path that keeps
|
|
`Shop.tier` correct even when a merchant cancels from Shopify's own billing
|
|
page instead of this app. Feature gates enforced **server-side** in both
|
|
loader and action (not just hidden in the UI): Delivery zones/rates need
|
|
Growth+ (`app.zones._index.tsx`, `app.rates._index.tsx`), the dispatch
|
|
dashboard needs Starter+ (`app.dashboard.tsx`, `app.dashboard.export.tsx`),
|
|
and location count is capped per tier (Free=1, Starter=3, Growth/Pro=
|
|
unlimited — `app.locations.new.tsx`, `app.locations._index.tsx`'s
|
|
seed-template action). Gated features currently only cover what's actually
|
|
built in Phases 0-7 — several Growth/Pro features listed in
|
|
`PRODUCT_STRATEGY.md` §6 (waitlists, reschedule portal, SMS, etc.) are
|
|
Phase 9/10 and not yet implemented, so aren't gated on anything yet.
|
|
|
|
**Every scheduling surface calls the same two service functions**
|
|
(`app/services/availability-request.server.ts`,
|
|
`app/services/hold-request.server.ts`) — the storefront widget (app-proxy
|
|
auth), POS (`extensions/pos-datetime`, session-token auth), and Plus
|
|
checkout (`extensions/checkout-datetime`, session-token auth) each have
|
|
their own thin route wrapper but share the exact same resolution logic and
|
|
Redis-backed capacity pool, so a slot booked from any one of them is
|
|
unavailable on the other two. `extensions/checkout-datetime`'s Thank You
|
|
block and `extensions/datetime-widget`'s new `order-confirmation.liquid`
|
|
block both show the confirmed slot after checkout — the Liquid block is
|
|
what actually satisfies "all plans," since Checkout UI Extensions'
|
|
thank-you/order-status targets are Plus-only; there's no
|
|
`purchase.order-status.block.render` target in this API version (verified
|
|
against `@shopify/ui-extensions`' own type definitions — an early guess
|
|
based on the target name pattern was wrong).
|
|
|
|
**Unverified without a live device/store to test against** (noted in-code
|
|
where relevant): `pos-datetime` and `checkout-datetime` both assume
|
|
`process.env.APP_URL` is substituted at build time to the app's backend
|
|
origin, and neither extension's actual runtime behavior has been exercised
|
|
outside of typechecking against `@shopify/ui-extensions`' bundled types
|
|
(which did catch several wrong API-shape guesses during development).
|
|
|
|
Phase 5's Google Maps / geocoding features (pickup-location map in the
|
|
widget, radius-zone eligibility, address auto-geocoding on Save Location)
|
|
are only live if `GOOGLE_MAPS_API_KEY` is set — either as an env var for
|
|
server-side geocoding, or as the "Google Maps API key" block setting in the
|
|
theme editor for the storefront map. Without a key, everything else in
|
|
Phase 5 (postal-code zones, distance-band rates, delivery-density
|
|
thresholds) still works — those don't need Maps at all.
|
|
|
|
The storefront widget's TypeScript source lives in `widget-src/datetime-widget/`,
|
|
**not** inside `extensions/datetime-widget/` — a Theme App Extension's
|
|
directory may only contain `assets`, `blocks`, `snippets`, and `locales`
|
|
(the CLI hard-rejects anything else, e.g. a `src/` folder, with "Only
|
|
assets, blocks, snippets, locales directories are allowed"). Editing the
|
|
widget? Run `npm run build:widget` to bundle it into
|
|
`extensions/datetime-widget/assets/datetime-widget.js` — it also runs
|
|
automatically before `npm run dev` / `npm run deploy`.
|
|
|
|
**Functions are JavaScript, not Rust** (`extensions/validation-slot/`,
|
|
`extensions/delivery-customization/`) — no Rust toolchain was available in
|
|
the environment that built Phase 4, and `IMPLEMENTATION_PLAN.md` §1
|
|
explicitly allows JS as a fallback. Both were generated with
|
|
`shopify app generate extension` (once a real Partner login was available)
|
|
and their business logic (`src/evaluate.js` in each) is verified two ways:
|
|
plain Vitest unit tests at the repo root (`npm test`) and real
|
|
`function-runner` fixture tests that compile actual WASM
|
|
(`npm run test:functions`, also in CI). `extensions/*/generated/` and
|
|
`extensions/*/dist/` aren't committed (matching the CLI's own
|
|
`.gitignore` for these extensions) — `npm run typegen:functions`
|
|
regenerates the types from the committed `schema.graphql`, and building
|
|
runs automatically as part of `npm run dev` / `test:functions`.
|