Initial Commit
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# add-payment-provider-adyen — sibling adapter for the slim payment-providers core
|
||||
|
||||
**Package**: `@suaveplan/payment-provider-adyen`
|
||||
**Status**: PROPOSED
|
||||
**Type**: sibling-package landing (follow-up to `2026-05-17-slim-payment-providers`)
|
||||
|
||||
- **Spec delta**: ./specs/payment-provider-adyen/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
`2026-05-17-slim-payment-providers` reshaped `@suaveplan/payment-providers`
|
||||
into a foundation-only package and removed five concrete adapter classes
|
||||
(Stripe, PayPal, Square, Mollie, Adyen). The slim-down's archival proposal
|
||||
explicitly names the follow-up wave: each removed adapter re-lands as a
|
||||
sibling `@suaveplan/payment-provider-*` package depending on the slim
|
||||
core. This change ships the Adyen sibling.
|
||||
|
||||
Adyen is the merchant-account-context adapter — its constructor takes an
|
||||
`apiKey` plus a `merchantAccount` string that is injected into every
|
||||
modification request body (`captures`, `cancels`, `refunds`) per Adyen's
|
||||
Checkout API v71 contract. It also generates a default `reference` value
|
||||
via an injectable `RandomIdFn` when the caller omits `idempotencyKey`,
|
||||
keeping the package universal-tier (no `crypto.randomUUID()` at module
|
||||
scope, no `node:crypto`).
|
||||
|
||||
## What changes
|
||||
|
||||
### ADDED
|
||||
|
||||
- New package `@suaveplan/payment-provider-adyen` at
|
||||
`packages/universal/payment-provider-adyen/`, version `0.1.0`,
|
||||
universal tier, `sideEffects: false`.
|
||||
- Exports `class AdyenProvider implements PaymentProvider` with
|
||||
`name: "adyen"`, plus the `AdyenConfig` type (extends
|
||||
`MerchantKeyConfig` from the slim core).
|
||||
- Re-uses the slim core's `httpRequest`, `composeAbortSignal`,
|
||||
`MerchantKeyConfig`, `RandomIdFn`, `PaymentProviderError`,
|
||||
`PAYMENT_PROVIDER_CODES`, `httpError`, and `parseError` surfaces —
|
||||
no duplication.
|
||||
- Subpath exports: `.` (root), `./adyen/adyen`, `./types/adyen`.
|
||||
- 100% test coverage + sibling `.md` per implementation file + root
|
||||
`README.md`.
|
||||
|
||||
### MODIFIED
|
||||
|
||||
- None — pure additive landing.
|
||||
|
||||
### REMOVED
|
||||
|
||||
- None — the removal happened in the slim-down change.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Surface | Source | Status |
|
||||
|---|---|---|
|
||||
| `AdyenProvider` class | `../suaveplan/packages/universal/payment-providers/src/adyen.ts` | ported |
|
||||
| `AdyenConfig` (extends `MerchantKeyConfig`) | `../suaveplan/packages/universal/payment-providers/src/types/provider.types.ts` (`MerchantKeyConfig`) | re-typed as a sibling-local extension |
|
||||
| Status / refund / payment-method mapping | `../suaveplan/packages/universal/payment-providers/src/adyen.ts` L31-L86 | ported verbatim |
|
||||
| `crypto.randomUUID()` default reference | upstream L113 | replaced with injectable `RandomIdFn` (universal-tier; defaults to `crypto.randomUUID()` only when `globalThis.crypto?.randomUUID` exists, otherwise required) |
|
||||
| Test suite (24 cases) | `../suaveplan/packages/universal/payment-providers/src/adyen.test.ts` | ported; `vi.spyOn(globalThis, "fetch")` rewritten to a real recording `FetchLike` closure (rule: no mocks) |
|
||||
| Co-located docs | `../suaveplan/packages/universal/payment-providers/src/adyen.md` | rewritten to meet genesis §4 (Purpose ≥2 sentences, Features, Basic + Advanced usage, API Reference, Implementation Notes — ≥200 words excl. code) |
|
||||
|
||||
## Parity Checklist
|
||||
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
|---|---|---|---|
|
||||
| `createIntent` POST `/payments` with merchantAccount + reference + captureDelayHours | ✅ | ✅ | reference auto-generated via injectable `randomId` when `idempotencyKey` is omitted |
|
||||
| `confirmIntent` POST `/payments/{id}/details` | ✅ | ✅ | |
|
||||
| `captureIntent` POST `/payments/{id}/captures` | ✅ | ✅ | merchantAccount in body |
|
||||
| `cancelIntent` POST `/payments/{id}/cancels` | ✅ | ✅ | merchantAccount in body |
|
||||
| `refund` POST `/payments/{id}/refunds` (optional amount, optional idempotency key) | ✅ | ✅ | |
|
||||
| `getIntent` GET `/payments/{id}` | ✅ | ✅ | |
|
||||
| `X-API-Key` header on every call | ✅ | ✅ | |
|
||||
| `Idempotency-Key` header on createIntent/refund when supplied | ✅ | ✅ | |
|
||||
| Status mapping (10 result codes + unknown → failed) | ✅ | ✅ | |
|
||||
| Refund status mapping (`received` → `pending`, else `failed`) | ✅ | ✅ | |
|
||||
| Payment-method mapping (`scheme` → `card`, else `wallet`) | ✅ | ✅ | |
|
||||
| `mock` / `vi.spyOn` test scaffolding | ✅ | ⬆️ | rewritten to use a real recording `FetchLike` closure — `@suaveplan/payment-providers` already exposes the seam, so no mocks are needed |
|
||||
| `crypto.randomUUID()` at module scope | ✅ | ⬆️ | replaced with injectable `RandomIdFn` — keeps the package universal-tier and deterministic in tests |
|
||||
| Sibling `.md` (≥200 words, mandatory sections) | 🚫 (upstream only had 20 lines) | ⬆️ | rewritten to satisfy genesis §4 |
|
||||
| Subpath exports per feature module | 🚫 (single barrel) | ✅ | added `./adyen/adyen` and `./types/adyen` |
|
||||
| Test-runner imports via `@suaveplan/testing/runner` | 🚫 (upstream used `@suaveplan/testing`) | ✅ | rule 11 |
|
||||
| `PaymentProviderError` raised on every failure path | ✅ | ✅ | via `httpRequest` from the slim core |
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Webhook signature verification (`webhookHmacKey` on `MerchantKeyConfig`
|
||||
is plumbed through but verification logic stays in `@suaveplan/payment`'s
|
||||
`WebhookVerifier` — same as every other sibling).
|
||||
- OpenTelemetry spans / counters — deferred until `@suaveplan/payment`
|
||||
itself wires OTel (matches the slim core deferral).
|
||||
- Direct Storybook stories — the package has no React surface.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/payment-provider-adyen` to enumerate.
|
||||
|
||||
No dependents (verified by `bunx turbo ls --filter=...@suaveplan/payment-provider-adyen`).
|
||||
This is a new sibling-leaf package; consumers opt in by adding it to their
|
||||
own `package.json`.
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/adyen/adyen.test.ts` — 24 cases ported from upstream, run against
|
||||
a real recording `FetchLike` closure. Every assertion targets either a
|
||||
request the closure recorded (URL, method, headers, body) or the typed
|
||||
`PaymentIntent` / `Refund` shape returned from the provider.
|
||||
- `src/types/adyen.test.ts` — type-only smoke test confirming `AdyenConfig`
|
||||
extends `MerchantKeyConfig` and accepts the documented options.
|
||||
- `tests/dep-graph.test.ts` — asserts no `node:`, `bun:`, `fs`, `path`,
|
||||
`stream`, `net`, `http`, or `crypto` imports anywhere in `src/`.
|
||||
- §13 gate set in full: lint + typecheck + test + coverage (100/100/100/100)
|
||||
- build + banned-pattern scan + barrel-shape scan + runner-import scan
|
||||
- missing-`.md` scan + README check + bare-version check +
|
||||
subpath-export count + security scan.
|
||||
@@ -0,0 +1,120 @@
|
||||
<!-- 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"`
|
||||
@@ -0,0 +1,88 @@
|
||||
# Tasks: add-payment-provider-adyen
|
||||
|
||||
## 1. Scaffold the package
|
||||
|
||||
- [ ] 1.1 Create `packages/universal/payment-provider-adyen/` with
|
||||
`package.json` (name `@suaveplan/payment-provider-adyen`, version
|
||||
`0.1.0`, `sideEffects: false`, runtime deps on
|
||||
`@suaveplan/payment`, `@suaveplan/payment-providers`, `tslib`).
|
||||
- [ ] 1.2 Add `vite.config.ts` (lib build, preserveModules, externals).
|
||||
- [ ] 1.3 Add `vitest.config.ts` (forks pool, istanbul 100/100/100/100).
|
||||
- [ ] 1.4 Add `tsconfig.json` + `tsconfig.lib.json`.
|
||||
- [ ] 1.5 Add `tests/setup/polyfills.ts` and `tests/dep-graph.test.ts`.
|
||||
|
||||
## 2. Implement `AdyenConfig`
|
||||
|
||||
- [ ] 2.1 Create `src/types/adyen.ts` exporting `AdyenConfig` as a
|
||||
sibling-local re-export / extension of `MerchantKeyConfig`.
|
||||
- [ ] 2.2 Create `src/types/adyen.md`.
|
||||
- [ ] 2.3 Create `src/types/index.ts` (named re-export, rule 6a).
|
||||
|
||||
## 3. Implement `AdyenProvider`
|
||||
|
||||
- [ ] 3.1 Create `src/adyen/adyen.ts` exporting
|
||||
`class AdyenProvider implements PaymentProvider` with `name = "adyen"`.
|
||||
- [ ] 3.2 Constructor accepts `AdyenConfig`; injectable `randomId` is
|
||||
used to generate the default `reference` when `idempotencyKey` is
|
||||
omitted.
|
||||
- [ ] 3.3 Six methods (`createIntent`, `confirmIntent`, `captureIntent`,
|
||||
`cancelIntent`, `refund`, `getIntent`) all dispatch via the slim
|
||||
core's `httpRequest`, with `X-API-Key` header and `merchantAccount`
|
||||
in the body where Adyen requires it.
|
||||
- [ ] 3.4 Status / refund / payment-method mapping ported verbatim from
|
||||
upstream.
|
||||
- [ ] 3.5 Every failure path surfaces a `PaymentProviderError` (via the
|
||||
slim core's `httpRequest`).
|
||||
- [ ] 3.6 Create `src/adyen/adyen.md` (Purpose ≥2 sentences, Features,
|
||||
Basic + Advanced usage, API Reference, Implementation Notes — ≥200
|
||||
words excluding code).
|
||||
- [ ] 3.7 Create `src/adyen/index.ts` (named re-export).
|
||||
- [ ] 3.8 Create `src/adyen/adyen.test.ts` covering all 24 verified-by
|
||||
test names verbatim, using a real recording `FetchLike` closure (no
|
||||
mocks).
|
||||
|
||||
## 4. Wire barrels + subpath exports
|
||||
|
||||
- [ ] 4.1 Create `src/index.ts` re-exporting `AdyenProvider` and
|
||||
`AdyenConfig` via explicit named exports (rule 6a).
|
||||
- [ ] 4.2 Add subpath exports for `.`, `./adyen/adyen`, `./types/adyen`.
|
||||
|
||||
## 5. README + root-doc refresh (§13b)
|
||||
|
||||
- [ ] 5.1 Author `README.md` (description, install, quick start,
|
||||
Modules list).
|
||||
- [ ] 5.2 Run `bun scripts/folder-reorg/generate-manifest.ts` to refresh
|
||||
`MANIFEST.md`.
|
||||
- [ ] 5.3 Run `bun scripts/generate-missing.ts` to refresh `MISSING.yml`.
|
||||
- [ ] 5.4 Hand-edit `STATUS.md` to add the new package row.
|
||||
- [ ] 5.5 Hand-edit `SUMMARY.md` to add the new package paragraph.
|
||||
- [ ] 5.6 `bunx markdownlint-cli2 --fix` across touched docs.
|
||||
|
||||
## 6. Verification gates (§13)
|
||||
|
||||
- [ ] 6.1 Step 0 — spec reconciliation walk.
|
||||
- [ ] 6.2 Step 1 —
|
||||
`bunx turbo lint typecheck test --filter=@suaveplan/payment-provider-adyen`.
|
||||
- [ ] 6.3 Step 2 —
|
||||
`bunx turbo test:coverage --filter=@suaveplan/payment-provider-adyen`
|
||||
reports 100/100/100/100.
|
||||
- [ ] 6.4 Step 3 —
|
||||
`bunx turbo build --filter=@suaveplan/payment-provider-adyen`.
|
||||
- [ ] 6.5 Step 4 — banned-pattern scan zero.
|
||||
- [ ] 6.6 Step 4a — barrel-shape scan zero.
|
||||
- [ ] 6.7 Step 4b — runner-import scan zero.
|
||||
- [ ] 6.8 Step 5 — every `.ts` has a sibling `.md`.
|
||||
- [ ] 6.9 Step 6 — README present.
|
||||
- [ ] 6.10 Step 7 — no bare versions.
|
||||
- [ ] 6.11 Step 8 — subpath-export count matches feature-module count
|
||||
(2 features + root = 3 entries).
|
||||
- [ ] 6.12 Step 9 — security scan zero.
|
||||
- [ ] 6.13 Step 10 — dependent sweep
|
||||
(`bunx turbo test --filter=...@suaveplan/payment-provider-adyen`) green.
|
||||
|
||||
## 7. Fold + archive (§14)
|
||||
|
||||
- [ ] 7.1 Fold `specs/payment-provider-adyen/spec.md` into
|
||||
`openspec/specs/payment-provider-adyen/spec.md`.
|
||||
- [ ] 7.2 Move `openspec/changes/add-payment-provider-adyen/` →
|
||||
`openspec/archive/2026-05-17-add-payment-provider-adyen/`.
|
||||
Reference in New Issue
Block a user