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>
72 lines
2.5 KiB
TypeScript
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 } : {}),
|
|
};
|
|
}
|