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,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.