# /opt/docker/dev/service_finder/backend/app/models/marketplace/organization.py import enum import uuid from datetime import datetime from typing import Any, List, Optional, TYPE_CHECKING 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 from geoalchemy2 import Geometry # MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t from app.database import Base if TYPE_CHECKING: from .service_request import ServiceRequest 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): """Szervezeti (Organization-level) szerepkörök. Ezek a szerepkörök a szervezeten/flottán belüli jogosultságokat határozzák meg. Minden szerepkörhöz egyedi JSON permissions objektum tartozik a fleet.org_roles táblában. Megjegyzés: Az értékek szinkronban kell lenniük a PostgreSQL fleet.orguserrole enum típussal. Jelenlegi DB enum: OWNER, ADMIN, MANAGER, MEMBER, AGENT """ OWNER = "OWNER" ADMIN = "ADMIN" MANAGER = "MANAGER" MEMBER = "MEMBER" AGENT = "AGENT" class OrgRole(Base): """ Dinamikus szerepkör modell (RBAC Phase 2). A szervezeti szerepkörök adatbázis-vezéreltek, nem hardkódolt Enum-ok. Alapértelmezett szerepkörök: OWNER, ADMIN, ACCOUNTANT, DRIVER, VIEWER. permissions: JSONB oszlop a capability-alapú jogosultságok tárolására. Példa: {"can_add_expense": True, "can_approve_expense": False} """ __tablename__ = "org_roles" __table_args__ = {"schema": "fleet"} id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) name_key: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True) display_name: Mapped[str] = mapped_column(String(100), nullable=False) description: Mapped[Optional[str]] = mapped_column(String(255)) is_system: Mapped[bool] = mapped_column(Boolean, default=True) priority: Mapped[int] = mapped_column(Integer, default=0) is_active: Mapped[bool] = mapped_column(Boolean, default=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) # ── RBAC Phase 2: JSONB permissions column ── # Stores capability flags for this role, e.g.: # {"can_add_expense": True, "can_approve_expense": True, "can_view_financials": True} permissions: Mapped[dict] = mapped_column( JSONB, nullable=False, default=dict, server_default=text("'{}'::jsonb") ) 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": "fleet"} 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("system.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(24), 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") # ── P0: Scope-Based Admin RBAC ── # Geographic region for scope-based admin access (e.g., "Pest_County", "Budapest") region: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True) # Business segment for scope-based admin access (e.g., "Fleet", "Dealer", "Service") segment: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True) 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)) plus_code: Mapped[Optional[str]] = mapped_column(String(20)) tax_number: Mapped[Optional[str]] = mapped_column(String(20), unique=True, index=True) reg_number: Mapped[Optional[str]] = mapped_column(String(50)) # ── P0 CRM: Kapcsolattartó (Contact Person) ── contact_person_name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, comment="Kapcsolattartó neve") contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, comment="Kapcsolattartó e-mail címe") contact_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="Kapcsolattartó telefonszáma") # ── P0 CRM: Számlázási cím (Billing Address) ── billing_zip: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) billing_city: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) billing_street_name: Mapped[Optional[str]] = mapped_column(String(150), nullable=True) billing_street_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) billing_house_number: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # ── P0 CRM: Értesítési cím (Notification Address) ── notification_zip: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) notification_city: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) notification_street_name: Mapped[Optional[str]] = mapped_column(String(150), nullable=True) notification_street_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) notification_house_number: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) org_type: Mapped[OrgType] = mapped_column( PG_ENUM(OrgType, name="orgtype", schema="fleet"), default=OrgType.individual ) is_affiliate_partner: Mapped[bool] = mapped_column(Boolean, server_default=text('false'), default=False) 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) # ── 📦 ELŐFIZETÉS / SUBSCRIPTION ── subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"), index=True) subscription_expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=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")) # P0 FEATURE: Subscription Tier FK — connects this org to a system.subscription_tiers record subscription_tier_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("system.subscription_tiers.id"), nullable=True, index=True ) # ── P0 3D CAPABILITY MATRIX: custom_features JSONB ── # Organization-specific feature overrides on top of the subscription tier defaults. # These values MERGE with (and override) tier.feature_capabilities at runtime. # Example: {"can_export_data": True, "can_use_analytics": False} custom_features: Mapped[dict] = mapped_column( JSONB, nullable=False, default=dict, server_default=text("'{}'::jsonb") ) 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")) visual_settings: Mapped[dict] = mapped_column( JSONB, nullable=False, default=lambda: {"theme": "default", "primary_color": None, "wall_logo_url": None}, server_default=text("'{\"theme\": \"default\", \"primary_color\": null, \"wall_logo_url\": null}'::jsonb") ) # Dinamikus szervezeti beállítások (pl. invite_expiry_days, role_hierarchy) settings: Mapped[dict] = mapped_column( JSONB, nullable=False, default=lambda: {"invite_expiry_days": 7, "max_members": 50, "allow_public_join": False}, server_default=text("'{\"invite_expiry_days\": 7, \"max_members\": 50, \"allow_public_join\": false}'::jsonb") ) # --- 🔍 CROWDSOURCED SEARCH FIELDS --- # Provider nicknames / alternative names for matching (e.g. ["MOL", "MOL LUB", "MOL Magyarország"]) aliases: Mapped[list] = mapped_column( JSONB, nullable=False, default=list, server_default=text("'[]'::jsonb") ) # Crowdsourced evaluation characteristics / tags (e.g. ["gyors", "megbízható", "kedvező ár"]) tags: Mapped[list] = mapped_column( JSONB, nullable=False, default=list, 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") # ── P0 3D CAPABILITY MATRIX: SubscriptionTier relationship ── subscription_tier: Mapped[Optional["SubscriptionTier"]] = relationship( "SubscriptionTier", foreign_keys=[subscription_tier_id], backref="organizations", lazy="selectin" ) class OrganizationFinancials(Base): __tablename__ = "organization_financials" __table_args__ = {"schema": "fleet"} id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.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": "fleet"} id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.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")) # Meghívott e-mail címe (ha a meghívottnak még nincs user fiókja) invited_email: Mapped[Optional[str]] = mapped_column(String(255)) # Meghívó lejárati ideje expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) # Dinamikus szerepkör - PostgreSQL ENUM típus (fleet.orguserrole) # A PG_ENUM használata biztosítja, hogy az SQLAlchemy megfelelő típuskényszerítést # (type cast) alkalmazzon a lekérdezéseknél, elkerülve az # "operator does not exist: fleet.orguserrole = character varying" hibát. role: Mapped[str] = mapped_column( PG_ENUM(OrgUserRole, name="orguserrole", schema="fleet", create_type=False), default="MEMBER", server_default=text("'MEMBER'") ) 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) # 🔴 HIÁNYZÓ OSZLOPOK - Pótolva a logic_spec alapján status: Mapped[str] = mapped_column(String(20), default="active", server_default=text("'active'")) 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()) 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": "fleet"} id: Mapped[int] = mapped_column(Integer, primary_key=True) organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.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": "fleet"} 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("fleet.organizations.id"), nullable=False) address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.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)) # PostGIS location field for geographic queries location: Mapped[Optional[Any]] = mapped_column( Geometry(geometry_type='POINT', srid=4326), nullable=True ) 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))" ) # Kapcsolat a ServiceRequest modellel service_requests: Mapped[List["ServiceRequest"]] = relationship( "ServiceRequest", back_populates="branch", cascade="all, delete-orphan" )