Some checks failed
CI / Lint, Unit & Integration Tests (push) Has been cancelled
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>
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import type { ActionFunctionArgs } from "@remix-run/node";
|
|
import { authenticate } from "../shopify.server";
|
|
import { createBookingFromOrder, type OrderWebhookPayload } from "../services/booking.server";
|
|
import { writeCheckoutSnapshot } from "../services/checkout-snapshot.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);
|
|
|
|
// This order just consumed capacity — refresh the snapshot the Cart/
|
|
// Checkout Validation Function reads so the next shopper can't book a
|
|
// slot this one filled (study §5.2 "has since become invalid").
|
|
if (admin) {
|
|
await writeCheckoutSnapshot(admin, shop);
|
|
}
|
|
|
|
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 } : {}),
|
|
};
|
|
}
|