first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
# @suaveplan/payment-provider-stripe - Universal Stripe adapter for @suaveplan/payment. Implements PaymentProvider against the Stripe REST API via the slim @suaveplan/payment-providers foundation: Bearer auth on every call, Idempotency-Key forwarding on every mutating call, exhaustive Stripe status/refund-status/refund-reason mapping, payment-method normalisation, every error raised as PaymentProviderError with provider: 'stripe' discriminant. No Node / browser / React deps; every HTTP call flows through the injectable FetchLike from the slim core.
Universal Stripe adapter for `@suaveplan/payment`. Implements
`PaymentProvider` against the Stripe REST API using the slim
`@suaveplan/payment-providers` foundation: Bearer auth on every call,
`Idempotency-Key` forwarding on every mutating call, exhaustive
Stripe → domain status / refund-status / refund-reason mapping,
payment-method normalisation, every error raised as
`PaymentProviderError` with `provider: "stripe"` discriminant.
This package is one of the five sibling adapters that re-landed
after `2026-05-17-slim-payment-providers` split the bundled
`@suaveplan/payment-providers` into a foundation-with-sibling-plugins
shape.
<!-- begin:auto-docs -->
## Documentation tree
- [stripe](./src/stripe/stripe.md) — `StripeProvider` is the Stripe adapter for `@suaveplan/payment`. It implements the `PaymentProvider` contract (six meth…
- [stripe](./src/stripe/stripe.md) — `StripeProvider` is the Stripe adapter for `@suaveplan/payment`. It implements the `PaymentProvider` contract (six meth…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/payment-provider-stripe \
@suaveplan/payment-providers \
@suaveplan/payment
```
## Quick start
```ts
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",
idempotencyKey: crypto.randomUUID(),
});
```
## Compose with the foundation registry
```ts
import { ProviderRegistry } from "@suaveplan/payment-providers";
import { StripeProvider } from "@suaveplan/payment-provider-stripe";
const registry = new ProviderRegistry();
registry.register(
new StripeProvider({ apiKey: process.env.STRIPE_SECRET_KEY ?? "" })
);
const stripe = registry.get("stripe"); // typed as PaymentProvider
```
## Features
- **`StripeProvider`** — implements `PaymentProvider` from
`@suaveplan/payment` (literal `name: "stripe"`, six lifecycle
methods).
- **Bearer authentication** on every request (read + write).
- **`Idempotency-Key` forwarding** — when the caller supplies
`idempotencyKey` on `CreatePaymentIntentInput` or `RefundInput`,
the provider forwards it verbatim to Stripe so retries collapse
into a single side effect.
- **Exhaustive status mappings** — seven Stripe payment-intent
statuses, three Stripe refund statuses, four Stripe refund reasons,
every unmapped value falls back to `failed` / `other`.
- **Payment-method normalisation** — `card` carries `brand` + `last4`;
any other Stripe `type` maps to `wallet`; absent payment method
maps to `undefined`.
- **Typed errors** — every failure is a `PaymentProviderError` from
the slim core with `provider: "stripe"` discriminant.
- **Universal tier** — zero Node, browser, React, or
platform-specific imports. Runs unchanged on Node, Bun, browsers,
and Web Workers.
- **Tree-shakeable** — `sideEffects: false` plus per-feature subpath
exports.
- **100% test coverage** (Istanbul) and **100% docs coverage**.
## Modules
- [`src/stripe/stripe.md`](./src/stripe/stripe.md) — `StripeProvider`
class, `StripeConfig`, `StripeIntent` / `StripeRefund` wire-format
interfaces.
## Related
- `@suaveplan/payment` — `PaymentEngine`, `WebhookVerifier`,
`PaymentProvider` interface, `PaymentIntent` / `Refund` / status
enums.
- `@suaveplan/payment-providers` — slim foundation with
`ProviderRegistry`, `httpRequest`, `composeAbortSignal`,
`PaymentProviderError`, shared config shapes.
- `@suaveplan/payment-provider-paypal`,
`@suaveplan/payment-provider-square`,
`@suaveplan/payment-provider-mollie`,
`@suaveplan/payment-provider-adyen` — sibling provider packages.
## License
MIT

View File

@@ -0,0 +1,201 @@
# StripeProvider
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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:
```ts
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`
```ts
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.