Feature: ingredient-level allergen + dietary tagging with modifier-aware propagation #10
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Blocked by #3 (inventory / recipe BOM). Graceful fallback works before #3 — see Pathway notes.
Background
The current data model stores
menu_items.allergensas a flat array on the item — coarse and misleading. Two real cases it gets wrong:Industry consensus (Supy, Kitchen Cut, Apicbase, Lavu, Toast, Oracle Simphony, CertiStar, Menutech) converges on the same architecture: allergens live on ingredients, not on menu items, and a recipe BOM propagates them to dishes — including through modifiers, which contribute their own recipe lines. This issue tracks adopting that pattern.
The word "allergen" also carries a judgement. We can split the field by intent so the same plumbing carries neutral disclosure, regulatory disclosure, and positive dietary claims.
Goals
Pathway D — Hybrid, item-level fallback + computed when recipe exists
The cheapest pathway that solves both cases without forcing every operator to author recipes. Plays naturally with the tiered-modes work in #2:
barmenu_items.contains[]literal (today's behavior), no recipe expansionbistromaterials.contains_tagsfor the current selection; manual field still readable as a fallback for items without a recipefullbistroplus cross-contact (may_contain) inference from co-located materials / shared prep stations (#4 territory)Data model (sketch — finalize alongside #3)
Adds to the
materialstable from #3:Replace / split
menu_items.allergens(operator-facing):(
dietaryalready exists in spirit — this just splits the currentallergensfield along the contains / may_contain axis.)Computation rule (server-side, exposed via the existing
/menuendpoint)For a menu item, the effective sets are:
current_selectiondefaults to "allis_default=truemodifiers + base recipe lines". The webapp'sModifierSelectorrecomputes the active set on every selection change.Modifier-conditional ("Latte+Whole milk")
Falls out for free from #3's "modifiers get their own
recipe_lines":contains_tags=["dairy"]contains_tags=[],dietary_tags=["vegan"]containsrecomputes → no "dairy" badgeRemovable-on-request
Operator flags a
modifier_groups.allows_omission BOOLon groups where "skip this entirely" is valid. Combined with the recipe expansion, omitting a removable group drops its materials from the union. UX surfaces this as yellow in the traffic-light pattern (see UX section).UX (customer surface)
Traffic-light pattern — established in the industry (CertiStar is the canonical example, but the pattern is widespread):
containscontainsbut only via a removable / swappable selectioncontainsand no modifier can change thatcontainsfrom a non-removable recipe lineIn the menu card / item page, each contains-tag pill renders in its computed color. Hovering / tapping shows why ("Dairy comes from Whole milk — swap to Oat").
The
may_containset renders as a separate, dimmer badge ("⚠ may contain shellfish — shared fryer").Stretch — customer allergen profile + filter (separate issue once D ships)
Customer-side: store
{ allergies: [...] }in webapp local settings, color the entire menu by that profile, hide red items unless explicitly opted into.This is a UX layer on top of D, not a competing model. Filing it now as a stretch note so it doesn't get forgotten when D lands.
Naming / framing (decided here)
contains(neutral disclosure) andmay_contain(voluntary).dietaryname.containsfor compliance.Acceptance criteria
materials.{contains_tags, may_contain_tags, dietary_tags}columns added in the migration that ships with #3.menu_items.contains+menu_items.may_containcolumns; existingmenu_items.allergensmigrated →contains, retired.menu_items.dietarystays as-is (already exists).GET /restaurants/{id}/menureturns the computed contains / may_contain / dietary sets per item, plus the raw recipe expansion so the webapp can recompute on modifier change.ModifierSelectorrecomputes the active contains-set live as modifiers toggle.contains_tags,may_contain_tags,dietary_tagseditors on each materialbarmode, hide the materials editor and treat the manual fields as the only sourceOut of scope
contains_tagsriding in the kind-30402 payload, but the multi-restaurant aggregator UX (#8) is where that surfaces.See also
References