Liquid's `| escape` filter encodes apostrophes/ampersands before the label
strings land in data-* attributes; depending on how the theme processes
translations that could survive into `dataset` still entity-encoded and then
render literally (e.g. "Couldn't load available dates") once assigned to
textContent. readConfig now decodes entities once via a detached <textarea>,
routed through a shared get(key, fallback) helper for every label + heading.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Many themes only expose "Add section" for the cart's checkout area, not
"Add block" next to the actual Checkout button — Shopify gives apps no
supported way to inject a block into an arbitrary spot in a theme's own
section markup, so a manually-placed app block can only ever land as a
disconnected standalone section on those themes.
Add a JS-based fallback via the app embed (already loaded site-wide,
independent of block placement): app-embed.liquid now carries the same
config settings as the block plus an "auto-place on cart page" toggle
(default on), emitted as an inert <template> with the widget's config as
data-* attributes. datetime-widget.ts's maybeAutoPlaceOnCart() finds the
theme's own Checkout button by CSS selector (a few common patterns, most
specific first) and inserts a live widget immediately before it — skipped
entirely if a block-placed widget already exists on the page, or no
Checkout button can be found by any known selector.
Also fix shopify.app.toml: automatically_update_urls_on_dev was left on
after the app got a real, permanent production domain
(metatron-delivery.thedomainnest.com) — leaving it on meant a future local
`shopify app dev` session would silently overwrite the live app's
application_url with a temporary Cloudflare tunnel, breaking the deployed
app until someone noticed and redeployed with the real URL.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes remaining DS-parity gaps from the feature audit:
- ProductRule model (product/collection/vendor/type/tag scoping) with
real server-side enforcement in hold-request.server.ts, plus shaped
availability in availability-request.server.ts. Covers per-product
prep time, cart-content-based slot blocking, and product-restricted
locations in one mechanism. New /app/rules admin page (Growth+).
- Driving-distance delivery zones via Google's Distance Matrix API,
cached like existing geocoding results.
- SHIPPING-only estimated arrival range (transitMinDays/transitMaxDays
on SlotTemplate) — widget shows "Arrives Thu-Sat" instead of a
meaningless ship-out time slot; carried through to the order
metafield write-back.
Storefront widget and POS extension now send cart contents (vendor/
type from cart.js, product ids for Admin-API-resolved collection/tag
rules) to both availability and hold endpoints.
checkout-datetime remains excluded from this deploy pending Shopify's
Network Access approval (unrelated to this work) — re-add from
../checkout-datetime-disabled and redeploy once granted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Prisma: Zone (postal-code list or radius), Rate (zone- or distance-band
keyed), GeocodeCache (permanent address->lat/lng cache per
IMPLEMENTATION_PLAN.md §9), Location.shopifyLocationId (maps to
Shopify's own Location resource for inventory checks), Booking.zoneId
(needed for per-zone delivery-density counts, not just per-location).
- app/lib/geo.ts: pure haversine distance + postal-code matching, unit
tested against known city-to-city distances.
- app/services/zones.server.ts: geocoding (Google Maps Geocoding API,
cached — never re-geocodes the same address twice), zone eligibility,
nearest-location auto-assignment ranked by distance, delivery-density
threshold checks (a sparse zone doesn't unlock until minOrders bookings
have already routed through it), and inventory-based location exclusion
via Shopify's InventoryLevel API (locations without a mapped
shopifyLocationId are left in rather than false-negative excluded).
- app/services/rates.server.ts: pure rate resolution by zone or distance
band, cheapest-match-wins when bands overlap.
- apps.scheduling.availability.tsx: LOCAL_DELIVERY requests with a
postalCode/address now auto-assign to the nearest eligible,
density-qualified zone/location instead of the shop's default location;
response includes the matched rate. Also fixed a real gap left over from
Phase 4: this route never actually read Booking counts into
getAvailability's `consumed` map, so capacity always showed as fully
available regardless of existing bookings — now it does.
- extensions/datetime-widget: LOCAL_DELIVERY now asks for a postal code
before showing dates; PICKUP shows a Google Maps pin for the location
(both gated on an optional Maps API key — a block setting in the theme
editor, since it needs to be public/client-side, not an app secret);
confirmation display and cart attributes (dd_zone_id, dd_rate_label)
carry the resolved zone/rate through to checkout.
- extensions/delivery-customization: now appends the resolved rate to the
relabeled delivery option ("Local delivery — Aug 25 ($5.99)") when one's
configured — real Cart Transform-based fee *charging* stays deferred to
v2 per IMPLEMENTATION_PLAN.md §5.4, this is display-only.
- Admin: /app/zones and /app/rates (Polaris CRUD, mirroring Phase 1's
patterns), plus shopifyLocationId and auto-geocode-on-save added to the
location edit form.
Fixed one real bug caught only by `npm run build` (not tsc/vitest, which
both passed clean): app.rates._index.tsx's component called
formatPriceLabel from rates.server.ts, and Remix correctly refuses to
bundle anything imported from a .server.ts path for the client. Moved the
pure (no I/O, no Prisma) formatter to app/lib/currency.ts.
Verified: lint, typecheck, 86 unit tests (+21 new: geo, zones, rates,
delivery-customization's rate-label case with a real WASM fixture run),
16 integration tests against live Postgres (+8 new: geocode caching,
postal/radius zone matching, nearest-first ranking, density thresholds),
both builds, and a live script exercising the full
zone-match -> density-check -> rate-resolve -> availability pipeline
together against the Postgres container.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`shopify app dev` failed dev preview with "Only assets, blocks, snippets,
locales directories are allowed" — the widget's TypeScript source lived in
extensions/datetime-widget/src/, which isn't one of the four directories a
Theme App Extension may contain. Moved it to widget-src/datetime-widget/
(a plain, non-extension folder outside extensions/) and updated
build:widget's esbuild input path accordingly; the bundled output still
lands in the same place (extensions/datetime-widget/assets/).
Also fixed a theme-check warning surfaced during the same run:
`script_tag` renders a parser-blocking <script> with no way to defer it —
switched to a manual <script defer> tag for the widget's JS asset.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>