mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
206 lines
6.9 KiB
TypeScript
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 },
|
|
});
|
|
});
|
|
});
|