From 835806f92d3edff62dee1d11186dc71374ce7851 Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 1 Jun 2026 00:01:02 +0300 Subject: [PATCH] =?UTF-8?q?feat(connect):=20Phase=202=20=E2=80=94=20cloud?= =?UTF-8?q?=20DB=20models=20for=20Xenia=20Connect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three new tables created automatically by create_all on next startup, plus an additive migration for an existing table: models/menu_snapshot.py (NEW) - menu_snapshots: one row per site, holds the full serialized category+product JSON; upserted on each sync from local_backend models/online_order.py (NEW) - online_orders: full order lifecycle from customer submission through delivery; tracks sync state so local_backend can poll for unprocessed orders (synced_to_local=0) models/manager_account.py (NEW) - manager_accounts + manager_site_access (M2M join table): remote manager login accounts with per-site access control models/site.py (MODIFIED) - order_counter column added; incremented atomically when each online order is created to generate "ORD-XXXX" public_ref values main.py (MODIFIED) - imports all three new models so create_all registers their tables - _run_migrations() added (same pattern as local_backend) for the additive order_counter column on the existing sites table Co-Authored-By: Claude Sonnet 4.6 --- cloud_backend/main.py | 24 +++++++++++++-- cloud_backend/models/manager_account.py | 26 ++++++++++++++++ cloud_backend/models/menu_snapshot.py | 12 ++++++++ cloud_backend/models/online_order.py | 41 +++++++++++++++++++++++++ cloud_backend/models/site.py | 2 ++ 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 cloud_backend/models/manager_account.py create mode 100644 cloud_backend/models/menu_snapshot.py create mode 100644 cloud_backend/models/online_order.py diff --git a/cloud_backend/main.py b/cloud_backend/main.py index dadbf30..24569e3 100644 --- a/cloud_backend/main.py +++ b/cloud_backend/main.py @@ -5,8 +5,11 @@ from fastapi.middleware.cors import CORSMiddleware from config import settings from database import engine, Base from auth_utils import hash_password -import models.admin # noqa: F401 -import models.site # noqa: F401 +import models.admin # noqa: F401 +import models.site # noqa: F401 +import models.menu_snapshot # noqa: F401 +import models.online_order # noqa: F401 +import models.manager_account # noqa: F401 from routers import auth, sites, heartbeat @@ -25,9 +28,26 @@ def _seed_default_admin(): db.commit() +def _run_migrations(): + """Apply additive schema changes that create_all won't handle.""" + from sqlalchemy import text + migrations = [ + # Per-site order counter for public_ref generation (e.g. "ORD-0042") + "ALTER TABLE sites ADD COLUMN order_counter INTEGER NOT NULL DEFAULT 0", + ] + 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): Base.metadata.create_all(bind=engine) + _run_migrations() _seed_default_admin() yield diff --git a/cloud_backend/models/manager_account.py b/cloud_backend/models/manager_account.py new file mode 100644 index 0000000..547d473 --- /dev/null +++ b/cloud_backend/models/manager_account.py @@ -0,0 +1,26 @@ +from sqlalchemy import Column, Integer, String, DateTime, Table, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +# Many-to-many: one manager can access multiple sites, one site can have multiple managers +manager_site_access = Table( + "manager_site_access", + Base.metadata, + Column("manager_id", Integer, ForeignKey("manager_accounts.id"), primary_key=True), + Column("site_id", Integer, ForeignKey("sites.id"), primary_key=True), +) + + +class ManagerAccount(Base): + __tablename__ = "manager_accounts" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, nullable=False, index=True) + password_hash = Column(String, nullable=False) + full_name = Column(String, nullable=True) + is_active = Column(Integer, default=1, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + sites = relationship("Site", secondary=manager_site_access, backref="manager_accounts") diff --git a/cloud_backend/models/menu_snapshot.py b/cloud_backend/models/menu_snapshot.py new file mode 100644 index 0000000..c8dc31f --- /dev/null +++ b/cloud_backend/models/menu_snapshot.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, Integer, Text, DateTime, ForeignKey +from sqlalchemy.sql import func +from database import Base + + +class MenuSnapshot(Base): + __tablename__ = "menu_snapshots" + + id = Column(Integer, primary_key=True, index=True) + site_id = Column(Integer, ForeignKey("sites.id"), nullable=False, index=True, unique=True) + snapshot_json = Column(Text, nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) diff --git a/cloud_backend/models/online_order.py b/cloud_backend/models/online_order.py new file mode 100644 index 0000000..ebc494c --- /dev/null +++ b/cloud_backend/models/online_order.py @@ -0,0 +1,41 @@ +from sqlalchemy import Column, Integer, String, Text, Float, DateTime, ForeignKey +from sqlalchemy.sql import func +from database import Base + + +class OnlineOrder(Base): + __tablename__ = "online_orders" + + id = Column(Integer, primary_key=True, index=True) + site_id = Column(Integer, ForeignKey("sites.id"), nullable=False, index=True) + + # Human-readable reference, unique per site (e.g. "ORD-0042") + public_ref = Column(String, unique=True, nullable=False) + order_type = Column(String, nullable=False) # "delivery" | "dine_in" + + # Customer info + customer_name = Column(String, nullable=False) + customer_phone = Column(String, nullable=True) + customer_address = Column(Text, nullable=True) # required for delivery + customer_notes = Column(Text, nullable=True) + + # Order content — JSON array of ordered items + items_json = Column(Text, nullable=False) + + # Financials + subtotal = Column(Float, nullable=False) + delivery_fee = Column(Float, default=0.0, nullable=False) + total = Column(Float, nullable=False) + + # Status lifecycle: + # pending_acceptance → accepted | rejected + # accepted → preparing → ready | out_for_delivery → delivered + status = Column(String, default="pending_acceptance", nullable=False, index=True) + rejection_reason = Column(String, nullable=True) + + # Sync state — local backend polls for these + synced_to_local = Column(Integer, default=0, nullable=False) # 0=no, 1=yes + local_order_id = Column(Integer, nullable=True) # set after local creates it + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) diff --git a/cloud_backend/models/site.py b/cloud_backend/models/site.py index 5a1c87a..5e6b786 100644 --- a/cloud_backend/models/site.py +++ b/cloud_backend/models/site.py @@ -21,3 +21,5 @@ class Site(Base): last_seen_ip = Column(String, nullable=True) last_seen_local_ip = Column(String, nullable=True) waiter_domain = Column(String, nullable=True) + # Monotonically incrementing counter used to generate public_ref for online orders + order_counter = Column(Integer, default=0, nullable=False)