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)