# Capability Spec Delta: payment-provider-adyen (add-payment-provider-adyen) **Package**: `@suaveplan/payment-provider-adyen` **Status**: SHIPPED > Every requirement below carries a `**Verified by:**` line at draft time. > This is a fresh capability — only `## ADDED Requirements` is used. ## ADDED Requirements ### Requirement: AdyenProvider implements PaymentProvider with merchant-account context The package SHALL export an `AdyenProvider` class implementing the structural `PaymentProvider` interface from `@suaveplan/payment`. The class SHALL accept an `AdyenConfig` constructor argument that extends the slim-core `MerchantKeyConfig` and carries `apiKey`, `merchantAccount`, optional `baseUrl`, `timeoutMs`, `signal`, `fetch`, `webhookHmacKey`, and an optional `randomId: RandomIdFn` for default `reference` generation. The provider SHALL: - POST `${baseUrl}/payments` for `createIntent`, including `merchantAccount`, `amount`, `reference` (caller-supplied `idempotencyKey` or the result of `randomId()`), optional `captureDelayHours: 0` when `captureMethod === "manual"`, `shopperStatement`, `returnUrl`, `paymentMethod.storedPaymentMethodId`, and `metadata`. - POST `${baseUrl}/payments/{id}/{details,captures,cancels,refunds}` for `confirmIntent` / `captureIntent` / `cancelIntent` / `refund`. - GET `${baseUrl}/payments/{id}` for `getIntent`. - Set `X-API-Key: ${apiKey}` on every call and forward `Idempotency-Key: ${idempotencyKey}` on `createIntent` / `refund` when the caller supplies one. - Inject `merchantAccount: ""` into the request body for every modification (`captures`, `cancels`, `refunds`). - Map result `Authorised` → `authorized`; `Refused` / `Error` → `failed`; `Cancelled` → `canceled`; `Pending` / `Received` / `RedirectShopper` / `IdentifyShopper` / `ChallengeShopper` / `PresentToShopper` → `pending`; every other code → `failed`. - Map refund status `received` → `pending`; every other status → `failed`. - Map payment-method type `scheme` → `card`; every other type → `wallet`. - Raise `PaymentProviderError` (carrying one of the `PAYMENT_PROVIDER_CODES` from `@suaveplan/payment-providers`) on every HTTP, timeout, or parse failure — the package SHALL NOT throw plain `Error` from any public method. `AdyenProvider.name` SHALL equal `"adyen"`. #### Scenario: merchant-account context forwarded on capture / cancel / refund - **WHEN** `captureIntent`, `cancelIntent`, or `refund` is invoked - **THEN** the recording `FetchLike` observes a request body containing `merchantAccount: ""` #### Scenario: caller-supplied idempotency key forwarded as both reference and header - **WHEN** `createIntent` is invoked with `idempotencyKey: "idem_1"` - **THEN** the recording `FetchLike` observes `Idempotency-Key: "idem_1"` on the request headers AND the request body's `reference` field equals `"idem_1"` #### Scenario: injectable randomId generates reference when idempotencyKey absent - **WHEN** `createIntent` is invoked without an `idempotencyKey` and the constructor was supplied with a deterministic `randomId` returning `"gen_REF_1"` - **THEN** the recording `FetchLike` observes a request body whose `reference` field equals `"gen_REF_1"` **Verified by:** `src/adyen/adyen.test.ts::"has correct name"` AND `src/adyen/adyen.test.ts::"creates a payment"` AND `src/adyen/adyen.test.ts::"uses automatic capture by default"` AND `src/adyen/adyen.test.ts::"submits payment details on confirm"` AND `src/adyen/adyen.test.ts::"captures a payment"` AND `src/adyen/adyen.test.ts::"cancels a payment"` AND `src/adyen/adyen.test.ts::"creates a refund"` AND `src/adyen/adyen.test.ts::"refunds without amount"` AND `src/adyen/adyen.test.ts::"fetches a payment"` AND `src/adyen/adyen.test.ts::"maps result Authorised to authorized"` AND `src/adyen/adyen.test.ts::"maps result Refused to failed"` AND `src/adyen/adyen.test.ts::"maps result Cancelled to canceled"` AND `src/adyen/adyen.test.ts::"maps result Error to failed"` AND `src/adyen/adyen.test.ts::"maps result Pending to pending"` AND `src/adyen/adyen.test.ts::"maps result Received to pending"` AND `src/adyen/adyen.test.ts::"maps result RedirectShopper to pending"` AND `src/adyen/adyen.test.ts::"maps result IdentifyShopper to pending"` AND `src/adyen/adyen.test.ts::"maps result ChallengeShopper to pending"` AND `src/adyen/adyen.test.ts::"maps result PresentToShopper to pending"` AND `src/adyen/adyen.test.ts::"maps unknown result to failed"` AND `src/adyen/adyen.test.ts::"maps payment without paymentMethod"` AND `src/adyen/adyen.test.ts::"maps non-scheme payment type as wallet"` AND `src/adyen/adyen.test.ts::"maps refund received to pending"` AND `src/adyen/adyen.test.ts::"maps other refund statuses to failed"` AND `src/adyen/adyen.test.ts::"uses injected randomId to generate reference when idempotencyKey is absent"` --- ### Requirement: AdyenConfig extends the slim-core MerchantKeyConfig The package SHALL export an `AdyenConfig` type that extends `MerchantKeyConfig` from `@suaveplan/payment-providers`. The type SHALL preserve every field of `MerchantKeyConfig` (`apiKey`, `merchantAccount`, optional `baseUrl`, `webhookHmacKey`, `randomId`, `fetch`, `timeoutMs`, `signal`) so consumers can use either name interchangeably. #### Scenario: AdyenConfig is structurally compatible with MerchantKeyConfig - **WHEN** a value typed as `MerchantKeyConfig` is passed where `AdyenConfig` is expected - **THEN** TypeScript SHALL accept the assignment without a structural mismatch **Verified by:** `src/types/adyen.test.ts::"accepts a MerchantKeyConfig value as AdyenConfig"` --- ### Requirement: Universal-tier purity The package SHALL contain zero imports from `node:*`, `bun:*`, `fs`, `path`, `stream`, `net`, `http`, or `crypto`. The package SHALL run unchanged on Node, Bun, browsers, and Web Workers. The default `reference` generation SHALL go through an injectable `RandomIdFn` (falling back to `globalThis.crypto?.randomUUID` only when present at call time) so the package never imports `node:crypto`. #### Scenario: dep-graph guard rejects banned imports - **WHEN** `tests/dep-graph.test.ts` walks every `.ts` file under `src/` - **THEN** zero offenders SHALL be reported **Verified by:** `tests/dep-graph.test.ts::"src contains no node/bun/fs/path/stream/net/http/crypto imports"` --- ### Requirement: Subpath exports per feature module The package's `package.json#exports` map SHALL expose `.` (root barrel), `./adyen/adyen` (provider class), and `./types/adyen` (config type) so consumers can tree-shake to a single feature. The root barrel SHALL use explicit named re-exports (rule 6a); `src/adyen/index.ts` and `src/types/index.ts` SHALL likewise use explicit named re-exports. #### Scenario: subpath-export count matches feature-module count - **WHEN** `Object.keys(pkg.exports).filter(k => k.startsWith("./") && k !== "." && k !== "./package.json")` is computed - **THEN** the resulting array SHALL contain exactly `["./adyen/adyen", "./types/adyen"]` **Verified by:** `tests/dep-graph.test.ts::"package.json exposes exactly the feature subpaths"`