Initial Commit
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: payment-providers (add-payment-providers)
|
||||
|
||||
**Package**: `@suaveplan/payment-providers`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Universal-tier purity
|
||||
|
||||
The package SHALL ship as a universal-tier package — no `node:*`,
|
||||
`bun:*`, `fs`, `path`, `stream`, `net`, `http`, or bare `crypto`
|
||||
imports in any file under `src/`. The package SHALL run unchanged under
|
||||
Node ≥ 18, Bun, modern browsers, and Web Workers. `package.json` SHALL
|
||||
declare `"sideEffects": false`. Every HTTP call SHALL go through the
|
||||
injectable `FetchLike` so the package never reaches into a runtime-
|
||||
specific HTTP stack.
|
||||
|
||||
#### Scenario: no platform imports in src/
|
||||
|
||||
- **WHEN** `grep -rnE "from ['\"](node:|bun:|fs|path|stream|net|http|crypto)['\"]" packages/universal/payment-providers/src/` is run
|
||||
- **THEN** the command produces zero matches
|
||||
|
||||
**Verified by:** `tests/dep-graph.test.ts::"src contains no node/bun/fs/path/stream/net/http/crypto imports"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Stable PAYMENT_PROVIDER_CODES registry and PaymentProviderError class
|
||||
|
||||
The package SHALL declare an error-code registry via
|
||||
`defineCodes("PAYMENT_PROVIDER", […])` containing exactly the codes
|
||||
`HTTP_ERROR`, `TIMEOUT`, `PARSE_ERROR`, and `OAUTH_TOKEN_FAILURE`.
|
||||
Every public failure path triggered by network or parsing errors SHALL
|
||||
throw a `PaymentProviderError extends SuaveplanError<PaymentProviderErrorCode>`
|
||||
carrying one of those codes. `PaymentProviderError` SHALL expose
|
||||
optional `provider`, `httpStatus`, `bodyExcerpt`, and `url`
|
||||
discriminants. The package SHALL NOT throw raw `Error` instances from
|
||||
any production code path under `src/`.
|
||||
|
||||
#### Scenario: every code reachable from a real call site
|
||||
|
||||
- **WHEN** the test suite exercises each factory (`httpError`, `timeoutError`, `parseError`, `oauthTokenFailure`)
|
||||
- **THEN** each path produces a `PaymentProviderError` whose `code` matches the expected `PAYMENT_PROVIDER.*` constant
|
||||
|
||||
#### Scenario: cause-bearing factories preserve the underlying error
|
||||
|
||||
- **WHEN** `parseError` or `oauthTokenFailure` is invoked with a `cause`
|
||||
- **THEN** the resulting `PaymentProviderError`'s `cause` is the original error
|
||||
|
||||
#### Scenario: HTTP error carries discriminants from the response
|
||||
|
||||
- **WHEN** `httpError(provider, status, url, body)` is invoked
|
||||
- **THEN** the resulting error's `httpStatus`, `url`, `provider`, and `bodyExcerpt` fields are all populated
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.test.ts::"every PAYMENT_PROVIDER.* code is reachable from a factory"` AND `src/error-codes/error-codes.test.ts::"preserves cause through parseError"` AND `src/error-codes/error-codes.test.ts::"preserves cause through oauthTokenFailure"` AND `src/error-codes/error-codes.test.ts::"httpError carries provider/url/httpStatus/bodyExcerpt discriminants"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: httpRequest helper with injectable FetchLike and timeout
|
||||
|
||||
The package SHALL export `httpRequest<T>(options: HttpOptions): Promise<ProviderResponse<T>>`
|
||||
that wraps an injectable `FetchLike` (defaulting to `globalThis.fetch`).
|
||||
The helper SHALL serialise `body` as JSON, set `Content-Type: application/json`
|
||||
and `Accept: application/json` headers (overridable by callers),
|
||||
enforce a per-call `timeoutMs` (default 30 000) via `AbortSignal` with
|
||||
a clean `clearTimeout` in the `finally` block, parse the response body
|
||||
as JSON (returning `{}` when the body is empty), and on a non-ok
|
||||
response throw a `PaymentProviderError(PAYMENT_PROVIDER.HTTP_ERROR)`
|
||||
carrying the HTTP status, URL, and a `bodyExcerpt` capped at 256 chars.
|
||||
A caller-supplied `signal` SHALL be composed with the internal timeout
|
||||
signal so external cancellation is honoured.
|
||||
|
||||
#### Scenario: 200 response returns parsed body
|
||||
|
||||
- **WHEN** the injected `FetchLike` resolves with `new Response("{\"ok\":true}", { status: 200 })`
|
||||
- **THEN** `httpRequest` resolves with `{ status: 200, data: { ok: true } }`
|
||||
|
||||
#### Scenario: 204 response returns empty data
|
||||
|
||||
- **WHEN** the injected `FetchLike` resolves with `new Response(null, { status: 204 })`
|
||||
- **THEN** `httpRequest` resolves with `{ status: 204, data: {} }`
|
||||
|
||||
#### Scenario: non-ok response throws typed error
|
||||
|
||||
- **WHEN** the injected `FetchLike` resolves with status 404 and body `{ "message": "Not Found" }`
|
||||
- **THEN** `httpRequest` rejects with a `PaymentProviderError` whose `code === "PAYMENT_PROVIDER.HTTP_ERROR"`, `httpStatus === 404`, and `bodyExcerpt` containing `"Not Found"`
|
||||
|
||||
#### Scenario: timeout aborts via AbortSignal
|
||||
|
||||
- **WHEN** the injected `FetchLike` never resolves and `timeoutMs` is set to 50
|
||||
- **THEN** `httpRequest` rejects with a `PaymentProviderError` whose `code === "PAYMENT_PROVIDER.TIMEOUT"`
|
||||
|
||||
**Verified by:** `src/http/http.test.ts::"makes a GET request and parses JSON"` AND `src/http/http.test.ts::"sends JSON body on POST"` AND `src/http/http.test.ts::"includes custom headers"` AND `src/http/http.test.ts::"throws PaymentProviderError on non-ok response"` AND `src/http/http.test.ts::"throws HTTP_ERROR when no message in body"` AND `src/http/http.test.ts::"handles empty response body"` AND `src/http/http.test.ts::"throws on non-ok empty response"` AND `src/http/http.test.ts::"throws TIMEOUT when AbortSignal fires"` AND `src/http/http.test.ts::"supports DELETE and PATCH methods"` AND `src/http/http.test.ts::"composes caller AbortSignal with internal timeout"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: StripeProvider full lifecycle
|
||||
|
||||
The package SHALL export `StripeProvider` implementing
|
||||
`PaymentProvider` from `@suaveplan/payment`. The provider SHALL:
|
||||
|
||||
- POST `${baseUrl}/payment_intents` for `createIntent`, forwarding
|
||||
`amount`, `currency` (lower-cased), `capture_method`, optional
|
||||
`customer`, `description`, `metadata`, `payment_method`.
|
||||
- POST `${baseUrl}/payment_intents/{id}/{confirm,capture,cancel}` for the
|
||||
matching methods.
|
||||
- POST `${baseUrl}/refunds` for `refund`, forwarding `payment_intent`
|
||||
and optional `amount`, `reason`, `metadata`.
|
||||
- GET `${baseUrl}/payment_intents/{id}` for `getIntent`.
|
||||
- Authenticate every call with `Authorization: Bearer ${apiKey}`.
|
||||
- Forward `idempotencyKey` as the `Idempotency-Key` header on
|
||||
`createIntent` and `refund`.
|
||||
- Map `requires_payment_method` / `requires_confirmation` → `created`;
|
||||
`requires_action` / `processing` → `pending`; `requires_capture` →
|
||||
`authorized`; `succeeded` → `captured`; `canceled` → `canceled`;
|
||||
every other Stripe status → `failed`.
|
||||
- Map refund status `succeeded` → `succeeded`; `pending` → `pending`;
|
||||
every other status → `failed`.
|
||||
- Map refund reason `duplicate` → `duplicate`, `fraudulent` →
|
||||
`fraudulent`, `requested_by_customer` → `requested_by_customer`,
|
||||
every other value → `other`.
|
||||
|
||||
`StripeProvider.name` SHALL equal `"stripe"`.
|
||||
|
||||
#### Scenario: createIntent forwards idempotency key
|
||||
|
||||
- **WHEN** `createIntent` is invoked with `idempotencyKey: "idem_1"`
|
||||
- **THEN** the recording `FetchLike` observes a request with header `Idempotency-Key: idem_1`
|
||||
|
||||
#### Scenario: status mapping is total
|
||||
|
||||
- **WHEN** `getIntent` returns a payload with an unknown Stripe status
|
||||
- **THEN** the mapped `PaymentIntent.status` is `"failed"`
|
||||
|
||||
**Verified by:** `src/providers/stripe/stripe.test.ts::"has correct name"` AND `src/providers/stripe/stripe.test.ts::"creates a payment intent"` AND `src/providers/stripe/stripe.test.ts::"confirms an intent"` AND `src/providers/stripe/stripe.test.ts::"captures an intent"` AND `src/providers/stripe/stripe.test.ts::"cancels an intent"` AND `src/providers/stripe/stripe.test.ts::"creates a refund"` AND `src/providers/stripe/stripe.test.ts::"refunds without explicit amount"` AND `src/providers/stripe/stripe.test.ts::"fetches an intent"` AND `src/providers/stripe/stripe.test.ts::"maps status requires_payment_method to created"` AND `src/providers/stripe/stripe.test.ts::"maps status requires_action to pending"` AND `src/providers/stripe/stripe.test.ts::"maps status processing to pending"` AND `src/providers/stripe/stripe.test.ts::"maps status requires_capture to authorized"` AND `src/providers/stripe/stripe.test.ts::"maps status succeeded to captured"` AND `src/providers/stripe/stripe.test.ts::"maps status canceled to canceled"` AND `src/providers/stripe/stripe.test.ts::"maps unknown status to failed"` AND `src/providers/stripe/stripe.test.ts::"maps refund status succeeded to succeeded"` AND `src/providers/stripe/stripe.test.ts::"maps refund status pending to pending"` AND `src/providers/stripe/stripe.test.ts::"maps refund status failed to failed"` AND `src/providers/stripe/stripe.test.ts::"maps refund status canceled to failed"` AND `src/providers/stripe/stripe.test.ts::"maps intent without payment method"` AND `src/providers/stripe/stripe.test.ts::"maps refund reason duplicate"` AND `src/providers/stripe/stripe.test.ts::"maps refund reason fraudulent"` AND `src/providers/stripe/stripe.test.ts::"maps refund reason other for unknown"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PayPalProvider OAuth token cache and order lifecycle
|
||||
|
||||
The package SHALL export `PayPalProvider` implementing
|
||||
`PaymentProvider`. The provider SHALL:
|
||||
|
||||
- POST `${baseUrl}/checkout/orders` for `createIntent`, mapping
|
||||
`captureMethod === "manual"` → `intent: "AUTHORIZE"` and otherwise
|
||||
`intent: "CAPTURE"`, marshalling `amount` to a string with two
|
||||
fractional digits (e.g. `5000n` → `"50.00"`).
|
||||
- POST `${baseUrl}/checkout/orders/{id}/{authorize,capture,void}` for
|
||||
`confirmIntent` / `captureIntent` / `cancelIntent`.
|
||||
- POST `${baseUrl}/payments/captures/{id}/refund` for `refund`.
|
||||
- GET `${baseUrl}/checkout/orders/{id}` for `getIntent`.
|
||||
- Authenticate every order call with `Authorization: Bearer ${token}`
|
||||
where `token` is fetched via the OAuth2 client-credentials flow from
|
||||
`${baseUrl.replace("/v2","/v1/oauth2/token")}` with header
|
||||
`Authorization: Basic ${base64Encode("${clientId}:${clientSecret}")}`.
|
||||
- Cache the access token until `expires_in * 1000 - 60_000` ms past the
|
||||
injected `now()`. The cache SHALL be reused across calls and SHALL
|
||||
refresh transparently after expiry.
|
||||
- Forward `idempotencyKey` as the `PayPal-Request-Id` header on
|
||||
`createIntent` and `refund`.
|
||||
- Map status `CREATED` / `SAVED` → `created`; `APPROVED` →
|
||||
`authorized`; `VOIDED` → `canceled`; `COMPLETED` → `captured`;
|
||||
`PAYER_ACTION_REQUIRED` → `pending`; every other status → `failed`.
|
||||
- Map refund status `COMPLETED` → `succeeded`; `PENDING` → `pending`;
|
||||
every other status → `failed`.
|
||||
- Translate `intent === "AUTHORIZE"` → `captureMethod: "manual"` and
|
||||
every other intent → `automatic`.
|
||||
- When the OAuth call fails, raise
|
||||
`PaymentProviderError(PAYMENT_PROVIDER.OAUTH_TOKEN_FAILURE)` carrying
|
||||
the cause.
|
||||
|
||||
`PayPalProvider.name` SHALL equal `"paypal"`.
|
||||
|
||||
#### Scenario: token cache hit avoids second OAuth call
|
||||
|
||||
- **WHEN** two `createIntent` invocations are issued back-to-back with the same provider instance and the injected `now()` advances by less than `expires_in * 1000 - 60_000` ms between them
|
||||
- **THEN** the recording `FetchLike` observes exactly one OAuth request and two order requests
|
||||
|
||||
#### Scenario: token cache refresh after expiry
|
||||
|
||||
- **WHEN** the second `createIntent` is issued after the injected `now()` advances past the cache TTL
|
||||
- **THEN** the recording `FetchLike` observes two OAuth requests and two order requests
|
||||
|
||||
**Verified by:** `src/providers/paypal/paypal.test.ts::"has correct name"` AND `src/providers/paypal/paypal.test.ts::"creates an order with OAuth token"` AND `src/providers/paypal/paypal.test.ts::"reuses cached token"` AND `src/providers/paypal/paypal.test.ts::"refreshes token after expiry"` AND `src/providers/paypal/paypal.test.ts::"authorizes an order"` AND `src/providers/paypal/paypal.test.ts::"captures an order"` AND `src/providers/paypal/paypal.test.ts::"voids an order"` AND `src/providers/paypal/paypal.test.ts::"refunds a capture"` AND `src/providers/paypal/paypal.test.ts::"refunds without explicit amount"` AND `src/providers/paypal/paypal.test.ts::"fetches an order"` AND `src/providers/paypal/paypal.test.ts::"maps status CREATED to created"` AND `src/providers/paypal/paypal.test.ts::"maps status SAVED to created"` AND `src/providers/paypal/paypal.test.ts::"maps status APPROVED to authorized"` AND `src/providers/paypal/paypal.test.ts::"maps status VOIDED to canceled"` AND `src/providers/paypal/paypal.test.ts::"maps status COMPLETED to captured"` AND `src/providers/paypal/paypal.test.ts::"maps status PAYER_ACTION_REQUIRED to pending"` AND `src/providers/paypal/paypal.test.ts::"maps unknown status to failed"` AND `src/providers/paypal/paypal.test.ts::"maps refund status COMPLETED to succeeded"` AND `src/providers/paypal/paypal.test.ts::"maps refund status PENDING to pending"` AND `src/providers/paypal/paypal.test.ts::"maps refund status CANCELLED to failed"` AND `src/providers/paypal/paypal.test.ts::"handles order with AUTHORIZE intent"` AND `src/providers/paypal/paypal.test.ts::"handles order without payments field"` AND `src/providers/paypal/paypal.test.ts::"wraps OAuth failure in OAUTH_TOKEN_FAILURE"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SquareProvider lifecycle with injectable randomId
|
||||
|
||||
The package SHALL export `SquareProvider` implementing
|
||||
`PaymentProvider`. The provider SHALL:
|
||||
|
||||
- POST `${baseUrl}/payments` for `createIntent`, forwarding
|
||||
`amount_money`, `idempotency_key` (caller-supplied or
|
||||
injected-`randomId`-generated), `autocomplete` (`true` unless
|
||||
`captureMethod === "manual"`), optional `customer_id`, `note`,
|
||||
`source_id`.
|
||||
- POST `${baseUrl}/payments/{id}/{complete,cancel}` for capture / cancel.
|
||||
- POST `${baseUrl}/refunds` for `refund`, generating
|
||||
`idempotency_key` the same way.
|
||||
- GET `${baseUrl}/payments/{id}` for `getIntent`.
|
||||
- Set `Authorization: Bearer ${apiKey}` and `Square-Version: 2024-01-18`
|
||||
on every call.
|
||||
- `confirmIntent` SHALL be a read-through to `getIntent` (Square auto-
|
||||
confirms).
|
||||
- Map status `APPROVED` → `authorized`; `COMPLETED` → `captured`;
|
||||
`CANCELED` → `canceled`; `FAILED` → `failed`; `PENDING` → `pending`;
|
||||
every other status → `failed`.
|
||||
- Map refund status `COMPLETED` → `succeeded`; `PENDING` → `pending`;
|
||||
every other status → `failed`.
|
||||
- Default refund reason `requested_by_customer` when the upstream
|
||||
record carries any `reason` string, else `other`.
|
||||
|
||||
`SquareProvider.name` SHALL equal `"square"`.
|
||||
|
||||
#### Scenario: default idempotency key uses injected randomId
|
||||
|
||||
- **WHEN** `createIntent` is invoked without `idempotencyKey` and the provider was constructed with `randomId: () => "deterministic-id"`
|
||||
- **THEN** the recording `FetchLike` observes a request body whose `idempotency_key` equals `"deterministic-id"`
|
||||
|
||||
**Verified by:** `src/providers/square/square.test.ts::"has correct name"` AND `src/providers/square/square.test.ts::"creates a payment"` AND `src/providers/square/square.test.ts::"creates manual capture payment"` AND `src/providers/square/square.test.ts::"returns current state (no-op for Square)"` AND `src/providers/square/square.test.ts::"completes a payment"` AND `src/providers/square/square.test.ts::"cancels a payment"` AND `src/providers/square/square.test.ts::"creates a refund"` AND `src/providers/square/square.test.ts::"refunds without amount"` AND `src/providers/square/square.test.ts::"fetches a payment"` AND `src/providers/square/square.test.ts::"maps status APPROVED to authorized"` AND `src/providers/square/square.test.ts::"maps status COMPLETED to captured"` AND `src/providers/square/square.test.ts::"maps status CANCELED to canceled"` AND `src/providers/square/square.test.ts::"maps status FAILED to failed"` AND `src/providers/square/square.test.ts::"maps status PENDING to pending"` AND `src/providers/square/square.test.ts::"maps unknown status to failed"` AND `src/providers/square/square.test.ts::"maps payment without card details"` AND `src/providers/square/square.test.ts::"maps refund without reason"` AND `src/providers/square/square.test.ts::"maps refund status PENDING"` AND `src/providers/square/square.test.ts::"maps refund status REJECTED to failed"` AND `src/providers/square/square.test.ts::"uses injected randomId for default idempotency key"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: MollieProvider lifecycle with string-amount marshalling
|
||||
|
||||
The package SHALL export `MollieProvider` implementing
|
||||
`PaymentProvider`. The provider SHALL:
|
||||
|
||||
- POST `${baseUrl}/payments` for `createIntent`, marshalling `amount`
|
||||
to a `{ currency, value: "50.00" }` shape via decimal formatting,
|
||||
defaulting `description` to `"Payment"` and `redirectUrl` to
|
||||
`"https://localhost/return"` when not supplied.
|
||||
- DELETE `${baseUrl}/payments/{id}` for `cancelIntent`.
|
||||
- POST `${baseUrl}/payments/{id}/refunds` for `refund`.
|
||||
- GET `${baseUrl}/payments/{id}` for `getIntent`.
|
||||
- `confirmIntent` and `captureIntent` SHALL be read-throughs to
|
||||
`getIntent` (Mollie auto-captures).
|
||||
- Set `Authorization: Bearer ${apiKey}` on every call.
|
||||
- Map status `open` → `created`; `pending` → `pending`; `authorized` →
|
||||
`authorized`; `paid` → `captured`; `canceled` / `expired` →
|
||||
`canceled`; `failed` → `failed`; every other status → `failed`.
|
||||
- Map refund status `refunded` → `succeeded`; `pending` / `queued` /
|
||||
`processing` → `pending`; every other status → `failed`.
|
||||
- Map `method === "creditcard"` → `type: "card"`; every other method →
|
||||
`type: "wallet"`.
|
||||
|
||||
`MollieProvider.name` SHALL equal `"mollie"`.
|
||||
|
||||
#### Scenario: amount marshalling round-trip
|
||||
|
||||
- **WHEN** `createIntent({ amount: { amount: 5000n, currency: "EUR" } })` is invoked
|
||||
- **THEN** the recording `FetchLike` observes a request body whose `amount.value === "50.00"` and `amount.currency === "EUR"`
|
||||
|
||||
**Verified by:** `src/providers/mollie/mollie.test.ts::"has correct name"` AND `src/providers/mollie/mollie.test.ts::"creates a payment"` AND `src/providers/mollie/mollie.test.ts::"uses default description and redirect"` AND `src/providers/mollie/mollie.test.ts::"returns current state on confirm"` AND `src/providers/mollie/mollie.test.ts::"returns current state on capture (Mollie auto-captures)"` AND `src/providers/mollie/mollie.test.ts::"cancels a payment"` AND `src/providers/mollie/mollie.test.ts::"creates a refund"` AND `src/providers/mollie/mollie.test.ts::"refunds without explicit amount"` AND `src/providers/mollie/mollie.test.ts::"fetches a payment"` AND `src/providers/mollie/mollie.test.ts::"maps status open to created"` AND `src/providers/mollie/mollie.test.ts::"maps status pending to pending"` AND `src/providers/mollie/mollie.test.ts::"maps status authorized to authorized"` AND `src/providers/mollie/mollie.test.ts::"maps status paid to captured"` AND `src/providers/mollie/mollie.test.ts::"maps status canceled to canceled"` AND `src/providers/mollie/mollie.test.ts::"maps status expired to canceled"` AND `src/providers/mollie/mollie.test.ts::"maps status failed to failed"` AND `src/providers/mollie/mollie.test.ts::"maps unknown status to failed"` AND `src/providers/mollie/mollie.test.ts::"maps refund status refunded to succeeded"` AND `src/providers/mollie/mollie.test.ts::"maps refund status pending to pending"` AND `src/providers/mollie/mollie.test.ts::"maps refund status queued to pending"` AND `src/providers/mollie/mollie.test.ts::"maps refund status processing to pending"` AND `src/providers/mollie/mollie.test.ts::"maps refund status failed to failed"` AND `src/providers/mollie/mollie.test.ts::"maps payment without method"` AND `src/providers/mollie/mollie.test.ts::"maps non-card method as wallet"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AdyenProvider lifecycle with merchant-account context
|
||||
|
||||
The package SHALL export `AdyenProvider` implementing
|
||||
`PaymentProvider`. The provider SHALL:
|
||||
|
||||
- POST `${baseUrl}/payments` for `createIntent`, including
|
||||
`merchantAccount`, `amount`, `reference` (caller-supplied
|
||||
`idempotencyKey` or injected-`randomId`-generated), optional
|
||||
`shopperStatement`, `returnUrl`, `paymentMethod.storedPaymentMethodId`,
|
||||
`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` on `createIntent` / `refund` when supplied.
|
||||
- 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`.
|
||||
|
||||
`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>"`
|
||||
|
||||
**Verified by:** `src/providers/adyen/adyen.test.ts::"has correct name"` AND `src/providers/adyen/adyen.test.ts::"creates a payment"` AND `src/providers/adyen/adyen.test.ts::"uses automatic capture by default"` AND `src/providers/adyen/adyen.test.ts::"submits payment details on confirm"` AND `src/providers/adyen/adyen.test.ts::"captures a payment"` AND `src/providers/adyen/adyen.test.ts::"cancels a payment"` AND `src/providers/adyen/adyen.test.ts::"creates a refund"` AND `src/providers/adyen/adyen.test.ts::"refunds without amount"` AND `src/providers/adyen/adyen.test.ts::"fetches a payment"` AND `src/providers/adyen/adyen.test.ts::"maps result Authorised to authorized"` AND `src/providers/adyen/adyen.test.ts::"maps result Refused to failed"` AND `src/providers/adyen/adyen.test.ts::"maps result Cancelled to canceled"` AND `src/providers/adyen/adyen.test.ts::"maps result Error to failed"` AND `src/providers/adyen/adyen.test.ts::"maps result Pending to pending"` AND `src/providers/adyen/adyen.test.ts::"maps result Received to pending"` AND `src/providers/adyen/adyen.test.ts::"maps result RedirectShopper to pending"` AND `src/providers/adyen/adyen.test.ts::"maps result IdentifyShopper to pending"` AND `src/providers/adyen/adyen.test.ts::"maps result ChallengeShopper to pending"` AND `src/providers/adyen/adyen.test.ts::"maps result PresentToShopper to pending"` AND `src/providers/adyen/adyen.test.ts::"maps unknown result to failed"` AND `src/providers/adyen/adyen.test.ts::"maps payment without paymentMethod"` AND `src/providers/adyen/adyen.test.ts::"maps non-scheme payment type as wallet"` AND `src/providers/adyen/adyen.test.ts::"maps refund received to pending"` AND `src/providers/adyen/adyen.test.ts::"maps other refund statuses to failed"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user