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>
110 lines
5.1 KiB
TypeScript
110 lines
5.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { evaluateCartAttributes, evaluateCheckout } from "../../extensions/validation-slot/src/evaluate.js";
|
|
|
|
const COMPLETE = {
|
|
dd_method: "PICKUP",
|
|
dd_date: "2026-08-25",
|
|
dd_start_min: "540",
|
|
dd_end_min: "600",
|
|
dd_location_id: "loc_123",
|
|
};
|
|
|
|
describe("evaluateCartAttributes", () => {
|
|
it("is valid when every required attribute is present", () => {
|
|
expect(evaluateCartAttributes(COMPLETE)).toEqual({ valid: true });
|
|
});
|
|
|
|
it("rejects a cart with no scheduling attributes at all", () => {
|
|
const result = evaluateCartAttributes({});
|
|
expect(result).toMatchObject({ valid: false, reason: "no_slot_selected" });
|
|
});
|
|
|
|
it("rejects a cart missing just one required attribute", () => {
|
|
const { dd_start_min, ...rest } = COMPLETE;
|
|
const result = evaluateCartAttributes(rest);
|
|
expect(result).toMatchObject({ valid: false, reason: "incomplete_slot_selection", missing: ["dd_start_min"] });
|
|
});
|
|
|
|
it("treats an empty-string attribute the same as missing", () => {
|
|
const result = evaluateCartAttributes({ ...COMPLETE, dd_date: "" });
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it("treats null/undefined attribute values as missing", () => {
|
|
const result = evaluateCartAttributes({ ...COMPLETE, dd_location_id: null, dd_method: undefined });
|
|
expect(result.valid).toBe(false);
|
|
if (result.valid) throw new Error("unreachable");
|
|
expect(result.reason).toBe("incomplete_slot_selection");
|
|
expect([...(result.missing ?? [])].sort()).toEqual(["dd_location_id", "dd_method"]);
|
|
});
|
|
});
|
|
|
|
// A snapshot that says loc_123 PICKUP on 2026-08-25 has a 09:00 slot with 2 left.
|
|
const SNAPSHOT = {
|
|
enforcement: { mode: "all" as const },
|
|
schedulableProductIds: [] as string[],
|
|
horizonDate: "2026-09-30",
|
|
slots: { "loc_123|PICKUP|2026-08-25|540": 2 },
|
|
closedDates: {} as Record<string, true>,
|
|
};
|
|
|
|
describe("evaluateCheckout — enforcement scope", () => {
|
|
it("never blocks when the shop set enforcement to off, even with nothing selected", () => {
|
|
const result = evaluateCheckout({ attributes: {}, snapshot: { ...SNAPSHOT, enforcement: { mode: "off" } } });
|
|
expect(result).toEqual({ valid: true });
|
|
});
|
|
|
|
it("blocks a slotless order when mode is 'all'", () => {
|
|
const result = evaluateCheckout({ attributes: {}, snapshot: SNAPSHOT });
|
|
expect(result).toMatchObject({ valid: false, reason: "no_slot_selected" });
|
|
});
|
|
|
|
it("skips enforcement for a 'tagged' shop when no cart product is flagged", () => {
|
|
const snapshot = { ...SNAPSHOT, enforcement: { mode: "tagged" as const }, schedulableProductIds: ["gid://shopify/Product/1"] };
|
|
const result = evaluateCheckout({ attributes: {}, snapshot, cartProductIds: ["gid://shopify/Product/999"] });
|
|
expect(result).toEqual({ valid: true });
|
|
});
|
|
|
|
it("enforces for a 'tagged' shop when a cart product is flagged", () => {
|
|
const snapshot = { ...SNAPSHOT, enforcement: { mode: "tagged" as const }, schedulableProductIds: ["gid://shopify/Product/1"] };
|
|
const result = evaluateCheckout({ attributes: {}, snapshot, cartProductIds: ["gid://shopify/Product/1"] });
|
|
expect(result).toMatchObject({ valid: false, reason: "no_slot_selected" });
|
|
});
|
|
});
|
|
|
|
describe("evaluateCheckout — live slot re-validation", () => {
|
|
it("passes a complete selection that still has capacity", () => {
|
|
expect(evaluateCheckout({ attributes: COMPLETE, snapshot: SNAPSHOT })).toEqual({ valid: true });
|
|
});
|
|
|
|
it("blocks when the picked slot has filled up since selection", () => {
|
|
const snapshot = { ...SNAPSHOT, slots: { "loc_123|PICKUP|2026-08-25|540": 0 } };
|
|
expect(evaluateCheckout({ attributes: COMPLETE, snapshot })).toMatchObject({ valid: false, reason: "slot_full" });
|
|
});
|
|
|
|
it("blocks when the picked date has been blacked out since selection", () => {
|
|
const snapshot = { ...SNAPSHOT, closedDates: { "loc_123|PICKUP|2026-08-25": true as const } };
|
|
expect(evaluateCheckout({ attributes: COMPLETE, snapshot })).toMatchObject({ valid: false, reason: "slot_unavailable" });
|
|
});
|
|
|
|
it("blocks when the picked slot no longer exists in the schedule", () => {
|
|
const snapshot = { ...SNAPSHOT, slots: { "loc_123|PICKUP|2026-08-25|600": 3 } };
|
|
expect(evaluateCheckout({ attributes: COMPLETE, snapshot })).toMatchObject({ valid: false, reason: "slot_unavailable" });
|
|
});
|
|
|
|
it("stays permissive past the snapshot horizon", () => {
|
|
const result = evaluateCheckout({ attributes: { ...COMPLETE, dd_date: "2027-01-01" }, snapshot: SNAPSHOT });
|
|
expect(result).toEqual({ valid: true });
|
|
});
|
|
|
|
it("falls back to presence-only when there is no snapshot at all", () => {
|
|
expect(evaluateCheckout({ attributes: COMPLETE, snapshot: null })).toEqual({ valid: true });
|
|
expect(evaluateCheckout({ attributes: {}, snapshot: null })).toMatchObject({ valid: false, reason: "no_slot_selected" });
|
|
});
|
|
|
|
it("falls back to presence-only when the snapshot carries no slot data yet (store mid-setup)", () => {
|
|
const bare = { enforcement: { mode: "all" as const }, slots: {}, closedDates: {}, horizonDate: "2026-09-30" };
|
|
expect(evaluateCheckout({ attributes: COMPLETE, snapshot: bare })).toEqual({ valid: true });
|
|
});
|
|
});
|