Conformance: how "extensible" stays true
@payfanout/conformance runs the identical contract against every adapter, the reason "add a PSP by writing an adapter, with zero changes to core or app code" is a guarantee and not a hope.
What the suite proves
- Capability coherence, an adapter can't claim a capability it doesn't honor.
- Integer-minor-unit boundaries proven for JPY (0 decimals) and BHD (3 decimals).
- Raw-body webhook signatures, including the re-serialization trap: same JSON value, different bytes ⇒ the adapter must reject.
- Stable webhook dedupe ids,
event.idis a durable key across retries/replays. - Error normalization with the untouched PSP error preserved on
raw. - Idempotency replay, same key twice → same result, one side effect.
Every shipped adapter passes the same suite; a future adapter is done when it does too.
Running it
The conformance tests run as part of the normal test suite:
pnpm run test # unit + shared conformance suites against in-memory PSP fakes
pnpm run test:coverage # enforces coverage thresholds (92% lines / 82% branches)The test pyramid layers on top of this:
- unit + conformance against in-memory PSP fakes, fast, always on;
- integration against real sandboxes, env-gated, validates every assumption the fakes encode (
pnpm run test:integration); - Playwright E2E through the demo app, real Stripe.js / Paysafe.js iframes, inline 3DS (
pnpm run e2e).
Writing a new adapter
The step-by-step guide for adding the next PSP lives in Writing an adapter. The short version: implement the ServerPaymentAdapter / ClientPaymentAdapter contract from @payfanout/core, then wire your adapter into runServerAdapterConformanceTests / runClientAdapterConformanceTests from @payfanout/conformance. When the suite is green, you're done, and core, server, and every consuming app stay untouched.