kitchen/backend/migrations/add_recipe_system.py
jtricerolph 8d688b459d Initial kitchen scaffold — Phase 1 kitchen port (build-verified 2026-07-11)
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>
2026-07-12 12:15:39 +00:00

628 lines
29 KiB
Python

"""
Migration: Recipe, Ingredient & Food Flag System
Creates all tables for the recipe system and pre-seeds reference data.
"""
import asyncio
from sqlalchemy import text
from database import engine
async def migrate():
# ══ CRITICAL: Add columns to existing tables FIRST (own transaction) ══
# These MUST succeed even if new table creation fails, otherwise
# the KitchenSettings SQLAlchemy model breaks ALL existing endpoints.
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS api_key VARCHAR(100)"
))
await conn.execute(text(
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS api_key_enabled BOOLEAN DEFAULT false"
))
await conn.execute(text(
"ALTER TABLE line_items ADD COLUMN IF NOT EXISTS ingredient_id INTEGER REFERENCES ingredients(id) ON DELETE SET NULL"
))
await conn.execute(text(
"CREATE INDEX IF NOT EXISTS idx_line_items_ingredient ON line_items(ingredient_id)"
))
print("+ Added columns to existing tables (api_key, api_key_enabled, ingredient_id)")
except Exception as e:
print(f"! CRITICAL: Failed to add columns to existing tables: {e}")
# ── pg_trgm extension (separate transaction — may need superuser) ──
try:
async with engine.begin() as conn:
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS pg_trgm"))
print("+ Enabled pg_trgm extension")
except Exception as e:
print(f"- pg_trgm extension not available ({e}), fuzzy search will be limited")
# ══ Add missing UNIQUE constraints (tables may have been created by
# SQLAlchemy create_all WITHOUT these constraints) ══
try:
async with engine.begin() as conn:
for idx_name, idx_def in [
("uix_ingredient_categories_kn", "ingredient_categories(kitchen_id, name)"),
("uix_ingredients_kn", "ingredients(kitchen_id, name)"),
("uix_ingredient_sources_kisc", "ingredient_sources(kitchen_id, ingredient_id, supplier_id, product_code)"),
("uix_ingredient_flags_if", "ingredient_flags(ingredient_id, food_flag_id)"),
("uix_food_flag_categories_kn", "food_flag_categories(kitchen_id, name)"),
("uix_food_flags_kn", "food_flags(kitchen_id, name)"),
("uix_line_item_flags_lf", "line_item_flags(line_item_id, food_flag_id)"),
("uix_menu_sections_kn", "menu_sections(kitchen_id, name)"),
("uix_recipes_kn", "recipes(kitchen_id, name)"),
("uix_recipe_flags_rf", "recipe_flags(recipe_id, food_flag_id)"),
("uix_recipe_cost_snapshots_rd", "recipe_cost_snapshots(recipe_id, snapshot_date)"),
]:
await conn.execute(text(
f"CREATE UNIQUE INDEX IF NOT EXISTS {idx_name} ON {idx_def}"
))
print("+ Ensured all unique constraints/indexes exist")
except Exception as e:
print(f"! Warning adding unique indexes: {e}")
async with engine.begin() as conn:
# ── ingredient_categories ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ingredient_categories (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
name VARCHAR(100) NOT NULL,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(kitchen_id, name)
)
"""))
print("+ Created ingredient_categories table")
except Exception as e:
if "already exists" in str(e).lower():
print("- ingredient_categories table already exists, skipping")
else:
raise
# ── ingredients ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ingredients (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
name VARCHAR(255) NOT NULL,
category_id INTEGER REFERENCES ingredient_categories(id) ON DELETE SET NULL,
standard_unit VARCHAR(20) NOT NULL,
yield_percent NUMERIC(5,2) DEFAULT 100.00,
manual_price NUMERIC(12,6),
notes TEXT,
is_archived BOOLEAN DEFAULT false,
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(kitchen_id, name)
)
"""))
print("+ Created ingredients table")
except Exception as e:
if "already exists" in str(e).lower():
print("- ingredients table already exists, skipping")
else:
raise
# ── ingredient_sources ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ingredient_sources (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
ingredient_id INTEGER NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
supplier_id INTEGER NOT NULL REFERENCES suppliers(id),
product_code VARCHAR(100),
description_pattern VARCHAR(255),
pack_quantity INTEGER,
unit_size NUMERIC(10,3),
unit_size_type VARCHAR(10),
latest_unit_price NUMERIC(10,2),
latest_invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL,
latest_invoice_date DATE,
price_per_std_unit NUMERIC(12,6),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(kitchen_id, ingredient_id, supplier_id, product_code)
)
"""))
print("+ Created ingredient_sources table")
except Exception as e:
if "already exists" in str(e).lower():
print("- ingredient_sources table already exists, skipping")
else:
raise
# Partial unique index for no-SKU items (product_code IS NULL)
try:
await conn.execute(text("""
CREATE UNIQUE INDEX IF NOT EXISTS uix_ingredient_source_desc
ON ingredient_sources(kitchen_id, ingredient_id, supplier_id, description_pattern)
WHERE product_code IS NULL
"""))
print("+ Created partial unique index uix_ingredient_source_desc")
except Exception as e:
if "already exists" in str(e).lower():
print("- Index uix_ingredient_source_desc already exists, skipping")
else:
raise
# ── food_flag_categories ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS food_flag_categories (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
name VARCHAR(100) NOT NULL,
propagation_type VARCHAR(20) NOT NULL,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(kitchen_id, name)
)
"""))
print("+ Created food_flag_categories table")
except Exception as e:
if "already exists" in str(e).lower():
print("- food_flag_categories table already exists, skipping")
else:
raise
# ── food_flags ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS food_flags (
id SERIAL PRIMARY KEY,
category_id INTEGER NOT NULL REFERENCES food_flag_categories(id) ON DELETE CASCADE,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
name VARCHAR(100) NOT NULL,
code VARCHAR(10),
icon VARCHAR(10),
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(kitchen_id, name)
)
"""))
print("+ Created food_flags table")
except Exception as e:
if "already exists" in str(e).lower():
print("- food_flags table already exists, skipping")
else:
raise
# ── ingredient_flags (canonical flag source) ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ingredient_flags (
id SERIAL PRIMARY KEY,
ingredient_id INTEGER NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
food_flag_id INTEGER NOT NULL REFERENCES food_flags(id) ON DELETE CASCADE,
flagged_by INTEGER REFERENCES users(id),
source VARCHAR(20) DEFAULT 'manual',
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(ingredient_id, food_flag_id)
)
"""))
print("+ Created ingredient_flags table")
except Exception as e:
if "already exists" in str(e).lower():
print("- ingredient_flags table already exists, skipping")
else:
raise
# ── line_item_flags (data entry mechanism) ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS line_item_flags (
id SERIAL PRIMARY KEY,
line_item_id INTEGER NOT NULL REFERENCES line_items(id) ON DELETE CASCADE,
food_flag_id INTEGER NOT NULL REFERENCES food_flags(id) ON DELETE CASCADE,
flagged_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(line_item_id, food_flag_id)
)
"""))
print("+ Created line_item_flags table")
except Exception as e:
if "already exists" in str(e).lower():
print("- line_item_flags table already exists, skipping")
else:
raise
# ── menu_sections ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS menu_sections (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
name VARCHAR(100) NOT NULL,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(kitchen_id, name)
)
"""))
print("+ Created menu_sections table")
except Exception as e:
if "already exists" in str(e).lower():
print("- menu_sections table already exists, skipping")
else:
raise
# ── recipes ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS recipes (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
name VARCHAR(255) NOT NULL,
recipe_type VARCHAR(20) NOT NULL,
menu_section_id INTEGER REFERENCES menu_sections(id) ON DELETE SET NULL,
description TEXT,
batch_portions INTEGER NOT NULL DEFAULT 1,
prep_time_minutes INTEGER,
cook_time_minutes INTEGER,
notes TEXT,
is_archived BOOLEAN DEFAULT false,
kds_menu_item_name VARCHAR(255),
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(kitchen_id, name)
)
"""))
print("+ Created recipes table")
except Exception as e:
if "already exists" in str(e).lower():
print("- recipes table already exists, skipping")
else:
raise
# ── recipe_ingredients ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS recipe_ingredients (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
ingredient_id INTEGER NOT NULL REFERENCES ingredients(id) ON DELETE RESTRICT,
quantity NUMERIC(10,3) NOT NULL,
notes TEXT,
sort_order INTEGER DEFAULT 0
)
"""))
print("+ Created recipe_ingredients table")
except Exception as e:
if "already exists" in str(e).lower():
print("- recipe_ingredients table already exists, skipping")
else:
raise
# ── recipe_sub_recipes ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS recipe_sub_recipes (
id SERIAL PRIMARY KEY,
parent_recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
child_recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE RESTRICT,
portions_needed NUMERIC(10,3) NOT NULL,
notes TEXT,
sort_order INTEGER DEFAULT 0,
CHECK(parent_recipe_id != child_recipe_id)
)
"""))
print("+ Created recipe_sub_recipes table")
except Exception as e:
if "already exists" in str(e).lower():
print("- recipe_sub_recipes table already exists, skipping")
else:
raise
# ── recipe_steps ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS recipe_steps (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
step_number INTEGER NOT NULL,
instruction TEXT NOT NULL,
image_path VARCHAR(500),
duration_minutes INTEGER,
notes TEXT
)
"""))
print("+ Created recipe_steps table")
except Exception as e:
if "already exists" in str(e).lower():
print("- recipe_steps table already exists, skipping")
else:
raise
# ── recipe_images ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS recipe_images (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
image_path VARCHAR(500) NOT NULL,
caption TEXT,
image_type VARCHAR(20) DEFAULT 'general',
sort_order INTEGER DEFAULT 0,
uploaded_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW()
)
"""))
print("+ Created recipe_images table")
except Exception as e:
if "already exists" in str(e).lower():
print("- recipe_images table already exists, skipping")
else:
raise
# ── recipe_flags (manual additions + override state) ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS recipe_flags (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
food_flag_id INTEGER NOT NULL REFERENCES food_flags(id) ON DELETE CASCADE,
source_type VARCHAR(20) NOT NULL,
is_active BOOLEAN DEFAULT true,
excludable_on_request BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(recipe_id, food_flag_id)
)
"""))
print("+ Created recipe_flags table")
except Exception as e:
if "already exists" in str(e).lower():
print("- recipe_flags table already exists, skipping")
else:
raise
# ── recipe_flag_overrides (audit log) ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS recipe_flag_overrides (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
food_flag_id INTEGER NOT NULL REFERENCES food_flags(id) ON DELETE CASCADE,
action VARCHAR(20) NOT NULL,
note TEXT NOT NULL,
user_id INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW()
)
"""))
print("+ Created recipe_flag_overrides table")
except Exception as e:
if "already exists" in str(e).lower():
print("- recipe_flag_overrides table already exists, skipping")
else:
raise
# ── recipe_change_log ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS recipe_change_log (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
change_summary TEXT NOT NULL,
user_id INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW()
)
"""))
print("+ Created recipe_change_log table")
except Exception as e:
if "already exists" in str(e).lower():
print("- recipe_change_log table already exists, skipping")
else:
raise
# ── recipe_cost_snapshots ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS recipe_cost_snapshots (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
cost_per_portion NUMERIC(12,6) NOT NULL,
total_cost NUMERIC(12,6) NOT NULL,
snapshot_date DATE NOT NULL,
trigger_source VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(recipe_id, snapshot_date)
)
"""))
print("+ Created recipe_cost_snapshots table")
except Exception as e:
if "already exists" in str(e).lower():
print("- recipe_cost_snapshots table already exists, skipping")
else:
raise
# ── event_orders ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS event_orders (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
name VARCHAR(255) NOT NULL,
event_date DATE,
notes TEXT,
status VARCHAR(20) DEFAULT 'DRAFT',
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
"""))
print("+ Created event_orders table")
except Exception as e:
if "already exists" in str(e).lower():
print("- event_orders table already exists, skipping")
else:
raise
# ── event_order_items ──
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS event_order_items (
id SERIAL PRIMARY KEY,
event_order_id INTEGER NOT NULL REFERENCES event_orders(id) ON DELETE CASCADE,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE RESTRICT,
quantity INTEGER NOT NULL,
notes TEXT,
sort_order INTEGER DEFAULT 0
)
"""))
print("+ Created event_order_items table")
except Exception as e:
if "already exists" in str(e).lower():
print("- event_order_items table already exists, skipping")
else:
raise
# (ALTER TABLE for existing tables already ran in separate transaction above)
# ── Indexes ──
for idx_name, idx_def in [
("idx_ingredients_kitchen", "ingredients(kitchen_id)"),
("idx_ingredients_category", "ingredients(category_id)"),
("idx_ingredient_sources_ingredient", "ingredient_sources(ingredient_id)"),
("idx_ingredient_sources_supplier", "ingredient_sources(supplier_id)"),
("idx_ingredient_flags_ingredient", "ingredient_flags(ingredient_id)"),
("idx_ingredient_flags_flag", "ingredient_flags(food_flag_id)"),
("idx_food_flags_category", "food_flags(category_id)"),
("idx_line_item_flags_line_item", "line_item_flags(line_item_id)"),
("idx_recipes_kitchen", "recipes(kitchen_id)"),
("idx_recipes_type", "recipes(kitchen_id, recipe_type)"),
("idx_recipes_section", "recipes(menu_section_id)"),
("idx_recipe_ingredients_recipe", "recipe_ingredients(recipe_id)"),
("idx_recipe_sub_recipes_parent", "recipe_sub_recipes(parent_recipe_id)"),
("idx_recipe_sub_recipes_child", "recipe_sub_recipes(child_recipe_id)"),
("idx_recipe_steps_recipe", "recipe_steps(recipe_id)"),
("idx_recipe_images_recipe", "recipe_images(recipe_id)"),
("idx_recipe_flags_recipe", "recipe_flags(recipe_id)"),
("idx_recipe_cost_snapshots_recipe", "recipe_cost_snapshots(recipe_id)"),
("idx_event_orders_kitchen", "event_orders(kitchen_id)"),
("idx_event_order_items_order", "event_order_items(event_order_id)"),
]:
try:
await conn.execute(text(
f"CREATE INDEX IF NOT EXISTS {idx_name} ON {idx_def}"
))
except Exception:
pass # Skip silently — indexes are non-critical if they exist
print("+ Created indexes")
# ══ Seeding in separate transaction (so failures don't rollback table creation) ══
async with engine.begin() as conn:
# ── Pre-seed ingredient categories for all existing kitchens ──
try:
result = await conn.execute(text("SELECT id FROM kitchens"))
kitchen_ids = [row[0] for row in result.fetchall()]
categories = [
"Dairy", "Meat", "Seafood", "Produce", "Dry Goods",
"Oils & Fats", "Herbs & Spices", "Bakery", "Beverages",
"Condiments", "Other"
]
for kid in kitchen_ids:
for i, cat in enumerate(categories):
await conn.execute(text("""
INSERT INTO ingredient_categories (kitchen_id, name, sort_order, created_at)
VALUES (:kid, :name, :sort, NOW())
ON CONFLICT (kitchen_id, name) DO NOTHING
"""), {"kid": kid, "name": cat, "sort": i})
print(f"+ Pre-seeded ingredient categories for {len(kitchen_ids)} kitchen(s)")
except Exception as e:
print(f"! Warning seeding ingredient categories: {e}")
# ── Pre-seed food flag categories and flags ──
try:
allergy_flags = [
("Celery", "Ce", None),
("Gluten", "Gl", None),
("Crustaceans", "Cr", None),
("Eggs", "Eg", None),
("Fish", "Fi", None),
("Lupin", "Lu", None),
("Milk", "Mi", None),
("Molluscs", "Mo", None),
("Mustard", "Mu", None),
("Tree Nuts", "TN", None),
("Peanuts", "Pn", None),
("Sesame", "Se", None),
("Soya", "So", None),
("Sulphites", "Su", None),
]
dietary_flags = [
("Vegetarian", "V", None),
("Vegan", "Ve", None),
("Pescatarian", "Pe", None),
("Gluten-Free", "GF", None),
]
for kid in kitchen_ids:
# Create Allergy category (only seed flags if category is new)
result = await conn.execute(text("""
INSERT INTO food_flag_categories (kitchen_id, name, propagation_type, sort_order, created_at)
VALUES (:kid, 'Allergy', 'contains', 0, NOW())
ON CONFLICT (kitchen_id, name) DO NOTHING
RETURNING id
"""), {"kid": kid})
row = result.fetchone()
allergy_is_new = row is not None
if row:
allergy_cat_id = row[0]
else:
r = await conn.execute(text(
"SELECT id FROM food_flag_categories WHERE kitchen_id = :kid AND name = 'Allergy'"
), {"kid": kid})
allergy_cat_id = r.fetchone()[0]
# Create Dietary category (only seed flags if category is new)
result = await conn.execute(text("""
INSERT INTO food_flag_categories (kitchen_id, name, propagation_type, sort_order, created_at)
VALUES (:kid, 'Dietary', 'suitable_for', 1, NOW())
ON CONFLICT (kitchen_id, name) DO NOTHING
RETURNING id
"""), {"kid": kid})
row = result.fetchone()
dietary_is_new = row is not None
if row:
dietary_cat_id = row[0]
else:
r = await conn.execute(text(
"SELECT id FROM food_flag_categories WHERE kitchen_id = :kid AND name = 'Dietary'"
), {"kid": kid})
dietary_cat_id = r.fetchone()[0]
# Only seed flags for newly created categories (don't re-insert user-deleted flags)
if allergy_is_new:
for i, (name, code, icon) in enumerate(allergy_flags):
await conn.execute(text("""
INSERT INTO food_flags (category_id, kitchen_id, name, code, icon, sort_order, created_at)
VALUES (:cat_id, :kid, :name, :code, :icon, :sort, NOW())
ON CONFLICT (kitchen_id, name) DO NOTHING
"""), {"cat_id": allergy_cat_id, "kid": kid, "name": name, "code": code, "icon": icon, "sort": i})
if dietary_is_new:
for i, (name, code, icon) in enumerate(dietary_flags):
await conn.execute(text("""
INSERT INTO food_flags (category_id, kitchen_id, name, code, icon, sort_order, created_at)
VALUES (:cat_id, :kid, :name, :code, :icon, :sort, NOW())
ON CONFLICT (kitchen_id, name) DO NOTHING
"""), {"cat_id": dietary_cat_id, "kid": kid, "name": name, "code": code, "icon": icon, "sort": i})
print(f"+ Pre-seeded food flag categories and flags for {len(kitchen_ids)} kitchen(s)")
except Exception as e:
print(f"! Warning seeding food flags: {e}")
if __name__ == "__main__":
print("Running migration: add_recipe_system")
asyncio.run(migrate())
print("Migration complete!")