Skip to content

feat(core): gateway-agnostic PaymentProvider interface (Stripe adapter + mock) - #18

Open
moset15 wants to merge 1 commit into
emdashCommerce:mainfrom
FIKANOVA:feat/payment-provider-interface
Open

feat(core): gateway-agnostic PaymentProvider interface (Stripe adapter + mock)#18
moset15 wants to merge 1 commit into
emdashCommerce:mainfrom
FIKANOVA:feat/payment-provider-interface

Conversation

@moset15

@moset15 moset15 commented Jul 19, 2026

Copy link
Copy Markdown

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.ts and routes/webhook.ts still call stripe/* directly; wiring them onto registry.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).
  • Stripe Connect and Subscriptions/Billing are intentionally out of scope — no structural Paystack equivalent to abstract over (see the module comment in payment-provider/types.ts).

Testing

19 unit tests (bun test) across types/adapter/mock/registry; tsc --noEmit clean; Biome formatted; tsdown build emits the new ./payment-provider entry.

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

…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

Copy link
Copy Markdown
Contributor

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:

  1. providerReference is currently ambiguous. The Stripe adapter returns a Checkout Session ID from initCheckout, but refund() later treats providerReference as 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.

  2. The normalized webhook event loses the provider event ID. The current Stripe webhook route dedupes using event.id, but NormalizedPaymentEvent does not expose a providerEventId. Without that, the generic webhook route cannot preserve current retry/idempotency behaviour.

  3. InitCheckoutInput is 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-codes mode: "payment" and only passes a minimal subset through, so wiring routes to this interface would regress existing Stripe behaviour.

  4. Billing address is not mapped in the Stripe adapter. customer_details.address should become billingAddress, and we should preserve the same billing/shipping fallback behaviour the current webhook route has, especially for digital-only carts.

  5. 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 in CreateRefundInput.

  6. registerPaymentProvider() silently overwrites existing providers with the same ID. That could accidentally replace stripe or 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 a supportsCurrency(currency) check.
  • The mock provider appends &mock=1 even when the URL has no query string.
  • mock.parseWebhookEvent() throws on invalid JSON, while the Stripe adapter returns an unhandled event.
  • 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants