metatrondelivery/IMPLEMENTATION_PLAN.md
metatroncubeswdev 952d274bea docs: add product strategy and implementation plan
Initial planning docs for the delivery date/time scheduling app.

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

26 KiB

Implementation Plan — Delivery Date & Time Shopify App

Audience: Claude Code (or any engineer) building this app from an empty repo. Read PRODUCT_STRATEGY.md first for the why and the feature rationale. This document is the how: stack, architecture, schema, file layout, and a phased, task-by-task build order with acceptance criteria. Target: All Shopify plans (Basic → Plus). Built-for-Shopify quality bar.


0. Ground rules for the build

  • Stack is fixed (see §1). Do not swap frameworks. If a library choice is ambiguous, prefer the one Shopify's own app template uses.
  • Build in phases (§6). Each phase ends with working, testable software and its own acceptance criteria. Do not start a phase before the previous one's acceptance criteria pass.
  • Every phase includes tests. No phase is "done" with failing or absent tests for the logic it introduced.
  • Scheduling correctness is sacred. Timezones, DST, capacity math, and slot-holds are where this app lives or dies. Write unit tests for every date/capacity function before wiring UI.
  • Never trust the client. The storefront widget is a convenience; the Validation Function is the enforcement. Any rule that matters must be enforced server-side.
  • Keep secrets out of git. Use .env (gitignored) + shopify.app.toml. Never commit API keys.
  • Conventional commits, small PRs per task, feature-branch per phase.

1. Tech stack

Layer Choice Notes
App framework Remix via @shopify/shopify-app-template-remix Shopify's official embedded-app template
Admin UI Polaris (@shopify/polaris) + App Bridge (@shopify/app-bridge-react) BfS requirement
App DB PostgreSQL (prod) / SQLite (dev) via Prisma Transactional scheduling data
Cache / queue / holds Redis + BullMQ Slot-hold TTLs, notification jobs, capacity recompute
Shopify data GraphQL Admin API (latest stable version) via @shopify/shopify-api in the template
Storefront widget Theme App Extension (App Embed + blocks), Preact or vanilla TS Small bundle, no CLS; all plans
Checkout Checkout UI Extensions (React) Native picker on Plus; Thank-you + Order-status blocks on all plans
Enforcement / rules Shopify Functions (Rust preferred, JS acceptable) Cart/Checkout Validation, Delivery Customization, Payment Customization, Cart Transform
POS POS UI Extension Same capacity pool
Auth/session Template's Prisma session storage
Billing Shopify Billing API (managed pricing or GraphQL appSubscriptionCreate) Tiers from strategy §6
Testing Vitest (unit), Playwright (E2E admin), Function unit tests via function-runner
Language TypeScript everywhere except Rust Functions strict mode on

2. High-level architecture

                        ┌─────────────────────────────────────────────┐
                        │              Merchant (Admin)               │
                        │   Remix embedded app · Polaris · App Bridge  │
                        │  locations · slots · rules · dashboard · billing
                        └───────────────┬─────────────────────────────┘
                                        │ GraphQL Admin API + app DB
                        ┌───────────────▼─────────────────────────────┐
                        │            App Backend (Remix)               │
   Redis/BullMQ  ◄──────┤  Scheduling service (single source of truth) │──────►  Postgres (Prisma)
   holds·jobs           │  availability · capacity · holds · bookings  │        locations, slots,
                        │  REST/GraphQL app API · webhooks · billing   │        bookings, resources...
                        └───┬──────────────┬───────────────┬───────────┘
                            │              │               │ metafields / metaobjects write-back
              storefront    │   checkout   │     POS       │
        ┌───────────────────▼──┐  ┌────────▼─────────┐  ┌──▼────────────────┐
        │ Theme App Extension  │  │ Checkout UI Ext  │  │ POS UI Extension  │
        │ date/time picker     │  │ (Plus native /   │  │ staff scheduling  │
        │ → cart attributes    │  │ TY+status blocks)│  │ same pool         │
        └──────────┬───────────┘  └────────┬─────────┘  └───────────────────┘
                   │ cart attributes        │ reads booking
        ┌──────────▼────────────────────────▼─────────────────────────────┐
        │                     Shopify Functions                            │
        │  Cart/Checkout Validation  → block completion if slot invalid    │
        │  Delivery Customization     → rename/reorder/hide delivery opts   │
        │  Payment Customization      → hide payment methods by method      │
        │  Cart Transform             → line adjustments (deposits, fees)   │
        └──────────────────────────────────────────────────────────────────┘

The single rule: every surface (widget, checkout, POS) calls the same Scheduling Service for availability, and the same capacity pool for holds/bookings. Behavior never diverges between channels. Functions enforce; UIs merely collect.

Data flow for a booking (non-Plus):

  1. Widget calls Scheduling Service → gets available dates/slots for method+location+cart.
  2. Shopper picks slot → widget writes selection to cart attributes and requests a SlotHold (TTL).
  3. At checkout, Validation Function reads cart attributes, re-checks the hold/availability via a metafield-backed snapshot → blocks completion if invalid.
  4. On orders/create webhook → convert hold to Booking, write date/time/location back to the order via order metafield, consume capacity/resources, release the hold.

3. Repository layout

delivery-datetime-app/
├── CLAUDE.md                     # root agent instructions (see §7)
├── README.md
├── PRODUCT_STRATEGY.md
├── IMPLEMENTATION_PLAN.md        # this file
├── shopify.app.toml              # app config, scopes, webhooks
├── .env.example
├── package.json
├── prisma/
│   ├── schema.prisma             # §4
│   └── migrations/
├── app/                          # Remix app
│   ├── routes/
│   │   ├── app._index.tsx        # dashboard home
│   │   ├── app.locations.*.tsx   # location CRUD
│   │   ├── app.slots.*.tsx       # slot templates / overrides
│   │   ├── app.rules.*.tsx       # product/collection rules
│   │   ├── app.blackouts.*.tsx
│   │   ├── app.rates.*.tsx
│   │   ├── app.dashboard.tsx     # ops/dispatch dashboard
│   │   ├── app.settings.*.tsx    # widget/i18n/templates
│   │   ├── app.billing.tsx
│   │   ├── api.availability.tsx  # PUBLIC app-proxy endpoint for widget
│   │   ├── api.hold.tsx          # create/release slot holds
│   │   └── webhooks.*.tsx        # orders/create, orders/updated, app/uninstalled, GDPR x3
│   ├── services/
│   │   ├── scheduling.server.ts  # availability engine (PURE, unit-tested)
│   │   ├── capacity.server.ts    # resource capacity math
│   │   ├── holds.server.ts       # Redis-backed holds w/ TTL
│   │   ├── booking.server.ts     # create/reschedule/cancel bookings
│   │   ├── zones.server.ts       # geocode + radius/distance eligibility
│   │   ├── rates.server.ts
│   │   ├── notifications.server.ts
│   │   ├── templates.server.ts   # vertical presets
│   │   └── shopify-sync.server.ts# metafield/metaobject write-back
│   ├── lib/
│   │   ├── time.ts               # tz/DST-safe date math (Luxon) — PURE, unit-tested
│   │   └── db.server.ts          # Prisma client
│   └── shopify.server.ts         # template auth/init
├── extensions/
│   ├── datetime-widget/          # Theme App Extension (storefront)
│   ├── checkout-datetime/        # Checkout UI Extension (Plus native + TY/status)
│   ├── pos-datetime/             # POS UI Extension
│   ├── validation-slot/          # Function: cart/checkout validation (Rust)
│   ├── delivery-customization/   # Function: delivery options (Rust)
│   ├── payment-customization/    # Function: payment methods (Rust)
│   └── cart-transform/           # Function: line adjustments (Rust)
├── jobs/
│   └── worker.ts                 # BullMQ worker: hold-expiry, notifications, capacity recompute
└── tests/
    ├── unit/                     # scheduling, capacity, time, zones
    └── e2e/                      # Playwright admin flows

4. Prisma schema (starting point)

Build incrementally per phase, but design toward this. shopDomain scopes every row (multi-tenant).

model Shop {
  id            String   @id @default(cuid())
  shopDomain    String   @unique
  plan          String   // basic|shopify|advanced|plus
  tier          String   @default("free") // free|starter|growth|pro
  timezone      String   @default("UTC")
  settings      Json     // widget copy, i18n, feature flags
  locations     Location[]
  createdAt     DateTime @default(now())
}

model Location {
  id            String   @id @default(cuid())
  shopDomain    String
  name          String
  address       String
  lat           Float?
  lng           Float?
  timezone      String
  active        Boolean  @default(true)
  slotTemplates SlotTemplate[]
  overrides     SlotOverride[]
  blackouts     BlackoutDate[]
  zones         Zone[]
  resources     CapacityResource[]
  @@index([shopDomain])
}

enum Method { SHIPPING LOCAL_DELIVERY PICKUP }

model SlotTemplate {
  id          String   @id @default(cuid())
  shopDomain  String
  locationId  String
  location    Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
  method      Method
  weekday     Int      // 0-6
  startMin    Int      // minutes from midnight, local
  endMin      Int
  capacity    Int      // default order capacity if no resources
  cutoffMin   Int?     // cutoff before slot (minutes)
  leadTimeMin Int      @default(0)
  @@index([shopDomain, locationId, method, weekday])
}

model SlotOverride {
  id          String   @id @default(cuid())
  shopDomain  String
  locationId  String
  location    Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
  date        DateTime // date-only, location-local
  method      Method
  closed      Boolean  @default(false)
  startMin    Int?
  endMin      Int?
  capacity    Int?
  @@index([shopDomain, locationId, date])
}

model BlackoutDate {
  id          String   @id @default(cuid())
  shopDomain  String
  locationId  String?  // null = all locations
  location    Location? @relation(fields: [locationId], references: [id], onDelete: Cascade)
  method      Method?  // null = all methods
  date        DateTime
  reason      String?
  source      String   @default("manual") // manual|holiday-import
  @@index([shopDomain, date])
}

model CapacityResource {
  id          String   @id @default(cuid())
  shopDomain  String
  locationId  String
  location    Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
  name        String   // "oven", "driver", "picker"
  perSlotMax  Int
  @@index([shopDomain, locationId])
}

model Zone {
  id          String   @id @default(cuid())
  shopDomain  String
  locationId  String
  location    Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
  type        String   // postal|radius|distance
  postalCodes String[]
  radiusKm    Float?
  minOrders   Int?     // delivery-density threshold
  @@index([shopDomain, locationId])
}

model ProductRule {
  id            String   @id @default(cuid())
  shopDomain    String
  scopeType     String   // product|collection|vendor|type|tag
  scopeValue    String
  allowedMethods Method[]
  leadTimeMin   Int?
  allowedLocationIds String[]
  blockSlotRule Json?    // cart-content constraints
  @@index([shopDomain, scopeType])
}

model Rate {
  id          String   @id @default(cuid())
  shopDomain  String
  method      Method
  zoneId      String?
  priceCents  Int
  keyedBy     String   // zone|distance|weight|value
  rules       Json
  @@index([shopDomain, method])
}

model SlotHold {
  id          String   @id @default(cuid())
  shopDomain  String
  cartToken   String
  locationId  String
  method      Method
  slotStart   DateTime
  resources   Json     // {resourceId: qty}
  expiresAt   DateTime
  @@index([shopDomain, slotStart])
  @@index([expiresAt])
}

model Booking {
  id            String   @id @default(cuid())
  shopDomain    String
  orderId       String   @unique
  orderName     String?
  locationId    String
  method        Method
  slotStart     DateTime
  slotEnd       DateTime
  status        String   @default("confirmed") // confirmed|rescheduled|cancelled|fulfilled|no_show
  resources     Json
  customerEmail String?
  customerPhone String?
  address       Json?
  reschedules   Reschedule[]
  createdAt     DateTime @default(now())
  @@index([shopDomain, slotStart])
  @@index([shopDomain, status])
}

model Reschedule {
  id          String   @id @default(cuid())
  bookingId   String
  booking     Booking  @relation(fields: [bookingId], references: [id], onDelete: Cascade)
  fromSlot    DateTime
  toSlot      DateTime
  reason      String?
  createdAt   DateTime @default(now())
}

model Waitlist {
  id          String   @id @default(cuid())
  shopDomain  String
  locationId  String
  method      Method
  slotStart   DateTime
  customerEmail String
  status      String   @default("waiting") // waiting|promoted|expired
  createdAt   DateTime @default(now())
  @@index([shopDomain, slotStart])
}

model Notification {
  id          String   @id @default(cuid())
  shopDomain  String
  bookingId   String?
  channel     String   // email|sms
  type        String   // reminder|eta|reschedule|waitlist
  sendAt      DateTime
  status      String   @default("pending")
  payload     Json
  @@index([shopDomain, sendAt, status])
}

(Deposit and RecurringBooking models are added in v2 — see strategy §3.3.)


5. Shopify configuration

5.1 Scopes (shopify.app.toml)

Request the minimum that covers the feature set (mirror DS but no more): read_products, read_customers, read_orders / write_orders (draft orders + order metafields), read_locations, write_metaobjects/read_metaobjects, write_cart_transforms, write_delivery_customizations, write_payment_customizations, plus the Functions' own access. Add read_markets/read_locales for i18n. Do not request write_customers or discounts unless a feature actually needs it — fewer scopes = faster review + more merchant trust.

5.2 Webhooks (mandatory + functional)

  • app/uninstalled → wipe shop data (or soft-delete + purge job)
  • orders/create → create Booking, consume capacity, write-back metafield, release hold
  • orders/updated / orders/cancelled → sync booking status, free capacity
  • customers/data_request, customers/redact, shop/redactGDPR, required for BfS

5.3 App Proxy

Expose /apps/scheduling/availability and /apps/scheduling/hold via app proxy so the storefront widget can call the backend from the theme without CORS pain.

5.4 Functions — behavior contracts

  • validation-slot (Cart & Checkout Validation): reject checkout if cart has schedulable items but no valid slot attribute, or if the slot's capacity snapshot shows it's full/blacked-out/past cutoff. This is the all-plan enforcement.
  • delivery-customization: rename/reorder/hide delivery options to match chosen method; surface zone rate.
  • payment-customization: hide payment methods incompatible with the method (e.g. no COD on shipping).
  • cart-transform: add a deposit line or delivery fee where applicable (v2 for deposits).

6. Phased build order

Each phase: branch phase-N-..., ship working software, meet acceptance criteria, then merge.

Phase 0 — Scaffold & CI

  • Init from shopify-app-template-remix; TypeScript strict; ESLint/Prettier.
  • Prisma + Postgres/SQLite; Redis + BullMQ skeleton; Vitest + Playwright configured.
  • shopify.app.toml with scopes, app proxy, GDPR + app/uninstalled webhooks wired (handlers can be stubs).
  • Dev store install works; embedded app loads with a Polaris shell.
  • Accept: shopify app dev runs, app installs, blank dashboard renders, CI runs lint+tests green.

Phase 1 — Core data model & admin CRUD

  • Implement Shop, Location, SlotTemplate, SlotOverride, BlackoutDate, Method in Prisma.
  • Polaris CRUD screens: create/edit locations (address + geocode via zones.server stub), weekly slot template editor, blackout calendar.
  • Vertical templates service: "Bakery/Florist/Grocer" one-click preset seeds locations+slots+copy.
  • Accept: merchant can fully configure a single location's weekly slots + blackouts in the admin; data persists; a template seeds a working config in one click. Unit tests for template seeding.

Phase 2 — Scheduling engine (the heart)

  • lib/time.ts: Luxon-based, tz/DST-safe helpers. 100% unit-tested (DST spring-forward/fall-back cases included).
  • services/scheduling.server.ts: pure function getAvailability(shop, method, location, dateRange, cart) → applies templates, overrides, blackouts, cutoffs, lead-times, product rules. No DB or Shopify calls inside the pure core (inject data).
  • services/capacity.server.ts: compute remaining capacity per slot from Bookings + Holds + resources.
  • Accept: given a fixture config, availability output is correct across timezones, DST boundaries, cutoffs, and lead-times. Full unit-test suite green. No UI yet.

Phase 3 — Storefront widget + cart attributes

  • Theme App Extension (App Embed + cart block), Preact/vanilla TS, no CLS, accessible, i18n-ready.
  • Calls app-proxy /availability; renders method → date → slot; hides unavailable; writes selection to cart attributes.
  • Method-specific, unambiguous copy (pickup vs delivery — fix DS's complaint).
  • Accept: on a dev store theme, shopper picks method/date/slot; unavailable dates hidden; selection saved to cart attributes and visible in cart. Lighthouse: no significant CLS, fast widget load.

Phase 4 — Enforcement (Functions) + slot-holds

  • holds.server.ts + Redis: create hold on slot select (TTL, e.g. 10 min); BullMQ job expires/releases holds.
  • validation-slot Function: block checkout when slot missing/invalid/full. Deploy and test on a non-Plus dev store to prove all-plan enforcement.
  • delivery-customization + payment-customization Functions.
  • orders/create webhook → Booking creation, capacity consumption, order metafield write-back, hold release.
  • Accept: on a non-Plus store, checkout is blocked without a valid slot; two concurrent carts cannot book the last unit of capacity (hold race test); completed order shows the slot on the order record. Function unit tests via function-runner.

Phase 5 — Multi-location, zones, rates, auto-assignment

  • Zone model + zones.server: geocode address, radius/driving-distance eligibility, delivery-density thresholds, nearest-location auto-assign, inventory-based exclusion (query product stock per location).
  • Rate model → surfaced through delivery-customization Function.
  • Google Maps display of pickup locations in the widget.
  • Accept: delivery eligibility correctly gates by zone/distance; order auto-assigns to nearest valid, in-stock location; rates vary by zone/method at checkout.

Phase 6 — Ops/dispatch dashboard

  • app.dashboard.tsx: bookings by day/slot/location, capacity-utilization heatmap, revenue by method, upcoming-fulfillment list; filters + CSV export.
  • Booking status transitions (confirmed→fulfilled/no_show).
  • Accept: merchant sees an accurate, filterable schedule of all bookings and can export a day's dispatch list.

Phase 7 — POS + Checkout UI extensions

  • POS UI Extension: staff run the same availability/hold/booking flow against the same pool.
  • Checkout UI Extension: native picker on Plus; Thank-you + Order-status confirmation blocks on all plans.
  • Accept: a POS order consumes the same capacity as online (no double-book); Plus checkout shows native picker; all plans show confirmed slot on thank-you/order-status.

Phase 8 — Billing & Built-for-Shopify hardening

  • Billing API: Free/Starter/Growth/Pro tiers (strategy §6) with feature gating.
  • Performance budget, accessibility pass, empty/loading/error states, onboarding checklist, help docs.
  • Verify all GDPR webhooks; data purge on uninstall.
  • Accept: tier upgrade/downgrade works and gates features; app passes an internal BfS checklist review; GDPR flows verified.

Phase 9 — Fast-follow (v1.x) — separate milestones

Waitlists, self-service reschedule portal (order-status + magic link), email→SMS reminders (BullMQ + Notification model), printable run sheets/picking lists, holiday auto-import, headless/Storefront-API support.

Phase 10 — v2

Predictive prep-time (AI on order history), deposits (Cart Transform + draft orders), recurring/subscription slots, merchant REST/GraphQL API + webhooks, group/multi-drop catering orders.


7. CLAUDE.md (place at repo root)

Create this so future Claude Code sessions have standing context:

# Delivery Date & Time — Shopify App

Scheduling app for Shipping / Local Delivery / Store Pickup with date-time slots,
capacity intelligence, and all-plan checkout enforcement. See PRODUCT_STRATEGY.md
(why) and IMPLEMENTATION_PLAN.md (how). Build in the phases defined there.

## Stack
Remix + Polaris + App Bridge · Prisma/Postgres · Redis/BullMQ · GraphQL Admin API ·
Theme App Extension (Preact) · Checkout UI Extensions (React) · Shopify Functions (Rust) ·
POS UI Extension · TypeScript strict · Vitest + Playwright.

## Non-negotiables
- Enforcement is server-side (Validation Function), never client-only.
- All date math is timezone/DST-safe (Luxon) and unit-tested before UI.
- One Scheduling Service + one capacity pool feeds every surface (web/checkout/POS).
- Multi-tenant: every query scoped by shopDomain.
- Minimal OAuth scopes. Secrets in .env, never committed.
- Each phase ships tested, working software; don't skip acceptance criteria.

## Commands
- `shopify app dev` — local dev against a dev store
- `npm test` — unit tests · `npm run test:e2e` — Playwright
- `npx prisma migrate dev` — DB migrations
- `shopify app deploy` — deploy extensions/functions

## Conventions
Conventional commits · feature branch per phase · Polaris components only in admin ·
no inline secrets · pure functions in services/*.server.ts core (inject data, no I/O in the math).

8. Testing & acceptance strategy

  • Unit (Vitest): time/DST helpers, availability engine, capacity math, zone eligibility, template seeding, rate resolution. These are pure — test exhaustively with fixtures.
  • Function tests: function-runner with recorded input JSON for validation/delivery/payment functions, including the "no slot," "slot now full," and "past cutoff" rejection cases.
  • Integration: webhook → booking creation → metafield write-back; hold create → expire → release.
  • E2E (Playwright): admin CRUD flows; storefront pick-a-slot happy path on a dev theme.
  • Concurrency test (critical): two simultaneous checkouts for the last capacity unit — exactly one succeeds. This is the slot-hold moat; it must have a dedicated test.
  • Manual matrix: verify enforcement on a non-Plus dev store (the whole point) and native checkout on a Plus dev store.

9. Risks & watch-items

  • Checkout extensibility limits (non-Plus): the picker cannot live in the core checkout steps on non-Plus — that is expected. Enforcement comes from the Validation Function + cart-attribute selection in the theme, not from a checkout-step UI. Do not architect around putting the picker inside non-Plus checkout.
  • Aug 26 2026 script deprecation: non-Plus stores are fully on the extensibility model; there is no checkout.liquid / Additional Scripts fallback. Build to extensions + Functions from day one (we already do).
  • Function latency budget: Validation Functions run on the hot checkout path — keep them fast and side-effect-free; the capacity snapshot they read must be cheap (denormalize into a metafield the Function can read directly).
  • Geocoding cost/quota: cache geocode results per address; don't call the maps API on every availability request.
  • Timezone bugs are the #1 source of scheduling failures — treat lib/time.ts as safety-critical.
  • BfS review: minimal scopes, GDPR webhooks, performance, and accessibility are gating. Bake them in, don't retrofit.