Initial Commit
This commit is contained in:
273
openspec/archive/2026-05-17-slim-payment-providers/design.md
Normal file
273
openspec/archive/2026-05-17-slim-payment-providers/design.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Design: slim-payment-providers (foundation-with-sibling-plugins)
|
||||
|
||||
This design doc is the **canonical cross-cutting architecture document for
|
||||
the `@suaveplan/payment-provider*` family**. Per `openspec/AGENTS.md` §0c
|
||||
(Multi-package / cross-cutting feature sets), the foundation package's
|
||||
`design.md` carries the shared model and each sibling package's `design.md`
|
||||
references this file by relative path rather than duplicating the shape.
|
||||
|
||||
## Why a foundation-with-sibling-plugins layout
|
||||
|
||||
The just-shipped `2026-05-17-add-payment-providers` migration packed five
|
||||
concrete adapters (Stripe, PayPal, Square, Mollie, Adyen) into a single
|
||||
`@suaveplan/payment-providers` bundle. Consumers that only need Stripe
|
||||
still install — and tree-shake around — PayPal's OAuth2 token cache,
|
||||
Adyen's merchant-context wiring, and three unused HTTP wrappers. The
|
||||
bundle also gates the next provider onboarding (Razorpay, Braintree,
|
||||
Klarna, etc.) behind a change to a single-publish unit that already has
|
||||
five tenants.
|
||||
|
||||
Genesis solves this elsewhere with a **foundation-with-sibling-plugins**
|
||||
shape:
|
||||
|
||||
- `@suaveplan/network` ships the `NetworkPlugin` interface, `PluginManager`,
|
||||
and shared bus/protocol primitives. Concrete transports live in sibling
|
||||
`@suaveplan/network-plugin-*` packages.
|
||||
- `@suaveplan/drive` ships the VFS core + `DrivePlugin` slot. Concrete
|
||||
storage transports / chunk schemes / sharing logic live in sibling
|
||||
`@suaveplan/drive-plugin-*`, `drive-transport-*`, `drive-sharing` packages.
|
||||
|
||||
This change retrofits the same shape onto `@suaveplan/payment-providers`:
|
||||
|
||||
```text
|
||||
@suaveplan/payment # interface owner (PaymentProvider)
|
||||
└── @suaveplan/payment-providers # FOUNDATION: shared types + http + registry + errors
|
||||
├── @suaveplan/payment-provider-stripe # sibling: StripeProvider
|
||||
├── @suaveplan/payment-provider-paypal # sibling: PayPalProvider
|
||||
├── @suaveplan/payment-provider-square # sibling: SquareProvider
|
||||
├── @suaveplan/payment-provider-mollie # sibling: MollieProvider
|
||||
└── @suaveplan/payment-provider-adyen # sibling: AdyenProvider
|
||||
```
|
||||
|
||||
The dep graph is **acyclic** by construction: every sibling depends on
|
||||
the foundation; nothing depends on a sibling except the consumer that
|
||||
chose to install it.
|
||||
|
||||
## What lives in the foundation
|
||||
|
||||
After this slim-down, `@suaveplan/payment-providers` contains exactly:
|
||||
|
||||
```text
|
||||
src/
|
||||
types/
|
||||
provider.ts # ApiKeyConfig, OAuthConfig, MerchantKeyConfig,
|
||||
# HttpOptions, ProviderResponse,
|
||||
# ProviderCommonOptions, NowFn, RandomIdFn,
|
||||
# Base64EncodeFn
|
||||
provider.md
|
||||
index.ts
|
||||
http/
|
||||
http.ts # httpRequest, FetchLike, composeAbortSignal
|
||||
http.md
|
||||
http.test.ts
|
||||
index.ts
|
||||
error-codes/
|
||||
error-codes.ts # PAYMENT_PROVIDER_CODES (6 codes),
|
||||
# PaymentProviderError, factories
|
||||
# (httpError, timeoutError, parseError,
|
||||
# oauthTokenFailure, providerNotRegistered,
|
||||
# providerAlreadyRegistered)
|
||||
error-codes.md
|
||||
error-codes.test.ts
|
||||
index.ts
|
||||
registry/
|
||||
registry.ts # ProviderRegistry class
|
||||
registry.md
|
||||
registry.test.ts
|
||||
define.ts # definePaymentProvider factory
|
||||
define.md
|
||||
define.test.ts
|
||||
index.ts
|
||||
index.ts # explicit named barrel (rule 6a)
|
||||
|
||||
tests/
|
||||
dep-graph.test.ts # universal-tier purity grep
|
||||
setup/polyfills.ts
|
||||
```
|
||||
|
||||
What is **gone**: the entire `src/providers/{stripe,paypal,square,mollie,adyen}/`
|
||||
tree (5 dirs × ~5 files each), and the matching 5 subpath exports in
|
||||
`package.json`.
|
||||
|
||||
## ProviderRegistry shape
|
||||
|
||||
```ts
|
||||
export class ProviderRegistry {
|
||||
public register(provider: PaymentProvider): void;
|
||||
public unregister(name: string): boolean;
|
||||
public get(name: string): PaymentProvider; // throws on miss
|
||||
public has(name: string): boolean;
|
||||
public list(): readonly PaymentProvider[];
|
||||
public readonly size: number;
|
||||
}
|
||||
```
|
||||
|
||||
Semantics:
|
||||
|
||||
- `register(provider)` indexes the instance by `provider.name`. Re-registering
|
||||
the same name throws `PaymentProviderError(PROVIDER_ALREADY_REGISTERED)`.
|
||||
- `unregister(name)` returns `true` if a provider was removed, `false`
|
||||
otherwise. Idempotent for the absent case (no throw).
|
||||
- `get(name)` throws `PaymentProviderError(PROVIDER_NOT_REGISTERED)` when
|
||||
the name is absent — strict mode keeps consumer code from silently
|
||||
routing to `undefined`.
|
||||
- `has(name)` is the soft-lookup counterpart for callers that prefer
|
||||
flow control over exception handling.
|
||||
- `list()` returns a frozen snapshot in insertion order so callers can
|
||||
enumerate without mutating the registry.
|
||||
- `size` is a numeric getter, not a method, mirroring `Map`/`Set`.
|
||||
|
||||
The registry is **process-local** and **synchronous**. There is no
|
||||
persistence, no auto-discovery, no plugin-loader scan. Consumers
|
||||
explicitly `import` + `register` the providers they intend to route to.
|
||||
|
||||
## definePaymentProvider factory
|
||||
|
||||
```ts
|
||||
export function definePaymentProvider<TName extends string>(
|
||||
spec: PaymentProviderSpec<TName>
|
||||
): PaymentProvider & { readonly name: TName };
|
||||
```
|
||||
|
||||
The spec captures the same six methods as `PaymentProvider`
|
||||
(`createIntent`, `confirmIntent`, `captureIntent`, `cancelIntent`,
|
||||
`refund`, `getIntent`) plus a literal-typed `name`. The factory:
|
||||
|
||||
1. Builds a plain object with the supplied methods bound to the spec
|
||||
(no `this` rebinding — sibling packages can keep methods as arrow
|
||||
functions referencing closed-over config).
|
||||
2. `Object.freeze`s the result so accidental mutation downstream throws
|
||||
in strict mode.
|
||||
3. Returns the result narrowed by the literal `TName` so consumers can
|
||||
discriminate at the type level (`provider.name === "stripe"` narrows
|
||||
`provider` to the Stripe shape).
|
||||
|
||||
The factory is **opt-in**: sibling packages MAY return a plain class
|
||||
instance from their `new XProvider(...)` constructor instead — both
|
||||
satisfy the structural `PaymentProvider` contract and both register
|
||||
identically into `ProviderRegistry`. The factory exists so providers
|
||||
without per-instance state (rare but possible — e.g. a deterministic
|
||||
test provider) can avoid the class boilerplate.
|
||||
|
||||
## composeAbortSignal — extracted public helper
|
||||
|
||||
`httpRequest` already composed a caller-supplied `signal` with its
|
||||
internal timeout signal. The slim-down extracts that composition into a
|
||||
named exported helper so sibling provider packages can reuse it when they
|
||||
build their own HTTP wrappers (e.g. PayPal's OAuth token fetch sits
|
||||
outside the per-call `httpRequest` and needs the same composition).
|
||||
|
||||
```ts
|
||||
export function composeAbortSignal(
|
||||
internal: AbortSignal,
|
||||
external: AbortSignal | undefined
|
||||
): AbortSignal;
|
||||
```
|
||||
|
||||
Behaviour is byte-identical to the previously-private helper — extraction
|
||||
is purely a public-surface change.
|
||||
|
||||
## Sibling-package contract
|
||||
|
||||
Every `@suaveplan/payment-provider-<name>` sibling MUST:
|
||||
|
||||
1. Declare `@suaveplan/payment-providers` and `@suaveplan/payment` as
|
||||
`workspace:^` deps (catalog externals for `@suaveplan/error`, `tslib`).
|
||||
2. Export exactly one class (`<Name>Provider`) implementing the
|
||||
`PaymentProvider` interface from `@suaveplan/payment`.
|
||||
3. Construct via an `<Name>Config` extending one of `ApiKeyConfig`,
|
||||
`OAuthConfig`, or `MerchantKeyConfig` from this foundation. Provider-
|
||||
specific config fields (`webhookSecret`, `merchantAccount`, `randomId`,
|
||||
etc.) extend the base.
|
||||
4. Use `httpRequest` from this foundation for every HTTP call.
|
||||
5. Raise only `PaymentProviderError` (no `throw new Error(...)`).
|
||||
6. Set `provider.name` to a literal string matching the package's short
|
||||
name (`"stripe"`, `"paypal"`, etc.).
|
||||
7. Ship at the universal tier (`"sideEffects": false`, no Node/browser/
|
||||
React imports).
|
||||
|
||||
A sibling package MAY:
|
||||
|
||||
- Extend `PAYMENT_PROVIDER_CODES` with provider-specific codes (e.g.
|
||||
`STRIPE_API_VERSION_MISMATCH`) via its own `defineCodes("STRIPE", ...)`
|
||||
registry.
|
||||
- Re-export the foundation's `ProviderRegistry` for downstream
|
||||
convenience, but MUST NOT instantiate a singleton registry at module
|
||||
load.
|
||||
- Declare additional injectable seams in its config beyond `fetch`,
|
||||
`now`, `randomId`, `base64Encode`, `signal`, `timeoutMs`.
|
||||
|
||||
A sibling package MUST NOT:
|
||||
|
||||
- Re-export concrete adapter classes from sibling packages (rule 4 — no
|
||||
re-exports of external deps for convenience).
|
||||
- Mutate the `PAYMENT_PROVIDER_CODES` registry from this foundation.
|
||||
- Depend on another `@suaveplan/payment-provider-*` sibling.
|
||||
|
||||
## Public-surface contract that sibling packages consume
|
||||
|
||||
Every sibling package will import the following from
|
||||
`@suaveplan/payment-providers`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
// registry
|
||||
ProviderRegistry,
|
||||
definePaymentProvider,
|
||||
// http
|
||||
httpRequest,
|
||||
composeAbortSignal,
|
||||
type FetchLike,
|
||||
// types
|
||||
type ApiKeyConfig,
|
||||
type OAuthConfig,
|
||||
type MerchantKeyConfig,
|
||||
type ProviderCommonOptions,
|
||||
type HttpOptions,
|
||||
type ProviderResponse,
|
||||
type NowFn,
|
||||
type RandomIdFn,
|
||||
type Base64EncodeFn,
|
||||
// errors
|
||||
PaymentProviderError,
|
||||
PAYMENT_PROVIDER_CODES,
|
||||
type PaymentProviderErrorCode,
|
||||
type PaymentProviderErrorOptions,
|
||||
httpError,
|
||||
timeoutError,
|
||||
parseError,
|
||||
oauthTokenFailure,
|
||||
providerNotRegistered,
|
||||
providerAlreadyRegistered,
|
||||
} from "@suaveplan/payment-providers";
|
||||
import type { PaymentProvider } from "@suaveplan/payment";
|
||||
```
|
||||
|
||||
This is the closed contract — the foundation will not grow new symbols
|
||||
without a follow-up OpenSpec change naming the sibling-package impact.
|
||||
|
||||
## File-layout decisions
|
||||
|
||||
- `registry/` is a single feature dir with two files (`registry.ts` and
|
||||
`define.ts`). Both belong to the same conceptual surface (constructing
|
||||
- indexing providers) and both are small enough that a separate dir
|
||||
per file would inflate the manifest. The dir has one `index.ts`
|
||||
barrel that explicitly re-exports both surfaces.
|
||||
- The Square-specific `SquareConfig` type is moved into the
|
||||
`@suaveplan/payment-provider-square` package — the foundation must not
|
||||
carry provider-specific shapes (every config field in the foundation
|
||||
applies to every provider, otherwise it doesn't belong here).
|
||||
- Removed providers are deleted in full (not stub-replaced) so the gate
|
||||
scans (§13 step 4, 4a, 5) stay clean.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Auto-discovery via the `package.json` `peerDependencies` graph
|
||||
(deferred — explicit registration is clearer for now).
|
||||
- A `loadProvider(name: string): Promise<PaymentProvider>` dynamic-
|
||||
import helper (deferred until consumers ask for it).
|
||||
- OTel spans / counters on `ProviderRegistry.{register,get}`
|
||||
(deferred — aligns with the parent migration's deferral).
|
||||
- A separate `payment-providers-react` browser-tier package for hook-
|
||||
based consumption (not required — registry is universal-tier; React
|
||||
consumers wrap it in a `useMemo`).
|
||||
231
openspec/archive/2026-05-17-slim-payment-providers/proposal.md
Normal file
231
openspec/archive/2026-05-17-slim-payment-providers/proposal.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Slim @suaveplan/payment-providers — extract concrete provider modules into sibling packages
|
||||
|
||||
**Package**: `@suaveplan/payment-providers`
|
||||
**Status**: PROPOSED
|
||||
**Type**: refactor (modification of the just-shipped migration `2026-05-17-add-payment-providers`)
|
||||
|
||||
- **Design**: ./design.md
|
||||
- **Spec delta**: ./specs/payment-providers/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
The just-shipped migration (`openspec/archive/2026-05-17-add-payment-providers/`)
|
||||
folded five concrete provider adapters (Stripe, PayPal, Square, Mollie, Adyen)
|
||||
into a single `@suaveplan/payment-providers` package. That bundle wired the
|
||||
shared shape (`PaymentProviderError`, `httpRequest`, `FetchLike`,
|
||||
`ApiKeyConfig`, `OAuthConfig`, `MerchantKeyConfig`) and the five adapter
|
||||
classes into one publish unit.
|
||||
|
||||
Consumers that only need Stripe pay for PayPal's OAuth machinery, Adyen's
|
||||
merchant-context wiring, and three more provider trees they never call.
|
||||
`@suaveplan/payment` already depends on this package transitively for its
|
||||
`PaymentProvider` interface; growing it with every new provider compounds the
|
||||
problem.
|
||||
|
||||
The genesis pattern for foundation-with-sibling-plugins (see
|
||||
`@suaveplan/network` + `network-plugin-*`, `@suaveplan/drive` +
|
||||
`drive-plugin-*`) is to ship the core abstraction + registry in the
|
||||
foundation package and the concrete plugins in sibling packages. This
|
||||
change retrofits `@suaveplan/payment-providers` to match that pattern:
|
||||
|
||||
1. **Slim the core** — `@suaveplan/payment-providers` keeps only the shared
|
||||
types (`ApiKeyConfig`, `OAuthConfig`, `MerchantKeyConfig`, `HttpOptions`,
|
||||
`ProviderResponse`, `ProviderCommonOptions`, `NowFn`, `RandomIdFn`,
|
||||
`Base64EncodeFn`, `FetchLike`), the `httpRequest` helper, the
|
||||
`PaymentProviderError` + `PAYMENT_PROVIDER_CODES` registry, plus the
|
||||
new `ProviderRegistry` + `definePaymentProvider` factory.
|
||||
2. **Add `ProviderRegistry`** — a thin in-memory registry that stores
|
||||
`PaymentProvider` instances by name with `register`/`get`/`has`/`list`/
|
||||
`unregister`, raising `PaymentProviderError(PROVIDER_NOT_REGISTERED)`
|
||||
on unknown lookups.
|
||||
3. **Add `definePaymentProvider`** — a factory helper that returns a
|
||||
`PaymentProvider`-shaped object with a stable identity so sibling
|
||||
provider packages can build adapters without duplicating boilerplate.
|
||||
4. **Remove the five concrete provider trees** (Stripe / PayPal / Square /
|
||||
Mollie / Adyen). They re-land as 5 sibling packages
|
||||
(`@suaveplan/payment-provider-stripe`, `-paypal`, `-square`, `-mollie`,
|
||||
`-adyen`) in a follow-up wave, each depending on this slimmed core.
|
||||
|
||||
## What changes
|
||||
|
||||
### REMOVED
|
||||
|
||||
- `StripeProvider` (entire `src/providers/stripe/` tree, including tests +
|
||||
`.md`). Re-lands as `@suaveplan/payment-provider-stripe.StripeProvider`.
|
||||
- `PayPalProvider` (entire `src/providers/paypal/` tree). Re-lands as
|
||||
`@suaveplan/payment-provider-paypal.PayPalProvider`.
|
||||
- `SquareProvider` (entire `src/providers/square/` tree). Re-lands as
|
||||
`@suaveplan/payment-provider-square.SquareProvider`.
|
||||
- `MollieProvider` (entire `src/providers/mollie/` tree). Re-lands as
|
||||
`@suaveplan/payment-provider-mollie.MollieProvider`.
|
||||
- `AdyenProvider` (entire `src/providers/adyen/` tree). Re-lands as
|
||||
`@suaveplan/payment-provider-adyen.AdyenProvider`.
|
||||
- Subpath exports `./providers/{stripe,paypal,square,mollie,adyen}/*`
|
||||
from `package.json#exports`.
|
||||
- `SquareConfig` type alias (Square-specific extension of `ApiKeyConfig`
|
||||
with `randomId`). The sibling package re-declares it locally — the core
|
||||
ships only provider-agnostic config shapes.
|
||||
|
||||
### ADDED
|
||||
|
||||
- `ProviderRegistry` class with `register(provider)`, `get(name)`,
|
||||
`has(name)`, `list()`, `unregister(name)`, and `size`. Lookup miss
|
||||
throws `PaymentProviderError(PROVIDER_NOT_REGISTERED)` from `get`;
|
||||
duplicate registration throws `PROVIDER_ALREADY_REGISTERED`.
|
||||
- `definePaymentProvider<TName extends string>(spec)` factory returning a
|
||||
`PaymentProvider & { readonly name: TName }`. Preserves caller-supplied
|
||||
method identities; freezes the resulting object so sibling packages can
|
||||
hand the value straight to `registry.register`.
|
||||
- `PROVIDER_NOT_REGISTERED` and `PROVIDER_ALREADY_REGISTERED` codes added
|
||||
to `PAYMENT_PROVIDER_CODES`.
|
||||
- `composeAbortSignal(internal, external)` extracted from `httpRequest`
|
||||
and re-exported as a public surface so sibling provider packages can
|
||||
compose their own signals without re-implementing the helper.
|
||||
|
||||
### MODIFIED
|
||||
|
||||
- `package.json#exports` shrinks from 9 entries (5 providers + 4 core) to
|
||||
5 entries (`.`, `./types/provider`, `./error-codes/error-codes`,
|
||||
`./http/http`, `./registry/registry`).
|
||||
- `README.md` rewritten to describe the slim core + sibling package
|
||||
ecosystem instead of the 5-adapter bundle.
|
||||
- `package.json#description` rewritten to reflect the slim shape.
|
||||
- `vite.config.ts#rollupOptions.external` stays unchanged
|
||||
(`@suaveplan/error`, `@suaveplan/payment`, `tslib`); no new externals
|
||||
are introduced.
|
||||
|
||||
## Migration
|
||||
|
||||
Consumers that imported a concrete provider from
|
||||
`@suaveplan/payment-providers` MUST switch to the matching sibling package.
|
||||
The provider class names, constructors, and `PaymentProvider` interface
|
||||
contract are preserved verbatim — only the package name changes.
|
||||
|
||||
Search command:
|
||||
|
||||
```bash
|
||||
grep -rln "from ['\"]@suaveplan/payment-providers['\"]" packages/ \
|
||||
| xargs grep -l "StripeProvider\|PayPalProvider\|SquareProvider\|MollieProvider\|AdyenProvider"
|
||||
```
|
||||
|
||||
Replacement table:
|
||||
|
||||
| Old (pre-slim) | New (post-slim) |
|
||||
|---|---|
|
||||
| `import { StripeProvider } from "@suaveplan/payment-providers"` | `import { StripeProvider } from "@suaveplan/payment-provider-stripe"` |
|
||||
| `import { PayPalProvider } from "@suaveplan/payment-providers"` | `import { PayPalProvider } from "@suaveplan/payment-provider-paypal"` |
|
||||
| `import { SquareProvider } from "@suaveplan/payment-providers"` | `import { SquareProvider } from "@suaveplan/payment-provider-square"` |
|
||||
| `import { MollieProvider } from "@suaveplan/payment-providers"` | `import { MollieProvider } from "@suaveplan/payment-provider-mollie"` |
|
||||
| `import { AdyenProvider } from "@suaveplan/payment-providers"` | `import { AdyenProvider } from "@suaveplan/payment-provider-adyen"` |
|
||||
| `import type { SquareConfig } from "@suaveplan/payment-providers"` | `import type { SquareConfig } from "@suaveplan/payment-provider-square"` |
|
||||
|
||||
`ApiKeyConfig`, `OAuthConfig`, `MerchantKeyConfig`, `HttpOptions`,
|
||||
`ProviderResponse`, `ProviderCommonOptions`, `NowFn`, `RandomIdFn`,
|
||||
`Base64EncodeFn`, `FetchLike`, `httpRequest`, `composeAbortSignal`,
|
||||
`PaymentProviderError`, `PAYMENT_PROVIDER_CODES`, `httpError`,
|
||||
`timeoutError`, `parseError`, `oauthTokenFailure` continue to be exported
|
||||
from `@suaveplan/payment-providers` unchanged.
|
||||
|
||||
Consumers that want a name-keyed lookup of multiple providers SHOULD now
|
||||
build a `ProviderRegistry`:
|
||||
|
||||
```ts
|
||||
import { ProviderRegistry } from "@suaveplan/payment-providers";
|
||||
import { StripeProvider } from "@suaveplan/payment-provider-stripe";
|
||||
import { PayPalProvider } from "@suaveplan/payment-provider-paypal";
|
||||
|
||||
const registry = new ProviderRegistry();
|
||||
registry.register(new StripeProvider({ apiKey: STRIPE_KEY }));
|
||||
registry.register(new PayPalProvider({ clientId, clientSecret }));
|
||||
|
||||
const stripe = registry.get("stripe"); // PaymentProvider
|
||||
```
|
||||
|
||||
## Source mapping
|
||||
|
||||
Kept-core requirements derive from `../suaveplan/enterprise/payment-providers.md`
|
||||
and the just-shipped `openspec/archive/2026-05-17-add-payment-providers/`
|
||||
delta. Removed-provider requirements derive from the same archived delta:
|
||||
|
||||
| Surface | Source | Status |
|
||||
|---|---|---|
|
||||
| Universal-tier purity | `enterprise/payment-providers.md` L37 | kept |
|
||||
| `PAYMENT_PROVIDER_CODES` registry + `PaymentProviderError` | `enterprise/payment-providers.md` L42, L61 | kept; codes extended with `PROVIDER_NOT_REGISTERED`, `PROVIDER_ALREADY_REGISTERED` |
|
||||
| `httpRequest` + `FetchLike` + timeout/abort | `archive/2026-05-17-add-payment-providers/specs/payment-providers/spec.md` Req "httpRequest helper..." | kept; `composeAbortSignal` now publicly exported |
|
||||
| `ApiKeyConfig` / `OAuthConfig` / `MerchantKeyConfig` / `HttpOptions` / `ProviderResponse` / `NowFn` / `RandomIdFn` / `Base64EncodeFn` | `archive/...` spec L62-L96 | kept |
|
||||
| `StripeProvider` full lifecycle | `archive/...` spec Req L100-L138 | REMOVED — re-lands in `@suaveplan/payment-provider-stripe` |
|
||||
| `PayPalProvider` OAuth + lifecycle | `archive/...` spec Req L142-L187 | REMOVED — re-lands in `@suaveplan/payment-provider-paypal` |
|
||||
| `SquareProvider` + injectable `randomId` | `archive/...` spec Req L191-L224 | REMOVED — re-lands in `@suaveplan/payment-provider-square` |
|
||||
| `MollieProvider` + string-amount marshalling | `archive/...` spec Req L228-L258 | REMOVED — re-lands in `@suaveplan/payment-provider-mollie` |
|
||||
| `AdyenProvider` + merchant context | `archive/...` spec Req L262-L293 | REMOVED — re-lands in `@suaveplan/payment-provider-adyen` |
|
||||
| `ProviderRegistry` | new genesis-originated | ADDED |
|
||||
| `definePaymentProvider` factory | new genesis-originated | ADDED |
|
||||
| `composeAbortSignal` public export | extracted from internal `http.ts` | ADDED |
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The 5 sibling provider packages themselves (`payment-provider-stripe`,
|
||||
`-paypal`, `-square`, `-mollie`, `-adyen`) — those are a follow-up wave,
|
||||
one OpenSpec change per package, each depending on this slimmed core.
|
||||
- Auto-discovery / dynamic loading of provider packages. Consumers
|
||||
explicitly install + register the providers they need.
|
||||
- `OpenTelemetry` spans / counters on the registry — same deferral as the
|
||||
parent migration (align with `@suaveplan/payment`).
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/payment-providers` to enumerate.
|
||||
|
||||
- `@suaveplan/payment` — [ ] tests run / [ ] docs reviewed / [x] no
|
||||
action needed (reason: imports only the `PaymentProvider` interface from
|
||||
itself; does NOT import any concrete adapter or shared type from
|
||||
`@suaveplan/payment-providers`. Verified by
|
||||
`grep -rn "@suaveplan/payment-providers" packages/universal/payment/`
|
||||
→ empty result).
|
||||
|
||||
Future dependents (the 5 sibling provider packages) will each carry their
|
||||
own OpenSpec change naming this slimmed core as a peer/runtime dep.
|
||||
|
||||
| Future sibling package | Will depend on this core for |
|
||||
|---|---|
|
||||
| `@suaveplan/payment-provider-stripe` | `httpRequest`, `composeAbortSignal`, `ApiKeyConfig`, `PaymentProviderError`, `PAYMENT_PROVIDER_CODES`, `definePaymentProvider`, optionally `ProviderRegistry` |
|
||||
| `@suaveplan/payment-provider-paypal` | `httpRequest`, `composeAbortSignal`, `OAuthConfig`, `NowFn`, `Base64EncodeFn`, `PaymentProviderError`, `oauthTokenFailure`, `definePaymentProvider` |
|
||||
| `@suaveplan/payment-provider-square` | `httpRequest`, `ApiKeyConfig`, `RandomIdFn`, `PaymentProviderError`, `definePaymentProvider` |
|
||||
| `@suaveplan/payment-provider-mollie` | `httpRequest`, `ApiKeyConfig`, `PaymentProviderError`, `definePaymentProvider` |
|
||||
| `@suaveplan/payment-provider-adyen` | `httpRequest`, `MerchantKeyConfig`, `RandomIdFn`, `PaymentProviderError`, `definePaymentProvider` |
|
||||
|
||||
## Parity Checklist
|
||||
|
||||
This is a slim-down refactor, not a fresh migration, so the parity bar is
|
||||
against the just-shipped `2026-05-17-add-payment-providers` delta — not an
|
||||
upstream library:
|
||||
|
||||
| Capability | Prior shape | Slimmed shape | Notes |
|
||||
|---|---|---|---|
|
||||
| `httpRequest` + `FetchLike` + timeout/abort | yes | yes | unchanged behaviour; `composeAbortSignal` extracted as public export |
|
||||
| `PaymentProviderError` + 4-code registry | yes | yes (+ 2 codes) | adds `PROVIDER_NOT_REGISTERED`, `PROVIDER_ALREADY_REGISTERED` |
|
||||
| `ApiKeyConfig` / `OAuthConfig` / `MerchantKeyConfig` / `HttpOptions` / `ProviderResponse` / `ProviderCommonOptions` / `NowFn` / `RandomIdFn` / `Base64EncodeFn` | yes | yes | verbatim |
|
||||
| `SquareConfig` | yes | 🚫 removed | Square-specific; sibling package re-declares locally so the core has no provider-specific shapes |
|
||||
| 5 concrete provider classes | yes | 🚫 removed | re-land in 5 sibling packages (`payment-provider-{stripe,paypal,square,mollie,adyen}`) per the follow-up wave |
|
||||
| `ProviderRegistry` + `definePaymentProvider` | 🚫 absent | ✅ added | new foundation API for the sibling-plugin pattern |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/registry/registry.test.ts` — exercises every `ProviderRegistry`
|
||||
method (`register`, `get`, `has`, `list`, `unregister`, `size`),
|
||||
duplicate-registration error, unknown-name error, both new error codes
|
||||
reachable from real call sites.
|
||||
- `src/registry/define.test.ts` — exercises `definePaymentProvider`,
|
||||
confirms it returns a frozen `PaymentProvider`, preserves method
|
||||
identities, and the result registers cleanly into `ProviderRegistry`.
|
||||
- `src/error-codes/error-codes.test.ts` — extended to cover both new
|
||||
codes and the two new factories (`providerNotRegistered`,
|
||||
`providerAlreadyRegistered`).
|
||||
- `src/http/http.test.ts` — extended to cover the now-public
|
||||
`composeAbortSignal` helper end-to-end.
|
||||
- `tests/dep-graph.test.ts` — unchanged; still asserts the package has
|
||||
no Node / browser / runtime-specific imports.
|
||||
- `src/index.ts` barrel + `package.json#exports` — manually verified to
|
||||
contain only the slim core symbols / paths.
|
||||
@@ -0,0 +1,195 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: payment-providers (slim-payment-providers)
|
||||
|
||||
**Package**: `@suaveplan/payment-providers`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at draft time.
|
||||
> This delta MODIFIES the canonical spec produced by
|
||||
> `2026-05-17-add-payment-providers`. The `## REMOVED Requirements`
|
||||
> block strips the five concrete-provider requirements that will re-land
|
||||
> in sibling packages; the `## ADDED Requirements` block introduces the
|
||||
> registry surface that grows in their place.
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: StripeProvider full lifecycle
|
||||
|
||||
The `StripeProvider` class and its entire requirement block (status
|
||||
mapping, refund-reason mapping, idempotency-key forwarding,
|
||||
`StripeProvider.name === "stripe"`) is removed from this package. The
|
||||
same surface re-lands in `@suaveplan/payment-provider-stripe` with the
|
||||
class and `**Verified by:**` test names preserved verbatim.
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PayPalProvider OAuth token cache and order lifecycle
|
||||
|
||||
The `PayPalProvider` class and its entire requirement block (OAuth2
|
||||
client-credentials flow, injectable `now()` cache TTL, injectable
|
||||
`base64Encode`, idempotency-key forwarding, status mapping,
|
||||
`PayPalProvider.name === "paypal"`, `OAUTH_TOKEN_FAILURE` wrapping) is
|
||||
removed from this package. The same surface re-lands in
|
||||
`@suaveplan/payment-provider-paypal` with the class and `**Verified by:**`
|
||||
test names preserved verbatim. The `OAUTH_TOKEN_FAILURE` code and the
|
||||
`oauthTokenFailure` factory remain in this foundation so the sibling
|
||||
package can raise them.
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SquareProvider lifecycle with injectable randomId
|
||||
|
||||
The `SquareProvider` class, its entire requirement block, and the
|
||||
Square-specific `SquareConfig` type are removed from this package. The
|
||||
class and config re-land in `@suaveplan/payment-provider-square` with
|
||||
the `**Verified by:**` test names preserved verbatim.
|
||||
|
||||
---
|
||||
|
||||
### Requirement: MollieProvider lifecycle with string-amount marshalling
|
||||
|
||||
The `MollieProvider` class and its entire requirement block are removed
|
||||
from this package. The class re-lands in
|
||||
`@suaveplan/payment-provider-mollie` with the `**Verified by:**` test
|
||||
names preserved verbatim.
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AdyenProvider lifecycle with merchant-account context
|
||||
|
||||
The `AdyenProvider` class and its entire requirement block are removed
|
||||
from this package. The class re-lands in
|
||||
`@suaveplan/payment-provider-adyen` with the `**Verified by:**` test
|
||||
names preserved verbatim.
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: ProviderRegistry indexes PaymentProvider instances by name
|
||||
|
||||
The package SHALL export a `ProviderRegistry` class that indexes
|
||||
`PaymentProvider` instances (from `@suaveplan/payment`) by their `name`
|
||||
field. The class SHALL expose `register(provider)`,
|
||||
`unregister(name)`, `get(name)`, `has(name)`, `list()`, and a numeric
|
||||
`size` getter. `register` SHALL throw
|
||||
`PaymentProviderError(PAYMENT_PROVIDER.PROVIDER_ALREADY_REGISTERED)`
|
||||
when a provider with the same name is already present. `get` SHALL
|
||||
throw `PaymentProviderError(PAYMENT_PROVIDER.PROVIDER_NOT_REGISTERED)`
|
||||
when the requested name is absent. `list()` SHALL return a frozen
|
||||
snapshot in insertion order so callers cannot mutate the registry
|
||||
through the returned array.
|
||||
|
||||
#### Scenario: register + get round-trips a provider
|
||||
|
||||
- **WHEN** a caller invokes `registry.register(stripe)` followed by `registry.get("stripe")`
|
||||
- **THEN** the second call returns the same `PaymentProvider` reference
|
||||
|
||||
#### Scenario: get on an unknown name throws PROVIDER_NOT_REGISTERED
|
||||
|
||||
- **WHEN** `registry.get("nope")` is invoked on an empty registry
|
||||
- **THEN** the call throws a `PaymentProviderError` whose `code === "PAYMENT_PROVIDER.PROVIDER_NOT_REGISTERED"` and whose `provider === "nope"`
|
||||
|
||||
#### Scenario: duplicate register throws PROVIDER_ALREADY_REGISTERED
|
||||
|
||||
- **WHEN** a provider with `name: "stripe"` is registered twice
|
||||
- **THEN** the second call throws a `PaymentProviderError` whose `code === "PAYMENT_PROVIDER.PROVIDER_ALREADY_REGISTERED"` and whose `provider === "stripe"`
|
||||
|
||||
#### Scenario: unregister returns true on hit and false on miss
|
||||
|
||||
- **WHEN** `registry.unregister("stripe")` is invoked after a successful registration
|
||||
- **THEN** the call returns `true`, a subsequent `registry.has("stripe")` returns `false`, and a second `registry.unregister("stripe")` returns `false`
|
||||
|
||||
#### Scenario: list returns a frozen snapshot in insertion order
|
||||
|
||||
- **WHEN** three providers are registered in order A, B, C and `registry.list()` is invoked
|
||||
- **THEN** the returned array equals `[A, B, C]` and is frozen (`Object.isFrozen` is `true`)
|
||||
|
||||
**Verified by:** `src/registry/registry.test.ts::"register + get round-trips a provider"` AND `src/registry/registry.test.ts::"throws PROVIDER_NOT_REGISTERED on unknown name"` AND `src/registry/registry.test.ts::"throws PROVIDER_ALREADY_REGISTERED on duplicate name"` AND `src/registry/registry.test.ts::"unregister returns true on hit and false on miss"` AND `src/registry/registry.test.ts::"list returns a frozen snapshot in insertion order"` AND `src/registry/registry.test.ts::"size tracks register and unregister"` AND `src/registry/registry.test.ts::"has reports membership without throwing"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: definePaymentProvider factory returns a frozen literal-typed provider
|
||||
|
||||
The package SHALL export `definePaymentProvider<TName extends string>(spec)`
|
||||
that returns a `PaymentProvider & { readonly name: TName }`. The factory
|
||||
SHALL freeze the returned object (`Object.isFrozen(result) === true`)
|
||||
and SHALL preserve method identity (the function instance passed in
|
||||
`spec.createIntent` SHALL be the same reference exposed on
|
||||
`result.createIntent`). The returned value SHALL be assignable to
|
||||
`PaymentProvider` so it can be passed directly to
|
||||
`ProviderRegistry.register`.
|
||||
|
||||
#### Scenario: factory result registers cleanly into ProviderRegistry
|
||||
|
||||
- **WHEN** `definePaymentProvider({ name: "test", createIntent: …, … })` is registered into a `ProviderRegistry`
|
||||
- **THEN** `registry.get("test")` returns the same reference the factory produced
|
||||
|
||||
#### Scenario: returned object is frozen
|
||||
|
||||
- **WHEN** the caller attempts to assign to a method on the factory result (in strict mode)
|
||||
- **THEN** `Object.isFrozen` is `true` for the result and the assignment throws
|
||||
|
||||
#### Scenario: method identity is preserved
|
||||
|
||||
- **WHEN** the same function `f` is passed as both `spec.createIntent` and `spec.getIntent`
|
||||
- **THEN** `result.createIntent === f && result.getIntent === f`
|
||||
|
||||
**Verified by:** `src/registry/define.test.ts::"factory result is assignable to PaymentProvider"` AND `src/registry/define.test.ts::"factory result registers cleanly into ProviderRegistry"` AND `src/registry/define.test.ts::"returned object is frozen"` AND `src/registry/define.test.ts::"method identity is preserved"` AND `src/registry/define.test.ts::"literal name narrows the result type"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PAYMENT_PROVIDER_CODES adds registry codes
|
||||
|
||||
`PAYMENT_PROVIDER_CODES` SHALL be extended with
|
||||
`PROVIDER_NOT_REGISTERED` and `PROVIDER_ALREADY_REGISTERED`. The package
|
||||
SHALL export `providerNotRegistered(name)` and
|
||||
`providerAlreadyRegistered(name)` factories that return a
|
||||
`PaymentProviderError` carrying the offending name on the `provider`
|
||||
discriminant. Both codes SHALL be reachable from real call sites
|
||||
exercised by the test suite (i.e. raised through `ProviderRegistry`
|
||||
methods, not only from the factories in isolation).
|
||||
|
||||
#### Scenario: providerNotRegistered carries the name as provider discriminant
|
||||
|
||||
- **WHEN** `providerNotRegistered("stripe")` is invoked
|
||||
- **THEN** the resulting `PaymentProviderError` has `code === "PAYMENT_PROVIDER.PROVIDER_NOT_REGISTERED"` and `provider === "stripe"`
|
||||
|
||||
#### Scenario: providerAlreadyRegistered carries the name as provider discriminant
|
||||
|
||||
- **WHEN** `providerAlreadyRegistered("stripe")` is invoked
|
||||
- **THEN** the resulting `PaymentProviderError` has `code === "PAYMENT_PROVIDER.PROVIDER_ALREADY_REGISTERED"` and `provider === "stripe"`
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.test.ts::"providerNotRegistered carries the name as provider discriminant"` AND `src/error-codes/error-codes.test.ts::"providerAlreadyRegistered carries the name as provider discriminant"` AND `src/registry/registry.test.ts::"throws PROVIDER_NOT_REGISTERED on unknown name"` AND `src/registry/registry.test.ts::"throws PROVIDER_ALREADY_REGISTERED on duplicate name"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: composeAbortSignal is publicly exported
|
||||
|
||||
The package SHALL export `composeAbortSignal(internal, external)` from
|
||||
`src/http/http.ts` and re-export it from `src/index.ts`. The function
|
||||
SHALL return the internal signal when `external` is `undefined`, SHALL
|
||||
return a fresh `AbortSignal` that aborts when either input aborts
|
||||
otherwise, and SHALL abort immediately when `external` is already
|
||||
aborted at call time. The reason of the aborting input SHALL propagate
|
||||
to the composed signal's `reason`.
|
||||
|
||||
#### Scenario: external undefined returns internal signal directly
|
||||
|
||||
- **WHEN** `composeAbortSignal(internal, undefined)` is invoked
|
||||
- **THEN** the returned signal is reference-equal to `internal`
|
||||
|
||||
#### Scenario: internal abort propagates to composed signal
|
||||
|
||||
- **WHEN** the internal `AbortController.abort("boom")` is invoked after composition
|
||||
- **THEN** the composed signal is aborted and its `reason` is the propagated `AbortSignal`
|
||||
|
||||
#### Scenario: already-aborted external aborts the composed signal immediately
|
||||
|
||||
- **WHEN** the external signal is already aborted at the time of `composeAbortSignal` call
|
||||
- **THEN** the composed signal is aborted before the function returns
|
||||
|
||||
**Verified by:** `src/http/http.test.ts::"composeAbortSignal returns internal when external is undefined"` AND `src/http/http.test.ts::"composeAbortSignal propagates internal abort"` AND `src/http/http.test.ts::"composeAbortSignal propagates external abort"` AND `src/http/http.test.ts::"composeAbortSignal aborts immediately when external is pre-aborted"` AND `src/http/http.test.ts::"composes caller AbortSignal with internal timeout"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
139
openspec/archive/2026-05-17-slim-payment-providers/tasks.md
Normal file
139
openspec/archive/2026-05-17-slim-payment-providers/tasks.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Tasks: slim-payment-providers
|
||||
|
||||
## 1. Remove concrete provider modules
|
||||
|
||||
- [ ] 1.1 Delete `packages/universal/payment-providers/src/providers/stripe/`.
|
||||
- [ ] 1.2 Delete `packages/universal/payment-providers/src/providers/paypal/`.
|
||||
- [ ] 1.3 Delete `packages/universal/payment-providers/src/providers/square/`.
|
||||
- [ ] 1.4 Delete `packages/universal/payment-providers/src/providers/mollie/`.
|
||||
- [ ] 1.5 Delete `packages/universal/payment-providers/src/providers/adyen/`.
|
||||
- [ ] 1.6 Delete the now-empty `src/providers/` directory.
|
||||
- [ ] 1.7 Drop the `SquareConfig` type from `src/types/provider.ts`
|
||||
(it ships with the sibling Square package).
|
||||
- [ ] 1.8 Remove the 5 `./providers/<name>/<name>` entries from
|
||||
`package.json#exports`.
|
||||
|
||||
## 2. Extract composeAbortSignal as a public helper
|
||||
|
||||
- [ ] 2.1 Promote the internal `composeSignals` function in
|
||||
`src/http/http.ts` to an exported `composeAbortSignal(internal, external)`.
|
||||
- [ ] 2.2 Re-export `composeAbortSignal` from `src/http/index.ts` and
|
||||
`src/index.ts`.
|
||||
- [ ] 2.3 Extend `src/http/http.test.ts` to cover `composeAbortSignal`
|
||||
directly (internal abort propagates, external abort propagates,
|
||||
already-aborted external triggers immediately, undefined external
|
||||
returns internal).
|
||||
- [ ] 2.4 Update `src/http/http.md` to document `composeAbortSignal`.
|
||||
|
||||
## 3. Add PROVIDER_NOT_REGISTERED + PROVIDER_ALREADY_REGISTERED codes
|
||||
|
||||
- [ ] 3.1 Extend the `PAYMENT_PROVIDER_CODES` registry in
|
||||
`src/error-codes/error-codes.ts` with `PROVIDER_NOT_REGISTERED` and
|
||||
`PROVIDER_ALREADY_REGISTERED`.
|
||||
- [ ] 3.2 Add `providerNotRegistered(name)` and
|
||||
`providerAlreadyRegistered(name)` factories.
|
||||
- [ ] 3.3 Extend `src/error-codes/error-codes.test.ts` to confirm both
|
||||
new codes are reachable from their factories and carry the `provider`
|
||||
discriminant when the registry surfaces them.
|
||||
- [ ] 3.4 Update `src/error-codes/error-codes.md` to list all 6 codes.
|
||||
|
||||
## 4. Build ProviderRegistry
|
||||
|
||||
- [ ] 4.1 Create `src/registry/registry.ts` exporting a `ProviderRegistry`
|
||||
class with `register`, `unregister`, `get`, `has`, `list`, `size`.
|
||||
- [ ] 4.2 `get(name)` MUST throw `PaymentProviderError(PROVIDER_NOT_REGISTERED)`
|
||||
on miss.
|
||||
- [ ] 4.3 `register(provider)` MUST throw
|
||||
`PaymentProviderError(PROVIDER_ALREADY_REGISTERED)` when the name is
|
||||
already present.
|
||||
- [ ] 4.4 `list()` MUST return a frozen array in insertion order.
|
||||
- [ ] 4.5 Create `src/registry/registry.md` with the §4 mandatory
|
||||
sections (Purpose, Features, Basic + Advanced usage, API Reference,
|
||||
Implementation Notes).
|
||||
- [ ] 4.6 Create `src/registry/registry.test.ts` covering every public
|
||||
method, both error paths, and the insertion-order guarantee.
|
||||
|
||||
## 5. Add definePaymentProvider factory
|
||||
|
||||
- [ ] 5.1 Create `src/registry/define.ts` exporting
|
||||
`definePaymentProvider<TName extends string>(spec)`.
|
||||
- [ ] 5.2 Result MUST be frozen and MUST carry the literal `TName`.
|
||||
- [ ] 5.3 Method identities MUST be preserved (the closure passed in
|
||||
IS the closure dispatched at call time).
|
||||
- [ ] 5.4 Create `src/registry/define.md` with the §4 mandatory sections.
|
||||
- [ ] 5.5 Create `src/registry/define.test.ts` covering the
|
||||
literal-type narrowing, frozen-shape guarantee, method-identity
|
||||
preservation, and the round-trip through `ProviderRegistry.register`.
|
||||
|
||||
## 6. Update barrels + subpath exports
|
||||
|
||||
- [ ] 6.1 Create `src/registry/index.ts` re-exporting `ProviderRegistry`
|
||||
and `definePaymentProvider` via explicit named exports (rule 6a).
|
||||
- [ ] 6.2 Rewrite `src/index.ts` to drop the 5 provider class exports
|
||||
and the `SquareConfig` type export, and add `ProviderRegistry`,
|
||||
`definePaymentProvider`, `composeAbortSignal`, `providerNotRegistered`,
|
||||
`providerAlreadyRegistered`.
|
||||
- [ ] 6.3 Add `./registry/registry` to `package.json#exports`. Final
|
||||
count: 5 entries (`.`, `./types/provider`, `./error-codes/error-codes`,
|
||||
`./http/http`, `./registry/registry`).
|
||||
- [ ] 6.4 Update `package.json#description` to describe the slim core.
|
||||
|
||||
## 7. Update README.md
|
||||
|
||||
- [ ] 7.1 Rewrite the one-paragraph description to reflect the slim
|
||||
shape.
|
||||
- [ ] 7.2 Rewrite the Quick Start example to use `ProviderRegistry` +
|
||||
a sibling-imported `StripeProvider`.
|
||||
- [ ] 7.3 Rewrite the Features list to drop the 5-adapter language and
|
||||
describe the foundation surface.
|
||||
- [ ] 7.4 Rewrite the Modules list — only `types/provider`,
|
||||
`error-codes/error-codes`, `http/http`, `registry/registry`.
|
||||
- [ ] 7.5 Add a "Sibling provider packages" section linking the 5
|
||||
follow-up packages by name (note they are not yet published).
|
||||
|
||||
## 8. Verification gates (§13)
|
||||
|
||||
- [ ] 8.1 Step 0 — spec reconciliation: walk every `### Requirement`
|
||||
in the delta, grep `src/` for the named symbols, confirm every
|
||||
`**Verified by:**` test name appears in the matching test file.
|
||||
- [ ] 8.2 Step 1 — `bunx turbo lint typecheck test --filter=@suaveplan/payment-providers`.
|
||||
- [ ] 8.3 Step 2 — `bunx turbo test:coverage --filter=@suaveplan/payment-providers`
|
||||
reports 100/100/100/100.
|
||||
- [ ] 8.4 Step 3 — `bunx turbo build --filter=@suaveplan/payment-providers`.
|
||||
- [ ] 8.5 Step 4 — banned-pattern scan returns zero output.
|
||||
- [ ] 8.6 Step 4a — barrel-shape scan returns zero output (no
|
||||
`export *` in any `<feature>/index.ts`).
|
||||
- [ ] 8.7 Step 4b — test-runner import scan returns zero output (no
|
||||
direct `vitest` / `bun:test` imports).
|
||||
- [ ] 8.8 Step 5 — every `.ts` (excl. `.test.ts`, `.types.ts`,
|
||||
`index.ts`) has a sibling `.md`.
|
||||
- [ ] 8.9 Step 6 — `README.md` exists.
|
||||
- [ ] 8.10 Step 7 — no bare versions in `package.json`.
|
||||
- [ ] 8.11 Step 8 — subpath-export count matches the feature-module
|
||||
count (4 features + root = 5 entries).
|
||||
- [ ] 8.12 Step 9 — security scan returns zero output.
|
||||
- [ ] 8.13 Step 10 — dependent sweep:
|
||||
`bunx turbo test --filter=...@suaveplan/payment-providers` green
|
||||
(currently only the package itself).
|
||||
|
||||
## 9. Root-doc refresh (§13b)
|
||||
|
||||
- [ ] 9.1 Run `bun scripts/folder-reorg/generate-manifest.ts` to refresh
|
||||
`MANIFEST.md`.
|
||||
- [ ] 9.2 Run `bun scripts/generate-missing.ts` to refresh `MISSING.yml`.
|
||||
- [ ] 9.3 Hand-edit `STATUS.md` row for `@suaveplan/payment-providers`
|
||||
to describe the slim shape (no row flip — package stays ✅).
|
||||
- [ ] 9.4 Hand-edit `SUMMARY.md` paragraph for
|
||||
`@suaveplan/payment-providers` to describe the slim shape.
|
||||
- [ ] 9.5 `bunx markdownlint-cli2 --fix` across touched docs.
|
||||
|
||||
## 10. Fold + archive (§14)
|
||||
|
||||
- [ ] 10.1 Fold `specs/payment-providers/spec.md` into
|
||||
`openspec/specs/payment-providers/spec.md` — apply the `## REMOVED`
|
||||
block (delete the 5 provider requirements + drop `SquareConfig` from
|
||||
the http/types requirement) and apply the `## ADDED` block (add the
|
||||
`ProviderRegistry`, `definePaymentProvider`, `composeAbortSignal`, and
|
||||
registry-code requirements).
|
||||
- [ ] 10.2 Move `openspec/changes/slim-payment-providers/` →
|
||||
`openspec/archive/2026-05-17-slim-payment-providers/`.
|
||||
Reference in New Issue
Block a user