Package Exports
- @classytic/flow
- @classytic/flow/counting
- @classytic/flow/domain
- @classytic/flow/domain/contracts
- @classytic/flow/domain/enums
- @classytic/flow/domain/policies
- @classytic/flow/events
- @classytic/flow/models
- @classytic/flow/packaging
- @classytic/flow/procurement
- @classytic/flow/reporting
- @classytic/flow/repositories
- @classytic/flow/reservations
- @classytic/flow/routing
- @classytic/flow/scanning
- @classytic/flow/services
- @classytic/flow/traceability
- @classytic/flow/types
- @classytic/flow/valuation
Readme
@classytic/flow
Production-grade inventory kernel and supply chain engine for MongoDB.
Double-entry inventory, location-based stock tracking, reservations, FIFO/FEFO/WAC valuation, lot/serial traceability, putaway/removal routing, wave picking with serpentine path optimization, cross-dock multi-destination routing, procurement, cycle counting, and replenishment — all as pure domain services with zero framework coupling.
Quick Start
import { createFlowEngine } from '@classytic/flow';
import mongoose from 'mongoose';
// 1. Create the engine
const flow = createFlowEngine({
mongoose: mongoose.connection,
mode: 'standard',
catalog: {
resolveSku: async (skuRef) => productService.findBySku(skuRef),
},
});
// 2. Access models
const { InventoryNode, Location, StockMoveGroup, StockMove, StockQuant, Reservation } = flow.models;
// 3. Context carries tenant + actor — never mixed into domain inputs
const ctx = { organizationId: 'org_1', actorId: 'user_1' };
// 4. Use services (pure async functions — no HTTP, no framework)
const availability = await flow.services.quant.getAvailability({
skuRef: 'SKU-RED-M',
nodeId: 'warehouse_1',
}, ctx);
// 5. Create and execute a transfer
const group = await flow.services.moveGroup.create({
groupType: 'transfer',
sourceNodeId: 'warehouse_1',
destinationNodeId: 'store_1',
items: [{
moveGroupId: '', // auto-assigned
operationType: 'transfer',
skuRef: 'SKU-RED-M',
sourceLocationId: 'loc_storage_wh1',
destinationLocationId: 'loc_storage_store1',
quantityPlanned: 50,
}],
}, ctx);
await flow.services.moveGroup.executeAction(group._id, 'confirm', {}, ctx);
await flow.services.moveGroup.executeAction(group._id, 'dispatch', {}, ctx);
await flow.services.moveGroup.executeAction(group._id, 'receive', {}, ctx);
// 6. Subscribe to events (bridge to your ledger, notifications, etc.)
flow.events.on('inventory.move.done', async (payload) => {
// payload: { organizationId, moveId, skuRef, quantityDone, ... }
await ledger.postInventoryMovement(payload);
});
// 7. Query active config at runtime
flow.services.mode; // 'standard'
flow.services.routing; // { putaway: false, removal: false, crossDock: false }
flow.services.valuation; // { method: 'wac' }Simple Mode (single store, basic stock)
Flow scales down to simple apps. You don't need locations, routing, or lots — just stock queries and moves:
const ctx = { organizationId: 'org_1', actorId: 'user_1' };
// Check availability
const stock = await flow.services.quant.getAvailability({ skuRef: 'SKU-RED-M' }, ctx);
// → { quantityOnHand: 50, quantityReserved: 5, quantityAvailable: 45, quantityIncoming: 20, quantityOutgoing: 0 }
// Reserve stock for an order
await flow.services.reservation.reserve({
reservationType: 'hard',
skuRef: 'SKU-RED-M',
locationId: 'loc_default',
quantity: 3,
ownerType: 'order',
ownerId: 'order_123',
}, ctx);Features
| Feature | Description |
|---|---|
| Double-entry inventory | Every stock change is a move between locations — never direct mutation |
| Location hierarchy | Warehouse > zone > aisle > bin. Virtual locations for vendor, customer, scrap, transit |
| Stock quants | Materialized read model of on-hand, reserved, available, incoming, outgoing |
| Reservations | First-class records with expiry, partial consumption, overconsumption guard, and pluggable allocation |
| Valuation | WAC (default), FIFO, FEFO, specific identification, standard cost, landed cost with penny-leak prevention |
| Lot/serial tracking | Full traceability with expiry, recall support, and FEFO allocation |
| Routing engine | Putaway strategies, removal strategies, route expansion |
| Wave picking | Serpentine (boustrophedon) path optimization with 3D coordinates, weight/volume/item cart batching with oversized pick splitting |
| Cross-dock routing | Multi-destination assignments — incoming stock auto-routed to waiting outbound orders in FIFO order |
| Procurement | Purchase order lifecycle, receiving, vendor management |
| Cycle counting | Full/cycle/blind counts with auto-reconciliation |
| Replenishment | Min/max, reorder point, safety stock, EOQ, multi-echelon |
| Scan resolution + GS1-128 | Device-agnostic barcode/QR/RFID → entity resolution. Built-in GS1-128 parser for GTIN, lot, expiry, serial. |
| Stock states | Sellable, damaged, quarantine, hold, returns, expired, in-transit |
| UoM conversion | Each/case/carton/pallet with per-SKU conversion factors |
| Packaging | StockPackage with parent/child nesting (carton → case → pallet), weight calculation |
| Stock ownership | ownerRef on quants — owned vs consignment/third-party in same location |
| QC workflow | Quality hold location + inspection route step |
| Multi-tenant | Organization-scoped via MongoKit multi-tenant plugin |
| Events | In-process emitter — bridge to Redis, webhooks, ledger, notifications |
| Capability tiers | simple / standard / enterprise — config-driven feature gating |
Safety & Data Integrity
Flow enforces invariants at the engine level so consumers don't have to:
| Guard | Behavior |
|---|---|
| Atomic quant updates | quantityAvailable = quantityOnHand - quantityReserved computed in a single MongoDB aggregation pipeline — no race window |
| Negative stock prevention | postMove() checks location.allowNegativeStock before decrementing; throws NegativeStockError if insufficient. Virtual sources (returns, receipts) auto-bypass. |
| Canonical posting path | receiveGroup() delegates to PostingService.postMove() per move — negative stock guard, move transition validation, and inventory.move.done events fire for every move |
| Reservation lifecycle | expire() and consume() both release quantityReserved from the quant. consume() validates quantity > 0 and caps at remaining — no overconsumption. Fulfillment releases all reservations. |
| Input validation | quantityDone > 0, quantity > 0, non-empty items[] enforced at service entry |
| Tenant-scoped idempotency | All idempotency keys prefixed with organizationId: (reservation, move, procurement) — no cross-tenant collision |
| WAC precision | Weighted average cost rounded to 4 decimal places; freezes at last known cost when stock goes negative |
| Penny-leak prevention | Landed cost post-capitalization uses remainder absorption — COGS + remaining = exactly the allocated cost per item, with cross-item correction on the last item |
| Concurrent allocation safety | Parallel availability check + batchUpsert for quant reservation locks within a single transaction |
| Event error isolation | Handler errors logged but never propagate to emitter |
| GS1 year rollover | Dynamic sliding window (±50 years from current year) instead of hardcoded cutoff |
| FIFO/FEFO zero-qty guard | Valuation engines return empty result for quantity <= 0 |
| Partially done moves | receiveGroup() reconciles partially_done moves — completes remaining quantity instead of skipping them |
Subpath Exports
| Import | Contents |
|---|---|
@classytic/flow |
createFlowEngine() factory + full API |
@classytic/flow/domain |
Entities, value objects, errors |
@classytic/flow/domain/contracts |
CatalogBridge, AuthContext interfaces |
@classytic/flow/domain/enums |
LocationType, MoveStatus, OperationType, etc. |
@classytic/flow/domain/policies |
AllocationPolicy, RemovalPolicy, ValuationPolicy |
@classytic/flow/models |
Mongoose schemas |
@classytic/flow/repositories |
Repository factories (MongoKit) |
@classytic/flow/services |
All domain services |
@classytic/flow/valuation |
Cost layers, WAC/FIFO/FEFO engine, landed cost |
@classytic/flow/routing |
Putaway, removal, wave engine, cross-dock engine |
@classytic/flow/reservations |
Allocation strategies and reservation service |
@classytic/flow/procurement |
Procurement order lifecycle |
@classytic/flow/counting |
Inventory count and reconciliation |
@classytic/flow/traceability |
Lot/serial trace queries |
@classytic/flow/scanning |
Scan resolution + GS1-128 parser |
@classytic/flow/packaging |
StockPackage service |
@classytic/flow/reporting |
Read models, metrics, stock aging |
@classytic/flow/events |
Event definitions and emitter |
@classytic/flow/types |
All TypeScript types |
Configuration
const flow = createFlowEngine({
// Required
mongoose: connection,
// Required — how Flow resolves product details
catalog: {
resolveSku: async (skuRef) => ({ sku: skuRef, displayName: '...', trackingMode: 'none', uom: 'unit' }),
},
// Optional — deployment mode (default: 'standard')
mode: 'standard',
// Optional — multi-tenant (omit for single-tenant)
multiTenant: { orgField: 'organizationId' },
// Optional — valuation method (default: 'wac')
valuation: { method: 'wac' },
// Optional — allocation policy (default: 'fifo') — resolves to built-in FifoStrategy/FefoStrategy/LifoStrategy
allocation: { defaultPolicy: 'fifo' },
// Optional — routing (enterprise mode)
routing: { putaway: true, removal: true, crossDock: false },
// Optional — custom allocation policy (overrides allocation.defaultPolicy)
policies: { allocation: myCustomPolicy },
// Optional — custom event emitter (default: in-process)
events: { adapter: myRedisEventBus },
// Optional — idempotency store
idempotency: myRedisIdempotency,
// Optional — MongoKit plugins per repository
plugins: {
quant: [cachePlugin({ adapter: redis, ttl: 30 })],
move: [auditTrailPlugin()],
},
});| Option | Type | Default | Description |
|---|---|---|---|
mongoose |
Connection |
required | Mongoose connection |
catalog |
CatalogBridge |
required | Product/SKU resolution |
mode |
FlowMode |
'standard' |
simple, standard, or enterprise |
multiTenant |
object |
undefined |
{ orgField } for org-scoped isolation |
valuation.method |
string |
'wac' |
wac, fifo, fefo, specific, standard |
allocation.defaultPolicy |
string |
'fifo' |
fifo, fefo, lifo — maps to built-in strategy |
routing.putaway |
boolean |
false |
Enable putaway strategy engine |
routing.removal |
boolean |
false |
Enable removal strategy engine |
routing.crossDock |
boolean |
false |
Enable cross-dock multi-destination routing |
What Flow Returns
const flow = createFlowEngine(config);
flow.models // All Mongoose models
flow.repositories // All MongoKit repositories
flow.services // All domain services + active config (mode, routing, valuation)
flow.events // In-process event emitter
flow.registerPolicy // Register custom allocation policies at runtimeWhat Flow Does NOT Include
Flow is a pure domain kernel. These are consumer responsibilities:
- HTTP routes — use Arc, Express, Fastify, NestJS, Next.js
- Durable workflows — use Streamline, BullMQ, cron
- Agent tooling — wrap
flow.servicesin your agent tools or MCP servers - Ledger integration — subscribe to
flow.events - Authentication — pass
AuthContextto service methods
Dependencies
| Dependency | Type | Required |
|---|---|---|
mongoose |
peer | Yes (>=9.0.0) |
@classytic/mongokit |
peer | Yes (>=3.0.0) — provides repository pattern, pagination, plugins |
Reference Docs
| Doc | Description |
|---|---|
| Architecture | Domain model, clean architecture layers, invariants, capability tiers |
| Algorithms | Valuation, allocation, routing, replenishment formulas |
| Workflows | Inbound, outbound, transfer, counting, return flows |
| Plugins & Adapters | Scan, events, labels, catalog, policies, workflow adapters |
| Integration | MongoKit, Arc, Streamline, framework examples |
| API Reference | Service methods, entity fields, events, errors |
| Test Strategy | Test layers, coverage targets, fixtures |
| Migration Plan | Step-by-step guide to integrate Flow into an Arc backend |
| Changelog | Version history and bug fixes |
License
MIT