Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/services/billing/payment-methods.server.test.ts
T

166 lines
6.6 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it, vi } from "vitest";
import type { CustomerPaymentGateway, MolliePaymentMethodId } from "@hivegrid/billing";
import {
assertCheckoutPaymentMethodAvailable,
CheckoutPaymentMethodError,
loadCheckoutPaymentMethods,
toMollieLocale,
} from "./payment-methods.server";
function gateway(methods: Array<{ id: MolliePaymentMethodId; description: string }>): CustomerPaymentGateway {
return {
createCustomer: vi.fn(),
createPayment: vi.fn(),
getPayment: vi.fn(),
findCustomerPaymentByExternalId: vi.fn(),
listPaymentMethods: vi.fn(async () => methods.map((method) => ({ provider: "mollie" as const, ...method }))),
};
}
const input = {
amountCents: 12_900,
currency: "EUR",
sequenceType: "first" as const,
locale: "de",
};
describe("checkout payment methods", () => {
it("maps app locales to supported Mollie locales", () => {
expect(toMollieLocale("de-DE")).toBe("de_DE");
expect(toMollieLocale("en")).toBe("en_GB");
expect(toMollieLocale("pl-PL")).toBe("pl_PL");
expect(toMollieLocale("hu-HU")).toBe("hu_HU");
expect(toMollieLocale("ru-RU")).toBeNull();
expect(toMollieLocale("uk-UA")).toBeNull();
});
it("builds a provider-backed choice list and falls back when discovery fails", async () => {
const provider = gateway([
{ id: "ideal", description: "iDEAL" },
{ id: "creditcard", description: "Credit card" },
]);
await expect(loadCheckoutPaymentMethods(input, { gateway: provider })).resolves.toEqual({
status: "ready",
methods: [
{ id: "ideal", label: "iDEAL" },
{ id: "creditcard", label: "Credit card" },
],
});
expect(provider.listPaymentMethods).toHaveBeenCalledWith({
amountCents: 12_900,
currency: "EUR",
sequenceType: "first",
locale: "de_DE",
});
const unavailable = gateway([]);
unavailable.listPaymentMethods = vi.fn(async () => {
throw new Error("provider unavailable");
});
await expect(loadCheckoutPaymentMethods(input, { gateway: unavailable })).resolves.toEqual({ status: "fallback", methods: [] });
});
it("accepts only a currently available provider method", async () => {
const provider = gateway([{ id: "paypal", description: "PayPal" }]);
await expect(assertCheckoutPaymentMethodAvailable(" PAYPAL ", input, { gateway: provider })).resolves.toBe("paypal");
await expect(assertCheckoutPaymentMethodAvailable("ideal", input, { gateway: provider })).rejects.toEqual(
new CheckoutPaymentMethodError("PAYMENT_METHOD_UNAVAILABLE")
);
await expect(assertCheckoutPaymentMethodAvailable(null, input, { gateway: provider })).resolves.toBeNull();
});
it("filters annual SEPA authorization methods before returning or accepting them", async () => {
const provider = gateway([
{ id: "ideal", description: "iDEAL" },
{ id: "creditcard", description: "Credit card" },
]);
const annualInput = { ...input, requirement: "sepa" as const };
await expect(loadCheckoutPaymentMethods(annualInput, { gateway: provider })).resolves.toEqual({
status: "ready",
methods: [{ id: "ideal", label: "iDEAL" }],
});
await expect(assertCheckoutPaymentMethodAvailable("creditcard", annualInput, { gateway: provider })).rejects.toMatchObject({
code: "PAYMENT_METHOD_UNAVAILABLE",
});
});
it("requires an available credit card above EUR 1,000 and selects it server-side", async () => {
const provider = gateway([
{ id: "ideal", description: "iDEAL" },
{ id: "creditcard", description: "Credit card" },
]);
const highValueInput = { ...input, amountCents: 100_001, requirement: "sepa" as const };
await expect(loadCheckoutPaymentMethods(highValueInput, { gateway: provider })).resolves.toEqual({
status: "ready",
methods: [{ id: "creditcard", label: "Credit card" }],
});
await expect(assertCheckoutPaymentMethodAvailable(null, highValueInput, { gateway: provider })).resolves.toBe("creditcard");
const cardUnavailable = gateway([{ id: "ideal", description: "iDEAL" }]);
await expect(loadCheckoutPaymentMethods(highValueInput, { gateway: cardUnavailable })).resolves.toEqual({
status: "required_card_unavailable",
methods: [],
});
await expect(assertCheckoutPaymentMethodAvailable(null, highValueInput, { gateway: cardUnavailable })).rejects.toMatchObject({
code: "PAYMENT_METHOD_UNAVAILABLE",
});
});
it("keeps SEPA available at exactly EUR 1,000", async () => {
const provider = gateway([
{ id: "ideal", description: "iDEAL" },
{ id: "creditcard", description: "Credit card" },
]);
await expect(loadCheckoutPaymentMethods({ ...input, amountCents: 100_000, requirement: "sepa" }, { gateway: provider })).resolves.toEqual({
status: "ready",
methods: [{ id: "ideal", label: "iDEAL" }],
});
});
it("uses the future recurring amount for a zero-value mandate payment", async () => {
const provider = gateway([
{ id: "ideal", description: "iDEAL" },
{ id: "creditcard", description: "Credit card" },
]);
await expect(
loadCheckoutPaymentMethods({ ...input, amountCents: 0, policyAmountCents: 100_001, requirement: "sepa" }, { gateway: provider })
).resolves.toEqual({
status: "ready",
methods: [{ id: "creditcard", label: "Credit card" }],
});
expect(provider.listPaymentMethods).toHaveBeenCalledWith({
amountCents: 0,
currency: "EUR",
sequenceType: "first",
locale: "de_DE",
});
});
it("requires a card for every zero-value first payment even below the high-value threshold", async () => {
const provider = gateway([
{ id: "ideal", description: "iDEAL" },
{ id: "creditcard", description: "Credit card" },
]);
const zeroValueInput = { ...input, amountCents: 0, policyAmountCents: 12_900, requirement: "any" as const };
await expect(loadCheckoutPaymentMethods(zeroValueInput, { gateway: provider })).resolves.toEqual({
status: "ready",
methods: [{ id: "creditcard", label: "Credit card" }],
});
await expect(assertCheckoutPaymentMethodAvailable(null, zeroValueInput, { gateway: provider })).resolves.toBe("creditcard");
const idealOnly = gateway([{ id: "ideal", description: "iDEAL" }]);
await expect(loadCheckoutPaymentMethods(zeroValueInput, { gateway: idealOnly })).resolves.toEqual({
status: "required_card_unavailable",
methods: [],
});
await expect(assertCheckoutPaymentMethodAvailable(null, zeroValueInput, { gateway: idealOnly })).rejects.toMatchObject({
code: "PAYMENT_METHOD_UNAVAILABLE",
});
});
});