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

v0.2.0

Published

Discount and coupon engine for retailOS. Pluggable DiscountStrategy and EligibilityCheck interfaces with built-in implementations for percentage, fixed, BOGO, tiered, bundle, manual, free-item, and shipping waiver discounts.

Readme

@devx-retailos/discount

Discount and coupon engine for POS checkouts. Pluggable DiscountStrategy and EligibilityCheck registries with eight built-in strategies (percentage, fixed, BOGO, tiered, bundle, manual, free item, shipping waiver) and an auditable application ledger. A pluggable DiscountSourceAdapter (Shopify built-in) mirrors externally-managed discounts/coupons into the same engine.

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

Requires @medusajs/framework and @medusajs/medusa ^2.15.0 as peer dependencies. Depends on @devx-retailos/core and @devx-retailos/rbac.

Setup

// medusa-config.ts
export default defineConfig({
  // ...
  plugins: [
    {
      resolve: "@devx-retailos/discount",
      options: {},
    },
  ],
})

The module registers under the discount key (exported as DISCOUNT_MODULE).

Usage

Resolve DiscountModuleService from the container, then evaluate or apply a discount against a cart snapshot:

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

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

const evaluation = await discounts.evaluate({
  coupon_code: "WELCOME10", // or discount_id: "disc_..."
  cart: {
    currency: "INR",
    organization_id: "org_01",
    store_id: "store_01",
    customer_id: "cus_01",
    line_items: [
      { id: "li_1", quantity: 1, unit_price: 5000, subtotal: 5000 },
    ],
  },
})
// → { applies, reason, total_reduction, adjustments, requires_approval, ... }

const { evaluation: result, application } = await discounts.apply({
  discount_id: evaluation.discount_id,
  cart,
  order_id: "order_01",
  applied_by_employee_id: "emp_01",
})

evaluate checks scope (active, validity window, organization, store), runs the discount's eligibility_rules, then runs its strategy. apply persists a DiscountApplication row and increments coupon usage; it returns application: null when the discount does not apply or approval is required but approved_by_employee_id is missing.

Built-in strategies (strategy_type)

| Key | Behavior | | --- | --- | | percentage | Percentage off matching lines, optional max_reduction cap | | fixed | Flat amount, distributed proportionally across eligible lines | | bogo | Buy X get Y at Z% off (same-set or cross-set pools) | | tiered_percentage | Stepped percentage based on subtotal tier thresholds | | bundle | Specific items at a fixed combined price | | manual | Cashier/manager ad-hoc discount with reason; pairs with approval gating | | free_item | Emits a free_item adjustment for the cart layer to fulfil | | shipping_waiver | Waives shipping up to an optional max amount |

Built-in eligibility checks (eligibility_rules[].type)

min_cart_total, max_cart_total, min_cart_quantity, customer_groups, time_window, usage_limit_total, usage_limit_per_customer.

Extension points

Register a custom strategy or eligibility check at boot — the interfaces live under the /types subpath:

import type { DiscountStrategy } from "@devx-retailos/discount/types"

const loyaltyTierStrategy: DiscountStrategy<{ percent: number }> = {
  type: "loyalty_tier",
  description: "Extra percentage off for loyalty members",
  validateConfig(config) {
    return config as { percent: number } // typically a zod parse
  },
  async evaluate(config, context) {
    const amount = Math.floor(
      (context.cart.line_items[0].subtotal * config.percent) / 100
    )
    return {
      applies: amount > 0,
      reason: `${config.percent}% loyalty discount`,
      adjustments: [
        { type: "line_reduction", line_item_id: context.cart.line_items[0].id, amount },
      ],
    }
  },
}

discounts.registerStrategy(loyaltyTierStrategy)
discounts.registerEligibilityCheck(myCheck) // EligibilityCheck from the same subpath

EligibilityCheck has the same shape with check(config, context) returning { passes, reason? }. Built-ins are also exported individually (e.g. percentageStrategy, minCartTotalCheck) along with createDefaultStrategyRegistry() / createDefaultEligibilityRegistry().

Discount sources (externally-managed discounts)

Brands whose promotions live in an external platform (e.g. Shopify) can mirror them into this engine instead of reimplementing fetch logic. A DiscountSourceAdapter fetches discounts; they're normalized and mapped onto the same strategies + eligibility checks above, then stored as local Discount/Coupon rows — so evaluate()/apply() work identically for local and sourced discounts. The full source payload is retained verbatim on Discount.source_payload, so no fetched field is lost even when the engine doesn't model it.

The built-in shopify adapter fetches all Shopify discount kinds — code and automatic, across Basic (percentage/fixed), BXGY, Free Shipping, and App/function. Discounts the engine can't reproduce offline are still stored but flagged non-evaluable (source_evaluable = false): App/function discounts (computed by Shopify server-side), customer-targeted discounts, and BXGY variants bogo can't express. Automatic (code-less) discounts are stored as coupon-less rows (source_is_automatic = true). Configure a source per organization:

await discounts.createDiscountSources([
  {
    organization_id: "org_01",
    name: "Shopify promos",
    adapter_type: "shopify",
    config: {
      shop_domain: "your-shop.myshopify.com",
      admin_access_token: "shpat_...",
      // api_version defaults to "2025-01", default_currency to "INR"
    },
    auto_sync_enabled: true,
  },
])

Two fetch paths, both ending in the same local rows:

  • On-demand (lazy) — when a coupon code isn't in the local mirror, evaluate({ coupon_code, cart }) falls back to the source (fetchDiscountByCode), mirrors it, then evaluates. cart.organization_id selects which source(s) to try.
  • Scheduled / bulksyncDiscountSource({ source_id }) crawls all discounts (incrementally by updated_at) and soft-deletes ones the source dropped. The shipped retailos-discount-source-sync job runs this hourly for sources with auto_sync_enabled; opt a source out by setting it to false.

Register a custom source (e.g. a different platform) the same way as strategies:

import type { DiscountSourceAdapter } from "@devx-retailos/discount/types"

discounts.registerDiscountSourceAdapter(myAdapter) // implements fetchDiscounts + fetchDiscountByCode

The built-in adapter and registry helpers are exported from the /sources subpath (shopifyDiscountSource, createDefaultDiscountSourceRegistry). Mapping helpers mapNormalizedDiscount / scopeToAppliesTo are exported from the package root.

v1 limits. Collection-scoped discounts map best-effort onto the line-item categories filter (a warning is logged). Usage counts refresh on fetch/sync, not on every checkout. Automatic discounts are stored but not auto-applied yet. Source webhooks are not yet consumed. The Shopify GraphQL field selection is pinned to the configured Admin API version — validate it against your store before relying on it in production.

Permissions

Registered via DISCOUNT_PERMISSIONS (subpath @devx-retailos/discount/permissions):

  • discount.read — view discounts
  • discount.create — create discounts
  • discount.update — edit discounts
  • discount.delete — deactivate discounts
  • discount.apply — apply a discount to a cart or order
  • discount.override — approve a manual or above-threshold discount
  • coupon.read — view coupons
  • coupon.create — issue coupon codes
  • coupon.redeem — redeem a coupon code against a cart
  • coupon.delete — deactivate coupons
  • discount_source.read — view external discount sources
  • discount_source.create — configure an external discount source
  • discount_source.sync — trigger a sync from an external source

API routes

Authenticated admin routes shipped by the plugin:

| Method | Path | Description | | --- | --- | --- | | GET | /admin/retailos/discounts | List discounts | | POST | /admin/retailos/discounts | Create a discount (validates strategy_type) | | GET | /admin/retailos/discounts/:id | Retrieve a discount | | POST | /admin/retailos/discounts/:id | Update a discount | | DELETE | /admin/retailos/discounts/:id | Deactivate a discount (is_active: false) | | POST | /admin/retailos/discounts/evaluate | Dry-run a discount against a cart | | POST | /admin/retailos/discounts/apply | Evaluate and record an application | | GET | /admin/retailos/discounts/strategies | List registered strategies and eligibility checks | | GET | /admin/retailos/coupons | List coupons | | POST | /admin/retailos/coupons | Create a coupon for a discount | | GET | /admin/retailos/coupons/:id | Retrieve a coupon | | DELETE | /admin/retailos/coupons/:id | Deactivate a coupon | | GET | /admin/retailos/discount-sources | List external discount sources | | POST | /admin/retailos/discount-sources | Configure a discount source (validates adapter_type) | | POST | /admin/retailos/discount-sources/:id/sync | Trigger a bulk sync from the source |

Related packages

  • @devx-retailos/core — shared types, Logger, RetailOSError, permission registry
  • @devx-retailos/rbac — organizations, stores, roles, permission enforcement
  • @devx-retailos/order — POS orders that consume discount applications
  • @devx-retailos/payments — pluggable payment adapters for tendering
  • @devx-retailos/gift-voucher — gift voucher issuance and redemption-as-tender
  • @devx-retailos/sdk-client — typed frontend client and React hooks

License

MIT