@happyvertical/smrt-inventory 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/__smrt-register__.ts","../src/models/InventoryLocation.ts","../src/collections/InventoryLocationCollection.ts","../src/models/StockLevel.ts","../src/collections/StockLevelCollection.ts","../src/models/StockMovement.ts","../src/collections/StockMovementCollection.ts","../src/services/StockService.ts","../src/services/dispatch-handlers.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` (and from any\n * other entry that loads class modules — e.g. `src/models/index.ts`,\n * `src/collections/index.ts`) so its top-level side effect runs ahead of any\n * class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// `new URL('./manifest.json', import.meta.url)` resolves at runtime to the\n// manifest sitting next to this module's compiled output. Vite warns at build\n// time that it cannot pre-resolve the URL; that is the intended behavior —\n// the URL must resolve to dist/manifest.json at runtime, not be inlined.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * InventoryLocation — a physical or virtual place where stock can sit.\n *\n * `kind` is intentionally a free-form string (see\n * {@link InventoryLocationKind}). Common values are `'warehouse'`,\n * `'factory'`, `'retail'`, `'in_transit'`, or `'virtual'`, but the\n * framework does not special-case any of them. Each application is free\n * to extend the vocabulary.\n *\n * Locations may optionally point at a place row in\n * `@happyvertical/smrt-places` via the plain-string `placeId` — useful\n * when an InventoryLocation maps to a real-world address or POI. The\n * reference is a plain string id, not a `@foreignKey()`, to avoid a\n * cross-package dependency on `smrt-places`.\n *\n * @packageDocumentation\n */\n\nimport {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { InventoryLocationKind } from '../types.js';\n\n/**\n * Options accepted by the {@link InventoryLocation} constructor.\n */\nexport interface InventoryLocationOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n code?: string;\n name?: string;\n kind?: InventoryLocationKind;\n placeId?: string;\n active?: boolean;\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'inventory_locations',\n conflictColumns: ['code', 'tenant_id'],\n api: { include: ['list', 'get', 'create', 'update'] },\n mcp: { include: ['list', 'get'] },\n cli: true,\n})\nexport class InventoryLocation extends SmrtObject {\n /** Tenant scope. `null` means the location record is global. */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * Short stable identifier (`'WH-EAST'`, `'STORE-42'`, `'IN-TRANSIT'`).\n * Together with `tenantId` this is the natural key.\n */\n @field({ required: true })\n code: string = '';\n\n /** Display name for UIs. */\n name: string = '';\n\n /**\n * Open-ended classifier (`'warehouse'`, `'factory'`, `'retail'`,\n * `'in_transit'`, `'virtual'`, or anything else your domain needs).\n * The framework never branches on this value.\n */\n kind: InventoryLocationKind = 'warehouse';\n\n /**\n * Optional plain-string reference to a `Place.id` in\n * `@happyvertical/smrt-places`. Cross-package id; intentionally not a\n * `@foreignKey()` so this package can be used without `smrt-places`\n * installed.\n */\n placeId: string = '';\n\n /** Soft-active flag — inactive locations stay queryable for history. */\n active: boolean = true;\n\n constructor(options: InventoryLocationOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.code !== undefined) this.code = options.code;\n if (options.name !== undefined) this.name = options.name;\n if (options.kind !== undefined) this.kind = options.kind;\n if (options.placeId !== undefined) this.placeId = options.placeId;\n if (options.active !== undefined) this.active = options.active;\n }\n}\n","/**\n * InventoryLocationCollection — query helpers for {@link InventoryLocation}.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { InventoryLocation } from '../models/InventoryLocation.js';\nimport type { InventoryLocationKind } from '../types.js';\n\nexport class InventoryLocationCollection extends SmrtCollection<InventoryLocation> {\n static readonly _itemClass = InventoryLocation;\n\n /**\n * Look up a location by its tenant-scoped `code`. Returns `null` when\n * no row matches.\n */\n async findByCode(code: string): Promise<InventoryLocation | null> {\n const matches = await this.list({ where: { code }, limit: 1 });\n return matches[0] ?? null;\n }\n\n /**\n * Find every location classified as the given kind (`'warehouse'`,\n * `'factory'`, `'retail'`, `'in_transit'`, …).\n */\n async findByKind(kind: InventoryLocationKind): Promise<InventoryLocation[]> {\n return this.list({ where: { kind }, orderBy: 'code ASC' });\n }\n\n /**\n * Find every location linked to a particular `Place.id` from\n * `@happyvertical/smrt-places`. Returns an empty array when no row\n * references the place.\n */\n async findByPlace(placeId: string): Promise<InventoryLocation[]> {\n if (!placeId) return [];\n return this.list({ where: { placeId }, orderBy: 'code ASC' });\n }\n\n /** Find every active location, optionally narrowed by kind. */\n async findActive(kind?: InventoryLocationKind): Promise<InventoryLocation[]> {\n const where: Record<string, unknown> = { active: true };\n if (kind) where.kind = kind;\n return this.list({ where, orderBy: 'code ASC' });\n }\n}\n","/**\n * StockLevel — materialized quantity at a `(skuId, locationId, state)` tuple.\n *\n * StockLevel is the live, mutable view of on-hand stock. There is at most\n * one row per `(skuId, locationId, state)` per tenant; the natural key\n * (`conflictColumns`) gives us idempotent upserts. Each row's `qty`\n * floats up and down as the {@link StockService} processes\n * receive/reserve/release/fulfill/transfer/adjust calls.\n *\n * **Do not call `create()` or `save()` on StockLevel directly.** All\n * mutations must go through {@link StockService}, which guarantees that\n * the level change and the corresponding append-only StockMovement land\n * together. Direct writes will silently desync the audit ledger and make\n * cycle counts unreliable.\n *\n * @packageDocumentation\n */\n\nimport {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { StockState } from '../types.js';\n\n/**\n * Options accepted by the {@link StockLevel} constructor.\n */\nexport interface StockLevelOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n skuId?: string;\n locationId?: string;\n state?: StockState;\n qty?: number;\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'inventory_stock_levels',\n conflictColumns: ['sku_id', 'location_id', 'state', 'tenant_id'],\n // StockLevel is materialized state, written only by StockService.\n // Mirror the read-only api/mcp posture on the CLI — `cli: true` would\n // generate create/update/delete subcommands that let a CLI user\n // mutate `qty` (or delete a row) without writing a paired\n // StockMovement, silently desyncing the audit ledger from the\n // materialized balance. The Gotchas section in CLAUDE.md spells out\n // the \"never call StockLevel.save() directly\" rule; the CLI must\n // follow the same constraint.\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n})\nexport class StockLevel extends SmrtObject {\n /** Tenant scope. `null` means the level row is global. */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** Plain string reference to the {@link Sku} this row tracks. */\n @field({ required: true })\n skuId: string = '';\n\n /** Plain string reference to the {@link InventoryLocation} this row tracks. */\n @field({ required: true })\n locationId: string = '';\n\n /** Logical state — `available`, `allocated`, `wip`, `qc_hold`, `damaged`. */\n @field({ required: true })\n state: StockState = 'available';\n\n /**\n * Current quantity. Fractional values are allowed (`= 0.0`) for\n * domains that count in units of measure other than whole pieces\n * (kilograms, litres, metres).\n */\n @field({ type: 'decimal' })\n qty: number = 0.0;\n\n constructor(options: StockLevelOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.skuId !== undefined) this.skuId = options.skuId;\n if (options.locationId !== undefined) this.locationId = options.locationId;\n if (options.state !== undefined) this.state = options.state;\n if (options.qty !== undefined) this.qty = options.qty;\n }\n}\n","/**\n * StockLevelCollection — read helpers and balance aggregations.\n *\n * Mutations live in {@link StockService}. The collection here is for\n * reading the materialized view only; callers that want to change\n * `qty` must go through the service so the StockMovement audit row is\n * also written.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { StockLevel } from '../models/StockLevel.js';\nimport type { StockState } from '../types.js';\n\nexport class StockLevelCollection extends SmrtCollection<StockLevel> {\n static readonly _itemClass = StockLevel;\n\n /**\n * Fetch the level row for a `(skuId, locationId, state)` tuple, or\n * `null` when the row has never been written. State defaults to\n * `'available'` because that is the common case (selling / picking\n * decisions are driven by available stock).\n */\n async getLevel(\n skuId: string,\n locationId: string,\n state: StockState = 'available',\n ): Promise<StockLevel | null> {\n const matches = await this.list({\n where: { skuId, locationId, state },\n limit: 1,\n });\n return matches[0] ?? null;\n }\n\n /**\n * Return every level row for the given SKU across all locations and\n * states. Useful for \"where is this SKU?\" admin screens.\n */\n async findBySku(skuId: string): Promise<StockLevel[]> {\n return this.list({ where: { skuId } });\n }\n\n /**\n * Return every level row at the given location. Useful for \"what is\n * in this warehouse?\" reports.\n */\n async findByLocation(locationId: string): Promise<StockLevel[]> {\n return this.list({ where: { locationId } });\n }\n\n /**\n * Sum `qty` across all level rows for the given SKU. Pass `state` to\n * narrow the sum to one logical state (e.g. only `available`);\n * omit it for grand total across all states.\n */\n async totalForSku(skuId: string, state?: StockState): Promise<number> {\n const where: Record<string, unknown> = { skuId };\n if (state) where.state = state;\n const levels = await this.list({ where });\n return levels.reduce((sum, row) => sum + Number(row.qty ?? 0), 0);\n }\n\n /**\n * Sum `qty` across all level rows at the given location, optionally\n * narrowed by state.\n */\n async totalForLocation(\n locationId: string,\n state?: StockState,\n ): Promise<number> {\n const where: Record<string, unknown> = { locationId };\n if (state) where.state = state;\n const levels = await this.list({ where });\n return levels.reduce((sum, row) => sum + Number(row.qty ?? 0), 0);\n }\n}\n","/**\n * StockMovement — append-only audit row written for every stock mutation.\n *\n * Every call into {@link StockService} produces exactly one StockMovement\n * row (or two for `transfer`, one per leg). These rows are the ledger of\n * record: balances on {@link StockLevel} are derived state that could\n * always, in principle, be reconstructed by replaying movements in\n * order.\n *\n * **Movements are append-only.** The package treats updates as a bug and\n * relies on this for cycle-count reconciliation, audit, and source-system\n * attribution. Never call `save()` on a previously-persisted StockMovement\n * with mutated fields, and never let CRUD UIs expose update/delete on the\n * table.\n *\n * Cross-package attribution lives in `sourceType` + `sourceId` so a\n * downstream package can write `'Contract'` + the contract id when it\n * reserves stock, then later query \"which movements were caused by\n * contract X?\" without joining across the framework.\n *\n * @packageDocumentation\n */\n\nimport {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { StockMovementReason, StockState } from '../types.js';\n\n/**\n * Options accepted by the {@link StockMovement} constructor.\n */\nexport interface StockMovementOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n skuId?: string;\n locationId?: string;\n fromState?: StockState | null;\n toState?: StockState | null;\n qty?: number;\n reasonCode?: StockMovementReason;\n sourceType?: string;\n sourceId?: string;\n note?: string;\n occurredAt?: Date | string;\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'inventory_stock_movements',\n // Append-only: the natural key is the surrogate id, so updates can\n // never collide on conflictColumns. Setting `id` explicitly here\n // documents the intent and prevents future contributors from\n // accidentally adding a \"domain natural key\" that would let upserts\n // overwrite history.\n conflictColumns: ['id'],\n // Movements are an audit log — never mutate or delete via generated\n // CRUD. Reads are fine for reporting and admin tooling. CLI follows\n // the same posture as api/mcp; `cli: true` would generate\n // create/update/delete subcommands that could rewrite or remove\n // audit rows from the shell, defeating the append-only invariant\n // documented in CLAUDE.md.\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n})\nexport class StockMovement extends SmrtObject {\n /** Tenant scope. `null` means the movement is global. */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** Plain string reference to the {@link Sku} being moved. */\n @field({ required: true })\n skuId: string = '';\n\n /** Plain string reference to the {@link InventoryLocation} being mutated. */\n @field({ required: true })\n locationId: string = '';\n\n /**\n * Origin state for transitions (e.g. `available` → `allocated` for a\n * reservation). `null` indicates \"no origin\" — used when stock enters\n * the system fresh via {@link StockService.receive} or production.\n */\n fromState: StockState | null = null;\n\n /**\n * Destination state. `null` indicates \"no destination\" — used for\n * fulfilment, where stock leaves the building entirely.\n */\n toState: StockState | null = null;\n\n /** Quantity moved. Always positive; the direction is encoded by from/to. */\n @field({ type: 'decimal' })\n qty: number = 0.0;\n\n /**\n * Why the movement happened (see {@link StockMovementReason}). Drawn\n * from the canonical list when emitted by {@link StockService}; free-form\n * strings are allowed for vertical-specific reasons.\n */\n @field({ required: true })\n reasonCode: StockMovementReason = 'adjustment';\n\n /**\n * Cross-package attribution tag — e.g. `'Contract'`, `'Fulfillment'`,\n * `'ProductionOrder'`, `'CycleCount'`. The package writing the\n * movement decides what tag makes sense; readers can group by\n * `(sourceType, sourceId)` to reconstruct \"what caused this\".\n */\n sourceType: string = '';\n\n /**\n * Cross-package id of the row that caused this movement. Plain string;\n * the framework never dereferences it.\n */\n sourceId: string = '';\n\n /** Optional free-form note shown in audit UIs. */\n note: string = '';\n\n /**\n * When the movement happened. Set to `now` at write time; explicit\n * values are allowed when back-dating an import.\n */\n occurredAt: Date = new Date();\n\n constructor(options: StockMovementOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.skuId !== undefined) this.skuId = options.skuId;\n if (options.locationId !== undefined) this.locationId = options.locationId;\n if (options.fromState !== undefined) this.fromState = options.fromState;\n if (options.toState !== undefined) this.toState = options.toState;\n if (options.qty !== undefined) this.qty = options.qty;\n if (options.reasonCode !== undefined) this.reasonCode = options.reasonCode;\n if (options.sourceType !== undefined) this.sourceType = options.sourceType;\n if (options.sourceId !== undefined) this.sourceId = options.sourceId;\n if (options.note !== undefined) this.note = options.note;\n if (options.occurredAt !== undefined) {\n this.occurredAt =\n options.occurredAt instanceof Date\n ? options.occurredAt\n : new Date(options.occurredAt);\n }\n }\n}\n","/**\n * StockMovementCollection — read-only access to the append-only audit log.\n *\n * Treat this collection as a ledger. The `StockService` is the only\n * sanctioned writer; query helpers here exist so reporting code, audit\n * UIs, and cycle-count tooling can reconstruct history.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { StockMovement } from '../models/StockMovement.js';\nimport type { StockMovementReason } from '../types.js';\n\nexport class StockMovementCollection extends SmrtCollection<StockMovement> {\n static readonly _itemClass = StockMovement;\n\n /**\n * Return every movement for the given SKU, newest first. Useful for a\n * per-SKU audit trail.\n */\n async findBySku(skuId: string): Promise<StockMovement[]> {\n return this.list({ where: { skuId }, orderBy: 'occurredAt DESC' });\n }\n\n /**\n * Return every movement at the given location, newest first. Useful\n * for a per-warehouse audit trail.\n */\n async findByLocation(locationId: string): Promise<StockMovement[]> {\n return this.list({\n where: { locationId },\n orderBy: 'occurredAt DESC',\n });\n }\n\n /**\n * Return every movement attributed to the given upstream source — for\n * example `findBySource('Contract', contract.id)` returns every\n * movement caused by the reservation/fulfilment/release of that\n * contract. Newest first.\n */\n async findBySource(\n sourceType: string,\n sourceId: string,\n ): Promise<StockMovement[]> {\n return this.list({\n where: { sourceType, sourceId },\n orderBy: 'occurredAt DESC',\n });\n }\n\n /**\n * Return every movement with the given reason code (`'receipt'`,\n * `'reservation'`, `'adjustment'`, …). Newest first.\n */\n async findByReason(\n reasonCode: StockMovementReason,\n ): Promise<StockMovement[]> {\n return this.list({\n where: { reasonCode },\n orderBy: 'occurredAt DESC',\n });\n }\n}\n","/**\n * StockService — the only sanctioned way to mutate stock.\n *\n * Every mutation goes through one of six methods (`receive`, `reserve`,\n * `release`, `fulfill`, `transfer`, `adjust`). Each one:\n *\n * 1. Updates the materialized {@link StockLevel} row(s) for the affected\n * `(skuId, locationId, state)` tuples via the collection's upsert path.\n * 2. Writes exactly one append-only {@link StockMovement} (or two for\n * `transfer`, one per leg) carrying the reason, source attribution,\n * quantity, and from/to state transition.\n *\n * **Atomicity**: each method runs every write inside a single database\n * transaction (`db.transaction(...)` on `@happyvertical/sql >= 0.74.0`).\n * Partial failure — e.g. the level write succeeds but the movement\n * write throws — rolls the entire mutation back, so the materialized\n * balance and the audit ledger never disagree. `transfer` writes both\n * legs (source out, destination in) plus both movement rows in one tx,\n * so a failure mid-`transfer` is also fully rolled back.\n *\n * If the underlying adapter does not expose `transaction()` (rare —\n * sqlite, postgres, and duckdb all do as of 0.74.0; the JSON adapter\n * does too), the service degrades to serial writes and emits a one-time\n * `console.warn`. Tests and consumers that observe that warning should\n * upgrade their adapter.\n *\n * Callers should never reach into the level or movement collections\n * directly to mutate balances — doing so silently desyncs the audit\n * ledger and breaks cycle counts.\n *\n * Concurrency: each method is a read-modify-write on the underlying\n * StockLevel row. Awaited (serial) callers always see a consistent\n * balance. Unawaited `Promise.all([reserve, reserve, ...])` against the\n * same `(skuId, locationId)` can still over-allocate — the per-tx\n * locking guarantees atomicity of each individual call, not isolation\n * across concurrent transactions. Callers that need hard atomicity\n * across concurrent reservations should serialise upstream (e.g. via\n * the job runner from `@happyvertical/smrt-jobs`).\n *\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { type DatabaseConfig, resolveDatabase } from '@happyvertical/smrt-core';\nimport {\n InventoryLocationCollection,\n StockLevelCollection,\n StockMovementCollection,\n} from '../collections/index.js';\nimport type { StockLevel } from '../models/StockLevel.js';\nimport type { StockMovementReason, StockState } from '../types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Thrown by {@link StockService.reserve} (and {@link StockService.fulfill},\n * {@link StockService.transfer}) when the caller asks to move more stock\n * than the source state currently holds. Carries enough context for a\n * caller to surface a meaningful UI message and decide whether to retry,\n * backorder, or cancel.\n */\nexport class InsufficientStockError extends Error {\n override name = 'InsufficientStockError';\n\n constructor(\n public readonly skuId: string,\n public readonly locationId: string,\n public readonly state: StockState,\n public readonly requested: number,\n public readonly available: number,\n ) {\n super(\n `Insufficient stock: sku=${skuId} location=${locationId} state=${state} ` +\n `requested=${requested} available=${available}`,\n );\n }\n}\n\n/**\n * Options shared by every {@link StockService} method that wants to leave\n * an audit attribution behind. Pairs neatly with the cross-package\n * pattern in {@link StockMovement.sourceType} / {@link StockMovement.sourceId}.\n */\nexport interface StockMutationOptions {\n /** Cross-package tag, e.g. `'Contract'`, `'Fulfillment'`, `'CycleCount'`. */\n sourceType?: string;\n /** Cross-package id of the row that caused this mutation. */\n sourceId?: string;\n /** Free-form note shown in audit UIs. */\n note?: string;\n /**\n * Override the reason code stamped on the {@link StockMovement}. Each\n * method picks a sensible default; explicit overrides are useful when a\n * vertical wants to flag a more specific reason (e.g. `'return'`\n * instead of `'receipt'`).\n */\n reasonCode?: StockMovementReason;\n}\n\n/**\n * Options accepted by the {@link StockService} factory.\n */\nexport interface StockServiceOptions {\n /**\n * Database to read/write through. Accepts the same shapes that\n * `SmrtCollection.create({ db })` accepts — a `DatabaseInterface`, a\n * connection-string URL, or a `{ type, url }` config object. Reused by\n * the internal collections so the service, level reads, and movement\n * writes always hit the same connection / pool.\n */\n db: DatabaseConfig;\n}\n\ninterface AdjustLevelOptions {\n skuId: string;\n locationId: string;\n state: StockState;\n delta: number;\n enforceNonNegative?: boolean;\n}\n\ninterface WriteMovementOptions {\n skuId: string;\n locationId: string;\n fromState: StockState | null;\n toState: StockState | null;\n qty: number;\n reasonCode: StockMovementReason;\n sourceType?: string;\n sourceId?: string;\n note?: string;\n}\n\n/**\n * Sanctioned stock-mutation surface.\n *\n * Construct via {@link createStockService} — the static factory wires up\n * the underlying collections and shares one database connection across\n * level reads and movement writes.\n *\n * @example\n * ```typescript\n * const service = await createStockService({ db });\n * await service.receive(sku.id, warehouse.id, 100, {\n * sourceType: 'PurchaseOrder',\n * sourceId: po.id,\n * });\n * await service.reserve(sku.id, warehouse.id, 10, {\n * sourceType: 'Contract',\n * sourceId: order.id,\n * });\n * await service.fulfill(sku.id, warehouse.id, 10, {\n * sourceType: 'Fulfillment',\n * sourceId: fulfillment.id,\n * });\n * ```\n */\nexport class StockService {\n private constructor(\n /**\n * The database config this service was bound to (URL string, config\n * object, or already-resolved `DatabaseInterface`). Exposed so\n * downstream services that compose StockService (e.g. BomService,\n * ProductionService in `@happyvertical/smrt-manufacturing`) can pass\n * the same value to their own collection factories without reaching\n * into private fields on the collections.\n */\n public readonly db: DatabaseConfig,\n public readonly levels: StockLevelCollection,\n public readonly movements: StockMovementCollection,\n public readonly locations: InventoryLocationCollection,\n /**\n * Marks a service instance handed to a {@link withTransaction}\n * callback. Public mutation methods on a tx-bound instance skip\n * opening a nested transaction and just execute against the already-\n * bound collections; outer (non-tx) instances open a fresh\n * transaction per mutation. Internal flag — consumers never set it.\n */\n private readonly inTransaction: boolean = false,\n ) {}\n\n /** Internal factory — prefer {@link createStockService}. */\n static async create(options: StockServiceOptions): Promise<StockService> {\n // Resolve the caller's `DatabaseConfig` to a single\n // `DatabaseInterface` instance FIRST, then construct every internal\n // collection on that shared instance. Without this step, passing\n // `{ type: 'sqlite', url: ':memory:' }` would make\n // `SmrtCollection.create({ db: config })` materialize a *fresh*\n // in-memory database per collection — `levels`, `movements`, and\n // `locations` would each point at their own empty DB. The runtime\n // ledger then desyncs: `adjust()` writes a movement to\n // `movements.db` while balance reads go through `levels.db`, and\n // audit queries on `movements` miss the row that was just written.\n // Resolving once means every collection (and the public `db` field\n // exposed for downstream composition) share the same handle.\n const resolved = await resolveDatabase(options.db);\n const [levels, movements, locations] = await Promise.all([\n StockLevelCollection.create({ db: resolved }),\n StockMovementCollection.create({ db: resolved }),\n InventoryLocationCollection.create({ db: resolved }),\n ]);\n return new StockService(\n resolved as unknown as DatabaseConfig,\n levels,\n movements,\n locations,\n );\n }\n\n /**\n * Run `work` inside a single database transaction with a tx-bound\n * {@link StockService} instance. All mutation calls on `tx` commit\n * atomically when `work` resolves and roll back if it throws.\n *\n * Use this when you need atomicity ACROSS multiple stock-service\n * calls — e.g. consuming materials for every line of a production\n * order in `@happyvertical/smrt-manufacturing`'s `ProductionService`,\n * or a custom workflow that reserves + fulfills + writes a custom\n * audit comment in one indivisible step. Individual mutation methods\n * (`receive`, `reserve`, etc.) are already atomic on their own — you\n * only need `withTransaction` for cross-call composition.\n *\n * Nesting is safe: calling `tx.withTransaction(...)` inside an\n * already-tx-bound callback simply runs the inner `work` on the same\n * transaction without opening a savepoint.\n *\n * When the underlying adapter does not expose `transaction()`, falls\n * through to a serial run on the regular collections with a one-time\n * warning. All four built-in adapters in `@happyvertical/sql >= 0.74.0`\n * support it; only test stubs would hit this branch.\n */\n async withTransaction<T>(work: (tx: StockService) => Promise<T>): Promise<T> {\n if (this.inTransaction) return work(this);\n\n const underlying = (this.levels as unknown as { db?: unknown }).db as\n | { transaction?: (cb: (txDb: unknown) => Promise<T>) => Promise<T> }\n | undefined;\n\n if (typeof underlying?.transaction !== 'function') {\n warnNonTransactional();\n return work(this);\n }\n\n return underlying.transaction(async (txDb) => {\n const [levels, movements, locations] = await Promise.all([\n StockLevelCollection.create({ db: txDb as DatabaseConfig }),\n StockMovementCollection.create({ db: txDb as DatabaseConfig }),\n InventoryLocationCollection.create({ db: txDb as DatabaseConfig }),\n ]);\n // Expose the tx-scoped db (not `this.db`) as the tx instance's\n // public `db` field. Callers compose this through `tx.db` when\n // spinning up adjacent services that should also write in the same\n // transaction — e.g. `BomService.create({ db: tx.db })` or\n // `MyCollection.create({ db: tx.db })`. If we leaked the outer db\n // here, those companion writes would commit independently of the\n // stock mutations, defeating the documented \"everything-in-one-tx\"\n // guarantee. Internal mutation methods don't read `tx.db`; they\n // use the tx-scoped collections passed alongside.\n const tx = new StockService(\n txDb as DatabaseConfig,\n levels,\n movements,\n locations,\n /* inTransaction */ true,\n );\n return work(tx);\n });\n }\n\n /**\n * Internal: run a single-method mutation in a transaction. If we're\n * already inside one (the instance was handed to a `withTransaction`\n * callback), reuse it; otherwise open a fresh one.\n */\n private async runAtomically<T>(\n work: (tx: {\n levels: StockLevelCollection;\n movements: StockMovementCollection;\n }) => Promise<T>,\n ): Promise<T> {\n if (this.inTransaction) {\n return work({ levels: this.levels, movements: this.movements });\n }\n return this.withTransaction(async (tx) =>\n work({ levels: tx.levels, movements: tx.movements }),\n );\n }\n\n /**\n * Add `qty` to available stock at the given location. Used for\n * purchase-order receipts, customer returns going back into available\n * inventory, and the \"produce\" leg of a production order.\n */\n async receive(\n skuId: string,\n locationId: string,\n qty: number,\n options: StockMutationOptions = {},\n ): Promise<void> {\n assertPositiveQty(qty, 'receive');\n await this.runAtomically(async (tx) => {\n await adjustLevel(tx.levels, {\n skuId,\n locationId,\n state: 'available',\n delta: qty,\n });\n await writeMovement(tx.movements, {\n skuId,\n locationId,\n fromState: null,\n toState: 'available',\n qty,\n reasonCode: options.reasonCode ?? 'receipt',\n sourceType: options.sourceType,\n sourceId: options.sourceId,\n note: options.note,\n });\n });\n }\n\n /**\n * Move `qty` from `available` to `allocated` at the given location.\n * Throws {@link InsufficientStockError} if available stock would go\n * negative.\n */\n async reserve(\n skuId: string,\n locationId: string,\n qty: number,\n options: StockMutationOptions = {},\n ): Promise<void> {\n assertPositiveQty(qty, 'reserve');\n await this.runAtomically(async (tx) => {\n await transitionState(tx, {\n skuId,\n locationId,\n fromState: 'available',\n toState: 'allocated',\n qty,\n reasonCode: options.reasonCode ?? 'reservation',\n sourceType: options.sourceType,\n sourceId: options.sourceId,\n note: options.note,\n });\n });\n }\n\n /**\n * Move `qty` from `allocated` back to `available`. Used when a\n * reservation is cancelled and the previously-reserved stock should\n * go back into the available pool.\n */\n async release(\n skuId: string,\n locationId: string,\n qty: number,\n options: StockMutationOptions = {},\n ): Promise<void> {\n assertPositiveQty(qty, 'release');\n await this.runAtomically(async (tx) => {\n await transitionState(tx, {\n skuId,\n locationId,\n fromState: 'allocated',\n toState: 'available',\n qty,\n reasonCode: options.reasonCode ?? 'release',\n sourceType: options.sourceType,\n sourceId: options.sourceId,\n note: options.note,\n });\n });\n }\n\n /**\n * Remove `qty` from `allocated` at the given location. Stock leaves\n * the building entirely (shipped, picked up, consumed). Throws\n * {@link InsufficientStockError} if allocated stock would go negative.\n */\n async fulfill(\n skuId: string,\n locationId: string,\n qty: number,\n options: StockMutationOptions = {},\n ): Promise<void> {\n assertPositiveQty(qty, 'fulfill');\n await this.runAtomically(async (tx) => {\n await assertAvailable(tx.levels, skuId, locationId, 'allocated', qty);\n await adjustLevel(tx.levels, {\n skuId,\n locationId,\n state: 'allocated',\n delta: -qty,\n // Already enforced via assertAvailable; skipping the second check\n // avoids a needless extra DB round-trip in the hot path.\n enforceNonNegative: false,\n });\n await writeMovement(tx.movements, {\n skuId,\n locationId,\n fromState: 'allocated',\n toState: null,\n qty,\n reasonCode: options.reasonCode ?? 'fulfillment',\n sourceType: options.sourceType,\n sourceId: options.sourceId,\n note: options.note,\n });\n });\n }\n\n /**\n * Move `qty` of `available` stock from `fromLocationId` to\n * `toLocationId`. Writes two movement rows — one for the `transfer_out`\n * leg, one for the `transfer_in` leg — so the audit log preserves the\n * lineage in both directions. Throws {@link InsufficientStockError} if\n * source available stock would go negative.\n *\n * Both legs (level writes + movement rows) run inside one transaction\n * — a failure mid-`transfer` rolls back the source debit so there's no\n * \"ghost stock disappearance\" (source decremented, destination never\n * credited).\n */\n async transfer(\n skuId: string,\n fromLocationId: string,\n toLocationId: string,\n qty: number,\n options: StockMutationOptions = {},\n ): Promise<void> {\n assertPositiveQty(qty, 'transfer');\n if (fromLocationId === toLocationId) {\n throw new Error(\n `transfer: fromLocationId and toLocationId must differ (got ${fromLocationId})`,\n );\n }\n await this.runAtomically(async (tx) => {\n await assertAvailable(tx.levels, skuId, fromLocationId, 'available', qty);\n\n // Source leg: decrement available at origin.\n await adjustLevel(tx.levels, {\n skuId,\n locationId: fromLocationId,\n state: 'available',\n delta: -qty,\n enforceNonNegative: false,\n });\n await writeMovement(tx.movements, {\n skuId,\n locationId: fromLocationId,\n fromState: 'available',\n toState: null,\n qty,\n reasonCode: options.reasonCode ?? 'transfer_out',\n sourceType: options.sourceType,\n sourceId: options.sourceId,\n note: options.note,\n });\n\n // Destination leg: increment available at destination.\n await adjustLevel(tx.levels, {\n skuId,\n locationId: toLocationId,\n state: 'available',\n delta: qty,\n });\n await writeMovement(tx.movements, {\n skuId,\n locationId: toLocationId,\n fromState: null,\n toState: 'available',\n qty,\n reasonCode: options.reasonCode ?? 'transfer_in',\n sourceType: options.sourceType,\n sourceId: options.sourceId,\n note: options.note,\n });\n });\n }\n\n /**\n * Apply a positive or negative `delta` to a level row. Used for cycle\n * counts and one-off corrections; `delta=+5` adds five units,\n * `delta=-2` removes two. By default the adjustment targets\n * `available` stock; pass an explicit `state` to adjust a different\n * bucket (e.g. `'damaged'` after a quality-control reclassification).\n *\n * Adjusting by `0` is rejected as a probable programming error — the\n * caller almost always meant a non-zero delta and a no-op write would\n * still cost an audit row.\n */\n async adjust(\n skuId: string,\n locationId: string,\n delta: number,\n options: StockMutationOptions & { state?: StockState } = {},\n ): Promise<void> {\n if (!Number.isFinite(delta) || delta === 0) {\n throw new Error(\n `adjust: delta must be a non-zero finite number (got ${delta})`,\n );\n }\n const state = options.state ?? 'available';\n await this.runAtomically(async (tx) => {\n if (delta < 0) {\n await assertAvailable(tx.levels, skuId, locationId, state, -delta);\n }\n await adjustLevel(tx.levels, {\n skuId,\n locationId,\n state,\n delta,\n enforceNonNegative: false,\n });\n await writeMovement(tx.movements, {\n skuId,\n locationId,\n fromState: delta < 0 ? state : null,\n toState: delta > 0 ? state : null,\n qty: Math.abs(delta),\n reasonCode: options.reasonCode ?? 'adjustment',\n sourceType: options.sourceType,\n sourceId: options.sourceId,\n note: options.note,\n });\n });\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Tx-scoped helpers\n//\n// These are module-level functions rather than instance methods so each\n// call site can pass tx-scoped collections through cleanly. The previous\n// versions read `this.levels` / `this.movements` directly which bypassed\n// the transaction. Public methods now wrap everything in\n// `runAtomically` and call these helpers with the tx-scoped pair.\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Read the current level row, sanity-check the result, and throw\n * {@link InsufficientStockError} when the requested quantity is not\n * available.\n */\nasync function assertAvailable(\n levels: StockLevelCollection,\n skuId: string,\n locationId: string,\n state: StockState,\n requested: number,\n): Promise<void> {\n const level = await levels.getLevel(skuId, locationId, state);\n const available = level ? Number(level.qty ?? 0) : 0;\n if (available < requested) {\n throw new InsufficientStockError(\n skuId,\n locationId,\n state,\n requested,\n available,\n );\n }\n}\n\n/**\n * Atomically transition `qty` from one state to another at a single\n * location. Used by `reserve` and `release`.\n */\nasync function transitionState(\n tx: { levels: StockLevelCollection; movements: StockMovementCollection },\n args: {\n skuId: string;\n locationId: string;\n fromState: StockState;\n toState: StockState;\n qty: number;\n reasonCode: StockMovementReason;\n sourceType?: string;\n sourceId?: string;\n note?: string;\n },\n): Promise<void> {\n await assertAvailable(\n tx.levels,\n args.skuId,\n args.locationId,\n args.fromState,\n args.qty,\n );\n await adjustLevel(tx.levels, {\n skuId: args.skuId,\n locationId: args.locationId,\n state: args.fromState,\n delta: -args.qty,\n enforceNonNegative: false,\n });\n await adjustLevel(tx.levels, {\n skuId: args.skuId,\n locationId: args.locationId,\n state: args.toState,\n delta: args.qty,\n });\n await writeMovement(tx.movements, {\n skuId: args.skuId,\n locationId: args.locationId,\n fromState: args.fromState,\n toState: args.toState,\n qty: args.qty,\n reasonCode: args.reasonCode,\n sourceType: args.sourceType,\n sourceId: args.sourceId,\n note: args.note,\n });\n}\n\n/**\n * Read-modify-write a level row by `delta`. Creates the row when it\n * doesn't exist yet. When `enforceNonNegative` is set, refuses to\n * write a negative `qty` and throws {@link InsufficientStockError}\n * with the original (non-decremented) balance.\n */\nasync function adjustLevel(\n levels: StockLevelCollection,\n options: AdjustLevelOptions,\n): Promise<StockLevel> {\n const enforce = options.enforceNonNegative ?? options.delta < 0;\n const existing = await levels.getLevel(\n options.skuId,\n options.locationId,\n options.state,\n );\n const previous = existing ? Number(existing.qty ?? 0) : 0;\n const next = previous + options.delta;\n\n if (enforce && next < 0) {\n throw new InsufficientStockError(\n options.skuId,\n options.locationId,\n options.state,\n Math.abs(options.delta),\n previous,\n );\n }\n\n if (existing) {\n existing.qty = next;\n await existing.save();\n return existing;\n }\n\n // SmrtCollection.create() saves the row before returning, so no further\n // save() is needed here. Calling save() again would emit a redundant\n // upsert round-trip on every hot-path first write.\n const level = await levels.create({\n skuId: options.skuId,\n locationId: options.locationId,\n state: options.state,\n qty: next,\n });\n return level;\n}\n\n/**\n * Append a movement to the audit log. Never updates an existing row —\n * the natural key is the surrogate id, so each call writes a fresh\n * append-only entry.\n */\nasync function writeMovement(\n movements: StockMovementCollection,\n options: WriteMovementOptions,\n): Promise<void> {\n // SmrtCollection.create() persists the row before returning. A\n // follow-up save() would emit a redundant upsert round-trip on every\n // single stock mutation (one or two per service call) — and the\n // movement table is append-only, so there's nothing to re-save.\n await movements.create({\n skuId: options.skuId,\n locationId: options.locationId,\n fromState: options.fromState,\n toState: options.toState,\n qty: options.qty,\n reasonCode: options.reasonCode,\n sourceType: options.sourceType ?? '',\n sourceId: options.sourceId ?? '',\n note: options.note ?? '',\n occurredAt: new Date(),\n });\n}\n\n/**\n * Reject zero / negative / non-finite quantities up front. Most stock\n * mutation methods accept positive quantities only; {@link StockService.adjust}\n * is the exception (it accepts signed deltas) and validates separately.\n */\nfunction assertPositiveQty(qty: number, op: string): void {\n if (!Number.isFinite(qty) || qty <= 0) {\n throw new Error(`${op}: qty must be a positive finite number (got ${qty})`);\n }\n}\n\nlet warnedNonTransactional = false;\n\n/**\n * Emit a one-time `console.warn` when the underlying SQL adapter does\n * not expose `transaction()`. All four `@happyvertical/sql >= 0.74.0`\n * adapters implement it — only test stubs or third-party adapters would\n * trip this branch.\n */\nfunction warnNonTransactional(): void {\n if (warnedNonTransactional) return;\n warnedNonTransactional = true;\n logger.warn(\n '[@happyvertical/smrt-inventory] StockService: underlying SQL adapter ' +\n 'does not expose `transaction()`. Stock mutations are degrading to ' +\n 'non-atomic serial writes — partial failures may leave the materialized ' +\n 'level and the audit ledger out of sync. Upgrade @happyvertical/sql to ' +\n '>= 0.74.0 or use one of its built-in adapters.',\n );\n}\n\n/**\n * Convenience factory. Returns a fully-initialized {@link StockService}\n * sharing the given database with its internal collections.\n */\nexport async function createStockService(\n options: StockServiceOptions,\n): Promise<StockService> {\n return StockService.create(options);\n}\n","/**\n * Opt-in DispatchBus subscribers that bridge upstream packages to the\n * {@link StockService}.\n *\n * **Off by default.** The package never auto-subscribes — consumer apps\n * call {@link installInventoryDispatchHandlers} from their `smrt.ts`\n * wiring once they have decided they want automatic stock motion when\n * contracts are created and fulfilments ship.\n *\n * The production-order leg (`production_order:posted` → consume\n * materials + produce SKUs) is deliberately *not* installed here; that\n * subscriber lives in `@happyvertical/smrt-manufacturing` (see issue\n * #1250) and depends on the BOM model that ships alongside it.\n *\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type {\n DatabaseConfig,\n DispatchBus,\n DispatchHandler,\n DispatchMetadata,\n} from '@happyvertical/smrt-core';\nimport type { StockMutationOptions } from './StockService.js';\nimport { createStockService, type StockService } from './StockService.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Shape of a single reserved line in a `contract:created` payload. The\n * producer (typically `@happyvertical/smrt-commerce`) packs one entry\n * per line that needs stock motion; everything else (taxes, fees,\n * non-physical items) should be filtered upstream.\n */\nexport interface ContractCreatedLine {\n skuId: string;\n locationId: string;\n qty: number;\n}\n\n/**\n * Shape of the `contract:created` payload this handler expects. Carries\n * the contract id (used for source attribution on the audit row) and\n * the list of line items to reserve.\n */\nexport interface ContractCreatedPayload {\n contractId: string;\n lines: ContractCreatedLine[];\n}\n\n/**\n * Shape of a single shipped line in a `fulfillment:shipped` payload.\n */\nexport interface FulfillmentShippedLine {\n skuId: string;\n locationId: string;\n qty: number;\n}\n\n/**\n * Shape of the `fulfillment:shipped` payload this handler expects.\n * Producers emit one per shipped fulfilment; the handler fulfils each\n * line against the level created by the matching `contract:created`\n * reservation.\n */\nexport interface FulfillmentShippedPayload {\n fulfillmentId: string;\n lines: FulfillmentShippedLine[];\n}\n\n/**\n * Options accepted by {@link installInventoryDispatchHandlers}.\n *\n * Provide either a pre-built {@link StockService} (when sharing one\n * across multiple subsystems) or a `db` for the helper to construct one\n * on first use.\n */\nexport type InstallInventoryDispatchHandlersOptions = {\n /**\n * Bus to subscribe on. Typically the app-wide `DispatchBus` created in\n * the application's `smrt.ts`.\n */\n dispatchBus: DispatchBus;\n /**\n * When `true` (default), install the `contract:created` handler.\n * Producers should publish a {@link ContractCreatedPayload}.\n */\n installContractReserved?: boolean;\n /**\n * When `true` (default), install the `fulfillment:shipped` handler.\n * Producers should publish a {@link FulfillmentShippedPayload}.\n */\n installFulfillmentShipped?: boolean;\n} & (\n | { stockService: StockService; db?: DatabaseConfig }\n | { db: DatabaseConfig; stockService?: undefined }\n);\n\n/**\n * Result handle returned by {@link installInventoryDispatchHandlers}.\n * Call `dispose()` to detach the subscribers (mainly useful in tests\n * and on graceful shutdown).\n */\nexport interface InstalledInventoryDispatchHandlers {\n /** Resolved StockService — useful for follow-up writes in the same scope. */\n stockService: StockService;\n /** Detach every installed subscriber. Idempotent. */\n dispose(): void;\n}\n\n/**\n * Subscribe a {@link StockService}-driven handler to the relevant signals\n * on the given {@link DispatchBus}. Returns a disposer for tests and\n * shutdown hooks.\n *\n * @example\n * ```typescript\n * import { createDispatchBus } from '@happyvertical/smrt-core';\n * import { installInventoryDispatchHandlers } from '@happyvertical/smrt-inventory';\n *\n * const bus = await createDispatchBus({ db });\n * const handlers = await installInventoryDispatchHandlers({\n * dispatchBus: bus,\n * db,\n * });\n * ```\n */\nexport async function installInventoryDispatchHandlers(\n options: InstallInventoryDispatchHandlersOptions,\n): Promise<InstalledInventoryDispatchHandlers> {\n const {\n dispatchBus,\n installContractReserved = true,\n installFulfillmentShipped = true,\n } = options;\n\n const stockService =\n options.stockService ?? (await buildStockService(options));\n\n const installed: Array<{ pattern: string; handler: DispatchHandler }> = [];\n\n if (installContractReserved) {\n const handler: DispatchHandler = async (payload, metadata) => {\n await handleContractCreated(\n stockService,\n payload as ContractCreatedPayload,\n metadata,\n );\n };\n dispatchBus.on('contract:created', handler);\n installed.push({ pattern: 'contract:created', handler });\n }\n\n if (installFulfillmentShipped) {\n const handler: DispatchHandler = async (payload, metadata) => {\n await handleFulfillmentShipped(\n stockService,\n payload as FulfillmentShippedPayload,\n metadata,\n );\n };\n dispatchBus.on('fulfillment:shipped', handler);\n installed.push({ pattern: 'fulfillment:shipped', handler });\n }\n\n return {\n stockService,\n dispose() {\n for (const entry of installed) {\n dispatchBus.off(entry.pattern, entry.handler);\n }\n installed.length = 0;\n },\n };\n}\n\nasync function handleContractCreated(\n stockService: StockService,\n payload: ContractCreatedPayload | null | undefined,\n metadata: DispatchMetadata,\n): Promise<void> {\n // Silent return on any of these would hide upstream contract drift —\n // producers emitting the wrong shape never get feedback. Source id\n // (`contractId`) is required: without it the resulting StockMovements\n // carry empty `sourceId`, breaking the audit trail back to the\n // originating contract. Each branch reports its specific reason so\n // the emitter can be diagnosed quickly.\n const reason = malformedContractPayloadReason(payload);\n if (reason || !payload) {\n warnMalformedPayload(\n 'contract:created',\n payload,\n metadata,\n reason ?? 'payload is null/undefined',\n );\n return;\n }\n // Validate every line BEFORE opening a transaction. A silent skip on\n // a null/undefined / shape-broken entry would look like a successful\n // commit while only reserving part of the contract — defeating the\n // \"atomic across lines\" guarantee just below. Fail the whole event\n // instead so producers get feedback and downstream observers see no\n // partial state.\n if (!areLinesWellFormed(payload.lines)) {\n warnMalformedPayload(\n 'contract:created',\n payload,\n metadata,\n 'one or more entries in `lines` are null/undefined or missing required fields (skuId: string, locationId: string, qty: finite number)',\n );\n return;\n }\n const baseOptions: StockMutationOptions = {\n sourceType: 'Contract',\n sourceId: payload.contractId,\n note: metadata.source ? `auto-reserve via ${metadata.source}` : undefined,\n };\n // Atomic across lines: a shortfall on line N rolls back lines 1..N-1.\n // Without this wrapper, an `InsufficientStockError` mid-loop would leave\n // a partially-reserved contract, and `DispatchBus` would only log the\n // async handler error — no compensating release would ever fire.\n await stockService.withTransaction(async (tx) => {\n for (const line of payload.lines) {\n await tx.reserve(line.skuId, line.locationId, line.qty, baseOptions);\n }\n });\n}\n\nasync function handleFulfillmentShipped(\n stockService: StockService,\n payload: FulfillmentShippedPayload | null | undefined,\n metadata: DispatchMetadata,\n): Promise<void> {\n // Same rationale as `handleContractCreated`: reject when the\n // source id (`fulfillmentId`) is missing rather than emit\n // StockMovements with no audit attribution; surface the specific\n // failure so producers can diagnose quickly.\n const reason = malformedFulfillmentPayloadReason(payload);\n if (reason || !payload) {\n warnMalformedPayload(\n 'fulfillment:shipped',\n payload,\n metadata,\n reason ?? 'payload is null/undefined',\n );\n return;\n }\n // Validate lines up front for the same reason as\n // `handleContractCreated`: a silent skip would partially fulfill the\n // shipment while looking successful.\n if (!areLinesWellFormed(payload.lines)) {\n warnMalformedPayload(\n 'fulfillment:shipped',\n payload,\n metadata,\n 'one or more entries in `lines` are null/undefined or missing required fields (skuId: string, locationId: string, qty: finite number)',\n );\n return;\n }\n const baseOptions: StockMutationOptions = {\n sourceType: 'Fulfillment',\n sourceId: payload.fulfillmentId,\n note: metadata.source ? `auto-fulfill via ${metadata.source}` : undefined,\n };\n // Atomic across lines: a shortfall on line N rolls back lines 1..N-1.\n // Same rationale as `handleContractCreated` above — without the tx\n // wrapper, a failing fulfilment leg would leave the audit ledger\n // inconsistent and no compensating reservation-restore would ever fire.\n await stockService.withTransaction(async (tx) => {\n for (const line of payload.lines) {\n await tx.fulfill(line.skuId, line.locationId, line.qty, baseOptions);\n }\n });\n}\n\n/**\n * Determine whether a `contract:created` payload is structurally\n * unusable and, if so, return a single human-readable reason naming\n * the specific field that failed. Returns `null` when the top-level\n * shape is acceptable. Line-level validation is a separate step.\n */\nfunction malformedContractPayloadReason(\n payload: ContractCreatedPayload | null | undefined,\n): string | null {\n if (!payload || typeof payload !== 'object') {\n return 'payload is null/undefined or not an object';\n }\n if (!payload.contractId || typeof payload.contractId !== 'string') {\n return 'missing or non-string `contractId` (required for audit-trail source attribution)';\n }\n if (!Array.isArray(payload.lines)) {\n return 'missing or non-array `lines`';\n }\n return null;\n}\n\n/**\n * Same shape as {@link malformedContractPayloadReason} but for\n * `fulfillment:shipped` events. The required source id field is\n * `fulfillmentId` here.\n */\nfunction malformedFulfillmentPayloadReason(\n payload: FulfillmentShippedPayload | null | undefined,\n): string | null {\n if (!payload || typeof payload !== 'object') {\n return 'payload is null/undefined or not an object';\n }\n if (!payload.fulfillmentId || typeof payload.fulfillmentId !== 'string') {\n return 'missing or non-string `fulfillmentId` (required for audit-trail source attribution)';\n }\n if (!Array.isArray(payload.lines)) {\n return 'missing or non-array `lines`';\n }\n return null;\n}\n\n/**\n * Validate that every entry in a dispatch `lines` array is a usable\n * line shape. Returns `false` as soon as it sees a null/undefined entry\n * (e.g. a serialized sparse array hole), a missing/non-string\n * `skuId` / `locationId`, or a non-finite `qty`. Used by both contract\n * and fulfilment handlers to fail-fast before opening a transaction —\n * a silent `continue` over a bad entry would partially mutate state\n * while making the malformed event look successful.\n */\nfunction areLinesWellFormed(\n lines: ReadonlyArray<\n ContractCreatedLine | FulfillmentShippedLine | null | undefined\n >,\n): boolean {\n for (const line of lines) {\n if (!line) return false;\n if (typeof line.skuId !== 'string' || !line.skuId) return false;\n if (typeof line.locationId !== 'string' || !line.locationId) return false;\n if (typeof line.qty !== 'number' || !Number.isFinite(line.qty))\n return false;\n }\n return true;\n}\n\n/**\n * Surface dispatch payloads that don't match the documented shape. The\n * subscribers used to silently return on a missing `lines` array, which\n * hid producer drift — emitters using stale shapes never got feedback\n * and the resulting \"stock didn't move\" bugs were a long way from the\n * dispatch layer. The warning carries the source attribution AND the\n * specific reason the payload was rejected so producers can fix the\n * emitter quickly. Each call site supplies its own `reason` string;\n * the generic shell wraps it with the signal name and payload-key\n * summary for forensic logging.\n */\nfunction warnMalformedPayload(\n signal: string,\n payload: unknown,\n metadata: DispatchMetadata,\n reason: string,\n): void {\n logger.warn(\n `[@happyvertical/smrt-inventory] dispatch handler ignored a ${signal} ` +\n `event with a malformed payload (${reason}). ` +\n `Source: ${metadata.source ?? '<unknown>'}; payload keys: ` +\n `${payload && typeof payload === 'object' ? Object.keys(payload).join(',') : typeof payload}`,\n );\n}\n\n/**\n * Build a `StockService` when no pre-built one was supplied. The option\n * union guarantees `db` is present in that branch, but TypeScript can't\n * narrow from a `??` value check, so this helper validates at runtime\n * with a clear error message.\n */\nasync function buildStockService(\n options: InstallInventoryDispatchHandlersOptions,\n): Promise<StockService> {\n if (!options.db) {\n throw new Error(\n 'installInventoryDispatchHandlers: either `stockService` or `db` is required',\n );\n }\n return createStockService({ db: options.db });\n}\n"],"names":["__decorateClass","logger"],"mappings":"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"}