npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@devx-retailos/order

v5.0.1

Published

Order module for retailOS. Manages POS orders, line items, and payments with built-in discount validation.

Downloads

938

Readme

@devx-retailos/order

POS order management for Medusa v2: orders, line items, payments, discount validation, advance orders with deposits and fulfillment stages, returns & policy-driven refunds, and daily / day-close reporting.

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/order

Peer dependencies: @medusajs/framework and @medusajs/medusa ^2.15.0. @devx-retailos/discount is an optional peer — required only if you place orders with discount_id or coupon_code.

Setup

// medusa-config.ts
module.exports = defineConfig({
  plugins: [
    { resolve: "@devx-retailos/rbac", options: {} },
    { resolve: "@devx-retailos/order", options: {} },
  ],
})

The module registers under the key retailos_order (exported as ORDER_MODULE) and registers its permission keys with @devx-retailos/rbac at boot.

Options

| Option | Type | Default | Purpose | | --- | --- | --- | --- | | refundPolicy | RefundPolicyConfig | ≤10d → 100%, ≤30d → 50%, beyond → ineligible; defective goods full refund | Time-based refund limitation used by the return flow. | | refundMatrix | RefundMatrix | { default: "ORIGINAL_TENDER" } | Decides the refund form (original tender / store credit / exchange-only), with optional per-category overrides. |

Both are read by the default refund-policy resolver; a brand can instead register a dynamic resolver (see Extension points).

Usage

import { ORDER_MODULE, type OrderModuleService } from "@devx-retailos/order"

const orders: OrderModuleService = container.resolve(ORDER_MODULE)

// Place a standard order (discount validated via @devx-retailos/discount if provided)
const { order } = await orders.placeOrder({
  organization_id: "org_...",
  store_id: "store_...",            // the PHYSICAL retail store
  cart_id: "cart_...",              // the cart this order came from
  ecommerce_store_name: "STORE_A",  // which ecommerce store it sells through
  currency: "INR",
  line_items: [{ id: "tmp-1", name: "T-Shirt", quantity: 2, unit_price: 499 }],
  coupon_code: "WELCOME10",
  placed_by_employee_id: "emp_...",
})

// Confirm: records the payment, sets status "confirmed", persists a permanent
// tax invoice (via @devx-retailos/invoice), records a stock issue (via
// @devx-retailos/inventory), and pushes the order to the registered ecommerce
// adapter (status becomes "synced_to_ecommerce"). All side modules are optional.
const result = await orders.confirmOrder(order.id, {
  payment: { method: "cash", amount: 998 },
})

Selling through more than one ecommerce store

A product id issued by one ecommerce store is meaningless in another, so a basket spanning two stores cannot become a single order there. Call placeOrder once per store, passing the same cart_id and invoice_code with a different ecommerce_store_name:

const shared = { cart_id: "cart_...", invoice_code: "INV-001", organization_id, store_id, currency: "INR" }

const a = await orders.placeOrder({ ...shared, ecommerce_store_name: "STORE_A", line_items: storeALines })
const b = await orders.placeOrder({ ...shared, ecommerce_store_name: "STORE_B", line_items: storeBLines })

(cart_id, ecommerce_store_name) is unique among active orders, so a retried or duplicated placement cannot create a second order for a store that already has one. Group the pieces back together with listOrders({ cart_id }).

invoice_code is deliberately not part of that key — adding a column to a unique index only weakens it, and the caller supplies the invoice code. Sharing one invoice across the pieces is the caller's job; the index guarantees there is at most one order per store regardless.

Splitting is the caller's job. This module does not decide which lines belong to which store, or how whole-bill figures divide across them — that is brand logic, and it stays in the brand. A single-store deployment simply passes the same ecommerce_store_name every time.

ecommerce_store_name is nullable on the model, because orders written before multi-store support predate the concept; it is required on PlaceOrderInput so every new order states it explicitly.

Advance orders

Passing advance_deposit_amount to placeOrder creates an advance order: the deposit is recorded, balance_due is tracked, and the order enters the first stage of its stage provider. Advance orders cannot use confirmOrder — collect the remainder with collectBalance:

const { order } = await orders.placeOrder({
  // ...as above
  advance_deposit_amount: 500,
  promised_fulfillment_date: new Date("2026-07-01"),
})

await orders.transitionStage(order.id, "processing", { changed_by_employee_id: "emp_..." })
await orders.collectBalance(order.id, [{ method: "card", amount: 1500 }])

listAdvanceOrders({ organization_id, fulfillment_stage?, promised_before?, promised_after? }) queries open advance orders.

Which coupons applied — *_discount_code is deprecated

online_discount_code and store_discount_code are deprecated as a way to learn which coupons applied, and will be removed in a future major. One column cannot name two coupons: an order carrying more than one records the first code against the summed amount, so a report grouping by it charges that coupon with liability it never earned and loses the others entirely.

Use listAppliedCoupons() from @devx-retailos/discount — one entry per coupon. It takes a fallback so orders written before that ledger existed still answer:

import { DISCOUNT_MODULE, type DiscountModuleService } from "@devx-retailos/discount"

const discounts = container.resolve<DiscountModuleService>(DISCOUNT_MODULE)

const applied = await discounts.listAppliedCoupons({
  order_id: order.id,
  fallback: {
    online_discount_code: order.online_discount_code,
    online_discount_amount: order.online_discount_amount,
    store_discount_code: order.store_discount_code,
    store_discount_amount: order.store_discount_amount,
  },
})

The two *_amount columns are not deprecated. They are the complete totals — nothing is missing from them — and grand_total and tax allocation derive from them. They stay.

All four columns are still written, unchanged. PlaceOrderInput still takes all four and still must: they are how the amounts reach the order. Nothing breaks yet.

Reports

const daily = await orders.dailyReport({ organization_id: "org_...", store_id: "store_...", date: "2026-06-11" })
// → order_count, subtotal, discount_total, tax_total, grand_total, payment_methods[]

const close = await orders.dayCloseReport({ organization_id: "org_...", store_id: "store_...", date: "2026-06-11" })
// → daily report + closed_at, opening_total, closing_total, variance

totalsForStore({ organization_id, store_id?, status? }) returns simple revenue aggregates.

Returns & refunds

A return is a new document — it never edits the original order. Refunds are based on what the customer actually paid per line (line subtotal − line discount − its prorated share of any order-level discount + tax), never on today's shelf price. A time-based policy then scales the refundable amount, and a matrix decides the form.

// 1. Preview — compute refundable amounts + eligibility without persisting
const preview = await orders.previewReturn({
  original_order_id: "ord_...",
  returning_store_id: "store_...",        // may differ from the selling store (cross-store)
  lines: [{ original_line_item_id: "oli_...", quantity: 1 }],
})
// → { refund_total, refund_multiplier_percent, refund_form, days_since_purchase, eligible, ... }

// 2. Create — validates eligibility, atomically claims returned quantity
//    (cumulative returns can never exceed what was purchased), persists the return
const { return: ret } = await orders.createReturn({
  original_order_id: "ord_...",
  returning_store_id: "store_...",
  lines: [{ original_line_item_id: "oli_...", quantity: 1 }],
  processed_by_employee_id: "emp_...",
})

// 3. Complete — quality check → mirror to Unicommerce → execute the refund → close
await orders.completeReturn(ret.id, {
  quality_results: [{ original_line_item_id: "oli_...", quality_result: "GOOD" }],
})

The refund is executed by form: STORE_CREDIT issues a @devx-retailos/gift-voucher voucher; ORIGINAL_TENDER delegates to a registered ReturnRefundExecutor (or records a pending refund payment if none is registered); EXCHANGE_ONLY records no monetary refund. On completion the return also (best-effort, when the relevant module is installed) mirrors to Unicommerce, persists a credit note via @devx-retailos/invoice, records a stock receive via @devx-retailos/inventory, and — for a cross-store return — writes an inter-store settlement (store Y refunded on store X's behalf). Missing sibling modules degrade gracefully — a refund is never lost because a mirror is unavailable.

Exchanges

An exchange is one return leg plus one new sale under a single id, committed as a saga:

const { exchange, return: ret, new_order } = await orders.createExchange({
  original_order_id: "ord_...",
  returning_store_id: "store_...",
  return_lines: [{ original_line_item_id: "oli_...", quantity: 1, quality_result: "GOOD" }],
  new_sale: {                       // the new item at today's price — a fresh order + invoice
    organization_id: "org_...",
    store_id: "store_...",
    cart_id: "cart_...",            // new_sale is a PlaceOrderInput — both are required
    ecommerce_store_name: "STORE_A",
    currency: "INR",
    line_items: [{ id: "tmp-1", name: "New Tee", quantity: 1, unit_price: 2500 }],
  },
  courier_pickup: false,            // true → raise a Unicommerce reverse pickup for the old item
  processed_by_employee_id: "emp_...",
})
// settle = new_invoice − credit_note: positive → customer pays the difference,
// negative → surplus issued as store credit.

The old item is returned as EXCHANGE_ONLY (restocked + credit note, no cash refund) and the new item goes out as a normal order — with its own fresh invoice and a Unicommerce sale order. If the sale leg fails before the return completes, the new order is cancelled and the return is rolled back — you never end up with half an exchange. For a courier exchange (courier_pickup: true), the old item is collected via a Unicommerce reverse pickup (the brand's UC adapter must implement createReversePickup) rather than a counter drop-off.

Inter-store settlement

Cross-store returns and exchanges write an inter-store settlement entry (debtor = selling store, creditor = returning store). List open entries with GET /admin/retailos/settlements and square them up with settleInterStore(id) / POST /admin/retailos/settlements/:id/settle.

Extension points

OrderEcommerceAdapter

Pushes confirmed POS orders to an ecommerce platform (Shopify, WooCommerce, …). Register an implementation in the container under the ORDER_ECOMMERCE_ADAPTER key ("order_ecommerce_adapter"). If no adapter is registered, confirmed orders stay at status "confirmed" and can be retried later; sync failures are recorded in the order's metadata.

import type { OrderEcommerceAdapter, NormalizedOrderForEcommerce } from "@devx-retailos/order"

const adapter: OrderEcommerceAdapter = {
  async createOrder(order: NormalizedOrderForEcommerce) {
    return { ecommerce_order_id: "ext_123" }
  },
  async cancelOrder(ecommerce_order_id: string) {},
}

AdvanceOrderStageProvider

Defines the fulfillment stages of advance orders. The built-in DefaultStageProvider ships stages pending → processing → ready → fulfilled / cancelled (fulfilled and cancelled are terminal).

import type { AdvanceOrderStageProvider } from "@devx-retailos/order"

const madeToOrder: AdvanceOrderStageProvider = {
  name: "made-to-order",
  stages: () => ["pending", "in_production", "qc", "ready", "fulfilled", "cancelled"],
  isTransitionAllowed: (from, to) => from !== "fulfilled" && from !== "cancelled",
}

orders.registerStageProvider(madeToOrder)
// then: placeOrder({ ..., advance_deposit_amount: 500, stage_provider: "made-to-order" })

RefundPolicyResolver

Decides the time tiers ("how much") and the refund matrix ("in what form") for a return. The default resolver reads the refundPolicy / refundMatrix plugin options. Register a custom resolver to vary policy per store, category, or product:

import type { RefundPolicyResolver } from "@devx-retailos/order"

const resolver: RefundPolicyResolver = {
  name: "seasonal",
  resolve: (ctx) => ({ policy: {/* tiers */}, matrix: { default: "STORE_CREDIT" } }),
}
orders.registerRefundPolicyResolver(resolver)

ReturnRefundExecutor

Executes an ORIGINAL_TENDER refund against the payment gateway. Register an implementation under the RETURN_REFUND_EXECUTOR key — typically wrapping @devx-retailos/payments refund with your own PaymentCart context. If none is registered, completeReturn records a pending refund payment for the brand to reconcile out of band.

import type { ReturnRefundExecutor } from "@devx-retailos/order"

const executor: ReturnRefundExecutor = {
  async executeRefund(ctx) {
    // ctx: { return_id, original_order_id, amount, currency, store_id, ... }
    return { status: "succeeded", reference: "gw_txn_123" }
  },
}
// container.register(RETURN_REFUND_EXECUTOR, asValue(executor))

Permissions

Registered via @devx-retailos/rbac (also exported as ORDER_PERMISSIONS from @devx-retailos/order/permissions):

| Key | Description | | --- | --- | | order.read | View orders and order details | | order.create | Place new orders | | order.update | Update order status, notes, and metadata | | order.cancel | Cancel orders | | order.refund | Mark orders as refunded | | order.void | Void orders | | order.return | Create and complete returns against orders | | order.return.approve | Approve returns outside the standard refund policy | | order.exchange | Create exchanges (return plus new sale) | | order.settlement | View and settle inter-store settlement ledger entries | | order.report | View order totals and revenue aggregates | | order.discount_apply | Place orders with a discount or coupon attached | | order.advance_stage | Transition an advance order's fulfillment stage | | order.collect_balance | Capture balance payment on an advance order | | order.invoice | Generate and download GST-compliant PDF invoices | | order.report.daily | View daily sales report and revenue aggregates | | order.report.day-close | Run the end-of-day Z-report and close the trading session |

API routes

All routes live under the authenticated Medusa admin scope. The report routes enforce their permission via @devx-retailos/rbac.

| Method | Path | Purpose | Permission | | --- | --- | --- | --- | | GET | /admin/retailos/orders | List orders (filterable) | — | | POST | /admin/retailos/orders | Place an order (standard or advance) | — | | GET | /admin/retailos/orders/:id | Retrieve order + line items + payments | — | | POST | /admin/retailos/orders/:id | Update status / notes / metadata | — | | DELETE | /admin/retailos/orders/:id | Cancel an order | — | | POST | /admin/retailos/orders/:id/confirm | Confirm + push to ecommerce adapter | — | | POST | /admin/retailos/orders/:id/returns/preview | Preview refundable amounts + eligibility | order.return | | POST | /admin/retailos/orders/:id/returns | Create a return | order.return | | POST | /admin/retailos/orders/:id/returns/:returnId/complete | QC → mirror → refund → close | order.return | | POST | /admin/retailos/orders/:id/exchanges | Create an exchange (return + new sale) | order.exchange | | GET | /admin/retailos/settlements | List inter-store settlement entries | order.settlement | | POST | /admin/retailos/settlements/:id/settle | Mark a settlement squared up | order.settlement | | POST | /admin/retailos/orders/:id/balance | Collect balance on an advance order | order.collect_balance | | PUT | /admin/retailos/orders/:id/stage | Transition an advance order's stage | order.advance_stage | | GET | /admin/retailos/orders/totals | Revenue aggregates per org/store | — | | GET | /admin/retailos/orders/reports/daily | Daily sales report | order.report.daily | | POST | /admin/retailos/orders/reports/day-close | End-of-day close report | order.report.day-close |

Errors

All extend RetailOSError from @devx-retailos/core; switch on err.code:

  • RETAILOS_ORDER_NOT_FOUND
  • RETAILOS_ORDER_INVALID_STATUS
  • RETAILOS_ORDER_DEPOSIT_EXCEEDS_TOTAL
  • RETAILOS_ORDER_INVALID_STAGE_TRANSITION
  • RETAILOS_ORDER_NOT_ADVANCE
  • RETAILOS_ORDER_CONFIRM_BLOCKED_FOR_ADVANCE
  • RETAILOS_ORDER_OVER_RETURN
  • RETAILOS_ORDER_RETURN_NOT_ELIGIBLE
  • RETAILOS_ORDER_RETURN_NOT_FOUND
  • RETAILOS_ORDER_RETURN_INVALID_STATUS

Related packages

  • @devx-retailos/core — shared types, RetailOSError, Logger, permission registry.
  • @devx-retailos/rbac — roles and permission checks the order routes rely on.
  • @devx-retailos/discount — discount/coupon validation used by placeOrder (optional).
  • @devx-retailos/payments — pluggable payment adapters, split tenders, refunds.
  • @devx-retailos/gift-voucher — store-credit vouchers issued on STORE_CREDIT refunds (optional).
  • @devx-retailos/invoice — GST-compliant PDF invoices generated from these orders.
  • @devx-retailos/products — catalog mirror that supplies product/variant data.

License

MIT