mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
158 lines
6 KiB
TypeScript
158 lines
6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||
|
||
const tx = {
|
||
courseBooking: { updateMany: vi.fn() },
|
||
customerPass: { update: vi.fn() },
|
||
};
|
||
|
||
vi.mock("@/src/lib/prisma", () => ({
|
||
prisma: {
|
||
courseBooking: { findUnique: vi.fn(), updateMany: vi.fn(), update: vi.fn() },
|
||
courseWaitlistEntry: { count: vi.fn() },
|
||
user: { findUnique: vi.fn() },
|
||
guestLead: { findUnique: vi.fn() },
|
||
$transaction: vi.fn((fn: (t: typeof tx) => unknown) => fn(tx)),
|
||
},
|
||
}));
|
||
vi.mock("@/src/lib/stripe-refund", () => ({ refundStripePayment: vi.fn() }));
|
||
vi.mock("@/src/lib/mail/resend", () => ({ sendMail: vi.fn() }));
|
||
vi.mock("@/src/lib/mail/templates/pass-booking-cancellation", () => ({
|
||
generatePassBookingCancellationEmail: vi
|
||
.fn()
|
||
.mockReturnValue({ subject: "s", html: "h", text: "t" }),
|
||
}));
|
||
vi.mock("@/src/modules/courses/actions/notify-waitlist", () => ({ notifyCourseWaitlist: vi.fn() }));
|
||
vi.mock("@/src/modules/notifications/lib/create-notification", () => ({
|
||
notifyBusinessManager: vi.fn(),
|
||
}));
|
||
vi.mock("@/src/modules/settings/lib/to-email-branding", () => ({
|
||
toEmailBranding: vi.fn().mockReturnValue({}),
|
||
}));
|
||
vi.mock("@/src/modules/courses/actions/cancellation-policy", async (importOriginal) => {
|
||
const actual = await importOriginal<object>();
|
||
return { ...actual, calculateRefundAmountCents: vi.fn() };
|
||
});
|
||
|
||
import { executeBookingCancellation } from "@/src/modules/courses/actions/cancel-booking-shared";
|
||
import { calculateRefundAmountCents } from "@/src/modules/courses/actions/cancellation-policy";
|
||
import { refundStripePayment } from "@/src/lib/stripe-refund";
|
||
import { prisma } from "@/src/lib/prisma";
|
||
import { generatePassBookingCancellationEmail } from "@/src/lib/mail/templates/pass-booking-cancellation";
|
||
import { notifyCourseWaitlist } from "@/src/modules/courses/actions/notify-waitlist";
|
||
import { notifyBusinessManager } from "@/src/modules/notifications/lib/create-notification";
|
||
import { sendMail } from "@/src/lib/mail/resend";
|
||
|
||
// Mit einer 10er-Karte gebucht: kein Geld, keine Stripe-Felder.
|
||
const passBooking = {
|
||
paymentStatus: "paid",
|
||
amountPaid: null,
|
||
platformFee: null,
|
||
platformFeeOnTop: false,
|
||
stripePaymentIntentId: null,
|
||
stripeAccountId: null,
|
||
userId: "user-1",
|
||
guestLeadId: null,
|
||
seats: 2,
|
||
seatsPurchased: 2,
|
||
spotIds: [] as string[],
|
||
spotLabels: [] as string[],
|
||
refundedAmount: null,
|
||
refundedFeeAmount: null,
|
||
refundedAt: null,
|
||
passId: "pass-1",
|
||
pass: { name: "10er-Karte" },
|
||
course: {
|
||
id: "course-1",
|
||
name: "Yoga",
|
||
date: new Date("2099-01-01"),
|
||
timeFrom: "10:00",
|
||
businessId: "biz-1",
|
||
business: { name: "Studio", branding: null },
|
||
},
|
||
};
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
vi.mocked(prisma.courseBooking.findUnique).mockResolvedValue(passBooking as never);
|
||
vi.mocked(prisma.user.findUnique).mockResolvedValue({ email: "kunde@example.com" } as never);
|
||
vi.mocked(prisma.courseWaitlistEntry.count).mockResolvedValue(0 as never);
|
||
vi.mocked(notifyCourseWaitlist).mockResolvedValue(undefined as never);
|
||
vi.mocked(notifyBusinessManager).mockResolvedValue(undefined as never);
|
||
vi.mocked(sendMail).mockResolvedValue(undefined as never);
|
||
vi.mocked(generatePassBookingCancellationEmail).mockReturnValue({
|
||
subject: "s",
|
||
html: "h",
|
||
text: "t",
|
||
});
|
||
tx.courseBooking.updateMany.mockResolvedValue({ count: 1 });
|
||
tx.customerPass.update.mockResolvedValue({});
|
||
vi.mocked(calculateRefundAmountCents).mockResolvedValue({
|
||
cancellationAllowed: true,
|
||
refundPercent: 100,
|
||
refundAmountCents: 0,
|
||
});
|
||
});
|
||
|
||
describe("executeBookingCancellation – Kartenbuchung", () => {
|
||
it("gibt die Einheiten zurück, wenn die Stornoregel 100 % erstatten würde", async () => {
|
||
const result = await executeBookingCancellation("booking-1", "user");
|
||
|
||
expect(result).toMatchObject({ success: true, creditsReturned: 2, refundedAmountCents: 0 });
|
||
expect(refundStripePayment).not.toHaveBeenCalled();
|
||
expect(tx.courseBooking.updateMany.mock.calls[0][0].data.paymentStatus).toBe("refunded");
|
||
expect(tx.customerPass.update).toHaveBeenCalledWith({
|
||
where: { id: "pass-1" },
|
||
data: { creditsRemaining: { increment: 2 } },
|
||
});
|
||
});
|
||
|
||
it("lässt die Einheiten verfallen, wenn die Frist verstrichen ist", async () => {
|
||
vi.mocked(calculateRefundAmountCents).mockResolvedValue({
|
||
cancellationAllowed: true,
|
||
refundPercent: 50,
|
||
refundAmountCents: 0,
|
||
});
|
||
|
||
const result = await executeBookingCancellation("booking-1", "user");
|
||
|
||
expect(result).toMatchObject({ success: true, creditsReturned: 0 });
|
||
expect(tx.courseBooking.updateMany.mock.calls[0][0].data.paymentStatus).toBe("cancelled");
|
||
expect(tx.customerPass.update).not.toHaveBeenCalled();
|
||
expect(vi.mocked(generatePassBookingCancellationEmail).mock.calls[0][0].creditsReturned).toBe(
|
||
false
|
||
);
|
||
});
|
||
|
||
it("gibt bei einer Kursabsage immer zurück, ohne die Stornoregel zu fragen", async () => {
|
||
const result = await executeBookingCancellation("booking-1", "manager", {
|
||
forceFullRefund: true,
|
||
});
|
||
|
||
expect(result).toMatchObject({ success: true, creditsReturned: 2 });
|
||
expect(calculateRefundAmountCents).not.toHaveBeenCalled();
|
||
expect(vi.mocked(generatePassBookingCancellationEmail).mock.calls[0][0].reason).toBe(
|
||
"course_cancelled"
|
||
);
|
||
});
|
||
|
||
it("storniert einzelne Plätze und gibt nur deren Einheiten zurück", async () => {
|
||
const result = await executeBookingCancellation("booking-1", "user", {
|
||
seatSelection: { seats: 1 },
|
||
});
|
||
|
||
expect(result).toMatchObject({ success: true, remainingSeats: 1, creditsReturned: 1 });
|
||
expect(tx.courseBooking.updateMany.mock.calls[0][0].data).toMatchObject({ seats: 1 });
|
||
expect(tx.customerPass.update.mock.calls[0][0].data).toEqual({
|
||
creditsRemaining: { increment: 1 },
|
||
});
|
||
});
|
||
|
||
it("schreibt bei einem Doppelklick nicht zweimal gut", async () => {
|
||
tx.courseBooking.updateMany.mockResolvedValue({ count: 0 });
|
||
|
||
const result = await executeBookingCancellation("booking-1", "user");
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(tx.customerPass.update).not.toHaveBeenCalled();
|
||
});
|
||
});
|