commit 952d274bea140d0ca2c126b70ddbcddf460f5970 Author: metatroncubeswdev Date: Sun Aug 23 15:33:49 2026 -0400 docs: add product strategy and implementation plan Initial planning docs for the delivery date/time scheduling app. Co-Authored-By: Claude Sonnet 5 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..feb3b1c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,40 @@ +# 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, in order. + +## 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 (Cart/Checkout **Validation Function**), never client-only. + The storefront widget collects; the Function enforces. This is what makes the app work + on non-Plus plans, which is the core competitive moat. +- All date math is timezone/DST-safe (Luxon) and unit-tested BEFORE any UI is wired. +- One Scheduling Service + one capacity pool feeds every surface (web / checkout / POS). + Behavior must never diverge between channels. +- Multi-tenant: every DB query scoped by `shopDomain`. +- Request the MINIMUM OAuth scopes needed. Secrets live in `.env`, never committed. +- Each phase ships tested, working software; do not skip a phase's acceptance criteria. +- Slot-holds (Redis, TTL) prevent last-slot double-booking — this has a dedicated + concurrency test that must pass. + +## Commands +- `shopify app dev` — local dev against a dev store +- `npm test` — Vitest unit tests +- `npm run test:e2e` — Playwright E2E +- `npx prisma migrate dev` — DB migrations +- `shopify app deploy` — deploy extensions / functions + +## Conventions +- Conventional commits; one feature branch per phase; small PRs per task. +- Polaris components only in the admin UI (Built-for-Shopify requirement). +- Pure functions in `app/services/*.server.ts` scheduling core — inject data, do no I/O + inside the math so it stays unit-testable. +- No inline secrets or API keys anywhere in source. + +## Current status +Greenfield. Start at Phase 0 (Scaffold & CI) in `IMPLEMENTATION_PLAN.md`. diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..27f6b86 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,479 @@ +# 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). + +```prisma +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/redact` → **GDPR, 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: + +```markdown +# 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. diff --git a/PRODUCT_STRATEGY.md b/PRODUCT_STRATEGY.md new file mode 100644 index 0000000..418c31a --- /dev/null +++ b/PRODUCT_STRATEGY.md @@ -0,0 +1,168 @@ +# Delivery Date & Time — Product Strategy & Differentiation Plan + +**Project:** Shopify delivery / pickup date-&-time scheduling app +**Owner:** Clicks to Cart (C2C Group), Kitchener, Ontario +**Benchmark:** *DS Pickup Delivery Date & Time* by Devesha Solution (5.0★, 64+ reviews, Built for Shopify, launched Jan 2025) +**Goal for v1:** Full feature parity with DS **plus** a set of differentiators that make us the obvious upgrade, targeting **all Shopify plans** (not Plus-only). +**Document version:** 1.0 · August 2026 + +--- + +## 1. Strategic thesis + +DS is a well-executed, well-supported, cheap app that wins on *ease of setup* and *hands-on human support*. Its ceiling is that it is a **static rules engine**: every lead time, capacity limit, and blackout date is typed in by hand, it has no awareness of real fulfillment load, and — by its own admission — full checkout-level enforcement only exists on Shopify Plus. + +We win by being the **operationally intelligent** scheduler: + +1. **Enforce on every plan, not just Plus.** We use Shopify **Cart & Checkout Validation Functions** (server-side, available on all plans) so an order physically cannot complete without a valid, still-available slot — closing the gap DS leaves open for the ~95% of merchants who aren't on Plus. +2. **Schedule against real capacity, not a number in a box.** Capacity is modeled as *resources* (ovens, drivers, pickers, delivery routes), with soft slot-holds during checkout to kill double-booking races, waitlists when full, and predictive prep-times learned from the store's own fulfillment history. +3. **Own the whole fulfillment lifecycle**, not just slot selection: customer self-service reschedule, automated reminders that cut no-shows, deposits for future-dated made-to-order items, recurring delivery subscriptions, and a real dispatch/ops dashboard with printable run sheets. +4. **Match DS on the things reviewers love** — dead-simple setup (vertical templates that pre-configure a bakery/florist/grocer in minutes), fast Built-for-Shopify-grade performance, and responsive support — so we never lose on their home turf. + +Positioning line: **"The only delivery scheduler that enforces on every plan and schedules against your real capacity."** + +--- + +## 2. Feature parity baseline (must-have to match DS) + +Everything DS does, we do. This is table stakes, not differentiation. + +| Area | Parity requirement | +|---|---| +| Fulfillment methods | Shipping, Local Delivery, Store Pickup as independent paths, each with own rules/rates/calendar | +| Date & time picker | Calendar date → available time slots; unavailable dates/slots **hidden**, not just rejected | +| Cut-off times | Per day/method threshold after which that day's slots close | +| Preparation time | Configurable lead-time buffer, settable per product | +| Date ranges | Range selection where the fulfillment model calls for it | +| Blackout dates | Block specific dates per location/method; disappear from the picker | +| Weekday/date enable-disable | Independent per location and per method | +| Order/slot limits | Max orders per slot/day per location to prevent overbooking | +| Multi-location | Each location: own address, hours, calendar, blackout dates | +| Map display | Pickup locations on Google Maps | +| Radius / driving-distance eligibility | Determine if an address qualifies for local delivery | +| Auto location assignment | Route order to nearest eligible location | +| Inventory-based location | Exclude a location that doesn't stock the ordered product | +| Product-scoped rules | Availability & lead time by product / collection / vendor / type | +| Cart-content rules | Block a slot based on what's actually in the cart | +| Product-condition-by-location | Restrict a product to certain locations | +| Zone/distance rates | Rates vary by location, postal/ZIP, distance, method | +| Placements | Cart page, cart drawer, product page, checkout (Plus), thank-you, order-status, POS | +| Accounts & i18n | Recognize returning customers; customizable, multi-language widget text | +| Write-back to order | Chosen date/time/location visible on the order record | + +--- + +## 3. Differentiators (how we beat DS) + +Grouped by theme. Each names the **DS gap** it closes and a rough **build tier** (v1 = launch, v1.x = fast-follow, v2 = later). + +### 3.1 Enforcement & correctness (the technical moat) + +- **All-plan checkout enforcement (v1).** Cart/Checkout Validation Function blocks completion server-side if the slot is missing or has since been taken — works on Basic/Shopify/Advanced, not just Plus. *DS gap: enforcement is Plus-only.* +- **Soft slot-holds with TTL (v1).** When a shopper reaches checkout, we reserve their slot's capacity for N minutes so two shoppers can't grab the last slot. Expired holds auto-release. *DS gap: "hidden unavailable" doesn't handle the race; last-slot double-booking is possible.* +- **Timezone- & DST-safe slots (v1).** All slots computed in the store/location local timezone, DST-correct, with explicit customer-facing timezone labels. *DS gap: a class of bugs DS reviewers hint at with "mixed date messaging."* +- **Unambiguous pickup-vs-delivery copy (v1).** Method-specific labels and confirmation strings, never a stray "estimated delivery date" on a pickup order. *DS gap: a named review complaint.* + +### 3.2 Capacity intelligence + +- **Resource-based capacity (v1).** Capacity modeled as named resources — oven batches, delivery drivers, pickers, prep stations — not just an order count. One slot can consume different resource amounts depending on cart contents. *DS gap: single flat "max orders per slot."* +- **Waitlist & backorder for full slots (v1.x).** Customer can join a waitlist; auto-promoted (and notified) if capacity frees up. +- **Delivery-density / minimum-order routes (v1.x).** Only offer a delivery day to a postal code once a route threshold is met, or steer sparse areas to specific days. Cuts delivery cost. *DS gap: none — radius eligibility only.* +- **Predictive prep-time & demand (v2, AI).** Learn actual prep/fulfillment durations from the store's order history and suggest lead-times and capacity limits, instead of pure manual config. *DS gap: explicitly cannot derive lead times from data.* + +### 3.3 Fulfillment lifecycle (beyond the slot pick) + +- **Customer self-service reschedule (v1.x).** Magic-link / order-status portal to change slot post-purchase, with live capacity re-check and hold. *DS gap: not offered.* +- **Automated reminders & ETA notifications (v1.x).** Email/SMS "your pickup is tomorrow," "out for delivery, ETA 2–4 PM." Cuts no-shows for the exact verticals DS serves. *DS gap: none.* +- **Deposits / partial payment for future-dated orders (v2).** Take a deposit now, balance on fulfillment — critical for cakes, furniture, made-to-order. *DS gap: none.* +- **Recurring / subscription slots (v2).** Weekly grocery or repeat delivery on a standing slot. *DS gap: one-off only.* +- **Group / multi-drop catering orders (v2).** One order, multiple delivery points or large batch capacity consumption. + +### 3.4 Merchant operations + +- **Dispatch & ops dashboard (v1).** Bookings by slot/day/location, capacity-utilization heatmap, revenue by method, no-show/reschedule tracking. *DS gap: basic calendar view only.* +- **Printable run sheets & picking lists (v1.x).** Per-driver route manifests and per-slot picking lists, exportable/printable. *DS gap: none.* +- **Vertical setup templates (v1).** One-click "Bakery / Florist / Grocer / Furniture / Caterer" presets that pre-configure methods, slots, prep-times, and copy. Productizes the hand-holding DS does manually — beats them on their *own* strength (ease of setup). +- **Auto-imported public holidays (v1.x).** Pull region/country holiday calendars so merchants don't hand-enter blackout dates. *DS gap: all blackout dates are manual.* + +### 3.5 Platform & integration + +- **Headless / Storefront API support (v1.x).** Works on Hydrogen / custom storefronts, not just theme widgets. +- **Merchant-facing REST/GraphQL API + webhooks (v2).** Push booking data to a WMS/ERP (e.g. Odoo — relevant to C2C's stack) so slots flow into back-office systems. +- **POS UI extension (v1).** Staff run the identical scheduling flow against the same capacity pool for phone/in-person orders. (Parity item, but we make the POS UX first-class.) + +--- + +## 4. Additional pain points we solve (that neither app markets) + +1. **The last-slot race condition** — solved by slot-holds (§3.1). A genuinely hard technical problem most apps ignore. +2. **No-shows and missed pickups** — solved by reminders + reschedule (§3.3). This is the #1 operational loss for bakeries/florists. +3. **Delivery cost bleed on sparse routes** — solved by delivery-density thresholds (§3.2). +4. **Setup fatigue** — solved by vertical templates + holiday auto-import (§3.4). Time-to-value in minutes. +5. **Made-to-order cash flow** — solved by deposits (§3.3). Merchants front material cost weeks before fulfillment. +6. **Back-office double entry** — solved by the merchant API/webhooks (§3.5). Bookings land in the ERP automatically. +7. **Festival/cultural demand spikes** (C2C's own use case) — solved by capacity intelligence + waitlists + pre-set surge blackout templates, so a Diwali/wedding-season spike doesn't overpromise. + +--- + +## 5. Data model (generic + Shopify mapping) + +Restated platform-agnostically, then mapped to how it lives on Shopify. **App-owned relational data lives in our Postgres via Prisma; a thin, read-optimized slice is mirrored to Shopify metafields/metaobjects so it's visible on the order and reusable by Functions and POS.** + +| Entity | Purpose | Shopify home | +|---|---|---| +| **Shop** | Installed store, plan, settings, billing tier | App DB | +| **FulfillmentMethod** | Shipping / Local Delivery / Pickup; top-level configurable type | App DB | +| **Location** | Dispatch/pickup point: address, coords, hours, blackout calendar | App DB (+ maps to Shopify Location where relevant) | +| **Zone** | Where a location's delivery reaches — ZIP list / radius / driving distance | App DB | +| **SlotTemplate** | Recurring weekly slot: weekday, start/end, capacity, per location per method | App DB | +| **SlotOverride** | Date-specific exception: extra hours, reduced capacity, closed | App DB | +| **BlackoutDate** | Blocked date scoped to one/several/all locations/methods | App DB | +| **CapacityResource** *(new)* | Named resource (oven, driver, picker) with per-slot limits a booking consumes | App DB | +| **ProductRule** | Product/collection/vendor/tag override of lead time, methods, locations | App DB (product refs via metafield) | +| **Rate** | Price table for a method keyed by zone/distance/weight/value | App DB → Delivery Customization Function | +| **SlotHold** *(new)* | Temporary capacity reservation during checkout, with TTL | App DB (Redis-backed) | +| **Booking** | Confirmed reservation: order ref, slot, location, method, date/time, resources consumed | App DB + **written back to order via metafield/attributes** | +| **Waitlist** *(new)* | Customer waiting on a full slot; promotion state | App DB | +| **Reschedule** *(new)* | Change history of a booking's slot | App DB | +| **Notification** *(new)* | Reminder/ETA message queue and delivery log | App DB | +| **Deposit** *(new, v2)* | Partial-payment record linked to a future-dated booking | App DB + Shopify draft/order | +| **RecurringBooking** *(new, v2)* | Standing slot subscription generating child Bookings | App DB | + +**Why app-DB-primary, not metaobject-primary (unlike DS):** capacity math, holds, waitlists, and analytics need transactional integrity and fast aggregate queries that metaobjects can't give. We treat Shopify as the *system of record for the order* and our DB as the *system of record for scheduling*, syncing the confirmed slot back onto the order so staff and POS see it natively. + +--- + +## 6. Pricing strategy + +DS prices oddly — the **same features cost more on a higher Shopify plan** ($6.99 → $12.99), which punishes growth and leaves value on the table. We price on **value delivered**, not the merchant's Shopify tier. + +| Tier | Price (target) | Who | Includes | +|---|---|---|---| +| **Free** | $0 | Solo / single-location | 1 location, 3 methods, date/time picker, blackout dates, product rules, widget customization, i18n, **all-plan checkout enforcement**, 1 vertical template | +| **Starter** | ~$9.99/mo | Growing single/dual location | Everything Free + cut-off/prep-time, order limits, resource capacity (basic), dispatch dashboard, holiday auto-import, up to 3 locations | +| **Growth** | ~$19.99/mo | Multi-location operators | Unlimited locations, zones/rates, distance auto-assignment, waitlists, reschedule portal, reminders (email), run sheets, delivery-density routes | +| **Pro** | ~$39.99/mo | High-volume / ops-heavy | SMS reminders, predictive prep-time (AI), deposits, recurring slots, merchant API/webhooks, headless support, priority support | + +Notes: undercut DS at the entry point (a genuinely capable Free tier wins installs and reviews), then monetize the operational intelligence they can't match. Keep a 7–14 day trial on paid tiers. Consider usage-based SMS as a metered add-on. + +--- + +## 7. Go-to-market & "Built for Shopify" posture + +- **Win DS's strengths first.** Reviewers pick DS for *setup ease* and *support*. Vertical templates + in-app guided onboarding + fast, human support are non-negotiable for parity. +- **Built for Shopify certification** is the credibility bar in this category (DS has it). Design to the BfS checklist from day one: App Bridge, Polaris, performance budgets, no layout shift on the storefront widget, accessibility, embedded-app best practices, mandatory GDPR webhooks. +- **Lead the listing with the two moats DS can't copy quickly:** "enforced on every plan" and "schedules against real capacity." Everything else is parity. +- **Target verticals in order:** bakeries → florists → grocers/fresh food → caterers → made-to-order furniture. These are DS's own review base; we out-feature them where each vertical hurts most (capacity for bakers, routes for grocers, deposits for furniture, multi-drop for caterers). + +--- + +## 8. Phased roadmap (summary) + +| Phase | Theme | Ships | +|---|---|---| +| **v1 (launch)** | Parity + core moats | All §2 parity, all-plan enforcement, slot-holds, resource capacity, timezone-safe picker, dispatch dashboard, vertical templates, POS | +| **v1.x (fast-follow)** | Lifecycle | Waitlists, reschedule portal, reminders (email→SMS), run sheets, holiday auto-import, delivery-density routes, headless | +| **v2 (expansion)** | Intelligence & monetization | Predictive prep-time (AI), deposits, recurring slots, merchant API/webhooks, group/catering orders | + +The engineering breakdown of v1 into concrete, buildable tasks for Claude Code is in **`IMPLEMENTATION_PLAN.md`**.