Files
0pt/tests/api/stripe-webhook-payment.test.ts
admin a1557b2a7d
CI (SIAX Cloud) / integration (push) Has been skipped
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / security (push) Successful in 14s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 14s
Security Scan (SIAX Cloud) / security (push) Successful in 30s
CI (SIAX Cloud) / quality (push) Successful in 35s
CI (SIAX Cloud) / integration (pull_request) Successful in 25s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 54s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m31s
feat(payments): bind orders to verified tenant and buyer
2026-09-15 19:53:58 +02:00

87 lines
3.1 KiB
TypeScript

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();
});