import { DatabaseConfig } from '@happyvertical/smrt-core'; import { DispatchBus } from '@happyvertical/smrt-core'; import { SmrtCollection } from '@happyvertical/smrt-core'; import { SmrtObject } from '@happyvertical/smrt-core'; import { SmrtObjectOptions } from '@happyvertical/smrt-core'; /** * Shape of a single reserved line in a `contract:created` payload. The * producer (typically `@happyvertical/smrt-commerce`) packs one entry * per line that needs stock motion; everything else (taxes, fees, * non-physical items) should be filtered upstream. */ export declare interface ContractCreatedLine { skuId: string; locationId: string; qty: number; } /** * Shape of the `contract:created` payload this handler expects. Carries * the contract id (used for source attribution on the audit row) and * the list of line items to reserve. */ export declare interface ContractCreatedPayload { contractId: string; lines: ContractCreatedLine[]; } /** * Convenience factory. Returns a fully-initialized {@link StockService} * sharing the given database with its internal collections. */ export declare function createStockService(options: StockServiceOptions): Promise; /** * Shape of a single shipped line in a `fulfillment:shipped` payload. */ export declare interface FulfillmentShippedLine { skuId: string; locationId: string; qty: number; } /** * Shape of the `fulfillment:shipped` payload this handler expects. * Producers emit one per shipped fulfilment; the handler fulfils each * line against the level created by the matching `contract:created` * reservation. */ export declare interface FulfillmentShippedPayload { fulfillmentId: string; lines: FulfillmentShippedLine[]; } /** * Result handle returned by {@link installInventoryDispatchHandlers}. * Call `dispose()` to detach the subscribers (mainly useful in tests * and on graceful shutdown). */ export declare interface InstalledInventoryDispatchHandlers { /** Resolved StockService — useful for follow-up writes in the same scope. */ stockService: StockService; /** Detach every installed subscriber. Idempotent. */ dispose(): void; } /** * Subscribe a {@link StockService}-driven handler to the relevant signals * on the given {@link DispatchBus}. Returns a disposer for tests and * shutdown hooks. * * @example * ```typescript * import { createDispatchBus } from '@happyvertical/smrt-core'; * import { installInventoryDispatchHandlers } from '@happyvertical/smrt-inventory'; * * const bus = await createDispatchBus({ db }); * const handlers = await installInventoryDispatchHandlers({ * dispatchBus: bus, * db, * }); * ``` */ export declare function installInventoryDispatchHandlers(options: InstallInventoryDispatchHandlersOptions): Promise; /** * Options accepted by {@link installInventoryDispatchHandlers}. * * Provide either a pre-built {@link StockService} (when sharing one * across multiple subsystems) or a `db` for the helper to construct one * on first use. */ export declare type InstallInventoryDispatchHandlersOptions = { /** * Bus to subscribe on. Typically the app-wide `DispatchBus` created in * the application's `smrt.ts`. */ dispatchBus: DispatchBus; /** * When `true` (default), install the `contract:created` handler. * Producers should publish a {@link ContractCreatedPayload}. */ installContractReserved?: boolean; /** * When `true` (default), install the `fulfillment:shipped` handler. * Producers should publish a {@link FulfillmentShippedPayload}. */ installFulfillmentShipped?: boolean; } & ({ stockService: StockService; db?: DatabaseConfig; } | { db: DatabaseConfig; stockService?: undefined; }); /** * Thrown by {@link StockService.reserve} (and {@link StockService.fulfill}, * {@link StockService.transfer}) when the caller asks to move more stock * than the source state currently holds. Carries enough context for a * caller to surface a meaningful UI message and decide whether to retry, * backorder, or cancel. */ export declare class InsufficientStockError extends Error { readonly skuId: string; readonly locationId: string; readonly state: StockState; readonly requested: number; readonly available: number; name: string; constructor(skuId: string, locationId: string, state: StockState, requested: number, available: number); } export declare class InventoryLocation extends SmrtObject { /** Tenant scope. `null` means the location record is global. */ tenantId: string | null; /** * Short stable identifier (`'WH-EAST'`, `'STORE-42'`, `'IN-TRANSIT'`). * Together with `tenantId` this is the natural key. */ code: string; /** Display name for UIs. */ name: string; /** * Open-ended classifier (`'warehouse'`, `'factory'`, `'retail'`, * `'in_transit'`, `'virtual'`, or anything else your domain needs). * The framework never branches on this value. */ kind: InventoryLocationKind; /** * Optional plain-string reference to a `Place.id` in * `@happyvertical/smrt-places`. Cross-package id; intentionally not a * `@foreignKey()` so this package can be used without `smrt-places` * installed. */ placeId: string; /** Soft-active flag — inactive locations stay queryable for history. */ active: boolean; constructor(options?: InventoryLocationOptions); } export declare class InventoryLocationCollection extends SmrtCollection { static readonly _itemClass: typeof InventoryLocation; /** * Look up a location by its tenant-scoped `code`. Returns `null` when * no row matches. */ findByCode(code: string): Promise; /** * Find every location classified as the given kind (`'warehouse'`, * `'factory'`, `'retail'`, `'in_transit'`, …). */ findByKind(kind: InventoryLocationKind): Promise; /** * Find every location linked to a particular `Place.id` from * `@happyvertical/smrt-places`. Returns an empty array when no row * references the place. */ findByPlace(placeId: string): Promise; /** Find every active location, optionally narrowed by kind. */ findActive(kind?: InventoryLocationKind): Promise; } /** * Open-ended classifier for an InventoryLocation. The framework does not * special-case any value — pass whatever taxonomy your application needs. * * The strings listed here are conventions, not an exhaustive enum: * - `warehouse` — fulfillment warehouse * - `factory` — manufacturing site * - `retail` — storefront / point-of-sale * - `in_transit` — virtual location for stock that has left A but not yet * arrived at B; balances `transfer()` semantics * - `virtual` — any other non-physical bucket (returns staging, scrap, * consignment pool) */ export declare type InventoryLocationKind = string; /** * Options accepted by the {@link InventoryLocation} constructor. */ export declare interface InventoryLocationOptions extends SmrtObjectOptions { tenantId?: string | null; code?: string; name?: string; kind?: InventoryLocationKind; placeId?: string; active?: boolean; } export declare class StockLevel extends SmrtObject { /** Tenant scope. `null` means the level row is global. */ tenantId: string | null; /** Plain string reference to the {@link Sku} this row tracks. */ skuId: string; /** Plain string reference to the {@link InventoryLocation} this row tracks. */ locationId: string; /** Logical state — `available`, `allocated`, `wip`, `qc_hold`, `damaged`. */ state: StockState; /** * Current quantity. Fractional values are allowed (`= 0.0`) for * domains that count in units of measure other than whole pieces * (kilograms, litres, metres). */ qty: number; constructor(options?: StockLevelOptions); } export declare class StockLevelCollection extends SmrtCollection { static readonly _itemClass: typeof StockLevel; /** * Fetch the level row for a `(skuId, locationId, state)` tuple, or * `null` when the row has never been written. State defaults to * `'available'` because that is the common case (selling / picking * decisions are driven by available stock). */ getLevel(skuId: string, locationId: string, state?: StockState): Promise; /** * Return every level row for the given SKU across all locations and * states. Useful for "where is this SKU?" admin screens. */ findBySku(skuId: string): Promise; /** * Return every level row at the given location. Useful for "what is * in this warehouse?" reports. */ findByLocation(locationId: string): Promise; /** * Sum `qty` across all level rows for the given SKU. Pass `state` to * narrow the sum to one logical state (e.g. only `available`); * omit it for grand total across all states. */ totalForSku(skuId: string, state?: StockState): Promise; /** * Sum `qty` across all level rows at the given location, optionally * narrowed by state. */ totalForLocation(locationId: string, state?: StockState): Promise; } /** * Options accepted by the {@link StockLevel} constructor. */ export declare interface StockLevelOptions extends SmrtObjectOptions { tenantId?: string | null; skuId?: string; locationId?: string; state?: StockState; qty?: number; } export declare class StockMovement extends SmrtObject { /** Tenant scope. `null` means the movement is global. */ tenantId: string | null; /** Plain string reference to the {@link Sku} being moved. */ skuId: string; /** Plain string reference to the {@link InventoryLocation} being mutated. */ locationId: string; /** * Origin state for transitions (e.g. `available` → `allocated` for a * reservation). `null` indicates "no origin" — used when stock enters * the system fresh via {@link StockService.receive} or production. */ fromState: StockState | null; /** * Destination state. `null` indicates "no destination" — used for * fulfilment, where stock leaves the building entirely. */ toState: StockState | null; /** Quantity moved. Always positive; the direction is encoded by from/to. */ qty: number; /** * Why the movement happened (see {@link StockMovementReason}). Drawn * from the canonical list when emitted by {@link StockService}; free-form * strings are allowed for vertical-specific reasons. */ reasonCode: StockMovementReason; /** * Cross-package attribution tag — e.g. `'Contract'`, `'Fulfillment'`, * `'ProductionOrder'`, `'CycleCount'`. The package writing the * movement decides what tag makes sense; readers can group by * `(sourceType, sourceId)` to reconstruct "what caused this". */ sourceType: string; /** * Cross-package id of the row that caused this movement. Plain string; * the framework never dereferences it. */ sourceId: string; /** Optional free-form note shown in audit UIs. */ note: string; /** * When the movement happened. Set to `now` at write time; explicit * values are allowed when back-dating an import. */ occurredAt: Date; constructor(options?: StockMovementOptions); } export declare class StockMovementCollection extends SmrtCollection { static readonly _itemClass: typeof StockMovement; /** * Return every movement for the given SKU, newest first. Useful for a * per-SKU audit trail. */ findBySku(skuId: string): Promise; /** * Return every movement at the given location, newest first. Useful * for a per-warehouse audit trail. */ findByLocation(locationId: string): Promise; /** * Return every movement attributed to the given upstream source — for * example `findBySource('Contract', contract.id)` returns every * movement caused by the reservation/fulfilment/release of that * contract. Newest first. */ findBySource(sourceType: string, sourceId: string): Promise; /** * Return every movement with the given reason code (`'receipt'`, * `'reservation'`, `'adjustment'`, …). Newest first. */ findByReason(reasonCode: StockMovementReason): Promise; } /** * Options accepted by the {@link StockMovement} constructor. */ export declare interface StockMovementOptions extends SmrtObjectOptions { tenantId?: string | null; skuId?: string; locationId?: string; fromState?: StockState | null; toState?: StockState | null; qty?: number; reasonCode?: StockMovementReason; sourceType?: string; sourceId?: string; note?: string; occurredAt?: Date | string; } /** * Why a StockMovement was written. * * Each {@link StockService} method writes one movement with a `reasonCode` * drawn from this list. Free-form `string` is also accepted so consumers * can introduce vocabulary specific to their business (e.g. `'shrink'`, * `'sample'`, `'consignment_out'`) without forking the package. */ export declare type StockMovementReason = 'receipt' | 'reservation' | 'release' | 'fulfillment' | 'transfer_out' | 'transfer_in' | 'adjustment' | 'production_consume' | 'production_produce' | (string & {}); /** * Options shared by every {@link StockService} method that wants to leave * an audit attribution behind. Pairs neatly with the cross-package * pattern in {@link StockMovement.sourceType} / {@link StockMovement.sourceId}. */ export declare interface StockMutationOptions { /** Cross-package tag, e.g. `'Contract'`, `'Fulfillment'`, `'CycleCount'`. */ sourceType?: string; /** Cross-package id of the row that caused this mutation. */ sourceId?: string; /** Free-form note shown in audit UIs. */ note?: string; /** * Override the reason code stamped on the {@link StockMovement}. Each * method picks a sensible default; explicit overrides are useful when a * vertical wants to flag a more specific reason (e.g. `'return'` * instead of `'receipt'`). */ reasonCode?: StockMovementReason; } /** * Sanctioned stock-mutation surface. * * Construct via {@link createStockService} — the static factory wires up * the underlying collections and shares one database connection across * level reads and movement writes. * * @example * ```typescript * const service = await createStockService({ db }); * await service.receive(sku.id, warehouse.id, 100, { * sourceType: 'PurchaseOrder', * sourceId: po.id, * }); * await service.reserve(sku.id, warehouse.id, 10, { * sourceType: 'Contract', * sourceId: order.id, * }); * await service.fulfill(sku.id, warehouse.id, 10, { * sourceType: 'Fulfillment', * sourceId: fulfillment.id, * }); * ``` */ export declare class StockService { /** * The database config this service was bound to (URL string, config * object, or already-resolved `DatabaseInterface`). Exposed so * downstream services that compose StockService (e.g. BomService, * ProductionService in `@happyvertical/smrt-manufacturing`) can pass * the same value to their own collection factories without reaching * into private fields on the collections. */ readonly db: DatabaseConfig; readonly levels: StockLevelCollection; readonly movements: StockMovementCollection; readonly locations: InventoryLocationCollection; /** * Marks a service instance handed to a {@link withTransaction} * callback. Public mutation methods on a tx-bound instance skip * opening a nested transaction and just execute against the already- * bound collections; outer (non-tx) instances open a fresh * transaction per mutation. Internal flag — consumers never set it. */ private readonly inTransaction; private constructor(); /** Internal factory — prefer {@link createStockService}. */ static create(options: StockServiceOptions): Promise; /** * Run `work` inside a single database transaction with a tx-bound * {@link StockService} instance. All mutation calls on `tx` commit * atomically when `work` resolves and roll back if it throws. * * Use this when you need atomicity ACROSS multiple stock-service * calls — e.g. consuming materials for every line of a production * order in `@happyvertical/smrt-manufacturing`'s `ProductionService`, * or a custom workflow that reserves + fulfills + writes a custom * audit comment in one indivisible step. Individual mutation methods * (`receive`, `reserve`, etc.) are already atomic on their own — you * only need `withTransaction` for cross-call composition. * * Nesting is safe: calling `tx.withTransaction(...)` inside an * already-tx-bound callback simply runs the inner `work` on the same * transaction without opening a savepoint. * * When the underlying adapter does not expose `transaction()`, falls * through to a serial run on the regular collections with a one-time * warning. All four built-in adapters in `@happyvertical/sql >= 0.74.0` * support it; only test stubs would hit this branch. */ withTransaction(work: (tx: StockService) => Promise): Promise; /** * Internal: run a single-method mutation in a transaction. If we're * already inside one (the instance was handed to a `withTransaction` * callback), reuse it; otherwise open a fresh one. */ private runAtomically; /** * Add `qty` to available stock at the given location. Used for * purchase-order receipts, customer returns going back into available * inventory, and the "produce" leg of a production order. */ receive(skuId: string, locationId: string, qty: number, options?: StockMutationOptions): Promise; /** * Move `qty` from `available` to `allocated` at the given location. * Throws {@link InsufficientStockError} if available stock would go * negative. */ reserve(skuId: string, locationId: string, qty: number, options?: StockMutationOptions): Promise; /** * Move `qty` from `allocated` back to `available`. Used when a * reservation is cancelled and the previously-reserved stock should * go back into the available pool. */ release(skuId: string, locationId: string, qty: number, options?: StockMutationOptions): Promise; /** * Remove `qty` from `allocated` at the given location. Stock leaves * the building entirely (shipped, picked up, consumed). Throws * {@link InsufficientStockError} if allocated stock would go negative. */ fulfill(skuId: string, locationId: string, qty: number, options?: StockMutationOptions): Promise; /** * Move `qty` of `available` stock from `fromLocationId` to * `toLocationId`. Writes two movement rows — one for the `transfer_out` * leg, one for the `transfer_in` leg — so the audit log preserves the * lineage in both directions. Throws {@link InsufficientStockError} if * source available stock would go negative. * * Both legs (level writes + movement rows) run inside one transaction * — a failure mid-`transfer` rolls back the source debit so there's no * "ghost stock disappearance" (source decremented, destination never * credited). */ transfer(skuId: string, fromLocationId: string, toLocationId: string, qty: number, options?: StockMutationOptions): Promise; /** * Apply a positive or negative `delta` to a level row. Used for cycle * counts and one-off corrections; `delta=+5` adds five units, * `delta=-2` removes two. By default the adjustment targets * `available` stock; pass an explicit `state` to adjust a different * bucket (e.g. `'damaged'` after a quality-control reclassification). * * Adjusting by `0` is rejected as a probable programming error — the * caller almost always meant a non-zero delta and a no-op write would * still cost an audit row. */ adjust(skuId: string, locationId: string, delta: number, options?: StockMutationOptions & { state?: StockState; }): Promise; } /** * Options accepted by the {@link StockService} factory. */ export declare interface StockServiceOptions { /** * Database to read/write through. Accepts the same shapes that * `SmrtCollection.create({ db })` accepts — a `DatabaseInterface`, a * connection-string URL, or a `{ type, url }` config object. Reused by * the internal collections so the service, level reads, and movement * writes always hit the same connection / pool. */ db: DatabaseConfig; } /** * Logical state of a quantity of stock at a `(skuId, locationId)` pair. * * StockLevel rows are tuples of `(skuId, locationId, state)`, so a single * SKU at a single location can simultaneously have non-zero quantities in * several states (e.g. 50 available, 10 allocated, 3 damaged). * * - `available` — on hand and free to allocate. * - `allocated` — reserved against a contract, order, or production plan; * physically still on site but no longer free to sell. * - `wip` — work-in-progress: consumed materials inside an active * production order, not yet emitted as finished goods. * - `qc_hold` — held pending quality control; not available to allocate * or ship until released. * - `damaged` — damaged or otherwise unsellable; kept on the books for * shrinkage accounting until written off. */ export declare type StockState = 'available' | 'allocated' | 'wip' | 'qc_hold' | 'damaged'; export { }