from datetime import datetime, timezone from uuid import uuid4 from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Index, Integer, String, Text, UniqueConstraint from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from .database import Base def utc_now() -> datetime: return datetime.now(timezone.utc).replace(tzinfo=None) def new_id() -> str: return str(uuid4()) class AdminUser(Base): __tablename__ = "AdminUser" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) email: Mapped[str] = mapped_column(String, unique=True, nullable=False) name: Mapped[str] = mapped_column(String, nullable=False) passwordHash: Mapped[str] = mapped_column(String, nullable=False) role: Mapped[str] = mapped_column(String, default="admin", nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) auditLogs: Mapped[list["AuditLog"]] = relationship(back_populates="actor") assignedLeads: Mapped[list["Lead"]] = relationship(back_populates="assignedUser", foreign_keys="Lead.assignedUserId") class AdminRole(Base): __tablename__ = "AdminRole" __table_args__ = (UniqueConstraint("code", name="uq_AdminRole_code"),) id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) name: Mapped[str] = mapped_column(String, nullable=False) code: Mapped[str] = mapped_column(String, nullable=False) dataScope: Mapped[str] = mapped_column(String, default="all", nullable=False) description: Mapped[str | None] = mapped_column(Text) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class AdminMenu(Base): __tablename__ = "AdminMenu" __table_args__ = (UniqueConstraint("permissionCode", name="uq_AdminMenu_permissionCode"),) id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) parentId: Mapped[str | None] = mapped_column(String, ForeignKey("AdminMenu.id", ondelete="CASCADE")) name: Mapped[str] = mapped_column(String, nullable=False) type: Mapped[str] = mapped_column(String, default="page", nullable=False) path: Mapped[str | None] = mapped_column(String) componentKey: Mapped[str | None] = mapped_column(String) permissionCode: Mapped[str | None] = mapped_column(String) icon: Mapped[str | None] = mapped_column(String) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) isVisible: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class AdminDepartment(Base): __tablename__ = "AdminDepartment" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) parentId: Mapped[str | None] = mapped_column(String, ForeignKey("AdminDepartment.id", ondelete="SET NULL")) name: Mapped[str] = mapped_column(String, nullable=False) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class AdminUserRole(Base): __tablename__ = "AdminUserRole" userId: Mapped[str] = mapped_column(String, ForeignKey("AdminUser.id", ondelete="CASCADE"), primary_key=True) roleId: Mapped[str] = mapped_column(String, ForeignKey("AdminRole.id", ondelete="CASCADE"), primary_key=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) class AdminRoleMenu(Base): __tablename__ = "AdminRoleMenu" roleId: Mapped[str] = mapped_column(String, ForeignKey("AdminRole.id", ondelete="CASCADE"), primary_key=True) menuId: Mapped[str] = mapped_column(String, ForeignKey("AdminMenu.id", ondelete="CASCADE"), primary_key=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) class AdminUserDepartment(Base): __tablename__ = "AdminUserDepartment" userId: Mapped[str] = mapped_column(String, ForeignKey("AdminUser.id", ondelete="CASCADE"), primary_key=True) deptId: Mapped[str] = mapped_column(String, ForeignKey("AdminDepartment.id", ondelete="CASCADE"), primary_key=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) class AdminRoleDepartment(Base): __tablename__ = "AdminRoleDepartment" roleId: Mapped[str] = mapped_column(String, ForeignKey("AdminRole.id", ondelete="CASCADE"), primary_key=True) deptId: Mapped[str] = mapped_column(String, ForeignKey("AdminDepartment.id", ondelete="CASCADE"), primary_key=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) DEFAULT_ADMIN_DEPT_ID = "00000000-0000-0000-0000-000000000001" class AdminOwnedContent: deptId: Mapped[str] = mapped_column( String, ForeignKey("AdminDepartment.id", ondelete="RESTRICT"), nullable=False, index=True, server_default=DEFAULT_ADMIN_DEPT_ID, ) createdById: Mapped[str | None] = mapped_column( String, ForeignKey("AdminUser.id", ondelete="SET NULL"), nullable=True, index=True, ) class MediaAsset(AdminOwnedContent, Base): __tablename__ = "MediaAsset" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) url: Mapped[str] = mapped_column(String, unique=True, nullable=False) name: Mapped[str | None] = mapped_column(String) mimeType: Mapped[str | None] = mapped_column(String) sizeBytes: Mapped[int | None] = mapped_column(Integer) group: Mapped[str | None] = mapped_column(String) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class HeroSlide(AdminOwnedContent, Base): __tablename__ = "HeroSlide" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) title: Mapped[str] = mapped_column(String, nullable=False) kicker: Mapped[str | None] = mapped_column(String) actionLabel: Mapped[str | None] = mapped_column(String) image: Mapped[str] = mapped_column(String, nullable=False) targetType: Mapped[str | None] = mapped_column(String) targetValue: Mapped[str | None] = mapped_column(String) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class VehicleOption(AdminOwnedContent, Base): __tablename__ = "VehicleOption" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) title: Mapped[str] = mapped_column(String, nullable=False) description: Mapped[str | None] = mapped_column(Text) image: Mapped[str | None] = mapped_column(String) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class HomeTeamBuilding(AdminOwnedContent, Base): __tablename__ = "HomeTeamBuilding" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) tag: Mapped[str] = mapped_column(String, nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False) image: Mapped[str] = mapped_column(String, nullable=False) demandKeyword: Mapped[str] = mapped_column(String, nullable=False) detailSubtitle: Mapped[str] = mapped_column(String, default="", nullable=False) detailParagraphs: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class HomeWildArchive(AdminOwnedContent, Base): __tablename__ = "HomeWildArchive" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) title: Mapped[str] = mapped_column(String, nullable=False) image: Mapped[str] = mapped_column(String, nullable=False) images: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False) demandKeyword: Mapped[str] = mapped_column(String, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class HomeWanfaRecommendation(AdminOwnedContent, Base): __tablename__ = "HomeWanfaRecommendation" __table_args__ = ( UniqueConstraint("categoryId", name="uq_HomeWanfaRecommendation_categoryId"), ) id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) categoryId: Mapped[str] = mapped_column( String, ForeignKey("WanfaCategory.id", ondelete="RESTRICT"), nullable=False, index=True, ) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) category: Mapped["WanfaCategory"] = relationship(back_populates="recommendations") class WanfaCategory(AdminOwnedContent, Base): __tablename__ = "WanfaCategory" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) label: Mapped[str] = mapped_column(String, nullable=False) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) routes: Mapped[list["WanfaRoute"]] = relationship( back_populates="category", order_by="WanfaRoute.sortOrder.asc()", ) recommendations: Mapped[list[HomeWanfaRecommendation]] = relationship(back_populates="category") class WanfaRoute(AdminOwnedContent, Base): __tablename__ = "WanfaRoute" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) categoryId: Mapped[str] = mapped_column( String, ForeignKey("WanfaCategory.id", ondelete="RESTRICT"), nullable=False, index=True, ) title: Mapped[str] = mapped_column(String, nullable=False) subtitle: Mapped[str] = mapped_column(Text, nullable=False) image: Mapped[str] = mapped_column(String, nullable=False) routeCount: Mapped[int] = mapped_column(Integer, nullable=False) demandKeyword: Mapped[str] = mapped_column(String, nullable=False) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) category: Mapped[WanfaCategory] = relationship(back_populates="routes") class DetailRecord(AdminOwnedContent, Base): __tablename__ = "DetailRecord" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) key: Mapped[str] = mapped_column(String, nullable=False, unique=True) eyebrow: Mapped[str] = mapped_column(String, nullable=False) duration: Mapped[str] = mapped_column(String, nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) subtitle: Mapped[str] = mapped_column(Text, nullable=False) intro: Mapped[str] = mapped_column(Text, nullable=False) highlights: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False) included: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False) excluded: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False) notes: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False) gallery: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False) priceStartingValue: Mapped[float | None] = mapped_column(Float, nullable=True) priceUnit: Mapped[str | None] = mapped_column(String, nullable=True) pricePeopleRange: Mapped[str | None] = mapped_column(String, nullable=True) priceDescription: Mapped[str | None] = mapped_column(Text, nullable=True) conciergeAdvisorId: Mapped[str | None] = mapped_column(String, nullable=True) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class ConciergeAdvisor(AdminOwnedContent, Base): __tablename__ = "ConciergeAdvisor" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) avatar: Mapped[str] = mapped_column(String, nullable=False) name: Mapped[str] = mapped_column(String, nullable=False) role: Mapped[str] = mapped_column(String, nullable=False) details: Mapped[list[dict[str, str]]] = mapped_column(JSONB, default=list, nullable=False) qrImage: Mapped[str] = mapped_column(String, nullable=False) isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True) sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class Lead(AdminOwnedContent, Base): __tablename__ = "Lead" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) leadType: Mapped[str] = mapped_column(String, default="general", nullable=False, index=True) contactName: Mapped[str | None] = mapped_column(String) customerId: Mapped[str | None] = mapped_column(String, ForeignKey("Customer.id", ondelete="SET NULL"), index=True) destination: Mapped[str | None] = mapped_column(String) phone: Mapped[str] = mapped_column(String, nullable=False) travelDate: Mapped[datetime | None] = mapped_column(DateTime) peopleCount: Mapped[int | None] = mapped_column(Integer) budgetMin: Mapped[int | None] = mapped_column(Integer) budgetMax: Mapped[int | None] = mapped_column(Integer) note: Mapped[str | None] = mapped_column(Text) vehicleDemand: Mapped[dict | None] = mapped_column(JSONB) sourcePage: Mapped[str | None] = mapped_column(String) status: Mapped[str] = mapped_column(String, default="new", nullable=False) assignedUserId: Mapped[str | None] = mapped_column(String, ForeignKey("AdminUser.id", ondelete="SET NULL")) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) assignedUser: Mapped[AdminUser | None] = relationship(back_populates="assignedLeads", foreign_keys=[assignedUserId]) followups: Mapped[list["LeadFollowup"]] = relationship(back_populates="lead", cascade="all, delete-orphan") class LeadFollowup(Base): __tablename__ = "LeadFollowup" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) leadId: Mapped[str] = mapped_column(String, ForeignKey("Lead.id", ondelete="CASCADE"), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False) nextAt: Mapped[datetime | None] = mapped_column(DateTime) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) lead: Mapped[Lead] = relationship(back_populates="followups") class Customer(Base): __tablename__ = "Customer" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) phone: Mapped[str] = mapped_column(String, unique=True, nullable=False) name: Mapped[str | None] = mapped_column(String) wechat: Mapped[str | None] = mapped_column(String) note: Mapped[str | None] = mapped_column(Text) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) orders: Mapped[list["Order"]] = relationship(back_populates="customer") browseHistory: Mapped[list["CustomerBrowseHistory"]] = relationship( back_populates="customer", cascade="all, delete-orphan", ) class CustomerBrowseHistory(Base): __tablename__ = "CustomerBrowseHistory" __table_args__ = ( UniqueConstraint( "customerId", "itemType", "itemId", name="uq_CustomerBrowseHistory_customer_item", ), Index( "ix_CustomerBrowseHistory_customer_visitedAt", "customerId", "visitedAt", ), ) id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) customerId: Mapped[str] = mapped_column( String, ForeignKey("Customer.id", ondelete="CASCADE"), nullable=False, index=True, ) itemType: Mapped[str] = mapped_column(String, nullable=False) itemId: Mapped[str] = mapped_column(String, nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) image: Mapped[str] = mapped_column(String, nullable=False, default="") visitedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) customer: Mapped[Customer] = relationship(back_populates="browseHistory") class Order(Base): __tablename__ = "Order" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) customerId: Mapped[str | None] = mapped_column(String, ForeignKey("Customer.id", ondelete="SET NULL")) status: Mapped[str] = mapped_column(String, default="draft", nullable=False) travelDate: Mapped[datetime | None] = mapped_column(DateTime) amount: Mapped[int | None] = mapped_column(Integer) note: Mapped[str | None] = mapped_column(Text) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) customer: Mapped[Customer | None] = relationship(back_populates="orders") class AuditLog(Base): __tablename__ = "AuditLog" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) actorId: Mapped[str | None] = mapped_column(String, ForeignKey("AdminUser.id", ondelete="SET NULL")) action: Mapped[str] = mapped_column(String, nullable=False) entity: Mapped[str] = mapped_column(String, nullable=False) entityId: Mapped[str | None] = mapped_column(String) before: Mapped[dict | list | None] = mapped_column(JSONB) after: Mapped[dict | list | None] = mapped_column(JSONB) createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False) actor: Mapped[AdminUser | None] = relationship(back_populates="auditLogs")