first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
# MollieProvider
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MollieProvider` is the sibling adapter that implements the
`PaymentProvider` contract from `@suaveplan/payment` against the Mollie
Payments API v2. It is the only `@suaveplan/payment-provider-*` sibling
that has to marshal money amounts to and from a string representation:
every Mollie request and response carries amounts as
`{ currency: "EUR", value: "10.00" }`, while the rest of the suaveplan
payment family models money as `bigint` minor-units. This package
owns that round-trip end-to-end and exposes the underlying helpers
(`formatMollieAmount`, `parseMollieAmount`) so callers can test or reuse
them in their own integrations.
## Features
- `MollieProvider` class implementing `PaymentProvider` with name
`"mollie"`.
- Constructor takes `ApiKeyConfig` from `@suaveplan/payment-providers`
(`apiKey`, optional `baseUrl`, `timeoutMs`, `fetch`, `signal`,
`webhookSecret`).
- Auto-sets `Authorization: Bearer <apiKey>` on every call.
- Uses the slim-core `httpRequest` helper for JSON serialisation,
per-call timeout, external `AbortSignal` composition, and typed
error surfacing.
- Defaults `description` to `"Payment"` and `redirectUrl` to
`"https://localhost/return"` when omitted, matching upstream
behaviour.
- `confirmIntent` and `captureIntent` are read-throughs to `getIntent`
because Mollie auto-captures.
- Pure, exported amount helpers (`formatMollieAmount`,
`parseMollieAmount`) and status maps (`mapMollieStatus`,
`mapMollieRefundStatus`) — all individually unit-testable.
- All failures surface as `PaymentProviderError` carrying a
`PAYMENT_PROVIDER_*` code; raw `Error` is never thrown.
## Usage: Basic Example
```ts
import { ProviderRegistry } from "@suaveplan/payment-providers";
import { MollieProvider } from "@suaveplan/payment-provider-mollie";
const registry = new ProviderRegistry();
registry.register(new MollieProvider({ apiKey: process.env.MOLLIE_API_KEY ?? "" }));
const mollie = registry.get("mollie");
const intent = await mollie.createIntent({
amount: { amount: 5000n, currency: "EUR" },
description: "Order #42",
returnUrl: "https://shop.example/return",
});
console.log(intent.id, intent.status);
```
## Usage: Advanced Example
```ts
import { MollieProvider, formatMollieAmount, parseMollieAmount } from "@suaveplan/payment-provider-mollie";
import type { FetchLike } from "@suaveplan/payment-providers";
// Inject a recording FetchLike for tests — no global monkey-patching.
const recorded: { url: string; init?: RequestInit }[] = [];
const recordingFetch: FetchLike = async (url, init) => {
recorded.push({ url, init });
return new Response(JSON.stringify({
id: "tr_xyz",
status: "paid",
amount: { value: "12.34", currency: "EUR" },
createdAt: "2026-01-01T00:00:00+00:00",
}), { status: 200, headers: { "content-type": "application/json" } });
};
const provider = new MollieProvider({
apiKey: "test_xyz",
baseUrl: "https://mollie.mock/v2",
timeoutMs: 5_000,
fetch: recordingFetch,
});
// Round-trip amount marshalling without hitting the network:
const wire = formatMollieAmount(1234n); // "12.34"
const back = parseMollieAmount(wire); // 1234n
// Drive the full lifecycle for assertions:
const intent = await provider.createIntent({
amount: { amount: 1234n, currency: "EUR" },
});
const captured = await provider.captureIntent(intent.id); // read-through
const refund = await provider.refund({
paymentIntentId: intent.id,
amount: { amount: 1234n, currency: "EUR" },
});
```
## API Reference
### `MollieProvider`
- `new MollieProvider(config: ApiKeyConfig)` — constructs the adapter.
`config.apiKey` is required; `baseUrl`, `timeoutMs`, `fetch`,
`signal`, and `webhookSecret` are optional. `baseUrl` defaults to
`MOLLIE_DEFAULT_BASE_URL`; `timeoutMs` defaults to `30_000`.
- `name: "mollie"` — literal-typed instance field.
- `createIntent(input: CreatePaymentIntentInput): Promise<PaymentIntent>` —
POST `${baseUrl}/payments`. Defaults `description` and `redirectUrl`
when omitted.
- `confirmIntent(intentId: string): Promise<PaymentIntent>` —
read-through to `getIntent`.
- `captureIntent(intentId: string): Promise<PaymentIntent>` —
read-through to `getIntent` (Mollie auto-captures).
- `cancelIntent(intentId: string): Promise<PaymentIntent>` — DELETE
`${baseUrl}/payments/${intentId}`.
- `refund(input: RefundInput): Promise<Refund>` — POST
`${baseUrl}/payments/${intentId}/refunds`. Omits the body's `amount`
field when `input.amount` is undefined (Mollie issues a full refund).
- `getIntent(intentId: string): Promise<PaymentIntent>` — GET
`${baseUrl}/payments/${intentId}`.
- Throws: `PaymentProviderError` with code
`PAYMENT_PROVIDER.HTTP_ERROR` / `PAYMENT_PROVIDER.TIMEOUT` /
`PAYMENT_PROVIDER.PARSE_ERROR`.
### `MOLLIE_DEFAULT_BASE_URL: "https://api.mollie.com/v2"`
Production Mollie REST v2 base URL constant.
### `MOLLIE_PROVIDER_NAME: "mollie"`
Stable provider identifier.
### `formatMollieAmount(amount: bigint): string`
Format a minor-units `bigint` into a two-decimal string. Always
produces exactly two minor digits, pads left for sub-major values
(`1n` → `"0.01"`), and supports negative values.
### `parseMollieAmount(value: string): bigint`
Parse a Mollie wire-format decimal string into a minor-units `bigint`.
Accepts no-decimal, single-decimal, two-decimal, or over-precision
inputs; over-precision is truncated, never rounded.
### `mapMollieStatus(status: string): PaymentStatus`
Total map from Mollie's payment-status string to the canonical
`PaymentStatus` union. Unknown values fall back to `"failed"`.
### `mapMollieRefundStatus(status: string): RefundStatus`
Total map from Mollie's refund-status string to the canonical
`RefundStatus` union. Unknown values fall back to `"failed"`.
## Implementation Notes
- The package is universal — no Node-specific or browser-specific
globals. The only runtime concept beyond the language standard is
the WHATWG `fetch` surface, available in every supported runtime.
- Tests inject a recording `FetchLike` closure into the constructor
instead of mutating `globalThis.fetch`. This satisfies rule 8 (no
mocks) and matches the binance / ecb sibling pattern.
- `confirmIntent` and `captureIntent` are deliberate read-throughs to
`getIntent`: Mollie auto-captures, so there is no separate confirm or
capture step at the protocol level. Returning the current state
preserves the `PaymentProvider` contract without falsifying the
underlying behaviour.
- `formatMollieAmount` and `parseMollieAmount` are inverses for every
`bigint` produced from a Mollie response. Round-trip is exact for
zero, single cent, exact major unit, and large values up to
`2^53`-and-beyond range (`bigint` is arbitrary-precision; the helpers
never go through `number`).
- Over-precision in `parseMollieAmount` (`"10.123"` → `1012n`) is
truncation, not rounding. Mollie emits exactly minor-unit precision;
over-precision is upstream garbage and the safest action is to
ignore the excess rather than to bias the result.
- The card-brand normalisation accepts any of `visa` / `mastercard` /
`amex` / `discover` / `diners` / `jcb` / `unionpay`; every other
label collapses to `"unknown"` rather than leaking arbitrary strings
into the `CardBrand` union.
- Forwarded `AbortSignal` composition (caller `signal` + per-call
timeout) is handled by the slim-core `httpRequest`; this package
does not re-implement it.