mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
186 lines
6.7 KiB
TypeScript
186 lines
6.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
const tx = {
|
|
course: { findUnique: vi.fn() },
|
|
customerPass: { findUnique: vi.fn(), updateMany: vi.fn() },
|
|
courseBooking: { findUnique: vi.fn(), upsert: vi.fn() },
|
|
};
|
|
|
|
vi.mock("@/src/lib/prisma", () => ({
|
|
prisma: {
|
|
$transaction: vi.fn((fn: (t: typeof tx) => unknown) => fn(tx)),
|
|
courseWaitlistEntry: { deleteMany: vi.fn() },
|
|
},
|
|
}));
|
|
vi.mock("@/src/lib/auth/getUser", () => ({
|
|
getUserData: vi.fn(),
|
|
isUser: (user: { role: string }) => user.role === "user",
|
|
}));
|
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn(), updateTag: vi.fn() }));
|
|
vi.mock("@/src/lib/mail/resend", () => ({ sendMail: vi.fn() }));
|
|
vi.mock("@/src/modules/courses/lib/notify-booking", () => ({ notifyManagerOfBooking: vi.fn() }));
|
|
vi.mock("@/src/modules/courses/lib/seat-counts", async (importOriginal) => ({
|
|
...(await importOriginal<object>()),
|
|
paidSeatCount: vi.fn(),
|
|
}));
|
|
vi.mock("@/src/modules/courses/lib/course-spots", async (importOriginal) => ({
|
|
...(await importOriginal<object>()),
|
|
getCourseSpots: vi.fn(),
|
|
getBookedSpotIds: vi.fn(),
|
|
getHeldSpotIds: vi.fn(),
|
|
}));
|
|
|
|
import { bookCourseWithPass } from "@/src/modules/products/actions/pass-booking-actions";
|
|
import { prisma } from "@/src/lib/prisma";
|
|
import { getUserData } from "@/src/lib/auth/getUser";
|
|
import { sendMail } from "@/src/lib/mail/resend";
|
|
import { notifyManagerOfBooking } from "@/src/modules/courses/lib/notify-booking";
|
|
import { paidSeatCount } from "@/src/modules/courses/lib/seat-counts";
|
|
import {
|
|
getBookedSpotIds,
|
|
getCourseSpots,
|
|
getHeldSpotIds,
|
|
} from "@/src/modules/courses/lib/course-spots";
|
|
|
|
const course = {
|
|
id: "course-1",
|
|
name: "Mat Pilates",
|
|
date: new Date("2099-01-01"),
|
|
timeFrom: "10:00",
|
|
timeTo: "11:00",
|
|
status: "published",
|
|
sport: ["pilates"],
|
|
maxParticipants: 10,
|
|
spotBooking: false,
|
|
room: "room-1",
|
|
coverImage: null,
|
|
businessId: "biz-1",
|
|
business: { isPublic: true, name: "Studio", address: "Weg 1", branding: null },
|
|
};
|
|
|
|
const pass = {
|
|
id: "pass-1",
|
|
userId: "user-1",
|
|
businessId: "biz-1",
|
|
name: "10er-Karte",
|
|
sports: [] as string[],
|
|
status: "active",
|
|
validUntil: null as Date | null,
|
|
creditsRemaining: 5,
|
|
};
|
|
|
|
const book = (overrides: Partial<Parameters<typeof bookCourseWithPass>[0]> = {}) =>
|
|
bookCourseWithPass({ courseId: "course-1", passId: "pass-1", seats: 1, ...overrides });
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(getUserData).mockResolvedValue({
|
|
id: "user-1",
|
|
email: "kunde@example.com",
|
|
name: "Anna",
|
|
role: "user",
|
|
} as never);
|
|
tx.course.findUnique.mockResolvedValue(course);
|
|
tx.customerPass.findUnique.mockResolvedValue(pass);
|
|
tx.customerPass.updateMany.mockResolvedValue({ count: 1 });
|
|
tx.courseBooking.findUnique.mockResolvedValue(null);
|
|
tx.courseBooking.upsert.mockResolvedValue({});
|
|
vi.mocked(paidSeatCount).mockResolvedValue(3);
|
|
vi.mocked(getCourseSpots).mockResolvedValue(null);
|
|
vi.mocked(prisma.courseWaitlistEntry.deleteMany).mockResolvedValue({ count: 0 } as never);
|
|
vi.mocked(sendMail).mockResolvedValue(undefined as never);
|
|
vi.mocked(notifyManagerOfBooking).mockResolvedValue(undefined);
|
|
});
|
|
|
|
describe("bookCourseWithPass", () => {
|
|
it("bucht, zieht die Einheiten atomar ab und legt eine Kartenbuchung ohne Zahlung an", async () => {
|
|
const result = await book({ seats: 2 });
|
|
|
|
expect(result).toEqual({ success: true, seats: 2, creditsRemaining: 3 });
|
|
expect(tx.customerPass.updateMany).toHaveBeenCalledWith({
|
|
where: { id: "pass-1", status: "active", creditsRemaining: { gte: 2 } },
|
|
data: { creditsRemaining: { decrement: 2 } },
|
|
});
|
|
const { create } = tx.courseBooking.upsert.mock.calls[0][0];
|
|
expect(create).toMatchObject({
|
|
courseId: "course-1",
|
|
userId: "user-1",
|
|
paymentStatus: "paid",
|
|
passId: "pass-1",
|
|
seats: 2,
|
|
amountPaid: null,
|
|
stripePaymentIntentId: null,
|
|
});
|
|
expect(sendMail).toHaveBeenCalled();
|
|
});
|
|
|
|
it("lehnt Gäste und Team-Accounts ab", async () => {
|
|
vi.mocked(getUserData).mockResolvedValue({ id: "m1", role: "manager" } as never);
|
|
expect((await book()).success).toBe(false);
|
|
expect(prisma.$transaction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("lehnt die Karte einer anderen Person ab", async () => {
|
|
tx.customerPass.findUnique.mockResolvedValue({ ...pass, userId: "someone-else" });
|
|
expect(await book()).toMatchObject({ success: false, error: "Karte nicht gefunden." });
|
|
expect(tx.customerPass.updateMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("lehnt eine Karte ab, die nicht für diese Kursart gilt", async () => {
|
|
tx.customerPass.findUnique.mockResolvedValue({ ...pass, sports: ["yoga"] });
|
|
expect(await book()).toMatchObject({
|
|
success: false,
|
|
error: "Diese Karte gilt nicht für diesen Kurs.",
|
|
});
|
|
});
|
|
|
|
it("lehnt eine abgelaufene Karte ab", async () => {
|
|
tx.customerPass.findUnique.mockResolvedValue({ ...pass, validUntil: new Date("2000-01-01") });
|
|
expect((await book()).success).toBe(false);
|
|
});
|
|
|
|
it("lehnt mehr Plätze ab, als Einheiten übrig sind", async () => {
|
|
tx.customerPass.findUnique.mockResolvedValue({ ...pass, creditsRemaining: 1 });
|
|
expect(await book({ seats: 2 })).toMatchObject({
|
|
success: false,
|
|
error: "Auf Ihrer Karte ist nur noch 1 Einheit frei.",
|
|
});
|
|
});
|
|
|
|
it("lehnt ab, wenn der Kurs voll ist", async () => {
|
|
vi.mocked(paidSeatCount).mockResolvedValue(10);
|
|
expect(await book()).toMatchObject({
|
|
success: false,
|
|
error: "Dieser Kurs ist bereits ausgebucht.",
|
|
});
|
|
expect(tx.customerPass.updateMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("überschreibt keine frühere, bezahlte Buchung mit ihren Zahlungsbelegen", async () => {
|
|
tx.courseBooking.findUnique.mockResolvedValue({
|
|
paymentStatus: "refunded",
|
|
stripePaymentIntentId: "pi_old",
|
|
});
|
|
expect((await book()).success).toBe(false);
|
|
expect(tx.courseBooking.upsert).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("lehnt einen Platz ab, den gerade jemand anderes im Checkout hält", async () => {
|
|
vi.mocked(getCourseSpots).mockResolvedValue([
|
|
{ id: "r1", type: "reformer", x: 0, y: 0, rotation: 0, label: "Reformer 1" },
|
|
] as never);
|
|
vi.mocked(getBookedSpotIds).mockResolvedValue(new Set());
|
|
vi.mocked(getHeldSpotIds).mockResolvedValue(new Set(["r1"]));
|
|
|
|
const result = await book({ spotIds: ["r1"] });
|
|
|
|
expect(result).toMatchObject({ success: false, spotUnavailable: true });
|
|
expect(tx.customerPass.updateMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("meldet, wenn eine gleichzeitige Buchung die Einheiten schon verbraucht hat", async () => {
|
|
tx.customerPass.updateMany.mockResolvedValue({ count: 0 });
|
|
expect((await book()).success).toBe(false);
|
|
expect(tx.courseBooking.upsert).not.toHaveBeenCalled();
|
|
});
|
|
});
|