átlagos kiegészítséek jó sok
This commit is contained in:
53
backend/app/models/marketplace/__init__.py
Normal file
53
backend/app/models/marketplace/__init__.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# marketplace package exports
|
||||
from .organization import (
|
||||
Organization,
|
||||
OrganizationMember,
|
||||
OrganizationFinancials,
|
||||
OrganizationSalesAssignment,
|
||||
OrgType,
|
||||
OrgUserRole,
|
||||
Branch,
|
||||
)
|
||||
|
||||
from .payment import PaymentIntent, PaymentIntentStatus
|
||||
from .finance import Issuer, IssuerType
|
||||
from .service import (
|
||||
ServiceProfile,
|
||||
ExpertiseTag,
|
||||
ServiceExpertise,
|
||||
)
|
||||
|
||||
from .logistics import Location, LocationType
|
||||
|
||||
# THOUGHT PROCESS: A StagedVehicleData nevet StagedVehicleData-ra javítottuk,
|
||||
# és ide csoportosítottuk a staged_data.py-ban lévő többi osztályt is.
|
||||
from .staged_data import (
|
||||
StagedVehicleData,
|
||||
ServiceStaging,
|
||||
DiscoveryParameter
|
||||
)
|
||||
|
||||
from .service_request import ServiceRequest
|
||||
|
||||
__all__ = [
|
||||
"Organization",
|
||||
"OrganizationMember",
|
||||
"OrganizationFinancials",
|
||||
"OrganizationSalesAssignment",
|
||||
"OrgType",
|
||||
"OrgUserRole",
|
||||
"Branch",
|
||||
"PaymentIntent",
|
||||
"PaymentIntentStatus",
|
||||
"Issuer",
|
||||
"IssuerType",
|
||||
"ServiceProfile",
|
||||
"ExpertiseTag",
|
||||
"ServiceExpertise",
|
||||
"ServiceStaging",
|
||||
"DiscoveryParameter",
|
||||
"Location",
|
||||
"LocationType",
|
||||
"StagedVehicleData",
|
||||
"ServiceRequest",
|
||||
]
|
||||
72
backend/app/models/marketplace/finance.py
Normal file
72
backend/app/models/marketplace/finance.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/marketplace/finance.py
|
||||
"""
|
||||
Finance modellek: Issuer (Kibocsátó) és FinancialLedger (Pénzügyi főkönyv) bővítése.
|
||||
"""
|
||||
|
||||
import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
from sqlalchemy import String, DateTime, JSON, ForeignKey, Numeric, Boolean, Integer, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class IssuerType(str, enum.Enum):
|
||||
"""Kibocsátó típusok (jogi forma)."""
|
||||
KFT = "KFT"
|
||||
EV = "EV"
|
||||
BT = "BT"
|
||||
ZRT = "ZRT"
|
||||
OTHER = "OTHER"
|
||||
|
||||
|
||||
class Issuer(Base):
|
||||
"""
|
||||
Kibocsátó (számlakibocsátó) entitás.
|
||||
|
||||
A rendszerben a számlákat kibocsátó jogi személyek vagy vállalkozások.
|
||||
Például: KFT, EV, stb. A revenue_limit meghatározza az adóhatár összegét.
|
||||
"""
|
||||
__tablename__ = "issuers"
|
||||
__table_args__ = {"schema": "finance"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
# Név és adószám
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
tax_id: Mapped[Optional[str]] = mapped_column(String(50), unique=True, index=True)
|
||||
|
||||
# Típus
|
||||
type: Mapped[IssuerType] = mapped_column(
|
||||
PG_ENUM(IssuerType, name="issuer_type", schema="finance"),
|
||||
default=IssuerType.OTHER,
|
||||
nullable=False
|
||||
)
|
||||
|
||||
# Bevételi limit (pl. KATA határ)
|
||||
revenue_limit: Mapped[float] = mapped_column(Numeric(18, 4), default=19500000.0)
|
||||
current_revenue: Mapped[float] = mapped_column(Numeric(18, 4), default=0.0)
|
||||
|
||||
# Aktív-e
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
# API konfiguráció (pl. számlázó rendszer integráció)
|
||||
api_config: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
# Időbélyegek
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Issuer {self.id}: {self.name} ({self.type})>"
|
||||
|
||||
|
||||
# Import FinancialLedger from audit module? We'll keep it separate.
|
||||
# The FinancialLedger class remains in audit.py, but we add fields there.
|
||||
# For completeness, we could also define it here, but to avoid duplication,
|
||||
# we'll just import it if needed.
|
||||
# Instead, we'll add a relationship from FinancialLedger to Issuer in audit.py.
|
||||
27
backend/app/models/marketplace/logistics.py
Executable file
27
backend/app/models/marketplace/logistics.py
Executable file
@@ -0,0 +1,27 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/marketplace/logistics.py
|
||||
import enum
|
||||
from typing import Optional
|
||||
from sqlalchemy import Integer, String, Enum
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from app.db.base_class import Base
|
||||
|
||||
class LocationType(str, enum.Enum):
|
||||
stop = "stop"
|
||||
warehouse = "warehouse"
|
||||
client = "client"
|
||||
|
||||
class Location(Base):
|
||||
__tablename__ = "locations"
|
||||
__table_args__ = {"schema": "fleet"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String)
|
||||
type: Mapped[LocationType] = mapped_column(
|
||||
PG_ENUM(LocationType, name="location_type", inherit_schema=True),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
coordinates: Mapped[Optional[str]] = mapped_column(String)
|
||||
address_full: Mapped[Optional[str]] = mapped_column(String)
|
||||
capacity: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
227
backend/app/models/marketplace/organization.py
Executable file
227
backend/app/models/marketplace/organization.py
Executable file
@@ -0,0 +1,227 @@
|
||||
# /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
|
||||
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
|
||||
|
||||
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": "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(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="fleet"),
|
||||
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")
|
||||
|
||||
# Kapcsolat a jármű költségekhez (TCO rendszer)
|
||||
vehicle_costs: Mapped[List["VehicleCost"]] = relationship("VehicleCost", back_populates="organization")
|
||||
|
||||
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"))
|
||||
|
||||
role: Mapped[OrgUserRole] = mapped_column(
|
||||
PG_ENUM(OrgUserRole, name="orguserrole", schema="fleet"),
|
||||
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": "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))"
|
||||
)
|
||||
224
backend/app/models/marketplace/payment.py
Normal file
224
backend/app/models/marketplace/payment.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/marketplace/payment.py
|
||||
"""
|
||||
Payment Intent modell a Stripe integrációhoz és belső fizetésekhez.
|
||||
Kettős Lakat (Double Lock) biztonságot valósít meg.
|
||||
"""
|
||||
|
||||
import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
from sqlalchemy import String, DateTime, JSON, ForeignKey, Numeric, Boolean, Integer, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
from app.models.system.audit import WalletType
|
||||
|
||||
|
||||
class PaymentIntentStatus(str, enum.Enum):
|
||||
"""PaymentIntent státuszok."""
|
||||
PENDING = "PENDING" # Létrehozva, vár Stripe fizetésre
|
||||
PROCESSING = "PROCESSING" # Fizetés folyamatban (belső ajándékozás)
|
||||
COMPLETED = "COMPLETED" # Sikeresen teljesítve
|
||||
FAILED = "FAILED" # Sikertelen (pl. Stripe hiba)
|
||||
CANCELLED = "CANCELLED" # Felhasználó által törölve
|
||||
EXPIRED = "EXPIRED" # Lejárt (pl. Stripe session timeout)
|
||||
|
||||
|
||||
class PaymentIntent(Base):
|
||||
"""
|
||||
Fizetési szándék (Prior Intent) a Kettős Lakat biztonsághoz.
|
||||
|
||||
Minden külső (Stripe) vagy belső fizetés előtt létre kell hozni egy PENDING
|
||||
státuszú PaymentIntent-et. A Stripe metadata tartalmazza az intent_token-t,
|
||||
így a webhook validáció során vissza lehet keresni.
|
||||
|
||||
Fontos mezők:
|
||||
- net_amount: A kedvezményezett által kapott összeg (pénztárcába kerül)
|
||||
- handling_fee: Kényelmi díj (rendszer bevétele)
|
||||
- gross_amount: net_amount + handling_fee (Stripe-nak küldött összeg)
|
||||
"""
|
||||
__tablename__ = "payment_intents"
|
||||
__table_args__ = {"schema": "finance"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
# Egyedi token a Stripe metadata számára
|
||||
intent_token: Mapped[uuid.UUID] = mapped_column(
|
||||
PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False, index=True
|
||||
)
|
||||
|
||||
# Fizető felhasználó
|
||||
payer_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||
payer: Mapped["User"] = relationship("User", foreign_keys=[payer_id], back_populates="payment_intents_as_payer")
|
||||
|
||||
# Kedvezményezett felhasználó (opcionális, ha None, akkor a rendszernek fizet)
|
||||
beneficiary_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
beneficiary: Mapped[Optional["User"]] = relationship("User", foreign_keys=[beneficiary_id], back_populates="payment_intents_as_beneficiary")
|
||||
|
||||
# Cél pénztárca típusa
|
||||
target_wallet_type: Mapped[WalletType] = mapped_column(
|
||||
PG_ENUM(WalletType, name="wallet_type", schema="finance"),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
# Összeg mezők (javított a kényelmi díj kezelésére)
|
||||
net_amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False, comment="Kedvezményezett által kapott összeg")
|
||||
handling_fee: Mapped[float] = mapped_column(Numeric(18, 4), default=0.0, comment="Kényelmi díj")
|
||||
gross_amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False, comment="Fizetendő összeg (net + fee)")
|
||||
|
||||
currency: Mapped[str] = mapped_column(String(10), default="EUR", nullable=False)
|
||||
|
||||
# Státusz
|
||||
status: Mapped[PaymentIntentStatus] = mapped_column(
|
||||
PG_ENUM(PaymentIntentStatus, name="payment_intent_status", schema="finance"),
|
||||
default=PaymentIntentStatus.PENDING,
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Stripe információk (külső fizetés esetén)
|
||||
stripe_session_id: Mapped[Optional[str]] = mapped_column(String(255), unique=True, index=True)
|
||||
stripe_payment_intent_id: Mapped[Optional[str]] = mapped_column(String(255), index=True)
|
||||
stripe_customer_id: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
|
||||
# Metaadatok (metadata foglalt név SQLAlchemy-ban, ezért meta_data)
|
||||
meta_data: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"), name="metadata")
|
||||
|
||||
# Időbélyegek
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), comment="Stripe session lejárati ideje")
|
||||
|
||||
# Tranzakció kapcsolat
|
||||
transaction_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), comment="Kapcsolódó FinancialLedger transaction_id")
|
||||
|
||||
# Soft delete
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<PaymentIntent {self.id}: {self.status} {self.gross_amount}{self.currency}>"
|
||||
|
||||
def mark_completed(self, transaction_id: Optional[uuid.UUID] = None) -> None:
|
||||
"""PaymentIntent befejezése sikeres fizetés után."""
|
||||
self.status = PaymentIntentStatus.COMPLETED
|
||||
self.completed_at = datetime.utcnow()
|
||||
if transaction_id:
|
||||
self.transaction_id = transaction_id
|
||||
|
||||
def mark_failed(self, reason: Optional[str] = None) -> None:
|
||||
"""PaymentIntent sikertelen státuszba helyezése."""
|
||||
self.status = PaymentIntentStatus.FAILED
|
||||
if reason and self.meta_data:
|
||||
self.meta_data = {**self.meta_data, "failure_reason": reason}
|
||||
|
||||
def is_valid_for_webhook(self) -> bool:
|
||||
"""Ellenőrzi, hogy a PaymentIntent érvényes-e webhook feldolgozásra."""
|
||||
return (
|
||||
self.status == PaymentIntentStatus.PENDING
|
||||
and not self.is_deleted
|
||||
and (self.expires_at is None or self.expires_at > datetime.utcnow())
|
||||
)
|
||||
|
||||
|
||||
# Import User modell a relationship-ekhez (circular import elkerülésére)
|
||||
from app.models.identity import User
|
||||
|
||||
|
||||
class WithdrawalPayoutMethod(str, enum.Enum):
|
||||
"""Kifizetési módok."""
|
||||
FIAT_BANK = "FIAT_BANK" # Banki átutalás (SEPA)
|
||||
CRYPTO_USDT = "CRYPTO_USDT" # USDT (ERC20/TRC20)
|
||||
|
||||
|
||||
class WithdrawalRequestStatus(str, enum.Enum):
|
||||
"""Kifizetési kérelem státuszai."""
|
||||
PENDING = "PENDING" # Beküldve, admin ellenőrzésre vár
|
||||
APPROVED = "APPROVED" # Jóváhagyva, kifizetés folyamatban
|
||||
REJECTED = "REJECTED" # Elutasítva (pl. hiányzó bizonylat)
|
||||
COMPLETED = "COMPLETED" # Kifizetés teljesítve
|
||||
CANCELLED = "CANCELLED" # Felhasználó által visszavonva
|
||||
|
||||
|
||||
class WithdrawalRequest(Base):
|
||||
"""
|
||||
Kifizetési kérelem (Withdrawal Request) a felhasználók Earned zsebéből való pénzkivételhez.
|
||||
|
||||
A felhasználó beküld egy kérést, amely admin jóváhagyást igényel.
|
||||
Ha 14 napon belül nem kerül jóváhagyásra, automatikusan REJECTED lesz és a pénz visszakerül a Earned zsebbe.
|
||||
"""
|
||||
__tablename__ = "withdrawal_requests"
|
||||
__table_args__ = {"schema": "finance"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
# Felhasználó aki a kérést benyújtotta
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||
user: Mapped["User"] = relationship("User", back_populates="withdrawal_requests", foreign_keys=[user_id])
|
||||
|
||||
# Összeg és pénznem
|
||||
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
|
||||
currency: Mapped[str] = mapped_column(String(10), default="EUR", nullable=False)
|
||||
|
||||
# Kifizetési mód
|
||||
payout_method: Mapped[WithdrawalPayoutMethod] = mapped_column(
|
||||
PG_ENUM(WithdrawalPayoutMethod, name="withdrawal_payout_method", schema="finance"),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
# Státusz
|
||||
status: Mapped[WithdrawalRequestStatus] = mapped_column(
|
||||
PG_ENUM(WithdrawalRequestStatus, name="withdrawal_request_status", schema="finance"),
|
||||
default=WithdrawalRequestStatus.PENDING,
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Okozata (pl. admin megjegyzés vagy automatikus elutasítás oka)
|
||||
reason: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
|
||||
# Admin információk
|
||||
approved_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
approved_by: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approved_by_id])
|
||||
approved_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
# Tranzakció kapcsolat (ha a pénz visszakerül a zsebbe)
|
||||
refund_transaction_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True))
|
||||
|
||||
# Időbélyegek
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# Soft delete
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<WithdrawalRequest {self.id}: {self.status} {self.amount}{self.currency}>"
|
||||
|
||||
def approve(self, admin_user_id: int) -> None:
|
||||
"""Admin jóváhagyás."""
|
||||
self.status = WithdrawalRequestStatus.APPROVED
|
||||
self.approved_by_id = admin_user_id
|
||||
self.approved_at = datetime.utcnow()
|
||||
self.reason = None
|
||||
|
||||
def reject(self, reason: str) -> None:
|
||||
"""Admin elutasítás."""
|
||||
self.status = WithdrawalRequestStatus.REJECTED
|
||||
self.reason = reason
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""Felhasználó visszavonja a kérést."""
|
||||
self.status = WithdrawalRequestStatus.CANCELLED
|
||||
self.reason = "User cancelled"
|
||||
|
||||
def is_expired(self, days: int = 14) -> bool:
|
||||
"""Ellenőrzi, hogy a kérelem lejárt-e (14 nap)."""
|
||||
from datetime import timedelta
|
||||
expiry_date = self.created_at + timedelta(days=days)
|
||||
return datetime.utcnow() > expiry_date
|
||||
159
backend/app/models/marketplace/service.py
Normal file
159
backend/app/models/marketplace/service.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service.py
|
||||
import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional
|
||||
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric, BigInteger
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB, ENUM as SQLEnum
|
||||
from geoalchemy2 import Geometry
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
|
||||
from app.database import Base
|
||||
|
||||
class ServiceStatus(str, enum.Enum):
|
||||
ghost = "ghost" # Nyers, robot által talált, nem validált
|
||||
active = "active" # Publikus, aktív szerviz
|
||||
flagged = "flagged" # Gyanús, kézi ellenőrzést igényel
|
||||
suspended = "suspended" # Felfüggesztett, tiltott szerviz
|
||||
|
||||
class ServiceProfile(Base):
|
||||
""" Szerviz szolgáltató adatai (v1.3.1). """
|
||||
__tablename__ = "service_profiles"
|
||||
__table_args__ = (
|
||||
Index('idx_service_fingerprint', 'fingerprint', unique=True),
|
||||
{"schema": "marketplace"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), unique=True)
|
||||
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id"))
|
||||
|
||||
fingerprint: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
|
||||
location: Mapped[Any] = mapped_column(Geometry(geometry_type='POINT', srid=4326, spatial_index=False), index=True)
|
||||
|
||||
status: Mapped[ServiceStatus] = mapped_column(
|
||||
SQLEnum(ServiceStatus, name="service_status", schema="marketplace"),
|
||||
server_default=ServiceStatus.ghost.value,
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
last_audit_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
google_place_id: Mapped[Optional[str]] = mapped_column(String(100), unique=True)
|
||||
rating: Mapped[Optional[float]] = mapped_column(Float)
|
||||
user_ratings_total: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
|
||||
# Aggregated verified review ratings (Social 3)
|
||||
rating_verified_count: Mapped[Optional[int]] = mapped_column(Integer, server_default=text("0"))
|
||||
rating_price_avg: Mapped[Optional[float]] = mapped_column(Float)
|
||||
rating_quality_avg: Mapped[Optional[float]] = mapped_column(Float)
|
||||
rating_time_avg: Mapped[Optional[float]] = mapped_column(Float)
|
||||
rating_communication_avg: Mapped[Optional[float]] = mapped_column(Float)
|
||||
rating_overall: Mapped[Optional[float]] = mapped_column(Float)
|
||||
last_review_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
vibe_analysis: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
social_links: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
specialization_tags: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
trust_score: Mapped[int] = mapped_column(Integer, default=30)
|
||||
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
verification_log: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
contact_phone: Mapped[Optional[str]] = mapped_column(String)
|
||||
contact_email: Mapped[Optional[str]] = mapped_column(String)
|
||||
website: Mapped[Optional[str]] = mapped_column(String)
|
||||
bio: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
# Kapcsolatok
|
||||
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
|
||||
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
|
||||
reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="service")
|
||||
|
||||
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())
|
||||
|
||||
class ExpertiseTag(Base):
|
||||
"""
|
||||
Szakmai címkék mesterlistája (MB 2.0).
|
||||
Ez a tábla vezérli a robotok keresését és a Gamification pontozást is.
|
||||
"""
|
||||
__tablename__ = "expertise_tags"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
key: Mapped[str] = mapped_column(String(50), unique=True, index=True)
|
||||
name_hu: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
name_en: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
category: Mapped[Optional[str]] = mapped_column(String(30), index=True)
|
||||
|
||||
# --- 🎮 GAMIFICATION ÉS DISCOVERY ---
|
||||
is_official: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
|
||||
suggested_by_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||
discovery_points: Mapped[int] = mapped_column(Integer, default=10, server_default=text("10"))
|
||||
search_keywords: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
|
||||
usage_count: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
|
||||
icon: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
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())
|
||||
|
||||
services: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="tag")
|
||||
suggested_by: Mapped[Optional["Person"]] = relationship("Person")
|
||||
|
||||
class ServiceExpertise(Base):
|
||||
"""
|
||||
KAPCSOLÓTÁBLA: Ez köti össze a szervizt a szakmáival.
|
||||
"""
|
||||
__tablename__ = "service_expertises"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"))
|
||||
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.expertise_tags.id", ondelete="CASCADE"))
|
||||
confidence_level: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
|
||||
|
||||
service = relationship("ServiceProfile", back_populates="expertises")
|
||||
tag = relationship("ExpertiseTag", back_populates="services")
|
||||
|
||||
class ServiceStaging(Base):
|
||||
""" Hunter (robot) adatok tárolója. """
|
||||
__tablename__ = "service_staging"
|
||||
__table_args__ = (
|
||||
Index('idx_staging_fingerprint', 'fingerprint', unique=True),
|
||||
{"schema": "marketplace"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
|
||||
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||
full_address: Mapped[Optional[str]] = mapped_column(String)
|
||||
fingerprint: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
raw_data: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
# Audit fix: contact_email hossza rögzítve a DB szinkronhoz
|
||||
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
|
||||
contact_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
website: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
external_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
|
||||
|
||||
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class DiscoveryParameter(Base):
|
||||
""" Robot vezérlési paraméterek adminból. """
|
||||
__tablename__ = "discovery_parameters"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
city: Mapped[str] = mapped_column(String(100))
|
||||
keyword: Mapped[str] = mapped_column(String(100))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
175
backend/app/models/marketplace/service.py.old
Executable file
175
backend/app/models/marketplace/service.py.old
Executable file
@@ -0,0 +1,175 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service.py
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional
|
||||
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric, BigInteger
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
|
||||
from geoalchemy2 import Geometry
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
|
||||
from app.database import Base
|
||||
|
||||
class ServiceProfile(Base):
|
||||
""" Szerviz szolgáltató adatai (v1.3.1). """
|
||||
__tablename__ = "service_profiles"
|
||||
__table_args__ = (
|
||||
Index('idx_service_fingerprint', 'fingerprint', unique=True),
|
||||
{"schema": "marketplace"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), unique=True)
|
||||
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id"))
|
||||
|
||||
fingerprint: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
|
||||
location: Mapped[Any] = mapped_column(Geometry(geometry_type='POINT', srid=4326, spatial_index=False), index=True)
|
||||
|
||||
status: Mapped[str] = mapped_column(String(20), server_default=text("'ghost'"), index=True)
|
||||
last_audit_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
google_place_id: Mapped[Optional[str]] = mapped_column(String(100), unique=True)
|
||||
rating: Mapped[Optional[float]] = mapped_column(Float)
|
||||
user_ratings_total: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
|
||||
# Aggregated verified review ratings (Social 3)
|
||||
rating_verified_count: Mapped[Optional[int]] = mapped_column(Integer, server_default=text("0"))
|
||||
rating_price_avg: Mapped[Optional[float]] = mapped_column(Float)
|
||||
rating_quality_avg: Mapped[Optional[float]] = mapped_column(Float)
|
||||
rating_time_avg: Mapped[Optional[float]] = mapped_column(Float)
|
||||
rating_communication_avg: Mapped[Optional[float]] = mapped_column(Float)
|
||||
rating_overall: Mapped[Optional[float]] = mapped_column(Float)
|
||||
last_review_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
vibe_analysis: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
social_links: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
specialization_tags: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
trust_score: Mapped[int] = mapped_column(Integer, default=30)
|
||||
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
verification_log: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
contact_phone: Mapped[Optional[str]] = mapped_column(String)
|
||||
contact_email: Mapped[Optional[str]] = mapped_column(String)
|
||||
website: Mapped[Optional[str]] = mapped_column(String)
|
||||
bio: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
# Kapcsolatok
|
||||
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
|
||||
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
|
||||
reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="service")
|
||||
|
||||
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())
|
||||
|
||||
class ExpertiseTag(Base):
|
||||
"""
|
||||
Szakmai címkék mesterlistája (MB 2.0).
|
||||
Ez a tábla vezérli a robotok keresését és a Gamification pontozást is.
|
||||
"""
|
||||
__tablename__ = "expertise_tags"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
# Egyedi azonosító kulcs (pl. 'ENGINE_REBUILD')
|
||||
key: Mapped[str] = mapped_column(String(50), unique=True, index=True)
|
||||
|
||||
# Megjelenítendő nevek
|
||||
name_hu: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
name_en: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
|
||||
# Főcsoport (pl. 'MECHANICS', 'ELECTRICAL', 'EMERGENCY')
|
||||
category: Mapped[Optional[str]] = mapped_column(String(30), index=True)
|
||||
|
||||
# --- 🎮 GAMIFICATION ÉS DISCOVERY ---
|
||||
|
||||
# Hivatalos címke (True) vagy júzer/robot által javasolt (False)
|
||||
is_official: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
|
||||
|
||||
# Ha júzer javasolta, itt tároljuk, ki volt az (XP jóváíráshoz)
|
||||
suggested_by_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||
|
||||
# ÁLLÍTHATÓ PONTÉRTÉK: Az adatbázisból jön, így bármikor módosítható.
|
||||
# Ritka szakmáknál magasabb, gyakoriaknál alacsonyabb érték állítható be.
|
||||
discovery_points: Mapped[int] = mapped_column(Integer, default=10, server_default=text("10"))
|
||||
|
||||
# Robot kulcsszavak (JSONB): ["fék", "betét", "tárcsa", "fékfolyadék"]
|
||||
# A Scout robot ez alapján azonosítja be a szervizt a weboldala alapján.
|
||||
search_keywords: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
|
||||
|
||||
# Népszerűségi mutató (hányszor lett felhasználva a rendszerben)
|
||||
usage_count: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
|
||||
|
||||
# UI ikon azonosító (pl. 'wrench', 'tire-flat', 'car-electric')
|
||||
icon: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
# Leírás a szakmáról (Adminisztratív célokra)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
# Időbélyegek
|
||||
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())
|
||||
|
||||
# --- KAPCSOLATOK ---
|
||||
services: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="tag")
|
||||
# Visszamutatás a beküldőre (ha van)
|
||||
suggested_by: Mapped[Optional["Person"]] = relationship("Person")
|
||||
|
||||
class ServiceExpertise(Base):
|
||||
"""
|
||||
KAPCSOLÓTÁBLA: Ez köti össze a szervizt a szakmáival.
|
||||
Itt tároljuk, hogy az adott szerviznél mennyire validált egy szakma.
|
||||
"""
|
||||
__tablename__ = "service_expertises"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"))
|
||||
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.expertise_tags.id", ondelete="CASCADE"))
|
||||
|
||||
# Mennyire biztos ez a tudás? (0: robot találta, 1: júzer mondta, 2: igazolt szakma)
|
||||
confidence_level: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
|
||||
|
||||
# Kapcsolatok visszafelé
|
||||
service = relationship("ServiceProfile", back_populates="expertises")
|
||||
tag = relationship("ExpertiseTag", back_populates="services")
|
||||
|
||||
class ServiceStaging(Base):
|
||||
""" Hunter (robot) adatok tárolója. """
|
||||
__tablename__ = "service_staging"
|
||||
__table_args__ = (
|
||||
Index('idx_staging_fingerprint', 'fingerprint', unique=True),
|
||||
{"schema": "marketplace"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
|
||||
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||
full_address: Mapped[Optional[str]] = mapped_column(String)
|
||||
fingerprint: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
raw_data: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
# Additional contact and identification fields
|
||||
contact_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
website: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
external_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
|
||||
|
||||
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class DiscoveryParameter(Base):
|
||||
""" Robot vezérlési paraméterek adminból. """
|
||||
__tablename__ = "discovery_parameters"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
city: Mapped[str] = mapped_column(String(100))
|
||||
keyword: Mapped[str] = mapped_column(String(100))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
95
backend/app/models/marketplace/service_request.py
Normal file
95
backend/app/models/marketplace/service_request.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service_request.py
|
||||
"""
|
||||
ServiceRequest - Piactér központi tranzakciós modellje.
|
||||
Epic 7: Marketplace ServiceRequest dedikált modell.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from sqlalchemy import String, ForeignKey, Text, DateTime, Numeric, Integer, Index
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class ServiceRequest(Base):
|
||||
"""
|
||||
Szervizigény (ServiceRequest) tábla.
|
||||
Egy felhasználó által létrehozott szervizigényt reprezentál, amely lehetővé teszi
|
||||
a szervizszolgáltatók számára árajánlatok készítését és a tranzakciók lebonyolítását.
|
||||
"""
|
||||
__tablename__ = "service_requests"
|
||||
__table_args__ = (
|
||||
Index('idx_service_request_status', 'status'),
|
||||
Index('idx_service_request_user_id', 'user_id'),
|
||||
Index('idx_service_request_asset_id', 'asset_id'),
|
||||
Index('idx_service_request_branch_id', 'branch_id'),
|
||||
{"schema": "marketplace"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Idegen kulcsok (Kapcsolódási pontok)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("identity.users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="A szervizigényt létrehozó felhasználó"
|
||||
)
|
||||
asset_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("vehicle.assets.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
comment="Érintett jármű (opcionális)"
|
||||
)
|
||||
branch_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("fleet.branches.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
comment="Célzott szerviz (ha van)"
|
||||
)
|
||||
|
||||
# Üzleti logika mezők
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
server_default="pending",
|
||||
index=True,
|
||||
comment="pending, quoted, accepted, scheduled, completed, cancelled"
|
||||
)
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="A szervizigény részletes leírása"
|
||||
)
|
||||
price_estimate: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(10, 2),
|
||||
nullable=True,
|
||||
comment="Becsült ár (opcionális)"
|
||||
)
|
||||
requested_date: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
comment="Kért szerviz dátum"
|
||||
)
|
||||
|
||||
# Audit
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
nullable=False,
|
||||
comment="Létrehozás időbélyege"
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
comment="Utolsó módosítás időbélyege"
|
||||
)
|
||||
|
||||
# Relationships (opcionális, de ajánlott a lazy loading miatt)
|
||||
user = relationship("User", back_populates="service_requests", lazy="selectin")
|
||||
asset = relationship("Asset", back_populates="service_requests", lazy="selectin")
|
||||
branch = relationship("Branch", back_populates="service_requests", lazy="selectin")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ServiceRequest(id={self.id}, status='{self.status}', user_id={self.user_id})>"
|
||||
94
backend/app/models/marketplace/staged_data.py
Normal file
94
backend/app/models/marketplace/staged_data.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any
|
||||
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float, Text, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base # MB 2.0 Standard: Központi bázis használata
|
||||
|
||||
class StagedVehicleData(Base):
|
||||
""" Robot 2.1 (Researcher) nyers adatgyűjtője. """
|
||||
__tablename__ = "staged_vehicle_data"
|
||||
__table_args__ = {"schema": "system", "extend_existing": True}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
source_url: Mapped[Optional[str]] = mapped_column(String)
|
||||
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
status: Mapped[str] = mapped_column(String(20), default="PENDING", index=True)
|
||||
error_log: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class ServiceStaging(Base):
|
||||
"""
|
||||
Robot 1.3 (Scout) által talált nyers szerviz adatok és a Robot 5 (Auditor) naplója.
|
||||
A séma és a mezők szinkronban az adatbázis audittal.
|
||||
"""
|
||||
__tablename__ = "service_staging"
|
||||
__table_args__ = {"schema": "marketplace", "extend_existing": True}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(255), index=True)
|
||||
|
||||
# 1. ⚠️ EXTRA OSZLOP: source
|
||||
source: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
external_id: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||
fingerprint: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
|
||||
# Elérhetőségek
|
||||
city: Mapped[str] = mapped_column(String(100), index=True)
|
||||
postal_code: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
full_address: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
contact_phone: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
website: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
|
||||
# 2. ⚠️ EXTRA OSZLOP: description
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
# 3. ⚠️ EXTRA OSZLOP: submitted_by
|
||||
submitted_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
|
||||
# 4. ⚠️ EXTRA OSZLOP: trust_score
|
||||
trust_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
|
||||
|
||||
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||||
validation_level: Mapped[int] = mapped_column(Integer, default=40, server_default=text("40"))
|
||||
|
||||
# --- Robot 5 (Auditor) technikai mezők ---
|
||||
|
||||
# 5. ⚠️ EXTRA OSZLOP: rejection_reason
|
||||
rejection_reason: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
|
||||
# 6. ⚠️ EXTRA OSZLOP: published_at
|
||||
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
# 7. ⚠️ EXTRA OSZLOP: service_profile_id
|
||||
service_profile_id: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
|
||||
# 8. ⚠️ EXTRA OSZLOP: organization_id
|
||||
organization_id: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
|
||||
# 9. ⚠️ EXTRA OSZLOP: audit_trail
|
||||
audit_trail: Mapped[Optional[dict]] = mapped_column(JSONB)
|
||||
|
||||
# Időbélyegek
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# 10. ⚠️ EXTRA OSZLOP: updated_at
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
class DiscoveryParameter(Base):
|
||||
""" Felderítési paraméterek (Városok, ahol a Scout keres). """
|
||||
__tablename__ = "discovery_parameters"
|
||||
__table_args__ = {"schema": "marketplace", "extend_existing": True}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
city: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
||||
country_code: Mapped[Optional[str]] = mapped_column(String(2), nullable=True, default="HU")
|
||||
keyword: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
73
backend/app/models/marketplace/staged_data1.2_.py.old
Executable file
73
backend/app/models/marketplace/staged_data1.2_.py.old
Executable file
@@ -0,0 +1,73 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/marketplace/staged_data.py
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any
|
||||
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class StagedVehicleData(Base):
|
||||
""" Robot 2.1 (Researcher) nyers adatgyűjtője. """
|
||||
__tablename__ = "staged_vehicle_data"
|
||||
__table_args__ = {"schema": "system"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
source_url: Mapped[Optional[str]] = mapped_column(String)
|
||||
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
status: Mapped[str] = mapped_column(String(20), default="PENDING", index=True)
|
||||
error_log: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class ServiceStaging(Base):
|
||||
""" Robot 1.3 (Scout) által talált nyers szerviz adatok és a Robot 5 (Auditor) naplója. """
|
||||
__tablename__ = "service_staging"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(255), index=True)
|
||||
source: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
external_id: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||
fingerprint: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
|
||||
# Elérhetőségek
|
||||
city: Mapped[str] = mapped_column(String(100), index=True)
|
||||
postal_code: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
full_address: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
contact_phone: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
website: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
|
||||
# Beküldés és Bizalom
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
submitted_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
trust_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
|
||||
|
||||
# Nyers adatok és Státusz
|
||||
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||||
|
||||
# --- Robot 5 (Auditor) technikai mezők ---
|
||||
# Ezek kellenek a munka naplózásához
|
||||
rejection_reason: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
service_profile_id: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
organization_id: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
audit_trail: Mapped[Optional[dict]] = mapped_column(JSONB)
|
||||
|
||||
# Időbélyegek
|
||||
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())
|
||||
|
||||
class DiscoveryParameter(Base):
|
||||
""" Felderítési paraméterek (Városok, ahol a Scout keres). """
|
||||
__tablename__ = "discovery_parameters"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
city: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
||||
keyword: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
Reference in New Issue
Block a user