Payment Provider Plugins
Payment adapters handle payment processing for orders. Unchained supports INVOICE and GENERIC providers; card gateways such as Stripe use the generic provider type.
For an overview of how payment fits into the order lifecycle, see Order Lifecycle.
Payment Types
| Type | Description | Use Cases |
|---|---|---|
INVOICE | Invoice-based payments | Pre-paid or post-paid invoices |
GENERIC | Gateway-backed and other payment methods | Stripe, Datatrans, crypto, bank transfer, cash |
Creating a payment provider
The recommended way is the registerPaymentProvider factory (or registerInvoicePayment for invoices). You supply only the behavior callbacks; the factory builds and registers the plugin.
Example: pre-paid invoice
A pre-paid invoice blocks order confirmation until payment is received — charge: false keeps the order PENDING, and payLaterAllowed: false requires payment before confirmation.
import { registerInvoicePayment } from '@unchainedshop/core';
registerInvoicePayment({
adapterId: 'prepaid-invoice',
payLaterAllowed: false,
charge: false, // payment is collected out of band; order stays PENDING
});
Example: card payment with Stripe
import Stripe from 'stripe';
import {
OrderPricingSheet,
PaymentError,
registerPaymentProvider,
} from '@unchainedshop/core';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
registerPaymentProvider({
adapterId: 'stripe-card',
type: 'GENERIC',
configurationError: process.env.STRIPE_SECRET_KEY
? null
: PaymentError.INCOMPLETE_CONFIGURATION,
// Create the intent and bind it to this order payment
sign: async (configuration, context) => {
if (!context.order) return null;
const pricing = OrderPricingSheet({
calculation: context.order.calculation,
currencyCode: context.order.currencyCode,
});
const intent = await stripe.paymentIntents.create({
amount: pricing.total().amount,
currency: context.order.currencyCode.toLowerCase(),
metadata: { orderPaymentId: context.orderPayment?._id ?? '' },
});
return intent.client_secret;
},
// Verify the completed intent against this order payment
charge: async (configuration, context) => {
const intentId = context.transactionContext?.paymentIntentId;
if (!intentId || !context.order || !context.orderPayment) return false;
const intent = await stripe.paymentIntents.retrieve(intentId);
const pricing = OrderPricingSheet({
calculation: context.order.calculation,
currencyCode: context.order.currencyCode,
});
const matchesOrder =
intent.metadata.orderPaymentId === context.orderPayment._id &&
intent.amount === pricing.total().amount &&
intent.currency === context.order.currencyCode.toLowerCase();
return intent.status === 'succeeded' && matchesOrder
? { transactionId: intent.id }
: false;
},
cancel: async (configuration, context) => {
if (context.orderPayment?.transactionId) {
await stripe.refunds.create({ payment_intent: context.orderPayment.transactionId });
}
return true;
},
});
Callback reference
These are the callbacks you pass to registerPaymentProvider (each receives (configuration, context)).
charge
Process the payment charge. Called during checkout.
| Return Value | Behavior |
|---|---|
{ transactionId } | Payment successful, proceed with checkout |
false | Payment not complete yet, order stays in PENDING |
| Throws error | Abort checkout, order stays in OPEN (cart) |
Pass false (not a function) for providers where payment is collected out of band.
isActive / isPayLaterAllowed
isActive (boolean, default true) toggles availability. isPayLaterAllowed (boolean, default false) controls whether order confirmation can proceed before payment completes — true = post-paid, false = pre-paid.
sign / validate
sign(configuration, context) returns a client token (e.g. a Stripe client_secret) for the front-end SDK. validate(configuration, context) validates a stored credential.
cancel / confirm
cancel refunds/voids a payment (called on order rejection); confirm captures a previously-authorized payment (called on CONFIRMED).
configurationError
A PaymentError | null surfaced when the provider is misconfigured (for example PaymentError.INCOMPLETE_CONFIGURATION) so it is marked invalid instead of crashing checkout.
Webhooks & low-level adapters
Most gateways confirm payments asynchronously via a webhook. To attach a webhook route (and for any behavior the factory doesn't expose), build a hand-written IPlugin with a routes entry and pluginRegistry.register() — see the shipped Stripe plugin and Plugin System.
Related
- Plugin Factories —
registerPaymentProvider/registerInvoicePayment - Plugin System — the plugin architecture
- Order Lifecycle — how payment fits into checkout
- Stripe Plugin — a complete shipped adapter