Files
skills/docs/money/payment/payment-provider-stripe/src/stripe/stripe.md
2026-05-29 08:20:56 +01:00

8.1 KiB

StripeProvider

(no submodules)

Purpose

StripeProvider is the Stripe adapter for @suaveplan/payment. It implements the PaymentProvider contract (six methods covering the payment-intent + refund lifecycle) against the Stripe REST API, using the slim @suaveplan/payment-providers foundation for every HTTP call. The class adds nothing beyond what PaymentProvider requires: no Stripe-specific surface bleeds into the consumer; no in-flight state is held between calls. Provider construction takes the same ApiKeyConfig shape every API-key-based sibling uses (apiKey, optional baseUrl, optional webhookSecret, plus the ProviderCommonOptions injectable seams — fetch, timeoutMs, signal).

Every mutating Stripe call (createIntent, refund) forwards the caller-supplied idempotencyKey as the standard Idempotency-Key HTTP header so retries against Stripe collapse into a single side effect. Bearer auth (Authorization: Bearer <apiKey>) is sent on every call, including read-only getIntent, so the provider works against either a Stripe live secret or test secret without any further wiring.

Stripe lifecycle states are mapped exhaustively into the domain enums from @suaveplan/payment: seven explicit Stripe status strings collapse into the five user-visible PaymentStatus values, an unknown status maps to failed, refund statuses collapse to succeeded / pending / failed, refund reasons collapse to the four RefundReason values with other as the fallback.

Features

  • Implements PaymentProvider from @suaveplan/payment — class with literal name: "stripe", all six methods, every return value conforms to PaymentIntent / Refund.
  • Routes every HTTP call through the slim-core httpRequest helper — inherits per-call timeout, AbortSignal composition, typed errors, empty-body handling, JSON parse-error handling.
  • Bearer authentication on every request (read + write).
  • Idempotency-Key header forwarded verbatim when the caller supplies idempotencyKey on CreatePaymentIntentInput or RefundInput. Omitted on read calls and on writes where the caller did not supply a key.
  • Exhaustive Stripe → domain status / refund-status / refund-reason mappers; unknown values fall back to failed / other.
  • Payment-method normalisation: Stripe payment_method.type === "card" maps to "card" with brand and last4; any other type maps to "wallet"; absent payment method maps to undefined.
  • Every raised PaymentProviderError carries provider: "stripe" so consumers running multiple providers can discriminate without parsing messages.
  • Universal — no node:*, no bun:*, no platform-specific imports anywhere under src/. Runs unchanged on Node, Bun, browsers, and Web Workers.

Usage: Basic

import { PaymentEngine } from "@suaveplan/payment";
import { StripeProvider } from "@suaveplan/payment-provider-stripe";

const provider = new StripeProvider({
    apiKey: process.env.STRIPE_SECRET_KEY ?? "",
});

const engine = new PaymentEngine({ provider });

const intent = await engine.createIntent({
    amount: { amount: 5000n, currency: "USD" },
    captureMethod: "automatic",
});

Usage: Advanced

Compose multiple providers via the slim-core ProviderRegistry, inject a custom FetchLike for tracing/retries, scope every call to a caller-supplied AbortSignal, and forward an Idempotency-Key through the engine to collapse retried createIntent calls into a single Stripe side effect:

import {
    ProviderRegistry,
    type FetchLike,
} from "@suaveplan/payment-providers";
import { StripeProvider } from "@suaveplan/payment-provider-stripe";

const tracingFetch: FetchLike = async (url, init) => {
    const start = performance.now();
    try {
        return await globalThis.fetch(url, init);
    } finally {
        // emit a span with (url, performance.now() - start)
    }
};

const ac = new AbortController();
const provider = new StripeProvider({
    apiKey: process.env.STRIPE_SECRET_KEY ?? "",
    baseUrl: "https://api.stripe.com/v1",
    webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
    fetch: tracingFetch,
    timeoutMs: 10_000,
    signal: ac.signal,
});

const registry = new ProviderRegistry();
registry.register(provider);

const intent = await registry.get("stripe").createIntent({
    amount: { amount: 12_99n, currency: "USD" },
    captureMethod: "manual",
    idempotencyKey: crypto.randomUUID(),
});

API Reference

class StripeProvider implements PaymentProvider

new StripeProvider(config: StripeConfig)
  • name — "stripe" (literal type, frozen on the class).
  • createIntent(input: CreatePaymentIntentInput): Promise<PaymentIntent> — POST /payment_intents. Maps input.amount.amount to Stripe's minor-unit integer (Number), lowercases the currency, forwards customer / description / metadata / payment_method when supplied. Forwards Idempotency-Key from input.idempotencyKey.
  • confirmIntent(intentId): Promise<PaymentIntent> — POST /payment_intents/:id/confirm. No body, no Idempotency-Key.
  • captureIntent(intentId): Promise<PaymentIntent> — POST /payment_intents/:id/capture. No body, no Idempotency-Key.
  • cancelIntent(intentId): Promise<PaymentIntent> — POST /payment_intents/:id/cancel. No body, no Idempotency-Key.
  • refund(input: RefundInput): Promise<Refund> — POST /refunds. Forwards amount / reason / metadata when supplied. Forwards Idempotency-Key from input.idempotencyKey.
  • getIntent(intentId): Promise<PaymentIntent> — GET /payment_intents/:id. No body, no Idempotency-Key.

type StripeConfig = ApiKeyConfig

Construction shape — alias for the shared ApiKeyConfig from @suaveplan/payment-providers. Re-exported by name so consumers can pin construction shape locally:

  • apiKey: string — Stripe secret key (live or test).
  • baseUrl?: string — defaults to https://api.stripe.com/v1.
  • webhookSecret?: string — surfaced for downstream WebhookVerifier wiring; not used by the provider itself.
  • fetch?: FetchLike — defaults to globalThis.fetch.
  • timeoutMs?: number — per-call timeout (default 30 000 ms).
  • signal?: AbortSignal — caller cancellation, composed with the internal timeout signal.

Error semantics

Every failure raised under StripeProvider is a PaymentProviderError from the slim core. Possible codes:

  • PAYMENT_PROVIDER.HTTP_ERROR — Stripe returned non-2xx; httpStatus carries the status code, bodyExcerpt carries up to 256 chars of the response.
  • PAYMENT_PROVIDER.TIMEOUT — the per-call timeout fired before Stripe responded.
  • PAYMENT_PROVIDER.PARSE_ERROR — Stripe returned a malformed JSON response.

Every raised error carries provider: "stripe" so consumers discriminate without parsing messages.

Implementation Notes

  • The provider holds no in-flight Promise cache and no token cache — Stripe uses static Bearer auth, so there's nothing to memoise. Idempotency is delegated to Stripe via the Idempotency-Key header; the provider just forwards the caller-supplied key.
  • The status / refund-status / refund-reason mappings are exhaustive and tested per-row. The fallback failed / other branches are exercised by passing an unmapped Stripe enum value.
  • mapPaymentMethod exists as a small dedicated helper so the card vs. wallet vs. absent branches stay testable in isolation; consumers can rely on paymentMethod.type === "card" always carrying brand and last4 when Stripe surfaced them.
  • The commonOptions private accessor exists so every method forwards fetch, signal, timeoutMs, and the provider field in lockstep. Calling sites stay symmetric — adding a new PaymentProvider method only needs to spread this.commonOptions() into the httpRequest call.
  • package.json declares "sideEffects": false and the only runtime imports are @suaveplan/payment (types-only) and @suaveplan/payment-providers (one named symbol: httpRequest). The dep-graph test pins universal purity.