Skip to main content

Inventory

inventory tracks the stock level of one product in one store. A unique compound index on (store, "product._id") guarantees exactly one record per store+product pair. Records are created and updated by the inventoryManager utility (addStock, removeStock, transferStock), not directly by the acts.

// models/inventory.ts (definition, trimmed of the doc comment)
import { coreApp } from "../mod.ts";
import {
coerce, date, defaulted, number, optional,
type RelationDataType, type RelationSortOrderType, string,
} from "lesan";
import { createUpdateAt } from "@lib";
import { product_excludes, store_excludes, user_excludes } from "./excludes.ts";

export const inventory_pure = {
quantity: defaulted(number(), 0),
minQuantity: optional(number()),
maxQuantity: optional(number()),
batchNo: optional(string()),
expirationDate: optional(coerce(date(), string(), (value) => new Date(value))),
location: optional(string()),
...createUpdateAt,
};

export const inventory_relations = {
store: {
schemaName: "store",
type: "single" as RelationDataType,
optional: false,
excludes: store_excludes,
relatedRelations: {
inventories: { type: "multiple" as RelationDataType, limit: 50, sort: { field: "_id", order: "desc" as RelationSortOrderType } },
},
},
product: {
schemaName: "product",
type: "single" as RelationDataType,
optional: false,
excludes: product_excludes,
relatedRelations: {
inventories: { type: "multiple" as RelationDataType, limit: 50, sort: { field: "_id", order: "desc" as RelationSortOrderType } },
},
},
lastCountedBy: {
schemaName: "user",
type: "single" as RelationDataType,
optional: true,
excludes: user_excludes,
relatedRelations: {
countedInventories: { type: "multiple" as RelationDataType, limit: 50, sort: { field: "_id", order: "desc" as RelationSortOrderType } },
},
},
};

Pure fieldsโ€‹

FieldTypeNotes
quantitydefaulted(number(), 0)current stock on hand
minQuantityoptional(number())re-order point hint
maxQuantityoptional(number())
batchNooptional(string())lot tracking
expirationDateoptional(coerce(date(), string()))for perishables
locationoptional(string())shelf/bin reference
createdAt / updatedAtspread from createUpdateAt

Relationsโ€‹

RelationTargetTypeBack-reference
storestoresingle (required)store.inventories
productproductsingle (required)product.inventories
lastCountedByusersingle (optional)user.countedInventories

The compound indexโ€‹

export const inventories = () =>
coreApp.odm.newModel("inventory", inventory_pure, inventory_relations);

export const createInventoryIndex = async () => {
const collection = coreApp.odm.getCollection("inventory");
try {
await collection.createIndex({ store: 1, "product._id": 1 }, { unique: true });
} catch (error) {
console.error("Inventory compound index already exists or creation failed:", (error as Error).message);
}
};

createInventoryIndex() is called at boot (from mod.ts, right after registration). The { store: 1, "product._id": 1 } compound unique index is the data-model guarantee that powers the whole inventory manager: there can only ever be one row for a store+product, so addStock on a fresh pair inserts and on an existing pair updates.

Note the index targets "product._id" โ€” a dotted path inside the embedded relation snapshot โ€” because the product field is an embedded pure projection of the product document, not a stored ObjectId.

In the workflowโ€‹

Run itโ€‹

curl -X POST http://localhost:1380/lesan \
-H "Content-Type: application/json" \
-H "token: <jwt>" \
-d '{
"service": "main",
"model": "inventory",
"act": "getInventories",
"details": {
"set": { "page": 1 },
"get": { "quantity": true, "minQuantity": true, "store": { "name": true }, "product": { "name": true } }
}
}'

Errors & fixesโ€‹

ErrorCauseFix
E11000 duplicate key error on compound indextwo records for the same store+productexpected if you bypass inventoryManager; the manager upserts instead
Inventory not found for this store and productremoveStock/transferStock on a store+product with no recordseed stock first via addStock
note

Runtime On npm/Bun import the framework from @hemedani/lesan; on Deno from jsr:@hemedani/lesan. The repo app itself uses the lesan path alias.