# /opt/docker/dev/service_finder/backend/app/models/organization.py import enum import uuid from datetime import datetime from typing import Any, List, Optional import sqlalchemy as sa from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Numeric, BigInteger, Float from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID, JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign from sqlalchemy.sql import func # MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t from app.database import Base class OrgType(str, enum.Enum): individual = "individual" service = "service" service_provider = "service_provider" fleet_owner = "fleet_owner" club = "club" business = "business" class OrgUserRole(str, enum.Enum): OWNER = "OWNER" ADMIN = "ADMIN" FLEET_MANAGER = "FLEET_MANAGER" DRIVER = "DRIVER" MECHANIC = "MECHANIC" RECEPTIONIST = "RECEPTIONIST" class Organization(Base): """ Szervezet entitás (MB 2.0). Támogatja a 'Digital Twin' logikát: a cég törölhető, de a statisztika és a jármű-életút adatok megmaradnak az eredeti Person-höz kötve. """ __tablename__ = "organizations" __table_args__ = {"schema": "data"} id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) # --- 🛡️ BIZTONSÁGI ÉS ÉLETÚT KIEGÉSZÍTÉSEK --- # A Jogi képviselő/Tulajdonos (A Person örök DNS-e az identity sémában) # Ez segít felismerni, ha ugyanaz az ember új céggel akar 'tiszta lapot' nyitni. legal_owner_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"), index=True) # ÉLETÚT DÁTUMOK (A kért logika alapján) # 1. A legelső regisztráció dátuma (Soha nem változik) first_registered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) # 2. Az AKTUÁLIS életciklus kezdete. Újraregisztrációkor ez frissül. # Az API ezt használja szűrőnek: a cég csak az ezutáni adatokat látja a Dashboardon. current_lifecycle_started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) # 3. Az utolsó deaktiválás/törlés időpontja last_deactivated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) # Hányszor regisztrált újra ez a cég/adószám (Reinkarnációs index) lifecycle_index: Mapped[int] = mapped_column(Integer, default=1, server_default=text("1")) # --- 🏢 ALAPADATOK (MEGŐRIZVE) --- address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id")) is_anonymized: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text("false")) anonymized_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) full_name: Mapped[str] = mapped_column(String, nullable=False) name: Mapped[str] = mapped_column(String, nullable=False) display_name: Mapped[Optional[str]] = mapped_column(String(50)) folder_slug: Mapped[str] = mapped_column(String(12), unique=True, index=True) default_currency: Mapped[str] = mapped_column(String(3), default="HUF") country_code: Mapped[str] = mapped_column(String(2), default="HU") language: Mapped[str] = mapped_column(String(5), default="hu") address_zip: Mapped[Optional[str]] = mapped_column(String(10)) address_city: Mapped[Optional[str]] = mapped_column(String(100)) address_street_name: Mapped[Optional[str]] = mapped_column(String(150)) address_street_type: Mapped[Optional[str]] = mapped_column(String(50)) address_house_number: Mapped[Optional[str]] = mapped_column(String(20)) address_hrsz: Mapped[Optional[str]] = mapped_column(String(50)) tax_number: Mapped[Optional[str]] = mapped_column(String(20), unique=True, index=True) reg_number: Mapped[Optional[str]] = mapped_column(String(50)) org_type: Mapped[OrgType] = mapped_column( PG_ENUM(OrgType, name="orgtype", schema="data"), default=OrgType.individual ) status: Mapped[str] = mapped_column(String(30), default="pending_verification") # Soft delete: is_active=False és is_deleted=True esetén a cég 'törölt' is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True) subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"), index=True) base_asset_limit: Mapped[int] = mapped_column(Integer, server_default=text("1")) purchased_extra_slots: Mapped[int] = mapped_column(Integer, server_default=text("0")) notification_settings: Mapped[Any] = mapped_column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb")) external_integration_config: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb")) # A technikai tulajdonos (User fiók - törölhető) owner_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id")) is_verified: Mapped[bool] = mapped_column(Boolean, default=False) # Időbélyegek az aktuális állapothoz created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now()) is_ownership_transferable: Mapped[bool] = mapped_column(Boolean, server_default=text("true")) # --- 🔗 KAPCSOLATOK (RELATIONSHIPS) --- assets: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan") members: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan") owner: Mapped[Optional["User"]] = relationship("User", back_populates="owned_organizations") financials: Mapped[List["OrganizationFinancials"]] = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan") # JAVÍTVA: Ha az Organization törlődik, a ServiceProfile megmarad 'Ghost'-ként (ondelete="SET NULL") service_profile: Mapped[Optional["ServiceProfile"]] = relationship("ServiceProfile", back_populates="organization", uselist=False) branches: Mapped[List["Branch"]] = relationship("Branch", back_populates="organization", cascade="all, delete-orphan") # Kapcsolat az örök személy rekordhoz legal_owner: Mapped[Optional["Person"]] = relationship("Person", back_populates="owned_business_entities") class OrganizationFinancials(Base): __tablename__ = "organization_financials" __table_args__ = {"schema": "data"} id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False) year: Mapped[int] = mapped_column(Integer, nullable=False) turnover: Mapped[Optional[float]] = mapped_column(Numeric(18, 2)) profit: Mapped[Optional[float]] = mapped_column(Numeric(18, 2)) employee_count: Mapped[Optional[int]] = mapped_column(Integer) source: Mapped[Optional[str]] = mapped_column(String(50)) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) organization: Mapped["Organization"] = relationship("Organization", back_populates="financials") class OrganizationMember(Base): __tablename__ = "organization_members" __table_args__ = {"schema": "data"} id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False) user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id")) person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id")) role: Mapped[OrgUserRole] = mapped_column( PG_ENUM(OrgUserRole, name="orguserrole", schema="data"), default=OrgUserRole.DRIVER ) permissions: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb")) is_permanent: Mapped[bool] = mapped_column(Boolean, default=False) is_verified: Mapped[bool] = mapped_column(Boolean, default=False) organization: Mapped["Organization"] = relationship("Organization", back_populates="members") user: Mapped[Optional["User"]] = relationship("User") person: Mapped[Optional["Person"]] = relationship("Person", back_populates="memberships") class OrganizationSalesAssignment(Base): __tablename__ = "org_sales_assignments" __table_args__ = {"schema": "data"} id: Mapped[int] = mapped_column(Integer, primary_key=True) organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id")) agent_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id")) assigned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) is_active: Mapped[bool] = mapped_column(Boolean, default=True) class Branch(Base): """ Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik. """ __tablename__ = "branches" __table_args__ = {"schema": "data"} id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False) address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id")) name: Mapped[str] = mapped_column(String(100), nullable=False) is_main: Mapped[bool] = mapped_column(Boolean, default=False) # Denormalizált adatok a gyors lekérdezéshez postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True) city: Mapped[Optional[str]] = mapped_column(String(100), index=True) street_name: Mapped[Optional[str]] = mapped_column(String(150)) street_type: Mapped[Optional[str]] = mapped_column(String(50)) house_number: Mapped[Optional[str]] = mapped_column(String(20)) stairwell: Mapped[Optional[str]] = mapped_column(String(20)) floor: Mapped[Optional[str]] = mapped_column(String(20)) door: Mapped[Optional[str]] = mapped_column(String(20)) hrsz: Mapped[Optional[str]] = mapped_column(String(50)) opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) branch_rating: Mapped[float] = mapped_column(Float, default=0.0) status: Mapped[str] = mapped_column(String(30), default="active") is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) # Kapcsolatok organization: Mapped["Organization"] = relationship("Organization", back_populates="branches") address: Mapped[Optional["Address"]] = relationship("Address") # Kapcsolatok (Primaryjoin tartva a rating rendszerhez) reviews: Mapped[List["Rating"]] = relationship( "Rating", primaryjoin="and_(Branch.id==foreign(Rating.target_branch_id))" )