TorbatYar/backend/services/hospitality/app/models/foundation.py
Mortezakoohjani e41ecfad4c Sync platform docs, infra, and module services with Accounting integration.
Include Loyalty/Communication/Sports Center backends and registry updates alongside production nginx and compose wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-25 22:35:23 +03:30

511 lines
18 KiB
Python

"""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"),
)