Files
xenia-pos-local/local_backend/main.py
bonamin 2ff48730f7 feat: Phase 2G — pay-later tab system
Backend:
- New models: Tab, TabEntry, TabPayment (3 new tables)
  - Tab: open|closed|forgiven, one open tab per customer enforced
  - TabEntry: what went ON the tab (per-item snapshot with description)
  - TabPayment: what came OFF the tab; balance = sum(entries) - sum(payments), never stored
- New router /api/tabs/:
  - GET / — list open tabs (sorted by balance desc); ?tab_status= override
  - GET /{id} — full tab detail with entries + payments
  - POST / — open new tab for a customer (400 if one already exists)
  - POST /{id}/entries — add entry directly (amount + description)
  - POST /{id}/pay — record payment; validates amount <= balance
  - POST /{id}/close — close tab (requires balance = 0)
  - POST /{id}/forgive — write off remaining balance
  - GET /customer/{customer_id} — all tabs for a customer (open first)
- POST /api/orders/{id}/items/{item_id}/tab:
  - Requires order has a customer assigned
  - Finds or auto-creates open tab for that customer
  - Sets order_item.status = "tabbed" (new valid status value)
  - Creates TabEntry with auto-generated description
  - Broadcasts order_updated SSE event
- Migrations: CREATE TABLE IF NOT EXISTS for tabs, tab_entries, tab_payments

Frontend:
- TabsPage (/tabs): open tabs list sorted by balance; each card shows charges/payments
  history, live balance, Πληρωμή modal (defaults to full balance), Κλείσιμο button
  (only shown when balance=0), Χάρισμα Υπολοίπου with confirm modal + reason
- CustomersPage CustomerDetail: open tab banner showing balance + entry count
  (appears between stats and contact info when customer has an open tab)
- OrderDetailPage: 📋 Καρτέλα button appears on active items when order has a customer;
  tabbed items show a 📋 Καρτέλα badge; tabItem mutation calls /items/{id}/tab
- Sidebar: CreditCard icon for Καρτέλες (after Πελάτες)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 03:07:22 +03:00

447 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"])