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"; // Public app-proxy endpoint (see apps.scheduling.availability.tsx for the // path-mirroring rationale). Called by the widget the moment a shopper // picks a slot, before it writes the cart attribute — this is what // actually reserves capacity (PRODUCT_STRATEGY.md §3.1, §4.1: "the // last-slot race condition"). The cart attribute write alone would just be // two shoppers racing to write the same free-text field; nothing would // stop both orders from completing. Actual resolution lives in // services/hold-request.server.ts, shared with the POS route. const VALID_METHODS = new Set(["SHIPPING", "LOCAL_DELIVERY", "PICKUP"]); export const action = async ({ request }: ActionFunctionArgs) => { const { session } = await authenticate.public.appProxy(request); if (!session) { return Response.json({ error: "Shop not found" }, { status: 404 }); } const body = await request.json(); const { intent, locationId, method, date, startMin, cartToken } = body as { intent?: "create" | "release"; locationId?: string; method?: string; date?: string; startMin?: number; cartToken?: string; }; if (!locationId || !method || !VALID_METHODS.has(method as Method) || !date || typeof startMin !== "number" || !cartToken) { return Response.json({ error: "Missing or invalid parameters" }, { status: 400 }); } const result = await resolveHoldRequest(session.shop, { intent: intent === "release" ? "release" : "create", locationId, method: method as Method, date, startMin, cartToken, }); return Response.json(result.body, { status: result.status }); };