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>
48 lines
2.0 KiB
TypeScript
48 lines
2.0 KiB
TypeScript
import type { ActionFunctionArgs } from "@remix-run/node";
|
|
import type { Method } from "@prisma/client";
|
|
import { authenticate } from "../shopify.server";
|
|
import { resolveHoldRequest } from "../services/hold-request.server";
|
|
import { parseCartLinesParam } from "../lib/cart-rule-params";
|
|
|
|
// POS UI Extension endpoint — see pos.scheduling.availability.tsx for the
|
|
// session-token/CORS rationale. Calls the exact same resolveHoldRequest()
|
|
// as the storefront's apps.scheduling.hold.tsx, so a staff-booked slot and
|
|
// a shopper-booked slot compete for the same Redis-backed capacity — a POS
|
|
// order can't double-book a slot an online shopper already holds, or vice
|
|
// versa (IMPLEMENTATION_PLAN.md Phase 7 accept criteria).
|
|
|
|
const VALID_METHODS = new Set<Method>(["SHIPPING", "LOCAL_DELIVERY", "PICKUP"]);
|
|
|
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
|
const { sessionToken, cors } = await authenticate.public.pos(request);
|
|
const shopDomain = sessionToken.dest.replace(/^https?:\/\//, "");
|
|
|
|
const body = await request.json();
|
|
const { intent, locationId, method, date, startMin, cartToken, cartLines } = body as {
|
|
intent?: "create" | "release";
|
|
locationId?: string;
|
|
method?: string;
|
|
date?: string;
|
|
startMin?: number;
|
|
cartToken?: string;
|
|
cartLines?: Array<{ vendor?: string; productType?: string }>;
|
|
};
|
|
|
|
if (!locationId || !method || !VALID_METHODS.has(method as Method) || !date || typeof startMin !== "number" || !cartToken) {
|
|
return cors(Response.json({ error: "Missing or invalid parameters" }, { status: 400 }));
|
|
}
|
|
|
|
const result = await resolveHoldRequest(shopDomain, {
|
|
intent: intent === "release" ? "release" : "create",
|
|
locationId,
|
|
method: method as Method,
|
|
date,
|
|
startMin,
|
|
cartToken,
|
|
// No Admin API client on POS session-token auth — vendor/type rules only, same as pos.scheduling.availability.tsx.
|
|
cartLines: parseCartLinesParam(Array.isArray(cartLines) ? JSON.stringify(cartLines) : undefined),
|
|
});
|
|
|
|
return cors(Response.json(result.body, { status: result.status }));
|
|
};
|