/** * Order webhook → booking, against real Postgres (TEST_DATABASE_URL). * Admin API calls (tagging/metafields) are skipped by passing no admin client. */ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; const url = process.env.TEST_DATABASE_URL; if (url) process.env.DATABASE_URL = url; const d = url ? describe : describe.skip; d("order webhooks", async () => { const { default: prisma } = await import("../../db.server"); const { handleOrderCreate, handleOrderCancelled } = await import("../orders.server"); const { ReservationService } = await import("../reservations.server"); const { withDefaults } = await import("../settings"); const svc = new ReservationService(prisma); const SHOP = "orders-test.myshopify.com"; let shopId = ""; let locId = ""; const sel = (o: Record = {}) => ({ t: "tok_orders_test_0001", m: "delivery", l: locId, d: "2030-06-04", s: "10:00-13:00", f: 799, v: 1, ...o }); const order = (id: number, attr: object | null, feePrice = "7.99") => ({ id, admin_graphql_api_id: `gid://shopify/Order/${id}`, name: `#${id}`, email: "priya@example.com", phone: "+15195550142", note_attributes: attr ? [{ name: "_datewise", value: JSON.stringify(attr) }] : [], customer: { first_name: "Priya", last_name: "R" }, shipping_address: { address1: "1 King St", city: "Kitchener", zip: "N2H 1A1", latitude: 43.45, longitude: -80.49 }, line_items: [ { title: "Marigold", quantity: 2, price: "19.99", product_id: 101, variant_id: 1001 }, ...(feePrice ? [{ title: "Delivery & handling", quantity: 1, price: feePrice, product_id: 900, variant_id: 9000 }] : []), ], }); beforeAll(async () => { await prisma.shop.deleteMany({ where: { domain: SHOP } }); }); afterAll(async () => { await prisma.shop.deleteMany({ where: { domain: SHOP } }); }); beforeEach(async () => { await prisma.shop.deleteMany({ where: { domain: SHOP } }); const shop = await prisma.shop.create({ data: { domain: SHOP, plan: "Growth", feeProductId: "gid://shopify/Product/900", settings: withDefaults({}) as object, locations: { create: [{ name: "Kitchener", address: "x", timezone: "America/Toronto", methods: { delivery: { enabled: true, weeklyHours: { 2: [{ start: "10:00", end: "19:00" }] }, slotMode: "generated", slotMinutes: 180, prepMinutes: 0, prepMode: "elapsed", horizonDays: 3650, slotCapacity: 1 } }, }], }, }, include: { locations: true }, }); shopId = shop.id; locId = shop.locations[0].id; }); const hold = (token = "tok_orders_test_0001", fee = 799) => svc.hold(shopId, token, { locationId: locId, method: "delivery", date: "2030-06-04", slot: "10:00-13:00" }, { dayCap: null, slotCap: 1 }, new Date(), 10, fee); it("confirms the held place, stores contact + items, plans reminders", async () => { await hold(); const r = await handleOrderCreate(SHOP, order(1, sel())); expect("ok" in r && r.ok).toBe(true); const b = await prisma.booking.findFirstOrThrow({ where: { shopId }, include: { reminderJobs: true } }); expect(b).toMatchObject({ status: "confirmed", customerName: "Priya R", fee: 799, slotStart: "10:00", reviewReason: null }); expect(b.startUtc?.toISOString()).toBe("2030-06-04T14:00:00.000Z"); expect(b.reminderJobs.length).toBeGreaterThan(0); }); it("is idempotent for the same order", async () => { await hold(); await handleOrderCreate(SHOP, order(2, sel())); await handleOrderCreate(SHOP, order(2, sel())); expect(await prisma.booking.count({ where: { shopId } })).toBe(1); }); it("flags a fee that doesn't match the quote, but keeps the place", async () => { await hold(); await handleOrderCreate(SHOP, order(3, sel(), "0.01")); const b = await prisma.booking.findFirstOrThrow({ where: { shopId } }); expect(b.status).toBe("confirmed"); expect(b.reviewReason).toMatch(/Fee paid 0.01 differs from quote 7.99/); }); it("orders without a time go to review when scheduling is required", async () => { await handleOrderCreate(SHOP, order(4, null, "")); const b = await prisma.booking.findFirstOrThrow({ where: { shopId } }); expect(b.status).toBe("needs_review"); }); it("cancellation frees the place for the next customer", async () => { await hold(); await handleOrderCreate(SHOP, order(5, sel())); expect((await hold("tok_orders_test_0002")).ok).toBe(false); await handleOrderCancelled(SHOP, { admin_graphql_api_id: "gid://shopify/Order/5" }); expect((await hold("tok_orders_test_0002")).ok).toBe(true); }); });