@devx-retailos/gift-voucher
v0.0.2
Published
Generic gift voucher module for retailOS: issuance, redemption-as-tender, ledger, expiry, and refund-to-voucher.
Keywords
Readme
@devx-retailos/gift-voucher
Gift voucher module for POS backends: issuance, redemption-as-tender, an append-only transaction ledger, expiry, top-up, void, and refund-to-voucher.
Part of retailOS, a Medusa v2 SDK for offline-store POS systems. Packages are installed independently and composed in a brand's Medusa backend.
Installation
npm install @devx-retailos/gift-voucherRequires @medusajs/framework and @medusajs/medusa ^2.15.0 as peer dependencies. Depends on @devx-retailos/core and @devx-retailos/rbac (vouchers are linked to RBAC organizations and stores, and API routes are permission-guarded).
Setup
// medusa-config.ts
export default defineConfig({
// ...
plugins: [
{
resolve: "@devx-retailos/gift-voucher",
options: {},
},
],
})The module registers under the gift_voucher key (exported as GIFT_VOUCHER_MODULE) and registers its permission keys at boot via a module loader.
Usage
import { GIFT_VOUCHER_MODULE, type GiftVoucherModuleService } from "@devx-retailos/gift-voucher"
const vouchers = container.resolve<GiftVoucherModuleService>(GIFT_VOUCHER_MODULE)
// 1. Issue — generates a unique code, writes an ISSUE ledger entry
const voucher = await vouchers.issue({
organization_id: "org_01",
store_id: "store_01",
currency: "INR",
initial_balance: 100000,
issued_by_employee_id: "emp_01",
expires_at: new Date("2027-01-01"),
})
// 2. Check validity and balance before tendering
const status = await vouchers.check(voucher.code)
// → { valid, balance, expires_at, status }
// 3. Redeem as tender at checkout — writes a DEBIT ledger entry
await vouchers.debit({ code: voucher.code, amount: 25000, order_id: "order_01" })
// 4. Refund back to the voucher — writes a CREDIT ledger entry
await vouchers.credit({ voucher_id: voucher.id, amount: 25000, order_id: "order_01" })
// Also available: topup(), void(), listTransactions(voucher_id)Voucher status moves through ACTIVE → PARTIALLY_USED → USED; VOIDED is terminal, and check() reports EXPIRED for past-expiry vouchers. Every balance change is recorded as a VoucherTxn (ISSUE, DEBIT, CREDIT, VOID, TOPUP).
Redemption as a payment tender
@devx-retailos/gift-voucher/payment-adapter exports a factory that produces a PaymentAdapter for @devx-retailos/payments — it debits the ledger on capture and credits on refund:
import { createVoucherPaymentAdapter } from "@devx-retailos/gift-voucher/payment-adapter"
const adapter = createVoucherPaymentAdapter(vouchers) // GiftVoucherModuleService
paymentsService.registerAdapter(adapter) // PaymentsModuleService from @devx-retailos/paymentsThe adapter's type is "voucher"; its config is { voucher_code, voucher_id? } (voucher_id is required for refunds).
Extension points
Voucher records can be mirrored to an external voucher backend by registering a VoucherBackendAdapter. The built-in internal adapter (ledger only, no external system) is registered by default:
import type { VoucherBackendAdapter } from "@devx-retailos/gift-voucher"
const externalAdapter: VoucherBackendAdapter<{ api_key: string }> = {
type: "external_provider",
validateConfig(config) {
return config as { api_key: string } // typically a zod parse
},
async issue(config, voucher) {
// create the voucher upstream; voucher = { id, code, amount, currency }
return { external_id: "ext_123" }
},
async debit(config, external_id, amount, currency) { /* ... */ },
async credit(config, external_id, amount, currency) { /* ... */ },
}
vouchers.registerAdapter(externalAdapter)
// then: vouchers.issue({ ..., adapter_type: "external_provider", adapter_config: { api_key } })Permissions
Registered via VOUCHER_PERMISSIONS (subpath @devx-retailos/gift-voucher/permissions):
voucher.read— view vouchers and their ledgervoucher.issue— issue a new gift vouchervoucher.redeem— redeem (debit) a voucher at checkoutvoucher.credit— credit (refund) an amount back to a vouchervoucher.void— void a voucher permanentlyvoucher.report— view voucher aggregates and export data
API routes
All routes are guarded with requirePermission from @devx-retailos/rbac/middlewares:
| Method | Path | Description | Permission |
| --- | --- | --- | --- |
| GET | /admin/retailos/vouchers | List vouchers | voucher.read |
| POST | /admin/retailos/vouchers | Issue a voucher | voucher.issue |
| GET | /admin/retailos/vouchers/:id | Voucher detail + full ledger | voucher.read |
| POST | /admin/retailos/vouchers/:id/topup | Add balance to a voucher | voucher.issue |
| POST | /admin/retailos/vouchers/:id/void | Void a voucher | voucher.void |
| GET | /admin/retailos/vouchers/check/:code | Check validity and balance by code | voucher.redeem |
Errors
All errors extend RetailOSError from @devx-retailos/core — switch on err.code:
RETAILOS_VOUCHER_NOT_FOUNDRETAILOS_VOUCHER_EXPIREDRETAILOS_VOUCHER_INSUFFICIENT_BALANCE(carriesavailableandrequested)RETAILOS_VOUCHER_ALREADY_VOIDEDRETAILOS_VOUCHER_INVALID_STATUS(carriesstatus)RETAILOS_VOUCHER_CODE_CONFLICT
Related packages
@devx-retailos/core— shared types,Logger,RetailOSError, permission registry@devx-retailos/rbac— organizations, stores, roles, permission enforcement@devx-retailos/payments— pluggable payment adapters; host for the voucher tender@devx-retailos/order— POS orders and payments capture@devx-retailos/discount— discount and coupon engine@devx-retailos/sdk-client— typed frontend client and React hooks
License
MIT
