Skip to main content

transferStock

transferStock moves a quantity of one product from one store to another. It works on the inventory and stockMovement models. You need it when goods physically move between warehouses or shelves: the act removes stock from the source store and adds it to the destination store in a single atomic-ish unit, writing two ledger entries (transfer_out and transfer_in) so the movement is fully auditable.

The validator (transferStock.val.ts)โ€‹

The set takes two store ids โ€” fromStoreId and toStoreId โ€” plus productId and quantity. That's it: no reason or reference fields, because the reasons are fixed by the implementation ("transfer_out" / "transfer_in") and the reference is always the other store. activeRoleId comes from @lib for grantAccess; get is a one-level inventory projection.

import { number, object, objectIdValidation } from "lesan";
import { selectStruct } from "../../../mod.ts";
import { activeRoleMixin } from "@lib";

export const transferStockValidator = () => {
return object({
set: object({
...activeRoleMixin,
fromStoreId: objectIdValidation,
toStoreId: objectIdValidation,
productId: objectIdValidation,
quantity: number(),
}),
get: selectStruct("inventory", 1),
});
};
note

About the imports All framework imports in this repo use the "lesan" alias. In your own app, import the same names from @hemedani/lesan (npm/Bun) or jsr:@hemedani/lesan (Deno). @lib points at the app's utils/ folder.

The implementation (transferStock.fn.ts)โ€‹

Another thin delegator. It destructures the four set fields, grabs the user from context, and forwards everything to the shared transferStock helper. Note there's no options object here โ€” the helper builds its own transfer descriptions internally.

import { type ActFn } from "lesan";
import { transferStock as transferStockUtil } from "@lib";
import { coreApp } from "../../../mod.ts";
import type { MyContext } from "@lib";

export const transferStockFn: ActFn = async (body) => {
const {
set: { fromStoreId, toStoreId, productId, quantity },
} = body.details;

const { user }: MyContext = coreApp.contextFns.getContextModel() as MyContext;

return await transferStockUtil(
fromStoreId as string,
toStoreId as string,
productId as string,
quantity as number,
user._id.toString(),
);
};

How it's registered (mod.ts)โ€‹

import { grantAccess, setTokens, setUser } from "@lib";
import { coreApp } from "../../../mod.ts";
import { transferStockFn } from "./transferStock.fn.ts";
import { transferStockValidator } from "./transferStock.val.ts";

export const transferStockSetup = () =>
coreApp.acts.setAct({
schema: "inventory",
actName: "transferStock",
preAct: [setTokens, setUser, grantAccess([{ roles: ["Manager", "Admin", "StoreHead"] }])],
validator: transferStockValidator(),
fn: transferStockFn,
});

Same Manager / Admin / StoreHead gate as the other mutating inventory acts.

In the workflowโ€‹

transferStock is the composition act โ€” the inventoryManager helper literally calls removeStock (as "transfer_out", referencing the destination) and then addStock (as "transfer_in", referencing the source). The net quantity across both stores is unchanged, so it's the inventory move that keeps the ledger balanced. Both ledger rows show up in getStockMovements. See the overview for context.

Run itโ€‹

curl -X POST http://localhost:1380/lesan \
-H "Content-Type: application/json" \
-H "token: <your-token>" \
-d '{
"service": "main",
"model": "inventory",
"act": "transferStock",
"details": {
"set": {
"activeRoleId": "<role-id>",
"fromStoreId": "<store-id>",
"toStoreId": "<store-id>",
"productId": "<product-id>",
"quantity": 10
},
"get": { "_id": 1, "quantity": 1 }
}
}'

The body of the response is whatever the helper returned, e.g. { success: true, productId, quantity, fromStoreId, toStoreId }.

Errors & fixesโ€‹

ErrorWhat it meansHow to fix
you should send your id with token key in req headersetTokens found no token headerAdd -H "token: <your-jwt>" to the request
Invalid or expired tokenJWT verification failedLog in again and use the fresh token
Invalid or missing token dataToken payload has no _idUse a token produced by the app's login act
user not existsetUser couldn't find the user docThe token references a deleted user โ€” log in again
activeRoleId is requiredgrantAccess needs the active roleSend activeRoleId in details.set (unless the user isGhost)
Active role not foundThe activeRoleId isn't among the user's rolesPass one of the role ids on the user document
You cant do thisThe active role is not Manager, Admin, or StoreHeadSwitch to an allowed role or use a ghost admin
Inventory not found for this store and productThe source store has no inventory record for the productaddStock to the source store first
Insufficient inventory quantityquantity exceeds the source store's stockReduce the quantity or restock the source store
Invalid ObjectId / validator failureAny of the three ids isn't a valid 24-char hex stringCheck the ids you're sending