Kuma/tests/api/stripe/checkout-pass.test.ts
2026-10-02 17:44:59 +02:00

191 lines
6.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("@/src/lib/prisma", () => ({ prisma: { product: { findUnique: vi.fn() } } }));
vi.mock("@/src/lib/auth/getUser", () => ({
getUserData: vi.fn(),
isUser: (user: { role: string }) => user.role === "user",
}));
vi.mock("@/src/lib/stripe", () => ({ getPlatformFeeCents: () => 50 }));
vi.mock("@/src/lib/stripe-checkout", () => ({
createBrandedCheckoutSession: vi.fn(),
studioCheckoutBranding: () => ({}),
}));
vi.mock("@/src/modules/settings/lib/discount-validation", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/src/modules/settings/lib/discount-validation")>();
return {
reserveDiscountCode: vi.fn(),
releaseDiscountReservation: vi.fn().mockResolvedValue(undefined),
calculateDiscountedPriceCents: actual.calculateDiscountedPriceCents,
};
});
vi.mock("@/src/modules/marketing/lib/consent", () => ({ grantConsentQuietly: vi.fn() }));
import { POST } from "@/src/app/api/stripe/checkout/pass/route";
import { prisma } from "@/src/lib/prisma";
import { getUserData } from "@/src/lib/auth/getUser";
import { createBrandedCheckoutSession } from "@/src/lib/stripe-checkout";
import {
releaseDiscountReservation,
reserveDiscountCode,
} from "@/src/modules/settings/lib/discount-validation";
import { grantConsentQuietly } from "@/src/modules/marketing/lib/consent";
const product = {
id: "prod-1",
name: "10er-Karte",
credits: 10,
priceCents: 12000,
validityMonths: 6,
isActive: true,
business: {
id: "biz-1",
name: "Studio",
slug: "studio",
isPublic: true,
branding: null,
stripeAccountId: "acct_studio",
stripeAccountStatus: "active",
},
};
function request(body: unknown) {
return new Request("http://localhost/api/stripe/checkout/pass", {
method: "POST",
body: JSON.stringify(body),
}) as never;
}
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getUserData).mockResolvedValue({
id: "user-1",
email: "kunde@example.com",
role: "user",
} as never);
vi.mocked(prisma.product.findUnique).mockResolvedValue(product as never);
vi.mocked(createBrandedCheckoutSession).mockResolvedValue({ url: "https://stripe/x" } as never);
});
describe("POST /api/stripe/checkout/pass", () => {
it("schickt Gäste zum Login", async () => {
vi.mocked(getUserData).mockResolvedValue(null);
const res = await POST(request({ productId: "prod-1" }));
expect(res.status).toBe(401);
expect((await res.json()).loginRequired).toBe(true);
});
it("lässt Team-Accounts keine Karte kaufen", async () => {
vi.mocked(getUserData).mockResolvedValue({ id: "m1", role: "manager" } as never);
expect((await POST(request({ productId: "prod-1" }))).status).toBe(403);
});
it("verkauft keine Karte, die nicht im Verkauf ist", async () => {
vi.mocked(prisma.product.findUnique).mockResolvedValue({
...product,
isActive: false,
} as never);
expect((await POST(request({ productId: "prod-1" }))).status).toBe(404);
expect(createBrandedCheckoutSession).not.toHaveBeenCalled();
});
it("nimmt die Servicegebühr pro Einheit als application_fee auf dem Studio-Account", async () => {
const res = await POST(request({ productId: "prod-1" }));
expect(res.status).toBe(200);
const [params, , accountId] = vi.mocked(createBrandedCheckoutSession).mock.calls[0];
expect(accountId).toBe("acct_studio");
expect(params.payment_intent_data?.application_fee_amount).toBe(500);
expect(params.line_items).toEqual([
expect.objectContaining({
quantity: 1,
price_data: expect.objectContaining({ unit_amount: 12000 }),
}),
expect.objectContaining({
quantity: 10,
price_data: expect.objectContaining({ unit_amount: 50 }),
}),
]);
expect(params.metadata).toMatchObject({ kind: "pass", productId: "prod-1", userId: "user-1" });
});
describe("mit Rabattcode", () => {
beforeEach(() => {
vi.mocked(reserveDiscountCode).mockResolvedValue({
reserved: true,
reservationToken: "tok-1",
discountCode: { id: "code-1", type: "percent", value: 25 },
});
});
it("mindert nur den Kartenpreis, nie die Servicegebühr", async () => {
const res = await POST(request({ productId: "prod-1", discountCode: "KARTE25" }));
expect(res.status).toBe(200);
expect(reserveDiscountCode).toHaveBeenCalledWith(expect.anything(), {
code: "KARTE25",
businessId: "biz-1",
productId: "prod-1",
userId: "user-1",
});
const params = vi.mocked(createBrandedCheckoutSession).mock.calls[0][0];
expect(params.line_items?.[0].price_data?.unit_amount).toBe(9000);
expect(params.payment_intent_data?.application_fee_amount).toBe(500);
expect(params.metadata).toMatchObject({
discountCodeId: "code-1",
discountAmountCents: "3000",
discountReservationToken: "tok-1",
});
});
it("lehnt einen ungültigen Code ab, statt zum vollen Preis weiterzumachen", async () => {
vi.mocked(reserveDiscountCode).mockResolvedValue({
reserved: false,
reason: "Dieser Rabattcode gilt nicht für Karten.",
});
const res = await POST(request({ productId: "prod-1", discountCode: "KURS10" }));
expect(res.status).toBe(400);
expect((await res.json()).error).toBe("Dieser Rabattcode gilt nicht für Karten.");
expect(createBrandedCheckoutSession).not.toHaveBeenCalled();
});
it("gibt die Reservierung frei, wenn Stripe die Session nicht anlegt", async () => {
vi.mocked(createBrandedCheckoutSession).mockRejectedValue(new Error("Stripe weg"));
vi.spyOn(console, "error").mockImplementation(() => {});
const res = await POST(request({ productId: "prod-1", discountCode: "KARTE25" }));
expect(res.status).toBe(500);
expect(releaseDiscountReservation).toHaveBeenCalledWith(expect.anything(), "tok-1");
});
});
it("antwortet bei falschen Feldtypen mit 400 statt 500", async () => {
const res = await POST(request({ productId: "prod-1", discountCode: 5 }));
expect(res.status).toBe(400);
expect(reserveDiscountCode).not.toHaveBeenCalled();
expect((await POST(request({ productId: "" }))).status).toBe(400);
});
it("antwortet bei kaputtem JSON mit 400", async () => {
const res = await POST(
new Request("http://localhost/api/stripe/checkout/pass", {
method: "POST",
body: "{kein json",
}) as never
);
expect(res.status).toBe(400);
});
it("speichert das Häkchen für Werbe-Mails", async () => {
await POST(request({ productId: "prod-1", marketingConsent: true }));
expect(grantConsentQuietly).toHaveBeenCalledWith({
userId: "user-1",
businessId: "biz-1",
source: "pass_purchase",
});
});
it("ohne Häkchen keine Einwilligung", async () => {
await POST(request({ productId: "prod-1" }));
expect(grantConsentQuietly).not.toHaveBeenCalled();
});
});