átlagos kiegészítséek jó sok

This commit is contained in:
Roo
2026-03-22 11:02:05 +00:00
parent f53e0b53df
commit 5d44339f21
249 changed files with 20922 additions and 2253 deletions

View 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",
]

View 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.

View 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)

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

View 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

View 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))

View 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))

View 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})>"

View 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))

View 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))