Foodops
All docs

Menu · Updated 2026-08-09 · v7202cde

Ingredients

Audio narration for this article is generated with the weekly docs refresh.

NEW stack only (2026-06-29, July-2026 launch). The legacy haafai.app.foodops Blazor monolith (its dedicated /ingredients screen + Haafai.Inventory controllers/views) is retired at launch. Ingredients are inventory products in the inventory API (hubits-api-inventory ProductsApiController, IsIngredient flag), authored via the admin app item form (foodops-admin-app — Catalog → Items → "Use as Ingredient"). Any haafai.app.foodops / Haafai.Inventory (monolith) citation below is historical (retired surface).

Plain-English summary

Ingredients are the raw materials consumed by recipes — flour, tomato, cooking oil. In Foodops an ingredient is not a separate object: it is a normal inventory item with the "Use as Ingredient" flag switched on ("Item is a raw material used in production"). Each ingredient has a unit (kg, litre, each — with optional alternate units and conversions) and its stock is tracked per location. Menu items consume ingredients through their recipes (bill of materials), so one sale can decrement several raw-material stocks, and a supplier price change on an ingredient flows into recipe cost.

When you'd use this

  • Setting up inventory tracking for a kitchen
  • Tracking raw-material cost into recipe cost
  • Doing stock counts at the ingredient level (not the menu-item level)
  • Building a recipe and needing the ingredient picker to actually find your raw materials

How to access

  • Admin backoffice (canonical): Catalog → Items (/catalog/items) — create the item and tick Use as Ingredient on the form. There is no separate "Ingredients" page in the admin app; ingredients are items with the flag. [code: foodops-admin-app/apps/admin/src/components/menu/ItemFormPanel.tsx:912-920]
  • Legacy back-office: the dedicated Ingredients screen (IngredientsController) in haafai.app.foodops is retired at the July-2026 launch (decommissioned with the monolith). Use the admin-app item form's "Use as Ingredient" flag instead.
  • API: ingredients live in the inventory service — GET /api/v1/products with filterIngredient (default true) includes/excludes them; IsIngredient is set on create/update. [code: hubits-api-inventory/Hubits.Inventory.Api/Controllers/ProductsApiController.cs:65,191,881,1096]

Walkthrough

Flagging an item as an ingredient (admin app)

Verified against foodops-admin-app@77ec95f.

  1. Catalog → Items → add or edit an item.
  2. In the flags row, tick Use as Ingredient ("Item is a raw material used in production"). It sits beside Sell as Menu Item and Set as Modifier — three independent toggles (an item can be any combination). The ingredient toggle is hidden only for Kit/Bundle product types (a 2026-07-22 fix, 35146e3, restored it for Service-type items — previously excluded alongside Kit/Bundle, now matching legacy behavior) and is scoped to the foodops brand. [code: ItemFormPanel.tsx:271-276,901-929]
  3. Configure the unit (and purchasing/inventory sections as for any stocked item). Save — the API persists IsIngredient on the product. [code: ProductsApiController.cs:881,1096]

Units and conversions

  1. On the item detail, the Units & conversions tab lists "Base unit and alternate units configured for this item." — served by the bill-of-materials API's available-units endpoint, which also exposes unit conversion-factor lookups used when a recipe consumes in a different unit than stock is held in. [code: foodops-admin-app UnitsTab.tsx:51-60; hubits-api-inventory BillOfMaterialsApiController.cs:311,419]

Consuming ingredients in a recipe

  1. On a menu item's Recipe tab (redesigned NEW 2026-07): the tab now supports multiple recipes (bills of materials) per menu item, one card each, replacing the old single-BOM view (previously only the first recipe in the list was loaded). Each recipe card has an editable header (Recipe Name + a Default recipe flag, saved via bomService.update), a Status dropdown driving a recipe workflow (bomService.getStatuses + bomService.changeStatus), and a New Recipe button adds another recipe to the item. [code: foodops-admin-app/apps/admin/src/components/menu/tabs/RecipeTab.tsx @ cc539d1 — const [boms, setBoms] = useState<Bom[]>([]); handleStatusbomService.changeStatus(bom.id, statusId); handleSaveHeaderbomService.update(bom.id, {name, isDefault})]
  2. Ingredient lines are now edited inline on the tab: the picker (placeholder now "Search ingredients to add…") calls a dedicated ingredient lookup — menuItemService.searchIngredients, replacing the generic menuItemService.search — and picking an ingredient adds it immediately at quantity 1, then opens inline edit where each line's Quantity, Unit (picked from bomService.availableUnits) and Cost/Unit are edited in place. Each recipe card shows a per-recipe Cost Summary — Ingredients total, Selling Price, and Food Cost % — so ingredient cost visibly rolls up into the item's margin on the tab itself. [code: RecipeTab.tsx @ cc539d1 — menuItemService.searchIngredients(search.trim()); handleSaveLinebomService.updateLineItem(bomId, line.id, {quantity, unitId, unitCost})]
  3. Recipe costing (Costing → Recipe Costing) builds ingredient rows with quantity and optional unit-cost override; at least one ingredient is required. (Unchanged by the 2026-07 Recipe-tab redesign — RecipeFormDialog.tsx is untouched in that range.) [code: RecipeFormDialog.tsx:75-94]
  4. Sales of the parent item deduct ingredient stock through the recipe — see Recipes for the deduction mechanics.

Stock at the ingredient level

  1. Ingredient stock is ordinary product stock: levels per location via the stocks API, counts via stock counts, movements via adjustments/transfers. [code: hubits-api-inventory StocksApiController.cs (/api/v1/stocks)]
  2. The item detail page shows "Set as Ingredient: Yes/No" among the variant facts. [code: foodops-admin-app ItemDetailPage.tsx:655-656]

Legacy flow (old back-office) — RETIRED

The monolith's Ingredients screen (created products via IngredientsHelper.CreateMenuProduct, updated variant stock immediately, exposed ingredient-only lookups filtered on Product.IsIngredient) is decommissioned at the July-2026 launch. The same IsIngredient flag is now set through the admin-app item form and persisted by the inventory API, so historical recipes/purchasing keyed on that flag carry over unchanged. Retained here only as historical context.

Key concepts

  • Ingredient = item flag, not entityIsIngredient is a boolean on Product. Everything else (units, stock, suppliers, cost) is standard inventory behaviour. [code: hubits-api-inventory DTOs/Products/ProductModels.cs:90]
  • Three independent flags — Use as Ingredient / Sell as Menu Item / Set as Modifier can combine freely: a "Cheese Slice" can be sold, used in recipes, and linked to a modifier option simultaneously. The modifier flag (added 2026-06-02, 4ed0add) marks a variant as linked to a modifier option for inventory & reporting; it does not change recipe consumption. [code: ItemFormPanel.tsx:901-929; ProductModels.cs (IsIngredient on Product, IsModifier on variant)]
  • filterIngredient on listings — product list endpoints take a filterIngredient flag (default true) so item pickers can include or exclude raw materials. [code: ProductsApiController.cs:65,191]
  • Units & conversion factors — an ingredient's base unit plus alternates; recipes can consume in grams while stock is held in kg, converted via the BoM API's conversion-factor lookup. [code: BillOfMaterialsApiController.cs:277-447]
  • Cost flow — ingredient cost feeds recipe cost (product-cost/{productVariantId}), which feeds menu-item margin in costing reports. [code: BillOfMaterialsApiController.cs:383]
  • Brand scoping — the ingredient toggle is foodops-only; the salesmade retail and finance brands hide it ("Only foodops has a production / recipe pipeline"). [code: ItemFormPanel.tsx:271-276]

Common questions

Q: How do I create an ingredient? A: Create a normal item in Catalog → Items and tick Use as Ingredient. Give it the unit you buy/count it in.

Q: Can an ingredient also be sold directly? A: Yes — also tick Sell as Menu Item. The flags are independent; a bottled water can be both sold and used in recipes.

Q: Why doesn't my ingredient appear in the recipe picker? A: Check the Use as Ingredient flag is on and the item is active. The recipe picker searches ingredient-flagged variants.

Q: What unit should I give an ingredient? A: The unit you stock-count in (kg, litre, each). Recipes can consume in a smaller unit via unit conversions — see the Units & conversions tab. [code: UnitsTab.tsx; BillOfMaterialsApiController.cs:419]

Q: Does selling a menu item reduce ingredient stock immediately? A: Deduction flows through the item's recipe — see Recipes for when deduction happens and how committed stock is reported.

Q: How do I see how much flour I have left? A: Inventory → Stock shows per-location levels for any stocked item, ingredients included; ingredient-level stock counts work the same as item counts.

Q: Do ingredients appear on the POS? A: Not unless they are also flagged Sell as Menu Item. Pure ingredients have no counter or customer-ordering footprint — they're consumed indirectly via recipes.

Q: How does an ingredient price change affect my menu costs? A: Updating the ingredient's cost re-prices recipes that consume it through the costing pipeline (recipe cost = Σ ingredient cost × quantity, with optional per-recipe overrides). [code: RecipeFormDialog.tsx:75-94; BillOfMaterialsApiController.cs:383]

Q: Can I import ingredients in bulk? A: You can bulk-import products through the inventory import pipeline (Import Wizard / menu-item imports), but the Salesmade (retail) import does NOT carry the ingredient flag — its handler defines an ExcludedColumns set containing isIngredient (alongside kitchen and isModifier) and filters those columns out, because retail brands have no production/recipe pipeline. So a Salesmade import creates the products, and "Use as Ingredient" must still be set afterwards (admin app → Catalog → Items, or the inventory API). [code: hubits-api-inventory/Hubits.Inventory.Api/Services/Import/SalesmadeProductImportHandler.cs:30-35,52 @ main — ExcludedColumns = { "kitchen", "isIngredient", "isModifier" }, applied via allColumns.Where(c => !ExcludedColumns.Contains(c.Name))] (Corrected 2026-07-28: this previously claimed the handler "explicitly maps IsIngredient columns" — it does the opposite.)

Q: What's the difference between an ingredient and a modifier? A: An ingredient is consumed inside a recipe (invisible to the customer). A modifier is a customer-facing choice on an item ("extra cheese"). The Set as Modifier flag exists so a modifier option can deplete a real stocked variant — see Modifiers.

Q: Is there a separate Ingredients page like the old app had? A: No — there's no separate Ingredients page. The old monolith /ingredients screen is retired at the July-2026 launch; the canonical path is the admin-app item form's "Use as Ingredient" flag.

Q: Who can manage ingredients? A: In the admin app, standard authenticated access governs the item form (no finer ingredient-specific gate found in current code). The legacy monolith's Products.View/Products.Create role gating retired with that app at the July-2026 launch.

Edge cases and known issues

  • Unit mismatch (grams vs kg) — recipes consuming in a unit with no configured conversion to the stock unit can mis-deduct; configure alternate units before authoring recipes (user story US-REC-EDGE-005 tracks this hazard). [code: BillOfMaterialsApiController.cs:419]

  • Unflagging an in-use ingredient — turning Use as Ingredient off does not detach existing recipe lines; the item simply stops appearing in new-ingredient pickers. Existing recipes keep consuming it (no guard found in current code as of 2026-06-11).

  • Kit/Bundle products can't be ingredients — the Use as Ingredient toggle is hidden only for Kit/Bundle product types; a 2026-07-22 fix (35146e3) restored the toggle for Service-type items (previously excluded) to match legacy behavior. [code: ItemFormPanel.tsx supportsIngredient(productTypeId)]

  • Single surface post-launch — with the monolith retired, the admin-app item form's "Use as Ingredient" flag is the only way to set IsIngredient; the old Ingredients screen no longer exists, so there's no two-surface drift risk. Pre-launch data set via the old screen carries over (same IsIngredient field).

  • 2026-08-07 — [email protected] ('feat(bom): reject line items that would create a circular recipe reference' + 'fix(recipes): cut a version on every composition-mutating path (REC-01)') adds two new backend behaviors on this entry's cited BillOfMaterialsApiController surface; neither contradicts a documented claim. (1) Circular-reference guard: AddLineItem now runs BillOfMaterialCycleValidator.FindCycleAsync before accepting a new line — if the ingredient variant being added has its own Active recipe that (directly or transitively) leads back to the recipe being edited, the request is rejected with 400 'This ingredient would create a circular recipe reference: {cyclePath}'. This is new validation layered onto the already-documented 'Add ingredient' picker flow (RecipeTab.tsx) — worth a line in the Walkthrough's 'Consuming ingredients in a recipe' step or the Edge-cases section: a recipe cannot consume, even indirectly, an ingredient that is itself produced by this recipe's own output item. (2) Recipe versioning (REC-01): Create, Update (header), AddLineItem, UpdateLineItem and RemoveLineItem on BillOfMaterialsApiController — plus a legacy full-recipe-upsert helper in ProductsApiController — now each call RecipeVersionHelper.CutIfChangedAsync after mutating, which freezes a version snapshot whenever the recipe's composition key (line items, quantity, unitId) actually changed; a Description/IsDefault-only header edit or a UnitCost-only recost correctly cuts nothing per the inline comments. This is a wholly new concept — versioned recipe history — not previously documented anywhere in this entry; no admin-app UI consumer is shown in this diff (backend-only), so the Recipe tab's currently-documented behavior is unaffected for now, but a future version-history surface would sit on top of this and should cross-reference here. Also in the same diff: ProductsApiController's legacy BOM-upsert helper swapped a hand-rolled TotalCost sum (which the removed comment says 'silently under-costed any EP line' once REC-03 landed) for the real bom.UpdateTotals(_dbContext) — a bug fix for an already-existing edible-portion cost-yield mechanic, not a new claim this entry makes, so not flagged as a contradiction. Unrelated in the same diff, out of scope for Ingredients: two haafai.domain dependency-version bumps (1.0.1045→1.0.1046) with no visible behavior change, and the 'library-conversions' commit ('implement API endpoints for library conversions and apply conversion functionality') — likely the backend the 2026-08-04 entry flagged as still unconfirmed for the admin-app's 'Quick add from standard-conversion library' flow — but no hunk for it was supplied in this diff range, so it can't be confirmed here; worth checking again once a library-conversions hunk is available. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-04 — [email protected] touches two files this entry cites but adds nothing that contradicts a documented claim; two additions are worth folding in as NEW_INFO. (1) UnitsTab.tsx gains two new ways to add an alternate-unit conversion beyond the manual Add-conversion form documented in the 2026-07-23 entry: a 'Quick add from standard-conversion library' flow (openLibrarybomService.getLibraryConversions(productVariantId) populates a picklist of common conversions grouped by unit category; handleApplyLibrarybomService.applyLibraryConversion({productVariantId, standardConversionId}) applies the selected one directly), and an inline 'create a new unit' mini-form nested inside the add panel (handleCreateUnitunitOfMeasureService.create({name, symbol, unitCategoryId, isOnlyWholeNumbers: false, isPublished: true}), falling back to a dropdown reload if the API doesn't echo the new id). Together these mean a user configuring an ingredient's alternate units no longer needs to know the exact numeric conversion factor by hand, nor leave the tab to define a brand-new unit first — both are now inline. This directly extends this entry's 'Unit mismatch (grams vs kg)' edge case ('configure alternate units before authoring recipes') with an easier on-ramp; worth a short Walkthrough/Units-tab addition once the corresponding bomService/unitOfMeasureService backend routes are confirmed (only the frontend hunk was supplied here, mirroring the caveat in the 2026-07-23 entry). (2) MenuItemsPage.tsx adds a real-time cache-invalidation listener: it now subscribes to an OrderHub SignalR connection (getOrderHubConnection(orgId)) and invalidates the ['sales','menu-items'] query on a MenuItemsUpdated event, which the added inline comment states hubits-api-sales broadcasts 'after every BOM deduction' — i.e. after a counter-app order payment deducts ingredient stock through the recipe. This doesn't change this entry's documented deduction mechanics (still 'see Recipes for the deduction mechanics'), but it does mean the admin-app items list now reflects post-sale ingredient/BOM stock deductions live rather than waiting out the prior 30s staleTime — worth a one-line note under 'Stock at the ingredient level' or a cross-reference from recipes.md. Unrelated in the same diff, out of scope for Ingredients: ItemFormPanel.tsx's outlet-loading refactor (outletService.getOutlets → a new useOutlets(orgId) hook) and the removal of the 'Leave empty to make available at all outlets.' hint text on the 'Items sold at Outlets' field — neither touches the ingredient-toggle lines (901-929/912-920) or any cited ingredient/BoM surface; also unrelated: a billing-page currency-display fix and an exports job-status design doc. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-28 — [email protected] ('Fix alcyon issues' + a payments-form batch) touches two files this entry cites, but neither hunk is about the ingredient flag/recipe/unit-conversion surface — it's an item-lifecycle (archive/restore) and list-UI bug-fix pass, tangential at most. ItemDetailPage.tsx gains an isArchived derivation (variant.recordStatusId === RECORD_STATUS_ARCHIVED) that now disables Receive, Adjust, Edit and Clone (Restore is the only live action) once an item is archived — since an ingredient is just an item with IsIngredient set, an archived ingredient's stock can no longer be received/adjusted nor its 'Use as Ingredient' flag toggled via the item form until it's restored. This narrows, but does not contradict, this entry's 'Stock at the ingredient level' claim that stock movements go through the ordinary adjustments/transfers UI — that's still true for non-archived items, archived is simply a new gate not previously documented (or previously existing) anywhere in this entry. Separately, MenuItemsPage.tsx fixes row-identity on the Archived tab (rowKeyOf keys rows by id:productVariantId instead of bare product id, and groupByProduct is now forced off when statusFilter === 'archived') so bulk-select/toggle don't collide across an archived product's variants — a list-UI correctness fix with no ingredient-specific behavior. Neither hunk touches IsIngredient, BoM/RecipeTab, unit conversions, or the three-toggle row, so no claim in this entry is contradicted; worth at most a one-line Edge-cases footnote about archived ingredients being locked from Receive/Adjust/Edit until restored. Unrelated in the same diff, fully out of scope for Ingredients: the payments-form commits (contact number field, receipt file upload, allocation-wiping fix, varchar(20) clamp) touch no menu/inventory/BoM surface at all. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-23 — [email protected] (commit: 'feat: allow adding/editing/deleting unit conversions from item Units tab') turns the item detail's Units & conversions tab from a read-only list into a full CRUD surface. UnitsTab.tsx now loads the base unit plus configured alternate-unit conversions via bomService.getUnitConversions(productVariantId) (replacing the prior bomService.availableUnits(productVariantId) call this entry's Walkthrough/Key-concepts cite at BillOfMaterialsApiController.cs:311,419) alongside the full unit catalog via unitOfMeasureService.dropdown(). Users can now, directly from the Units tab: Add a new alternate-unit conversion — pick any unit not already the base or an existing conversion, from any category ('matching legacy, where a product's packaging can cross dimensions' per the inline comment), enter a numeric value >0 and an optional example description — via bomService.addUnitConversion; Edit an existing conversion's value/example inline via bomService.updateUnitConversion; and Delete a conversion (behind a confirm dialog) via bomService.deleteUnitConversion. This is the admin-app UI consumer for the unit-conversions CRUD endpoints added to BillOfMaterialsApiController in [email protected] (already noted in the 2026-07-22 verification entry as backend-only with 'no foodops-admin-app diff in this range shows a UI wired to these endpoints yet'). It also directly answers this entry's 'Unit mismatch (grams vs kg)' edge case, which advises to 'configure alternate units before authoring recipes' without ever specifying a mechanism — the Add-conversion flow on the Units tab is that mechanism. Does not contradict the Walkthrough's existing description of the tab (it never claimed the tab was read-only, nor that available-units was its only possible data source), so no edit is required; worth a short Walkthrough addition documenting the add/edit/delete flow and updating the internal code citation from the available-units endpoint to the newer unit-conversions endpoints. Caveat: only the UnitsTab.tsx frontend hunk was supplied in this diff range — no bomService.ts or backend controller hunk to directly confirm which routes getUnitConversions/addUnitConversion/updateUnitConversion/deleteUnitConversion hit, though the naming strongly implies the unit-conversions endpoints from the 2026-07-22 backend entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-22 — [email protected] (commit 'feat: allow editing recipe output quantity') adds an editable Output Quantity field to the per-recipe header on the Recipe tab, alongside the already-noted-in-history Output Unit field (2026-07-12 verification entry). RecipeTab.tsx gained editQuantity state (seeded from bom.quantity in startEditHeader), a numeric 'Output Quantity' input rendered next to Recipe Name/Output Unit, and handleSaveHeader now validates parsedQuantity > 0 (toast 'Output quantity must be greater than 0' otherwise) before including quantity: parsedQuantity in the bomService.update payload. The recipe header therefore now sets three fields — Name, Output Unit, Output Quantity — plus the Default flag, not just 'Recipe Name + a Default recipe flag' as the current Walkthrough step 1 states; worth folding into the still-pending Output Unit note. Separately in the same diff range, commit 'fix: show "Use as Ingredient" for Service items to match legacy behavior' is handled above via edits[] (direct contradiction of the physical-good-only claim). Unrelated in the same diff, out of scope for Ingredients: an 'alcyon issues' fix touching no visible ingredient/recipe/BoM/unit-conversion/stock surface in the supplied hunks. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-22 — hubits-api-inventory diff 468b93d..a44d2df (commit: 'feat: add per-item unit conversion CRUD endpoints to bill-of-materials API') adds two new endpoints to BillOfMaterialsApiController not yet covered by this entry: GET unit-conversions/{productVariantId} lists a product variant's base unit plus all its configured alternate-unit conversions (id, conversion unit id/name/symbol, value, list price, list price with tax, example description, source), and POST unit-conversions adds a new alternate-unit conversion — deriving the alternate unit's selling price by multiplying the variant's base ListPrice/ListPriceWithTax by the conversion Value, mirroring the legacy MenuItemsController.AddConversionFactor behavior per the inline comment (any-category unit conversions allowed; duplicates blocked only on exact unit+value match). The add action is gated [Authorize(Roles = ModelConstants.Roles.Products.Edit)], consistent with the already-documented 2026-07-16 authorization overhaul on this controller. This is new surface adjacent to — but at higher line numbers than, and additive to — this entry's existing 'Units and conversions' walkthrough (which cites only the read-side available-units/conversion-factor lookups at BillOfMaterialsApiController.cs:311,419) and the 'Unit mismatch (grams vs kg)' edge case, which advises to 'configure alternate units before authoring recipes' without ever specifying a mechanism — these two new endpoints look like exactly that configuration mechanism, previously undocumented. Does not contradict any existing claim (the entry never asserted conversions were read-only or that no add-endpoint existed), so no edit is required. No foodops-admin-app diff in this range shows a UI wired to these endpoints yet — this is backend-only for now; worth a short walkthrough/edge-case addition once (or if) an admin-app consumer for adding alternate units appears. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-20 — [email protected] (commit: 'fix(catalog): variant item create/edit — add Default Stock Location field...') extends the Default-Stock-Location requirement previously documented (2026-07-11 entry, for the single/non-variant item form) to the variant-item create/edit flow: a new required Field (id 'it-variant-stock-loc', FormSelect over formOptions.stockLocations, no 'None' option) now renders inside the isVariantFlow block whenever showInventory is true, alongside VariantBuilder. Since ingredients are inventory-tracked physical-good items (per this entry's own Key Concepts/Edge Cases), an ingredient created via the variant flow (e.g. an item with size/pack variants, one flagged Use as Ingredient) now also requires a Default Stock Location per variant before it can be saved — closing a gap where the variant path previously lacked this requirement. Does not contradict any existing claim (the entry never asserted variant-flow items were exempt), so no edit is required; worth a short walkthrough note alongside the still-pending 2026-07-11 note. Unrelated in the same diff, out of scope for Ingredients: a toast now lists validation errors on failed submit (cosmetic), the 'Item Preparation' (kitchens) section is now hidden for variant-flow items to avoid showing a duplicate product-level kitchens picker (per-variant kitchens shown in the grid instead — a kitchens/prep concern, not ingredients/recipes/units/stock), and MenuItemsPage.tsx gained scroll-to-new-variant + silent-reload/preserve-expansion UX polish after adding a variant. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-16 — hubits-api-inventory diff db09019..fe5c8f4 adds role-based authorization across the entire inventory-API surface this entry cites. Controller-level bare [Authorize]/[SessionAuth] on ProductsApiController, CategoriesApiController, and BillOfMaterialsApiController became [Authorize(Roles = InventoryRolePolicies.ProductsRead)]; StocksApiController's became [Authorize(Roles = InventoryRolePolicies.StockAdjustmentsRead)]. Individual mutating actions across all three controllers (product/variant Create/Update/clone/toggle-enabled/gallery, category Create/Update/status/image/sort-order, and every BOM action — Create, Update, ChangeStatus, AddLineItem, UpdateLineItem, DeleteLineItem, SoftDelete, Restore) now additionally require ModelConstants.Roles.Products.Create/Edit/Delete per action. This directly contradicts the entry's 'Who can manage ingredients?' answer (see edits[]), which asserted no finer gate exists in current code beyond standard authentication — that claim was true when written (2026-06-11) but is now false. Unrelated in the same diff, out of scope for Ingredients: warehouse status filtering, per-warehouse stock-location auto-creation, a stock-doubling fix on first adjustment for a new warehouse, and BOM org-stamping on recipe creation. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-12 — [email protected] diff intersecting this entry's cited RecipeTab.tsx/ItemFormPanel.tsx surface. Two items in this range are already captured by prior verification entries and are NOT re-flagged here: (1) the recipe-card Delete button + confirm-and-soft-delete path and the 'Deleted' filter on the Status dropdown (2026-07-09 entry), and (2) ItemFormPanel.tsx's Default Stock Location becoming a required field with the includeNone→placeholder Field UI change (2026-07-11 entry). NEW here: the recipe header's editable fields grew a third field, Output Unit. editUnitId/allUnits state was added, populated via unitOfMeasureService.dropdown() when startEditHeader runs, rendered as a <select> alongside Recipe Name and the Default recipe checkbox, and now included in the bomService.update(bom.id, {name, unitId, isDefault}) save payload. This means the per-recipe header no longer just sets Name + Default flag — it also sets the recipe's own output unit (the unit the finished BOM/recipe is expressed in), distinct from each ingredient line's Quantity/Unit and from an ingredient's own base/alternate units already documented under Units & conversions. Does not contradict the Walkthrough's step 1 description of the header (it never claimed those two fields were exhaustive), but the step is now incomplete without mentioning Output Unit — worth a short addition alongside the still-pending Delete-button note. Unrelated in the same diff: two rounds of admin test-coverage additions, a PDF-template-edit fix, a PO-filters fix, and scattered onFocus={selectOnFocus} additions on numeric inputs (cosmetic select-on-focus UX, no behavior change worth documenting). ItemDetailPage.tsx patch unavailable, as in the 2026-07-11 pass. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-11 — [email protected]: the RecipeTab.tsx Delete-button/soft-delete change in this range is the same commit already captured by the 2026-07-09 Verification history entry (not re-flagged here). NEW relative to current entry content: ItemFormPanel.tsx now makes Default Stock Location a required field for any inventory-tracked item — if (showInventory && !defaultStockLocationId) next.defaultStockLocationId = 'Default stock location is required' — with a code comment explaining that without it 'the opening stock never lands in a warehouse (no StockLocationProductVariant row) and the item shows "No warehouse data available" in Stock by Location.' The Field UI dropped the includeNone/'— None —' option in favor of a required placeholder="— Select —" selector. Since ingredients are stocked/inventory-tracked items (per this entry's own Key Concepts and Edge Cases sections), this directly affects the ingredient-creation walkthrough step 'Configure the unit (and purchasing/inventory sections as for any stocked item)' — an ingredient item created without a Default Stock Location will now hit a new validation error it previously would not have. Does not contradict any existing claim (the entry never asserted Default Stock Location was optional), so no edit is required, but it's worth a short walkthrough note. Unrelated changes in the same commit range (PDF template edit fix, PO filters fix, two rounds of admin test-coverage additions, ItemDetailPage.tsx patch unavailable) do not touch this entry's documented ingredient/BoM/stock surface. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-11 — hubits-api-inventory diff 8df401c..2d99fed (commit 'fix(recipe): complete soft-delete/restore') fixes a real backend gap in the recipe-deletion path first noted speculatively in the 2026-07-09 verification entry. Previously, BillOfMaterialsApiController.SoftDelete fetched the BOM row but never actually set BillOfMaterialStatusId or saved the change — the per-recipe Delete button (bomService.softDelete) only invalidated cache, it did not persist a Deleted status. GetAll also had no filter excluding Deleted BOMs. This patch fixes both: SoftDelete now sets BillOfMaterialStatusId = Deleted and saves; GetAll now filters out BillOfMaterialStatusId == Deleted; Restore now explicitly sets status back to Draft and saves. Net effect: the recipe-card Delete action (already described in the 2026-07-09 note as 'presumably' setting a Deleted status server-side) now actually works end-to-end and deleted recipes are correctly excluded from the card list on reload — this had likely been silently broken until now. Does not contradict any claim currently in the entry body (recipe deletion still isn't documented in the main Walkthrough, only flagged as a gap); this closes that gap with confirming/completing evidence. Unrelated in the same diff: ProductsApiController Create/AddVariant SKU-resolution now falls back to a sequence-number (00xxx) generator instead of a GUID slice when SKU is omitted — out of scope for ingredients (no IsIngredient/BoM/unit-conversion/stock surface touched); also unrelated: warehouse status filtering, per-warehouse stock-location creation, and a stock-doubling fix on first adjustment for a new warehouse. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-09 — [email protected] adds a recipe-deletion path to the Recipe tab that this entry does not yet document. Each recipe card now has a destructive Delete button (next to 'Add ingredient') that opens a confirm dialog ('Delete recipe?' / '"" will be removed from this item.'); confirming calls bomService.softDelete(bom.id) and reloads the card list. This is a soft-delete (the BOM record presumably keeps a Deleted status server-side) but 'Deleted' is now explicitly filtered out of the Status dropdown's own option list (statuses.filter(s => s.name?.toLowerCase() !== 'deleted')), so deletion is a dedicated action rather than something reachable by picking 'Deleted' from the Status dropdown. Does not contradict anything currently documented — the entry never mentioned recipe deletion or a 'Deleted' status option — but the Walkthrough's multi-recipe description ('a New Recipe button adds another recipe to the item') is now incomplete without a corresponding removal path. Consider adding a short step/bullet noting the per-recipe Delete button + confirm-and-soft-delete behavior. Unrelated changes in the same commit range (PDF template edit fix, PO filters fix, two rounds of admin test-coverage additions) do not touch this entry's cited surface. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-04 — hubits-api-inventory diff 8225783..a359fd1 (post 04df7a9) shows BillOfMaterialsApiController backend catching up to the already-documented multi-recipe Recipe-tab redesign: a dedicated GET /statuses endpoint was added (backing bomService.getStatuses for the Status dropdown), BomResponse gained SellingPrice/TotalCost/StatusId/StatusName/IsDefault/OutputUnitId fields (backing the per-recipe Cost Summary), BOM line item UnitCost now returns the real li.CostPrice instead of a hardcoded 0 (backing the inline Cost/Unit editing), and the recipe header Name now sources from BillOfMaterial.Description rather than the linked ProductVariant's name (enabling distinct names across multiple recipes on the same item, consistent with the multi-recipe-per-item model). None of this contradicts the entry's documented claims — it is backend implementation now matching/enabling behavior already described — but it's worth noting as confirming evidence. Unrelated to this entry: the same diff also adds a Kit/Bundle assembly feature (AssemblyKitStockType, KitComponents, KitBillOfMaterialId) and multi-tax-per-variant support (TaxTypeIds) to ProductsApiController — out of scope for Ingredients, belongs in a Kits/Bundles or Tax entry if one exists. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-04 — [email protected] changed how the item form is reached from Catalog → Items: MenuItemsPage.tsx no longer renders ItemFormPanel as a modal Dialog (the local formOpen/editing state and the <ItemFormPanel> JSX were removed); 'Add item' and the row 'Edit' action now call navigate('/catalog/items/new') / navigate('/catalog/items/:productVariantId/edit') instead. ItemFormPanel gained a presentation prop ('page' | 'modal', default 'modal') so it can render as stacked full-page sections via SalesFormShell rather than a tabbed dialog. This does not contradict anything this entry asserts (the entry never specifies modal vs. page), but it does mean the Walkthrough's 'Catalog → Items → add or edit an item' step now lands on its own URL/page rather than opening an overlay — relevant to the entry's existing UI-TODO screenshot note. The diff does not touch the ingredient-toggle lines themselves (ItemFormPanel.tsx:901-929/912-920) or any other cited ingredient/BoM/stock behavior — those claims remain accurate. Unrelated changes in the same commit range (FormSelect Radix-display bugfix, multi-tax name join on ItemDetailPage, product-family image upload, Field grid layout) do not touch documented ingredient behavior. (auto-applied by the truth pipeline; adversarially gated)

Warning: Code-verified 2026-05-04 · codebase only (auth.json stale) Verified_by: codebase · Last verified: 2026-05-04 Source-repo globs curated against real haafai paths. Ingredients are authored in IngredientsController.cs + Views/Ingredients/** in haafai.app.foodops and stored as inventory items via Haafai.Inventory/Controllers/ProductsController.cs. No commits to any of these paths since 2026-04-15 — ingredient-side code is dormant. Removed foodops.counter.app: src/menu/** and foodops.ordering.api: src/menu/** from the previous broken-glob declarations: ingredients have no counter-side or customer-ordering UI footprint (they're consumed indirectly by recipes for stock deduction). Verified against haafai.app.foodops:6fe837f. Stub status retained — full Playwright walk still pending.

2026-05-04 — codebase verification at haafai/haafai.app.foodops:6fe837f; UI re-walk pending auth refresh

2026-06-03 — source_repos re-pointed for the 2026-06-03 backend microservice split. Ingredients are modeled as inventory products, so ADDED hubits-api-inventory (products/stock/categories API — ProductsApiController.cs + StocksApiController.cs + CategoriesApiController.cs + DTOs/Products/**) pinned to main HEAD d0dd020, and foodops-admin-app (product/ingredient SETUP backoffice — MenuItemsPage + ItemFormPanel + menu tabs/UnitsTab.tsx for UoM) pinned to main HEAD f3ae3e9. All added paths verified present on main via gh api. Existing haafai.app.foodops (Ingredients controller/views + Haafai.Inventory products/stocks/views/viewmodels) globs retained as the monolith fallback during the split — all still resolve on prod. No fallback/broken globs found. Stub status retained.

2026-06-04 — content re-verification against hubits-api-inventory@d0dd020 (main). Ingredients are still modeled as inventory products (IsIngredient on Product); no change to ingredient CRUD, unit handling, or recipe-linked stock deduction. NEW adjacent feature: commit 4ed0add (2026-06-02) added an IsModifier flag to product variants, surfaced as the "Set as Modifier" toggle next to "Use as Ingredient" on the FoodOps item form (foodops-admin-app ItemFormPanel.tsx:921) — added a clarifying note that the three item flags are independent. Stub status retained; full Playwright walk still pending.

  • 2026-06-11 — codebase: promoted stub → draft against hubits-api-inventory@5150eac, foodops-admin-app@77ec95f, haafai.app.foodops@f06cdd9 (all prod). Full entry written around the ingredient-as-item-flag model: the three-toggle row (ItemFormPanel:901-929), filterIngredient listing param, Units & conversions tab (BoM API available-units / conversion-factor / product-cost), Recipe tab + RecipeFormDialog consumption, ingredient-level stock via /api/v1/stocks, and the still-functional legacy Ingredients screen. Added the BillOfMaterialsApiController glob to source_repos (path verified on main). CORRECTION vs old stub: there is no /ingredients URL in the new admin app — that access note belonged to the legacy app only. Confidence medium; UI pending Playwright walk.

2026-06-29 — NEW-stack re-verification for the July-10 launch (legacy haafai.app.foodops monolith RETIRED). Re-pinned to prod HEADs: hubits-api-inventory@04df7a9, foodops-admin-app@f6ffd0b, haafai.domain@e1389444 (staging, ADDED — BoM entities). Dropped the six retired haafai/haafai.app.foodops source_repos (IngredientsController, Views/Ingredients, Haafai.Inventory Products/Stocks controllers/views/viewmodels). CONFIRMED on prod: inventory API ProductsApiController/StocksApiController/CategoriesApiController/BillOfMaterialsApiController + admin-app ItemFormPanel/UnitsTab/RecipeTab all resolve; IsIngredient flag model intact. CORRECTIONS: marked the legacy Ingredients screen + "Legacy flow" section + the related Q&As as RETIRED (the canonical surface is the admin-app item-form "Use as Ingredient" flag); reframed the "legacy vs new surface drift" edge case to "single surface post-launch". Human-curated Plain-English/Key-Concepts/Common-questions preserved; three-flag (Use as Ingredient / Sell as Menu Item / Set as Modifier) model unchanged. NOT re-walked live (code-only).

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.

Go beyond POS.
Choose the total solution.

Focus on creating the best experience for your guests while we handle the rest.