Audited the implementation against DS_Delivery_Date_Time_App_Study.docx and closed the actionable gaps (see IMPLEMENTATION_REVIEW_2026-09-04.md). Core (code + unit tests, 156 green): - Wire excludeLocationsWithoutStock into resolveAvailabilityRequest; widget now sends variantIds so inventory-based location exclusion actually runs. - Live slot re-validation at checkout: new checkout-snapshot.server.ts writes a shop-metafield capacity snapshot; validation-slot's evaluateCheckout rejects a complete selection that has since filled / blacked out / closed / hit the daily cap / left the schedule. Refreshed on order webhooks and slot/blackout/location/enforcement edits. - Scopable checkout enforcement: Shop.enforcementMode (all|tagged|off) + enforcementTag, new app.settings.tsx admin page, honoured via the snapshot. - Per-day order cap: Location.dailyOrderCap threaded through getAvailability (dailyCap + consumedPerDate); admin field on the location screen. - Product-rule slot blocking: ProductRule.blockedStartMins, unioned in resolveProductRuleConstraints, enforced in the engine and resolveHoldRequest; admin field on the product rules screen. - Product-page placement: product-availability.liquid block + widget data-mode="preview" (read-only earliest-date line). - Second locale: datetime-widget fr.json / fr.schema.json. - Migration 20260904120000_review_gaps (apply with prisma migrate deploy). New Functions (source + unit tests; need `shopify app deploy` to ship): - extensions/payment-customization: cart.payment-methods.transform.run — hides cash-on-delivery / pay-in-store gateways on SHIPPING orders. - extensions/checkout-datetime/src: restored from a gitignored dist-only state — Plus native picker + Thank you / Order status confirmation blocks, all calling the existing checkout.scheduling.* routes (one capacity pool). tsconfig ships checkJs:false pending reconciliation with live checkout types. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
Implementation Review — 2026-09-04
Scope: Coverage audit of the current implementation against
DS_Delivery_Date_Time_App_Study.docx (the "DS Pickup Delivery Date & Time"
teardown by C2C).
Reviewed: app/ (routes, services, lib), extensions/, prisma/schema.prisma,
widget-src/, shopify.app.toml, tests/.
Verdict: The scheduling core, concurrency safety, multi-surface consistency, zones/auto-assignment, and all-plan checkout enforcement + confirmation — the hard parts and the competitive moat — are solid and tested. The meaningful gaps are inventory-based location exclusion (dead code), payment customization (absent), rate application vs. display, product-page placement, and live slot re-validation at checkout. Several gaps are consciously documented deferrals; the inventory one looks like an actual wiring bug.
Fully covered
| Study requirement | Where |
|---|---|
| 3 independent fulfillment methods (Shipping / Local Delivery / Pickup) with own rules, calendars | Method enum; per-method SlotTemplate / SlotOverride / BlackoutDate |
| Date→time slot picker; unavailable dates/slots hidden, not rejected | app/services/scheduling.server.ts:89 filters past-cutoff / under-lead / blacked-out / full |
| Cut-off times + preparation/lead time (incl. per-product) | cutoffMin, leadTimeMin on template; ProductRule.leadTimeMin folded in at app/services/availability-request.server.ts:210 |
| Blackout dates; per-weekday / per-method / per-location enable-disable | BlackoutDate, SlotTemplate per weekday, SlotOverride.closed |
| Per-slot capacity limit | capacity + remainingCapacity() in app/services/capacity.server.ts |
| Multi-location; radius and driving-distance zones; postal zones; nearest-location auto-assign; delivery-density threshold | app/services/zones.server.ts |
| Product / collection / vendor / type / tag rules → allowed methods, lead time, allowed locations | app/services/product-rules.server.ts |
| Rate resolution by zone / distance per method | app/services/rates.server.ts |
| Cart + cart-drawer placement; cross-theme auto-inject before checkout button | widget-src/datetime-widget/datetime-widget.ts:570 |
| Thank-you / order-status confirmation on all plans | extensions/datetime-widget/blocks/order-confirmation.liquid |
| POS scheduling against the same capacity pool | app/routes/pos.scheduling.* reuse resolveAvailabilityRequest |
| Last-slot race prevention | atomic Lua hold in app/services/holds.server.ts:22 + tests/integration/holds.concurrency.test.ts |
| Server-side checkout enforcement on non-Plus | extensions/validation-slot Function |
Single availability service across surfaces; multi-tenant shopDomain scoping; DST-safe Luxon math |
✅ |
| Order write-back (metafield + note attributes) | app/routes/webhooks.orders.create.tsx |
| Ops dashboard: bookings by day/slot/location, utilization, revenue by method, CSV export | app/services/dashboard.server.ts |
| DS review complaint (pickup vs "estimated delivery date" mismatch) | explicitly fixed via method-specific labels + delivery-customization rename |
Partial / deviations from the study
-
Inventory-based location exclusion is coded but never called.
excludeLocationsWithoutStock(app/services/zones.server.ts:241) has zero callers —resolveAvailabilityRequestnever invokes it. Study §3.4 "product-inventory-based location choice" is effectively absent at runtime. Looks like a wiring bug, not a deliberate deferral. -
Rates are displayed, not applied.
delivery-customizationonly renames the delivery option to append a price label (extensions/delivery-customization/src/evaluate.js:23). Nothing adjusts the actual charged shipping price or feeds a Shopify rate. Study §3.6 / the DS core value prop ("location-based shipping/delivery/pickup rates") is only half there. -
Checkout re-validation checks presence only.
validation-slot(extensions/validation-slot/src/evaluate.js) verifies thedd_*attributes exist; it does not re-check that the slot is still open (capacity / blackout / cutoff). Study §5.2 explicitly names "or has since become invalid." Mitigation is the 10-minute hold TTL, which the code comments acknowledge. -
Validation Function can't be scoped by product. Every order on an activated shop is treated as requiring a slot — no ProductRule-scoped enforcement (Functions can't read the app DB). A store mixing schedulable and non-schedulable items can't use it selectively.
-
"Date range" selection reinterpreted. The schema/engine implement a SHIPPING arrival-range display (
transitMinDays/transitMaxDays), not a shopper-selectable multi-day range. Study §3.2 "support for picking a range rather than a single date." -
Product-rule "block a specific slot based on cart contents" (§3.5, "fragile item that can't go on the earliest slot") is only expressible as a lead-time push. The planned
blockSlotRule Json?field was dropped fromschema.prisma. -
Pickup map is single-location. The widget maps the one resolved location (
widget-src/datetime-widget/datetime-widget.ts:427); there's no multi-pin pickup-location chooser. Study §3.4 "pickup locations shown visually on Google Maps." -
Multi-language is structure-only. Locale keys exist but only
en.default.jsonships — no second translation. -
Per-day order cap not modeled — only per-slot
capacity. Study §3.3 "in a given slot or day."
Not implemented
- Payment customization (§5.2 — "hide cash-on-delivery for a shipped order").
write_payment_customizationsscope is requested but no such Function exists. - Product-page placement (§3.7). No product-page block — only cart / drawer / thank-you / order-status / POS.
- Checkout UI Extension source (
extensions/checkout-datetime/) — only a gitignoreddist/bundle; nosrc/orshopify.extension.tomltracked. The Plus native picker is not a maintained deliverable. (The all-plan Liquid confirmation block does cover thank-you / order-status.) - Returning-customer recognition (§3.8) —
read_customersdeliberately dropped; documented as deferred. - Cart Transform (deposits / fees) — scope requested, marked v2.
- Named capacity resources (oven / driver / picker pools) — capacity is a
single integer;
CapacityResourcefrom the plan was never added. - GDPR compliance webhooks are commented out in
shopify.app.tomlpending Shopify's Protected Customer Data approval (handlers exist). Blocks a Built-for-Shopify submission.
Implementation follow-up — 2026-09-04 (same day)
Actioned in this pass ("Core + new Functions" scope; Cart Transform / deposits, returning-customer recognition, and GDPR re-enable stay deferred).
Done — code + passing unit tests (npm test, 156 green)
- Inventory-based location exclusion is now wired.
resolveAvailabilityRequestcallsexcludeLocationsWithoutStockwhen the app-proxy surface supplies an Admin client + cart variant GIDs; the widget now sendsvariantIds. Out-of-stock locations are dropped from both the single-location and delivery-zone paths, with a dedicated "no location stocks every item" message. - Live slot re-validation at checkout. New
app/services/checkout-snapshot.server.tswrites a shop-metafield capacity snapshot; thevalidation-slotFunction (evaluateCheckout) now rejects a complete selection when the snapshot shows the slot is full, blacked out, closed, past the daily cap, or gone from the schedule. Snapshot is refreshed onorders/create|updated|cancelledand on slot / blackout / location / enforcement edits. - Scopable checkout enforcement.
Shop.enforcementMode(all/tagged/off) +enforcementTag, new Checkout enforcement admin page (app.settings.tsx), honoured by the Function via the snapshot'senforcementblock andschedulableProductIds. - Per-day order cap.
Location.dailyOrderCap, threaded throughgetAvailability(dailyCap+consumedPerDate) — the whole date drops once its bookings reach the cap, and per-slotremainingCapacityis clamped to the day's remaining budget. Admin field on the location edit screen. - Product-rule slot blocking (§3.5).
ProductRule.blockedStartMins, unioned inresolveProductRuleConstraints, applied ingetAvailabilityand re-enforced server-side inresolveHoldRequest. Admin field on the product rules screen. - Product-page placement. New
blocks/product-availability.liquid+ widgetdata-mode="preview"— a read-only "earliest available date per method" line that collects nothing. - Second locale.
extensions/datetime-widget/locales/fr.json+fr.schema.json(French), proving the i18n path end-to-end.
New migration: prisma/migrations/20260904120000_review_gaps (hand-written;
apply with prisma migrate deploy — the dev DB was unreachable here).
Done — source + unit tests, needs shopify app deploy / CLI to ship
- payment-customization Function — new
extensions/payment-customization/(cart.payment-methods.transform.run): hides cash-on-delivery / pay-in-store gateways onSHIPPINGorders. Pure logic unit-tested (tests/unit/payment-customization.test.ts). Runnpm --prefix extensions/payment-customization run typegenthenshopify app deploy; merchant activates it under Settings → Payments. - checkout-datetime Checkout UI Extension —
src/restored (was a gitignoreddist/only):Checkout.jsx(Plus native picker →checkout.scheduling.*routes, same capacity pool),ThankYou.jsx,OrderStatus.jsx.tsconfigshipscheckJs:falseuntil the<s-*>props are reconciled against the live@shopify/ui-extensionscheckout types (see its README) — the server side it calls is already tested.
Not done (unchanged from above)
- Cart Transform / deposits — v2.
- Returning-customer recognition — needs
read_customers+ Protected Customer Data approval. - GDPR compliance webhooks — still blocked on Shopify approval.
- Multi-pin pickup-location map picker — still single-location.
- Rates remain display-only (delivery-customization renames the option; it doesn't set the charged price). Left as a product decision.
- Worker periodic snapshot sweep — event-driven refresh covers the real cases;
the 30-day horizon sweep needs offline-session storage first (noted in
jobs/worker.ts).
Suggested priority order
- Wire
excludeLocationsWithoutStockintoresolveAvailabilityRequest(or delete it if intentionally cut). — bug - Decide rate strategy: real
delivery-customizationpricing / Shopify rate integration vs. accept display-only and document it. - Live slot re-validation at checkout (metafield capacity snapshot the Validation Function can read) — closes the §5.2 "since become invalid" gap.
- Product-page placement block.
- Payment customization Function (COD-on-shipping etc.).
- Product-scoped enforcement toggle for the Validation Function.
- Second locale + per-day cap + multi-pin pickup map as smaller follow-ups.
- Re-enable GDPR webhooks once Protected Customer Data access is granted (launch blocker for BfS).