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