mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
changed to direct charge and changed designs
This commit is contained in:
parent
b5703aaf8e
commit
55d8838c0e
86 changed files with 5854 additions and 3349 deletions
78
tests/api/stripe/account-session.test.ts
Normal file
78
tests/api/stripe/account-session.test.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("@/src/lib/auth/getUser", () => ({ requireManager: vi.fn() }));
|
||||
vi.mock("@/src/lib/prisma", () => ({
|
||||
prisma: { business: { findUnique: vi.fn() } },
|
||||
}));
|
||||
vi.mock("@/src/lib/stripe", () => ({
|
||||
stripe: { accountSessions: { create: vi.fn() } },
|
||||
}));
|
||||
|
||||
import { POST } from "@/src/app/api/stripe/account-session/route";
|
||||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
import { stripe } from "@/src/lib/stripe";
|
||||
|
||||
function request(body: unknown) {
|
||||
return new Request("http://localhost/api/stripe/account-session", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}) as never;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requireManager).mockResolvedValue({ id: "manager-1" } as never);
|
||||
vi.mocked(stripe.accountSessions.create).mockResolvedValue({
|
||||
client_secret: "accs_secret_1",
|
||||
} as never);
|
||||
});
|
||||
|
||||
describe("POST /api/stripe/account-session", () => {
|
||||
it("gibt das Secret für das eigene Studio heraus", async () => {
|
||||
vi.mocked(prisma.business.findUnique).mockResolvedValue({
|
||||
managerId: "manager-1",
|
||||
stripeAccountId: "acct_studio",
|
||||
} as never);
|
||||
|
||||
const res = await POST(request({ businessId: "biz-1" }));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ clientSecret: "accs_secret_1" });
|
||||
const params = vi.mocked(stripe.accountSessions.create).mock.calls[0][0];
|
||||
expect(params.account).toBe("acct_studio");
|
||||
// Erstattungen nur über Kumasys Stornierung - sonst wüsste die Buchung nichts davon.
|
||||
expect(params.components.disputes_list?.features?.refund_management).toBe(false);
|
||||
expect(params.components.payouts?.features?.external_account_collection).toBe(true);
|
||||
});
|
||||
|
||||
it("weist fremde Studios ab, ohne Stripe zu fragen", async () => {
|
||||
vi.mocked(prisma.business.findUnique).mockResolvedValue({
|
||||
managerId: "manager-2",
|
||||
stripeAccountId: "acct_fremd",
|
||||
} as never);
|
||||
|
||||
const res = await POST(request({ businessId: "biz-2" }));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(stripe.accountSessions.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("meldet ein fehlendes Stripe-Konto verständlich", async () => {
|
||||
vi.mocked(prisma.business.findUnique).mockResolvedValue({
|
||||
managerId: "manager-1",
|
||||
stripeAccountId: null,
|
||||
} as never);
|
||||
|
||||
const res = await POST(request({ businessId: "biz-1" }));
|
||||
|
||||
expect(res.status).toBe(409);
|
||||
expect((await res.json()).error).toMatch(/Stripe-Konto/);
|
||||
});
|
||||
|
||||
it("lehnt Anfragen ohne businessId ab", async () => {
|
||||
const res = await POST(request({}));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
|
@ -19,14 +19,14 @@ vi.mock("@/src/lib/prisma", () => ({
|
|||
|
||||
vi.mock("@/src/lib/stripe", () => ({
|
||||
stripe: { checkout: { sessions: { create: vi.fn() } } },
|
||||
calculatePlatformFeeCents: vi.fn().mockReturnValue(100),
|
||||
getPlatformFeeCents: vi.fn().mockReturnValue(50),
|
||||
eurosToCents: vi.fn().mockReturnValue(2900),
|
||||
}));
|
||||
|
||||
import { getUserData, isUser } from "@/src/lib/auth/getUser";
|
||||
import { isManagerOrEmployeeEmail } from "@/src/lib/auth/account-role-by-email";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
import { stripe } from "@/src/lib/stripe";
|
||||
import { stripe, getPlatformFeeCents } from "@/src/lib/stripe";
|
||||
|
||||
let ipCounter = 0;
|
||||
|
||||
|
|
@ -127,3 +127,106 @@ describe("POST /api/stripe/checkout/course – Gast mit Team-Account-E-Mail (Tic
|
|||
expect(isManagerOrEmployeeEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/stripe/checkout/course – Servicegebühr", () => {
|
||||
it("schlägt die Gebühr als eigene Position auf und behält sie als application_fee ein", async () => {
|
||||
vi.mocked(getUserData).mockResolvedValue({ id: "user-1", email: "kunde@example.com" } as never);
|
||||
vi.mocked(isUser).mockReturnValue(true);
|
||||
vi.mocked(prisma.courseBooking.findUnique).mockResolvedValue(null as never);
|
||||
|
||||
const res = await POST(makeRequest({ courseId: "course-1" }));
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const [args, options] = vi.mocked(stripe.checkout.sessions.create).mock.calls[0] as never as [
|
||||
{
|
||||
line_items: { price_data: { unit_amount: number; product_data: { name: string } } }[];
|
||||
payment_intent_data: { application_fee_amount?: number; transfer_data?: unknown };
|
||||
metadata: Record<string, string>;
|
||||
},
|
||||
{ stripeAccount?: string },
|
||||
];
|
||||
|
||||
// Direct Charge: die Session entsteht auf dem Studio-Account, ohne
|
||||
// Transfer von der Plattform.
|
||||
expect(options).toEqual({ stripeAccount: "acct_123" });
|
||||
expect(args.payment_intent_data.transfer_data).toBeUndefined();
|
||||
|
||||
// Kurs zum vollen Preis + separate Gebührenposition
|
||||
expect(args.line_items).toHaveLength(2);
|
||||
expect(args.line_items[0].price_data.unit_amount).toBe(2900);
|
||||
expect(args.line_items[1].price_data.product_data.name).toBe("Servicegebühr");
|
||||
expect(args.line_items[1].price_data.unit_amount).toBe(50);
|
||||
|
||||
// Kumasy bekommt exakt die Gebühr als application_fee.
|
||||
expect(args.payment_intent_data.application_fee_amount).toBe(50);
|
||||
|
||||
// Der Webhook erkennt daran, dass die Gebühr obendrauf kam.
|
||||
expect(args.metadata.platformFeeModel).toBe("on_top");
|
||||
});
|
||||
|
||||
it("erzeugt keine Gebührenposition, wenn die Gebühr auf 0 konfiguriert ist", async () => {
|
||||
vi.mocked(getPlatformFeeCents).mockReturnValueOnce(0);
|
||||
vi.mocked(getUserData).mockResolvedValue({ id: "user-1", email: "kunde@example.com" } as never);
|
||||
vi.mocked(isUser).mockReturnValue(true);
|
||||
vi.mocked(prisma.courseBooking.findUnique).mockResolvedValue(null as never);
|
||||
|
||||
const res = await POST(makeRequest({ courseId: "course-1" }));
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const args = vi.mocked(stripe.checkout.sessions.create).mock.calls[0][0] as never as {
|
||||
line_items: unknown[];
|
||||
payment_intent_data: { application_fee_amount?: number };
|
||||
};
|
||||
expect(args.line_items).toHaveLength(1);
|
||||
// Ohne Gebühr wird gar keine application_fee gesetzt statt einer von 0.
|
||||
expect(args.payment_intent_data.application_fee_amount).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/stripe/checkout/course – Checkout-Seite", () => {
|
||||
function loggedIn() {
|
||||
vi.mocked(getUserData).mockResolvedValue({ id: "user-1", email: "kunde@example.com" } as never);
|
||||
vi.mocked(isUser).mockReturnValue(true);
|
||||
vi.mocked(prisma.courseBooking.findUnique).mockResolvedValue(null as never);
|
||||
}
|
||||
|
||||
it("verlangt die Rechnungsadresse und trägt das Branding des Studios", async () => {
|
||||
loggedIn();
|
||||
|
||||
const res = await POST(makeRequest({ courseId: "course-1" }));
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const args = vi.mocked(stripe.checkout.sessions.create).mock.calls[0][0] as never as {
|
||||
billing_address_collection: string;
|
||||
branding_settings: Record<string, unknown>;
|
||||
};
|
||||
expect(args.billing_address_collection).toBe("required");
|
||||
// Ohne eigenes Studio-Branding greifen die Kumasy-Farben.
|
||||
expect(args.branding_settings).toMatchObject({
|
||||
display_name: "Studio",
|
||||
background_color: "#1E293B",
|
||||
button_color: "#F4C00C",
|
||||
border_style: "rounded",
|
||||
});
|
||||
});
|
||||
|
||||
it("bucht ohne Branding weiter, wenn Stripe das Branding ablehnt", async () => {
|
||||
loggedIn();
|
||||
vi.mocked(stripe.checkout.sessions.create)
|
||||
.mockRejectedValueOnce(
|
||||
Object.assign(new Error("Invalid logo"), { param: "branding_settings[logo][url]" })
|
||||
)
|
||||
.mockResolvedValueOnce({ url: "https://checkout.stripe.com/ohne-branding" } as never);
|
||||
|
||||
const res = await POST(makeRequest({ courseId: "course-1" }));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.json()).url).toBe("https://checkout.stripe.com/ohne-branding");
|
||||
const retry = vi.mocked(stripe.checkout.sessions.create).mock.calls[1][0] as never as {
|
||||
branding_settings?: unknown;
|
||||
billing_address_collection: string;
|
||||
};
|
||||
expect(retry.branding_settings).toBeUndefined();
|
||||
expect(retry.billing_address_collection).toBe("required");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
281
tests/api/stripe/webhook.test.ts
Normal file
281
tests/api/stripe/webhook.test.ts
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("@/src/lib/stripe", () => ({
|
||||
stripe: {
|
||||
webhooks: { constructEvent: vi.fn() },
|
||||
paymentIntents: { retrieve: vi.fn() },
|
||||
},
|
||||
getWebhookSecrets: () => ["whsec_platform", "whsec_connect"],
|
||||
onAccount: (id: string | null) => (id ? { stripeAccount: id } : undefined),
|
||||
}));
|
||||
vi.mock("@/src/lib/stripe-refund", () => ({ refundStripePayment: vi.fn() }));
|
||||
vi.mock("@/src/lib/stripe-fee", () => ({
|
||||
recordStripeFee: vi.fn(),
|
||||
recordStripeFeeForCharge: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/src/lib/prisma", () => ({
|
||||
prisma: {
|
||||
$transaction: vi.fn(),
|
||||
user: { findUnique: vi.fn(), findFirst: vi.fn() },
|
||||
course: { findUnique: vi.fn() },
|
||||
courseBooking: { findUnique: vi.fn(), findFirst: vi.fn(), update: vi.fn(), count: vi.fn() },
|
||||
courseWaitlistEntry: { deleteMany: vi.fn() },
|
||||
business: { findUnique: vi.fn(), update: vi.fn() },
|
||||
},
|
||||
}));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn(), revalidateTag: vi.fn() }));
|
||||
vi.mock("@/src/lib/mail/resend", () => ({ sendMail: vi.fn() }));
|
||||
vi.mock("@/src/lib/mail/templates/booking-confirmation", () => ({
|
||||
generateBookingConfirmationEmail: () => ({ subject: "s", html: "h", text: "t" }),
|
||||
}));
|
||||
vi.mock("@/src/lib/mail/templates/booking-refund", () => ({
|
||||
generateBookingRefundEmail: () => ({ subject: "s", html: "h", text: "t" }),
|
||||
}));
|
||||
vi.mock("@/src/modules/settings/lib/to-email-branding", () => ({ toEmailBranding: () => ({}) }));
|
||||
vi.mock("@/src/modules/notifications/lib/create-notification", () => ({
|
||||
notifyBusinessManager: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/src/lib/auth/account-role-by-email", () => ({
|
||||
isManagerOrEmployeeEmail: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/src/modules/settings/lib/discount-validation", () => ({
|
||||
confirmDiscountReservation: vi.fn(),
|
||||
releaseDiscountReservation: vi.fn(),
|
||||
}));
|
||||
|
||||
import { POST } from "@/src/app/api/stripe/webhook/route";
|
||||
import { stripe } from "@/src/lib/stripe";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
import { refundStripePayment } from "@/src/lib/stripe-refund";
|
||||
import { recordStripeFee, recordStripeFeeForCharge } from "@/src/lib/stripe-fee";
|
||||
import { isManagerOrEmployeeEmail } from "@/src/lib/auth/account-role-by-email";
|
||||
import { releaseDiscountReservation } from "@/src/modules/settings/lib/discount-validation";
|
||||
|
||||
// Die Datenbank-Transaktion der Kapazitätsprüfung.
|
||||
const tx = {
|
||||
courseBooking: { findUnique: vi.fn(), findFirst: vi.fn(), count: vi.fn(), upsert: vi.fn() },
|
||||
course: { findUnique: vi.fn() },
|
||||
};
|
||||
|
||||
function request() {
|
||||
return new Request("http://localhost/api/stripe/webhook", {
|
||||
method: "POST",
|
||||
headers: { "stripe-signature": "t=1,v1=abc" },
|
||||
body: "{}",
|
||||
}) as never;
|
||||
}
|
||||
|
||||
/** Ein Event, das nur mit dem angegebenen Secret als gültig signiert gilt. */
|
||||
function deliver(event: object, signedWith = "whsec_platform") {
|
||||
vi.mocked(stripe.webhooks.constructEvent).mockImplementation((_body, _sig, secret) => {
|
||||
if (secret !== signedWith)
|
||||
throw new Error("No signatures found matching the expected signature");
|
||||
return event as never;
|
||||
});
|
||||
return POST(request());
|
||||
}
|
||||
|
||||
function sessionEvent(
|
||||
type: string,
|
||||
opts: { account?: string; paymentStatus?: string; metadata?: Record<string, string> } = {}
|
||||
) {
|
||||
return {
|
||||
type,
|
||||
account: opts.account,
|
||||
data: {
|
||||
object: {
|
||||
id: "cs_1",
|
||||
payment_status: opts.paymentStatus ?? "paid",
|
||||
amount_total: 2950,
|
||||
payment_intent: "pi_1",
|
||||
customer_email: "kunde@example.com",
|
||||
customer_details: { email: "kunde@example.com", name: "Kunde" },
|
||||
metadata: {
|
||||
courseId: "course-1",
|
||||
userId: "user-1",
|
||||
isGuest: "false",
|
||||
platformFeeModel: "on_top",
|
||||
...opts.metadata,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
vi.mocked(prisma.$transaction).mockImplementation(((callback: (t: typeof tx) => unknown) =>
|
||||
callback(tx)) as never);
|
||||
tx.courseBooking.findUnique.mockResolvedValue(null);
|
||||
tx.courseBooking.findFirst.mockResolvedValue(null);
|
||||
tx.courseBooking.count.mockResolvedValue(0);
|
||||
tx.courseBooking.upsert.mockResolvedValue({ id: "booking-1" });
|
||||
tx.course.findUnique.mockResolvedValue({ maxParticipants: 10 });
|
||||
|
||||
vi.mocked(stripe.paymentIntents.retrieve).mockResolvedValue({
|
||||
application_fee_amount: 50,
|
||||
} as never);
|
||||
vi.mocked(isManagerOrEmployeeEmail).mockResolvedValue(false);
|
||||
vi.mocked(recordStripeFee).mockResolvedValue(undefined);
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: "user-1" } as never);
|
||||
vi.mocked(prisma.course.findUnique).mockResolvedValue({
|
||||
name: "Yoga",
|
||||
date: new Date("2099-01-01"),
|
||||
timeFrom: "10:00",
|
||||
timeTo: "11:00",
|
||||
coverImage: null,
|
||||
businessId: "biz-1",
|
||||
maxParticipants: 10,
|
||||
business: { name: "Studio", address: "Weg 1", branding: null },
|
||||
} as never);
|
||||
vi.mocked(prisma.courseBooking.findUnique).mockResolvedValue({ userId: "user-1" } as never);
|
||||
vi.mocked(prisma.courseBooking.count).mockResolvedValue(1 as never);
|
||||
vi.mocked(refundStripePayment).mockResolvedValue({
|
||||
success: true,
|
||||
stripeRefundId: "re_1",
|
||||
amountRefundedCents: 2950,
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stripe-Webhook – Signatur", () => {
|
||||
it("akzeptiert Events vom Connect-Endpoint mit dessen eigenem Secret", async () => {
|
||||
const res = await deliver({ type: "customer.created", data: { object: {} } }, "whsec_connect");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(stripe.webhooks.constructEvent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("lehnt ab, wenn kein Secret passt", async () => {
|
||||
const res = await deliver({ type: "customer.created", data: { object: {} } }, "whsec_fremd");
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stripe-Webhook – Buchung", () => {
|
||||
it("bucht eine Direct Charge auf dem Studio-Account mit Gebühr obendrauf", async () => {
|
||||
const res = await deliver(
|
||||
sessionEvent("checkout.session.completed", { account: "acct_studio" }),
|
||||
"whsec_connect"
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
// Die application_fee liegt am PaymentIntent des Studio-Accounts.
|
||||
expect(stripe.paymentIntents.retrieve).toHaveBeenCalledWith("pi_1", undefined, {
|
||||
stripeAccount: "acct_studio",
|
||||
});
|
||||
expect(tx.courseBooking.upsert.mock.calls[0][0].create).toMatchObject({
|
||||
paymentStatus: "paid",
|
||||
stripeSessionId: "cs_1",
|
||||
stripePaymentIntentId: "pi_1",
|
||||
stripeAccountId: "acct_studio",
|
||||
amountPaid: 2950,
|
||||
platformFee: 50,
|
||||
platformFeeOnTop: true,
|
||||
});
|
||||
expect(recordStripeFee).toHaveBeenCalledWith("pi_1", "acct_studio");
|
||||
});
|
||||
|
||||
it("speichert die Rechnungsadresse aus dem Checkout an der Buchung", async () => {
|
||||
const event = sessionEvent("checkout.session.completed", { account: "acct_studio" });
|
||||
(event.data.object as Record<string, unknown>).customer_details = {
|
||||
email: "kunde@example.com",
|
||||
name: "Erika Mustermann",
|
||||
address: {
|
||||
line1: "Sandweg 91",
|
||||
line2: null,
|
||||
postal_code: "60316",
|
||||
city: "Frankfurt",
|
||||
country: "DE",
|
||||
},
|
||||
};
|
||||
|
||||
await deliver(event, "whsec_connect");
|
||||
|
||||
expect(tx.courseBooking.upsert.mock.calls[0][0].create.billingAddress).toEqual({
|
||||
name: "Erika Mustermann",
|
||||
line1: "Sandweg 91",
|
||||
line2: null,
|
||||
postalCode: "60316",
|
||||
city: "Frankfurt",
|
||||
country: "DE",
|
||||
});
|
||||
});
|
||||
|
||||
it("verbucht eine Session von vor der Umstellung als Altbuchung", async () => {
|
||||
// Prozentuale Gebühr, Destination Charge auf der Plattform: kein
|
||||
// event.account, kein platformFeeModel in den Metadaten.
|
||||
const event = sessionEvent("checkout.session.completed");
|
||||
delete (event.data.object.metadata as Record<string, string>).platformFeeModel;
|
||||
|
||||
await deliver(event);
|
||||
|
||||
expect(stripe.paymentIntents.retrieve).toHaveBeenCalledWith("pi_1", undefined, undefined);
|
||||
expect(tx.courseBooking.upsert.mock.calls[0][0].create).toMatchObject({
|
||||
stripeAccountId: null,
|
||||
platformFeeOnTop: false,
|
||||
});
|
||||
// Bei Altbuchungen trug die Plattform die Stripe-Gebühr.
|
||||
expect(recordStripeFee).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("bucht SEPA erst, wenn die verzögerte Zahlung angekommen ist", async () => {
|
||||
await deliver(
|
||||
sessionEvent("checkout.session.completed", {
|
||||
account: "acct_studio",
|
||||
paymentStatus: "unpaid",
|
||||
}),
|
||||
"whsec_connect"
|
||||
);
|
||||
expect(prisma.$transaction).not.toHaveBeenCalled();
|
||||
|
||||
await deliver(
|
||||
sessionEvent("checkout.session.async_payment_succeeded", { account: "acct_studio" }),
|
||||
"whsec_connect"
|
||||
);
|
||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("erstattet eine Überbuchung auf dem Studio-Account und gibt die Gebühr ans Studio zurück", async () => {
|
||||
tx.courseBooking.count.mockResolvedValue(10);
|
||||
|
||||
await deliver(
|
||||
sessionEvent("checkout.session.completed", { account: "acct_studio" }),
|
||||
"whsec_connect"
|
||||
);
|
||||
|
||||
expect(refundStripePayment).toHaveBeenCalledWith({
|
||||
paymentIntentId: "pi_1",
|
||||
stripeAccountId: "acct_studio",
|
||||
refundApplicationFee: true,
|
||||
});
|
||||
expect(recordStripeFee).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stripe-Webhook – nie bezahlt und Gebühren", () => {
|
||||
it("gibt bei gescheiterter SEPA-Zahlung die Rabatt-Reservierung frei", async () => {
|
||||
await deliver(
|
||||
sessionEvent("checkout.session.async_payment_failed", {
|
||||
account: "acct_studio",
|
||||
paymentStatus: "unpaid",
|
||||
metadata: { discountReservationToken: "tok_1" },
|
||||
}),
|
||||
"whsec_connect"
|
||||
);
|
||||
|
||||
expect(releaseDiscountReservation).toHaveBeenCalledWith(prisma, "tok_1");
|
||||
});
|
||||
|
||||
it("trägt die Stripe-Gebühr aus charge.updated nach - nur für Direct Charges", async () => {
|
||||
const charge = { payment_intent: "pi_1", balance_transaction: "txn_1" };
|
||||
|
||||
await deliver({ type: "charge.updated", account: "acct_studio", data: { object: charge } });
|
||||
expect(recordStripeFeeForCharge).toHaveBeenCalledWith(charge, "acct_studio");
|
||||
|
||||
vi.mocked(recordStripeFeeForCharge).mockClear();
|
||||
await deliver({ type: "charge.updated", data: { object: charge } });
|
||||
expect(recordStripeFeeForCharge).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue