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, PrintJob 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 import models.waste # noqa: F401 — registers WasteLog import models.schedule # noqa: F401 — registers ScheduledShift import models.chat # noqa: F401 — registers Conversation, ConversationParticipant, ChatMessage import models.prep_zone # noqa: F401 — registers PrepZone, prep_zone_printers, product_prep_zones import models.pricing # noqa: F401 — registers PriceGroup, PriceModifier, Deal, PriceEventLog, WaiterDiscountSettings import models.recovery_code # noqa: F401 — registers RecoveryCode 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 ws as ws_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 from routers import waste as waste_router from routers import kds as kds_router from routers import schedule as schedule_router from routers import chat as chat_router from routers import prep_zones as prep_zones_router from routers import pricing as pricing_router from routers import fiscal as fiscal_router from routers import phone as phone_router from routers import recovery as recovery_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 )""", # Product unit of measure (piece / portion / kg / liter / gram / ml) "ALTER TABLE products ADD COLUMN unit_type VARCHAR NOT NULL DEFAULT 'piece'", # 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 2J — staff shift scheduling """CREATE TABLE IF NOT EXISTS scheduled_shifts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), scheduled_date DATE NOT NULL, start_time VARCHAR NOT NULL, end_time VARCHAR NOT NULL, notes TEXT, created_by_id INTEGER NOT NULL REFERENCES users(id), created_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", # Phase 2H — void/waste log """CREATE TABLE IF NOT EXISTS waste_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL REFERENCES products(id), quantity REAL NOT NULL, unit_cost_snapshot REAL, total_cost REAL, reason VARCHAR NOT NULL, reason_notes TEXT, logged_by_id INTEGER NOT NULL REFERENCES users(id), business_day_id INTEGER REFERENCES business_days(id), logged_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", # 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' )""", # Printer duplicate copies (0 = print once, 1 = print twice, etc.) "ALTER TABLE printers ADD COLUMN duplicates INTEGER NOT NULL DEFAULT 0", # Waiter cancellation permissions "ALTER TABLE users ADD COLUMN can_cancel_orders INTEGER NOT NULL DEFAULT 0", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('orders.waiter_cancellations_allowed', 'false', CURRENT_TIMESTAMP)", # KDS — kitchen display system status tracking "ALTER TABLE orders ADD COLUMN kds_status VARCHAR NOT NULL DEFAULT 'pending'", "ALTER TABLE orders ADD COLUMN order_type VARCHAR NOT NULL DEFAULT 'here'", "ALTER TABLE order_items ADD COLUMN kds_status VARCHAR NOT NULL DEFAULT 'pending'", # KDS notifications — message type + zone tagging on staff messages "ALTER TABLE staff_messages ADD COLUMN message_type VARCHAR NOT NULL DEFAULT 'manager'", "ALTER TABLE staff_messages ADD COLUMN kds_zone VARCHAR", # Chat system """CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, type VARCHAR NOT NULL DEFAULT 'direct', name VARCHAR, is_system INTEGER NOT NULL DEFAULT 0, created_by INTEGER REFERENCES users(id), created_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", """CREATE TABLE IF NOT EXISTS conversation_participants ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id INTEGER NOT NULL REFERENCES conversations(id), user_id INTEGER NOT NULL REFERENCES users(id), joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_read_at DATETIME )""", """CREATE TABLE IF NOT EXISTS chat_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id INTEGER NOT NULL REFERENCES conversations(id), sender_id INTEGER NOT NULL REFERENCES users(id), body TEXT NOT NULL, sent_at DATETIME DEFAULT CURRENT_TIMESTAMP, deleted_at DATETIME )""", # Prep Zones — replaces per-product printer assignment """CREATE TABLE IF NOT EXISTS prep_zones ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, description VARCHAR )""", """CREATE TABLE IF NOT EXISTS prep_zone_printers ( prep_zone_id INTEGER NOT NULL REFERENCES prep_zones(id), printer_id INTEGER NOT NULL REFERENCES printers(id), PRIMARY KEY (prep_zone_id, printer_id) )""", """CREATE TABLE IF NOT EXISTS product_prep_zones ( product_id INTEGER NOT NULL REFERENCES products(id), prep_zone_id INTEGER NOT NULL REFERENCES prep_zones(id), PRIMARY KEY (product_id, prep_zone_id) )""", # KDS decline tracking on items "ALTER TABLE order_items ADD COLUMN decline_note TEXT", # Prep zone notification name "ALTER TABLE prep_zones ADD COLUMN notification_name VARCHAR", # KDS status change timestamp on orders (for "latest change" sorting in PWA) "ALTER TABLE orders ADD COLUMN kds_status_changed_at DATETIME", # Auto-close table after full payment setting "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('orders.auto_close_on_full_payment', 'false', CURRENT_TIMESTAMP)", # Quick Add: allow direct 1-unit add from product list without opening config modal "ALTER TABLE products ADD COLUMN quick_add_enabled INTEGER NOT NULL DEFAULT 1", # Per-waiter settings + favorites sync (JSON blob) "ALTER TABLE users ADD COLUMN waiter_settings TEXT", # Prep zone print settings — synced with KDS auto-print controls "ALTER TABLE prep_zones ADD COLUMN auto_print VARCHAR NOT NULL DEFAULT 'none'", "ALTER TABLE prep_zones ADD COLUMN print_copies INTEGER NOT NULL DEFAULT 1", # Bypass KDS/serve flow: new orders immediately start as served "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('orders.bypass_kds_serve', 'false', CURRENT_TIMESTAMP)", # Prep zone: rename auto_print value 'primary' → 'master' for existing rows "UPDATE prep_zones SET auto_print = 'master' WHERE auto_print = 'primary'", # Prep zone: master printer (single FK) + per-role copy counts "ALTER TABLE prep_zones ADD COLUMN master_printer_id INTEGER REFERENCES printers(id)", "ALTER TABLE prep_zones ADD COLUMN master_copies INTEGER NOT NULL DEFAULT 1", "ALTER TABLE prep_zones ADD COLUMN secondary_copies INTEGER NOT NULL DEFAULT 1", # Prep zone: item sort / category grouping options "ALTER TABLE prep_zones ADD COLUMN sort_items_by VARCHAR NOT NULL DEFAULT 'order_time'", "ALTER TABLE prep_zones ADD COLUMN group_by_category INTEGER NOT NULL DEFAULT 0", "ALTER TABLE prep_zones ADD COLUMN category_order TEXT NOT NULL DEFAULT '[]'", # Prep zone: ticket formatting option "ALTER TABLE prep_zones ADD COLUMN print_checkboxes INTEGER NOT NULL DEFAULT 0", # Prep zone: KDS bypass options "ALTER TABLE prep_zones ADD COLUMN bypass_pending INTEGER NOT NULL DEFAULT 0", "ALTER TABLE prep_zones ADD COLUMN bypass_kds INTEGER NOT NULL DEFAULT 0", "ALTER TABLE prep_zones ADD COLUMN auto_ready_to_served INTEGER NOT NULL DEFAULT 0", # Prep zone: migrate existing master_printer_id from first printer in zone (best-effort) # (handled in Python at runtime, not SQL — no-op SQL here) # Product tags (JSON array of strings) "ALTER TABLE products ADD COLUMN tags TEXT", # Hide revenue from waiters on shift overview "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('shifts.hide_revenue_from_waiters', 'false', CURRENT_TIMESTAMP)", # WebSocket event log — append-only, pruned to 24h on startup """CREATE TABLE IF NOT EXISTS sync_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_type VARCHAR NOT NULL, payload TEXT NOT NULL, target_user_ids TEXT, created_at VARCHAR NOT NULL )""", "CREATE INDEX IF NOT EXISTS ix_sync_events_created_at ON sync_events (created_at)", # Migrate from legacy per-product printer to prep zones: clear old printer_zone_id "UPDATE products SET printer_zone_id = NULL WHERE printer_zone_id IS NOT NULL", # Per-printer ESC/POS code page index (n in ESC t n) — default 29 = CP737 on Jolimark "ALTER TABLE printers ADD COLUMN codepage_n INTEGER NOT NULL DEFAULT 29", # Price adjustment: per-item on-the-fly price delta (positive or negative) "ALTER TABLE order_items ADD COLUMN price_adjustment REAL NOT NULL DEFAULT 0.0", # Customer count: how many people are at the table for this order "ALTER TABLE orders ADD COLUMN customer_count INTEGER", # Service items: flag on products, these are non-inventoried table-service items "ALTER TABLE products ADD COLUMN is_service_item INTEGER NOT NULL DEFAULT 0", # Table seat count — physical seats at each table (foundation for per-seat dish assignment) "ALTER TABLE tables ADD COLUMN seat_count INTEGER", # Courses — optional course assignment for sequenced firing "ALTER TABLE order_items ADD COLUMN course_id INTEGER", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('orders.courses_enabled', 'false', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('orders.courses', '[]', CURRENT_TIMESTAMP)", # Print job lifecycle tracking — auto-retry system """CREATE TABLE IF NOT EXISTS print_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL REFERENCES orders(id), printer_id INTEGER NOT NULL REFERENCES printers(id), zone_id INTEGER, item_ids TEXT NOT NULL, copies INTEGER NOT NULL DEFAULT 1, status VARCHAR NOT NULL DEFAULT 'pending', retry_count INTEGER NOT NULL DEFAULT 0, first_attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_attempted_at DATETIME, succeeded_at DATETIME, cancelled_at DATETIME, cancel_reason VARCHAR )""", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('orders.waiter_price_adjust_allowed', 'false', CURRENT_TIMESTAMP)", # Pricing system — Phase 2 pricing overhaul """CREATE TABLE IF NOT EXISTS price_groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, description TEXT, color VARCHAR, is_active INTEGER NOT NULL DEFAULT 0, auto_enable_time VARCHAR, auto_disable_time VARCHAR, auto_days TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by INTEGER REFERENCES users(id) )""", """CREATE TABLE IF NOT EXISTS price_modifiers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, description TEXT, color VARCHAR, is_active INTEGER NOT NULL DEFAULT 1, is_favorite INTEGER NOT NULL DEFAULT 0, allow_stack INTEGER NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0, scope VARCHAR NOT NULL DEFAULT 'global', item_id INTEGER REFERENCES products(id), action_type VARCHAR NOT NULL, action_value REAL NOT NULL, round_to VARCHAR, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by INTEGER REFERENCES users(id), updated_at DATETIME, updated_by INTEGER REFERENCES users(id) )""", """CREATE TABLE IF NOT EXISTS price_modifier_conditions ( id INTEGER PRIMARY KEY AUTOINCREMENT, modifier_id INTEGER NOT NULL REFERENCES price_modifiers(id) ON DELETE CASCADE, condition_type VARCHAR NOT NULL, params TEXT NOT NULL DEFAULT '{}' )""", """CREATE TABLE IF NOT EXISTS price_modifier_targets ( id INTEGER PRIMARY KEY AUTOINCREMENT, modifier_id INTEGER NOT NULL REFERENCES price_modifiers(id) ON DELETE CASCADE, target_type VARCHAR NOT NULL, target_id INTEGER, target_tag VARCHAR )""", """CREATE TABLE IF NOT EXISTS deals ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, description TEXT, color VARCHAR, is_active INTEGER NOT NULL DEFAULT 1, sort_order INTEGER NOT NULL DEFAULT 0, action_type VARCHAR NOT NULL, action_modifier_id INTEGER REFERENCES price_modifiers(id), action_value REAL, action_free_item_id INTEGER REFERENCES products(id), action_free_target_type VARCHAR, action_free_target_ids TEXT, action_free_quantity INTEGER NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by INTEGER REFERENCES users(id) )""", """CREATE TABLE IF NOT EXISTS deal_conditions ( id INTEGER PRIMARY KEY AUTOINCREMENT, deal_id INTEGER NOT NULL REFERENCES deals(id) ON DELETE CASCADE, condition_type VARCHAR NOT NULL, params TEXT NOT NULL DEFAULT '{}' )""", """CREATE TABLE IF NOT EXISTS deal_targets ( id INTEGER PRIMARY KEY AUTOINCREMENT, deal_id INTEGER NOT NULL REFERENCES deals(id) ON DELETE CASCADE, target_type VARCHAR NOT NULL, target_id INTEGER, target_tag VARCHAR )""", """CREATE TABLE IF NOT EXISTS price_event_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL REFERENCES orders(id), order_item_id INTEGER REFERENCES order_items(id), event_type VARCHAR NOT NULL, modifier_id INTEGER REFERENCES price_modifiers(id), deal_id INTEGER REFERENCES deals(id), price_before REAL, price_after REAL, delta_amount REAL, applied_by_user_id INTEGER REFERENCES users(id), waiter_note TEXT, conditions_snapshot TEXT, selected_item_ids TEXT, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", "CREATE INDEX IF NOT EXISTS ix_price_event_log_order_id ON price_event_log (order_id)", "CREATE INDEX IF NOT EXISTS ix_price_event_log_order_item_id ON price_event_log (order_item_id)", """CREATE TABLE IF NOT EXISTS waiter_discount_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, can_apply_discounts INTEGER NOT NULL DEFAULT 0, max_discount_percent REAL, max_discount_amount REAL, max_total_value_shift REAL, max_total_value_workday REAL, max_items_per_shift INTEGER, max_items_per_workday INTEGER, max_items_per_order INTEGER )""", # Extend order_discounts with price snapshot columns for clean audit trail "ALTER TABLE order_discounts ADD COLUMN price_before REAL", "ALTER TABLE order_discounts ADD COLUMN price_after REAL", # Global discount settings in pos_settings "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('discounts.enabled', 'true', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('discounts.max_total_value_workday', '', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('discounts.max_total_value_shift', '', CURRENT_TIMESTAMP)", "INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('discounts.max_items_per_shift', '', CURRENT_TIMESTAMP)", # Deal item binding — which deal created this item, and which item it's linked to "ALTER TABLE order_items ADD COLUMN deal_id INTEGER REFERENCES deals(id)", "ALTER TABLE order_items ADD COLUMN linked_item_id INTEGER REFERENCES order_items(id)", # Multi-select targets: JSON arrays of IDs/tags per target row "ALTER TABLE price_modifier_targets ADD COLUMN target_ids TEXT", "ALTER TABLE price_modifier_targets ADD COLUMN target_tags TEXT", "ALTER TABLE deal_targets ADD COLUMN target_ids TEXT", "ALTER TABLE deal_targets ADD COLUMN target_tags TEXT", # Role & permissions overhaul — granular per-user permission flags # Access permissions "ALTER TABLE users ADD COLUMN perm_access_dashboard INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN perm_access_waiter_app INTEGER NOT NULL DEFAULT 1", "ALTER TABLE users ADD COLUMN perm_access_kds INTEGER NOT NULL DEFAULT 0", # Order action permissions "ALTER TABLE users ADD COLUMN perm_cancel_orders INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN perm_apply_discounts INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN perm_modify_prices INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN perm_open_orders INTEGER NOT NULL DEFAULT 1", "ALTER TABLE users ADD COLUMN perm_close_orders INTEGER NOT NULL DEFAULT 1", # Management permissions "ALTER TABLE users ADD COLUMN perm_view_reports INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN perm_manage_staff INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN perm_manage_tables INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN perm_manage_menu INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN perm_manage_settings INTEGER NOT NULL DEFAULT 0", # Migrate existing managers/sysadmins to have dashboard access + all management perms "UPDATE users SET perm_access_dashboard=1, perm_access_waiter_app=1, perm_access_kds=1, perm_cancel_orders=1, perm_apply_discounts=1, perm_modify_prices=1, perm_open_orders=1, perm_close_orders=1, perm_view_reports=1, perm_manage_staff=1, perm_manage_tables=1, perm_manage_menu=1, perm_manage_settings=1 WHERE role IN ('manager', 'sysadmin')", # Migrate existing waiters: update can_cancel_orders → perm_cancel_orders "UPDATE users SET perm_cancel_orders = can_cancel_orders WHERE role = 'waiter'", # Rename legacy roles: manager → store_manager, sysadmin → superadmin "UPDATE users SET role = 'superadmin' WHERE role = 'sysadmin'", "UPDATE users SET role = 'store_manager' WHERE role = 'manager'", # Feature: modifier groups (folders), multi-select extras, compact mode """CREATE TABLE IF NOT EXISTS product_modifier_groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL REFERENCES products(id), modifier_type VARCHAR NOT NULL, name VARCHAR NOT NULL, sort_order INTEGER NOT NULL DEFAULT 0 )""", "ALTER TABLE product_options ADD COLUMN multi_select INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_options ADD COLUMN is_compact INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_options ADD COLUMN group_id INTEGER REFERENCES product_modifier_groups(id)", "ALTER TABLE product_ingredients ADD COLUMN is_compact INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_ingredients ADD COLUMN group_id INTEGER REFERENCES product_modifier_groups(id)", "ALTER TABLE product_preference_choices ADD COLUMN is_compact INTEGER NOT NULL DEFAULT 0", "ALTER TABLE product_preference_sets ADD COLUMN group_id INTEGER REFERENCES product_modifier_groups(id)", # Fiscal printer (ΦΗΜ) — product fields "ALTER TABLE products ADD COLUMN fiscal_name VARCHAR", "ALTER TABLE products ADD COLUMN fiscal_vat_group_id INTEGER", # Fiscal status on orders (NULL=fiscal off, pending/success/failed) "ALTER TABLE orders ADD COLUMN fiscal_status VARCHAR", # Drop legacy can_cancel_orders column — data already migrated to perm_cancel_orders "ALTER TABLE users DROP COLUMN can_cancel_orders", # Recovery codes for account self-recovery """CREATE TABLE IF NOT EXISTS recovery_codes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, code_hash VARCHAR NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, used_at DATETIME )""", "CREATE INDEX IF NOT EXISTS ix_recovery_codes_user_id ON recovery_codes (user_id)", # Multi-select preference sets "ALTER TABLE product_preference_sets ADD COLUMN allow_multi_select INTEGER NOT NULL DEFAULT 0", # Per-choice quantity on multi-select preference sets (x2, x3, …) "ALTER TABLE product_preference_sets ADD COLUMN allow_choice_quantity INTEGER NOT NULL DEFAULT 0", # Tracks the hash of the product image last pushed to the cloud (Xenia Connect) "ALTER TABLE products ADD COLUMN cloud_image_hash VARCHAR", ] for sql in migrations: try: with engine.connect() as conn: conn.execute(text(sql)) conn.commit() except Exception: pass def _init_chat(): """Bootstrap the system group chat and sync all active users into it.""" from services.chat_service import ensure_system_group, add_user_to_system_group from models.user import User from database import SessionLocal db = SessionLocal() try: ensure_system_group(db) active_users = db.query(User).filter(User.is_active == True).all() # noqa: E712 for u in active_users: add_user_to_system_group(db, u.id) finally: db.close() @asynccontextmanager async def lifespan(app: FastAPI): import asyncio from services.sse_bus import init_loop as sse_init_loop from services.ws_bus import init_loop as ws_init_loop, prune_old_events from services.reservation_tasks import start_reservation_tasks from services.printer_service import start_print_retry_thread loop = asyncio.get_running_loop() sse_init_loop(loop) ws_init_loop(loop) Base.metadata.create_all(bind=engine) _run_migrations() start_print_retry_thread() pruned = prune_old_events(hours=24) if pruned: import logging logging.getLogger(__name__).info("ws_bus: pruned %d old sync_events", pruned) _init_chat() 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/notifications", tags=["messages"]) app.include_router(sse_router.router, prefix="/api/sse", tags=["sse"]) app.include_router(ws_router.router, prefix="/api/ws", tags=["ws"]) 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"]) app.include_router(waste_router.router, prefix="/api/waste", tags=["waste"]) app.include_router(kds_router.router, prefix="/api/kds", tags=["kds"]) app.include_router(schedule_router.router, prefix="/api/schedule", tags=["schedule"]) app.include_router(chat_router.router, prefix="/api/chat", tags=["chat"]) app.include_router(prep_zones_router.router, prefix="/api/prep-zones", tags=["prep-zones"]) app.include_router(pricing_router.router, prefix="/api/pricing", tags=["pricing"]) app.include_router(fiscal_router.router, prefix="/api/fiscal", tags=["fiscal"]) app.include_router(phone_router.router, prefix="/api/phone", tags=["phone"]) app.include_router(recovery_router.router, prefix="/api/recovery", tags=["recovery"])