Initial Commit
This commit is contained in:
120
openspec/specs/payment-provider-adyen/spec.md
Normal file
120
openspec/specs/payment-provider-adyen/spec.md
Normal file
@@ -0,0 +1,120 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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: "<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"`
|
||||
Reference in New Issue
Block a user