"""Hospitality foundation aggregates — Phase 10.0. Independent aggregates use UUID references within hospitality_db only. No SQLAlchemy relationship graphs between aggregates. Venue formats are configurable catalog values — no format-specific engines. """ 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, OptimisticLockMixin, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import ( AuditAction, BundleKey, BundleStatus, GUID, LifecycleStatus, MenuStatus, TableStatus, VenueFormat, ) class Venue( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Root hospitality venue (cafe / restaurant / bakery / …) for a tenant.""" __tablename__ = "venues" code: Mapped[str] = mapped_column(String(50), 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.DRAFT, nullable=False ) venue_format: Mapped[VenueFormat] = mapped_column( default=VenueFormat.RESTAURANT, nullable=False ) legal_name: Mapped[str | None] = mapped_column(String(255), nullable=True) timezone: Mapped[str] = mapped_column(String(64), default="Asia/Tehran", nullable=False) language: Mapped[str] = mapped_column(String(16), default="fa", nullable=False) currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False) settings: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint("tenant_id", "code", name="uq_venues_tenant_code"), Index("ix_venues_tenant_status", "tenant_id", "status"), Index("ix_venues_tenant_deleted", "tenant_id", "is_deleted"), Index("ix_venues_tenant_format", "tenant_id", "venue_format"), ) class Branch( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Unlimited branches / outlets under a venue.""" __tablename__ = "branches" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) code: Mapped[str] = mapped_column(String(50), 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 ) address: Mapped[dict | None] = mapped_column(JSON, nullable=True) timezone: Mapped[str | None] = mapped_column(String(64), nullable=True) working_hours: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "venue_id", "code", name="uq_branches_tenant_code" ), Index("ix_branches_venue", "tenant_id", "venue_id"), Index("ix_branches_tenant_deleted", "tenant_id", "is_deleted"), ) class DiningArea( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Floor / dining area shell for table service.""" __tablename__ = "dining_areas" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) branch_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), 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 ) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "venue_id", "code", name="uq_dining_areas_tenant_code" ), Index("ix_dining_areas_venue", "tenant_id", "venue_id"), Index("ix_dining_areas_tenant_deleted", "tenant_id", "is_deleted"), ) class DiningTable( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Table shell — seating / QR table binding engines come in later phases.""" __tablename__ = "dining_tables" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) dining_area_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) capacity: Mapped[int] = mapped_column(Integer, default=2, nullable=False) status: Mapped[TableStatus] = mapped_column( default=TableStatus.AVAILABLE, nullable=False ) qr_token: Mapped[str | None] = mapped_column(String(100), nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "venue_id", "code", name="uq_dining_tables_tenant_code" ), Index("ix_dining_tables_venue", "tenant_id", "venue_id"), Index("ix_dining_tables_area", "tenant_id", "dining_area_id"), Index("ix_dining_tables_tenant_deleted", "tenant_id", "is_deleted"), ) class Menu( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Digital menu shell — catalog depth expands in later phases.""" __tablename__ = "menus" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[MenuStatus] = mapped_column(default=MenuStatus.DRAFT, nullable=False) currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False) is_default: 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_menus_tenant_code"), Index("ix_menus_venue", "tenant_id", "venue_id"), Index("ix_menus_tenant_deleted", "tenant_id", "is_deleted"), ) class MenuCategory( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): __tablename__ = "menu_categories" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) menu_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) code: Mapped[str] = mapped_column(String(50), 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 ) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) __table_args__ = ( UniqueConstraint( "tenant_id", "menu_id", "code", name="uq_menu_categories_tenant_code" ), Index("ix_menu_categories_menu", "tenant_id", "menu_id"), Index("ix_menu_categories_tenant_deleted", "tenant_id", "is_deleted"), ) class MenuItem( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Menu item shell — pricing/modifiers engines come later.""" __tablename__ = "menu_items" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) menu_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) category_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), 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 ) base_price: Mapped[int] = mapped_column(Integer, default=0, nullable=False) currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False) is_available: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) attributes: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "menu_id", "code", name="uq_menu_items_tenant_code" ), Index("ix_menu_items_menu", "tenant_id", "menu_id"), Index("ix_menu_items_category", "tenant_id", "category_id"), Index("ix_menu_items_tenant_deleted", "tenant_id", "is_deleted"), ) class BundleDefinition( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Catalog of licensable bundles available to a tenant (feature-based licensing).""" __tablename__ = "bundle_definitions" code: Mapped[str] = mapped_column(String(50), nullable=False) bundle_key: Mapped[BundleKey] = mapped_column(nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[BundleStatus] = mapped_column( default=BundleStatus.AVAILABLE, nullable=False ) feature_keys: Mapped[list | None] = mapped_column(JSON, nullable=True) permission_prefixes: Mapped[list | None] = mapped_column(JSON, nullable=True) api_prefixes: Mapped[list | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint("tenant_id", "code", name="uq_bundle_definitions_tenant_code"), UniqueConstraint( "tenant_id", "bundle_key", name="uq_bundle_definitions_tenant_key" ), Index("ix_bundle_definitions_tenant_deleted", "tenant_id", "is_deleted"), ) class TenantBundle( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Activated bundle for a tenant/venue — hidden features must not expose APIs.""" __tablename__ = "tenant_bundles" venue_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) bundle_definition_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) bundle_key: Mapped[BundleKey] = mapped_column(nullable=False) status: Mapped[BundleStatus] = mapped_column( default=BundleStatus.ACTIVE, nullable=False ) activated_by: Mapped[str | None] = mapped_column(String(100), nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "venue_id", "bundle_key", name="uq_tenant_bundles_tenant_venue_key", ), Index("ix_tenant_bundles_status", "tenant_id", "status"), Index("ix_tenant_bundles_tenant_deleted", "tenant_id", "is_deleted"), ) class FeatureToggle( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Per-tenant/venue feature toggle for capability discovery.""" __tablename__ = "feature_toggles" venue_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) feature_key: Mapped[str] = mapped_column(String(100), nullable=False) enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) bundle_key: Mapped[BundleKey | None] = mapped_column(nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "venue_id", "feature_key", name="uq_feature_toggles_tenant_venue_key", ), Index("ix_feature_toggles_enabled", "tenant_id", "enabled"), Index("ix_feature_toggles_tenant_deleted", "tenant_id", "is_deleted"), ) class HospitalityRole( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): __tablename__ = "hospitality_roles" venue_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), 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 ) permission_keys: Mapped[list | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "code", name="uq_hospitality_roles_tenant_code" ), Index("ix_hospitality_roles_tenant_deleted", "tenant_id", "is_deleted"), ) class HospitalityPermission( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): __tablename__ = "hospitality_permissions" code: Mapped[str] = mapped_column(String(100), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) bundle_key: Mapped[BundleKey | None] = mapped_column(nullable=True) status: Mapped[LifecycleStatus] = mapped_column( default=LifecycleStatus.ACTIVE, nullable=False ) __table_args__ = ( UniqueConstraint( "tenant_id", "code", name="uq_hospitality_permissions_tenant_code" ), Index("ix_hospitality_permissions_tenant_deleted", "tenant_id", "is_deleted"), ) class HospitalityConfiguration( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): __tablename__ = "hospitality_configurations" venue_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) branch_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) config: Mapped[dict | None] = mapped_column(JSON, nullable=True) status: Mapped[LifecycleStatus] = mapped_column( default=LifecycleStatus.ACTIVE, nullable=False ) __table_args__ = ( UniqueConstraint( "tenant_id", "code", name="uq_hospitality_configurations_tenant_code" ), Index("ix_hospitality_configurations_venue", "tenant_id", "venue_id"), Index( "ix_hospitality_configurations_tenant_deleted", "tenant_id", "is_deleted" ), ) class HospitalityEvent( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Domain event shell records (not the outbox bus).""" __tablename__ = "hospitality_events" venue_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=False) event_type: Mapped[str] = mapped_column(String(100), nullable=False) title: Mapped[str] = mapped_column(String(255), nullable=False) payload: Mapped[dict | None] = mapped_column(JSON, nullable=True) status: Mapped[LifecycleStatus] = mapped_column( default=LifecycleStatus.ACTIVE, nullable=False ) __table_args__ = ( Index("ix_hospitality_events_venue", "tenant_id", "venue_id"), Index("ix_hospitality_events_type", "tenant_id", "event_type"), Index("ix_hospitality_events_tenant_deleted", "tenant_id", "is_deleted"), ) class HospitalitySetting( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): __tablename__ = "hospitality_settings" venue_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) key: Mapped[str] = mapped_column(String(100), nullable=False) value: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "venue_id", "key", name="uq_hospitality_settings_tenant_key" ), Index("ix_hospitality_settings_tenant_deleted", "tenant_id", "is_deleted"), ) class HospitalityAuditLog(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): """Append-only audit trail — no soft delete.""" __tablename__ = "hospitality_audit_logs" entity_type: Mapped[str] = mapped_column(String(100), nullable=False) entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) action: Mapped[AuditAction] = mapped_column(nullable=False) actor_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True) changes: Mapped[dict | None] = mapped_column(JSON, nullable=True) message: Mapped[str | None] = mapped_column(Text, nullable=True) __table_args__ = ( Index("ix_hospitality_audit_entity", "tenant_id", "entity_type", "entity_id"), Index("ix_hospitality_audit_actor", "tenant_id", "actor_user_id"), )