# 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
```mermaid
flowchart LR
subgraph Storefront
W[Theme app block + embed
datewise.js] -->|/apps/datewise/* app proxy, HMAC-signed| P
G[Checkout guard
validation function]
O[Order status block
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
Polaris web components] --> B
H[webhooks.orders.*] --> OR[orders.server]
S & B & OR --> E[[engine/
pure TS, tested]]
S & B & OR --> R[(reservations.server
row-locked counters)]
end
R --> DB[(PostgreSQL)]
K[worker.ts
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.ts`** holds 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 `_datewise` cart attribute (token, method, location, date, slot, quoted fee, rule version), plus readable attributes for staff ("Local delivery date: Sat, Nov 7"). The `orders/create` webhook converts the hold into a booking, tags the order, writes a `datewise.booking` metafield 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.
```bash
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:
1. **Locations & hours.** Locations are imported switched off; review hours and turn one on.
2. **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).
3. **Settings → Checkout guard → Turn on.**
4. Set `APP_URL` in `extensions/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 with `npm 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 deploy` publishes 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
```bash
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.