Initial Commit
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# Add @suaveplan/payment-provider-stripe sibling adapter
|
||||
|
||||
**Package**: `@suaveplan/payment-provider-stripe`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
|
||||
## Why
|
||||
|
||||
The `2026-05-17-slim-payment-providers` change extracted the five
|
||||
concrete provider adapters (Stripe, PayPal, Square, Mollie, Adyen) out
|
||||
of `@suaveplan/payment-providers` and into sibling packages so
|
||||
consumers pay only for the providers they install. This change lands
|
||||
the **Stripe** sibling.
|
||||
|
||||
`@suaveplan/payment-provider-stripe` re-lands the `StripeProvider`
|
||||
class (previously `@suaveplan/payment-providers.StripeProvider` prior
|
||||
to the slim-down) verbatim against the slim foundation. It implements
|
||||
`PaymentProvider` from `@suaveplan/payment`, uses `httpRequest` from
|
||||
the slim foundation for every HTTP call, forwards Bearer auth and the
|
||||
caller-supplied `Idempotency-Key` header on every mutating request,
|
||||
maps Stripe payment-intent / refund status codes to the domain
|
||||
`PaymentStatus` / `RefundStatus` enums, and raises only
|
||||
`PaymentProviderError`.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `@suaveplan/payment-provider-stripe` package at
|
||||
`packages/universal/payment-provider-stripe/` (`0.1.0`, universal
|
||||
tier, `sideEffects: false`).
|
||||
- ADDED: `class StripeProvider implements PaymentProvider` with
|
||||
literal `name: "stripe"`. Constructor accepts a `StripeConfig`
|
||||
extending `ApiKeyConfig` (Stripe secret key + optional
|
||||
`webhookSecret`, plus the shared `ProviderCommonOptions`).
|
||||
- ADDED: All six `PaymentProvider` methods — `createIntent`,
|
||||
`confirmIntent`, `captureIntent`, `cancelIntent`, `refund`,
|
||||
`getIntent` — each routed through `httpRequest` against the Stripe
|
||||
REST API.
|
||||
- ADDED: Stripe-domain → `PaymentIntent` / `Refund` mappers
|
||||
(`mapIntent`, `mapRefund`, `mapStatus`, `mapRefundStatus`,
|
||||
`mapRefundReason`) that translate every Stripe lifecycle state into
|
||||
the `@suaveplan/payment` enums.
|
||||
- ADDED: `StripeConfig` type re-export so consumers can pin the
|
||||
construction shape without reaching into the slim core for
|
||||
`ApiKeyConfig`.
|
||||
- ADDED: Subpath exports for `./stripe/stripe` so consumers can
|
||||
tree-shake the provider class directly.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Webhook signature verification — `@suaveplan/payment` already ships
|
||||
the `WebhookVerifier`; this package only carries the
|
||||
`webhookSecret` field on `StripeConfig` for downstream wiring.
|
||||
- OpenTelemetry spans / counters on the provider — defers to a
|
||||
monorepo-wide observability pass tracking
|
||||
`@suaveplan/payment`-level instrumentation.
|
||||
- Stripe Connect, Customer / Setup-intent / Subscription APIs — the
|
||||
`PaymentProvider` contract only covers payment-intent + refund
|
||||
lifecycle; non-contract Stripe surfaces are deferred to dedicated
|
||||
follow-up packages.
|
||||
- Currency conversion — Stripe's minor-unit (cents) amount is mapped
|
||||
to `MoneyLike.amount: bigint` 1:1 with currency uppercased; no
|
||||
zero-decimal currency adjustment is performed in this change
|
||||
(parity with the pre-slim shape).
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/enterprise/payment-providers.md`
|
||||
and the verbatim upstream Stripe implementation that was bundled into
|
||||
the just-slimmed `@suaveplan/payment-providers` package.
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Universal-tier purity (no Node / browser / React deps) | `enterprise/payment-providers.md` L37 | verbatim cross-cutting rule |
|
||||
| Zero `Math.random` / `Date.now` at module scope; injectable seams | `enterprise/payment-providers.md` L40 | satisfied via shared `ProviderCommonOptions` (fetch/timeoutMs/signal) |
|
||||
| Structured errors extending `@suaveplan/error` | `enterprise/payment-providers.md` L42, L61 | uses `PaymentProviderError` from the slim core; raises no raw `Error` |
|
||||
| `AbortSignal` everywhere async, per-call timeout | `enterprise/payment-providers.md` L43 | inherits `httpRequest` composition of caller `signal` + internal timeout |
|
||||
| Idempotency-key wiring on mutating calls | `enterprise/payment-providers.md` L44 | `createIntent` / `refund` forward `Idempotency-Key` when caller supplies it |
|
||||
| Tree-shakeable, `sideEffects: false` | `enterprise/payment-providers.md` L55 | verbatim |
|
||||
| `workspace:^` internal deps + Bun catalog externals | `enterprise/payment-providers.md` L56 | verbatim |
|
||||
| 100% Istanbul coverage + 100% docs coverage | `enterprise/payment-providers.md` L50-L51 | genesis-wide policy as well |
|
||||
| File-size budget (250 soft / 300 hard / 500 test) | `enterprise/payment-providers.md` L52 | verbatim |
|
||||
| StripeProvider full lifecycle (`createIntent` / `confirmIntent` / `captureIntent` / `cancelIntent` / `refund` / `getIntent`) | `/mnt/s/web/suaveplan/packages/universal/payment-providers/src/stripe.ts` L130-L209 | verbatim port against slim foundation; previously folded as a removed requirement by `2026-05-17-slim-payment-providers` (`specs/payment-providers/spec.md` "Requirement: StripeProvider full lifecycle") |
|
||||
| Stripe status → `PaymentStatus` mapping table | `/mnt/s/web/suaveplan/packages/universal/payment-providers/src/stripe.ts` L45-L56 | verbatim 7-row mapping including `unknown → failed` fallback |
|
||||
| Stripe refund status → `RefundStatus` mapping | `/mnt/s/web/suaveplan/packages/universal/payment-providers/src/stripe.ts` L58-L62 | verbatim 3-branch mapping (`succeeded` / `pending` / `failed`) |
|
||||
| Stripe refund reason → `RefundReason` mapping | `/mnt/s/web/suaveplan/packages/universal/payment-providers/src/stripe.ts` L104-L111 | verbatim 4-branch mapping (`duplicate` / `fraudulent` / `requested_by_customer` / `other`) |
|
||||
| Bearer auth header on every request | `/mnt/s/web/suaveplan/packages/universal/payment-providers/src/stripe.ts` L211-L217 | verbatim `Authorization: Bearer <apiKey>` header |
|
||||
| `StripeProvider.name === "stripe"` | `/mnt/s/web/suaveplan/packages/universal/payment-providers/src/stripe.ts` L119 | verbatim literal name |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `provider: "stripe"` discriminant on every raised `PaymentProviderError` | Slim-foundation `httpRequest` accepts a `provider` field on `HttpOptions`; wiring it on every call lets consumers discriminate Stripe failures from sibling-package failures without parsing messages. Not required by the upstream file but mandatory for the slim-core contract. |
|
||||
| Injectable `fetch` / `signal` / `timeoutMs` forwarded to `httpRequest` on every call | The upstream file held its own `timeoutMs` but never forwarded the caller's external `signal` or `fetch`. Required by the slim foundation (every sibling MUST flow through `ProviderCommonOptions`); upgraded from the upstream surface in this migration. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
Every requirement in `specs/payment-provider-stripe/spec.md` carries a
|
||||
`**Verified by:**` line citing concrete `it(...)` titles in:
|
||||
|
||||
- `src/stripe/stripe.test.ts` — every `PaymentProvider` method, the
|
||||
Bearer / `Idempotency-Key` header forwarding, every status / refund-
|
||||
status / refund-reason branch, `provider` discriminant on raised
|
||||
errors, injected `FetchLike` + `signal` forwarding.
|
||||
- `tests/dep-graph.test.ts` — pins universal-tier purity (no
|
||||
`node:` / `bun:` / `fs` / `path` / etc imports under `src/`).
|
||||
|
||||
Tests use a real recording `FetchLike` closure — no `vi.mock`, no
|
||||
`vi.stubGlobal`. The Stripe REST API surface is exercised purely
|
||||
through the injected fetch.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. This package is brand new under
|
||||
`packages/universal/payment-provider-stripe/` and is not yet consumed
|
||||
by any other package in the monorepo. Consumers that previously
|
||||
imported `StripeProvider` from `@suaveplan/payment-providers` (none
|
||||
exist on genesis; that surface was removed by
|
||||
`2026-05-17-slim-payment-providers` before any consumer landed) will
|
||||
import from the new package per the slim-core migration table.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/payment-provider-stripe` to
|
||||
enumerate. The package is brand new with no current dependents.
|
||||
|
||||
| Dependent | Action |
|
||||
|---|---|
|
||||
| (none) | [x] no action needed — new package, no consumers yet |
|
||||
|
||||
Future dependents will be: `@suaveplan/api-plugin-payment` (when it
|
||||
lands as a Stripe-configurable host) and any application-layer
|
||||
package that wires the `PaymentEngine` to Stripe.
|
||||
@@ -0,0 +1,258 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: payment-provider-stripe (add-payment-provider-stripe)
|
||||
|
||||
**Package**: `@suaveplan/payment-provider-stripe`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at
|
||||
> authoring time. This is the inaugural delta for the
|
||||
> `payment-provider-stripe` capability — every block is `## ADDED
|
||||
> Requirements`.
|
||||
|
||||
## ADDED 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 `StripeProvider` instance 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 `FetchLike` starts with `https://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 `PaymentProviderError`s 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 `FetchLike` receives one call with `method: "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 `FetchLike` receives 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 `FetchLike` receives 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 `FetchLike` receives 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 `FetchLike` receives one call with URL ending `/refunds`, body `payment_intent: "pi_123"`, `amount: 2000`, `reason: "requested_by_customer"`
|
||||
|
||||
#### Scenario: getIntent GETs /payment_intents/:id
|
||||
|
||||
- **WHEN** `getIntent("pi_123")` is called
|
||||
- **THEN** the injected `FetchLike` receives one call with `method: "GET"`, URL ending `/payment_intents/pi_123`
|
||||
|
||||
#### Scenario: provider discriminant flows to raised errors
|
||||
|
||||
- **WHEN** the injected `FetchLike` returns a 500 response on any method
|
||||
- **THEN** the rejection is a `PaymentProviderError` whose `code === PAYMENT_PROVIDER.HTTP_ERROR` and `provider === "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 `PaymentProvider` method is invoked
|
||||
- **THEN** the `Authorization` header on the recorded fetch call equals `Bearer <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_1` header
|
||||
|
||||
#### Scenario: createIntent omits Idempotency-Key when not supplied
|
||||
|
||||
- **WHEN** `createIntent({ ... })` is called without `idempotencyKey`
|
||||
- **THEN** the recorded fetch call carries no `Idempotency-Key` header
|
||||
|
||||
#### 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_1` header
|
||||
|
||||
#### Scenario: refund omits Idempotency-Key when not supplied
|
||||
|
||||
- **WHEN** `refund({ paymentIntentId: "pi_123" })` is called without `idempotencyKey`
|
||||
- **THEN** the recorded fetch call carries no `Idempotency-Key` header
|
||||
|
||||
**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 `status` is any of the eight rows above
|
||||
- **THEN** the mapped `PaymentIntent.status` equals 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 `status` is any of the three rows above
|
||||
- **THEN** the mapped `Refund.status` equals 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 `reason` is any of the listed values, an unknown value, or absent
|
||||
- **THEN** the mapped `Refund.reason` equals 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.paymentMethod` equals `{ 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.type` equals `"wallet"`
|
||||
|
||||
#### Scenario: absent payment method maps to undefined
|
||||
|
||||
- **WHEN** the Stripe API returns a payment intent without `payment_method`
|
||||
- **THEN** the mapped `PaymentIntent.paymentMethod` is `undefined`
|
||||
|
||||
**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 `.ts` file under `src/` 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"`
|
||||
@@ -0,0 +1,94 @@
|
||||
# Tasks: add-payment-provider-stripe
|
||||
|
||||
> Every checkbox reflects work actually done in this change. No
|
||||
> retroactive ticking.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/payment-providers.md` end-to-end
|
||||
- [x] 1.2 Read `openspec/specs/payment-providers/spec.md` (slim-core canonical spec)
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
|
||||
- [x] 1.5 Fill in `## Source mapping` — every requirement cites a line
|
||||
range in the upstream source or appears in the "no source line"
|
||||
table with justification
|
||||
- [x] 1.6 Draft `specs/payment-provider-stripe/spec.md` from
|
||||
`openspec/templates/spec-delta.md`
|
||||
- [x] 1.7 Every requirement in the spec delta carries a
|
||||
`**Verified by:**` line naming concrete `it(...)` titles
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/universal/payment-provider-stripe/`
|
||||
(`package.json` at `0.1.0`, `vite.config.ts`, `vitest.config.ts`,
|
||||
`tsconfig.json`, `tsconfig.lib.json`)
|
||||
- [x] 2.2 Implement `src/stripe/stripe.types.ts` — `StripeConfig`,
|
||||
`StripeIntent`, `StripeRefund` wire-format types
|
||||
- [x] 2.3 Implement `src/stripe/stripe.ts` — `StripeProvider` class,
|
||||
every `PaymentProvider` method routed through `httpRequest` with
|
||||
Bearer auth + `Idempotency-Key` forwarding, mappers from Stripe
|
||||
domain to `PaymentIntent` / `Refund`, every error raised through
|
||||
the slim-core `PaymentProviderError`
|
||||
- [x] 2.4 `src/stripe/index.ts` — explicit named barrel
|
||||
- [x] 2.5 `src/index.ts` — package root barrel re-exporting
|
||||
`StripeProvider` + `StripeConfig`
|
||||
- [x] 2.6 `package.json#exports` — root + `./stripe/stripe` subpath
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 `src/stripe/stripe.test.ts` — covers every public method,
|
||||
Bearer-auth header presence, `Idempotency-Key` forwarding when
|
||||
caller supplies it AND absence when caller omits, every status /
|
||||
refund-status / refund-reason branch, payment-method absence,
|
||||
`provider: "stripe"` discriminant propagation on raised errors,
|
||||
injected `FetchLike` + caller `signal` forwarding
|
||||
- [x] 3.2 `tests/dep-graph.test.ts` — pins universal-tier purity (no
|
||||
`node:` / `bun:` / `fs` / `path` / `stream` / `net` / `http` /
|
||||
`crypto` imports under `src/`)
|
||||
- [x] 3.3 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
- [x] 3.4 Zero `vi.mock` / `vi.stubGlobal` / `jest.mock` — all tests
|
||||
use real recording `FetchLike` closures
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 `src/stripe/stripe.md` — sibling doc with Purpose,
|
||||
Features, Basic Usage, Advanced Usage, API Reference,
|
||||
Implementation Notes (≥200 words excl. code)
|
||||
- [x] 4.2 `README.md` at package root linking the module doc under
|
||||
`## Modules`
|
||||
- [x] 4.3 Update `CLAUDE.md` package listing under the Network or
|
||||
Foundation section — add `payment-provider-stripe` to the
|
||||
`payment-providers` family description
|
||||
|
||||
## 5. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [x] 5.1 `bun run check` — Biome 0/0/0 across the monorepo
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/payment-provider-stripe` — green
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/payment-provider-stripe` — 100/100/100/100
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/payment-provider-stripe` — exit 0 (PASS 0 error, 0 warn, 0 info)
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/payment-provider-stripe` — green
|
||||
|
||||
## 6. Pre-archive 9-row checklist (AGENTS.md Section 13)
|
||||
|
||||
- [x] 6.1 Generics threaded; literal `name: "stripe"` pinned at the type level
|
||||
- [x] 6.2 Wire-format Stripe responses parsed through typed `StripeIntent` / `StripeRefund` interfaces
|
||||
- [x] 6.3 Every slim-core error code raised by `httpRequest` flows through this provider with `provider: "stripe"` discriminant
|
||||
- [x] 6.4 N/A — no strict-mode toggle on the provider
|
||||
- [x] 6.5 N/A — no OTel instruments in this change
|
||||
- [x] 6.6 N/A — provider holds no in-flight dedup state
|
||||
- [x] 6.7 No retroactive tick
|
||||
- [x] 6.8 No `export *` in any feature barrel — scan returns zero output
|
||||
- [x] 6.9 No direct `vitest` / `bun:test` imports — scan returns zero output
|
||||
|
||||
## 7. Root-doc refresh (§13b)
|
||||
|
||||
- [x] 7.1 `bun scripts/folder-reorg/generate-manifest.ts` — `MANIFEST.md` refreshed (258 packages, 34 plugins; new row for payment-provider-stripe)
|
||||
- [ ] 7.2 `bun scripts/generate-missing.ts` — DEFERRED: pre-existing failure on missing `FOLDERS.yml` (unrelated to this change)
|
||||
- [x] 7.3 Added `@suaveplan/payment-provider-stripe` row to `STATUS.md`
|
||||
- [x] 7.4 Added per-package paragraph to `SUMMARY.md`
|
||||
- [x] 7.5 `bunx markdownlint-cli2 --fix` — exit 0; no findings in touched files
|
||||
|
||||
## 8. Archive
|
||||
|
||||
- [x] 8.1 Fold `specs/payment-provider-stripe/spec.md` into `openspec/specs/payment-provider-stripe/spec.md`
|
||||
- [x] 8.2 Move `openspec/changes/add-payment-provider-stripe/` → `openspec/archive/2026-05-17-add-payment-provider-stripe/`
|
||||
Reference in New Issue
Block a user