13 KiB
Capability Spec: payment-provider-stripe
Package: @suaveplan/payment-provider-stripe
Status: SHIPPED
Every requirement below carries a
**Verified by:**line.History: introduced by
2026-05-17-add-payment-provider-stripeas the inaugural folded spec — the Stripe sibling adapter on the2026-05-17-slim-payment-providersfoundation.
Requirements
Requirement: StripeProvider implements PaymentProvider with literal name
The package SHALL export a StripeProvider class that implements the
PaymentProvider interface from @suaveplan/payment. The class SHALL
expose a readonly name field whose value is the string literal
"stripe" so consumers (including ProviderRegistry) can discriminate
it at both the type and value level. The constructor SHALL accept a
StripeConfig extending ApiKeyConfig from
@suaveplan/payment-providers — apiKey (Stripe secret key,
required), optional baseUrl (defaults to https://api.stripe.com/v1),
optional webhookSecret, plus every ProviderCommonOptions seam
(fetch, timeoutMs, signal).
Scenario: name is the literal "stripe"
- WHEN a
StripeProviderinstance is constructed with any config - THEN
provider.name === "stripe"and the field is typed as the literal"stripe"
Scenario: defaults to api.stripe.com when baseUrl omitted
- WHEN
new StripeProvider({ apiKey: "sk_test_x" })is constructed and any method is invoked - THEN the URL passed to the injected
FetchLikestarts withhttps://api.stripe.com/v1
Verified by: src/stripe/stripe.test.ts::"exposes literal name 'stripe'" AND src/stripe/stripe.test.ts::"defaults baseUrl to api.stripe.com/v1 when omitted"
Requirement: All six PaymentProvider methods route through slim-core httpRequest
The class SHALL implement every method on the PaymentProvider
contract — createIntent, confirmIntent, captureIntent,
cancelIntent, refund, getIntent — and every implementation SHALL
delegate its single HTTP call to httpRequest from
@suaveplan/payment-providers. The provider SHALL forward the
caller-supplied fetch, signal, and timeoutMs from its
constructor StripeConfig to every httpRequest call so external
cancellation and timeout overrides flow through unchanged. Every
httpRequest call SHALL set provider: "stripe" on its options so
raised PaymentProviderErrors carry the Stripe discriminant.
Scenario: createIntent POSTs to /payment_intents
- WHEN
createIntent({ amount: { amount: 5000n, currency: "USD" }, captureMethod: "manual", customerId: "cust_1", description: "Test", metadata: { k: "v" }, paymentMethodId: "pm_1" })is called - THEN the injected
FetchLikereceives one call withmethod: "POST", URL ending/payment_intents, JSON body containing the Stripe-cased fields (amount: 5000,currency: "usd",capture_method: "manual",customer: "cust_1",description: "Test",metadata: { k: "v" },payment_method: "pm_1")
Scenario: confirmIntent POSTs to /payment_intents/:id/confirm
- WHEN
confirmIntent("pi_123")is called - THEN the injected
FetchLikereceives one call with URL ending/payment_intents/pi_123/confirm
Scenario: captureIntent POSTs to /payment_intents/:id/capture
- WHEN
captureIntent("pi_123")is called - THEN the injected
FetchLikereceives one call with URL ending/payment_intents/pi_123/capture
Scenario: cancelIntent POSTs to /payment_intents/:id/cancel
- WHEN
cancelIntent("pi_123")is called - THEN the injected
FetchLikereceives one call with URL ending/payment_intents/pi_123/cancel
Scenario: refund POSTs to /refunds
- WHEN
refund({ paymentIntentId: "pi_123", amount: { amount: 2000n, currency: "USD" }, reason: "requested_by_customer", metadata: {} })is called - THEN the injected
FetchLikereceives one call with URL ending/refunds, bodypayment_intent: "pi_123",amount: 2000,reason: "requested_by_customer"
Scenario: getIntent GETs /payment_intents/:id
- WHEN
getIntent("pi_123")is called - THEN the injected
FetchLikereceives one call withmethod: "GET", URL ending/payment_intents/pi_123
Scenario: provider discriminant flows to raised errors
- WHEN the injected
FetchLikereturns a 500 response on any method - THEN the rejection is a
PaymentProviderErrorwhosecode === PAYMENT_PROVIDER.HTTP_ERRORandprovider === "stripe"
Verified by: src/stripe/stripe.test.ts::"createIntent POSTs to /payment_intents with Stripe-cased body fields" AND src/stripe/stripe.test.ts::"confirmIntent POSTs to /payment_intents/:id/confirm" AND src/stripe/stripe.test.ts::"captureIntent POSTs to /payment_intents/:id/capture" AND src/stripe/stripe.test.ts::"cancelIntent POSTs to /payment_intents/:id/cancel" AND src/stripe/stripe.test.ts::"refund POSTs to /refunds with snake_case body" AND src/stripe/stripe.test.ts::"getIntent GETs /payment_intents/:id" AND src/stripe/stripe.test.ts::"surfaces non-ok responses as PaymentProviderError with provider 'stripe'"
Requirement: Bearer auth header on every request
Every HTTP call originated by StripeProvider SHALL include an
Authorization: Bearer <apiKey> header where <apiKey> is the
constructor-supplied secret. This header SHALL be present on read
calls (getIntent) as well as mutating calls
(createIntent/confirmIntent/captureIntent/cancelIntent/refund).
Scenario: Bearer header present on every method
- WHEN any
PaymentProvidermethod is invoked - THEN the
Authorizationheader on the recorded fetch call equalsBearer <constructor-apiKey>
Verified by: src/stripe/stripe.test.ts::"includes Bearer Authorization header on every method"
Requirement: Idempotency-Key forwarded when caller supplies it
When the caller supplies an idempotencyKey on CreatePaymentIntentInput
or RefundInput, StripeProvider SHALL forward it as an
Idempotency-Key header on the matching POST request. When the
caller omits idempotencyKey, no Idempotency-Key header SHALL be
present on the request. Read-only requests (getIntent) SHALL never
carry an Idempotency-Key header (the Stripe API ignores it on GET).
Scenario: createIntent forwards Idempotency-Key when supplied
- WHEN
createIntent({ ..., idempotencyKey: "idem_1" })is called - THEN the recorded fetch call carries an
Idempotency-Key: idem_1header
Scenario: createIntent omits Idempotency-Key when not supplied
- WHEN
createIntent({ ... })is called withoutidempotencyKey - THEN the recorded fetch call carries no
Idempotency-Keyheader
Scenario: refund forwards Idempotency-Key when supplied
- WHEN
refund({ ..., idempotencyKey: "ref_idem_1" })is called - THEN the recorded fetch call carries an
Idempotency-Key: ref_idem_1header
Scenario: refund omits Idempotency-Key when not supplied
- WHEN
refund({ paymentIntentId: "pi_123" })is called withoutidempotencyKey - THEN the recorded fetch call carries no
Idempotency-Keyheader
Verified by: src/stripe/stripe.test.ts::"forwards Idempotency-Key header on createIntent when supplied" AND src/stripe/stripe.test.ts::"omits Idempotency-Key header on createIntent when not supplied" AND src/stripe/stripe.test.ts::"forwards Idempotency-Key header on refund when supplied" AND src/stripe/stripe.test.ts::"omits Idempotency-Key header on refund when not supplied"
Requirement: Stripe payment-intent status mapping
StripeProvider SHALL map Stripe payment-intent status strings to
PaymentStatus enum values using the following exhaustive table:
| Stripe status | PaymentStatus |
|---|---|
requires_payment_method |
created |
requires_confirmation |
created |
requires_action |
pending |
processing |
pending |
requires_capture |
authorized |
succeeded |
captured |
canceled |
canceled |
| any other / unknown | failed |
Scenario: every documented mapping resolves correctly
- WHEN the Stripe API returns a payment intent whose
statusis any of the eight rows above - THEN the mapped
PaymentIntent.statusequals the right-hand column
Verified by: src/stripe/stripe.test.ts::"maps Stripe status 'requires_payment_method' to 'created'" AND src/stripe/stripe.test.ts::"maps Stripe status 'requires_confirmation' to 'created'" AND src/stripe/stripe.test.ts::"maps Stripe status 'requires_action' to 'pending'" AND src/stripe/stripe.test.ts::"maps Stripe status 'processing' to 'pending'" AND src/stripe/stripe.test.ts::"maps Stripe status 'requires_capture' to 'authorized'" AND src/stripe/stripe.test.ts::"maps Stripe status 'succeeded' to 'captured'" AND src/stripe/stripe.test.ts::"maps Stripe status 'canceled' to 'canceled'" AND src/stripe/stripe.test.ts::"maps unknown Stripe status to 'failed'"
Requirement: Stripe refund status mapping
StripeProvider SHALL map Stripe refund status strings to
RefundStatus enum values using the following exhaustive table:
| Stripe refund status | RefundStatus |
|---|---|
succeeded |
succeeded |
pending |
pending |
| any other / unknown | failed |
Scenario: every documented mapping resolves correctly
- WHEN the Stripe API returns a refund whose
statusis any of the three rows above - THEN the mapped
Refund.statusequals the right-hand column
Verified by: src/stripe/stripe.test.ts::"maps Stripe refund status 'succeeded' to 'succeeded'" AND src/stripe/stripe.test.ts::"maps Stripe refund status 'pending' to 'pending'" AND src/stripe/stripe.test.ts::"maps unknown Stripe refund status to 'failed'"
Requirement: Stripe refund reason mapping
StripeProvider SHALL map Stripe refund reason strings to
RefundReason enum values using the following exhaustive table:
| Stripe reason | RefundReason |
|---|---|
duplicate |
duplicate |
fraudulent |
fraudulent |
requested_by_customer |
requested_by_customer |
| any other / unknown | other |
When the Stripe refund object omits reason entirely, the mapped
Refund.reason SHALL be "other".
Scenario: every documented mapping resolves correctly
- WHEN the Stripe API returns a refund whose
reasonis any of the listed values, an unknown value, or absent - THEN the mapped
Refund.reasonequals the right-hand column (or"other"when absent)
Verified by: src/stripe/stripe.test.ts::"maps Stripe refund reason 'duplicate' to 'duplicate'" AND src/stripe/stripe.test.ts::"maps Stripe refund reason 'fraudulent' to 'fraudulent'" AND src/stripe/stripe.test.ts::"maps Stripe refund reason 'requested_by_customer' to 'requested_by_customer'" AND src/stripe/stripe.test.ts::"maps unknown Stripe refund reason to 'other'" AND src/stripe/stripe.test.ts::"maps absent Stripe refund reason to 'other'"
Requirement: Payment-method mapping (card → wallet → absent)
When the Stripe payment intent carries a payment_method object,
StripeProvider SHALL surface it on the returned PaymentIntent as
a PaymentMethod with id, type ("card" if Stripe reports a card
type, otherwise "wallet"), brand (from payment_method.card.brand
when present), and last4 (from payment_method.card.last4 when
present). When the Stripe payment intent omits payment_method, the
mapped PaymentIntent.paymentMethod SHALL be undefined.
Scenario: card payment method is mapped to type 'card' with brand and last4
- WHEN the Stripe API returns a payment intent with
payment_method: { id: "pm_1", type: "card", card: { brand: "visa", last4: "4242" } } - THEN the mapped
PaymentIntent.paymentMethodequals{ id: "pm_1", type: "card", brand: "visa", last4: "4242" }
Scenario: non-card payment method maps to type 'wallet'
- WHEN the Stripe API returns a payment intent with
payment_method: { id: "pm_w", type: "us_bank_account" } - THEN the mapped
PaymentIntent.paymentMethod.typeequals"wallet"
Scenario: absent payment method maps to undefined
- WHEN the Stripe API returns a payment intent without
payment_method - THEN the mapped
PaymentIntent.paymentMethodisundefined
Verified by: src/stripe/stripe.test.ts::"maps card payment_method with brand and last4" AND src/stripe/stripe.test.ts::"maps non-card payment_method type to 'wallet'" AND src/stripe/stripe.test.ts::"maps absent payment_method to undefined"
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 slim-core httpRequest helper (and therefore
through the injectable FetchLike) so the package never reaches into
a runtime-specific HTTP stack.
Scenario: no platform imports in src/
- WHEN the dep-graph test walks every
.tsfile undersrc/and matches the banned-import regex - THEN the resulting offender array is empty
Verified by: tests/dep-graph.test.ts::"src contains no node/bun/fs/path/stream/net/http/crypto imports"