Skip to content

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:

RecordPurpose
Ingredient InventoryThe current stock for one ingredient at one outlet.
Ingredient TransactionThe 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

text
User action
  -> API route
    -> Ingredient stock movement service
      -> validate movement type and reason
      -> create Ingredient Transaction
      -> update Ingredient Inventory current stock

This means stock changes are calculated in one place instead of separately inside each screen.

What creates stock movements

User actionMovement written
Create ingredient inventory with a starting countADJUSTMENT / inventory_count from 0 to the starting stock.
Edit ingredient inventory current stockADJUSTMENT / inventory_count from the previous count to the new count.
Delete ingredient inventory with non-zero stockADJUSTMENT / inventory_count to bring stock to 0, then the inventory row is removed.
Create a manual ingredient transactionIN, OUT, or ADJUSTMENT, depending on the selected type and reason.
Receive an ingredient stock orderIN / purchase for each received item.

Movement rules

Each transaction type only accepts reasons that match that type.

TypeValid examples
INpurchase, delivery, return, transfer in, production
OUTconsumption, waste, expiry, damage, transfer out, theft
ADJUSTMENTinventory 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 / purchase transaction.
  • 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 actionBlocked when
Delete supplierAn ingredient or stock order still references the supplier.
Delete ingredient bundleA 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:

bash
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-issues

The 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.