@devx-retailos/payments
v0.0.2
Published
Generic payments layer for retailOS. Pluggable PaymentAdapter interface with built-in cash adapter and stubs for Razorpay, Snapmint, Pine Labs, and Stripe. Supports split tenders and partial refunds.
Downloads
405
Keywords
Readme
@devx-retailos/payments
Generic payments layer for Medusa v2 POS backends: a pluggable PaymentAdapter interface with a fully working cash adapter, stub adapters for Razorpay, Snapmint, Pine Labs, and Stripe, split-tender reconciliation, and partial refunds.
Part of retailOS, a Medusa v2 SDK for offline-store POS systems. Each @devx-retailos/* package is an independently installable Medusa plugin; a brand backend composes the ones it needs in medusa-config.ts.
Installation
npm install @devx-retailos/paymentsPeer dependencies: @medusajs/framework and @medusajs/medusa ^2.15.0.
Setup
// medusa-config.ts
module.exports = defineConfig({
plugins: [
{ resolve: "@devx-retailos/payments", options: {} },
],
})The module registers under the key payments (exported as PAYMENTS_MODULE).
Key concepts
- PaymentMethod — a configured tender at an organization/store, pointing at an
adapter_typewith adapter-specificconfig(validated by the adapter'svalidateConfig). - Payment — one tender attempt with a status lifecycle (
pending → initialized → authorized → captured → partially_refunded/refunded, plusvoided/failed). Every transition is recorded as a PaymentEvent. - Split tenders — initiate any number of payments against the same
cart_id/order_id(cash + card + financing), then usereconcileto know when the basket is fully paid. - Partial refunds — refunds create PaymentRefund rows; refunding less than the captured amount moves the payment to
partially_refunded.
Usage
import { PAYMENTS_MODULE, type PaymentsModuleService } from "@devx-retailos/payments"
const payments: PaymentsModuleService = container.resolve(PAYMENTS_MODULE)
// Start a payment against a cart (split tender: call initiate once per tender)
const { payment, init } = await payments.initiate({
payment_method_id: "pm_...",
cart: { currency: "INR", total: 2500, organization_id: "org_...", store_id: "store_..." },
amount: 1000,
cart_id: "cart_...",
initiated_by_employee_id: "emp_...",
})
// Capture (cash captures immediately; gateway adapters may authorize first)
await payments.capture({ payment_id: payment.id, cart })
// Partial refund
await payments.refund({
payment_id: payment.id,
cart,
request: { amount: 250, reason: "damaged item" },
initiated_by_employee_id: "emp_...",
})
// Is the basket fully paid across all tenders?
const summary = await payments.reconcile({ cart_id: "cart_..." })
// → { required, captured, refunded, outstanding, fully_paid, payments }Other service methods: authorize, void, handleWebhook, listAdapters, plus generated CRUD for PaymentMethod, Payment, PaymentRefund, and PaymentEvent.
Built-in adapters
Exported from @devx-retailos/payments/adapters and pre-registered in the service:
| Adapter | type | Status |
| --- | --- | --- |
| cashAdapter | cash | Fully working. Offline, captures immediately, supports void and partial refunds. |
| razorpayAdapter | razorpay | Stub — interface and config schema in place, gateway calls not wired. |
| snapmintAdapter | snapmint | Stub |
| pineLabsAdapter | pinelabs | Stub |
| stripeAdapter | stripe | Stub |
BUILTIN_PAYMENT_ADAPTERS and createDefaultAdapterRegistry() are also exported if you need the raw registry.
Extension points
Implement PaymentAdapter<TConfig> (from @devx-retailos/payments/types) and register it on the service:
import type { PaymentAdapter } from "@devx-retailos/payments/types"
const upiAdapter: PaymentAdapter<{ vpa: string }> = {
type: "upi-qr",
description: "UPI via dynamic QR",
capabilities: {
supports_authorize_capture: false,
supports_partial_capture: false,
supports_refund: true,
supports_partial_refund: true,
supports_void: false,
supports_webhook: true,
is_offline: false,
},
validateConfig: (config) => config as { vpa: string },
initialize: async (config, ctx) => ({ status: "initialized" }),
capture: async (config, ctx, request) => ({ status: "captured", captured_amount: request.amount }),
refund: async (config, ctx, request) => ({ status: "succeeded", refunded_amount: request.amount }),
// optional: authorize, void, handleWebhook
}
payments.registerAdapter(upiAdapter)The service checks capabilities before delegating — e.g. partial capture/refund is rejected up front if the adapter does not support it.
Permissions
Exported as PAYMENTS_PERMISSIONS from @devx-retailos/payments/permissions:
| Key | Description |
| --- | --- |
| payment.method.read | View configured payment methods |
| payment.method.write | Create or update payment methods |
| payment.method.delete | Deactivate payment methods |
| payment.read | View payment records |
| payment.initiate | Start a payment against a cart |
| payment.capture | Capture an authorized payment |
| payment.void | Void a payment before capture |
| payment.refund | Refund a captured payment (full or partial) |
| payment.refund.approve | Approve a refund above the configured threshold |
| payment.webhook | Server-only permission for accepting provider webhooks |
Related packages
@devx-retailos/core— shared types,Logger, permission registry.@devx-retailos/rbac— roles and permission checks.@devx-retailos/order— POS orders that these payments settle.@devx-retailos/invoice— GST-compliant PDF invoices.
License
MIT
