-
Notifications
You must be signed in to change notification settings - Fork 5
feat(core): gateway-agnostic PaymentProvider interface (Stripe adapter + mock) - #18
feat(core): gateway-agnostic PaymentProvider interface (Stripe adapter + mock) #18moset15 wants to merge 1 commit into
Conversation
...nd mock Adds a gateway-agnostic payment seam so additional processors (e.g. Paystack for the African market) can plug in without touching core checkout code: - payment-provider/types.ts: the PaymentProvider interface — init hosted checkout, verify webhook (constant-time, algorithm-agnostic), parse webhook event into a normalised shape, refund, format amount. - payment-provider/stripe-provider.ts: thin adapter implementing the interface over the existing stripe/* modules — no changes to any file under src/stripe/. - payment-provider/mock-provider.ts: in-memory implementation for tests and credential-free local checkout runs. - payment-provider/registry.ts: runtime provider selection via settings:paymentProvider (plugin KV), defaulting to "stripe" so existing installs behave identically. - New ./payment-provider subpath export + tsdown entry. - 19 unit tests (bun test) across the three new modules; tsc --noEmit clean; Biome formatted. Routes (checkout.ts / webhook.ts) still call stripe/* directly — wiring them onto resolveProvider() is staged as a follow-up commit so this diff stays reviewable and behaviour-neutral. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cavewebs
commented
Jul 25, 2026
Thanks for putting this together. The direction is good, and I like the intent of introducing a gateway-agnostic seam with a Stripe adapter. Kudos to the additional mock provider, registry, and separate package export.
I’m going to request changes before merging, because I don’t think the abstraction is safe enough yet to become the foundation for the follow-up checkout/webhook wiring.
Main blockers:
-
providerReferenceis currently ambiguous. The Stripe adapter returns a Checkout Session ID frominitCheckout, butrefund()later treatsproviderReferenceas a PaymentIntent ID. That will break Stripe refunds once this is wired into real flows. I think we need to split these concepts, e.g.checkoutReference/paymentReference/ provider transaction reference. -
The normalized webhook event loses the provider event ID. The current Stripe webhook route dedupes using
event.id, butNormalizedPaymentEventdoes not expose aproviderEventId. Without that, the generic webhook route cannot preserve current retry/idempotency behaviour. -
InitCheckoutInputis currently too narrow to replace the existing hosted checkout route. The current Stripe flow supports shipping options, billing address collection, Stripe Tax, subscription mode/trials, subscription metadata, Connect transfer data, and application fees. The adapter hard-codesmode: "payment"and only passes a minimal subset through, so wiring routes to this interface would regress existing Stripe behaviour. -
Billing address is not mapped in the Stripe adapter.
customer_details.addressshould becomebillingAddress, and we should preserve the same billing/shipping fallback behaviour the current webhook route has, especially for digital-only carts. -
Refund idempotency is too coarse.
refund:${providerReference}:${amount}means two legitimate partial refunds of the same amount against the same payment could collapse into one Stripe idempotency key. We need an explicit refund request ID or idempotency key inCreateRefundInput. -
registerPaymentProvider()silently overwrites existing providers with the same ID. That could accidentally replacestripeor another registered provider. I’d prefer duplicate IDs to throw unless an explicit override option is passed.
There are also a few smaller issues worth cleaning up:
supportedCurrencies()says it returns ISO currency codes, but Stripe returns["*"]. The type should reflect that or become asupportsCurrency(currency)check.- The mock provider appends
&mock=1even when the URL has no query string. mock.parseWebhookEvent()throws on invalid JSON, while the Stripe adapter returns anunhandledevent.- Some comments say checkout/webhook routes are already refactored to use
PaymentProvider, but this PR explicitly defers that wiring. - CI is green, but the test step is
continue-on-error, so the check does not prove tests passed.
Overall: I’m supportive of the approach, but I’d like the seam tightened before merge so the follow-up wiring does not introduce payment, refund, or webhook regressions.
What
Introduces a payment seam so processors beyond Stripe can plug into core without forking it:
initCheckout(hosted),verifyWebhook(constant-time, hash-algorithm-agnostic),parseWebhookEvent(normalised event shape),refund,formatAmount.Why
We are building a Paystack provider (cards + M-Pesa STK — the dominant payment rail in Kenya) for a production store on DashCommerce. Paystack's webhook signing is HMAC-SHA512 and its checkout is hosted-page-first, so the seam is deliberately shaped around what both gateways share rather than around Stripe's API surface.
What this PR does not do
src/stripe/*is untouched — the Stripe adapter (payment-provider/stripe-provider.ts) wraps the existing modules.routes/checkout.tsandroutes/webhook.tsstill callstripe/*directly; wiring them ontoregistry.resolveProvider()is staged as a follow-up commit so this diff stays reviewable and behaviour-neutral (the registry defaults to"stripe", so existing installs behave identically).payment-provider/types.ts).Testing
19 unit tests (
bun test) across types/adapter/mock/registry;tsc --noEmitclean; Biome formatted;tsdownbuild emits the new./payment-providerentry.Happy to reshape the interface if you'd prefer different seams — the Paystack implementation living at FIKANOVA/dashcommerce-paystack is the second consumer and keeps this honest.
🤖 Generated with Claude Code