@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.
- package/AGENTS.md +121 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +213 -0
- package/dist/index.d.ts +557 -0
- package/dist/index.js +922 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +1023 -0
- package/dist/smrt-knowledge.json +618 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @happyvertical/smrt-inventory
|
|
2
|
+
|
|
3
|
+
Multi-location stock tracking. Strictly industry-neutral — the same primitives serve apparel, furniture, automotive, CPG, electronics, and any other vertical that counts discrete units across locations.
|
|
4
|
+
|
|
5
|
+
## Models
|
|
6
|
+
|
|
7
|
+
| Model | Purpose |
|
|
8
|
+
|---|---|
|
|
9
|
+
| _(catalog shapes — `Product`, `Material`, `ProductVariant`, `Sku` — live in `@happyvertical/smrt-products`)_ | Sku is the smallest sellable / countable unit; its `attributes` JSON carries axis-value pins. ProductVariant is the per-product axis declaration. Import either from `@happyvertical/smrt-products` directly. |
|
|
10
|
+
| `InventoryLocation` | Warehouse / factory / retail / in-transit / virtual. Open-ended `kind` string. Optional `placeId` references `@happyvertical/smrt-places`. `conflictColumns: ['code', 'tenant_id']`. |
|
|
11
|
+
| `StockLevel` | Materialized `qty` for a `(skuId, locationId, state)` tuple. Upserted in place. **Mutated only by `StockService`.** States: `available`, `allocated`, `wip`, `qc_hold`, `damaged`. |
|
|
12
|
+
| `StockMovement` | Append-only audit log. Every `StockService` mutation writes one (or two for `transfer`). `sourceType` + `sourceId` carry cross-package attribution. |
|
|
13
|
+
|
|
14
|
+
All three inventory models (`InventoryLocation`, `StockLevel`, `StockMovement`) use `@TenantScoped({ mode: 'optional' })` + nullable `tenantId` so they can be used either tenant-scoped or globally. The catalog shapes (`Product`, `Material`, `ProductVariant`, `Sku`) live in `@happyvertical/smrt-products` and carry their own tenant decoration there.
|
|
15
|
+
|
|
16
|
+
## StockService — the only sanctioned way to mutate stock
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createStockService } from '@happyvertical/smrt-inventory';
|
|
20
|
+
|
|
21
|
+
const service = await createStockService({ db });
|
|
22
|
+
await service.receive(skuId, locationId, qty, { sourceType, sourceId });
|
|
23
|
+
await service.reserve(skuId, locationId, qty, { sourceType, sourceId });
|
|
24
|
+
await service.release(skuId, locationId, qty, { sourceType, sourceId });
|
|
25
|
+
await service.fulfill(skuId, locationId, qty, { sourceType, sourceId });
|
|
26
|
+
await service.transfer(skuId, fromLocId, toLocId, qty, { sourceType, sourceId });
|
|
27
|
+
await service.adjust(skuId, locationId, delta, { sourceType, sourceId });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Method | Behavior | Movement reason |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `receive` | +qty available — purchase receipt, return, production produce | `receipt` |
|
|
33
|
+
| `reserve` | available → allocated. Throws `InsufficientStockError` on overdraw | `reservation` |
|
|
34
|
+
| `release` | allocated → available (order cancel) | `release` |
|
|
35
|
+
| `fulfill` | -qty allocated. Stock leaves the building | `fulfillment` |
|
|
36
|
+
| `transfer` | move stock between locations (writes two movements) | `transfer_out`, `transfer_in` |
|
|
37
|
+
| `adjust` | signed delta — cycle counts and one-off corrections | `adjustment` |
|
|
38
|
+
|
|
39
|
+
All methods reject zero / negative / non-finite quantities except `adjust`, which accepts non-zero signed deltas. Negative deltas that would drive a state below zero throw `InsufficientStockError`.
|
|
40
|
+
|
|
41
|
+
## Opt-in DispatchBus hooks
|
|
42
|
+
|
|
43
|
+
Off by default. Wire them up explicitly in the application's `smrt.ts`:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { installInventoryDispatchHandlers } from '@happyvertical/smrt-inventory';
|
|
47
|
+
|
|
48
|
+
const handlers = await installInventoryDispatchHandlers({
|
|
49
|
+
dispatchBus: bus,
|
|
50
|
+
db, // or stockService: existingService
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This subscribes to:
|
|
55
|
+
- `contract:created` → calls `service.reserve()` for every line inside one `stockService.withTransaction(...)`, attributed to `('Contract', payload.contractId)`
|
|
56
|
+
- `fulfillment:shipped` → calls `service.fulfill()` for every line inside one `stockService.withTransaction(...)`, attributed to `('Fulfillment', payload.fulfillmentId)`
|
|
57
|
+
|
|
58
|
+
Both handlers are atomic across lines: a shortfall on line N rolls back lines 1..N-1 so the event is all-or-nothing. Without this guarantee, `DispatchBus` would only log the async handler error and a partially-reserved (or partially-fulfilled) contract would have no compensating event to fix it.
|
|
59
|
+
|
|
60
|
+
Per-handler toggles: `installContractReserved`, `installFulfillmentShipped`. The `production_order:posted` hook is deliberately not installed here — it depends on the BOM model and ships in `@happyvertical/smrt-manufacturing` (issue #1250).
|
|
61
|
+
|
|
62
|
+
## Schema migration (Phase 1 release)
|
|
63
|
+
|
|
64
|
+
The `Sku` model moved out of this package and into `@happyvertical/smrt-products`. Previously it lived in this package's `inventory_skus` table; it now lives in `product_skus` over there.
|
|
65
|
+
|
|
66
|
+
**Upgrade procedure** for deployed consumers:
|
|
67
|
+
|
|
68
|
+
1. **Let the framework create the destination table first.** Boot the new version once with `@happyvertical/smrt-products` registered in your `SmrtClassOptions`; the lazy `syncSchema` path will create `product_skus` with the right primary key, NOT NULL, UNIQUE (`code`, `tenant_id`), and indexes — they're derived from the `Sku` model decorators and would be stripped by a raw `CREATE TABLE AS SELECT`.
|
|
69
|
+
|
|
70
|
+
2. **Idempotently copy rows across.** SQLite + Postgres:
|
|
71
|
+
|
|
72
|
+
```sql
|
|
73
|
+
BEGIN;
|
|
74
|
+
INSERT INTO product_skus (
|
|
75
|
+
id, slug, context, created_at, updated_at,
|
|
76
|
+
tenant_id, product_id, code, barcode, name,
|
|
77
|
+
attributes, weight_grams, parent_sku_id, active
|
|
78
|
+
)
|
|
79
|
+
SELECT
|
|
80
|
+
id, slug, context, created_at, updated_at,
|
|
81
|
+
tenant_id, product_id, code, barcode, name,
|
|
82
|
+
attributes, weight_grams, parent_sku_id, active
|
|
83
|
+
FROM inventory_skus
|
|
84
|
+
WHERE NOT EXISTS (
|
|
85
|
+
SELECT 1 FROM product_skus p WHERE p.id = inventory_skus.id
|
|
86
|
+
);
|
|
87
|
+
COMMIT;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
3. **Drop the legacy table** once you've verified row counts match:
|
|
91
|
+
|
|
92
|
+
```sql
|
|
93
|
+
DROP TABLE IF EXISTS inventory_skus;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`StockLevel.skuId` and `StockMovement.skuId` carry plain string ids that still resolve to the same rows after the rename, so no inventory data has to move.
|
|
97
|
+
|
|
98
|
+
## Gotchas
|
|
99
|
+
|
|
100
|
+
- **Movements are append-only.** The materialized `StockLevel` row is derived state; the `StockMovement` ledger is the source of truth. Never let CRUD UIs expose update/delete on the movement table. If you find yourself wanting to "fix" a movement row, write a compensating `adjust()` call instead.
|
|
101
|
+
- **Never call `StockLevel.create()` or `save()` directly.** Going around `StockService` silently desyncs the audit log and breaks cycle counts.
|
|
102
|
+
- **Cross-industry constraint.** This package's vocabulary stays generic: `InventoryLocation`, `StockLevel`, `StockMovement`. Apparel-specific concepts (`Style`, `Makeup`, `Colorway`, fashion `Season`, tech-pack metadata) and their analogues in furniture / automotive / CPG live in the relevant template package, never here. PRs that introduce industry vocabulary should be rejected.
|
|
103
|
+
- **Cross-package references are plain strings.** `skuId`, `placeId`, `sourceId` — all plain string ids, never `@foreignKey()`. Inventory's stock-motion logic only ever reads StockLevel/StockMovement; it doesn't follow `skuId` back to the catalog's Sku table, so the layering stays loose.
|
|
104
|
+
- **`conflictColumns` include `tenant_id`** on `InventoryLocation` and `StockLevel`. NULL-matching semantics (`(code, NULL) IS NOT DISTINCT FROM (code, NULL)`) are handled by `@happyvertical/sql >= 0.74.0` natively, so two saves with the same `(code, NULL)` tuple correctly merge in place. Pass `nullsDistinct: true` at the sql layer to opt back into NULL-distinct semantics.
|
|
105
|
+
- **Atomic per-method, transactional across composition.** Each `StockService` mutation (`receive` / `reserve` / `release` / `fulfill` / `transfer` / `adjust`) wraps every write in a single `db.transaction(...)` (via `@happyvertical/sql >= 0.74.0`). Partial failure rolls the whole call back — level writes and the matching `StockMovement` audit row commit together or not at all. `transfer` writes both legs (source decrement, destination increment, and both audit rows) inside one tx, so a failure mid-`transfer` is fully reverted.
|
|
106
|
+
Callers composing multiple `StockService` calls into one logical unit (e.g. consuming materials line-by-line for a production order) should wrap them in `await stockService.withTransaction(async (tx) => { ... })` — `tx` is a tx-bound `StockService` with the same public API; mutation calls inside the callback share one transaction and either all commit or all roll back.
|
|
107
|
+
If the underlying SQL adapter does not expose `transaction()` (extremely rare — all four built-in `@happyvertical/sql >= 0.74.0` adapters implement it), the service degrades to non-atomic serial writes and emits a one-time `console.warn`.
|
|
108
|
+
- **Concurrent reservations: tightened, not bulletproof.** `@happyvertical/sql >= 0.74.0` serialises null-aware *upserts* via a per-key in-process lock (SQLite) or advisory lock (Postgres). That lock fixed the row-creation race (issue #1246) — two concurrent `create()` calls on the same conflict-column tuple no longer race the underlying `INSERT ... ON CONFLICT`. It does NOT serialise the read-modify-write compound that `reserve` / `fulfill` / `transfer` / `adjust` perform on an existing row: each method reads the current level (`SELECT`), computes the new value in JS, then writes it back (`UPDATE` via `save()` on the loaded row). The whole compound is inside one `db.transaction(...)` from round-3, so the level write and audit row commit together — but two concurrent transactions on the same `(skuId, locationId)` can each see the same starting balance, compute the same decrement, and double-spend. Awaited (serial) calls always behave correctly. Use a job queue (e.g. `@happyvertical/smrt-jobs`) or your own mutex when you need hard atomicity across concurrent callers. The full fix is `SELECT ... FOR UPDATE` inside the same transaction (Postgres) or upgrading the journal mode (SQLite); deferred until we see a real high-contention workload that warrants it.
|
|
109
|
+
|
|
110
|
+
## Source attribution
|
|
111
|
+
|
|
112
|
+
Every `StockMovement` carries `sourceType` + `sourceId` so downstream queries can reconstruct "what caused this movement". Conventions used by the built-in dispatch handlers:
|
|
113
|
+
|
|
114
|
+
| sourceType | Emitter | Note |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `Contract` | `smrt-commerce` `contract:created` | All reservation/release/fulfilment legs caused by a contract |
|
|
117
|
+
| `Fulfillment` | `smrt-commerce` `fulfillment:shipped` | Outbound shipment |
|
|
118
|
+
| `PurchaseOrder` | (your code) | Inbound receipt |
|
|
119
|
+
| `CycleCount` | (your code) | Adjustments from physical counts |
|
|
120
|
+
| `TransferOrder` | (your code) | Both legs of a transfer |
|
|
121
|
+
| `ProductionOrder` | `smrt-manufacturing` (issue #1250) | Materials consumed + finished goods produced |
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# @happyvertical/smrt-inventory
|
|
2
|
+
|
|
3
|
+
Multi-location stock tracking for the SMRT framework. Strictly industry-neutral — the same primitives serve apparel, furniture, automotive, CPG, electronics, and any other vertical that counts discrete units across locations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @happyvertical/smrt-inventory
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Set up SKUs, locations, and a stock service
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import {
|
|
17
|
+
createStockService,
|
|
18
|
+
InventoryLocationCollection,
|
|
19
|
+
} from '@happyvertical/smrt-inventory';
|
|
20
|
+
// The `@happyvertical/smrt-products` root entry pulls in Vite virtual
|
|
21
|
+
// modules (`@smrt/client` etc.) and won't resolve under plain Node /
|
|
22
|
+
// tsx. Import from the `/collections` subpath for server-side scripts,
|
|
23
|
+
// tests, and SSR runtimes that don't run the Vite plugin.
|
|
24
|
+
import { SkuCollection } from '@happyvertical/smrt-products/collections';
|
|
25
|
+
|
|
26
|
+
const db = { type: 'sqlite', url: 'app.db' };
|
|
27
|
+
const skus = await SkuCollection.create({ db });
|
|
28
|
+
const locations = await InventoryLocationCollection.create({ db });
|
|
29
|
+
const stock = await createStockService({ db });
|
|
30
|
+
|
|
31
|
+
const widget = await skus.create({
|
|
32
|
+
productId: 'prod-1',
|
|
33
|
+
code: 'WIDGET-001',
|
|
34
|
+
barcode: '0123456789012',
|
|
35
|
+
attributes: { finish: 'matte' },
|
|
36
|
+
});
|
|
37
|
+
await widget.save();
|
|
38
|
+
|
|
39
|
+
const warehouse = await locations.create({
|
|
40
|
+
code: 'WH-EAST',
|
|
41
|
+
name: 'Warehouse East',
|
|
42
|
+
kind: 'warehouse',
|
|
43
|
+
});
|
|
44
|
+
await warehouse.save();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Move stock through its lifecycle
|
|
48
|
+
|
|
49
|
+
Every method writes one (or two, for transfers) `StockMovement` audit rows so the ledger stays in lockstep with the materialized levels.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Inbound receipt — +qty available.
|
|
53
|
+
await stock.receive(widget.id!, warehouse.id!, 100, {
|
|
54
|
+
sourceType: 'PurchaseOrder',
|
|
55
|
+
sourceId: po.id,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Reserve against an order — available → allocated.
|
|
59
|
+
// Throws InsufficientStockError if there isn't enough available.
|
|
60
|
+
await stock.reserve(widget.id!, warehouse.id!, 10, {
|
|
61
|
+
sourceType: 'Contract',
|
|
62
|
+
sourceId: order.id,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Ship — removes from allocated, leaves the building.
|
|
66
|
+
await stock.fulfill(widget.id!, warehouse.id!, 10, {
|
|
67
|
+
sourceType: 'Fulfillment',
|
|
68
|
+
sourceId: shipment.id,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Cycle count caught five extra units — non-zero signed delta.
|
|
72
|
+
await stock.adjust(widget.id!, warehouse.id!, 5, {
|
|
73
|
+
sourceType: 'CycleCount',
|
|
74
|
+
sourceId: count.id,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Move stock between locations — writes transfer_out + transfer_in legs.
|
|
78
|
+
await stock.transfer(widget.id!, warehouse.id!, store.id!, 12, {
|
|
79
|
+
sourceType: 'TransferOrder',
|
|
80
|
+
sourceId: xfer.id,
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Query balances and the audit log
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import {
|
|
88
|
+
StockLevelCollection,
|
|
89
|
+
StockMovementCollection,
|
|
90
|
+
} from '@happyvertical/smrt-inventory';
|
|
91
|
+
|
|
92
|
+
const levels = await StockLevelCollection.create({ db });
|
|
93
|
+
const movements = await StockMovementCollection.create({ db });
|
|
94
|
+
|
|
95
|
+
// What's on hand at this location across every state?
|
|
96
|
+
const here = await levels.findByLocation(warehouse.id!);
|
|
97
|
+
|
|
98
|
+
// What's the available total for a SKU across all locations?
|
|
99
|
+
const availableTotal = await levels.totalForSku(widget.id!, 'available');
|
|
100
|
+
|
|
101
|
+
// What movements were caused by a particular contract?
|
|
102
|
+
const audit = await movements.findBySource('Contract', order.id);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Catch the insufficient-stock error
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { InsufficientStockError } from '@happyvertical/smrt-inventory';
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await stock.reserve(widget.id!, warehouse.id!, 9999);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (err instanceof InsufficientStockError) {
|
|
114
|
+
console.log(
|
|
115
|
+
`Only ${err.available} available for ${err.skuId} at ${err.locationId}, requested ${err.requested}`,
|
|
116
|
+
);
|
|
117
|
+
} else {
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Multi-tenancy
|
|
124
|
+
|
|
125
|
+
The three inventory models (`InventoryLocation`, `StockLevel`, `StockMovement`) use `@TenantScoped({ mode: 'optional' })` with a nullable `tenantId`. The catalog shapes (`Sku`, `Product`, `ProductVariant`, `Material`) live in `@happyvertical/smrt-products` and carry their own tenant decoration there; the cross-package id refs flow through unchanged. Wrap mutations in `withTenant()` from `@happyvertical/smrt-tenancy` to scope queries automatically.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { withTenant } from '@happyvertical/smrt-tenancy';
|
|
129
|
+
|
|
130
|
+
await withTenant({ tenantId: 'tenant-a' }, async () => {
|
|
131
|
+
// Every read/write through skus, locations, levels, movements, and the
|
|
132
|
+
// StockService is filtered by tenant_id = 'tenant-a'.
|
|
133
|
+
await stock.receive(widget.id!, warehouse.id!, 100);
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Opt-in DispatchBus wiring
|
|
138
|
+
|
|
139
|
+
The package ships handlers that bridge `contract:created` → `reserve()` and `fulfillment:shipped` → `fulfill()`. Off by default; install them explicitly in your `smrt.ts` to enable automatic stock motion:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { createDispatchBus } from '@happyvertical/smrt-core';
|
|
143
|
+
import { installInventoryDispatchHandlers } from '@happyvertical/smrt-inventory';
|
|
144
|
+
|
|
145
|
+
const bus = await createDispatchBus({ db });
|
|
146
|
+
const handlers = await installInventoryDispatchHandlers({
|
|
147
|
+
dispatchBus: bus,
|
|
148
|
+
db,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// In smrt-commerce (or your own code):
|
|
152
|
+
await bus.emit('contract:created', {
|
|
153
|
+
contractId: order.id,
|
|
154
|
+
lines: [{ skuId, locationId, qty }],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Later, on shipment:
|
|
158
|
+
await bus.emit('fulfillment:shipped', {
|
|
159
|
+
fulfillmentId: shipment.id,
|
|
160
|
+
lines: [{ skuId, locationId, qty }],
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Per-handler toggles (`installContractReserved`, `installFulfillmentShipped`) let consumers pick exactly the signals they care about. The `production_order:posted` handler is intentionally *not* installed here; that bridge lives in `@happyvertical/smrt-manufacturing`.
|
|
165
|
+
|
|
166
|
+
## API
|
|
167
|
+
|
|
168
|
+
### Models
|
|
169
|
+
|
|
170
|
+
| Export | Description |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `InventoryLocation` | Physical or virtual stocking site with open-ended `kind`. |
|
|
173
|
+
| `StockLevel` | Materialized `(skuId, locationId, state) → qty` row. **Never mutate directly — use `StockService`.** |
|
|
174
|
+
| `StockMovement` | Append-only audit row. One per mutation; two for transfers. |
|
|
175
|
+
|
|
176
|
+
### Collections
|
|
177
|
+
|
|
178
|
+
| Export | Description |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `InventoryLocationCollection` | `findByCode`, `findByKind`, `findByPlace`, `findActive` |
|
|
181
|
+
| `StockLevelCollection` | `getLevel`, `findBySku`, `findByLocation`, `totalForSku`, `totalForLocation` |
|
|
182
|
+
| `StockMovementCollection` | `findBySku`, `findByLocation`, `findBySource`, `findByReason` |
|
|
183
|
+
|
|
184
|
+
All catalog shapes (`Product`, `Material`, `ProductVariant`, `Sku`) live in [`@happyvertical/smrt-products`](../products). This package adds the stock-motion layer (`InventoryLocation`, `StockLevel`, `StockMovement`, `StockService`) on top.
|
|
185
|
+
|
|
186
|
+
### Service
|
|
187
|
+
|
|
188
|
+
| Export | Description |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `StockService` | The only sanctioned mutation surface. Six methods: `receive`, `reserve`, `release`, `fulfill`, `transfer`, `adjust`. |
|
|
191
|
+
| `createStockService({ db })` | Convenience factory. |
|
|
192
|
+
| `InsufficientStockError` | Thrown by `reserve` / `fulfill` / `transfer` / negative `adjust` when stock would go negative. Carries `skuId`, `locationId`, `state`, `requested`, `available`. |
|
|
193
|
+
| `installInventoryDispatchHandlers({ dispatchBus, db })` | Opt-in DispatchBus wiring for `contract:created` and `fulfillment:shipped`. |
|
|
194
|
+
|
|
195
|
+
### Types
|
|
196
|
+
|
|
197
|
+
| Export | Description |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `StockState` | `'available' \| 'allocated' \| 'wip' \| 'qc_hold' \| 'damaged'` |
|
|
200
|
+
| `InventoryLocationKind` | Open-ended classifier string |
|
|
201
|
+
| `StockMovementReason` | Canonical reason vocabulary (`'receipt'`, `'reservation'`, `'release'`, `'fulfillment'`, `'transfer_out'`, `'transfer_in'`, `'adjustment'`, `'production_consume'`, `'production_produce'`) plus free-form strings |
|
|
202
|
+
|
|
203
|
+
## Dependencies
|
|
204
|
+
|
|
205
|
+
| Package | Purpose |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `@happyvertical/smrt-core` | SmrtObject / SmrtCollection / DispatchBus |
|
|
208
|
+
| `@happyvertical/smrt-tenancy` | Optional tenant scoping |
|
|
209
|
+
| `@happyvertical/sql` | Database adapter |
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|