import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { NextRequest } from "next/server"; const mocks = vi.hoisted(() => ({ constructEvent: vi.fn(), getOrder: vi.fn(), setStatusIfCurrent: vi.fn(), })); vi.mock("@/lib/stripe", () => ({ getStripe: () => ({ webhooks: { constructEvent: mocks.constructEvent } }), })); vi.mock("@/lib/orders-store", () => ({ getOrder: mocks.getOrder, setStatusIfCurrent: mocks.setStatusIfCurrent, })); import { POST } from "@/app/api/webhooks/stripe/route"; const savedWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET; const owner = { tenant_id: "tenant-test", buyer_subject: "buyer-test" }; beforeEach(() => { process.env.STRIPE_WEBHOOK_SECRET = "whsec_fake_local_regression"; mocks.constructEvent.mockReset(); mocks.getOrder.mockReset(); mocks.setStatusIfCurrent.mockReset(); mocks.setStatusIfCurrent.mockResolvedValue({ order_id: "order-1" }); }); afterEach(() => { if (savedWebhookSecret === undefined) delete process.env.STRIPE_WEBHOOK_SECRET; else process.env.STRIPE_WEBHOOK_SECRET = savedWebhookSecret; }); function delivery() { return new NextRequest("https://0pt.app/api/webhooks/stripe", { method: "POST", headers: { "stripe-signature": "signed-in-test" }, body: "{}", }); } function session(kind: "deposit" | "balance", paymentStatus: string, amount: number) { mocks.constructEvent.mockReturnValue({ type: "checkout.session.completed", data: { object: { metadata: { order_id: "order-1", kind }, payment_status: paymentStatus, amount_total: amount, currency: "sek", } }, }); } it("advances a matching paid deposit only after a verified event", async () => { session("deposit", "paid", 5000); mocks.getOrder.mockResolvedValue({ order_id: "order-1", ...owner, status: "draft", deposit_ore: 5000, balance_ore: 5000 }); expect((await POST(delivery())).status).toBe(200); expect(mocks.setStatusIfCurrent).toHaveBeenCalledWith("order-1", "draft", "in_production"); }); it("never advances an unpaid or amount-mismatched deposit", async () => { mocks.getOrder.mockResolvedValue({ order_id: "order-1", ...owner, status: "draft", deposit_ore: 5000, balance_ore: 5000 }); session("deposit", "unpaid", 5000); expect((await POST(delivery())).status).toBe(503); session("deposit", "paid", 4999); expect((await POST(delivery())).status).toBe(422); expect(mocks.setStatusIfCurrent).not.toHaveBeenCalled(); }); it("marks a matching paid balance delivered only after acceptance", async () => { session("balance", "paid", 5000); mocks.getOrder.mockResolvedValue({ order_id: "order-1", ...owner, status: "accepted", deposit_ore: 5000, balance_ore: 5000 }); expect((await POST(delivery())).status).toBe(200); expect(mocks.setStatusIfCurrent).toHaveBeenCalledWith("order-1", "accepted", "delivered"); }); it("does not acknowledge a paid event for a legacy anonymous draft", async () => { session("deposit", "paid", 5000); mocks.getOrder.mockResolvedValue({ order_id: "order-1", status: "draft", deposit_ore: 5000 }); expect((await POST(delivery())).status).toBe(503); expect(mocks.setStatusIfCurrent).not.toHaveBeenCalled(); });