"""QR Menu & QR Ordering shells — Phase 12.2. Public QR surfaces and guest ordering/cart shells consuming the digital menu catalog. POS / kitchen / payment / reservation engines are out of scope. """ from __future__ import annotations import uuid from sqlalchemy import ( Boolean, Index, Integer, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.types import JSON from app.core.database import Base from app.models.base import ( ActorAuditMixin, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import ( CartStatus, GUID, LifecycleStatus, OrderingSessionStatus, QrSessionStatus, QrTargetType, ) class QrCode( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """QR code mapping token/slug to venue + optional menu/table targets.""" __tablename__ = "qr_codes" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) code: Mapped[str] = mapped_column(String(50), nullable=False) token: Mapped[str] = mapped_column(String(64), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[LifecycleStatus] = mapped_column( default=LifecycleStatus.ACTIVE, nullable=False ) target_type: Mapped[QrTargetType] = mapped_column( default=QrTargetType.MENU, nullable=False ) menu_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) dining_area_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) table_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) ordering_enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint("tenant_id", "venue_id", "code", name="uq_qr_codes_tenant_code"), UniqueConstraint("tenant_id", "token", name="uq_qr_codes_tenant_token"), Index("ix_qr_codes_venue", "tenant_id", "venue_id"), Index("ix_qr_codes_token", "tenant_id", "token"), Index("ix_qr_codes_tenant_deleted", "tenant_id", "is_deleted"), ) class QrMenuSession( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Guest browse session opened via QR — catalog view shell only.""" __tablename__ = "qr_menu_sessions" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) qr_code_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) menu_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) table_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) session_token: Mapped[str] = mapped_column(String(64), nullable=False) status: Mapped[QrSessionStatus] = mapped_column( default=QrSessionStatus.OPEN, nullable=False ) locale: Mapped[str | None] = mapped_column(String(16), nullable=True) guest_label: Mapped[str | None] = mapped_column(String(255), nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "session_token", name="uq_qr_menu_sessions_token" ), Index("ix_qr_menu_sessions_qr", "tenant_id", "qr_code_id"), Index("ix_qr_menu_sessions_venue", "tenant_id", "venue_id"), Index("ix_qr_menu_sessions_tenant_deleted", "tenant_id", "is_deleted"), ) class QrOrderingSession( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Guest ordering session shell — not a POS/kitchen ticket.""" __tablename__ = "qr_ordering_sessions" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) qr_code_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) qr_menu_session_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) menu_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) table_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) session_token: Mapped[str] = mapped_column(String(64), nullable=False) status: Mapped[OrderingSessionStatus] = mapped_column( default=OrderingSessionStatus.DRAFT, nullable=False ) guest_label: Mapped[str | None] = mapped_column(String(255), nullable=True) notes: Mapped[str | None] = mapped_column(Text, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "session_token", name="uq_qr_ordering_sessions_token" ), Index("ix_qr_ordering_sessions_qr", "tenant_id", "qr_code_id"), Index("ix_qr_ordering_sessions_venue", "tenant_id", "venue_id"), Index("ix_qr_ordering_sessions_tenant_deleted", "tenant_id", "is_deleted"), ) class Cart( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Cart shell bound to an ordering session — no payment posting.""" __tablename__ = "carts" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) ordering_session_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) status: Mapped[CartStatus] = mapped_column(default=CartStatus.ACTIVE, nullable=False) currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False) notes: Mapped[str | None] = mapped_column(Text, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "ordering_session_id", name="uq_carts_ordering_session", ), Index("ix_carts_session", "tenant_id", "ordering_session_id"), Index("ix_carts_venue", "tenant_id", "venue_id"), Index("ix_carts_tenant_deleted", "tenant_id", "is_deleted"), ) class CartLine( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Cart line with menu item + optional modifier option refs and price snapshot.""" __tablename__ = "cart_lines" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) cart_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) menu_item_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) quantity: Mapped[int] = mapped_column(Integer, default=1, nullable=False) unit_price: Mapped[int] = mapped_column(Integer, default=0, nullable=False) currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False) modifier_option_ids: Mapped[list | None] = mapped_column(JSON, nullable=True) line_notes: Mapped[str | None] = mapped_column(Text, nullable=True) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) __table_args__ = ( Index("ix_cart_lines_cart", "tenant_id", "cart_id"), Index("ix_cart_lines_item", "tenant_id", "menu_item_id"), Index("ix_cart_lines_tenant_deleted", "tenant_id", "is_deleted"), )