mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:41:16 +00:00
188 lines
6.7 KiB
TypeScript
188 lines
6.7 KiB
TypeScript
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<unknown>[])
|
|
);
|
|
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();
|
|
});
|
|
});
|