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
87 lines
3.1 KiB
TypeScript
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();
|
|
});
|