import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from database import engine, Base from middleware.license_check import LicenseCheckMiddleware from services.cloud_sync import start_cloud_sync # Import all models so SQLAlchemy can create their tables import models.user # noqa: F401 — also registers WaiterZone import models.table # noqa: F401 import models.printer # noqa: F401 import models.product # noqa: F401 import models.order # noqa: F401 — also registers OrderAuditLog, OrderDiscount import models.business_day # noqa: F401 import models.shift # noqa: F401 — registers WaiterShift, ShiftBreak import models.settings # noqa: F401 import models.flag # noqa: F401 — registers TableFlagDef, TableFlagAssignment import models.message # noqa: F401 — registers StaffMessage, StaffMessageAck, QuickMessageTemplate import models.reservation # noqa: F401 import models.notes # noqa: F401 — registers SiteNote, SiteTodo import models.expenses # noqa: F401 — registers Contact, Expense, ExpensePayment import models.customers # noqa: F401 — registers Customer import models.tabs # noqa: F401 — registers Tab, TabEntry, TabPayment from routers import auth, tables, products, orders, waiters, reports, system, setup as setup_router from routers import business_day as business_day_router from routers import shifts as shifts_router from routers import settings as settings_router from routers import flags as flags_router from routers import messages as messages_router from routers import sse as sse_router from routers import data_transfer as data_transfer_router from routers import connect_orders as connect_orders_router from routers import reservations as reservations_router from routers import notes as notes_router from routers.expenses import contacts_router, expenses_router from routers import customers as customers_router from routers import tabs as tabs_router def _run_migrations(): """Apply additive schema changes that create_all won't handle. Each migration gets its own connection so a no-op (column already exists) doesn't leave a dirty transaction that blocks subsequent migrations.""" from sqlalchemy import text migrations = [ "ALTER TABLE product_ingredients ADD COLUMN extra_cost REAL NOT NULL DEFAULT 0.0", "ALTER TABLE products ADD COLUMN image_url VARCHAR", "ALTER TABLE tables ADD COLUMN group_id INTEGER REFERENCES table_groups(id)", "ALTER TABLE table_groups ADD COLUMN prefix VARCHAR", "ALTER TABLE table_groups ADD COLUMN color VARCHAR", "ALTER TABLE products ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_preference_sets ADD COLUMN default_choice_id INTEGER", "ALTER TABLE product_preference_choices ADD COLUMN sub_choices TEXT", "ALTER TABLE product_preference_choices ADD COLUMN disables_subset INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_preference_sets ADD COLUMN shared_subset TEXT", "ALTER TABLE product_options ADD COLUMN sub_choices TEXT", # Zone-based access control """CREATE TABLE IF NOT EXISTS waiter_zones ( id INTEGER PRIMARY KEY AUTOINCREMENT, waiter_id INTEGER NOT NULL REFERENCES users(id), group_id INTEGER REFERENCES table_groups(id), assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", # Payment tracking on items "ALTER TABLE order_items ADD COLUMN paid_by INTEGER REFERENCES users(id)", "ALTER TABLE order_items ADD COLUMN paid_at DATETIME", "ALTER TABLE order_items ADD COLUMN payment_method VARCHAR", # Full audit log """CREATE TABLE IF NOT EXISTS order_audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL REFERENCES orders(id), event_type VARCHAR NOT NULL, waiter_id INTEGER REFERENCES users(id), item_ids TEXT, amount REAL, payment_method VARCHAR, note TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", # Waiter profile fields "ALTER TABLE users ADD COLUMN full_name VARCHAR", "ALTER TABLE users ADD COLUMN nickname VARCHAR", "ALTER TABLE users ADD COLUMN mobile_phone VARCHAR", "ALTER TABLE users ADD COLUMN avatar_url VARCHAR", # Quick options (flat, allow_multiple) """CREATE TABLE IF NOT EXISTS product_quick_options ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL REFERENCES products(id), name VARCHAR NOT NULL, price REAL NOT NULL DEFAULT 0.0, allow_multiple INTEGER NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0 )""", # allow_multiple flag on extras (product_options) "ALTER TABLE product_options ADD COLUMN allow_multiple INTEGER NOT NULL DEFAULT 0", # Discounts table (future-proofed, schema ready now) """CREATE TABLE IF NOT EXISTS order_discounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL REFERENCES orders(id), item_id INTEGER REFERENCES order_items(id), discount_type VARCHAR NOT NULL, discount_value REAL NOT NULL, applied_by INTEGER NOT NULL REFERENCES users(id), applied_at DATETIME DEFAULT CURRENT_TIMESTAMP, reason TEXT )""", # Business day scoping on orders "ALTER TABLE orders ADD COLUMN business_day_id INTEGER REFERENCES business_days(id)", # Shift attribution on paid items "ALTER TABLE order_items ADD COLUMN paid_in_shift_id INTEGER REFERENCES waiter_shifts(id)", # Seed default POS settings (INSERT OR IGNORE = no-op if already exists) "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('shifts.waiter_self_start', 'true', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('shifts.waiter_self_end', 'true', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('business_day.force_close_allowed', 'true', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('flags.display_mode', 'both', CURRENT_TIMESTAMP)", # Table flags """CREATE TABLE IF NOT EXISTS table_flag_defs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, emoji VARCHAR, color VARCHAR DEFAULT '#6b7280', text_color VARCHAR DEFAULT NULL, sort_order INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", # Migration: add text_color if upgrading from older schema "ALTER TABLE table_flag_defs ADD COLUMN text_color VARCHAR DEFAULT NULL", """CREATE TABLE IF NOT EXISTS table_flag_assignments ( id INTEGER PRIMARY KEY AUTOINCREMENT, table_id INTEGER NOT NULL REFERENCES tables(id), flag_id INTEGER NOT NULL REFERENCES table_flag_defs(id), assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP, assigned_by INTEGER REFERENCES users(id) )""", # Staff messaging """CREATE TABLE IF NOT EXISTS quick_message_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, body VARCHAR NOT NULL, sort_order INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", """CREATE TABLE IF NOT EXISTS staff_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, sender_id INTEGER NOT NULL REFERENCES users(id), body TEXT NOT NULL, target_waiter_ids TEXT NOT NULL DEFAULT '[]', table_ids TEXT NOT NULL DEFAULT '[]', created_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", """CREATE TABLE IF NOT EXISTS staff_message_acks ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER NOT NULL REFERENCES staff_messages(id), waiter_id INTEGER NOT NULL REFERENCES users(id), acked_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", # Seed default flag definitions "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (1, 'Χρειάζεται καθάρισμα', '🧹', '#ef4444', 1)", "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (2, 'Χρειάζεται Βοήθεια', '🆘', '#f97316', 2)", "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (3, 'Χρειάζεται Σερβιτόρο', '🔔', '#eab308', 3)", "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (4, 'Περιμένει να πληρώσει', '💳', '#3b82f6', 4)", "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (5, 'VIP', '⭐', '#8b5cf6', 5)", "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (6, 'Ευγενικός Πελάτης', '😊', '#22c55e', 6)", "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (7, 'Αγενής Πελάτης', '😤', '#dc2626', 7)", "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (8, 'Αλλεργίες', '⚠️', '#f59e0b', 8)", "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (9, 'Παιδιά στο τραπέζι', '👶', '#06b6d4', 9)", "INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (10, 'Επέτειος / Γενέθλια', '🎂', '#ec4899', 10)", # Seed default quick message templates "INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (1, 'Σε χρειάζομαι τώρα', 1)", "INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (2, 'Πάρε διάλειμμα', 2)", "INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (3, 'Ετοιμάσου για κλείσιμο', 3)", "INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (4, 'Ήρθε νέος πελάτης', 4)", "INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (5, 'Ο πελάτης περιμένει να πληρώσει', 5)", # Product lifecycle status (active / archived) "ALTER TABLE products ADD COLUMN lifecycle_status VARCHAR NOT NULL DEFAULT 'active'", # Favorite flags + ordering on all product sub-item types "ALTER TABLE product_quick_options ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_quick_options ADD COLUMN favorite_sort_order INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_options ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_options ADD COLUMN favorite_sort_order INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_ingredients ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_ingredients ADD COLUMN favorite_sort_order INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_preference_sets ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_preference_sets ADD COLUMN favorite_sort_order INTEGER NOT NULL DEFAULT 0", # Sub-category support "ALTER TABLE categories ADD COLUMN parent_id INTEGER REFERENCES categories(id)", "ALTER TABLE categories ADD COLUMN general_sort_order INTEGER NOT NULL DEFAULT 0", # Auto-expand flag for sub-categories on the PWA accordion "ALTER TABLE categories ADD COLUMN auto_expanded INTEGER NOT NULL DEFAULT 0", # Printer protocol field "ALTER TABLE printers ADD COLUMN protocol VARCHAR NOT NULL DEFAULT 'escpos_tcp'", # Compact (half-width) display flag for quick options "ALTER TABLE product_quick_options ADD COLUMN is_compact INTEGER NOT NULL DEFAULT 0", # Print layout + per-type font settings "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.ticket_mode', 'detailed', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_order_number', '48:1:0', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_meta', '0:0:0', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_item_name', '16:1:0', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_quick', '0:0:0', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_pref', '0:0:0', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_extra', '0:0:0', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_ingredient', '0:0:0', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_item_note', '0:0:0', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_order_note', '0:1:0', CURRENT_TIMESTAMP)", # Offline/emergency payment tracking "ALTER TABLE order_audit_log ADD COLUMN offline_uuid VARCHAR", "ALTER TABLE order_audit_log ADD COLUMN offline_at VARCHAR", "ALTER TABLE order_audit_log ADD COLUMN is_duplicate INTEGER NOT NULL DEFAULT 0", # Cancellation tracking on order items (for reports) "ALTER TABLE order_items ADD COLUMN cancelled_by INTEGER REFERENCES users(id)", "ALTER TABLE order_items ADD COLUMN cancel_reason TEXT", "ALTER TABLE order_items ADD COLUMN cancelled_at DATETIME", # Manager account fields (added for setup wizard / future password login) "ALTER TABLE users ADD COLUMN password_hash VARCHAR", "ALTER TABLE users ADD COLUMN email VARCHAR", # Venue identity settings "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('venue.name', '', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('venue.type', '', CURRENT_TIMESTAMP)", # Security settings "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.login_method', 'password', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.autofill_username', 'true', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.auto_lock', 'false', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.auto_lock_seconds', '300', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.auto_logout', 'false', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.auto_logout_seconds', '1800', CURRENT_TIMESTAMP)", # Xenia Connect — online order fields on orders table "ALTER TABLE orders ADD COLUMN source VARCHAR NOT NULL DEFAULT 'pos'", "ALTER TABLE orders ADD COLUMN online_order_ref VARCHAR", "ALTER TABLE orders ADD COLUMN online_order_cloud_id INTEGER", "ALTER TABLE orders ADD COLUMN online_status VARCHAR", "ALTER TABLE orders ADD COLUMN online_customer_name VARCHAR", "ALTER TABLE orders ADD COLUMN online_customer_phone VARCHAR", "ALTER TABLE orders ADD COLUMN online_customer_address TEXT", "ALTER TABLE orders ADD COLUMN online_customer_notes TEXT", "ALTER TABLE orders ADD COLUMN online_order_type VARCHAR", # Xenia Connect — digital menu fields on products "ALTER TABLE products ADD COLUMN digital_visible INTEGER NOT NULL DEFAULT 1", "ALTER TABLE products ADD COLUMN digital_available INTEGER NOT NULL DEFAULT 1", "ALTER TABLE products ADD COLUMN digital_name VARCHAR", "ALTER TABLE products ADD COLUMN digital_description VARCHAR", "ALTER TABLE products ADD COLUMN digital_price REAL", "ALTER TABLE products ADD COLUMN digital_discount REAL NOT NULL DEFAULT 0.0", "ALTER TABLE products ADD COLUMN digital_image_url VARCHAR", # Per-printer line width (chars per line) — default 48 for 80mm Jolimark "ALTER TABLE printers ADD COLUMN line_width INTEGER NOT NULL DEFAULT 48", # Product description (plain-text, for menu display) "ALTER TABLE products ADD COLUMN description TEXT", # Staff note (internal memo on a waiter's profile) "ALTER TABLE users ADD COLUMN note VARCHAR", # Reservations table """CREATE TABLE IF NOT EXISTS reservations ( id INTEGER PRIMARY KEY AUTOINCREMENT, guest_name VARCHAR NOT NULL, party_size INTEGER NOT NULL, phone VARCHAR, email VARCHAR, note TEXT, reserved_for DATETIME NOT NULL, table_id INTEGER REFERENCES tables(id), status VARCHAR NOT NULL DEFAULT 'pending', source VARCHAR NOT NULL DEFAULT 'manager_app', created_by_user_id INTEGER REFERENCES users(id), online_customer_id VARCHAR, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", # Phase 2A — product cost tracking "ALTER TABLE products ADD COLUMN cost_simple REAL", "ALTER TABLE products ADD COLUMN cost_breakdown TEXT", "ALTER TABLE order_items ADD COLUMN unit_cost REAL", # Phase 2B — staff payroll "ALTER TABLE users ADD COLUMN hourly_rate REAL", "ALTER TABLE waiter_shifts ADD COLUMN hourly_rate_snapshot REAL", # Phase 2G — pay-later tab system """CREATE TABLE IF NOT EXISTS tabs ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL REFERENCES customers(id), status VARCHAR NOT NULL DEFAULT 'open', opened_at DATETIME DEFAULT CURRENT_TIMESTAMP, closed_at DATETIME, closed_by_id INTEGER REFERENCES users(id), notes TEXT )""", """CREATE TABLE IF NOT EXISTS tab_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, tab_id INTEGER NOT NULL REFERENCES tabs(id), order_id INTEGER REFERENCES orders(id), order_item_id INTEGER REFERENCES order_items(id), amount REAL NOT NULL, description TEXT, created_by_id INTEGER NOT NULL REFERENCES users(id), created_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", """CREATE TABLE IF NOT EXISTS tab_payments ( id INTEGER PRIMARY KEY AUTOINCREMENT, tab_id INTEGER NOT NULL REFERENCES tabs(id), amount REAL NOT NULL, payment_method VARCHAR, received_by_id INTEGER NOT NULL REFERENCES users(id), notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", # Phase 2F — customer CRM """CREATE TABLE IF NOT EXISTS customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, nickname VARCHAR, phone VARCHAR, email VARCHAR, notes TEXT, is_active INTEGER NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by_id INTEGER NOT NULL REFERENCES users(id) )""", "ALTER TABLE orders ADD COLUMN customer_id INTEGER REFERENCES customers(id)", # Phase 2E — cash drawer reconciliation "ALTER TABLE waiter_shifts ADD COLUMN counted_cash_end REAL", "ALTER TABLE waiter_shifts ADD COLUMN cash_discrepancy REAL", "ALTER TABLE business_days ADD COLUMN store_opening_cash REAL", "ALTER TABLE business_days ADD COLUMN store_closing_cash REAL", "ALTER TABLE business_days ADD COLUMN store_cash_discrepancy REAL", # Phase 2D — expense tracking + supplier contacts """CREATE TABLE IF NOT EXISTS contacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, type VARCHAR NOT NULL DEFAULT 'supplier', phone VARCHAR, email VARCHAR, address TEXT, notes TEXT, is_active INTEGER NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", """CREATE TABLE IF NOT EXISTS expenses ( id INTEGER PRIMARY KEY AUTOINCREMENT, description VARCHAR NOT NULL, category VARCHAR NOT NULL, contact_id INTEGER REFERENCES contacts(id), total_amount REAL NOT NULL, paid_amount REAL NOT NULL DEFAULT 0.0, due_date DATETIME, business_day_id INTEGER REFERENCES business_days(id), created_by_id INTEGER NOT NULL REFERENCES users(id), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, notes TEXT )""", """CREATE TABLE IF NOT EXISTS expense_payments ( id INTEGER PRIMARY KEY AUTOINCREMENT, expense_id INTEGER NOT NULL REFERENCES expenses(id), amount REAL NOT NULL, paid_at DATETIME DEFAULT CURRENT_TIMESTAMP, paid_by_id INTEGER NOT NULL REFERENCES users(id), notes TEXT )""", # Phase 2C — notes & todos """CREATE TABLE IF NOT EXISTS site_notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, body TEXT NOT NULL, created_by_id INTEGER NOT NULL REFERENCES users(id), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, is_pinned INTEGER NOT NULL DEFAULT 0 )""", """CREATE TABLE IF NOT EXISTS site_todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, body TEXT NOT NULL, is_done INTEGER NOT NULL DEFAULT 0, done_at DATETIME, done_by_id INTEGER REFERENCES users(id), created_by_id INTEGER NOT NULL REFERENCES users(id), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, priority VARCHAR NOT NULL DEFAULT 'normal' )""", ] for sql in migrations: try: with engine.connect() as conn: conn.execute(text(sql)) conn.commit() except Exception: pass @asynccontextmanager async def lifespan(app: FastAPI): import asyncio from services.sse_bus import init_loop from services.reservation_tasks import start_reservation_tasks init_loop(asyncio.get_running_loop()) Base.metadata.create_all(bind=engine) _run_migrations() sync_task = await start_cloud_sync() reservation_task = await start_reservation_tasks() yield sync_task.cancel() reservation_task.cancel() app = FastAPI(title="POS Local Backend", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(LicenseCheckMiddleware) # Serve product images as static files IMAGE_DIR = "/app/data/product_images" os.makedirs(IMAGE_DIR, exist_ok=True) app.mount("/static/product_images", StaticFiles(directory=IMAGE_DIR), name="product_images") # Serve waiter avatars as static files AVATAR_DIR = "/app/data/avatars" os.makedirs(AVATAR_DIR, exist_ok=True) app.mount("/static/avatars", StaticFiles(directory=AVATAR_DIR), name="avatars") app.include_router(setup_router.router, prefix="/api/setup", tags=["setup"]) app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) app.include_router(tables.router, prefix="/api/tables", tags=["tables"]) app.include_router(products.router, prefix="/api/products", tags=["products"]) app.include_router(orders.router, prefix="/api/orders", tags=["orders"]) app.include_router(waiters.router, prefix="/api/waiters", tags=["waiters"]) app.include_router(reports.router, prefix="/api/reports", tags=["reports"]) app.include_router(system.router, prefix="/api/system", tags=["system"]) app.include_router(business_day_router.router, prefix="/api/business-day", tags=["business-day"]) app.include_router(shifts_router.router, prefix="/api/shifts", tags=["shifts"]) app.include_router(settings_router.router, prefix="/api/settings", tags=["settings"]) app.include_router(flags_router.router, prefix="/api/flags", tags=["flags"]) app.include_router(messages_router.router, prefix="/api/messages", tags=["messages"]) app.include_router(sse_router.router, prefix="/api/sse", tags=["sse"]) app.include_router(data_transfer_router.router, prefix="/api/data-transfer", tags=["data-transfer"]) app.include_router(connect_orders_router.router, prefix="/api/connect", tags=["connect"]) app.include_router(reservations_router.router, prefix="/api/reservations", tags=["reservations"]) app.include_router(notes_router.router, prefix="/api/notes", tags=["notes"]) app.include_router(contacts_router, prefix="/api/contacts", tags=["contacts"]) app.include_router(expenses_router, prefix="/api/expenses", tags=["expenses"]) app.include_router(customers_router.router, prefix="/api/customers", tags=["customers"]) app.include_router(tabs_router.router, prefix="/api/tabs", tags=["tabs"])