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

121 lines
7.0 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# 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"`