107 lines
4.6 KiB
TypeScript
107 lines
4.6 KiB
TypeScript
/**
|
|
* 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<string, unknown> = {}) => ({ 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);
|
|
});
|
|
});
|