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

v0.0.2

Published

Unicommerce WMS integration for retailOS. Provides a generic HTTP service, sale order payload helpers, pluggable UnicommerceOrderAdapter interface, and admin recovery endpoints.

Downloads

331

Readme

@devx-retailos/unicommerce

Unicommerce WMS integration for retailOS brand backends.

Provides a generic HTTP service, sale order payload helpers, a pluggable UnicommerceOrderAdapter interface, and six admin recovery endpoints.


Contents


Overview

Unicommerce (Uniware) is an order/warehouse management system. It IP-whitelists only the deployed backend server, so recovery operations cannot be run locally. This package solves that by exposing HTTP admin endpoints — any authenticated admin can retry failed operations via Postman or curl without SSH access.

Architecture

medusa-config.ts
  └─ modules:
       ├─ @devx-retailos/unicommerce    ← UnicommerceService (HTTP client) + 6 admin routes
       └─ @/modules/uc-adapter     ← Brand-specific UnicommerceOrderAdapter (your code)

The package ships two kinds of admin endpoints:

| Kind | Endpoints | How it works | |---|---|---| | Generic | GET /sale-order/:code, POST /adjust-inventory, POST /sale-order/:code/cancel | Call UnicommerceService directly. No brand schema needed. | | Adapter | POST /create-sale-order, POST /complete-sale-order-fulfillment, POST /process-return | Delegate to your UnicommerceOrderAdapter implementation. |


Installation

pnpm add @devx-retailos/unicommerce

Configuration

The service reads credentials from module options (preferred) or falls back to environment variables.

| Option / Env var | Required | Description | |---|---|---| | apiUrl / UNICOMMERCE_API_URL | yes | Base URL, e.g. https://yourstore.unicommerce.com | | clientId / UNICOMMERCE_CLIENT_ID | yes | OAuth client ID | | username / UNICOMMERCE_USERNAME | yes | Unicommerce admin username | | password / UNICOMMERCE_PASSWORD | yes | Unicommerce admin password | | channelCode / UNICOMMERCE_CHANNEL | no | Sale order channel code (default: POSX) | | shippingCode / UNICOMMERCE_SHIPPING_CODE | no | Shipping provider code (default: SELF) | | gatePassCodePrefix / UNICOMMERCE_GATEPASS_INITIALS | no | Prefix used to derive gate pass codes as ${prefix}-${orderCode} |


Registering the module

Add both the unicommerce module and your adapter module to medusa-config.ts:

// medusa-config.ts
import { defineConfig } from "@medusajs/framework/utils"

export default defineConfig({
  modules: [
    // 1. The @devx-retailos/unicommerce module (service + admin routes)
    {
      resolve: "@devx-retailos/unicommerce",
      options: {
        apiUrl: process.env.UNICOMMERCE_API_URL,
        clientId: process.env.UNICOMMERCE_CLIENT_ID,
        username: process.env.UNICOMMERCE_USERNAME,
        password: process.env.UNICOMMERCE_PASSWORD,
        channelCode: process.env.UNICOMMERCE_CHANNEL,
        shippingCode: process.env.UNICOMMERCE_SHIPPING_CODE,
      },
    },

    // 2. Your brand-specific order adapter (see next section)
    { resolve: "@/modules/unicommerce-adapter" },
  ],
})

Implementing the order adapter

The three "adapter" endpoints (create-sale-order, complete-sale-order-fulfillment, process-return) delegate to a brand-provided module registered at the UNICOMMERCE_ORDER_ADAPTER container key. This is where your order schema knowledge lives.

Step 1 — Create the adapter module

src/modules/unicommerce-adapter/
├── index.ts
└── service.ts

src/modules/unicommerce-adapter/index.ts

import { Module } from "@medusajs/framework/utils"
import { UNICOMMERCE_ORDER_ADAPTER } from "@devx-retailos/unicommerce"
import UnicommerceAdapterService from "./service"

export default Module(UNICOMMERCE_ORDER_ADAPTER, {
  service: UnicommerceAdapterService,
})

src/modules/unicommerce-adapter/service.ts

import { MedusaService } from "@medusajs/framework/utils"
import { MedusaContainer } from "@medusajs/framework/types"
import type { UnicommerceOrderAdapter } from "@devx-retailos/unicommerce"

// Import your existing subscriber / job handlers
import unicommerceSaleOrderCreationHandler from "@/subscribers/unicommerce-sale-order-creation"
import completeUnicommerceFulfillments from "@/jobs/complete-unicommerce-fulfillments"
import unicommerceReturnProcessingHandler from "@/subscribers/unicommerce-return-processing"

class UnicommerceAdapterService extends MedusaService({}) implements UnicommerceOrderAdapter {
  async createSaleOrders(orderIds: string[], container: MedusaContainer): Promise<void> {
    for (const orderId of orderIds) {
      await unicommerceSaleOrderCreationHandler({
        event: { data: { order_id: orderId } } as any,
        container,
      } as any)
    }
  }

  async completeFulfillments(orderIds: string[], container: MedusaContainer): Promise<void> {
    await completeUnicommerceFulfillments(container, { orderIds })
  }

  async processReturns(orderIds: string[], container: MedusaContainer): Promise<void> {
    for (const orderId of orderIds) {
      await unicommerceReturnProcessingHandler({
        event: { data: { orderId } } as any,
        container,
      } as any)
    }
  }
}

export default UnicommerceAdapterService

Step 2 — Register in medusa-config.ts

modules: [
  { resolve: "@devx-retailos/unicommerce", options: { ... } },
  { resolve: "@/modules/unicommerce-adapter" },     // ← add this
]

If the adapter is not registered, the three adapter endpoints return 501 Not Implemented with a descriptive error message.


Admin API reference

All endpoints are under /admin/unicommerce/ and require the standard Medusa admin Authorization: Bearer <token> header.


GET /admin/unicommerce/sale-order/:code

Check the current status of a sale order in Unicommerce.

Path param

| Param | Description | |---|---| | code | Unicommerce sale order code (same as your internal order ID by default) |

Example

curl -X GET \
  https://your-backend.com/admin/unicommerce/sale-order/ORDER-001 \
  -H "Authorization: Bearer <token>"

Success response — Unicommerce saleOrderDTO object:

{
  "successful": true,
  "saleOrderDTO": {
    "code": "ORDER-001",
    "statusCode": "FULFILLABLE",
    "saleOrderItems": [
      { "code": "ORDER-001_1", "itemSku": "SKU-XYZ", "statusCode": "FULFILLABLE" }
    ]
  }
}

POST /admin/unicommerce/sale-order/:code/cancel

Cancel a sale order. Performs a pre-flight status check — returns 409 if the order is already CANCELLED, COMPLETE, or CLOSED.

Path param

| Param | Description | |---|---| | code | Unicommerce sale order code |

Example

curl -X POST \
  https://your-backend.com/admin/unicommerce/sale-order/ORDER-001/cancel \
  -H "Authorization: Bearer <token>"

409 — already terminal

{
  "message": "Cannot cancel sale order \"ORDER-001\" — current status is COMPLETE.",
  "statusCode": "COMPLETE"
}

Success response — Unicommerce cancel response.


POST /admin/unicommerce/adjust-inventory

Bulk inventory adjustment at a facility. Supports ADD, REMOVE, REPLACE, TRANSFER.

Request body

{
  facilityCode: string                 // required — Unicommerce facility code
  inventoryAdjustments: Array<{
    itemSKU: string                    // required
    quantity: number                   // required
    shelfCode: string                  // required
    adjustmentType: "ADD" | "REMOVE" | "REPLACE" | "TRANSFER"  // required
    facilityCode: string               // required (same as top-level)
    inventoryType?: "GOOD_INVENTORY" | "BAD_INVENTORY" | "QC_REJECTED" | "VIRTUAL_INVENTORY"
    remarks?: string
    transferToShelfCode?: string       // required when adjustmentType = "TRANSFER"
    sla?: string
  }>
  forceAllocate?: boolean              // default: false
}

Example — add inventory for an UNFULFILLABLE backlog

curl -X POST \
  https://your-backend.com/admin/unicommerce/adjust-inventory \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "facilityCode": "POSX_Central",
    "inventoryAdjustments": [
      {
        "itemSKU": "SKU-XYZ",
        "quantity": 50,
        "shelfCode": "DEFAULT",
        "adjustmentType": "ADD",
        "facilityCode": "POSX_Central",
        "inventoryType": "GOOD_INVENTORY",
        "remarks": "Manual adjustment for fulfillment backlog"
      }
    ]
  }'

POST /admin/unicommerce/create-sale-order

Retry sale order creation for orders stuck in FAILED_SALE_ORDER. Triggers your adapter's createSaleOrders method asynchronously.

Request body

{
  "order_ids": ["order-uuid-1", "order-uuid-2"]
}

Response — 202 Accepted (fire-and-forget)

{
  "message": "Sale order creation triggered for 2 order(s).",
  "order_ids": ["order-uuid-1", "order-uuid-2"]
}

When to use

  • Order stuck at warehouse_status = FAILED_SALE_ORDER
  • Fix the root cause first (bad address format, SKU not in Unicommerce item master), then call this endpoint to retry

POST /admin/unicommerce/complete-sale-order-fulfillment

Run the fulfillment pipeline (invoice → manifest → deliver) for specific orders. Triggers your adapter's completeFulfillments method asynchronously.

Request body

{
  "order_ids": ["order-uuid-1", "order-uuid-2"]
}

Response — 202 Accepted

{
  "message": "Fulfillment pipeline triggered for 2 order(s).",
  "order_ids": ["order-uuid-1", "order-uuid-2"]
}

When to use

  • Orders stuck at UNFULFILLABLE — after calling adjust-inventory to add stock, Unicommerce auto-marks them FULFILLABLE, then call this to complete dispatch
  • Bulk backlog after a cron gap (e.g. 4,500+ orders missed by rolling time window)

POST /admin/unicommerce/process-return

Trigger the reverse-logistics flow for returned / exchanged orders. Triggers your adapter's processReturns method asynchronously.

Request body

{
  "order_ids": ["order-uuid-1", "order-uuid-2"]
}

Response — 202 Accepted

{
  "message": "Return processing triggered for 2 order(s).",
  "order_ids": ["order-uuid-1", "order-uuid-2"]
}

When to use

  • Return subscriber fired but failed, and the physical return happened without Unicommerce inventory being updated
  • Order was never dispatched in Unicommerce first — fix that, then call this

Service methods

Resolve UnicommerceService from the container (container.resolve(UNICOMMERCE_MODULE)) to call Unicommerce directly. All methods authenticate lazily and retry once on an expired token. Beyond the sale-order, fulfillment, return, and adjustInventoryBulk methods used by the admin routes, the service covers the protocol surface every brand POS needs:

Gate pass

Inbound-goods ("purchase gate pass") operations. createGatePass derives the code from gatePassCodePrefix + orderCode and is idempotent: if Unicommerce reports the code already exists (error 100222), the existing gate pass is fetched and returned instead of failing.

import { UNICOMMERCE_MODULE, UnicommerceService } from "@devx-retailos/unicommerce"

const uc = container.resolve<UnicommerceService>(UNICOMMERCE_MODULE)

const gatePass = await uc.createGatePass({ orderCode: "ORDER-1", partyCode: "VENDOR-1", facilityId })
await uc.addItemToGatePass({ gatePassCode: "POSX-ORDER-1", itemCode: "IT-1", facilityId })
await uc.removeItemFromGatePass({ gatePassCode: "POSX-ORDER-1", itemCode: "IT-1", facilityId })
await uc.getGatePass({ gatePassCodes: ["POSX-ORDER-1"], facilityId })
await uc.completeGatePass({ gatePassCode: "POSX-ORDER-1", facilityId })
await uc.discardGatePass({ gatePassCode: "POSX-ORDER-1", facilityId })

Catalog lookup

await uc.getItemByItemCode({ itemCode: "IT-1", facilityId })   // by warehouse item code
await uc.getItemTypeBySkuCode({ skuCode: "SKU-A", facilityId }) // by catalog SKU code

Inventory export

pullInventoryExport runs Unicommerce's async export-job flow end to end (create job → poll until COMPLETE → download + parse CSV) and returns the parsed rows. Failures are returned as { successful: false, error } rather than thrown, so a multi-facility sync can continue past one bad facility.

const result = await uc.pullInventoryExport({
  facilityId,
  updatedSince: new Date(new Date().setHours(0, 0, 0, 0)).toISOString(),
})
// result.data → Array<Record<columnHeader, value>>

For the live, per-bucket snapshot use getInventorySnapshot({ updatedSinceInMinutes, facilityId }) and normalize it with the inventory helpers.

Fulfillment pipeline

fulfillSaleOrder runs the per-order pipeline — fetch the sale order → verify every item is FULFILLABLE → create an invoice/label per shipping package → close the manifest → mark delivered. It returns a structured per-stage result instead of throwing, so you map the outcome onto your own order status and persist it. channel / shippingProviderCode fall back to the configured module options.

const result = await uc.fulfillSaleOrder({ saleOrderCode: "ORDER-1" })

if (result.successful) {
  // result.stage === "COMPLETE", result.shippingManifestCode available
} else {
  // result.stage ∈ "FETCH" | "FULFILLABILITY" | "INVOICE" | "MANIFEST" | "DELIVER"
  // result.error, result.unfulfillableItems (when stage === "FULFILLABILITY")
}

Scheduling, order selection (time windows, status filters), and persistence of the result stay in your backend — this method owns only the Unicommerce protocol sequence.


Payload helpers

Exported from @devx-retailos/unicommerce/helpers. Use these in your adapter / subscriber to build the Unicommerce sale order payload.

transformAddressForUnicommerce(address, email)

Converts your internal address object to the Unicommerce address format.

import { transformAddressForUnicommerce } from "@devx-retailos/unicommerce/helpers"

const ucAddress = transformAddressForUnicommerce(
  {
    id: "addr-001",
    firstName: "Priya",
    lastName: "Sharma",
    address1: "12 MG Road",
    address2: "Block B",
    city: "Bengaluru",
    state: "Karnataka",
    country: "India",
    pinCode: "560001",
    phone: "9876543210",
  },
  "[email protected]",
)

buildSaleOrderPayload(config)

Assembles the complete Unicommerce POST /saleOrder/create request body.

import {
  buildSaleOrderPayload,
  transformAddressForUnicommerce,
} from "@devx-retailos/unicommerce/helpers"

const payload = buildSaleOrderPayload({
  orderId: orderDetail.id,
  orderCode: orderDetail.order_code,
  orderCreatedAt: orderDetail.created_at,
  customerId: orderDetail.customer_id,
  customerName: `${orderDetail.first_name} ${orderDetail.last_name}`.trim(),
  gstNumber: orderDetail.gst_number ?? "",
  email: orderDetail.email ?? "",
  phone: orderDetail.phone ?? "",
  facilityCode: storeDetails.warehouse_mapping_id,
  products: orderDetail.order_product_details.map((p) => ({
    id: p.id,
    variant_sku: p.variant_sku,
    quantity: p.quantity,
    unit_price: Number(p.unit_price),
    total_discount: Number(p.total_discount),
  })),
  addresses: [billingUcAddress, shippingUcAddress],
  billingAddressId: billingAddress.id,
  shippingAddressId: shippingAddress.id,
  totalDiscount: Number(orderDetail.total_discount ?? 0),
  totalPrepaidAmount: Number(orderDetail.total_with_discount),
  customFields: [{ name: "STN", value: "RETAIL" }],
  channelCode: process.env.UNICOMMERCE_CHANNEL,
})

createSaleOrderItems(products, facilityCode)

Splits products with quantity > 1 into individual Unicommerce line items with chronological codes (product_id_1, product_id_2, …).

import { createSaleOrderItems } from "@devx-retailos/unicommerce/helpers"

const items = createSaleOrderItems(products, "POSX_Central")

Inventory helpers

Exported from @devx-retailos/unicommerce/helpers. These turn raw Unicommerce inventory responses into a brand-agnostic shape. They are pure functions — no storage opinion. Persist the result into whatever inventory table your backend owns.

normalizeInventorySnapshot(rows, facilityCode)

Maps the raw inventorySnapshots rows from getInventorySnapshot(...) to NormalizedInventory[]. All ten Unicommerce stock buckets are mapped to snake_case fields and missing buckets default to 0.

import { normalizeInventorySnapshot } from "@devx-retailos/unicommerce/helpers"

const snapshot = await uc.getInventorySnapshot({ updatedSinceInMinutes: 1440, facilityId })
const rows = normalizeInventorySnapshot(snapshot.inventorySnapshots ?? [], facilityId)
// rows[0] → { sku, facility_code, inventory, open_sale, open_purchase, putaway_pending,
//             inventory_blocked, pending_stock_transfer, vendor_inventory, virtual_inventory,
//             pending_inventory_assessment, bad_inventory }

mergeInventoryExportMetadata(normalized, exportRows, columns?)

The JSON snapshot omits bad-inventory for most facilities; the CSV export carries it. This overlays bad_inventory onto the normalized rows, matched by SKU (case-insensitive). Column headers default to Unicommerce's export (Item SkuCode, Bad Inventory) and can be overridden.

import { mergeInventoryExportMetadata } from "@devx-retailos/unicommerce/helpers"

const { data: exportRows } = await uc.pullInventoryExport({ facilityId, updatedSince })
const merged = mergeInventoryExportMetadata(rows, exportRows)

normalizeSaleOrderStatus(rawStatusCode)

Unicommerce returns a wide, inconsistent set of statusCode values across sale orders and their items. This collapses the known Unicommerce vocabulary into the NormalizedSaleOrderStatus enum so you branch on a stable value instead of raw strings. Unrecognized codes return UNKNOWN.

import { normalizeSaleOrderStatus, NormalizedSaleOrderStatus } from "@devx-retailos/unicommerce"

const status = normalizeSaleOrderStatus(item.statusCode) // e.g. "MANIFESTED" → READY_TO_SHIP
if (status === NormalizedSaleOrderStatus.DELIVERED) { /* ... */ }

normalizeSaleOrderStatus is also available from @devx-retailos/unicommerce/helpers; the NormalizedSaleOrderStatus enum is exported from the package root and @devx-retailos/unicommerce/types.

The enum covers Unicommerce protocol statuses only. Brand pipeline/failure states (FAILED_INVOICE, FAILED_MANIFEST, ERROR, …) are your backend's own order status — keep them out of this mapping.


Error codes

All errors from this package extend RetailOSError and have a stable code field.

| Code | Class | Description | |---|---|---| | RETAILOS_UNICOMMERCE_AUTH_FAILED | UnicommerceAuthError | OAuth token fetch failed | | RETAILOS_UNICOMMERCE_API_ERROR | UnicommerceApiError | A Unicommerce REST call returned an error or non-200 | | RETAILOS_UNICOMMERCE_ADAPTER_NOT_REGISTERED | UnicommerceAdapterNotRegisteredError | UNICOMMERCE_ORDER_ADAPTER key not found in the container |

import { UnicommerceApiError } from "@devx-retailos/unicommerce"

try {
  await unicommerceService.createSaleOrder(payload)
} catch (err) {
  if (err instanceof UnicommerceApiError) {
    console.log(err.code)      // "RETAILOS_UNICOMMERCE_API_ERROR"
    console.log(err.operation) // "createSaleOrder"
    console.log(err.details)   // raw Unicommerce errors array
  }
}

Failure recovery runbook

Case 1 — FAILED_SALE_ORDER (bad address or SKU not in item master)

1. Check CloudWatch logs for the specific error (address validation / item not found)
2. If address: fix the order address in the POS system
   If SKU: create the item in Unicommerce item master via the Unicommerce portal
3. POST /admin/unicommerce/create-sale-order
   Body: { "order_ids": ["<order_id>"] }

Case 2 — UNFULFILLABLE (0 inventory in Unicommerce)

1. POST /admin/unicommerce/adjust-inventory
   Body: { "facilityCode": "<facility>", "inventoryAdjustments": [{ "itemSKU": "...", "quantity": N, "adjustmentType": "ADD", ... }] }
2. Unicommerce automatically marks affected orders as FULFILLABLE
3. POST /admin/unicommerce/complete-sale-order-fulfillment
   Body: { "order_ids": ["<order_id_1>", "<order_id_2>", ...] }

Case 3 — Cron window gap (bulk backlog)

1. POST /admin/unicommerce/complete-sale-order-fulfillment
   Body: { "order_ids": ["id1", "id2", ... all affected order IDs] }

Case 4 — FAILED_RETURN (return subscriber failed)

1. Verify the sale order exists and was dispatched in Unicommerce:
   GET /admin/unicommerce/sale-order/<code>
2. If not dispatched: run Case 2 / Case 3 first
3. POST /admin/unicommerce/process-return
   Body: { "order_ids": ["<order_id>"] }

Case 5 — Cancel a stuck order

1. GET /admin/unicommerce/sale-order/<code>    ← check current status
2. POST /admin/unicommerce/sale-order/<code>/cancel
   Returns 409 if already CANCELLED / COMPLETE