# 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/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. ```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 * **VPS (Docker): see `deploy/README.md`.** Web: `npm run build && npm run start` (or the Dockerfile). Worker: the same image with `node 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 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 | | `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 ```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.