Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
# Add @suaveplan/payment-provider-mollie — sibling Mollie adapter on the slim core
**Package**: `@suaveplan/payment-provider-mollie`
**Status**: PROPOSED
**Type**: new package (genesis-original, follow-up to the slim refactor)
- **Design**: ./design.md
- **Spec delta**: ./specs/payment-provider-mollie/spec.md
## Why
The slim refactor (`openspec/archive/2026-05-17-slim-payment-providers/`)
removed `MollieProvider` from `@suaveplan/payment-providers` and
committed it to re-land as a sibling package. This change ships that
sibling — `@suaveplan/payment-provider-mollie` — implementing the
`PaymentProvider` contract from `@suaveplan/payment` on top of the slim
core (`httpRequest`, `composeAbortSignal`, `ApiKeyConfig`,
`PaymentProviderError`, `PAYMENT_PROVIDER_CODES`).
Mollie's quirk is **amount marshalling**: every money field on the wire
is a `{ currency, value }` object where `value` is a decimal-formatted
string (`"50.00"`, `"0.01"`, `"12345678.90"`). The genesis money model
is `bigint` minor-units via `@suaveplan/money` (`{ amount: 5000n,
currency: "EUR" }`). The provider owns the round-trip — formatting
minor-units to two-decimal strings for outbound requests, and parsing
incoming strings back to `bigint` minor-units — with full coverage of
the carry / padding / large-value edges that bit hand-rolled
implementations elsewhere.
## What changes
### ADDED
- `MollieProvider` class implementing
`PaymentProvider` from `@suaveplan/payment`. Constructor accepts
`ApiKeyConfig` (apiKey + optional baseUrl / timeoutMs / fetch /
signal / webhookSecret).
- `MOLLIE_DEFAULT_BASE_URL = "https://api.mollie.com/v2"`.
- `MOLLIE_PROVIDER_NAME = "mollie"`.
- `formatMollieAmount(amount: bigint): string` — minor-units →
`"X.YY"` two-decimal string. Pure, exported, deterministic.
- `parseMollieAmount(value: string): bigint` — `"X.YY"` →
minor-units `bigint`. Pure, exported, total over the shape Mollie
emits.
- `mapMollieStatus(status: string): PaymentStatus` and
`mapMollieRefundStatus(status: string): RefundStatus` — pure,
exported, total status maps with documented unknown-status fallback.
- Subpath exports `./mollie/mollie` for tree-shaking.
### REMOVED
Nothing — this is a fresh package.
### MODIFIED
- `STATUS.md` — new row for `@suaveplan/payment-provider-mollie`
flipped to ✅.
- `SUMMARY.md` — new paragraph for the sibling.
- `MANIFEST.md` / `MISSING.yml` — refreshed via the generator scripts.
## Source mapping
The shape of the adapter is anchored to upstream `MollieProvider` at
`/mnt/s/web/suaveplan/packages/universal/payment-providers/src/mollie.ts`
(the pre-slim, pre-sibling implementation) and to the original migration
spec at
`openspec/archive/2026-05-17-add-payment-providers/specs/payment-providers/spec.md`
(Mollie requirement block, L228–L258). The class signature, HTTP verbs,
status mapping, refund-status mapping, payment-method mapping, default
description / redirectUrl, and `Authorization: Bearer ${apiKey}` header
are preserved verbatim.
Departures from upstream:
| Surface | Upstream | Sibling | Reason |
|---|---|---|---|
| `vi.spyOn(globalThis, "fetch")` in tests | yes | replaced with real `FetchLike` closure injected via constructor | rule 8 / slim-core pattern — recording adapter, no global monkey-patching |
| `vi` / `vitest` test-runner import | yes | `@suaveplan/testing/runner` | AGENTS.md §7 |
| `@suaveplan/payment-providers` internal `httpRequest` | yes | unchanged — now imported from the public slim-core export | follow-up to slim refactor |
| Amount-formatting helpers (`formatAmount`, `parseAmount`) | private file-locals | exported (`formatMollieAmount`, `parseMollieAmount`) | reusable, individually unit-testable for edge cases |
## Migration
This is a fresh package — there is no in-tree migration. External
consumers that previously imported `MollieProvider` from
`@suaveplan/payment-providers` (pre-slim) MUST switch the import to
`@suaveplan/payment-provider-mollie`. The class name, constructor
signature, and `PaymentProvider` interface contract are preserved
verbatim; only the package name changes.
```ts
// Before (pre-slim)
import { MollieProvider } from "@suaveplan/payment-providers";
// After
import { MollieProvider } from "@suaveplan/payment-provider-mollie";
```
## Out of scope
- Webhook signature verification — Mollie does not sign its webhooks
with a shared secret in the standard v2 API surface that this
adapter implements. The `webhookSecret` field on `ApiKeyConfig` is
accepted (for shape parity with the other sibling providers) but
not consumed by this package.
- iDEAL issuer enumeration, Mollie Profiles API, Mollie Customers
API, Mollie Subscriptions API — out of scope for the
`PaymentProvider` contract. Future siblings can layer on top.
- OpenTelemetry spans / counters — deferred to align with
`@suaveplan/payment` and `@suaveplan/payment-providers`, which also
do not yet ship OTel wiring.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/payment-provider-mollie` to
enumerate.
- No dependents (verified by the fresh-package status — this package is
brand-new and nothing on `genesis` imports it yet). Future consumers
(apps, plugins, payment-engine bootstraps) opt in by adding the
dependency.
## Parity Checklist
This is a fresh sibling that preserves the upstream `MollieProvider`
behaviour verbatim. Parity is against
`/mnt/s/web/suaveplan/packages/universal/payment-providers/src/mollie.ts`:
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| `MollieProvider implements PaymentProvider` | yes | ✅ | verbatim, name `"mollie"` |
| `ApiKeyConfig` constructor (`apiKey`, optional `baseUrl`, `timeoutMs`, `webhookSecret`, `fetch`, `signal`) | yes | ✅ | now imports `ApiKeyConfig` from the slim core; the upstream version inlined the same shape |
| `Authorization: Bearer ${apiKey}` header on every call | yes | ✅ | unchanged |
| `createIntent` → `POST /payments` | yes | ✅ | unchanged |
| `confirmIntent` → read-through to `getIntent` | yes | ✅ | unchanged (Mollie auto-captures) |
| `captureIntent` → read-through to `getIntent` | yes | ✅ | unchanged (Mollie auto-captures) |
| `cancelIntent` → `DELETE /payments/{id}` | yes | ✅ | unchanged |
| `refund` → `POST /payments/{id}/refunds` | yes | ✅ | unchanged; refund without explicit amount also covered |
| `getIntent` → `GET /payments/{id}` | yes | ✅ | unchanged |
| Default `description = "Payment"` | yes | ✅ | unchanged |
| Default `redirectUrl = "https://localhost/return"` | yes | ✅ | unchanged |
| Status map (`open` → `created`, `pending`, `authorized`, `paid` → `captured`, `canceled`, `expired` → `canceled`, `failed`, other → `failed`) | yes | ✅ | exported as `mapMollieStatus` for direct unit coverage |
| Refund status map (`refunded` → `succeeded`, `pending`/`queued`/`processing` → `pending`, other → `failed`) | yes | ✅ | exported as `mapMollieRefundStatus` |
| Payment method map (`creditcard` → `card`, other defined method → `wallet`, undefined → no method) | yes | ✅ | unchanged |
| `card` brand + last4 extraction from `details` | yes | ✅ | unchanged |
| Amount marshalling: `bigint` → `"X.YY"` (`formatAmount`) | yes | ✅ | extracted + exported as `formatMollieAmount`; covered with edge cases 1 cent, large values, exact decimals |
| Amount marshalling: `"X.YY"` → `bigint` (`parseAmount`) | yes | ✅ | extracted + exported as `parseMollieAmount`; covered with single-decimal, missing decimal, padded decimal |
| All errors raised as `PaymentProviderError` from the slim core | yes | ✅ | unchanged — `httpRequest` raises typed errors; this package never throws raw `Error` |
| `vi.spyOn` in tests | yes | 🚫 replaced | rule 8 / slim-core pattern: tests inject a recording `FetchLike` |
| Direct `from "vitest"` test imports | yes | 🚫 replaced | AGENTS.md §7: `from "@suaveplan/testing/runner"` |
## Verification approach
- `src/mollie/mollie.test.ts` — exercises every `PaymentProvider`
method using injected `FetchLike` closures that record requests and
return controlled `Response`s. Covers:
- `createIntent` happy path + default description / redirectUrl
- `confirmIntent` / `captureIntent` read-through
- `cancelIntent` HTTP DELETE
- `refund` with explicit amount + without
- `getIntent` HTTP GET with card brand + last4 extraction
- Full status-map matrix (all 8 cases incl. unknown → `failed`)
- Full refund-status-map matrix (all 5 cases)
- Payment-method map for `creditcard` / `ideal` / undefined
- Amount marshalling edge cases: 1 cent (`1n` → `"0.01"`), exact
dollar (`100n` → `"1.00"`), large (`12345678901234n` →
`"123456789012.34"`), and the inverse round-trips.
- `Authorization: Bearer …` header surfaced on every recorded call.
- Network failure surfaces as `PaymentProviderError` (HTTP_ERROR).
- All tests import from `@suaveplan/testing/runner`.
- 100/100/100/100 coverage gate (vitest istanbul) enforced in
`vitest.config.ts`.