Kuma/tests/lib/stripe-fee.test.ts
2026-10-01 23:28:32 +02:00

116 lines
3.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("@/src/lib/stripe", () => ({
stripe: {
paymentIntents: { retrieve: vi.fn() },
balanceTransactions: { retrieve: vi.fn() },
},
}));
vi.mock("@/src/lib/prisma", () => ({
prisma: { courseBooking: { updateMany: vi.fn(), count: vi.fn() } },
}));
import {
recordStripeFee,
recordStripeFeeForCharge,
stripeProcessingFeeCents,
} from "@/src/lib/stripe-fee";
import { stripe } from "@/src/lib/stripe";
import { prisma } from "@/src/lib/prisma";
// Echte Form vom Test-Account: fee=171 enthält BEIDE Abzüge.
const balanceTransaction = {
fee: 171,
fee_details: [
{ type: "stripe_fee", amount: 121 },
{ type: "application_fee", amount: 50 },
],
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(prisma.courseBooking.updateMany).mockResolvedValue({ count: 1 } as never);
});
describe("stripeProcessingFeeCents", () => {
it("zählt unsere application_fee nicht als Stripe-Gebühr", () => {
// Mit bt.fee stünden hier 171 - 50 Cent zu viel im Zahlungen-Tab.
expect(stripeProcessingFeeCents(balanceTransaction as never)).toBe(121);
});
it("rechnet weitere Stripe-Abzüge wie Steuern mit", () => {
const withTax = {
fee_details: [...balanceTransaction.fee_details, { type: "tax", amount: 23 }],
};
expect(stripeProcessingFeeCents(withTax as never)).toBe(144);
});
});
describe("recordStripeFee", () => {
it("liest die Gebühr auf dem Studio-Account und speichert sie nur einmal", async () => {
vi.mocked(stripe.paymentIntents.retrieve).mockResolvedValue({
latest_charge: { balance_transaction: balanceTransaction },
} as never);
await recordStripeFee("pi_1", "acct_studio");
expect(stripe.paymentIntents.retrieve).toHaveBeenCalledWith(
"pi_1",
{ expand: ["latest_charge.balance_transaction"] },
{ stripeAccount: "acct_studio" }
);
expect(prisma.courseBooking.updateMany).toHaveBeenCalledWith({
// stripeFee: null - ein zweites Event darf den Wert nicht überschreiben.
where: { stripePaymentIntentId: "pi_1", stripeFee: null },
data: { stripeFee: 121 },
});
});
it("tut nichts, solange Stripe die Zahlung nicht abgerechnet hat", async () => {
vi.mocked(stripe.paymentIntents.retrieve).mockResolvedValue({
latest_charge: { balance_transaction: null },
} as never);
await recordStripeFee("pi_1", "acct_studio");
expect(prisma.courseBooking.updateMany).not.toHaveBeenCalled();
});
});
describe("recordStripeFeeForCharge", () => {
const charge = { payment_intent: "pi_1", balance_transaction: "txn_1" };
it("trägt die Gebühr nach, wenn eine Buchung auf sie wartet", async () => {
vi.mocked(prisma.courseBooking.count).mockResolvedValue(1 as never);
vi.mocked(stripe.balanceTransactions.retrieve).mockResolvedValue(balanceTransaction as never);
await recordStripeFeeForCharge(charge as never, "acct_studio");
expect(stripe.balanceTransactions.retrieve).toHaveBeenCalledWith("txn_1", undefined, {
stripeAccount: "acct_studio",
});
expect(prisma.courseBooking.updateMany).toHaveBeenCalledWith({
where: { stripePaymentIntentId: "pi_1", stripeFee: null },
data: { stripeFee: 121 },
});
});
it("fragt Stripe gar nicht erst, wenn keine Buchung wartet", async () => {
// charge.updated feuert auch bei Erstattungen - dann ist die Gebühr längst da.
vi.mocked(prisma.courseBooking.count).mockResolvedValue(0 as never);
await recordStripeFeeForCharge(charge as never, "acct_studio");
expect(stripe.balanceTransactions.retrieve).not.toHaveBeenCalled();
});
it("ignoriert Charges ohne Abrechnung", async () => {
await recordStripeFeeForCharge(
{ payment_intent: "pi_1", balance_transaction: null } as never,
"acct_studio"
);
expect(prisma.courseBooking.count).not.toHaveBeenCalled();
});
});