metatrondelivery/app/routes/webhooks.orders.create.tsx
MOHAN 03574a4914 feat: product rules, driving-distance zones, and shipping date ranges
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>
2026-08-26 00:28:45 +05:30

72 lines
2.5 KiB
TypeScript

import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import { createBookingFromOrder, type OrderWebhookPayload } from "../services/booking.server";
// Converts the cart's dd_* attributes into a confirmed Booking, releases
// the matching Redis hold, and writes the slot back onto the order via a
// metafield so staff (and, later, POS) see it natively on the order record
// — IMPLEMENTATION_PLAN.md §5.2 / §2 data flow. Idempotent: Shopify
// redelivers webhooks, and booking.server.ts's upsert-on-orderId handles
// that safely.
export const action = async ({ request }: ActionFunctionArgs) => {
const { shop, topic, payload, admin } = await authenticate.webhook(request);
console.log(`Received ${topic} webhook for ${shop}`);
const order = payload as unknown as OrderWebhookPayload;
await createBookingFromOrder(shop, order);
if (admin) {
const bookingSummary = summarizeBookingAttributes(order);
if (bookingSummary) {
await admin.graphql(
`#graphql
mutation setBookingMetafield($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
userErrors { field message }
}
}`,
{
variables: {
metafields: [
{
ownerId: order.admin_graphql_api_id,
namespace: "delivery_datetime",
key: "booking",
type: "json",
value: JSON.stringify(bookingSummary),
},
],
},
},
);
}
}
return new Response();
};
function summarizeBookingAttributes(order: OrderWebhookPayload) {
const attrs = order.note_attributes ?? [];
const get = (key: string) => attrs.find((a) => a.name === key)?.value;
const method = get("dd_method");
const date = get("dd_date");
const startMin = get("dd_start_min");
const endMin = get("dd_end_min");
const locationId = get("dd_location_id");
// SHIPPING-only "date range" parity item (PRODUCT_STRATEGY.md §2) — present
// only when the matched slot template had transit days configured.
const arrivalRangeStart = get("dd_arrival_range_start");
const arrivalRangeEnd = get("dd_arrival_range_end");
if (!method || !date || !startMin || !endMin || !locationId) return null;
return {
method,
date,
startMin: Number(startMin),
endMin: Number(endMin),
locationId,
...(arrivalRangeStart ? { arrivalRangeStart } : {}),
...(arrivalRangeEnd ? { arrivalRangeEnd } : {}),
};
}