166 lines
6.6 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
});
|