Plain-English summary
Inventory in Foodops is three connected things: Items (everything you sell or use, with SKUs and stock levels), Stock Adjustments (manual corrections like waste, theft, or stocktake results), and Stock Transfers (moving stock between warehouses). Together they answer "how much of X do we have, where is it, and how did it get to that number".
When you'd use this
Every restaurant needs to know what's in stock. You'll work with these screens when:
- Adding a new menu item or raw ingredient (Items)
- Doing a monthly count and the books don't match reality (Stock Adjustment → Stocktaking Results)
- A delivery is fine but a kitchen ran out (Stock Transfer between warehouses)
- A walk-in freezer fails and you write off MVR 800 of fish (Stock Adjustment → Damaged Goods)
- A regulator asks "how did this stock level change last month" (the audit trail across all three)
Sales-driven stock movement happens automatically through Recipes and Ingredients — these screens cover everything outside of normal sales.
How to access
- Items list:
/products(titled "Items" in the UI) - Stock Adjustments:
/inventory/adjustments(admin app; create/edit at/inventory/adjustments/new+/inventory/adjustments/:id/edit— the legacy/stockadjustmentsURL retired with the monolith) [code: foodops-admin-app/apps/admin/src/pages/inventory/StockAdjustmentFormPage.tsx routes @ 93e9085] - Stock Transfers:
/inventory/transfers(admin app; create at/inventory/transfers/new— legacy/stocktransfersretired) [code: foodops-admin-app/apps/admin/src/pages/inventory/StockTransfersPage.tsx —navigate('/inventory/transfers/new')@ 93e9085] - Sidebar: under the inventory/stock area in the Foodops sidebar (icon varies by plan)
Items list — /products
The Items page is the master catalogue of everything in your inventory.

Tabs
| Tab | Filters to |
|---|---|
| All | Every item regardless of type or status |
| Low Stock | Items where current stock ≤ low-stock threshold |
| Inventory | Stock-tracked items (have a stock-on-hand count) |
| Service | Non-stock items (services, fees, surcharges — never deplete) |
| Inactive | Disabled items (kept for historical reporting) |
Columns
- SKU — auto-assigned (
00001,00002...) or custom - NAME — display name
- CATEGORY — menu category
- SALES PRICE (BEFORE TAX) — list price excluding tax
- SALES PRICE (AFTER TAX) — what the customer pays
- STOCK ON HAND — current quantity in the active warehouse, or
-if untracked - ENABLED — toggle: visible on POS / hidden
Walkthrough — add a new item
- Click New Item (top-right). A modal opens.
- Fill name, SKU (or accept auto), category, sales price, tax type
- Choose Item Type: Inventory (tracked) or Service (untracked)
- If Inventory: set opening stock, low-stock threshold, cost price per warehouse
- Tick "Items sold at Outlets" with the relevant outlets — without this, the item exists but never appears on POS
- Save
Warning: The "Items sold at Outlets" checkbox is the #1 cause of "I created an item but it doesn't show on POS" — see Edge cases below.
Stock Adjustments — /inventory/adjustments
A stock adjustment is a manual correction to inventory levels, with a documented reason.
List view
Columns: Reference, Date, Reason, Comments, Adjusted By, Status. Tabs: All · Draft · Adjusted.
Walkthrough — create an adjustment

- Click New Adjustment — this now navigates to a dedicated
/inventory/adjustments/newpage rather than opening an in-page modal (editing a draft opens/inventory/adjustments/:id/edit); the modal form is retained only for the item-detail page's "Adjust" prefill entry point [code: foodops-admin-app/apps/admin/src/pages/inventory/StockAdjustmentsPage.tsx —navigate('/inventory/adjustments/new')(dialog render deleted); StockAdjustmentFormPage.tsx (new — routes/inventory/adjustments/newand/:id/edit); StockAdjustmentDetailsPage.tsx — Edit navigates to/inventory/adjustments/{id}/edit; StockAdjustmentFormDialog.tsx — thin wrapper kept for prefillItems/prefillWarehouseId @ 93e9085] - Choose Adjustment Type (currently "Quantity Adjustment")
- Pick the Date (defaults to today)
- Select a Reason from the dropdown (the standard seeded set for the Adjustment reason group):
- Stocktaking Results — your monthly count diverged from the system
- Damaged Goods — broken/spoiled stock
- Stolen Goods — confirmed shrinkage
- Stock Written Off — out-of-spec, expired, no longer usable
- Stock On Fire — disaster write-off
- New Stock Entry — bring previously-untracked stock onto the books
- Inventory Revaluation — re-state stock value without changing quantity
- Custom Reason — add your own (reasons are stored in the DB and you can create new ones inline)
- Pick the Warehouse the adjustment applies to (defaults to Primary Warehouse for the active outlet, e.g., "KB Test Outlet - Main")
- Add Comments (optional but strongly recommended for audit)
- Click Add from list to pick items. For each item, the table shows:
- Quantity Available — current system level
- New Quantity — what the count should be
- Quantity Adjusted — auto-computed delta (positive = stock up, negative = write-down)
- Save Draft to come back later, or Save & Submit to immediately apply the adjustment to inventory ("Save & Approve" is the Salesmade/OLD-POS-only button label via the form's
simplifiedprop; the FoodOps build shows "Save & Submit", whose submit path chains status straight to Adjusted — the "immediately apply" semantics are unchanged) [code: foodops-admin-app/apps/admin/src/components/inventory/StockAdjustmentForm.tsx — footerButtons{simplified ? 'Save & Approve' : 'Save & Submit'}+changeStatus(..., STOCK_ADJUSTMENT_STATUS.Adjusted); both FoodOps hosts passsimplified={getBrand().id === 'salesmade'}(false on FoodOps) @ 93e9085]
State machine
Draft → Adjusted (immutable; correction by another adjustment, never edit)
The two-button save (Draft vs Submit) is the manual approval gate — useful when junior staff prepare adjustments but a manager must approve.
Stock Transfers — /inventory/transfers
A stock transfer moves inventory between warehouses (e.g., central store → outlet kitchen).
List view
Columns: Reference, Date, Reason, Items, Source, Destination, Status.
State machine
Draft → Pending → Approved → In Transit → Transferred → Received
↓
Rejected
Tabs match the states. The Approved → In Transit step is the moment stock leaves source; Received is when destination confirms it arrived. Discrepancies between Transferred and Received quantities are common and surface on the receiving step.
Walkthrough — create a transfer

- Click New Transfer — this now navigates to a dedicated create page (mirroring the Stock Adjustment flow) rather than opening an in-page modal [code: foodops-admin-app/apps/admin/src/pages/inventory/StockTransfersPage.tsx —
navigate('/inventory/transfers/new')(dialog render removed) @ 93e9085] - Date (defaults to today)
- Source Warehouse — where the stock leaves from
- Destination Warehouse — where it's going
- Reference Document No. — supplier-side reference, internal slip number, etc.
- Carrier — who's doing the transport (truck driver, delivery service)
- Reason / Comment — free text
- Add from list to pick items. For each item, the table shows:
- Available Quantity — shown as a dual badge with both Source and Destination stock levels side-by-side (e.g. "Source 12 Destination 4"), not source-only
- Transfer Quantity to ship
- Choose a save mode — the FoodOps form footer renders exactly two save buttons [code: foodops-admin-app/apps/admin/src/components/inventory/StockTransferForm.tsx — footerButtons
{simplified ? 'Save & Mark as In-Transit' : 'Save & Submit'}plus "Save Draft";submit(simplified ? 'inTransit' : 'submit')@ 93e9085]:- Save Draft — keep editing later (no stock movement)
- Save & Submit — submits the transfer for approval: the status moves to Pending Approval, awaiting an approver. ("Save & Mark as In-Transit" is the Salesmade/simplified-brand button label — it is not rendered on FoodOps, so there is no three-mode save here.)
Receiving a transfer
When stock arrives at destination, open the transfer and mark Received. You can adjust the Received quantity per line if there's a discrepancy. Stock is added to destination on receipt — if Received < Transferred, the difference becomes a write-off at destination.
Key concepts
- Item / Product — anything you sell or use, identified by SKU. Two flavours: Inventory (stock-tracked, has a stock-on-hand count per warehouse) and Service (untracked — pure sale, no depletion).
- Warehouse — a physical location stock lives in. Each outlet has at least one (Primary Warehouse). Multi-outlet orgs may have central warehouses on higher plans.
- Stock-on-hand — current quantity at a specific warehouse. Three things change it: PO receipts (up), recipe-driven sales deduction (down), stock adjustments (either way). Transfers move it between warehouses without changing the total.
- Low-stock threshold — per-item, per-warehouse trigger. Stock at or below threshold appears on the Low Stock tab and on dashboard alerts.
- Stock Adjustment — a documented manual change to inventory, with a reason and an audit trail. Never edit — correct via a new adjustment.
- Stock Transfer — movement between warehouses with a multi-stage approval and tracking workflow. Source loses stock at "In Transit"; destination gains it at "Received".
- Service item — the opposite of an inventory item: never depletes, never appears on Low Stock. Use for delivery fees, surcharges, gift cards, deposits.
- Modifier item (FoodOps only) — an item/variant flagged Set as Modifier on the item form. Marks it as something linked to a modifier option (e.g., "Extra cheese", "No onions") for inventory and reporting purposes, rather than a stand-alone menu line. This is a separate flag from Inventory/Service classification — an item can be stock-tracked AND a modifier. Restaurant brands only. [code: hubits-api-inventory/Hubits.Inventory.Api/DTOs/Products/ProductModels.cs:120 (
IsModifieron variant DTOs); foodops-admin-app/apps/admin/src/components/menu/ItemFormPanel.tsx:921 ("Set as Modifier" toggle)] - SKU — Stock Keeping Unit. Auto-numbered or custom. The canonical reference across recipes, POs, and reports.
Common questions
Q: I see "STOCK ON HAND" as - for all items. Why isn't anything tracked?
A: Items default to untracked unless you explicitly enable inventory tracking on each item. For an item to show stock numbers, it must be type Inventory AND have an opening stock value set on at least one warehouse. Edit the item and check both.
Q: What's the difference between an adjustment and a transfer? A: An adjustment changes total stock (waste, theft, count corrections). A transfer moves existing stock from one warehouse to another — total stock across the org stays the same.
Q: Can I edit a stock adjustment after it's approved? A: No. Approved adjustments are immutable for audit integrity. To "fix" a wrong adjustment, create a new adjustment that reverses or corrects it — the audit trail keeps both records.
Q: Why does my Stock Transfer have so many statuses? A: Real-world transfers have multiple touch points: someone prepares it (Draft), a manager approves (Pending → Approved), the driver picks it up (In Transit), the driver hands it over (Transferred), and the destination signs for it (Received). Each step is a record of who did what and when. (A "Save & Mark as In-Transit" skip-approval shortcut exists only on the simplified Salesmade brand — the FoodOps form always routes through approval.)
Q: What if I don't have multiple warehouses — do I still need transfers? A: No. Single-warehouse orgs ignore the Transfers screen entirely. Add stock via PO receipts, correct via Adjustments, deplete via sales (recipes).
Q: How is "Low Stock" calculated?
A: Per item, per warehouse: when stock_on_hand ≤ low_stock_threshold, it shows up. Threshold is set on the item, defaults to 0 (so by default no item triggers low stock — you need to set thresholds for the alert to be useful).
Q: Can I bulk-import items from a spreadsheet? A: Yes during onboarding via the bulk import tool, or on-demand through support. The template maps your columns (SKU, Name, Category, Price, Tax, Cost, Opening Stock) to Foodops fields. Once imported, edits are one-at-a-time through the UI.
Q: An item exists in the catalogue but doesn't appear on the POS — what's wrong? A: Most common cause: "Items sold at Outlets" was not checked for the active outlet during item creation. Edit the item, scroll to that section, tick the outlet, save. Then click Reload Cache at the bottom of the POS to refresh.
Q: Where do I configure new warehouses? A: Settings → Outlets → [your outlet] → Warehouses. The number of warehouses per outlet is plan-limited; see Pricing.
Q: What does the "Set as Modifier" toggle on the item form do? A: It flags the item/variant as a modifier — something that's linked to a modifier option (like "Extra cheese" or "No onions") rather than sold as its own menu line. The flag is used for inventory tracking and reporting on that modifier; it sits alongside "Sell as Menu Item" and "Use as Ingredient" and is independent of whether the item is stock-tracked. It only appears for restaurant (FoodOps) brands.
Q: What's the difference between Menu Item, Ingredient, and Modifier flags? A: They're three independent flags on an item — Menu Item (sellable directly), Ingredient (raw material used in recipes), and Modifier (linked to a modifier option) — separate from its stock-tracking type (Inventory vs Service). They're not mutually exclusive: an inventory-tracked item can be both an ingredient and a modifier. (Modifier is FoodOps/restaurant-brand only.)
Q: Does the system auto-deduct stock when an order is placed? A: Only if the menu item has a Recipe with the linked ingredients. Without a recipe, sales record revenue but don't touch stock — useful for ready-to-sell items (bottled drinks, packaged snacks).
Q: How do I do a stocktake (physical count)? A: Recommended flow: print a stock-on-hand report (filter to the warehouse), do the count physically, create a Stock Adjustment with reason "Stocktaking Results", enter the counted quantities in New Quantity for each item with a discrepancy, comment briefly, and Save & Approve. The "Quantity Adjusted" column auto-shows the delta for your records.
Edge cases and known issues
"STOCK ON HAND" column shows - for all items
- Symptom: Every item shows
-instead of a number on the Items list, even after PO receipts - Cause: Items haven't been flagged as Inventory type, or no opening stock was set
- Workaround: Edit each item, set Item Type = Inventory, set opening stock per warehouse. Or filter the Items list by the Inventory tab to confirm which items are stock-tracked
Item created but invisible on POS
- Symptom: Item exists in
/productslist but isn't visible at the POS counter - Cause: "Items sold at Outlets" was not checked during creation, OR cached menu data wasn't refreshed
- Workaround: Edit item → check the outlet → save. Then click Reload Cache on POS
Stock transfer stuck in Pending
- Symptom: Transfer created with "Save & Request Approval" but never moves to Approved
- Cause: Approver is missing, on leave, or doesn't have permission
- Workaround: Admin reassigns approval to another user. (The "Save & Mark as In-Transit" skip-approval path is a Salesmade/simplified-brand button — not rendered on the FoodOps form. [code: foodops-admin-app/apps/admin/src/components/inventory/StockTransferForm.tsx —
{simplified ? 'Save & Mark as In-Transit' : 'Save & Submit'}@ 93e9085])
Negative stock after a stocktaking adjustment
- Symptom: Stock On Hand shows a negative number after applying an adjustment
- Cause: The "New Quantity" you entered was lower than the system already thought you had — usually an unrecorded sale or unrecorded waste before the count
- Workaround: Investigate the gap (check Recipe-driven sales, prior adjustments). Once explained, leave the negative as a record, or apply a second adjustment to bring it to the actual physical count
Adjustment reasons feel limited
- Symptom: Need a reason that isn't in the dropdown (e.g., "Customer Comp")
- Cause: The built-in reasons cover the standard accounting categories
- Workaround: Use Custom Reason — you can type whatever fits. Reports group by Custom Reasons separately, so consider standardizing the labels your team uses
Sale stock movement / adjustment lands on the wrong warehouse (known issue)
- Symptom: A POS sale deducts stock from the item's default stock location rather than the selling outlet's warehouse, or a stock adjustment ignores the chosen Warehouse and posts against the Primary Warehouse
- Cause: Known stock-posting defect — the sale stock-out leg resolves the location from the product variant's default stock location, and the adjustment endpoint can fall back to Primary when the warehouse id isn't carried through (tracked internally as the stock-posting bug). Intended behaviour is: sales deduct from the selling outlet's warehouse, adjustments post to the warehouse you selected
- Workaround: For multi-warehouse orgs, verify stock movements landed where expected via the stock-on-hand report after a sale or adjustment, and correct with a Stock Adjustment if needed. Single-warehouse orgs are unaffected. A fix is in progress
Cost price not flowing into recipes
- Symptom: Recipe Total Cost shows 0.00 even though items are added
- Cause: The item's cost price wasn't set in inventory (different field from sales price)
- Workaround: Edit the item, set cost price per warehouse, return to the recipe — Total Cost recalculates
Plan limits
Plan tiers and their limits are not listed here — they are DB-backed and change with the lineup. See Pricing for the live catalogue.
Confirm exact plan boundaries against the in-app subscription page.
API and integration notes
- Items endpoint family in
hubits-api-inventory(ProductsApiController) exposes Item CRUD with SKU as the natural key - Stock movement is recorded as
StockTransactionrows tagged with a reason:SalesDelivery(deduct on sale) /SalesReturn(restock on void/return), the adjustment reasons above (Damaged Goods, Stolen Goods, Stocktaking Results, etc.), and transfer reasons — seehaafai.domainStockHelper.cs+ModelConstants.StockTransactionReasons - The "Stock On Hand" column is computed from these stock transactions (
StockLocationProductVariantcurrent value), recomputed on each movement - CSV export available from list views (3-dot menu top-right) for Items, Adjustments, and Transfers
- Approval permissions are role-based — assignable per role in Settings → Users & Roles
- Kit/Assembly authoring via the products API (NEW 2026-07; API-level, admin-app UI re-walk pending) — Kit/Bundle items (
ProductTypeId8) can now be authored through the inventory API with a stock mode: Kit (component-level stock, the default when omitted; matches theStockHelpercomponent-consumption behavior confirmed in the 2026-06-29 verification below) vs Assembly (with optionalAutoAssemblyEnabled/AutoAssemblyThreshold, validated > 0 when enabled) — composed ofKitComponentspersisted as an auto-createdIsBundleddefault bill-of-material (a kit with no components is rejected; a component's cost defaults to its variant CostPrice). Assembly-mode runtime stock behavior is not yet code-verified — scope this to API capability until the admin-app UI pass. [code: hubits-api-inventory/Hubits.Inventory.Api/Controllers/ProductsApiController.cs — Create/UpdateKitBundleProductTypeId == 8branches + SyncKitBillOfMaterialAsync() (BadRequest "A kit product requires at least one component.",var cost = comp.CostPrice ?? variant.CostPrice;,bom.TotalCostrecompute)] - Bill-of-materials API is now real persistence, not response-only stubs (NEW 2026-07; API-level, admin-app UI re-walk pending) — Create now saves a sequence-numbered BoM enforcing one default per variant; status changes persist via a new
GET api/v1/bill-of-materials/statuses+ ChangeStatus; and line-item add/update/remove actually write, with cost fallback: provided cost → cost price list → variant CostPrice. [code: hubits-api-inventory/Hubits.Inventory.Api/Controllers/BillOfMaterialsApiController.cs — GetStatuses(); ChangeStatusbom.BillOfMaterialStatusId = request.GuidStatusId.Value; await _dbContext.SaveChangesAsync(ct);; Create_dbContext.BillOfMaterials.Add(bom); await _dbContext.SaveChangesAsync(ct);(old body returned OK without saving); ClearDefaultForVariantAsync(); AddLineItem cost-resolution body]
Related
- Recipes & Ingredients — how stock auto-deducts on sales
- Suppliers & Purchase Orders — how stock comes in from external vendors
- Settings Overview — warehouse, outlet, and role configuration
- Reports Catalogue — stock-on-hand, low-stock, transfer, and adjustment reports
- User Stories: Inventory — operational scenarios
What changed (2026-04-15 → 2026-05-04)
11b1995(2026-04-20) — touchedHaafai.Inventoryalongside session/cash-withdrawal fixes (PO receipts incrementing stock during an open session)ba7d8c5(2026-04-23) — surfaced document-generation errors on stock movement PDFs- 2 substantive commits total in
Haafai.Inventory/**since the watermark — both bug fixes, not feature/flow changes. Items list, Stock Adjustments form, Stock Transfers state machine all unchanged from the Playwright pass.
Note: Drift report v2 cross-check Drift detector v2 deterministic sweep classified this entry MEDIUM (3 commits, no glob fallback). Confirmed: globs match real paths in
haafai.app.foodops/Haafai.Inventory/**, no fallback. Score is real signal — bug-fix-only commits → no behavior drift in the documented surfaces.
- 2026-08-11 — [email protected] (main) supplies the admin-app UI pass this entry's 2026-08-09 verification note said was the last missing piece for the InventoryPeriodSiteStatus/InventoryPeriodCheckKeys constants (haafai.domain, previously enum-only with no controller/service/UI). Three new files under apps/admin/src/pages/inventory/ implement a full Inventory Periods feature: InventoryPeriodsPage.tsx (list + create period, name + inclusive start/end date, periods may not overlap), InventoryPeriodSitePanel.tsx (per-site — one per outlet plus a 'Central / unassigned' pseudo-site — close checklist and status), and InventoryPeriodCheckConfigPanel.tsx (per-org enable/threshold settings for each check). Confirmed in code/test comments: the checklist is explicitly advisory — 'no check blocks a close ... Whatever is outstanding when you close is recorded against the period' — so this resolves rather than confirms the earlier speculation about a possible Adjustment/Transfer submit-time gate; there is still no evidence any documented Save & Submit flow in this entry is blocked by period status. A closed site's checklist figures can go stale: a driftedSinceCloseDate flag surfaces 'These figures changed after close' when a sale dated inside the period posts after the site was closed. Closed sites can be reopened via a reason-gated mutation (reopenSite(periodId, siteId, reason)), and a period cannot be deleted once any site is closed (UI-confirmed by test, matching the closedSiteCount API-refusal inference already logged 2026-08-09). This is new, UI-confirmed, previously-undocumented functionality entirely additive to this entry — nothing in the entry's body currently mentions inventory periods, so nothing is contradicted. No edit needed; strong candidate for a new 'Inventory Periods (close workflow)' subsection or Key-concept bullet once a live Playwright pass confirms how it's reached from the inventory sidebar/nav and whether it's plan-gated. Unrelated in the same diff range: InventoryFilterBar.tsx and the StockAdjustments/StockCounts/StockTransfers list pages swap the DATE_PRESETS button row + two DatePickers for a single DateRangePicker component with built-in presets — internal filter-bar refactor only, this entry doesn't document date-filter UI mechanics, no action needed. (auto-applied by the truth pipeline; adversarially gated)
- 2026-08-09 — [email protected] (main) adds two new constant groups under a labelled epic (E2-A / VAR-01) not previously mentioned anywhere in this entry: InventoryPeriodSiteStatus (Open=1/Closing=2/Closed=3) and InventoryPeriodCheckKeys (closing_count_coverage, unapproved_counts, draft_wastage, transfers_in_transit, uncosted_ingredients, recipe_coverage, plus an
Allarray). Per the code's own doc comments this is a per-site inventory-period close workflow: Closing and Closed both 'freeze inventory documents dated inside the window' — i.e., exactly the Stock Adjustments and Stock Transfers surfaces this entry documents — with Closed additionally requiring a reason + audit row to reopen, while Closing is freely revertible. The check keys back an advisory (non-blocking, per the comment 'No check blocks a close (R-A5)') close checklist: closing_count_coverage is called out as the highest-risk figure because uncounted items silently fall back to book balance and read as zero variance rather than 'no closing count'. This is meaningfully more relevant to this entry's documented surface (Items/Adjustments/Transfers) than the previously-logged REC-02/REC-08 recipe-versioning constants, since a frozen-period would directly gate the Stock Adjustment and Stock Transfer save/submit flows this entry walks through. However, as with those prior constant-only sightings, no controller, service, or API endpoint consuming InventoryPeriodSiteStatus/InventoryPeriodCheckKeys is present in this diff pass — only the enum/constant definitions and their doc comments — so there is no confirmed behavior change yet (no evidence of an actual freeze being enforced, no new endpoint, no admin-app UI). Nothing here contradicts any current claim in the entry (inventory periods, period close, or a close checklist aren't mentioned anywhere in this entry's body). No edit needed now; this is the strongest candidate yet for a future Key-concept/Edge-case addition ('Adjustments/Transfers may be blocked or require an audited reopen when the inventory period is Closed') once a diff pass shows the controller/service code that actually reads these constants and, ideally, an admin-app UI pass confirming how period status is surfaced and enforced live. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated) - 2026-08-08 — [email protected] (main) supplies hunks for two items already logged as summaries: the RejectedQty-aware stockQty fix and the .Update()→StorageLocationHelper.MarkChanged() persistence fix (both fully covered by the 2026-08-01/2026-08-04 notes; no new call sites, no action). Newly visible in this pass (previously commit subjects only, flagged 'not shown in the supplied hunks and therefore not evaluated' on 2026-08-04): ModelConstants.cs adds VoidBinReview plus VoidType/VoidReason/VoidReviewStatus enums backing 'feat: add void bin schema' — a SalesOrders void-approval feature with no intersection with this entry's Items/Adjustments/Transfers surface; no action. Also newly visible: two constant groups apparently backing 'feat(recipes): every recipe line declares as-purchased or edible-portion (REC-02)' — RecipeQuantityBasis (AsPurchased=1/EdiblePortion=2) and, per the code's own doc comments, a REC-08 'staged recipe-migration proposal' pair (RecipeMigrationProposalStatus: Draft/Committed/Abandoned; RecipeMigrationTargetType: ProductVariant vs BillOfMaterialLineItem). The comments describe a real design distinction — editing ProductVariant.TrimYieldPercent directly re-costs recipes but cuts no version, whereas editing BillOfMaterialLineItem.QuantityBasisId/TrimYieldPercentOverride is part of the recipe's composition key and does cut a version — which conceptually explains (but doesn't yet show the code for) the RecipeVersionHelper.CutIfChangedAsync versioning behavior already flagged, unexplained, in the 2026-08-08 hubits-api-inventory verification note above. No controller or service code consuming any of these new constants is present in this diff pass — only the constant/enum definitions — so there is no confirmed behavior change yet to the documented Bill-of-Materials API surface (line-item CRUD, Kit/Assembly). Nothing here contradicts any current claim in this entry (recipe quantity-basis and migration-proposal concepts aren't mentioned anywhere in the entry's body). No edit needed; worth revisiting once a diff pass includes the actual REC-02/REC-08 controller or service logic, as a possible extension to the existing 'Bill-of-materials API is now real persistence' bullet under API and integration notes. (auto-applied by the truth pipeline; adversarially gated)
- 2026-08-08 — [email protected] (main) adds two genuinely new, previously-undocumented BillOfMaterialsApiController behaviors on top of this entry's existing 'real persistence, not response-only stubs' note. (1) feat(bom): AddLineItem now calls a new BillOfMaterialCycleValidator.FindCycleAsync before adding an ingredient line — if the target ingredient variant has an Active recipe of its own that would make the parent recipe reachable from itself, the call now returns 400 'This ingredient would create a circular recipe reference: {cyclePath}' instead of silently allowing it. This is a new error condition on the Bill-of-Materials line-item-add surface not currently mentioned anywhere in this entry. (2) A new RecipeVersionHelper.CutIfChangedAsync call is now wired into Create (cuts v1 at BoM creation, even with zero lines), Update (header edits to Quantity/UnitId, which are part of the composition key, cut a version; Description/IsDefault-only edits correctly do not), AddLineItem, and UpdateLineItem (quantity/unit changes cut a version; a UnitCost-only recost does not) — per the code's own comments this establishes recipe versioning/drift-detection ('every recipe has a version ... edited without recording vs never had one'). RemoveLineItem's diff is truncated in this pass (cuts off right after the method starts loading the full BoM with its line items instead of the line alone, for UpdateTotals/ComputeCompositionKey), so whether it also cuts a version on removal is unconfirmed from this hunk. Neither change contradicts anything currently documented — this entry does not mention circular-reference validation or recipe versioning at all — so no edit is needed, but both are new, code-confirmed (not yet UI-confirmed) capabilities worth a future 'API and integration notes' bullet once an admin-app pass shows how (or whether) the cycle-rejection error and any version history surface live. Also in this range: the ProductsApiController BoM-sync helper backing the already-documented Kit/Assembly authoring path replaces a hand-rolled 'bom.TotalCost = lines.Sum(...)' with a call to the real bom.UpdateTotals(_dbContext), fixing silent under-costing of edible-portion/yield-uplift lines — a bug fix, not a change to any documented Kit/Assembly claim (this entry doesn't describe EP/yield costing). ProductsApiController.Update/UpdateVariant also now evict the org's price-list caches on save — out of scope, price lists aren't documented here. Not evaluated (no hunks supplied in this diff pass): fix(products) archived-variant grouped-list fallback, fix(inventory) reservation-overbooking/batch-load/timeout changes, and feat(library-conversions) implementation — commit subjects only. (auto-applied by the truth pipeline; adversarially gated)
- 2026-08-04 — [email protected] (main) is a superset of the 5249fe1 range already logged on 2026-08-01 (the MarkChanged / 'stop DbSet.Update turning a pending SLPV insert into a 0-row update' fix); the additional StockHelper.cs hunks visible in this diff show more of the same already-logged call sites (five of the reported 'at least 9') swapping context.StockLocationProductVariants.Update(...) for StorageLocationHelper.MarkChanged(context, ...) — no call sites beyond what was already described, no action needed there. Genuinely new in this range: commit 'fix(stock): fully rejected deliveries no longer enter stock, cost, or tax' changes the same file's PO-receive stock-quantity resolution (two call sites, ~line 899 and ~line 1324) so that when RejectedQty > 0, stockQty resolves to AcceptedQty only (previously: AcceptedQty>0 ? AcceptedQty : ReceivedQty — which could let a fully-rejected delivery's ReceivedQty leak into stock, cost, and tax). This concerns Purchase Order receiving, which this entry does not document (see Related: Suppliers & Purchase Orders) — StockHelper.cs is only cited here generically as the implementation backing 'stock movement is recorded as StockTransaction rows... see StockHelper.cs'. Doesn't contradict anything currently in this entry; candidate for purchasing/suppliers.md rather than this one. Also present in this commit range but not shown in the supplied hunks and therefore not evaluated: 'feat: add void bin schema' (Outlet.EnableVoidBin, VoidBinEntry + new VoidType/VoidReason/VoidReviewStatus/VoidBinReview constants in ModelConstants.cs) — a SalesOrders void-approval feature, unrelated to Items/Adjustments/Transfers; and 'feat: per-quantity BOM stock reversal (recursive + modifiers), shared with cancel', whose actual StockHelper.cs body isn't included in this diff pass — this could eventually intersect the entry's Kit/Assembly/Bill-of-Materials API notes, so worth a follow-up pass once its hunk is available. No entry edit needed now. (auto-applied by the truth pipeline; adversarially gated)
- 2026-08-03 — [email protected] (main) adds a new Bill-of-Materials capability adjacent to the per-variant unit-conversion CRUD already logged in the 2026-07-22 verification note: BillOfMaterialsApiController gains GetLibraryConversions (GET library-conversions/{productVariantId} — resolves the variant's base unit against the StandardUnitConversions table and returns candidate standard/system conversions, excluding any already configured on the variant) and ApplyLibraryConversion (POST apply-library-conversion — applies one of those standard conversions to the variant as a new alternate unit, tagged Source = Library, with price derived from the variant's base price rather than independently set). This is additive to, not a contradiction of, the entry's existing 'per-item unit conversion CRUD endpoints' note — it's a second, curated way to add a conversion (pick from a library) alongside the existing manual GetUnitConversions/AddUnitConversion path. No foodops-admin-app diff is in this range, so whether the admin item form surfaces a 'pick from standard conversions' UI (vs. only manual entry) is unconfirmed — scope as API-level capability pending a UI re-walk, consistent with this entry's existing 'API-level, admin-app UI re-walk pending' precedent for Kit/Assembly, Bill-of-Materials, and per-item unit conversions. Also in this range: ProductsApiController's groupByProduct list-aggregation (the collapsed variant-product row already flagged as UI-pending in the 2026-07-13 note) fixes a bug where a fully-archived product's row fell back to an EMPTY variant set — ProductVariantId read as Guid.Empty (every row action targeted a nonexistent id), VariantCount 0, price null, IsEnabled vacuously true, and RecordStatusId reported the parent's status so an archived row could never be restored from the list. The fix now falls back to the product's non-deleted (archived) variants when no Active variant remains, so archived-only rows resolve to a real variant id and correct counts/price/enabled state. This doesn't contradict anything currently documented — groupByProduct itself still isn't described in this entry's body — it just closes a defect in an already-flagged-pending feature. Also present but out of scope: perf-only refactors batch-loading StockTransaction navigation properties in the StockAdjustments/StockCounts ChangeStatus and StockTransfers MarkTransferred/Receive paths (replacing per-transaction lazy-load round trips with one batched load via a new IncludeStockTransactionProcessingNavs()/LoadChangeStatusNavigationsAsync()), plus a reservation-overbooking-race fix and a DB command-timeout change named in the commit subject but not present in the supplied diff hunks — none are user-facing behavior changes to this entry's documented flows. No entry edit needed; candidate for a future 'API and integration notes' bullet on library conversions once an admin-app UI pass confirms how (or whether) it's surfaced. (auto-applied by the truth pipeline; adversarially gated)
- 2026-08-03 — [email protected] (main) adds a 'Variance only' toggle to the Stock Count detail page's Related-items tab (StockCountRelatedTab.tsx), UI-confirmed by a new test (StockCountDetailsPage.varianceOnly.test.tsx). The items table — same Item/Quantity Available/New Quantity/Quantity Adjusted columns this entry documents for the Stock Adjustment 'Add from list' step — now defaults to showing all lines with a switch + count summary ('N of M items with a variance'); toggling it filters to only rows where quantityChanged !== 0, and shows an empty-state message ('No items on this count have a variance') if none qualify. This is a Stock Count screen, which this entry's body still does not document as its own feature (the 2026-07-13/2026-07-17 verification notes already flagged StockCountForm/StockCountsPage as 'not yet documented ... pending a UI re-walk'; the 'How do I do a stocktake' Q&A only describes the Stock-Adjustment-based 'Stocktaking Results' path). Nothing here contradicts any current claim — it's additive UX on an already-flagged-pending screen. The rest of this diff (StockAdjustmentsPage/StockCountsPage/StockTransfersPage extracting DATE_PRESETS/presetRange/isoDate into a shared apps/admin/src/utils/datePresets module, adding org-timezone-aware preset matching via a new
orgTzparam, and fixing a search-debounce race so an in-flight 300ms timer no longer resets pagination on an unchanged term) is internal refactor/bugfix to list-filter plumbing not described anywhere in this entry (date-range presets and search-debounce timing aren't documented) — no action needed there. Worth folding the variance-only toggle into a future Stock Count walkthrough once that screen gets its own documented section. (auto-applied by the truth pipeline; adversarially gated) - 2026-08-01 — [email protected] (main), commit 'fix(stock): stop DbSet.Update turning a pending SLPV insert into a 0-row update': across at least 9 call sites in StockHelper.cs's serial-tracked and non-serial-tracked stock-posting paths,
context.StockLocationProductVariants.Update(productStockLocationOut/In)is replaced withStorageLocationHelper.MarkChanged(context, ...). Per EF Core change-tracking semantics, calling.Update()on an entity already tracked as a pending INSERT (a StockLocationProductVariant row created earlier in the same unit of work — e.g. the first time a variant holds stock at a given location) flips it to Modified, so EF emits an UPDATE against a row that doesn't exist yet; that UPDATE matches 0 rows and the pending insert — and the stock level it carries — is silently dropped. The newMarkChanged()helper presumably branches on tracked state before deciding Add vs Update, but its own implementation isn't in this diff. A trailing, truncated hunk inAllocateStockToTransferadds a matching 'Get-or-create' comment describing the same class of bug ('a variant that has never held stock at this location has no row yet, and the oldif (row != null)guard dropped its commitment outright'), but the actual code change there is cut off in this diff and is not evaluated here. This does not contradict this entry's existing claims — 'Stock On Hand... computed from these stock transactions... recomputed on each movement' describes the intended behavior, which this fix protects rather than changes; the entry never claimed persistence was defect-free. It's consistent with (and likely a further hardening of) the get-or-create / first-movement-at-a-new-location class of stock-posting defects already logged in this entry's 2026-07-11 verification note (the stock-doubling fix) and adjacent to the existing 'Sale stock movement / adjustment lands on the wrong warehouse (known issue)' edge case, though it is a distinct bug (silent 0-row no-op vs double-counting/wrong-warehouse). No foodops-admin-app or API diff is in this range, so there's no new UI-facing symptom to document — this is a domain-layer persistence-correctness fix, not a workflow or UI change. No entry edit needed now; worth a note only if a live symptom (e.g. an item's first stock movement at a new warehouse silently failing to register) is ever reported and traced back to this bug class. (auto-applied by the truth pipeline; adversarially gated) - 2026-07-25 — [email protected] (main), commit 'feat(stock): honor forceAllowNegativeStock so paid POS sales allocate + go negative even when AllowNegativeStock is off': AllocateStockToInvoice() gains a new
forceAllowNegativeStockparameter (default false) that is OR'd into every existingproductVariant.AllowNegativeStockgate across both serial-tracked branches and the non-serial-tracked path — so when true, a sale is allowed to fully allocate and post negative stock even for an item whose per-item 'Allow Negative Stock' flag is off, instead of failing with 'Insufficient stock for invoice' / 'Stock not available.' A new fallback also creates one synthetic StockTrackingSerial for any leftover quantity once real serials are exhausted, so the negative remainder posts instead of being silently dropped. This does not contradict anything currently documented — this entry does not mention an 'Allow Negative Stock' item flag, invoice/POS-sale stock-allocation failure behavior, or any forced-negative-stock override anywhere in its Key concepts, Common questions, or Edge cases sections (the existing 'Negative stock after a stocktaking adjustment' edge case is scoped to Stock Adjustments, not sales/invoice allocation, and is unaffected). Per the commit subject this override is wired for paid POS sales specifically, but the caller that setsforceAllowNegativeStock=trueis not included in this diff hunk, so the exact trigger condition (e.g. payment-status check at the POS call site) is unconfirmed from code alone — scope as domain-layer capability pending confirmation of the calling code and a POS/admin-app UI re-walk, consistent with this entry's existing 'API-level, UI re-walk pending' precedent for Kit/Assembly and Bill-of-Materials. Worth a new Edge case or Key-concept note once the call site is confirmed: paid POS sales may now deduct stock below zero regardless of the item's Allow Negative Stock setting, distinct from the existing Primary-Warehouse-fallback known issue. (auto-applied by the truth pipeline; adversarially gated) - 2026-07-22 — [email protected] (main), commit 'feat: add per-item unit conversion CRUD endpoints to bill-of-materials API': BillOfMaterialsApiController gains GetUnitConversions (GET unit-conversions/{productVariantId} — returns the variant's base unit plus all configured ProductVariantUnitConversionFactor rows) and AddUnitConversion (POST unit-conversions, Products.Edit-gated — adds an alternate selling/stock unit for a variant, e.g. 'Box of 24' alongside the base 'Each'). Per the code's own comment this deliberately mirrors legacy MenuItemsController.AddConversionFactor: a conversion unit from ANY category is allowed (cross-dimension packaging), duplicates are rejected only on exact unit+value match, and ListPrice/ListPriceWithTax are auto-derived as the variant's base price × the conversion value (not independently settable). A third endpoint (update-existing-conversion) is visible but truncated in this diff. This is additive, new API surface — it doesn't contradict this entry's documented Items/Stock Adjustment/Stock Transfer flows or Bill-of-Materials claims, since unit conversions aren't currently mentioned anywhere in the entry. No foodops-admin-app diff is in this range, so it's unconfirmed whether/how the admin item form or BoM UI surfaces multi-unit selling yet — scope as API-level capability pending a UI re-walk, consistent with this entry's existing 'API-level, admin-app UI re-walk pending' precedent for Kit/Assembly and Bill-of-Materials persistence. Candidate for a new bullet under 'API and integration notes' (per-variant alternate-unit conversions, auto-priced off base price) once an admin-app pass confirms the live UI. (auto-applied by the truth pipeline; adversarially gated)
- 2026-07-21 — [email protected] (main) adds a naming convention for Variant-type products not previously documented in this entry. New helper BuildVariantName() now forces every Variant-type product-variant's Name to the form "{ParentName}/{VariantName}" (stripping any existing "Parent/" prefix before re-adding it, so the operation is idempotent), applied consistently in Create, AddVariant, and UpdateVariant. Separately, Update now cascades a parent-product rename to its variants: when a Variant-type product's Name changes, RenameVariantForParent() re-prefixes every existing variant whose current name still starts with the OLD parent name, via a new re-slash rebuild ('per counter/KDS/menu parity' per the code's own comment). Neither behavior contradicts anything currently documented — this entry's Items-list section describes the NAME column only generically ('display name') and does not describe any parent/variant naming convention or rename-cascade behavior, so nothing needs correction. It is new, previously-undocumented, user-facing surface: for Variant-type items, the Items-list NAME column should now read as "Parent/Variant" rather than a freeform name, and editing a parent product's name should retroactively re-prefix all its variants. No foodops-admin-app diff is in this range, so it's unconfirmed whether/how the admin Items list or item-edit form surfaces or enforces this live — scope as API-level capability pending a UI re-walk, consistent with this entry's existing 'API-level, UI re-walk pending' precedent for Kit/Assembly and Bill-of-Materials. Also present in this same diff range but already covered: the parent-product-name search-match commit ('fix(products): match parent product name in list search...') was already logged in the 2026-07-20 verification note — no new action there. A third commit in this range ('fix: match legacy bulk-update export filename') has no corresponding hunk in the supplied diff and is not evaluated here. (auto-applied by the truth pipeline; adversarially gated)
- 2026-07-20 — [email protected] (main), commit "fix(products): match parent product name in list search so variants are found by their family name": ProductsApiController.GetAll's list-search predicate (backing the Items list /
/products) previously matched only a variant's own Name, SKU, or Code; it now also matchespv.Product.Name(the parent product/family name). This is additive, not contradicting anything currently documented — this entry's Items-list section describes Tabs and Columns but does not currently document the search box's match fields, so nothing needs correction. Practical effect: searching by a product's family name (e.g. the parent 'Shawarma' product) will now surface all of its variants even when a variant's own display name, SKU, or code doesn't contain the search term — previously such variants were invisible to that search. No foodops-admin-app diff is in this range, so it's unconfirmed whether the admin-app Items list search box calls this same GetAll endpoint/param, though the entry's existing 'API and integration notes' already cites ProductsApiController as the Items endpoint backing this screen. Minor, worth a one-line mention under Items list if a UI pass confirms it; not urgent enough to block on. (auto-applied by the truth pipeline; adversarially gated) - 2026-07-17 — [email protected] (main) removes the
simplifiedprop from StockTransferLineItemEditor.tsx entirely, making the item-picker table's rendering brand-uniform. Two confirmed UI changes to the Stock Transfer 'Add from list' step (step 8 in this entry's walkthrough): (1) the Available-Quantity cell — previously a single right-aligned number for FoodOps, and a Source/Destination dual badge only for the simplified/Salesmade brand — now always renders the dual 'Source XXX Destination YYY' badge for every brand; (2) the Item column now always prefixes the SKU ({sku} - {description}) when a SKU is present, previously a Salesmade-only format. Column headers 'Available Quantity'/'Transfer Quantity' are also now unconditional (previously FoodOps showed plain 'Available'/'Quantity'), which brings actual behavior in line with this entry's existing screenshot caption listing those header names — no edit needed there. Unrelated in the same diff: StockCountForm.tsx now auto-loads warehouse items on warehouse selection (void autoLoadWarehouseItems(newId)), a Stock Count feature this entry does not yet document in its body (only flagged as UI-pending in the 2026-07-13 verification note) — no action needed until that feature gets its own walkthrough pass. A cosmetic toggle-switch off-state color change (bg-muted → bg-input) is out of scope. (auto-applied by the truth pipeline; adversarially gated) - 2026-07-17 — [email protected] (main) adds granular role-based authorization across every controller this entry cites. ProductsApiController, StockAdjustmentsApiController, StockCountsApiController, StockTransfersApiController, StocksApiController, StockTransactionReasonsApiController, and BillOfMaterialsApiController previously carried only a bare
[Authorize](any authenticated user) at the controller level; each is now[Authorize(Roles = InventoryRolePolicies.{Products,StockAdjustments,StockTransfers}Read)]. Individual mutating actions (product Create/Update/AddVariant/clone/toggle-enabled/gallery, and Create/Update/ChangeStatus/Void/Dispatch/Receive/Cancel plus comment CRUD on Stock Adjustments/Stock Counts/Stock Transfers, and line-item/status endpoints on Bill of Materials) each gained their own[Authorize(Roles = ModelConstants.Roles.{Products,StockAdjustments,StockTransfers}.{Create,Edit,Delete})]attribute. This doesn't contradict the entry's existing 'Approval permissions are role-based — assignable per role in Settings → Users & Roles' line (still accurate — just now far broader than approval alone), but it is new, previously-undocumented surface: viewing and mutating Items/Stock Adjustments/Stock Counts/Stock Transfers/Bill of Materials now requires specific role grants, not merely being logged in. No foodops-admin-app diff is in this range, so it's unconfirmed how a user lacking the new role experiences this in the UI (403 toast, hidden action, or blocked page) — scope as API-level capability pending a UI re-walk, consistent with this entry's existing 'API-level, UI re-walk pending' precedent for Kit/Assembly and Bill-of-Materials. Candidate for a short addition under 'API and integration notes' once the admin-app's role-to-permission mapping and resulting UI gating are confirmed live. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated) - 2026-07-13 — [email protected] (main) adds two changes intersecting this entry's documented Items and Stock Adjustment surfaces, both additive/new rather than contradicting anything currently documented. (1) ProductsApiController.GetAll gains a new
productTypeIdexact-match filter (distinct from the existing filterMenuItem/filterIngredient/filterVariant bucket toggles) and agroupByProductmode that returns one row per Product instead of per variant, aggregating VariantCount/ListPriceMax/ProductTypeId over the product's Active variants (ProductListResponsegained matchingProductTypeId,VariantCount,ListPriceMaxfields) — per the code's own comments this collapses variant-type products into a single expandable row whose count matches the existing ListVariants expand view. No foodops-admin-app diff is in this range, so it's unconfirmed whether the Items list (/products) has actually adopted a collapsed/expandable variant-product row yet — scope as API capability pending a UI re-walk, consistent with this entry's existing Kit/Assembly and Bill-of-Materials 'API-level, UI re-walk pending' precedent. (2) StockAdjustmentsApiController.ChangeStatus now guards against posting stock to a warehouse with no storage location: ifProcessStockis true and the warehouse'sStockLocationsis empty, it returns 400 'This warehouse has no storage location set up, so there is no stock to adjust here. Add a storage location to the warehouse first.' instead of risking a NullReferenceException in the domain posting path — a new, previously-undocumented error condition on the Stock Adjustment walkthrough's Save & Submit step (step 8); admin-app surfacing (toast vs inline error) is unconfirmed, no admin-app diff in range. Also present but not separately noteworthy:InvalidateAllProductsForOrgAsynccache-invalidation calls added across StockAdjustments/StockCounts Create/Update/ChangeStatus/Void — plumbing to keep the new grouped-by-product cache consistent, no independent user-facing behavior. Does not contradict any current claim (SKU-as-natural-key, Draft→Adjusted state machine, or the existing warehouse-fallback known issue, which concerns ID propagation to Primary Warehouse, not storage-location existence). (auto-applied by the truth pipeline; adversarially gated) - 2026-07-13 — [email protected] (main) adds a new StockCountForm.tsx (apps/admin/src/components/inventory/, ~660 lines, truncated in this diff pass) — a dedicated admin-app UI for Stock Counts, distinct from the Stock Adjustment 'Stocktaking Results' reason path this entry currently documents as the stocktake workflow (see 'How do I do a stocktake' Q&A). Confirmed from the diff: on FoodOps (non-simplified) the form reuses the existing Reason picker + StockLineItemEditor (manual item add, same pattern as Stock Adjustments); on the simplified/Salesmade brand it auto-loads every item at the selected warehouse into a bulk table with Available/Counted/Difference columns plus a zero-quantity filter and search box — mirroring 'OLD POS' behavior per the file's own comments. Internally, a Stock Count is implemented as a StockAdjustmentRequest row with a hardcoded transaction-type id of 2, and reuses STOCK_ADJUSTMENT_STATUS. This does not contradict the entry's existing Stock-Adjustment-based stocktake guidance (nothing in this diff removes or deprecates the 'Stocktaking Results' reason), but it is new, not-yet-documented user-facing surface: a purpose-built Stock Count screen may supersede or sit alongside the documented workflow. The diff doesn't show routing/menu wiring or the save-button footer for this form (cut off mid-file), so treat as API/component-level, UI-routing and live-behavior unconfirmed pending a Playwright pass — consistent with this entry's existing 'API-level, UI re-walk pending' precedent for Kit/Assembly and Bill-of-Materials. All other hunks in this range (ProductVariantPicker stockTrackedOnly wiring, StockAdjustmentForm warehouse-change re-snapshot, select-on-focus inputs, StockDetailsDialog column dividers, StockTransferLineItemEditor header alignment) restate or extend changes already logged in this entry's 2026-07-11/2026-07-12 verification notes or are cosmetic-only (e.g. the Available-Quantity header's text-align flip in StockTransferLineItemEditor) and need no further action. (auto-applied by the truth pipeline; adversarially gated)
- 2026-07-12 — [email protected] (main) is the full patch for the warehouse-change fix already logged in this entry's 2026-07-11 note (subject 'fix(inventory): preserve line items on warehouse change to match legacy behavior'); the fuller diff confirms that note's description of
handleWarehouseChangedropping the FoodOps-only clear-on-change branch in favor of the sharedreSnapshotOnWarehouseChange/snapshotItemsForWarehousepath, so nothing there needs correction. New in this fuller diff: astockTrackedOnlyprop was added toProductVariantPicker(excludes Service/Non-Inventory product types from search results) and wired into bothStockLineItemEditor(the Stock Adjustment 'Add from list' picker) andStockTransferLineItemEditor(the Stock Transfer 'Add from list' picker). This means the item picker in both the Stock Adjustments and Stock Transfers walkthroughs (step 7 and step 8 respectively) now only offers stock-tracked (Inventory-type) items — Service items can no longer be searched/added to an adjustment or transfer line. The entry does not currently state either way whether Service items were selectable in these flows, so nothing is contradicted, but this is new, UI-confirmed (not just API-level) user-facing behavior worth a short note under the 'Add from list' step in both walkthroughs, e.g. 'only Inventory-type items can be added here; Service items are filtered out.' Also present in the same diff but not worth separate documentation: select-all-on-focus behavior added to quantity inputs in StockLineItemEditor, StockTransferLineItemEditor, and ReceiveTransferLineEditor, and column-divider CSS styling in StockDetailsDialog — pure UX/visual polish with no behavioral claim affected. (auto-applied by the truth pipeline; adversarially gated) - 2026-07-11 — [email protected] (main) changes Stock Adjustment mid-form warehouse-change behavior. Previously,
handleWarehouseChangeinStockAdjustmentForm.tsxbranched onsimplified: the Salesmade/simplified build re-snapshotted line-item quantities against the new warehouse, but the FoodOps (non-simplified) build hit a different path — 'Foodops: changing warehouse re-bases the line items, so clear them' — wiping the entire items table whenever the Warehouse selector was changed. This commit range (subject: 'fix(inventory): preserve line items on warehouse change to match legacy behavior') deletes the FoodOps-only clear-on-change branch entirely:handleWarehouseChangenow unconditionally callsreSnapshotOnWarehouseChange, which uses a new sharedsnapshotItemsForWarehouse()helper to re-fetch Quantity Available/Value for every existing line against the newly-selected warehouse (serial/batch-tracked lines are left untouched) instead of clearing the table — for both brands. The same helper is now also used in the item-detail 'Adjust' prefill flow, re-snapshotting prefilled rows againstprefillWarehouseIdrather than showing the item's global stock. Does not contradict any current claim in this entry — the Walkthrough's step order ('Pick the Warehouse' step 5, then 'Add from list to pick items' step 7) documents the intended forward flow and never asserted what happens if the Warehouse is changed after items are already added, so nothing here needs correction. It is new user-facing behavior worth documenting once UI-confirmed: on FoodOps, changing the Warehouse mid-form no longer discards your item selection — Quantity Available simply re-syncs to the new location's stock. Candidate for a short note under step 5 of the Stock Adjustment walkthrough or a new Edge case, pending a live/Playwright confirmation of the resulting on-screen behavior. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated) - 2026-07-11 — [email protected] (main) fixes a SKU-generation gap in ProductsApiController's Create and AddVariant flows that intersects this entry's documented Items-list claim ('SKU — auto-assigned (
00001,00002...) or custom'). Before this fix, when a caller omitted SKU,resolvedCodefell back toCode(if provided) or a 12-char slice of the new variant's GUID, andvariant.SKUwas then set to that sameresolvedCode— so an item created without an explicit SKU could end up with an unrelatedCodevalue or a GUID fragment as its SKU, not a sequential00001-style number. The fix introducesresolvedSku, which now callsSequenceNumberTypeHelper.GetAndUpdateNextNumber(_dbContext, ModelConstants.SequenceNumberTypes.ProductSKU, orgId)when SKU is blank (falling back to the GUID slice only if the sequence call itself returns empty), andCodenow defaults to that resolved SKU instead of the other way around — per the commit's own comment, 'Empty SKU -> next sequence number (00xxx); GUID slice only if the sequence fails.' This does not contradict the entry: the documented '00001, 00002...' auto-numbering behavior is exactly what this fix implements/restores on the Create and AddVariant code paths. Worth noting as closing a prior gap where that documented behavior could be bypassed. Also in the same commit range and out of scope for this entry: a new warehouse/outlet relation and a default-stock-location naming change to warehouse name (not touching Items/Adjustments/Transfers documented fields) and the already-covered stock-doubling-on-new-warehouse fix (see prior 2026-07-11 note). (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated) - 2026-07-11 — [email protected] (main) fixes a real stock-posting bug that intersects this entry's documented Stock Adjustment flow. When a stock adjustment (or a stock-in/receipt) created the first StockLocationProductVariant record for a product at a given warehouse,
StockHelper.cswas seedingCurrentStockLeveldirectly with the adjustment/receipt quantity (stockAdjustmentRequestItem.QuantityChangedorstockOutTransaction.Quantity) AND lettingStockTransaction.ChangeStatusapply the same quantity again via+=when the transaction completed — doubling the resulting stock-on-hand for that first movement at a new location. Fixed by seedingCurrentStockLevel = 0at record-creation time (four call sites inStockHelper.cs) and letting theChangeStatus+=do the only increment, per the commit's own explanatory comment ('Seeding it here too double-counts the very first adjustment for a new location') and the commit subject 'Fixed: stock doubled when adjustment created with new warehouse'. This does not contradict any current claim in this entry — the entry's existing text ('Stock On Hand... computed from these stock transactions... recomputed on each movement,' and the Draft→Adjusted adjustment flow) describes the intended behavior, which is what this fix restores; it was never documented as having a doubling defect, so nothing here needs correction, only a note that a first-movement-at-a-new-warehouse doubling bug existed pre-fix and is now closed. Unrelated in the same commit range and out of scope for this entry: newSalesOrders.Edit.RestrictDecreaseQuantity/RestrictRemoveItempermissions and a newCostingrole group inModelConstants.cs(no touch to Items/Adjustments/Transfers surfaces), and a new null-CustomerId guard inAllocateStockToInvoice(per-customer virtual delivery-location stock allocation for invoices — a sales/invoicing concern this entry does not document). (auto-applied by the truth pipeline; adversarially gated) - 2026-07-08 — [email protected] (main) adds two backend changes touching this entry's documented flows, neither of which contradicts existing text but both are new user-facing surface not yet captured. (1) Warehouse-active validation: Create/Update on StockAdjustmentsApiController and StockTransfersApiController (and the not-yet-documented StockCountsApiController) now call a new IsActiveWarehouseAsync/GetActiveWarehouseIdsAsync check and return 400 'Warehouse not found or is not active.' ('Source'/'Destination warehouse not found or is not active.' for transfers) if the selected warehouse has been deactivated — previously no such guard existed. This is a new error condition on the 'Pick the Warehouse' (adjustments) and 'Source/Destination Warehouse' (transfers) steps documented in the Walkthrough sections; worth a note under Edge cases once confirmed how the admin-app surfaces the 400 (no foodops-admin-app diff in this range, so the UI treatment — toast vs inline field error — is unconfirmed). (2) BillOfMaterialsApiController.SoftDelete/Restore now actually persist the status change (
bom.BillOfMaterialStatusId = Deleted/Draft; await _dbContext.SaveChangesAsync(ct);) instead of only invalidating cache and returning OK — extending the same 'real persistence, not response-only stubs' pattern this entry already documents for Create/ChangeStatus/line-items (2026-07 note), but SoftDelete/Restore weren't previously called out as part of that fix. GetAll also now excludes Deleted-status BoMs from listing results. Both are API-level only; scope as capability pending a UI re-walk, consistent with this entry's existing Kit/Assembly and BoM precedent. Does not contradict any current claim (warehouse selection defaults, Draft→Adjusted state machine, transfer lifecycle, or the existing 'adjustment endpoint can fall back to Primary Warehouse' known issue, which is about ID propagation, not active/inactive status, and is unaffected by this diff). (auto-applied by the truth pipeline; adversarially gated) - 2026-07-04 — hubits-api-inventory@31237f1 (main) hardens the Item operation log ("Item Detail History tab"): Update no longer unconditionally writes a bare 'Edited' row on every save — it now snapshots per-field values before/after the mutation (Name, SKU, Code, Barcode, Selling Price, Selling Price Incl. Tax, Cost Price, Minimum Stock Level, Critical Stock Level, Allow Negative Stock?, Available for Dine-In/Takeout/Delivery?, Is Modifier?) via a new VariantSnapshot()/GetSnapshotDifferences() diff, and only logs an Edited entry when a real field changed (or a kit component was touched), now carrying the field-level diff (e.g. 'Name: New
Old'). Separately, AddVariant switched from an untracked existence check (AnyAsync) to a tracked load of the parent Product so new variants are now also recorded on the product's operation log (previously not attached). This is API-level only — no foodops-admin-app diff is in scope here, so it's unconfirmed whether/how the admin item's History tab UI renders these richer diff rows; scope as API capability pending a UI re-walk, matching this entry's existing 'API-level, UI re-walk pending' precedent for Kit/Assembly and Bill-of-Materials. Does not contradict any currently documented claim — SKU-as-natural-key, IsModifier, and the single item-level low-stock threshold framing (Minimum Stock Level appears as a per-variant field, consistent with the entry's existing 'Threshold is set on the item' language) all remain intact; the separate 'Critical Stock Level' field is new evidence not yet tied to any documented user-facing behavior and should not be assumed equivalent to the documented low-stock threshold without a UI check. (auto-applied by the truth pipeline; adversarially gated) - 2026-07-04 — hubits-api-inventory@aa5c715 (main) adds multi-tax support per product variant: a new
TaxTypeIdslist (backed by a newProductVariantTaxesjoin table viaSyncProductVariantTaxes()) is now read/written on Create, AddVariant, UpdateVariant, GetById, ListVariants, and GetVariantDetail inProductsApiController.cs, alongside the existing singularTaxTypeId. Backward compatibility is preserved: the legacyTaxTypeIdcolumn is kept synced to the first tax in the effective list (variant.TaxTypeId = effective.FirstOrDefault()), and an emptyTaxTypeIdsfalls back to the legacy single value. This is API-level only — the diff does not touchfoodops-admin-app, so it's unconfirmed whether the Items walkthrough's single 'tax type' selection step (step 2 under 'Walkthrough — add a new item') has gained a multi-select UI yet. Scope as API capability pending an admin-app UI re-walk, matching the existing Kit/Assembly and Bill-of-Materials 'API-level, UI re-walk pending' precedent already in this entry. (Unrelated in the same commit range: Cloudflare Images migration + new product-image CRUD endpoints — out of scope for this entry, which does not currently document item images.) (auto-applied by the truth pipeline; adversarially gated)
Success: Verified 2026-04-15 · playwright (frontmatter refresh 2026-05-04 · codebase) Status:
stable· Review due: 2026-07-14 This entry was verified against the live app on 2026-04-15. Frontmatter v2 refresh on 2026-05-04: tightenedHaafai.Inventory/**to per-feature globs (Views/Products,Views/StockAdjustments,Views/StockTransfers,Views/Stocks,Services), droppedhubits-api-inventory(Tier 2, not in default drift sweep). Anchorba7d8c5is the freshest substantive commit on the inventory module since 2026-04-15. Recent commits are bug fixes — no UI/state-machine drift.
2026-06-03 — source_repos re-pointed for the 2026-06-03 backend microservice split. The prior (2026-05-04) frontmatter refresh had DROPPED hubits-api-inventory as "Tier 2, not in default drift sweep" — re-ADDED it now that it is the live inventory backend post-split: ProductsApiController + StocksApiController + StockAdjustmentsApiController + StockTransfersApiController + StockCountsApiController + Services/Api/** + DTOs/{Products,StockAdjustments,StockTransfers}/**, pinned to main HEAD d0dd020. Also ADDED foodops-admin-app (inventory backoffice — pages/inventory/** + components/inventory/**, the StockAdjustments/StockTransfers/StockCounts/Warehouses screens) pinned to main HEAD f3ae3e9. All added paths verified present on main via gh api. Existing haafai.app.foodops/Haafai.Inventory/** globs retained as the monolith fallback during the split (re-verified at ba7d8c5, all still resolve on main). No fallback/broken globs found.
2026-06-04 — content re-verification against hubits-api-inventory@d0dd020 (main). REAL CHANGE: commit 4ed0add (2026-06-02) added an IsModifier boolean to the product-variant DTOs (CreateProductVariantRequest/UpdateProductVariantRequest/ProductVariantDetailResponse) and wired it through Create/AddVariant/UpdateVariant/GetVariantDetail in ProductsApiController. User-facing surface is the "Set as Modifier" toggle on the FoodOps item form (foodops-admin-app ItemFormPanel.tsx:921, gated to isRestaurantBrand). Added a Key-concept line and two Q&As. Inventory/Service item types, Stock Adjustment reasons + Draft→Adjusted state machine, and the Stock Transfer lifecycle all re-verified unchanged against d0dd020; post-watermark commits to Stocks/Adjustments/Transfers/Counts controllers are tracing/observability + new transfer receive/cancel endpoints that match the documented Received/partial-receive behavior — no user-facing drift.
2026-06-29 — NEW-STACK re-verification for July-10 launch (legacy haafai.app.foodops monolith fully retired). Re-pointed all five legacy haafai.app.foodops/Haafai.Inventory/** source_repos globs to the new stack: stock-posting engine now lives in haafai.domain (Haafai.Models/Services/Stocks/StockHelper.cs, StockTransactionReasonHelper.cs, StockAdjustments/**, StockTransfers/**, Constants/ModelConstants.cs @ staging e1389444ca), API surface in hubits-api-inventory @ main 04df7a9c4b, back-office in foodops-admin-app @ main f6ffd0b19d. Verified: (1) sale stock deduction is live — StockHelper.cs:432/2205 posts a SalesDelivery stock transaction, and StockHelper.cs:3499-3504 consumes component stock for Kit/BoM products (AssemblyKitStockType == Kit); (2) Inventory vs Service item types, the Draft→Adjusted adjustment flow, and the transfer lifecycle all still present (controllers StockAdjustmentsApiController/StockTransfersApiController/StockCountsApiController exist on main). REAL CHANGES: adjustment reasons are now DB-seeded + customizable via StockTransactionReasonsApiController (api/v1/stock-transaction-reasons); the seeded Adjustment-group set per ModelConstants.StockTransactionReasons is Stocktaking Results / Damaged Goods / Stolen Goods / Stock Written Off / Stock On Fire / New Stock Entry / Inventory Revaluation / Adjustment — added the two missing reasons + the customizable note. KNOWN ISSUE flagged: sale stock-out resolves location from productVariant.DefaultStockLocationId (StockHelper.cs:3504) and adjustments can fall back to Primary Warehouse — documented as an edge case (intended = sell-outlet warehouse).
Additional verification events are appended here by the weekly KB sync and by manual re-verification passes. See _meta/failure-log.md for the global failure index.