- delivery-label renames only options matching the chosen method (en/fr dates) - payment-rules hides COD / pay-in-store for shipped orders - functions matched by handle (fixes guard lookup matching any 'datewise' title) - scopes: write_delivery_customizations, write_payment_customizations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
9.7 KiB
Markdown
134 lines
9.7 KiB
Markdown
# 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<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.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
|
|
|
|
* 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 |
|
|
| `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.
|