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.7.0

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

Peer deps (exactly what package.json declares):

| Peer | Range | |---|---| | @classytic/mongokit | >=3.16.0 | | @classytic/primitives | >=0.5.0 | | @classytic/repo-core | >=0.6.0 | | mongoose | >=9.4.1 |

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, ResolveOptions, PricingBridge, PricelistEngine, PricelistEngineConfig, PricelistTenantOptions), document types (PriceListDocument, PriceRuleDocument, TierLadderDocument, ConditionDocument), domain errors (PricelistError, DuplicateDefaultPriceListError, PriceAmountPrecisionError, PriceListModelCollisionError — all implement HttpError from @classytic/repo-core/errors), 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.

Determinism — the rule comparator is a total order: SCOPE_RANK ASC → minQuantity DESC → priority ASC → rules-array position ASC. Two rules identical on the first three keys resolve by their position in rules — documented behavior, never an accident of sort internals. Date windows (validFrom / validTo) are millisecond-precision and inclusive on BOTH ends; overlapping windows on the same key are allowed and resolved by the same comparator.

Money — every monetary value is an INTEGER in minor units (paisa/cents): basePrice, costPrice, fixedPrice, formulaSurcharge, margins, and the resolved price. Float basePrice / costPrice throw PriceAmountPrecisionError (422); float rule amounts are rejected by schema validators on write. Percent fields (percentDiscount, formulaDiscount) may be fractional. Each rule computation rounds once (half-up) at its output, so stacked rules always feed integers forward.


Repositories

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

| Verb | Signature | |---|---| | resolvePrice | (priceListId, input: ResolvePriceInput, options?: ResolveOptions) => Promise<ResolvedPriceResult \| null> |

ResolveOptions carries organizationId, maxRecursionDepth, plus the repo-core 0.6 cancellation/retry contract — signal (checked at every op boundary, including recursive other_pricelist reads), retryPolicy, and session (snapshot-consistent reads inside a host transaction).

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

create / update translate the residual E11000 from the partial unique default-pricelist index (organizationId, channel, isDefault: true) into DuplicateDefaultPriceListError (409) — the unique index is the authoritative race-proof guard (N concurrent default creates → exactly one winner); callers handle one typed error shape.

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 on the repository automatically, driven by one resolveTenantConfig() result that also generates the schema field — plugin and schema can't drift. 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 to disable plugin-level scoping entirely (the plugin is then not wired at all).

Mongokit 3.16 fail-closed defaults stay in charge: onMismatch: 'throw' (caller-supplied cross-tenant values are rejected, never rewritten) and allowDataInjection: false (tenant scope must come from the call context). Host-set knobs (contextKey, required, skipWhen, resolveContext, allowDataInjection, onMismatch, skipOperations) are forwarded to the plugin unchanged.

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

Boot, indexes, collisions

  • autoIndex: false for production boots; run await engine.syncIndexes() from a deploy script (rule 35).
  • A PriceList model already registered on the connection throws PriceListModelCollisionError; forceRecreate: true opts into delete + re-register (hot-reload / test fixtures). Two engines in one process → two Mongoose connections.
  • collectionPrefix re-namespaces the physical pricelists collection (e.g. commerce_pricelists) without touching model code.

Events

None yet — pricelist emits nothing today, so no transport/outbox machinery ships. When mutations start emitting, the org P8 dispatch contract and Zod event catalog apply.


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 — 112 tests total:

| Project | Files | Tests | |---|---|---| | unit | tests/unit/rule-resolver.test.ts | 26 | | integration | resolution, edge-cases, complex-rule-chains, tiered-and-conditions, tenant-and-engine-contract | 75 | | 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.