import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("@/src/lib/prisma", () => { const prisma = { customerPass: { findUnique: vi.fn(), create: vi.fn() }, product: { findUnique: vi.fn() }, user: { findUnique: vi.fn() }, discountRedemption: { updateMany: vi.fn(), findUnique: vi.fn(), delete: vi.fn() }, discountCode: { update: vi.fn() }, $transaction: vi.fn(), }; // Callback-Transaktion: läuft mit demselben Mock; Array-Transaktion: alle Promises. prisma.$transaction.mockImplementation((arg: unknown) => typeof arg === "function" ? arg(prisma) : Promise.all(arg as Promise[]) ); return { prisma }; }); vi.mock("@/src/lib/stripe", () => ({ stripe: { paymentIntents: { retrieve: vi.fn() } }, onAccount: (id: string | null) => (id ? { stripeAccount: id } : undefined), })); vi.mock("@/src/lib/stripe-fee", () => ({ recordStripeFee: vi.fn() })); vi.mock("@/src/lib/stripe-refund", () => ({ refundStripePayment: vi.fn() })); vi.mock("@/src/lib/mail/resend", () => ({ sendMail: vi.fn() })); vi.mock("@/src/modules/notifications/lib/create-notification", () => ({ notifyBusinessManager: vi.fn(), })); import { handlePassCheckoutCompleted, handlePassCheckoutUnpaid, isPassCheckout, } from "@/src/modules/products/lib/pass-webhook"; import { prisma } from "@/src/lib/prisma"; import { stripe } from "@/src/lib/stripe"; import { recordStripeFee } from "@/src/lib/stripe-fee"; import { refundStripePayment } from "@/src/lib/stripe-refund"; import { sendMail } from "@/src/lib/mail/resend"; import { notifyBusinessManager } from "@/src/modules/notifications/lib/create-notification"; const session = { id: "cs_1", payment_status: "paid", payment_intent: "pi_1", amount_total: 12500, customer_details: null, metadata: { kind: "pass", productId: "prod-1", userId: "user-1" }, }; beforeEach(() => { vi.clearAllMocks(); vi.mocked(prisma.customerPass.findUnique).mockResolvedValue(null); vi.mocked(prisma.customerPass.create).mockResolvedValue({ id: "pass-1" } as never); vi.mocked(prisma.product.findUnique).mockResolvedValue({ id: "prod-1", name: "10er-Karte", credits: 10, validityMonths: 6, sports: ["pilates"], businessId: "biz-1", business: { name: "Studio", branding: null }, } as never); vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: "user-1", email: "Kunde@Example.com", name: "Anna", } as never); vi.mocked(stripe.paymentIntents.retrieve).mockResolvedValue({ application_fee_amount: 500, } as never); vi.mocked(recordStripeFee).mockResolvedValue(undefined); vi.mocked(sendMail).mockResolvedValue(undefined as never); vi.mocked(notifyBusinessManager).mockResolvedValue(undefined as never); }); describe("isPassCheckout", () => { it("erkennt Kartenkäufe an den Metadaten", () => { expect(isPassCheckout(session as never)).toBe(true); expect(isPassCheckout({ metadata: { courseId: "c1" } } as never)).toBe(false); }); }); describe("handlePassCheckoutCompleted", () => { it("legt die Karte mit Momentaufnahme des Produkts und Ablaufdatum an", async () => { await handlePassCheckoutCompleted(session as never, "acct_studio"); const { data } = vi.mocked(prisma.customerPass.create).mock.calls[0][0]; expect(data).toMatchObject({ productId: "prod-1", businessId: "biz-1", userId: "user-1", email: "kunde@example.com", name: "10er-Karte", sports: ["pilates"], creditsTotal: 10, creditsRemaining: 10, source: "purchase", amountPaid: 12500, platformFee: 500, priceCents: 12000, stripeAccountId: "acct_studio", stripeSessionId: "cs_1", }); expect(data.validUntil).toBeInstanceOf(Date); expect(recordStripeFee).toHaveBeenCalledWith("pi_1", "acct_studio"); expect(sendMail).toHaveBeenCalled(); expect(vi.mocked(notifyBusinessManager).mock.calls[0][1].type).toBe("pass.sold"); }); it("legt bei erneuter Zustellung derselben Session keine zweite Karte an", async () => { vi.mocked(prisma.customerPass.findUnique).mockResolvedValue({ id: "pass-1" } as never); await handlePassCheckoutCompleted(session as never, "acct_studio"); expect(prisma.customerPass.create).not.toHaveBeenCalled(); }); it("wartet bei SEPA, bis die Zahlung angekommen ist", async () => { await handlePassCheckoutCompleted({ ...session, payment_status: "unpaid" } as never, "acct"); expect(prisma.customerPass.create).not.toHaveBeenCalled(); }); it("erstattet, wenn das Produkt inzwischen gelöscht wurde", async () => { vi.mocked(prisma.product.findUnique).mockResolvedValue(null); vi.mocked(refundStripePayment).mockResolvedValue({ success: true, stripeRefundId: "re_1", amountRefundedCents: 12500, }); await handlePassCheckoutCompleted(session as never, "acct_studio"); expect(prisma.customerPass.create).not.toHaveBeenCalled(); expect(refundStripePayment).toHaveBeenCalledWith( expect.objectContaining({ paymentIntentId: "pi_1", refundApplicationFee: true }) ); }); }); describe("Kartenkauf mit Rabattcode", () => { const discounted = { ...session, amount_total: 10500, metadata: { ...session.metadata, discountCodeId: "code-1", discountAmountCents: "2000", discountReservationToken: "tok-1", }, }; it("speichert den Rabatt an der Karte und bestätigt die Reservierung", async () => { vi.mocked(prisma.discountRedemption.updateMany).mockResolvedValue({ count: 1 } as never); await handlePassCheckoutCompleted(discounted as never, "acct_studio"); const { data } = vi.mocked(prisma.customerPass.create).mock.calls[0][0]; expect(data).toMatchObject({ discountCodeId: "code-1", discountAmount: 2000, // Der Kartenpreis ist schon rabattiert: gezahlt minus Servicegebühr. priceCents: 10000, }); expect(prisma.discountRedemption.updateMany).toHaveBeenCalledWith({ where: { stripeSessionId: "tok-1", status: "reserved" }, data: { status: "confirmed", passId: "pass-1" }, }); }); it("gibt die Reservierung frei, wenn der Kauf nie bezahlt wurde", async () => { vi.mocked(prisma.discountRedemption.findUnique).mockResolvedValue({ id: "red-1", discountCodeId: "code-1", status: "reserved", } as never); await handlePassCheckoutUnpaid(discounted as never); expect(prisma.discountRedemption.delete).toHaveBeenCalledWith({ where: { id: "red-1" } }); expect(prisma.discountCode.update).toHaveBeenCalledWith({ where: { id: "code-1" }, data: { redeemedCount: { decrement: 1 } }, }); }); it("ohne Rabattcode bleibt nichts freizugeben", async () => { await handlePassCheckoutUnpaid(session as never); expect(prisma.discountRedemption.findUnique).not.toHaveBeenCalled(); }); });