App Store review 1.2.2: the reviewer's store got a test charge, but billing.check ran with isTest:false and ignored it, so the approved plan never registered. Checks now count test subscriptions too, and cancel uses each subscription's own test flag. Uninstall resets the stored plan to free (Shopify cancels the subscription), so a reinstall asks for approval again. 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/delivery-label delivery customization: chosen date/time on the checkout option (JS) + tests
extensions/payment-rules payment customization: hide pay-in-person for shipping (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
- VPS (Docker): see
deploy/README.md. Web:npm run build && npm run start(or the Dockerfile). Worker: the same image withnode build/worker.mjs. 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 |
write_delivery_customizations |
show the chosen date and time on the checkout delivery option |
write_payment_customizations |
hide pay-in-person methods for shipped orders |
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.