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

@classytic/pricelist

v0.1.1

Published

Universal pricing primitives — pricelists, rules, and resolution engine for MongoDB

Readme

@classytic/pricelist

Universal pricing primitives — pricelists, rules, and resolution engine for MongoDB.

Install

npm install @classytic/pricelist \
  @classytic/mongokit @classytic/primitives @classytic/repo-core \
  mongoose zod

Peer deps (exactly what package.json declares):

| Peer | Range | |---|---| | @classytic/mongokit | >=3.11.0 | | @classytic/primitives | >=0.1.0 | | @classytic/repo-core | >=0.2.0 | | mongoose | >=9.4.1 | | zod | >=4.0.0 |

Node >=22. ESM only.

Exports

| Subpath | Contents | |---|---| | @classytic/pricelist | createPricelistEngine, PriceListRepository, createPriceListModel, createPricelistBridge, pure resolver (findApplicableRules, computeRulePrice), all domain types (PriceList, PriceRule, TierLadder, Condition, ResolvePriceInput, ResolvedPriceResult, RuleMatch, ResolveContext, PricingBridge, PricelistEngine, PricelistEngineConfig), document types (PriceListDocument, PriceRuleDocument, TierLadderDocument, ConditionDocument), and every enum value | | @classytic/pricelist/enums | Enum-only entry: RuleScope, SCOPE_RANK, ComputationType, BasePriceType, CombineStrategy, ConditionField, ConditionOp — no mongoose, no engine |


Quick start

import mongoose from 'mongoose';
import { createPricelistEngine } from '@classytic/pricelist';

// 1. Boot the engine against an existing mongoose connection.
const engine = createPricelistEngine({
  connection: mongoose.connection,
  // tenant: { fieldType: 'objectId' }   // default — use 'string' for UUID hosts
});

// 2. Create a pricelist with embedded rules.
const list = await engine.repositories.priceList.create({
  organizationId: '64b0c0c0c0c0c0c0c0c0c0c0',
  name: 'Wholesale',
  currency: 'BDT',
  isDefault: false,
  isActive: true,
  combineStrategy: 'first-match',
  rules: [
    {
      scope: 'global',
      minQuantity: 10,
      base: 'list_price',
      computation: 'percentage',
      percentDiscount: 15,
      priority: 10,
    },
  ],
});

// 3. Resolve a price.
const result = await engine.repositories.priceList.resolvePrice(
  String(list._id),
  { productId: 'prod_1', quantity: 20, basePrice: 10000 },
  { organizationId: '64b0c0c0c0c0c0c0c0c0c0c0' },
);

// result → { price: 8500, ruleMatched: true, computation: 'percentage',
//            ruleIndex: 0, appliedRuleIndices: [0], combineStrategy: 'first-match' }

Everything else on the repository is inherited from mongokit Repository<PriceListDocument>: getById, getByQuery, getAll, findAll, create, update, delete, restore, count, aggregate. Use those directly.


Concepts

PriceList — A named, currency-scoped container of rules. Owns rules: PriceRule[] as an embedded array plus a combineStrategy that decides how multiple applicable rules fold into a final price. organizationId scopes every pricelist to one tenant / branch.

PriceRule — One pricing decision inside a pricelist. Declares a scope (what it targets), an optional scopeRef, a base (list_price | cost_price | other_pricelist), a computation (fixed | percentage | formula), a minQuantity gate, optional date validity (validFrom / validTo), optional quantity tiers, optional categorical conditions, and a priority tie-break.

TierLadder — An optional [minQty, maxQty ?? ∞] bracket inside a rule. Each tier carries its own computation block; the matching tier overrides the rule-level computation. A tiered rule that has no tier matching the input quantity is excluded entirely.

Condition — An optional AND-combined filter on customerGroup, customerTag, or channel using operator eq | in | not_in. A rule with no conditions is unfiltered.

Resolution pipelineresolvePrice() walks rules through a pure resolver (findApplicableRules) that filters by date validity, scope match, minQuantity, tier range, and conditions, then sorts by SCOPE_RANK ASC → minQuantity DESC → priority ASC. The repository then resolves each rule's base price (including recursive other_pricelist chains with cycle detection, depth-capped at 5) and applies the pricelist's combineStrategy.

Combine strategiesfirst-match (default, Odoo-like: first applicable wins), best-price (evaluate all, return the lowest unit price), stack (apply sequentially in sort order; each rule's output feeds the next as base, unless the next rule uses cost_price or other_pricelist).

Scope specificityvariant: 0product: 1customer_group: 1.5category: 2global: 3. Lower rank wins. customer_group sits between product and category so a product-scoped wholesale rule still beats a global wholesale rule, while a customer-group rule beats a category rule at the same priority.


Repositories

PriceListRepository extends mongokit's Repository<PriceListDocument> directly — no service wrapper layer. The only domain verb is:

| Verb | Signature | |---|---| | resolvePrice | (priceListId, input: ResolvePriceInput, options?: { organizationId?, maxRecursionDepth?, _visited? }) => Promise<ResolvedPriceResult \| null> |

null is returned only when the pricelist is missing or inactive. When no rule matches, the result is { ruleMatched: false, price: basePrice, combineStrategy }.

The pure resolver (findApplicableRules, computeRulePrice) is exported separately for unit testing without Mongo.


Catalog bridge

createPricelistBridge(repo) returns an adapter that matches @classytic/catalog's PricingBridge port (structural, no direct cross-package import). It reads the customer's first customerGroups[0] as the target priceListId and forwards product / variant / category / quantity / base and cost prices to resolvePrice().

import { createPricelistBridge } from '@classytic/pricelist';

const pricingBridge = createPricelistBridge(engine.repositories.priceList);
// pass into catalog (or any host expecting the PricingBridge shape)

Multi-tenant scoping

The engine wires multiTenantPlugin({ tenantField: 'organizationId' }) on the repository automatically. organizationId is stored as Schema.Types.ObjectId with ref: 'organization' by default so $lookup / .populate() work. Pass tenant: { fieldType: 'string' } for UUID / slug-based auth systems, or tenant: false when your framework already enforces scoping.

createPricelistEngine({ connection, tenant: { fieldType: 'string' } });
createPricelistEngine({ connection, tenant: false });

Design principles

  • No barrels inside src/. Only src/index.ts re-exports — internal folders import directly from source files.
  • No cross-package imports. Siblings (catalog, cart, order, etc.) integrate via structural bridge shapes — never import from '@classytic/catalog'.
  • Peer-dep siblings only. @classytic/mongokit, @classytic/primitives, @classytic/repo-core are peers, not dependencies.
  • Zod v4 at the seams. Host-side schemas compose against Zod v4.
  • Pure resolver + thin repository. Rule-matching logic is pure and unit-tested without Mongo; the repository adds DB-bound base resolution and combine-strategy application.
  • Extend mongokit Repository<T> directly. No service layer.

Tests

npm test              # unit + integration + scenarios
npm run test:unit
npm run test:integration
npm run test:scenarios

Current suite — 94 tests total:

| Project | Files | Tests | |---|---|---| | unit | tests/unit/rule-resolver.test.ts | 25 | | integration | resolution, edge-cases, complex-rule-chains, tiered-and-conditions | 58 | | scenarios | rule-layering-at-scale, midnight-date-transitions, b2b-contract-lock-bulk | 11 |

Integration + scenario tests spin up mongodb-memory-server; unit tests hit only the pure resolver and require no Mongo.


Changelog

See CHANGELOG.md.

License

MIT. See LICENSE.