Kuma/tests/products/pass-admin-actions.test.ts
2026-10-02 14:30:54 +02:00

206 lines
6.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
const tx = {
customerPass: { updateMany: vi.fn() },
passAdjustment: { create: vi.fn() },
};
vi.mock("@/src/lib/prisma", () => ({
prisma: {
$transaction: vi.fn((fn: (t: typeof tx) => unknown) => fn(tx)),
product: { findFirst: vi.fn() },
user: { findFirst: vi.fn() },
customerPass: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), updateMany: vi.fn() },
courseBooking: { count: vi.fn() },
passAdjustment: { create: vi.fn() },
},
}));
vi.mock("@/src/lib/auth/getUser", () => ({ requireManager: vi.fn() }));
vi.mock("next/cache", () => ({ revalidatePath: vi.fn(), updateTag: vi.fn() }));
vi.mock("@/src/lib/mail/resend", () => ({ sendMail: vi.fn() }));
vi.mock("@/src/lib/stripe-refund", () => ({ refundStripePayment: vi.fn() }));
import {
adjustPassCredits,
issuePass,
refundPass,
} from "@/src/modules/products/actions/pass-admin-actions";
import { prisma } from "@/src/lib/prisma";
import { requireManager } from "@/src/lib/auth/getUser";
import { sendMail } from "@/src/lib/mail/resend";
import { refundStripePayment } from "@/src/lib/stripe-refund";
const purchasedPass = {
id: "pass-1",
userId: "user-1",
status: "active",
source: "purchase",
creditsTotal: 10,
creditsRemaining: 10,
validUntil: null,
stripePaymentIntentId: "pi_1",
stripeAccountId: "acct_studio",
amountPaid: 12500,
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(requireManager).mockResolvedValue({
id: "mgr-1",
businesses: [{ id: "biz-1" }],
} as never);
vi.mocked(prisma.customerPass.findFirst).mockResolvedValue(purchasedPass as never);
vi.mocked(prisma.customerPass.updateMany).mockResolvedValue({ count: 1 } as never);
vi.mocked(prisma.courseBooking.count).mockResolvedValue(0 as never);
vi.mocked(sendMail).mockResolvedValue(undefined as never);
tx.customerPass.updateMany.mockResolvedValue({ count: 1 });
tx.passAdjustment.create.mockResolvedValue({});
});
describe("issuePass", () => {
beforeEach(() => {
vi.mocked(prisma.product.findFirst).mockResolvedValue({
id: "prod-1",
name: "10er-Karte",
sports: ["pilates"],
businessId: "biz-1",
business: { name: "Studio", branding: null },
} as never);
vi.mocked(prisma.customerPass.create).mockResolvedValue({} as never);
});
it("vergibt eine Karte an ein bestehendes Konto, mit eigenem Guthaben und Protokoll", async () => {
vi.mocked(prisma.user.findFirst).mockResolvedValue({ id: "user-1" } as never);
const result = await issuePass({
productId: "prod-1",
email: "Kunde@Example.com",
credits: 6,
validUntil: null,
reason: "Restguthaben vom alten Anbieter",
});
expect(result.success).toBe(true);
const { data } = vi.mocked(prisma.customerPass.create).mock.calls[0][0];
expect(data).toMatchObject({
userId: "user-1",
email: "kunde@example.com",
creditsTotal: 6,
creditsRemaining: 6,
source: "manual",
sports: ["pilates"],
});
expect(data.adjustments).toEqual({
create: expect.objectContaining({ type: "issue", creditsDelta: 6, managerId: "mgr-1" }),
});
});
it("hält die Karte für eine Adresse ohne Konto bereit", async () => {
vi.mocked(prisma.user.findFirst).mockResolvedValue(null);
await issuePass({
productId: "prod-1",
email: "neu@example.com",
credits: 10,
validUntil: "2099-03-14",
reason: "",
});
const { data } = vi.mocked(prisma.customerPass.create).mock.calls[0][0];
expect(data.userId).toBeNull();
expect(data.validUntil).toBeInstanceOf(Date);
});
it("lehnt ein Produkt eines fremden Studios ab", async () => {
vi.mocked(prisma.product.findFirst).mockResolvedValue(null);
const result = await issuePass({
productId: "prod-x",
email: "a@b.de",
credits: 1,
validUntil: null,
reason: "",
});
expect(result.success).toBe(false);
expect(prisma.customerPass.create).not.toHaveBeenCalled();
});
});
describe("adjustPassCredits", () => {
it("verlangt einen Grund", async () => {
const result = await adjustPassCredits({ passId: "pass-1", delta: 1, reason: " " });
expect(result).toEqual({ success: false, error: "Bitte gib einen Grund an." });
});
it("erhöht bei einer Gutschrift auch die Gesamtzahl der Karte", async () => {
await adjustPassCredits({ passId: "pass-1", delta: 2, reason: "Kulanz" });
expect(tx.customerPass.updateMany.mock.calls[0][0].data).toEqual({
creditsRemaining: { increment: 2 },
creditsTotal: { increment: 2 },
});
});
it("zieht nie unter null ab", async () => {
tx.customerPass.updateMany.mockResolvedValue({ count: 0 });
const result = await adjustPassCredits({ passId: "pass-1", delta: -20, reason: "Korrektur" });
expect(tx.customerPass.updateMany.mock.calls[0][0].where).toMatchObject({
creditsRemaining: { gte: 20 },
});
expect(result.success).toBe(false);
expect(tx.passAdjustment.create).not.toHaveBeenCalled();
});
});
describe("refundPass", () => {
it("erstattet eine ungenutzte Karte komplett, die Gebühr trägt das Studio", async () => {
vi.mocked(refundStripePayment).mockResolvedValue({
success: true,
stripeRefundId: "re_1",
amountRefundedCents: 12500,
});
vi.mocked(prisma.customerPass.update).mockResolvedValue({} as never);
const result = await refundPass({ passId: "pass-1" });
expect(result.success).toBe(true);
expect(refundStripePayment).toHaveBeenCalledWith(
expect.objectContaining({ paymentIntentId: "pi_1", refundApplicationFee: false })
);
expect(vi.mocked(refundStripePayment).mock.calls[0][0].amountCents).toBeUndefined();
});
it("erstattet keine Karte, mit der schon gebucht wurde", async () => {
vi.mocked(prisma.courseBooking.count).mockResolvedValue(1 as never);
const result = await refundPass({ passId: "pass-1" });
expect(result.success).toBe(false);
expect(refundStripePayment).not.toHaveBeenCalled();
});
it("erstattet keine vergebene Karte - da floss kein Geld", async () => {
vi.mocked(prisma.customerPass.findFirst).mockResolvedValue({
...purchasedPass,
source: "manual",
stripePaymentIntentId: null,
amountPaid: null,
} as never);
expect((await refundPass({ passId: "pass-1" })).success).toBe(false);
expect(refundStripePayment).not.toHaveBeenCalled();
});
it("setzt die Karte zurück, wenn Stripe die Erstattung ablehnt", async () => {
vi.mocked(refundStripePayment).mockResolvedValue({ success: false, error: "kaputt" });
vi.mocked(prisma.customerPass.update).mockResolvedValue({} as never);
const result = await refundPass({ passId: "pass-1" });
expect(result.success).toBe(false);
expect(prisma.customerPass.update).toHaveBeenCalledWith({
where: { id: "pass-1" },
data: { status: "active", creditsRemaining: 10 },
});
});
});