Ingredient Stock Architecture
Overview
Ingredient stock uses one shared movement model for every stock-changing action. The admin console and store app can create stock movements in different ways, but they all write the same two records:
| Record | Purpose |
|---|---|
| Ingredient Inventory | The current stock for one ingredient at one outlet. |
| Ingredient Transaction | The append-only ledger entry showing what changed, why, and the stock before and after. |
The ledger is the history. The inventory row is the latest count.
Stock movement flow
User action
-> API route
-> Ingredient stock movement service
-> validate movement type and reason
-> create Ingredient Transaction
-> update Ingredient Inventory current stockThis means stock changes are calculated in one place instead of separately inside each screen.
What creates stock movements
| User action | Movement written |
|---|---|
| Create ingredient inventory with a starting count | ADJUSTMENT / inventory_count from 0 to the starting stock. |
| Edit ingredient inventory current stock | ADJUSTMENT / inventory_count from the previous count to the new count. |
| Delete ingredient inventory with non-zero stock | ADJUSTMENT / inventory_count to bring stock to 0, then the inventory row is removed. |
| Create a manual ingredient transaction | IN, OUT, or ADJUSTMENT, depending on the selected type and reason. |
| Receive an ingredient stock order | IN / purchase for each received item. |
Movement rules
Each transaction type only accepts reasons that match that type.
| Type | Valid examples |
|---|---|
| IN | purchase, delivery, return, transfer in, production |
| OUT | consumption, waste, expiry, damage, transfer out, theft |
| ADJUSTMENT | inventory count, correction, system adjustment, quality check |
For example, an OUT movement cannot use the reason purchase, and an IN movement cannot use the reason consumption.
Inventory edit rules
Once an ingredient inventory row exists, its outlet and ingredient are locked. Only the current stock count can be changed.
This prevents an old inventory row from being reused for a different outlet or ingredient, which would make old ledger entries point at the wrong stock record.
Order receiving rules
When a stock order is received:
- Each received line creates an
IN / purchasetransaction. - The inventory count for that outlet and ingredient increases.
- A line cannot receive more than the remaining ordered quantity.
- Receiving an already finalized order is blocked.
- If only part of the order is received, the order stays partially received until the remaining quantities are received.
Delete guards
Rewardly blocks deletes that would create broken references:
| Delete action | Blocked when |
|---|---|
| Delete supplier | An ingredient or stock order still references the supplier. |
| Delete ingredient bundle | A product or product variant still references the bundle. |
These guards prevent new missing-reference data. They do not repair old records that were already missing references before the guard existed.
Audit script
The API service includes a read-only audit for checking existing data:
NODE_ENV=production npm run audit:ingredient-stock
NODE_ENV=production npm run audit:ingredient-stock -- --merchant=<merchant-id-or-subdomain>
NODE_ENV=production npm run audit:ingredient-stock -- --json
NODE_ENV=production npm run audit:ingredient-stock -- --fail-on-issuesThe audit does not write data. It checks for:
- inventory counts that do not match the latest transaction
- inventory-count transactions pointing at the wrong inventory row
- invalid transaction type/reason pairs
- over-received stock order lines
- ingredients with missing suppliers
- products with missing ingredient bundles
- bundles with missing ingredients
Use the audit before and after production releases or data cleanup work to confirm that new stock movements are not adding drift.
Operational note
Deploying this implementation prevents new stock movement drift, but it does not automatically migrate or repair legacy data. Existing missing references or historical ledger mismatches need a separate remediation decision and migration plan.