FastAPI backend (Python 3.11, MSSQL ODBC for SambaPOS, Azure DI OCR),
kitchen_db on central PG. React/TS/Vite frontend with navy sidebar layout.
Backend: auth.py (APP_SLUG=kitchen, SimpleNamespace — archive routes use
.kitchen_id/.is_admin without modification), main.py (51 migrations, scheduler,
internal router for KDS bookings feed), api/internal.py, full archive API
(31 routers: invoices, recipes, menus, sambapos, resos, newbook, disputes,
purchase_orders, etc.), models, migrations, OCR pipeline.
kitchen_id pinned to 1 (B1 — single hotel).
Frontend: AuthGate (app=kitchen, token shim for archive compat — B5b pending),
Layout (navy sidebar, 6 sections, Lucide icons, teal --app-primary),
App.tsx (Outlet pattern, UploadApp outside Layout), index.css (full :root block).
strict: false — archive components have type issues; build clean.
Note: 45 archive components call fetch('/api/...') without /kitchen/ prefix
(B5b). Runtime 404s; deferred until after initial testing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
286 lines
13 KiB
Python
286 lines
13 KiB
Python
"""
|
|
Migration: Add allergen_keywords table for keyword-based allergen suggestion,
|
|
plus is_prepackaged/product_ingredients/label_image_path columns on ingredients.
|
|
Seeds default keywords from OFF allergens taxonomy per kitchen.
|
|
"""
|
|
import asyncio
|
|
from sqlalchemy import text
|
|
from database import engine
|
|
|
|
|
|
# Default keywords informed by Open Food Facts allergens taxonomy, FSA guidance,
|
|
# UK food labelling terms, common food products, and dish/meal association keywords.
|
|
# Three tiers:
|
|
# 1. Direct ingredients (flour, milk, egg, etc.)
|
|
# 2. Derivative/processed forms (casein, malt extract, surimi, etc.)
|
|
# 3. Dish/meal associations — "this product often contains X" (carbonara, pesto, satay, etc.)
|
|
ALLERGEN_KEYWORDS = {
|
|
"Gluten": [
|
|
# --- Direct grains & flours ---
|
|
"wheat", "flour", "bread", "pasta", "barley", "rye", "oat", "spelt",
|
|
"couscous", "bulgur", "semolina", "noodle", "crouton", "breadcrumb",
|
|
"panko", "tortilla", "pita", "naan", "sourdough", "brioche",
|
|
"croissant", "pastry", "pastries", "biscuit", "cracker", "kamut", "durum",
|
|
"farro", "seitan", "malt", "starch",
|
|
# --- Extended grains & derivatives ---
|
|
"einkorn", "emmer", "triticale", "wheat bran", "wheat protein",
|
|
"wheat starch", "modified starch", "wheat rusk", "rusk",
|
|
"pearl barley", "barley malt", "malt extract", "malt vinegar",
|
|
"durum wheat", "wholemeal", "wholegrain", "wheat flour",
|
|
"rye flour", "barley flour", "oat fibre",
|
|
# --- Baked goods & products ---
|
|
"doughnut", "waffle", "bagel", "chapati", "focaccia", "ciabatta",
|
|
"pumpernickel", "pretzel", "pitta", "wrap", "flatbread",
|
|
"crumpet", "muffin", "cake", "sponge", "scone",
|
|
"pancake", "crepe", "batter", "dumpling",
|
|
# --- Dish/meal associations (often contains gluten) ---
|
|
"carbonara", "lasagne", "lasagna", "pizza", "quiche",
|
|
"tempura", "ramen", "gyoza", "dim sum", "spring roll",
|
|
"scotch egg", "bhaji", "samosa", "pie", "gratin",
|
|
"fondue", "gnocchi", "ravioli", "trifle", "brownie",
|
|
"tiramisu", "crumble", "french toast",
|
|
"soy sauce", # most soy sauce contains wheat
|
|
],
|
|
"Milk": [
|
|
# --- Direct dairy ---
|
|
"milk", "cream", "butter", "cheese", "yogurt", "yoghurt", "whey",
|
|
"casein", "lactose", "ghee", "mascarpone", "ricotta", "mozzarella",
|
|
"parmesan", "cheddar", "brie", "camembert", "gruyere", "halloumi",
|
|
"paneer", "creme fraiche", "custard", "bechamel", "dairy",
|
|
"condensed milk", "evaporated milk", "buttermilk", "kefir",
|
|
"quark", "fromage", "emmental", "gouda", "stilton", "feta",
|
|
"milk powder", "skimmed milk", "whole milk",
|
|
# --- Extended cheese varieties & derivatives ---
|
|
"taleggio", "pecorino", "roquefort", "manchego", "provolone",
|
|
"reblochon", "raclette", "edam", "havarti", "jarlsberg",
|
|
"wensleydale", "red leicester", "lancashire", "monterey jack",
|
|
"cottage cheese", "cream cheese", "processed cheese",
|
|
"clotted cream", "double cream", "single cream", "whipping cream",
|
|
"sour cream", "curd", "dulce de leche",
|
|
# --- Milk protein derivatives ---
|
|
"lactalbumin", "lactoglobulin", "caseinates", "sodium caseinate",
|
|
"calcium caseinate", "milk solids", "milk protein", "milk fat",
|
|
# --- Milk-containing products ---
|
|
"milk chocolate", "white chocolate", "ice cream", "gelato",
|
|
"panna cotta", "lassi", "skyr",
|
|
# --- Dish/meal associations (often contains milk) ---
|
|
"carbonara", "risotto", "gratin", "fondue", "quiche",
|
|
"bechamel", "mousse", "creme brulee", "souffle",
|
|
"tikka masala", "korma", "bisque", "chowder",
|
|
"ranch", "bearnaise", "scone", "pancake", "waffle",
|
|
"french toast", "brioche", "croissant", "brownie",
|
|
"tiramisu", "trifle", "gnocchi", "ravioli",
|
|
"naan", "tzatziki", "raita",
|
|
],
|
|
"Eggs": [
|
|
# --- Direct egg forms ---
|
|
"egg", "meringue", "mayonnaise", "aioli", "hollandaise",
|
|
"quiche", "frittata", "omelette", "albumin",
|
|
"egg white", "egg yolk", "whole egg", "dried egg", "egg powder",
|
|
"egg wash", "eggnog", "duck egg", "quail egg",
|
|
"lecithin", "lysozyme",
|
|
# --- Dish/meal associations (often contains egg) ---
|
|
"carbonara", "caesar", "pad thai", "ramen",
|
|
"scotch egg", "french toast", "pancake", "waffle",
|
|
"brioche", "croissant", "scone", "brownie",
|
|
"tiramisu", "mousse", "creme brulee", "souffle",
|
|
"trifle", "macaron", "meringue", "coleslaw",
|
|
"waldorf", "gnocchi", "ravioli", "tempura",
|
|
"tartare sauce", "ranch", "bearnaise",
|
|
"custard", "batter",
|
|
],
|
|
"Fish": [
|
|
# --- Common fish species ---
|
|
"cod", "salmon", "tuna", "haddock", "mackerel", "sardine", "sardines",
|
|
"anchovy", "anchovies", "trout", "bass", "bream", "sole", "plaice",
|
|
"halibut", "swordfish", "monkfish", "pollock", "herring",
|
|
"whitebait", "fish sauce", "worcestershire", "fish",
|
|
# --- Extended species ---
|
|
"hake", "coley", "john dory", "brill", "turbot", "dab",
|
|
"flounder", "sprat", "pike", "perch", "tilapia", "snapper",
|
|
"grouper", "barramundi", "sea bream", "sea bass",
|
|
"dover sole", "lemon sole", "pilchard",
|
|
# --- Processed fish products ---
|
|
"smoked salmon", "gravlax", "kipper", "rollmop", "surimi",
|
|
"fish stock", "bonito", "katsuobushi", "fish cake",
|
|
"fish finger", "fish pie", "taramasalata",
|
|
# --- Dish/meal associations (often contains fish) ---
|
|
"caesar", # anchovy in dressing
|
|
"kedgeree", "paella", "bouillabaisse", "sushi",
|
|
"tom yum", "laksa",
|
|
],
|
|
"Crustaceans": [
|
|
# --- Direct crustacean types ---
|
|
"prawn", "shrimp", "crab", "lobster", "crayfish", "langoustine",
|
|
"scampi", "crustacean",
|
|
# --- Extended terms ---
|
|
"crawfish", "king prawn", "tiger prawn",
|
|
"shrimp paste", "crab paste", "crab stick",
|
|
"potted shrimp",
|
|
# --- Dish/meal associations (often contains crustaceans) ---
|
|
"bisque", "thermidor", "paella", "bouillabaisse",
|
|
"tom yum", "laksa", "gumbo", "dim sum", "sushi",
|
|
"pad thai",
|
|
],
|
|
"Molluscs": [
|
|
# --- Direct mollusc types ---
|
|
"squid", "calamari", "octopus", "mussel", "clam", "oyster",
|
|
"scallop", "cockle", "whelk", "snail", "escargot", "mollusc",
|
|
# --- Extended terms ---
|
|
"cuttlefish", "abalone", "periwinkle", "razor clam",
|
|
"limpet", "winkle",
|
|
# --- Dish/meal associations (often contains molluscs) ---
|
|
"paella", "bouillabaisse", "chowder", "gumbo",
|
|
"marinara", # often includes shellfish
|
|
],
|
|
"Peanuts": [
|
|
# --- Direct forms ---
|
|
"peanut", "groundnut", "arachis", "monkey nut",
|
|
"peanut butter", "peanut oil", "peanut flour", "peanut paste",
|
|
"groundnut oil",
|
|
# --- Dish/meal associations (often contains peanuts) ---
|
|
"satay", "pad thai", "laksa", "kung pao",
|
|
],
|
|
"Tree Nuts": [
|
|
# --- Direct nut types ---
|
|
"almond", "hazelnut", "walnut", "cashew", "pecan", "pistachio",
|
|
"macadamia", "brazil nut", "pine nut", "chestnut", "praline",
|
|
"marzipan", "frangipane", "nougat",
|
|
# --- Extended nut products ---
|
|
"almond milk", "almond flour", "almond butter", "ground almond",
|
|
"hazelnut oil", "walnut oil", "cashew butter",
|
|
"pistachio paste", "pine kernel", "mixed nuts",
|
|
"nut butter", "nut milk", "nut oil",
|
|
"amaretti", "pecan pie",
|
|
# --- Dish/meal associations (often contains tree nuts) ---
|
|
"pesto", # pine nuts + parmesan
|
|
"baklava", "macaron", "korma", "waldorf",
|
|
"praline", "frangipane",
|
|
],
|
|
"Soya": [
|
|
# --- Direct soya forms ---
|
|
"soy", "soya", "tofu", "tempeh", "edamame", "miso", "tamari",
|
|
"soybean", "soy lecithin", "soya lecithin",
|
|
# --- Extended forms ---
|
|
"soy sauce", "soy protein", "soy flour", "soya oil",
|
|
"soy milk", "soybean oil", "soy protein isolate",
|
|
"bean curd", "natto", "kinako", "yuba",
|
|
# --- Dish/meal associations (often contains soya) ---
|
|
"teriyaki", "ramen", "gyoza", "dim sum", "spring roll",
|
|
"sushi", "stir fry",
|
|
],
|
|
"Celery": [
|
|
# --- Direct forms ---
|
|
"celery", "celeriac",
|
|
"celery salt", "celery seed", "celery oil", "celery powder",
|
|
# --- Dish/meal associations (often contains celery) ---
|
|
"waldorf", "bloody mary", "bolognese", "soffritto", "mirepoix",
|
|
],
|
|
"Mustard": [
|
|
# --- Direct forms ---
|
|
"mustard", "dijon",
|
|
"mustard seed", "mustard powder", "mustard oil",
|
|
"english mustard", "wholegrain mustard", "mustard flour",
|
|
# --- Dish/meal associations (often contains mustard) ---
|
|
"vinaigrette", "coleslaw", "dhal",
|
|
],
|
|
"Sesame": [
|
|
# --- Direct forms ---
|
|
"sesame", "tahini", "halva", "halvah",
|
|
"sesame oil", "sesame seed", "sesame paste", "gomashio",
|
|
# --- Dish/meal associations (often contains sesame) ---
|
|
"hummus", # tahini
|
|
"falafel", "ramen", "gyoza", "spring roll",
|
|
"sushi", "dim sum",
|
|
],
|
|
"Sulphites": [
|
|
# --- Chemical names ---
|
|
"sulphite", "sulfite", "sulphur dioxide", "sulfur dioxide",
|
|
"metabisulphite", "metabisulfite",
|
|
# --- E numbers ---
|
|
"e220", "e221", "e222", "e223", "e224", "e226", "e227", "e228",
|
|
# --- Chemical salt forms ---
|
|
"sodium sulphite", "sodium bisulphite", "potassium sulphite",
|
|
"calcium sulphite", "potassium bisulphite",
|
|
"sodium metabisulphite", "potassium metabisulphite",
|
|
# --- Foods commonly containing sulphites ---
|
|
"wine", "dried fruit", "vinegar", "cordial", "molasses",
|
|
# --- Dish/meal associations ---
|
|
"vinaigrette",
|
|
],
|
|
"Lupin": [
|
|
# --- Direct forms ---
|
|
"lupin", "lupine", "lupini",
|
|
"lupin flour", "lupin seed", "lupin bean",
|
|
],
|
|
}
|
|
|
|
|
|
async def migrate():
|
|
async with engine.begin() as conn:
|
|
# 1. Create allergen_keywords table
|
|
await conn.execute(text("""
|
|
CREATE TABLE IF NOT EXISTS allergen_keywords (
|
|
id SERIAL PRIMARY KEY,
|
|
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
|
|
food_flag_id INTEGER NOT NULL REFERENCES food_flags(id) ON DELETE CASCADE,
|
|
keyword VARCHAR(100) NOT NULL,
|
|
is_default BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
CONSTRAINT uq_allergen_keywords_kit_flag_kw UNIQUE (kitchen_id, food_flag_id, keyword)
|
|
)
|
|
"""))
|
|
await conn.execute(text(
|
|
"CREATE INDEX IF NOT EXISTS idx_allergen_keywords_kitchen ON allergen_keywords(kitchen_id)"
|
|
))
|
|
await conn.execute(text(
|
|
"CREATE INDEX IF NOT EXISTS idx_allergen_keywords_flag ON allergen_keywords(food_flag_id)"
|
|
))
|
|
print("+ Created allergen_keywords table")
|
|
|
|
# 2. Add new columns to ingredients table
|
|
for col_sql in [
|
|
"ALTER TABLE ingredients ADD COLUMN IF NOT EXISTS is_prepackaged BOOLEAN DEFAULT FALSE",
|
|
"ALTER TABLE ingredients ADD COLUMN IF NOT EXISTS product_ingredients TEXT",
|
|
"ALTER TABLE ingredients ADD COLUMN IF NOT EXISTS label_image_path VARCHAR(500)",
|
|
]:
|
|
try:
|
|
await conn.execute(text(col_sql))
|
|
except Exception:
|
|
pass
|
|
print("+ Added is_prepackaged, product_ingredients, label_image_path columns to ingredients")
|
|
|
|
# 3. Seed default keywords (separate transaction for safety)
|
|
async with engine.begin() as conn:
|
|
result = await conn.execute(text("SELECT id FROM kitchens"))
|
|
kitchen_ids = [row[0] for row in result.fetchall()]
|
|
|
|
for kid in kitchen_ids:
|
|
seeded = 0
|
|
for flag_name, keywords in ALLERGEN_KEYWORDS.items():
|
|
# Find the food_flag by name for this kitchen
|
|
flag_result = await conn.execute(text(
|
|
"SELECT id FROM food_flags WHERE kitchen_id = :kid AND name = :name LIMIT 1"
|
|
), {"kid": kid, "name": flag_name})
|
|
flag_row = flag_result.fetchone()
|
|
if not flag_row:
|
|
continue
|
|
flag_id = flag_row[0]
|
|
|
|
for kw in keywords:
|
|
await conn.execute(text("""
|
|
INSERT INTO allergen_keywords (kitchen_id, food_flag_id, keyword, is_default, created_at)
|
|
VALUES (:kid, :fid, :kw, TRUE, NOW())
|
|
ON CONFLICT (kitchen_id, food_flag_id, keyword) DO NOTHING
|
|
"""), {"kid": kid, "fid": flag_id, "kw": kw.lower()})
|
|
seeded += 1
|
|
|
|
print(f" Kitchen {kid}: seeded up to {seeded} allergen keywords")
|
|
|
|
print("+ Allergen keywords migration complete")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("Running migration: add_allergen_keywords")
|
|
asyncio.run(migrate())
|
|
print("Migration complete!")
|