Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
# @suaveplan/payment-provider-adyen - Universal Adyen Checkout API v71 adapter for @suaveplan/payment. Implements the PaymentProvider interface via the slim @suaveplan/payment-providers core (httpRequest + composeAbortSignal + MerchantKeyConfig + PaymentProviderError), forwarding X-API-Key auth on every call and injecting the configured merchantAccount into capture / cancel / refund request bodies. Generates default payment references via an injectable RandomIdFn so the package stays universal and deterministic under test.
Universal **Adyen Checkout API v71 adapter** for `@suaveplan/payment`.
Implements the `PaymentProvider` interface via the slim
`@suaveplan/payment-providers` core (`httpRequest`,
`composeAbortSignal`, `MerchantKeyConfig`, `PaymentProviderError`),
forwarding `X-API-Key` auth on every call and injecting the configured
`merchantAccount` into capture / cancel / refund request bodies.
Generates default payment references via an injectable `RandomIdFn`
so the package stays universal and deterministic under test.
<!-- begin:auto-docs -->
## Documentation tree
- [adyen](./src/adyen/adyen.md) — Universal adapter that implements the `PaymentProvider` interface from `@suaveplan/payment` against Adyen's Checko…
- [adyen](./src/adyen/adyen.md) — Universal adapter that implements the `PaymentProvider` interface from `@suaveplan/payment` against Adyen's Checko…
- [mapping](./src/adyen/mapping.md) — Pure mapping helpers that lift Adyen Checkout API v71 response shapes into the canonical `PaymentIntent`, `PaymentMetho…
- [types](./src/types/adyen.md) — Type alias exported by `@suaveplan/payment-provider-adyen` describing the constructor argument accepted by `AdyenProvid…
- [adyen](./src/types/adyen.md) — Type alias exported by `@suaveplan/payment-provider-adyen` describing the constructor argument accepted by `AdyenProvid…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/payment-provider-adyen @suaveplan/payment @suaveplan/payment-providers
```
## Quick start
```ts
import { AdyenProvider } from "@suaveplan/payment-provider-adyen";
import { ProviderRegistry } from "@suaveplan/payment-providers";
import { PaymentEngine } from "@suaveplan/payment";
const adyen = new AdyenProvider({
apiKey: process.env.ADYEN_API_KEY ?? "",
merchantAccount: "AcmeMerchantECOM",
});
const registry = new ProviderRegistry();
registry.register(adyen);
const engine = new PaymentEngine({ provider: registry.get("adyen") });
const intent = await engine.createIntent({
amount: { amount: 5_000n, currency: "USD" },
captureMethod: "automatic",
});
```
## Features
- **Full `PaymentProvider` surface** — `createIntent` /
`confirmIntent` / `captureIntent` / `cancelIntent` / `refund` /
`getIntent`.
- **`X-API-Key` authentication** with `Idempotency-Key` forwarding on
`createIntent` and `refund`.
- **`merchantAccount` context** injected into every modification body
(`/captures`, `/cancels`, `/refunds`).
- **Injectable `RandomIdFn`** for default `reference` generation —
with `globalThis.crypto.randomUUID()` fallback at
call time.
- **Adyen status mapping** for all 10 documented v71 result codes
plus `failed` fallback.
- **Typed errors only** — every failure surfaces as
`PaymentProviderError` (HTTP / timeout / parse) from the slim core.
- **Universal tier** — zero `node:` / `bun:` / `fs` / `path` /
`stream` / `net` / `http` / `crypto` imports; runs on Node, Bun,
browsers, and Web Workers.
- **Tree-shakeable** — `sideEffects: false` + per-feature subpath
exports.
- **100% test coverage** (Istanbul) + 100% docs coverage.
## Modules
- [`src/adyen/adyen.md`](./src/adyen/adyen.md) — the `AdyenProvider`
class.
- [`src/adyen/mapping.md`](./src/adyen/mapping.md) — pure mappers
(`mapStatus`, `mapRefundStatus`, `mapPaymentMethod`, `mapPayment`).
- [`src/types/adyen.md`](./src/types/adyen.md) — `AdyenConfig` type
alias for the slim-core `MerchantKeyConfig`.
## Related
- [`@suaveplan/payment-providers`](../payment-providers/README.md) —
slim foundation: `httpRequest`, `composeAbortSignal`,
`ProviderRegistry`, `PaymentProviderError`, shared config types.
- [`@suaveplan/payment`](../payment/README.md) — `PaymentEngine`,
`WebhookVerifier`, `PaymentProvider` interface.
## License
MIT

View File

@@ -0,0 +1,216 @@
# `AdyenProvider`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Universal adapter that implements the `PaymentProvider` interface
from `@suaveplan/payment` against Adyen's Checkout API v71. The class
authenticates every request via the `X-API-Key` header (rather than
HTTP Basic auth like Stripe/Mollie or OAuth like PayPal), and injects
the configured `merchantAccount` string into every request body Adyen
requires it on (`/payments`, `/payments/{id}/captures`,
`/payments/{id}/cancels`, `/payments/{id}/refunds`).
The class is the sole runtime export of
`@suaveplan/payment-provider-adyen`. It depends only on the slim core
`@suaveplan/payment-providers` (for `httpRequest`,
`PaymentProviderError`, `RandomIdFn`, and `MerchantKeyConfig`) and on
the type-only `@suaveplan/payment` interface — no Node, browser, or
React imports, no `node:crypto`, and no module-scope wall-clock or
RNG access. Default `reference` generation goes through an injectable
`RandomIdFn`, falling back to `globalThis.crypto.randomUUID()` only at
call time and only when available; tests inject a deterministic id
source instead.
## Features
- **Full `PaymentProvider` surface** — `createIntent`, `confirmIntent`,
`captureIntent`, `cancelIntent`, `refund`, `getIntent`.
- **`X-API-Key` authentication** — set on every request, including
`GET`s.
- **`Idempotency-Key` header forwarding** — when the caller supplies
`idempotencyKey` on `createIntent` or `refund`, the same value is
echoed as both the request-level `Idempotency-Key` header and (for
`createIntent`) the `reference` field in the body.
- **Injectable `randomId`** — default `reference` generation uses
caller-supplied `RandomIdFn`; falls back to
`globalThis.crypto.randomUUID()` only at call time so the package
stays universal and never imports `node:crypto`.
- **`merchantAccount` context injection** — every modification body
(`/captures`, `/cancels`, `/refunds`) carries the configured value.
- **Adyen status mapping** — `Authorised` / `Refused` / `Cancelled` /
`Error` / `Pending` / `Received` / `RedirectShopper` /
`IdentifyShopper` / `ChallengeShopper` / `PresentToShopper` plus
unknown codes mapped to the canonical `PaymentStatus`.
- **Refund status mapping** — `received` → `pending`, every other
status → `failed`.
- **Payment-method mapping** — `scheme` → `card`, everything else →
`wallet`; brand lower-cased; `lastFour` preserved as `last4`.
- **Typed errors** — every HTTP / timeout / parse failure surfaces as
a `PaymentProviderError` (one of `PAYMENT_PROVIDER_CODES`) via the
shared `httpRequest` helper; the class never throws plain `Error`.
- **Universal purity** — zero `node:`, `bun:`, `fs`, `path`,
`stream`, `net`, `http`, or `crypto` imports.
## Usage: Basic Example
```ts
import { AdyenProvider } from "@suaveplan/payment-provider-adyen";
const adyen = new AdyenProvider({
apiKey: process.env.ADYEN_API_KEY ?? "",
merchantAccount: "AcmeMerchantECOM",
});
const intent = await adyen.createIntent({
amount: { amount: 4_999n, currency: "USD" },
description: "Acme premium subscription",
captureMethod: "automatic",
});
console.log(intent.id, intent.status);
```
## Usage: Advanced Example
```ts
import { AdyenProvider } from "@suaveplan/payment-provider-adyen";
import {
ProviderRegistry,
type FetchLike,
} from "@suaveplan/payment-providers";
import { PaymentEngine } from "@suaveplan/payment";
// Test-time injection: recording fetch + deterministic randomId.
const calls: Array<{ url: string; init?: RequestInit }> = [];
const recordingFetch: FetchLike = async (url, init) => {
calls.push({ url, init });
return new Response(
JSON.stringify({
pspReference: "PSP_REC",
resultCode: "Authorised",
amount: { value: 4999, currency: "USD" },
paymentMethod: { type: "scheme", brand: "visa", lastFour: "4242" },
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
};
let refCounter = 0;
const adyen = new AdyenProvider({
apiKey: "AQE_test",
merchantAccount: "AcmeMerchantECOM",
baseUrl: "https://checkout-test.adyen.com/v71",
fetch: recordingFetch,
randomId: () => `ref_${++refCounter}`,
timeoutMs: 5_000,
});
const registry = new ProviderRegistry();
registry.register(adyen);
const engine = new PaymentEngine({ provider: registry.get("adyen") });
const intent = await engine.createIntent({
amount: { amount: 4_999n, currency: "USD" },
captureMethod: "manual",
});
// Verify the recording layer saw the right bytes:
const body = JSON.parse(String(calls[0]?.init?.body));
console.assert(body.merchantAccount === "AcmeMerchantECOM");
console.assert(body.reference === "ref_1");
console.assert(body.captureDelayHours === 0);
```
## API Reference
### `new AdyenProvider(config: AdyenConfig)`
Construct an Adyen adapter. `config` is the `MerchantKeyConfig` alias
documented in [`types/adyen.md`](../types/adyen.md). Required:
`apiKey`, `merchantAccount`. Optional: `baseUrl`, `timeoutMs`,
`signal`, `fetch`, `randomId`, `webhookHmacKey`. When `randomId` is
omitted the class falls back to `globalThis.crypto.randomUUID()` at
call time; if neither is available, the first auto-generated
`reference` raises a `PaymentProviderError` with code
`PAYMENT_PROVIDER.PARSE_ERROR`.
### `readonly name: "adyen"`
Stable identifier consumed by `ProviderRegistry` and any
multi-provider routing layer.
### `createIntent(input): Promise<PaymentIntent>`
POST `${baseUrl}/payments`. Request body always carries
`merchantAccount`, `amount`, and `reference` (caller's `idempotencyKey`
or `randomId()`). When `captureMethod === "manual"` the body adds
`captureDelayHours: 0`. Optional `description` → `shopperStatement`,
`returnUrl` → `returnUrl`, `paymentMethodId` →
`paymentMethod.storedPaymentMethodId`, `metadata` → `metadata`.
### `confirmIntent(intentId): Promise<PaymentIntent>`
POST `${baseUrl}/payments/${intentId}/details` with `{ details: {} }`.
### `captureIntent(intentId): Promise<PaymentIntent>`
POST `${baseUrl}/payments/${intentId}/captures` with
`{ merchantAccount, amount: {} }`. Returns a `PaymentIntent` with
`status: "captured"` and `capturedAmount` reflecting the response.
### `cancelIntent(intentId): Promise<PaymentIntent>`
POST `${baseUrl}/payments/${intentId}/cancels` with
`{ merchantAccount }`. Returns a `PaymentIntent` with
`status: "canceled"`.
### `refund(input): Promise<Refund>`
POST `${baseUrl}/payments/${input.paymentIntentId}/refunds` with
`{ merchantAccount }` plus optional `amount` when supplied. Forwards
`Idempotency-Key` when `input.idempotencyKey` is set. Refund `status`
is mapped via `mapRefundStatus`.
### `getIntent(intentId): Promise<PaymentIntent>`
GET `${baseUrl}/payments/${intentId}`.
### Errors
Every method propagates `PaymentProviderError` for HTTP non-2xx
responses, network failures, request timeouts, and JSON parse
failures — see `@suaveplan/payment-providers`
[`error-codes.md`](../../../payment-providers/src/error-codes/error-codes.md).
The class never throws plain `Error`.
## Implementation Notes
- The default `baseUrl` is the **test** endpoint
(`https://checkout-test.adyen.com/v71`). Live integrations MUST
override `baseUrl` with the merchant-specific
`https://<prefix>-checkout-live.adyenpayments.com/checkout/v71`
URL. Adyen does not provide a single shared live endpoint.
- The cancel response carries no `amount` (Adyen models cancellation
as a zero-value state change), so `cancelIntent` returns
`amount`/`capturedAmount`/`refundedAmount` all set to `{ amount: 0n,
currency: <response.currency> }`. Consumers needing the original
authorisation amount should refetch via `getIntent`.
- `BigInt(p.amount.value)` is safe because Adyen returns integer minor
units across the Checkout API; the conversion would silently round
any floating-point input that ever appeared, which is not a
documented v71 case.
- The class holds no mutable state beyond the constructor seams; every
method composes its own `headers` and request body fresh, so
concurrent calls are safe.
- `mapping.ts` is a sibling file holding the pure status / refund /
payment-method / payment mappers; splitting them out keeps `adyen.ts`
inside the 300-line hard limit (rule 3) and documents the mapping
contract on its own page.
- Webhook signature verification is **not** in scope for this package
— it lives in `@suaveplan/payment`'s `WebhookVerifier`. The
`webhookHmacKey` field on `AdyenConfig` is plumbed through so
consumers can pass it to the verifier without re-typing it.

View File

@@ -0,0 +1,123 @@
# Adyen response mapping
## Purpose
Pure mapping helpers that lift Adyen Checkout API v71 response shapes
into the canonical `PaymentIntent`, `PaymentMethod`, `PaymentStatus`,
and `RefundStatus` types exported by `@suaveplan/payment`. Splitting
these helpers out of `adyen.ts` keeps the provider class under the
300-line hard limit (rule 3) and keeps the mappers separately testable
through the public provider surface — every helper is exercised by the
`AdyenProvider` test suite via real recording `FetchLike` closures.
The mappers are deliberately total: every Adyen `resultCode` that the
v71 contract documents has an explicit row in `RESULT_STATUS`, every
unknown code falls through to `failed`, every non-`received` refund
status collapses to `failed`, and every non-`scheme` payment-method
type collapses to `wallet`. This matches the upstream behaviour
verbatim and the spec delta covers each row with a dedicated test.
## Features
- `mapStatus(resultCode)` — 10-entry result-code table + `failed`
fallback.
- `mapRefundStatus(status)` — single-row table (`received` → `pending`)
with `failed` fallback.
- `mapPaymentMethod(pspReference, method)` — emits `card` for `scheme`,
`wallet` for everything else; lower-cases the brand; preserves
`lastFour` as `last4`.
- `mapPayment(payment, captureMethod, nowIso)` — composes the above
into a full `PaymentIntent`. `capturedAmount` is always `0n` here
because Adyen's `/payments` and `/payments/{id}` endpoints never
return a captured state — capture is a separate `/captures` call
modelled by `AdyenProvider.captureIntent`, which builds its own
response.
- Typed `AdyenAmount` / `AdyenPaymentMethod` / `AdyenPayment` /
`AdyenModification` interfaces describing the response excerpts the
provider consumes.
## Usage: Basic Example
```ts
import { mapStatus, mapRefundStatus } from "@suaveplan/payment-provider-adyen/adyen/adyen";
mapStatus("Authorised"); // -> "authorized"
mapStatus("RedirectShopper"); // -> "pending"
mapStatus("WhoKnows"); // -> "failed"
mapRefundStatus("received"); // -> "pending"
mapRefundStatus("error"); // -> "failed"
```
## Usage: Advanced Example
```ts
import { mapPayment, type AdyenPayment } from "@suaveplan/payment-provider-adyen/adyen/adyen";
const raw: AdyenPayment = {
pspReference: "PSP_999",
resultCode: "Authorised",
amount: { value: 12_500, currency: "USD" },
merchantReference: "order_42",
paymentMethod: { type: "scheme", brand: "VISA", lastFour: "4242" },
};
const intent = mapPayment(raw, "manual", "2026-05-17T12:00:00.000Z");
intent.amount.amount; // -> 12500n
intent.amount.currency; // -> "USD"
intent.capturedAmount.amount; // -> 0n (capture is a separate /captures call)
intent.paymentMethod?.brand; // -> "visa"
intent.paymentMethod?.last4; // -> "4242"
intent.paymentMethod?.type; // -> "card"
```
## API Reference
### `mapStatus(resultCode: string): PaymentStatus`
Look up the Adyen result code in the static `RESULT_STATUS` table.
Returns `"failed"` for any code not in the table.
### `mapRefundStatus(status: string): RefundStatus`
Returns `"pending"` for `"received"`, `"failed"` for anything else.
### `mapPaymentMethod(pspReference: string, method?: AdyenPaymentMethod): PaymentMethod | undefined`
Returns `undefined` when `method` is `undefined`. Otherwise composes a
`PaymentMethod` with `id` `"adyen_<pspReference>"`, `type` `"card"`
when `method.type === "scheme"` and `"wallet"` otherwise, lower-cased
`brand`, and `last4` taken from `method.lastFour`.
### `mapPayment(payment: AdyenPayment, captureMethod: "automatic" | "manual", nowIso: string): PaymentIntent`
Composes a full `PaymentIntent`. `nowIso` is supplied by the caller so
the provider class controls the timestamp source; the helper does not
read the wall clock itself. `capturedAmount` is always `0n` in the same
currency — Adyen's `/payments` endpoint never reports a captured state,
so the mapped intent records the captured value as zero and leaves
capture reporting to `AdyenProvider.captureIntent`.
### Types
`AdyenAmount`, `AdyenPaymentMethod`, `AdyenPayment`, and
`AdyenModification` are minimal `readonly` excerpts of the upstream v71
shapes — only the fields the mappers consume are declared, so changes
to the wider Adyen response shape do not invalidate the types here.
## Implementation Notes
- The `RESULT_STATUS` table is frozen at module load time and exported
only through the wrapping `mapStatus` function so consumers cannot
mutate it.
- `BigInt(p.amount.value)` is safe because Adyen always returns
integer minor units — every Checkout response field is documented as
an integer; floating-point input would silently round here.
- The mappers are pure: same input → same output, no IO, no
closures over module state. The provider class injects `nowIso` and
`captureMethod` so callers can fix the timestamp in tests.
- `mapPaymentMethod` widens the lower-cased `brand` to
`PaymentMethod["brand"]` via a `as` cast. Adyen returns the brand
free-form, and `PaymentMethod["brand"]` is a discriminated union;
the cast is the boundary at which untyped wire data meets the typed
domain model. The provider tests cover the common brands (`visa`,
`applepay`) end-to-end.

View File

@@ -0,0 +1,115 @@
# `AdyenConfig`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Type alias exported by `@suaveplan/payment-provider-adyen` describing
the constructor argument accepted by `AdyenProvider`. It re-exports the
slim-core `MerchantKeyConfig` verbatim so consumers importing only this
sibling package do not need a second import from
`@suaveplan/payment-providers`, and so future Adyen-specific fields can
be added here without disturbing the foundation contract.
The type is structurally identical to `MerchantKeyConfig` — any value
typed as `MerchantKeyConfig` is assignable to `AdyenConfig` and vice
versa. This is deliberate: the slim-core shape already carries every
Adyen-specific seam (`apiKey`, `merchantAccount`, `webhookHmacKey`,
`randomId`), and forking the type would create a second source of
truth for the same contract.
## Features
- Re-exports `MerchantKeyConfig` under an Adyen-local name so callers
see the right vocabulary.
- Carries the injectable `randomId: RandomIdFn` seam used by
`AdyenProvider.createIntent` to generate the default `reference`
field when no `idempotencyKey` is supplied.
- Carries the standard `ProviderCommonOptions` seams (`fetch`,
`timeoutMs`, `signal`) so tests can swap in a recording `FetchLike`
and exercise abort propagation without monkey-patching globals.
- Carries `webhookHmacKey` for future webhook verification wiring
(currently consumed by `@suaveplan/payment`'s `WebhookVerifier`).
## Usage: Basic Example
```ts
import { AdyenProvider, type AdyenConfig } from "@suaveplan/payment-provider-adyen";
const config: AdyenConfig = {
apiKey: process.env.ADYEN_API_KEY ?? "",
merchantAccount: "AcmeMerchantECOM",
};
const adyen = new AdyenProvider(config);
```
## Usage: Advanced Example
```ts
import {
AdyenProvider,
type AdyenConfig,
} from "@suaveplan/payment-provider-adyen";
import type { FetchLike } from "@suaveplan/payment-providers";
// Deterministic reference generation + recording fetch for tests.
const calls: Array<{ url: string; init?: RequestInit }> = [];
const recordingFetch: FetchLike = async (url, init) => {
calls.push({ url, init });
return new Response(
JSON.stringify({
pspReference: "PSP_TEST",
resultCode: "Authorised",
amount: { value: 5000, currency: "USD" },
}),
{ status: 200 }
);
};
let counter = 0;
const config: AdyenConfig = {
apiKey: "AQE_test_key",
merchantAccount: "AcmeMerchantECOM",
baseUrl: "https://checkout-test.adyen.com/v71",
fetch: recordingFetch,
randomId: () => `gen_ref_${++counter}`,
timeoutMs: 5_000,
};
const adyen = new AdyenProvider(config);
```
## API Reference
### `type AdyenConfig`
Alias for `MerchantKeyConfig` from `@suaveplan/payment-providers`.
Fields (all read-only):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `apiKey` | `string` | yes | — | Adyen Checkout API key, sent as `X-API-Key`. |
| `merchantAccount` | `string` | yes | — | Adyen merchant account code, injected into modification bodies. |
| `baseUrl` | `string` | no | `https://checkout-test.adyen.com/v71` | Endpoint override (use `https://<prefix>-checkout-live.adyenpayments.com/checkout/v71` for live). |
| `webhookHmacKey` | `string` | no | — | Reserved for webhook signature verification. |
| `randomId` | `() => string` | no | `globalThis.crypto?.randomUUID` when available | Default-`reference` generator. |
| `fetch` | `FetchLike` | no | `globalThis.fetch` | Injectable HTTP client. |
| `timeoutMs` | `number` | no | `30_000` | Per-call timeout in milliseconds. |
| `signal` | `AbortSignal` | no | — | External cancellation token. |
## Implementation Notes
- `AdyenConfig` is intentionally a `type` alias rather than an
`interface extends MerchantKeyConfig` — the latter would diverge the
two types under `tsc --strict` if `MerchantKeyConfig` ever gains an
optional field. Aliasing preserves structural equality forever.
- The `randomId` seam is the universal escape hatch for default
`reference` generation. Without it, the provider would have to call
`crypto.randomUUID()` at request time and bring the entire
`node:crypto` polyfill surface into bundlers targeting browsers and
Web Workers. With it, callers in those environments can pass a
deterministic id source (UUIDv7, NanoID, sequence counter) and the
package stays pure.