Files
2026-05-27 23:29:53 +01:00

7.0 KiB

Capability Spec Delta: payment-provider-adyen (add-payment-provider-adyen)

Package: @suaveplan/payment-provider-adyen Status: PROPOSED

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: "<configured-value>" 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: "<configured-value>"

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"