mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
added card creation and booking functionality
This commit is contained in:
parent
86330af16e
commit
7aa887c5aa
85 changed files with 7434 additions and 314 deletions
|
|
@ -9,6 +9,10 @@ vi.mock("@/src/modules/courses/actions/booking-actions", () => ({
|
|||
vi.mock("@/src/modules/courses/actions/waitlist-actions", () => ({
|
||||
joinWaitlistAsUser: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/src/modules/products/actions/pass-booking-actions", () => ({
|
||||
bookCourseWithPass: vi.fn(),
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh: vi.fn() }) }));
|
||||
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||
|
||||
import { BookingDialog } from "@/src/modules/courses/ui/components/BookingDialog";
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ function renderView(initialCourses = [...thisWeek, ...later], initialWindow = wi
|
|||
initialWindow={initialWindow}
|
||||
serviceFeeCents={50}
|
||||
cancellationSummary="Kostenlos stornierbar bis Kursbeginn."
|
||||
products={[]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
158
tests/courses/cancel-pass-booking.test.ts
Normal file
158
tests/courses/cancel-pass-booking.test.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
86
tests/courses/delete-course.test.ts
Normal file
86
tests/courses/delete-course.test.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("@/src/lib/prisma", () => ({
|
||||
prisma: {
|
||||
course: { findUnique: vi.fn(), delete: vi.fn(), deleteMany: vi.fn() },
|
||||
courseBooking: { count: vi.fn() },
|
||||
},
|
||||
}));
|
||||
vi.mock("@/src/lib/auth/getUser", () => ({
|
||||
getUserData: vi.fn(),
|
||||
isManager: () => true,
|
||||
isEmployee: () => false,
|
||||
getEffectiveManagerId: (user: { id: string }) => user.id,
|
||||
}));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||
vi.mock("@/src/modules/courses/lib/public-courses-cache", () => ({
|
||||
refreshPublicCourses: vi.fn(),
|
||||
}));
|
||||
|
||||
import { deleteCourse } from "@/src/modules/courses/actions/course-actions";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
import { getUserData } from "@/src/lib/auth/getUser";
|
||||
|
||||
const instance = {
|
||||
id: "course-2",
|
||||
createdBy: "mgr-1",
|
||||
parentCourseId: "template-1",
|
||||
date: new Date("2099-01-08"),
|
||||
businessId: "biz-1",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getUserData).mockResolvedValue({ id: "mgr-1", role: "manager" } as never);
|
||||
vi.mocked(prisma.course.findUnique).mockResolvedValue(instance as never);
|
||||
vi.mocked(prisma.courseBooking.count).mockResolvedValue(0 as never);
|
||||
});
|
||||
|
||||
describe("deleteCourse – Buchungen mit Zahlungen", () => {
|
||||
it("löscht keinen Kurs mit bezahlten Buchungen oder Rechnungen", async () => {
|
||||
vi.mocked(prisma.courseBooking.count).mockResolvedValue(2 as never);
|
||||
|
||||
const result = await deleteCourse("course-2");
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(prisma.course.delete).not.toHaveBeenCalled();
|
||||
const { where } = vi.mocked(prisma.courseBooking.count).mock.calls[0][0]!;
|
||||
expect(where).toMatchObject({
|
||||
course: { id: "course-2" },
|
||||
OR: [
|
||||
{ paymentStatus: { in: ["paid", "refunding", "partially_refunded"] } },
|
||||
{ invoiceNumber: { not: null } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("prüft bei 'diesen und folgende' alle betroffenen Termine", async () => {
|
||||
await deleteCourse("course-2", "this_and_following");
|
||||
|
||||
expect(vi.mocked(prisma.courseBooking.count).mock.calls[0][0]?.where?.course).toEqual({
|
||||
parentCourseId: "template-1",
|
||||
date: { gte: instance.date },
|
||||
});
|
||||
expect(prisma.course.deleteMany).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prüft beim Löschen einer Vorlage auch alle Termine, die mitgelöscht werden", async () => {
|
||||
vi.mocked(prisma.course.findUnique).mockResolvedValue({
|
||||
...instance,
|
||||
id: "template-1",
|
||||
parentCourseId: null,
|
||||
} as never);
|
||||
|
||||
await deleteCourse("template-1");
|
||||
|
||||
expect(vi.mocked(prisma.courseBooking.count).mock.calls[0][0]?.where?.course).toEqual({
|
||||
OR: [{ id: "template-1" }, { parentCourseId: "template-1" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("löscht Kurse ohne solche Buchungen wie bisher", async () => {
|
||||
const result = await deleteCourse("course-2");
|
||||
expect(result.success).toBe(true);
|
||||
expect(prisma.course.delete).toHaveBeenCalledWith({ where: { id: "course-2" } });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue