- segmented method control with line icons - round calendar days with availability dots and scarcity badges - pill time chips; confirmation stub shows method, place, date, time, fee - colours derive from theme text colour + one merchant accent (light/dark safe) - rename leftover ss- internals to dw- Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Datewise: delivery & pickup scheduling for Shopify
A Shopify app (built for public App Store listing) that lets shoppers choose local delivery, store pickup or shipping, then a location, date and time slot. The merchant controls hours, preparation time, cutoffs, holidays, capacity, product rules, delivery zones and fees.
It's modelled on the scope of DS Delivery Date-Time (see the spec in docs/), and aims at the pain points merchants actually lose money on:
| Pain point | What Datewise does |
|---|---|
| Overbooking. Two shoppers take the last slot, or the cart shows a slot that is gone by checkout. | Real capacity holds: picking a slot reserves it for N minutes (row-locked in Postgres). Late orders are re-reserved or sent to a review queue, never silently overbooked. |
| Festival rush (Diwali, Mother's Day, Valentine's) | Rush control: separate per-day/per-slot limits, earlier cutoff ("closes 2 days before at 6 PM"), extra prep, peak fee, and a waitlist that offers freed places automatically. |
| Perishables promised on dates they can't be fresh | Fresh-stock windows: tie bookable dates to stock arrival dates + shelf life; the calendar tells customers when the next fresh batch is ready. |
| "Can I change my delivery time?" emails | Customer self-reschedule on the order status page, within the merchant's policy (up to X hours before, max N changes), with the same rule checks as checkout. |
| No-shows and missed deliveries | Reminders by email, SMS (Twilio) or WhatsApp (Meta Cloud API), re-planned automatically on every change. |
| Driver chaos | Daily run sheet: stops grouped by promised window, ordered nearest-next, Google Maps route links, CSV. |
| Buy now / Shop Pay skipping the cart widget | Checkout guard, a Cart & Checkout Validation Function (works on every plan, including Basic): blocks checkout if a required time is missing or past, or if the fee was tampered with. |
| "Why can't the customer pick Friday?" | Diagnostics screen that runs the exact checkout engine and names the rule, closure, cutoff or capacity limit responsible. |
Architecture
flowchart LR
subgraph Storefront
W[Theme app block + embed<br/>datewise.js] -->|/apps/datewise/* app proxy, HMAC-signed| P
G[Checkout guard<br/>validation function]
O[Order status block<br/>customer account extension] -->|session token| C
end
subgraph App server [React Router app]
P[proxy.* routes] --> S[storefront.server]
C[api.customer.booking] --> B[bookings.server]
A[Admin UI routes<br/>Polaris web components] --> B
H[webhooks.orders.*] --> OR[orders.server]
S & B & OR --> E[[engine/ <br/>pure TS, tested]]
S & B & OR --> R[(reservations.server<br/>row-locked counters)]
end
R --> DB[(PostgreSQL)]
K[worker.ts<br/>sweep holds · reminders · reconcile] --> DB
K --> N[Resend · Twilio · WhatsApp]
app/engine/is the scheduling brain: hours, closures, cutoffs, prep (clock or opening-hours), DST-safe time zones, product rules, freshness, peak days, capacity, zones and fees, location ranking, reminders, run sheets. It's pure TypeScript with no I/O, and the clickable prototype runs this same code in the browser.app/services/reservations.server.tsholds the capacity contract. Every location/method/date/slot has a counter row. Mutations lock the rows they touch in sorted order (SELECT … FOR UPDATE), so there are no races and no deadlocks.- Selection → order: the widget writes a
_datewisecart attribute (token, method, location, date, slot, quoted fee, rule version), plus readable attributes for staff ("Local delivery date: Sat, Nov 7"). Theorders/createwebhook converts the hold into a booking, tags the order, writes adatewise.bookingmetafield and plans reminders. - Fees on every plan: carrier-calculated rates aren't available on Basic, so fees are added as a hidden Delivery & handling product line (one variant per amount, created on demand). The checkout guard and the webhook both check that the line matches the quote.
Repository layout
app/engine/ scheduling engine (+ __tests__, 43 tests)
app/services/ reservations, storefront API, orders, bookings, reminders, waitlist, fees, catalog, maps, messaging
app/routes/app.* merchant admin (Today, Bookings, Locations & hours, Product rules, Fresh stock, Rush control, Run sheet, Diagnostics, Settings)
app/routes/proxy.* storefront API behind the Shopify app proxy
app/routes/api.customer.* customer-account API (self-reschedule)
app/routes/webhooks.* orders/create, cancelled, fulfilled, app/uninstalled, GDPR
app/jobs/worker.ts background worker
extensions/datewise-widget theme app extension (cart block + app embed for drawers), en/fr
extensions/checkout-guard validation function (JS) + tests
extensions/order-reschedule customer account UI extension (Preact)
prisma/ schema + SQL migration
prototype/ the clickable demo (runs the real engine)
tests/e2e/ browser test: real services + Postgres, mock theme + Admin API
Getting started (developer)
Prerequisites: Node 20+, PostgreSQL 14+, a Shopify Partner account, the Shopify CLI, and a development store.
npm install
cp .env.example .env # set DATABASE_URL and APP_ENCRYPTION_KEY
npx prisma generate
npx prisma migrate deploy
npm run config:link # links shopify.app.toml to your Partner app
npm run dev # shopify app dev: tunnel, install on dev store, live extensions
npm run worker # in a second terminal
Then, in the dev store:
- Locations & hours. Locations are imported switched off; review hours and turn one on.
- Theme editor. Add the Delivery date & time block to the cart page, and switch on the Datewise app embed (needed for cart drawers, waitlist links and the checkout-button guard).
- Settings → Checkout guard → Turn on.
- Set
APP_URLinextensions/order-reschedule/src/config.js(customer self-reschedule needs new customer accounts).
Deploying
- Web:
npm run build && npm run start(or the Dockerfile). Worker: the same image withnpm run worker. Run at least one worker, or holds only expire lazily and reminders won't send. - Any Node host with Postgres works (Fly.io, Render, Railway, a VPS). Shared PHP-style hosting won't run it.
npm run deploypublishes the extensions and app config.
Permissions (each is used by a shipped feature)
| Scope | Why |
|---|---|
read_products, write_products, write_publications |
rules by collection/tag/type; create and publish the hidden fee product |
read_orders, write_orders |
order webhooks, reconciliation, tag orders and write the booking metafield |
read_customers |
verify that the customer rescheduling actually owns the order |
read_locations, read_inventory |
link Shopify locations; stock-aware location choice |
write_validations |
switch the checkout guard on and off |
Customer names, emails, phones and addresses are protected customer data. Request Level 2 access in the Partner Dashboard before review.
What is enforced where (Basic plan and up)
| Rule | Cart widget | Checkout guard | Order webhook |
|---|---|---|---|
| Closed days, cutoffs, prep, freshness, product rules | ✅ hides/blocks | — | ✅ re-checked on reschedule |
| Time required | ✅ blocks button | ✅ blocks checkout | ✅ review queue |
| Date in the past | ✅ | ✅ | — |
| Capacity | ✅ hold | ❌ functions have no network access | ✅ re-reserve or review |
| Fee matches quote | ✅ | ✅ | ✅ flags mismatch |
Checkout-page pickers (Plus-only UI extension targets) and carrier-calculated rates are intentionally not in v1.
Tests
npm test # engine + checkout guard (DB tests skip without TEST_DATABASE_URL)
TEST_DATABASE_URL=postgresql://… npm test # + Postgres: concurrency, idempotency, reschedule, webhooks
npm run e2e:server & npm run e2e # real browser drives the widget against the real services
Covered: cutoff boundaries (13:59 vs 14:00), overnight/weekend operating-hours prep, both DST transitions, closures vs special openings, method-specific exceptions, mixed carts and conflicting rules, freshness windows, peak cutoffs/capacity/waitlist, 25 concurrent shoppers for 3 places, duplicate and concurrent webhooks, expired holds at order time, atomic reschedules (including opposite-direction swaps), cancellation, stale-hold sweeps, postal patterns and ranges, radius zones, maps outages (never accept unverified addresses), split inventory, reminders, run-sheet ordering, CSV escaping, fee tampering, keyboard navigation.
Not yet verified in a live store
This code builds, typechecks and passes its tests, but it hasn't been run inside a real Shopify store yet. Before launch, run the spec's acceptance list (docs/) on a development store, especially:
- theme compatibility of the drawer injection (selectors for Dawn and its family are built in; others may need the Cart drawer selector setting),
- the customer-account extension and checkout guard on real checkouts (Shop Pay, Apple Pay, Buy now),
- WhatsApp template approval and Twilio sender setup,
- App Store requirements: privacy policy, listing, screencast, Built for Shopify performance checks.