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

v0.4.0

Published

Framework-agnostic universal order engine — durable order records, fulfillment lifecycle, returns/exchanges, saga-based creation pipeline, bridge ports for payment/inventory/tax/loyalty. Powered by MongoDB via @classytic/mongokit.

Readme

@classytic/order

Framework-agnostic order engine on top of @classytic/mongokit + Mongoose. Durable orders, fulfillment FSMs, returns/exchanges, 9-step creation pipeline, bridge ports for every host integration.

Install

npm install @classytic/order \
  @classytic/mongokit @classytic/primitives \
  mongoose zod

Peer deps (exactly what package.json declares):

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

Node >=22. ESM only.

Exports

| Subpath | Contents | |---|---| | @classytic/order | Engine (createOrder), repositories, domain entities, enums, bridge port types, state machines, errors, event catalog, saga primitives | | @classytic/order/schemas/order | Zod bodies: createOrderBody, transitionBody, updatePaymentStateBody, etc. | | @classytic/order/schemas/fulfillment | Zod bodies: createFulfillmentBody, fulfillmentTransitionBody, etc. | | @classytic/order/schemas/order-change | Zod bodies: requestChangeBody, confirmChangeBody, etc. | | @classytic/order/schemas/common | moneySchema, addressSchema, orderNumberParam, etc. |

Per-schema subpaths are independent entry points — importing only schemas does not pull mongoose, mongokit, or the engine factory into the bundle.


60-second integration

import mongoose from 'mongoose';
import { createOrder, repoOptionsFromCtx, type OrderContext } from '@classytic/order';

// 1. Boot the engine (owns its mongoose models on your connection).
const engine = await createOrder({
  connection: mongoose.connection,
  defaultCurrency: 'BDT',
  bridges: { catalog: myCatalogBridge },   // only catalog is required
  // multiTenant: false                    // if your framework handles tenant scoping
});
await engine.syncIndexes();

// 2. Build your request context (one helper per HTTP request).
const ctx: OrderContext = {
  organizationId: req.headers['x-organization-id'] as string,
  actorRef: req.user?.id ?? 'anonymous',
  actorKind: 'user',
  correlationId: req.id,
};

// 3. Place an order.
const order = await engine.repositories.order.create(
  {
    channel: 'web',
    orderType: 'standard',
    lines: [{ kind: 'sku', offerId: 'sku-123', quantity: 2 }],
    customer: { email: '[email protected]', name: 'Jane' },
    idempotencyKey: req.id,
  } as Record<string, unknown>,
  repoOptionsFromCtx(ctx),
);

// 4. Transition via FSM.
await engine.repositories.order.transition(order.orderNumber, 'confirmed', ctx);

// 5. Create a fulfillment, add tracking.
const ful = await engine.repositories.fulfillment.createForOrder(
  { orderNumber: order.orderNumber, fulfillmentType: 'physical', lines: [{ orderLineId: 'line_0', quantity: 1 }] },
  ctx,
);
await engine.repositories.fulfillment.addTracking(
  ful.fulfillmentNumber,
  { carrier: 'redx', trackingNumber: 'RDX-123' },
  ctx,
);

Anything not listed above is plain mongokit Repository<TDoc>: getById, getByQuery, getAll, create, update, delete, restore, count, aggregate, findAll. Use those directly.


createOrder config

| Field | Required | Notes | |---|---|---| | connection | yes | Mongoose connection (existing) | | bridges.catalog | yes | Product resolver — see Bridges below | | defaultCurrency | | ISO 4217, default USD | | multiTenant | | true (default), false, or MultiTenantOptions. Set false if your framework handles tenant scoping already | | tenantFieldType | | 'objectId' (default) or 'string' — type of organizationId on every model | | eventTransport | | Any Arc-compatible EventTransport. Default: in-process bus | | idPrefixes | | { order, fulfillment, orderChange, quotation, blanketOrder } — defaults ORD / FUL / CHG / QUO / BLK | | idPartition | | yearly (default), monthly, daily | | collectionPrefix | | Prepended to every physical collection name (e.g. 'commerce_') | | autoIndex | | false for prod Atlas, true (default) for dev, or per-model override | | orderTypes | | Extra OrderTypeHandler[] (merged with 6 built-ins) | | fulfillmentHandlers | | Extra FulfillmentHandler[] (merged with 6 built-ins) | | repositoryPlugins | | Per-repo extra mongokit plugins | | modules | | Opt-in feature modules — see Modules below | | logger | | Non-fatal error logger. Default console |


Engine surface

engine.config              // frozen OrderConfig
engine.bridges             // frozen OrderBridges (what you passed in)
engine.events              // EventTransport — subscribe / publish
engine.models              // Order, Fulfillment, OrderChange, OrderEvent (+ Quotation, BlanketOrder if enabled)
engine.repositories        // order, fulfillment, orderChange, orderEvent (+ quotation, blanketOrder if enabled)
engine.modules             // resolved Required<OrderModules> flags
engine.orderTypes          // OrderTypeRegistry
engine.fulfillmentHandlers // FulfillmentHandlerRegistry
engine.fsm                 // FsmRegistry (order FSM + 6 fulfillment FSMs + change FSM)
engine.defaultCurrency     // string
engine.syncIndexes()       // create/update all indexes
engine.destroy()           // graceful shutdown

Repository domain verbs

There is no service layer. All business logic lives on repositories that extend mongokit's Repository<TDoc> directly.

| Repository | Domain verbs (beyond mongokit base CRUD) | |---|---| | order | create (pipeline-aware), transition, cancel, updatePaymentState, confirmPayment, addNote, addHold, resolveHold | | fulfillment | createForOrder, transition, addTracking | | orderChange | requestChange, confirm, decline | | orderEvent | append (implements OrderEventSink) |

Opt-in modules add:

| Repository | Domain verbs | |---|---| | quotation (when modules.quotation) | create, send, markViewed, accept, reject, expire, expireDue, convertToOrder | | blanketOrder (when modules.blanket) | create, pause, resume, cancel, generateOne, generateDue, expireDue |


The 9-step creation pipeline

OrderRepository.create() runs a single saga with a payment pivot. Pre-pivot steps compensate in LIFO on failure; post-pivot steps must succeed (retry, never compensate back).

PRE-PIVOT:   1. validate          2. snapshot-lines   3. compute-totals   4. reserve-inventory
PIVOT:       5. authorize-payment
POST-PIVOT:  6. create-record     7. post-create-hooks 8. earn-loyalty    9. notify

PricingBridge.resolveLine() runs inside step 2/3, CreditPort.checkCredit() runs after totals and before step 4; credit failures seed an on_hold order with a credit_check_failed hold instead of failing the saga.


Collections & custom IDs

| Collection | Custom ID (via customIdPlugin) | |---|---| | orders | orderNumberORD-2026-0001 | | order_fulfillments | fulfillmentNumberFUL-2026-0001 | | order_changes | changeNumberCHG-2026-0001 | | order_events | _id only (append-only audit log) | | order_idempotency | internal saga state | | quotations (opt-in) | quotationNumberQUO-2026-0001 | | blanket_orders (opt-in) | blanketNumberBLK-2026-0001 |

All records are soft-deletable (softDeletePlugin({ ttlDays: 2555 }) — 7-year retention). Unique indexes are partial on deletedAt: null so soft-deleted rows never collide with new ones. GDPR hard-delete: repo.delete(id, { mode: 'hard' }).


Order types (6 built-in)

| Code | FSM | Default fulfillment | |---|---|---| | standard | Order FSM | physical | | subscription | Order FSM | subscription | | booking | Order FSM + no_show | booking | | enrollment | Order FSM | digital | | auction | Order FSM | physical | | trade | Order FSM | physical |

Fulfillment handlers (6 built-in)

| Code | States | |---|---| | physical | pending → picking → packed → shipped → in_transit → delivered | | digital | pending → granted | | booking | pending → checked_in → in_progress → completed | no_show | | subscription | pending → active → renewing → active | expired | | service | pending → assigned → in_progress → completed | | food_delivery | pending → accepted → preparing → ready → dispatched → delivered |

All FSMs also allow canceled from non-terminal states. Register extras via createOrder({ orderTypes, fulfillmentHandlers }).


Opt-in modules

Every module defaults to false so lean hosts (simple ecom, SaaS, course) pay zero surface-area cost for enterprise features they don't use.

const engine = await createOrder({
  connection: mongoose.connection,
  bridges: { catalog },
  modules: { quotation: true, blanket: true },
});

| Module | Adds | |---|---| | quotation | Quotation entity, quotations collection, QuotationRepository, order:quotation.* events, draft → sent → viewed → accepted \| rejected \| expired → converted FSM, convertToOrder() that runs the full pipeline | | blanket | BlanketOrder entity, blanket_orders collection, BlanketOrderRepository, order:blanket.* events, cadence-driven generateDue(now, ctx) that creates a fresh Order per due cycle |

Resolved flags are exposed on engine.modules so host code can conditionally wire behavior.


Multi-tenant scoping

By default createOrder auto-wires multiTenantPlugin({ tenantField: 'organizationId' }) on every repository. The plugin reads organizationId from the spread RepoOptions on every read/write.

await repo.getByQuery({ orderNumber }, repoOptionsFromCtx(ctx));
await repo.getAll({ filters: { status: 'pending' }, ...repoOptionsFromCtx(ctx) });

Set multiTenant: false when your HTTP framework (Arc, etc.) enforces tenant scoping at its own middleware layer. Repositories still write organizationId onto the doc explicitly, so either wiring works.

tenantFieldType: 'objectId' (default) uses Schema.Types.ObjectId + ref: 'organization' so $lookup, .populate(), and QueryParser lookups work. Use 'string' for UUID / slug-based auth systems.


Bridge ports (host integrations)

Only catalog is required. Every other bridge degrades gracefully if unwired. No bridge is imported from another @classytic/* package — wiring is structural.

| Bridge | Required | Purpose | |---|---|---| | catalog | yes | resolveSnapshot(offerId, qty, selections, ctx) + capacity commit/release | | revenue | | Payment lifecycle (authorize / capture / refund / escrow) | | flow | | Inventory reservation + warehouse routing | | pricing | | Per-line price resolution (pricelists, tiered / B2B) | | credit | | Customer credit-worthiness check before persist | | tax | | Tax calculation | | loyalty | | Points earn / redeem | | fulfillmentProvider | | Carrier labels + tracking | | notification | | Email / SMS / push | | booking | | Per-slot availability for the booking order type (default Mongo-backed impl auto-wires against the order collection) |

import type { OrderCatalogBridge, RevenueBridge } from '@classytic/order';

const catalog: OrderCatalogBridge = {
  async resolveSnapshot(offerId, quantity, selections, ctx) { /* return LineSnapshot | null */ },
  async commitCapacity() { return { status: 'committed' }; },
  async releaseCapacity() { /* no-op if offers module off */ },
};

const engine = await createOrder({
  connection: mongoose.connection,
  bridges: { catalog /* , revenue, flow, pricing, credit, tax, loyalty, ... */ },
});

All bridge port types are exported from the root entry.


Events

Events use the package: prefix — order:*, never order.* — and are structurally compatible with @classytic/arc's DomainEvent:

{ type: 'order:confirmed', payload, meta: { id, timestamp, resource, resourceId, organizationId, correlationId } }

Full string list in the ORDER_EVENTS const. Representative events: order:created, order:confirmed, order:fulfilled, order:canceled, order:payment.authorized, order:payment.captured, order:payment.refunded, order:payment.state_updated, order:fulfillment.created, order:fulfillment.transition, order:change.requested, order:change.confirmed, order:hold.added, order:hold.resolved, order:credit.checked, order:credit.blocked, order:price_resolved. Opt-in modules add order:quotation.* and order:blanket.*.

import { ORDER_EVENTS } from '@classytic/order';

await engine.events.subscribe?.(ORDER_EVENTS.ORDER_CONFIRMED, handler);
await engine.events.subscribe?.('order:*', handler);               // all order events
await engine.events.subscribe?.('order:fulfillment.*', handler);   // fulfillment only
await engine.events.subscribe?.('order:payment.*', handler);       // payment only

Drop in any Arc transport (Memory / Redis / Kafka / outbox) via createOrder({ eventTransport }). Default is InProcessOrderBus — fire-and-forget, non-durable, good for single-node. Each event also ships a Zod schema (coreOrderEventDefinitions / orderEventDefinitions) so hosts can register them with arc's EventRegistry directly.


Arc integration (optional)

Order repositories are Arc.RepositoryLike directly. Wire defineResource with createAdapter(engine.models.X, engine.repositories.x):

import { defineResource } from '@classytic/arc';
import { createAdapter } from './shared/adapter.js';
import { orgScoped } from './shared/presets.js';

const engine = await ensureOrderEngine();
const adapter = createAdapter(engine.models.Order, engine.repositories.order);

export default defineResource({
  name: 'order',
  prefix: '/orders',
  adapter,
  presets: [orgScoped],   // if you opted out of multiTenant on the engine
  routes: [
    { method: 'POST', path: '/place', raw: true, handler: placeHandler },
    { method: 'POST', path: '/:id/action', raw: true, handler: actionHandler },
  ],
});

Use repoOptionsFromCtx(ctx) inside handlers to forward request context to repo calls.


Design rules

  • No barrel files inside src/. Only src/index.ts re-exports. Internal folders import directly from source files.
  • No service wrapper layer. Repositories extend mongokit's Repository<T> directly. Domain logic lives on the repo.
  • No cross-package imports. Siblings (catalog, revenue, flow, tax, loyalty, pricing, credit, fulfillmentProvider, notification, booking) are wired via bridge ports — never imported from @classytic/*.
  • Zod v4. Use z.record(z.string(), z.unknown()) — never z.record(z.unknown()).

See CLAUDE.md for the full agent / contributor rules.


Testing

npm test   # integration tests via mongodb-memory-server

See tests/helpers/setup.ts for the per-file connection pattern if you want to copy it for your host tests.


What this package is NOT

  • Not a service layer. Repositories are the API surface. If you catch yourself writing an OrderService that forwards to the repo, delete it.
  • Not HTTP-aware. No route wiring, no controllers. Arc, Express, Nest, or any framework can consume it.
  • Not coupled to other @classytic/* packages. Integration is through bridge contracts, never direct imports.
  • Not a replacement for cart. Cart → checkout handoff calls repositories.order.create(...). Cart logic lives elsewhere.

License

MIT. See LICENSE.

Changelog

See CHANGELOG.md.