STABLE: Final schema sync, optimized gitignore
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,15 +1,18 @@
|
|||||||
# Python cache
|
# Python cache
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
backend/__pycache__/
|
||||||
|
backend/app/scripts/__pycache__/
|
||||||
|
|
||||||
# Docker & Data (Master Book 2.0 izoláció)
|
# Docker & Data (Master Book 2.0 izoláció)
|
||||||
ollama_data/
|
ollama_data/
|
||||||
n8n/data/*.log
|
n8n/
|
||||||
n8n/data/*.json
|
|
||||||
temp/
|
temp/
|
||||||
|
infra/postgres/data/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs/*.log
|
logs/*.log
|
||||||
|
*.log
|
||||||
|
|
||||||
# IDE & AI Config
|
# IDE & AI Config
|
||||||
.continue/
|
.continue/
|
||||||
@@ -18,4 +21,4 @@ vscode_config/
|
|||||||
|
|
||||||
# Backup files
|
# Backup files
|
||||||
*.bak
|
*.bak
|
||||||
*.old
|
full_db_dump.sql
|
||||||
|
|||||||
90
archive/2026.02.18 Archive_old_mapps/brand_seeder.py.old
Normal file
90
archive/2026.02.18 Archive_old_mapps/brand_seeder.py.old
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/workers/brand_seeder.py
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import logging
|
||||||
|
from sqlalchemy import text
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
|
||||||
|
# Logolás beállítása a Sentinel monitorozáshoz
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s')
|
||||||
|
logger = logging.getLogger("Smart-Seeder-v1.0.2")
|
||||||
|
|
||||||
|
async def seed_with_priority():
|
||||||
|
"""
|
||||||
|
Feltölti a catalog_discovery táblát az RDW alapján.
|
||||||
|
Logika: Csak azokat a márkákat keressük, amikből legalább 10 db fut az utakon,
|
||||||
|
hogy ne szemeteljük tele a katalógust egyedi barkács-járművekkel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# RDW SoQL lekérdezés: Márka (merk), Típus (voertuigsoort) és Darabszám (total)
|
||||||
|
# A szerveroldali csoportosítás és szűrés (having total >= 10) miatt villámgyors.
|
||||||
|
RDW_URL = (
|
||||||
|
"https://opendata.rdw.nl/resource/m9d7-ebf2.json?"
|
||||||
|
"$select=merk,voertuigsoort,count(*)%20as%20total"
|
||||||
|
"&$group=merk,voertuigsoort"
|
||||||
|
"&$having=total%20>=%2010"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("📥 Adatok lekérése az RDW-től prioritásos besoroláshoz...")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=120) as client:
|
||||||
|
try:
|
||||||
|
resp = await client.get(RDW_URL)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
logger.error(f"❌ RDW API hiba: {resp.status_code}")
|
||||||
|
return
|
||||||
|
|
||||||
|
raw_data = resp.json()
|
||||||
|
logger.info(f"📊 {len(raw_data)} potenciális márka-kategória páros érkezett.")
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
for entry in raw_data:
|
||||||
|
make = str(entry.get("merk", "")).upper().strip()
|
||||||
|
v_kind = entry.get("voertuigsoort", "")
|
||||||
|
|
||||||
|
if not make:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- PRIORITÁS LOGIKA (Master Book 2.0 szerint) ---
|
||||||
|
# 1. Személyautó (Personenauto) -> 'pending' (Azonnal feldolgozandó)
|
||||||
|
# 2. Motor (Motorfiets) -> 'queued_motor'
|
||||||
|
# 3. Minden más (Teher, Busz, Mezőgazdasági) -> 'queued_heavy'
|
||||||
|
|
||||||
|
if "Personenauto" in v_kind:
|
||||||
|
status = 'pending'
|
||||||
|
v_class = 'car'
|
||||||
|
elif "Motorfiets" in v_kind:
|
||||||
|
status = 'queued_motor'
|
||||||
|
v_class = 'motorcycle'
|
||||||
|
else:
|
||||||
|
status = 'queued_heavy'
|
||||||
|
v_class = 'truck'
|
||||||
|
|
||||||
|
# UPSERT Logika: Ha már létezik, de még 'pending', akkor frissítjük a státuszt,
|
||||||
|
# de nem írjuk felül a már feldolgozott (processed) rekordokat.
|
||||||
|
query = text("""
|
||||||
|
INSERT INTO data.catalog_discovery (make, model, vehicle_class, source, status)
|
||||||
|
VALUES (:make, 'ALL_VARIANTS', :v_class, 'smart_seeder_v1_0_2', :status)
|
||||||
|
ON CONFLICT (make, model, vehicle_class)
|
||||||
|
DO UPDATE SET
|
||||||
|
status = CASE
|
||||||
|
WHEN data.catalog_discovery.status = 'pending' THEN EXCLUDED.status
|
||||||
|
ELSE data.catalog_discovery.status
|
||||||
|
END
|
||||||
|
WHERE data.catalog_discovery.make = EXCLUDED.make;
|
||||||
|
""")
|
||||||
|
|
||||||
|
await db.execute(query, {
|
||||||
|
"make": make,
|
||||||
|
"v_class": v_class,
|
||||||
|
"status": status
|
||||||
|
})
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
logger.info("✅ Discovery lista sikeresen feltöltve és prioritizálva.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Kritikus hiba a seeder futása közben: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(seed_with_priority())
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# /app/services/harvester_base.py
|
# /opt/docker/dev/service_finder/backend/app/services/harvester_base.py
|
||||||
import httpx
|
import httpx
|
||||||
import logging
|
import logging
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -8,12 +8,13 @@ from app.models.asset import AssetCatalog
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class BaseHarvester:
|
class BaseHarvester:
|
||||||
|
""" MDM Adatgyűjtő Alaposztály. """
|
||||||
def __init__(self, category: str):
|
def __init__(self, category: str):
|
||||||
self.category = category # car, bike, truck
|
self.category = category # 'car', 'motorcycle', 'truck'
|
||||||
self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/2.0"}
|
self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/2.1"}
|
||||||
|
|
||||||
async def check_exists(self, db: AsyncSession, brand: str, model: str, gen: str = None):
|
async def check_exists(self, db: AsyncSession, brand: str, model: str, gen: str = None):
|
||||||
"""Ellenőrzi a katalógusban való létezést."""
|
""" Ellenőrzi a katalógusban való létezést az új AssetCatalog modellben. """
|
||||||
stmt = select(AssetCatalog).where(
|
stmt = select(AssetCatalog).where(
|
||||||
AssetCatalog.make == brand,
|
AssetCatalog.make == brand,
|
||||||
AssetCatalog.model == model,
|
AssetCatalog.model == model,
|
||||||
@@ -26,7 +27,7 @@ class BaseHarvester:
|
|||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
async def log_entry(self, db: AsyncSession, brand: str, model: str, specs: dict):
|
async def log_entry(self, db: AsyncSession, brand: str, model: str, specs: dict):
|
||||||
"""Létrehoz vagy frissít egy bejegyzést az AssetCatalog-ban."""
|
""" Létrehoz vagy frissít egy bejegyzést. Támogatja a factory_data dúsítást. """
|
||||||
existing = await self.check_exists(db, brand, model, specs.get("generation"))
|
existing = await self.check_exists(db, brand, model, specs.get("generation"))
|
||||||
if not existing:
|
if not existing:
|
||||||
new_v = AssetCatalog(
|
new_v = AssetCatalog(
|
||||||
@@ -37,9 +38,11 @@ class BaseHarvester:
|
|||||||
year_to=specs.get("year_to"),
|
year_to=specs.get("year_to"),
|
||||||
vehicle_class=self.category,
|
vehicle_class=self.category,
|
||||||
fuel_type=specs.get("fuel_type"),
|
fuel_type=specs.get("fuel_type"),
|
||||||
engine_code=specs.get("engine_code")
|
power_kw=specs.get("power_kw"),
|
||||||
|
engine_capacity=specs.get("engine_capacity"),
|
||||||
|
factory_data=specs.get("factory_data", {}) # MDM JSONB tárolás
|
||||||
)
|
)
|
||||||
db.add(new_v)
|
db.add(new_v)
|
||||||
logger.info(f"🆕 Új katalógus elem: {brand} {model}")
|
logger.info(f"🆕 Új katalógus elem rögzítve: {brand} {model}")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
48
archive/2026.02.18 Archive_old_mapps/harvester_cars.py.old
Normal file
48
archive/2026.02.18 Archive_old_mapps/harvester_cars.py.old
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/services/harvester_cars.py
|
||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from .harvester_base import BaseHarvester
|
||||||
|
|
||||||
|
class VehicleHarvester(BaseHarvester):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(category="car")
|
||||||
|
self.base_url = "https://www.carqueryapi.com/api/0.3/"
|
||||||
|
|
||||||
|
async def _get_api_data(self, params: dict):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
response = await client.get(self.base_url, params=params, headers=self.headers, timeout=15.0)
|
||||||
|
if response.status_code == 200:
|
||||||
|
text = response.text
|
||||||
|
if text.startswith("?("): text = text[2:-2]
|
||||||
|
return response.json()
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"CarQuery Robot Hiba: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def harvest_all(self, db: AsyncSession):
|
||||||
|
""" Automatikus CarQuery szinkronizáció MDM alapon. """
|
||||||
|
print("🚗 Személyautó Robot: Indul az adatgyűjtés...")
|
||||||
|
|
||||||
|
makes_data = await self._get_api_data({"cmd": "getMakes", "sold_in_us": 0})
|
||||||
|
if not makes_data: return
|
||||||
|
|
||||||
|
for make in makes_data.get("Makes", [])[:50]: # Teszt limit
|
||||||
|
make_id = make['make_id']
|
||||||
|
make_name = make['make_display']
|
||||||
|
|
||||||
|
models_data = await self._get_api_data({"cmd": "getModels", "make": make_id})
|
||||||
|
if not models_data: continue
|
||||||
|
|
||||||
|
for model in models_data.get("Models", []):
|
||||||
|
specs = {
|
||||||
|
"factory_data": {"api_source": "carquery", "api_make_id": make_id}
|
||||||
|
}
|
||||||
|
await self.log_entry(db, make_name, model['model_name'], specs)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await asyncio.sleep(1) # Rate limiting
|
||||||
|
|
||||||
|
print("🏁 Személyautó Robot: Adatok szinkronizálva.")
|
||||||
115
archive/2026.02.18 Archive_old_mapps/technical_enricher.py.old
Normal file
115
archive/2026.02.18 Archive_old_mapps/technical_enricher.py.old
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from sqlalchemy import select, and_
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from app.db.session import SessionLocal
|
||||||
|
from app.models.vehicle_definitions import VehicleModelDefinition
|
||||||
|
from app.services.ai_service import AIService
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("Robot-Bulk-Master")
|
||||||
|
|
||||||
|
class TechEnricher:
|
||||||
|
API_URL = "https://opendata.rdw.nl/resource/kyri-nuah.json"
|
||||||
|
RDW_TOKEN = os.getenv("RDW_APP_TOKEN")
|
||||||
|
HEADERS = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def fetch_rdw_tech_data(cls, make, model):
|
||||||
|
params = {"merk": make.upper(), "handelsbenaming": str(model).strip().upper(), "$limit": 1}
|
||||||
|
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
|
||||||
|
try:
|
||||||
|
resp = await client.get(cls.API_URL, params=params, timeout=15)
|
||||||
|
return resp.json()[0] if resp.status_code == 200 and resp.json() else None
|
||||||
|
except: return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def run(cls):
|
||||||
|
logger.info("🚀 Master-Merge Robot FOLYAMATOS ÜZEMMÓD INDUL...")
|
||||||
|
|
||||||
|
while True: # Folyamatos ciklus, amíg el nem fogy az adat
|
||||||
|
async with SessionLocal() as main_db:
|
||||||
|
stmt = select(VehicleModelDefinition.id).where(
|
||||||
|
VehicleModelDefinition.status == "unverified"
|
||||||
|
).limit(50) # Egyszerre 50 ID-t foglalunk le
|
||||||
|
res = await main_db.execute(stmt)
|
||||||
|
ids = res.scalars().all()
|
||||||
|
|
||||||
|
if not ids:
|
||||||
|
logger.info("🏁 Minden rekord feldolgozva. A robot megáll.")
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info(f"📦 Új csomag indítása: {len(ids)} rekord.")
|
||||||
|
|
||||||
|
for m_id in ids:
|
||||||
|
async with SessionLocal() as db:
|
||||||
|
try:
|
||||||
|
current = await db.get(VehicleModelDefinition, m_id)
|
||||||
|
if not current: continue
|
||||||
|
|
||||||
|
logger.info(f"🧪 Feldolgozás: {current.make} {current.marketing_name} (ID: {m_id})")
|
||||||
|
|
||||||
|
rdw_data = await cls.fetch_rdw_tech_data(current.make, current.marketing_name)
|
||||||
|
if rdw_data:
|
||||||
|
current.engine_capacity = int(float(rdw_data.get("cilinderinhoud", 0))) or current.engine_capacity
|
||||||
|
current.power_kw = int(float(rdw_data.get("netto_maximum_vermogen_kw", 0))) or current.power_kw
|
||||||
|
|
||||||
|
ai_data = await AIService.get_clean_vehicle_data(current.make, current.marketing_name, current.vehicle_type)
|
||||||
|
|
||||||
|
if ai_data:
|
||||||
|
tech_code = ai_data.get("technical_code") or "N/A"
|
||||||
|
new_ccm = ai_data.get("ccm") or current.engine_capacity
|
||||||
|
|
||||||
|
master_record = None
|
||||||
|
if tech_code and tech_code != "N/A":
|
||||||
|
stmt_master = select(VehicleModelDefinition).where(and_(
|
||||||
|
VehicleModelDefinition.make == current.make,
|
||||||
|
VehicleModelDefinition.technical_code == tech_code,
|
||||||
|
VehicleModelDefinition.engine_capacity == new_ccm,
|
||||||
|
VehicleModelDefinition.status == 'ai_enriched',
|
||||||
|
VehicleModelDefinition.id != m_id
|
||||||
|
))
|
||||||
|
master_record = (await db.execute(stmt_master)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if master_record:
|
||||||
|
logger.info(f"🔗 Merge: ID:{m_id} -> Master ID:{master_record.id}")
|
||||||
|
syns = set(master_record.synonyms or [])
|
||||||
|
syns.update(ai_data.get("synonyms", []))
|
||||||
|
syns.add(current.marketing_name)
|
||||||
|
master_record.synonyms = list(syns)
|
||||||
|
current.status = "duplicate"
|
||||||
|
current.parent_id = master_record.id
|
||||||
|
else:
|
||||||
|
current.technical_code = tech_code if tech_code != "N/A" else f"N/A-{m_id}"
|
||||||
|
current.marketing_name = ai_data.get("marketing_name", current.marketing_name)
|
||||||
|
current.engine_capacity = new_ccm
|
||||||
|
current.power_kw = ai_data.get("kw") or current.power_kw
|
||||||
|
current.year_from = ai_data.get("year_from")
|
||||||
|
current.year_to = ai_data.get("year_to")
|
||||||
|
current.synonyms = ai_data.get("synonyms", [])
|
||||||
|
|
||||||
|
if ai_data.get("maintenance"):
|
||||||
|
old_spec = current.specifications or {}
|
||||||
|
old_spec.update(ai_data.get("maintenance"))
|
||||||
|
current.specifications = old_spec
|
||||||
|
|
||||||
|
current.status = "ai_enriched"
|
||||||
|
else:
|
||||||
|
if not current.technical_code:
|
||||||
|
current.technical_code = f"UNKNOWN-{m_id}"
|
||||||
|
|
||||||
|
current.updated_at = datetime.datetime.now()
|
||||||
|
await db.commit()
|
||||||
|
logger.info(f"✅ Mentve (ID: {m_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await db.rollback()
|
||||||
|
logger.error(f"❌ Hiba ID:{m_id}: {e}")
|
||||||
|
finally:
|
||||||
|
await db.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(TechEnricher.run())
|
||||||
55
archive/data-1772053521182.csv
Executable file
55
archive/data-1772053521182.csv
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
"schema_name","table_name"
|
||||||
|
"data","addresses"
|
||||||
|
"data","asset_assignments"
|
||||||
|
"data","asset_costs"
|
||||||
|
"data","asset_events"
|
||||||
|
"data","asset_financials"
|
||||||
|
"data","asset_inspections"
|
||||||
|
"data","asset_reviews"
|
||||||
|
"data","asset_telemetry"
|
||||||
|
"data","assets"
|
||||||
|
"data","audit_logs"
|
||||||
|
"data","badges"
|
||||||
|
"data","branches"
|
||||||
|
"data","catalog_discovery"
|
||||||
|
"data","credit_logs"
|
||||||
|
"data","discovery_parameters"
|
||||||
|
"data","exchange_rates"
|
||||||
|
"data","expertise_tags"
|
||||||
|
"data","feature_definitions"
|
||||||
|
"data","geo_postal_codes"
|
||||||
|
"data","geo_street_types"
|
||||||
|
"data","geo_streets"
|
||||||
|
"data","level_configs"
|
||||||
|
"data","model_feature_maps"
|
||||||
|
"data","org_sales_assignments"
|
||||||
|
"data","org_subscriptions"
|
||||||
|
"data","organization_financials"
|
||||||
|
"data","organization_members"
|
||||||
|
"data","organizations"
|
||||||
|
"data","point_rules"
|
||||||
|
"data","points_ledger"
|
||||||
|
"data","ratings"
|
||||||
|
"data","service_expertises"
|
||||||
|
"data","service_profiles"
|
||||||
|
"data","service_specialties"
|
||||||
|
"data","service_staging"
|
||||||
|
"data","subscription_tiers"
|
||||||
|
"data","system_parameters"
|
||||||
|
"data","translations"
|
||||||
|
"data","user_badges"
|
||||||
|
"data","user_stats"
|
||||||
|
"data","vehicle_catalog"
|
||||||
|
"data","vehicle_logbook"
|
||||||
|
"data","vehicle_model_definitions"
|
||||||
|
"data","vehicle_ownership_history"
|
||||||
|
"data","vehicle_ownerships"
|
||||||
|
"data","vehicle_types"
|
||||||
|
"identity","persons"
|
||||||
|
"identity","social_accounts"
|
||||||
|
"identity","users"
|
||||||
|
"identity","verification_tokens"
|
||||||
|
"identity","wallets"
|
||||||
|
"public","alembic_version"
|
||||||
|
"public","spatial_ref_sys"
|
||||||
|
"system","pending_actions"
|
||||||
|
Can't render this file because it contains an unexpected character in line 10 and column 15.
|
521
archive/data-1772053575794.csv
Executable file
521
archive/data-1772053575794.csv
Executable file
@@ -0,0 +1,521 @@
|
|||||||
|
"table_name","index_name","column_name"
|
||||||
|
"addresses","addresses_pkey","id"
|
||||||
|
"alembic_version","alembic_version_pkey","version_num"
|
||||||
|
"asset_assignments","asset_assignments_pkey","id"
|
||||||
|
"asset_costs","asset_costs_pkey","id"
|
||||||
|
"asset_costs","ix_data_asset_costs_registration_uuid","registration_uuid"
|
||||||
|
"asset_events","asset_events_pkey","id"
|
||||||
|
"asset_events","ix_data_asset_events_registration_uuid","registration_uuid"
|
||||||
|
"asset_financials","asset_financials_asset_id_key","asset_id"
|
||||||
|
"asset_financials","asset_financials_pkey","id"
|
||||||
|
"asset_inspections","asset_inspections_pkey","id"
|
||||||
|
"asset_reviews","asset_reviews_pkey","id"
|
||||||
|
"asset_telemetry","asset_telemetry_asset_id_key","asset_id"
|
||||||
|
"asset_telemetry","asset_telemetry_pkey","id"
|
||||||
|
"assets","assets_pkey","id"
|
||||||
|
"assets","ix_data_assets_license_plate","license_plate"
|
||||||
|
"assets","ix_data_assets_registration_uuid","registration_uuid"
|
||||||
|
"assets","ix_data_assets_vin","vin"
|
||||||
|
"audit_logs","audit_logs_pkey","id"
|
||||||
|
"audit_logs","ix_data_audit_logs_action","action"
|
||||||
|
"audit_logs","ix_data_audit_logs_id","id"
|
||||||
|
"audit_logs","ix_data_audit_logs_ip_address","ip_address"
|
||||||
|
"audit_logs","ix_data_audit_logs_target_id","target_id"
|
||||||
|
"audit_logs","ix_data_audit_logs_target_type","target_type"
|
||||||
|
"audit_logs","ix_data_audit_logs_timestamp","timestamp"
|
||||||
|
"badges","badges_name_key","name"
|
||||||
|
"badges","badges_pkey","id"
|
||||||
|
"badges","ix_data_badges_id","id"
|
||||||
|
"branches","branches_pkey","id"
|
||||||
|
"branches","ix_data_branches_city","city"
|
||||||
|
"branches","ix_data_branches_postal_code","postal_code"
|
||||||
|
"catalog_discovery","_make_model_class_uc","model"
|
||||||
|
"catalog_discovery","_make_model_class_uc","make"
|
||||||
|
"catalog_discovery","_make_model_class_uc","vehicle_class"
|
||||||
|
"catalog_discovery","catalog_discovery_pkey","id"
|
||||||
|
"catalog_discovery","ix_data_catalog_discovery_id","id"
|
||||||
|
"catalog_discovery","ix_data_catalog_discovery_make","make"
|
||||||
|
"catalog_discovery","ix_data_catalog_discovery_model","model"
|
||||||
|
"catalog_discovery","ix_data_catalog_discovery_status","status"
|
||||||
|
"catalog_discovery","ix_data_catalog_discovery_vehicle_class","vehicle_class"
|
||||||
|
"credit_logs","credit_logs_pkey","id"
|
||||||
|
"discovery_parameters","discovery_parameters_pkey","id"
|
||||||
|
"exchange_rates","exchange_rates_pkey","id"
|
||||||
|
"exchange_rates","exchange_rates_target_currency_key","target_currency"
|
||||||
|
"expertise_tags","expertise_tags_pkey","id"
|
||||||
|
"expertise_tags","ix_data_expertise_tags_key","key"
|
||||||
|
"feature_definitions","feature_definitions_pkey","id"
|
||||||
|
"feature_definitions","ix_data_feature_definitions_category","category"
|
||||||
|
"feature_definitions","ix_data_feature_definitions_code","code"
|
||||||
|
"geo_postal_codes","geo_postal_codes_pkey","id"
|
||||||
|
"geo_postal_codes","ix_data_geo_postal_codes_city","city"
|
||||||
|
"geo_postal_codes","ix_data_geo_postal_codes_zip_code","zip_code"
|
||||||
|
"geo_street_types","geo_street_types_name_key","name"
|
||||||
|
"geo_street_types","geo_street_types_pkey","id"
|
||||||
|
"geo_streets","geo_streets_pkey","id"
|
||||||
|
"geo_streets","ix_data_geo_streets_name","name"
|
||||||
|
"level_configs","ix_data_level_configs_id","id"
|
||||||
|
"level_configs","level_configs_level_number_key","level_number"
|
||||||
|
"level_configs","level_configs_pkey","id"
|
||||||
|
"model_feature_maps","model_feature_maps_pkey","id"
|
||||||
|
"org_sales_assignments","org_sales_assignments_pkey","id"
|
||||||
|
"org_subscriptions","org_subscriptions_pkey","id"
|
||||||
|
"organization_financials","ix_data_organization_financials_id","id"
|
||||||
|
"organization_financials","organization_financials_pkey","id"
|
||||||
|
"organization_members","ix_data_organization_members_id","id"
|
||||||
|
"organization_members","organization_members_pkey","id"
|
||||||
|
"organizations","ix_data_organizations_folder_slug","folder_slug"
|
||||||
|
"organizations","ix_data_organizations_id","id"
|
||||||
|
"organizations","ix_data_organizations_subscription_plan","subscription_plan"
|
||||||
|
"organizations","ix_data_organizations_tax_number","tax_number"
|
||||||
|
"organizations","organizations_pkey","id"
|
||||||
|
"pending_actions","ix_system_pending_actions_id","id"
|
||||||
|
"pending_actions","pending_actions_pkey","id"
|
||||||
|
"persons","ix_identity_persons_id","id"
|
||||||
|
"persons","ix_identity_persons_identity_hash","identity_hash"
|
||||||
|
"persons","persons_id_uuid_key","id_uuid"
|
||||||
|
"persons","persons_pkey","id"
|
||||||
|
"pg_aggregate","pg_aggregate_fnoid_index","aggfnoid"
|
||||||
|
"pg_am","pg_am_name_index","amname"
|
||||||
|
"pg_am","pg_am_oid_index","oid"
|
||||||
|
"pg_amop","pg_amop_fam_strat_index","amopstrategy"
|
||||||
|
"pg_amop","pg_amop_fam_strat_index","amopfamily"
|
||||||
|
"pg_amop","pg_amop_fam_strat_index","amoprighttype"
|
||||||
|
"pg_amop","pg_amop_fam_strat_index","amoplefttype"
|
||||||
|
"pg_amop","pg_amop_oid_index","oid"
|
||||||
|
"pg_amop","pg_amop_opr_fam_index","amopfamily"
|
||||||
|
"pg_amop","pg_amop_opr_fam_index","amoppurpose"
|
||||||
|
"pg_amop","pg_amop_opr_fam_index","amopopr"
|
||||||
|
"pg_amproc","pg_amproc_fam_proc_index","amprocrighttype"
|
||||||
|
"pg_amproc","pg_amproc_fam_proc_index","amproclefttype"
|
||||||
|
"pg_amproc","pg_amproc_fam_proc_index","amprocfamily"
|
||||||
|
"pg_amproc","pg_amproc_fam_proc_index","amprocnum"
|
||||||
|
"pg_amproc","pg_amproc_oid_index","oid"
|
||||||
|
"pg_attrdef","pg_attrdef_adrelid_adnum_index","adrelid"
|
||||||
|
"pg_attrdef","pg_attrdef_adrelid_adnum_index","adnum"
|
||||||
|
"pg_attrdef","pg_attrdef_oid_index","oid"
|
||||||
|
"pg_attribute","pg_attribute_relid_attnam_index","attname"
|
||||||
|
"pg_attribute","pg_attribute_relid_attnam_index","attrelid"
|
||||||
|
"pg_attribute","pg_attribute_relid_attnum_index","attnum"
|
||||||
|
"pg_attribute","pg_attribute_relid_attnum_index","attrelid"
|
||||||
|
"pg_auth_members","pg_auth_members_member_role_index","roleid"
|
||||||
|
"pg_auth_members","pg_auth_members_member_role_index","member"
|
||||||
|
"pg_auth_members","pg_auth_members_role_member_index","member"
|
||||||
|
"pg_auth_members","pg_auth_members_role_member_index","roleid"
|
||||||
|
"pg_authid","pg_authid_oid_index","oid"
|
||||||
|
"pg_authid","pg_authid_rolname_index","rolname"
|
||||||
|
"pg_cast","pg_cast_oid_index","oid"
|
||||||
|
"pg_cast","pg_cast_source_target_index","casttarget"
|
||||||
|
"pg_cast","pg_cast_source_target_index","castsource"
|
||||||
|
"pg_class","pg_class_oid_index","oid"
|
||||||
|
"pg_class","pg_class_relname_nsp_index","relnamespace"
|
||||||
|
"pg_class","pg_class_relname_nsp_index","relname"
|
||||||
|
"pg_class","pg_class_tblspc_relfilenode_index","reltablespace"
|
||||||
|
"pg_class","pg_class_tblspc_relfilenode_index","relfilenode"
|
||||||
|
"pg_collation","pg_collation_name_enc_nsp_index","collnamespace"
|
||||||
|
"pg_collation","pg_collation_name_enc_nsp_index","collname"
|
||||||
|
"pg_collation","pg_collation_name_enc_nsp_index","collencoding"
|
||||||
|
"pg_collation","pg_collation_oid_index","oid"
|
||||||
|
"pg_constraint","pg_constraint_conname_nsp_index","connamespace"
|
||||||
|
"pg_constraint","pg_constraint_conname_nsp_index","conname"
|
||||||
|
"pg_constraint","pg_constraint_conparentid_index","conparentid"
|
||||||
|
"pg_constraint","pg_constraint_conrelid_contypid_conname_index","conname"
|
||||||
|
"pg_constraint","pg_constraint_conrelid_contypid_conname_index","conrelid"
|
||||||
|
"pg_constraint","pg_constraint_conrelid_contypid_conname_index","contypid"
|
||||||
|
"pg_constraint","pg_constraint_contypid_index","contypid"
|
||||||
|
"pg_constraint","pg_constraint_oid_index","oid"
|
||||||
|
"pg_conversion","pg_conversion_default_index","conforencoding"
|
||||||
|
"pg_conversion","pg_conversion_default_index","oid"
|
||||||
|
"pg_conversion","pg_conversion_default_index","contoencoding"
|
||||||
|
"pg_conversion","pg_conversion_default_index","connamespace"
|
||||||
|
"pg_conversion","pg_conversion_name_nsp_index","connamespace"
|
||||||
|
"pg_conversion","pg_conversion_name_nsp_index","conname"
|
||||||
|
"pg_conversion","pg_conversion_oid_index","oid"
|
||||||
|
"pg_database","pg_database_datname_index","datname"
|
||||||
|
"pg_database","pg_database_oid_index","oid"
|
||||||
|
"pg_db_role_setting","pg_db_role_setting_databaseid_rol_index","setrole"
|
||||||
|
"pg_db_role_setting","pg_db_role_setting_databaseid_rol_index","setdatabase"
|
||||||
|
"pg_default_acl","pg_default_acl_oid_index","oid"
|
||||||
|
"pg_default_acl","pg_default_acl_role_nsp_obj_index","defaclrole"
|
||||||
|
"pg_default_acl","pg_default_acl_role_nsp_obj_index","defaclnamespace"
|
||||||
|
"pg_default_acl","pg_default_acl_role_nsp_obj_index","defaclobjtype"
|
||||||
|
"pg_depend","pg_depend_depender_index","objsubid"
|
||||||
|
"pg_depend","pg_depend_depender_index","objid"
|
||||||
|
"pg_depend","pg_depend_depender_index","classid"
|
||||||
|
"pg_depend","pg_depend_reference_index","refobjid"
|
||||||
|
"pg_depend","pg_depend_reference_index","refobjsubid"
|
||||||
|
"pg_depend","pg_depend_reference_index","refclassid"
|
||||||
|
"pg_description","pg_description_o_c_o_index","objoid"
|
||||||
|
"pg_description","pg_description_o_c_o_index","classoid"
|
||||||
|
"pg_description","pg_description_o_c_o_index","objsubid"
|
||||||
|
"pg_enum","pg_enum_oid_index","oid"
|
||||||
|
"pg_enum","pg_enum_typid_label_index","enumlabel"
|
||||||
|
"pg_enum","pg_enum_typid_label_index","enumtypid"
|
||||||
|
"pg_enum","pg_enum_typid_sortorder_index","enumtypid"
|
||||||
|
"pg_enum","pg_enum_typid_sortorder_index","enumsortorder"
|
||||||
|
"pg_event_trigger","pg_event_trigger_evtname_index","evtname"
|
||||||
|
"pg_event_trigger","pg_event_trigger_oid_index","oid"
|
||||||
|
"pg_extension","pg_extension_name_index","extname"
|
||||||
|
"pg_extension","pg_extension_oid_index","oid"
|
||||||
|
"pg_foreign_data_wrapper","pg_foreign_data_wrapper_name_index","fdwname"
|
||||||
|
"pg_foreign_data_wrapper","pg_foreign_data_wrapper_oid_index","oid"
|
||||||
|
"pg_foreign_server","pg_foreign_server_name_index","srvname"
|
||||||
|
"pg_foreign_server","pg_foreign_server_oid_index","oid"
|
||||||
|
"pg_foreign_table","pg_foreign_table_relid_index","ftrelid"
|
||||||
|
"pg_index","pg_index_indexrelid_index","indexrelid"
|
||||||
|
"pg_index","pg_index_indrelid_index","indrelid"
|
||||||
|
"pg_inherits","pg_inherits_parent_index","inhparent"
|
||||||
|
"pg_inherits","pg_inherits_relid_seqno_index","inhrelid"
|
||||||
|
"pg_inherits","pg_inherits_relid_seqno_index","inhseqno"
|
||||||
|
"pg_init_privs","pg_init_privs_o_c_o_index","objsubid"
|
||||||
|
"pg_init_privs","pg_init_privs_o_c_o_index","objoid"
|
||||||
|
"pg_init_privs","pg_init_privs_o_c_o_index","classoid"
|
||||||
|
"pg_language","pg_language_name_index","lanname"
|
||||||
|
"pg_language","pg_language_oid_index","oid"
|
||||||
|
"pg_largeobject","pg_largeobject_loid_pn_index","loid"
|
||||||
|
"pg_largeobject","pg_largeobject_loid_pn_index","pageno"
|
||||||
|
"pg_largeobject_metadata","pg_largeobject_metadata_oid_index","oid"
|
||||||
|
"pg_namespace","pg_namespace_nspname_index","nspname"
|
||||||
|
"pg_namespace","pg_namespace_oid_index","oid"
|
||||||
|
"pg_opclass","pg_opclass_am_name_nsp_index","opcmethod"
|
||||||
|
"pg_opclass","pg_opclass_am_name_nsp_index","opcnamespace"
|
||||||
|
"pg_opclass","pg_opclass_am_name_nsp_index","opcname"
|
||||||
|
"pg_opclass","pg_opclass_oid_index","oid"
|
||||||
|
"pg_operator","pg_operator_oid_index","oid"
|
||||||
|
"pg_operator","pg_operator_oprname_l_r_n_index","oprright"
|
||||||
|
"pg_operator","pg_operator_oprname_l_r_n_index","oprleft"
|
||||||
|
"pg_operator","pg_operator_oprname_l_r_n_index","oprnamespace"
|
||||||
|
"pg_operator","pg_operator_oprname_l_r_n_index","oprname"
|
||||||
|
"pg_opfamily","pg_opfamily_am_name_nsp_index","opfname"
|
||||||
|
"pg_opfamily","pg_opfamily_am_name_nsp_index","opfnamespace"
|
||||||
|
"pg_opfamily","pg_opfamily_am_name_nsp_index","opfmethod"
|
||||||
|
"pg_opfamily","pg_opfamily_oid_index","oid"
|
||||||
|
"pg_parameter_acl","pg_parameter_acl_oid_index","oid"
|
||||||
|
"pg_parameter_acl","pg_parameter_acl_parname_index","parname"
|
||||||
|
"pg_partitioned_table","pg_partitioned_table_partrelid_index","partrelid"
|
||||||
|
"pg_policy","pg_policy_oid_index","oid"
|
||||||
|
"pg_policy","pg_policy_polrelid_polname_index","polname"
|
||||||
|
"pg_policy","pg_policy_polrelid_polname_index","polrelid"
|
||||||
|
"pg_proc","pg_proc_oid_index","oid"
|
||||||
|
"pg_proc","pg_proc_proname_args_nsp_index","proname"
|
||||||
|
"pg_proc","pg_proc_proname_args_nsp_index","pronamespace"
|
||||||
|
"pg_proc","pg_proc_proname_args_nsp_index","proargtypes"
|
||||||
|
"pg_publication","pg_publication_oid_index","oid"
|
||||||
|
"pg_publication","pg_publication_pubname_index","pubname"
|
||||||
|
"pg_publication_namespace","pg_publication_namespace_oid_index","oid"
|
||||||
|
"pg_publication_namespace","pg_publication_namespace_pnnspid_pnpubid_index","pnnspid"
|
||||||
|
"pg_publication_namespace","pg_publication_namespace_pnnspid_pnpubid_index","pnpubid"
|
||||||
|
"pg_publication_rel","pg_publication_rel_oid_index","oid"
|
||||||
|
"pg_publication_rel","pg_publication_rel_prpubid_index","prpubid"
|
||||||
|
"pg_publication_rel","pg_publication_rel_prrelid_prpubid_index","prrelid"
|
||||||
|
"pg_publication_rel","pg_publication_rel_prrelid_prpubid_index","prpubid"
|
||||||
|
"pg_range","pg_range_rngmultitypid_index","rngmultitypid"
|
||||||
|
"pg_range","pg_range_rngtypid_index","rngtypid"
|
||||||
|
"pg_replication_origin","pg_replication_origin_roiident_index","roident"
|
||||||
|
"pg_replication_origin","pg_replication_origin_roname_index","roname"
|
||||||
|
"pg_rewrite","pg_rewrite_oid_index","oid"
|
||||||
|
"pg_rewrite","pg_rewrite_rel_rulename_index","rulename"
|
||||||
|
"pg_rewrite","pg_rewrite_rel_rulename_index","ev_class"
|
||||||
|
"pg_seclabel","pg_seclabel_object_index","objsubid"
|
||||||
|
"pg_seclabel","pg_seclabel_object_index","objoid"
|
||||||
|
"pg_seclabel","pg_seclabel_object_index","classoid"
|
||||||
|
"pg_seclabel","pg_seclabel_object_index","provider"
|
||||||
|
"pg_sequence","pg_sequence_seqrelid_index","seqrelid"
|
||||||
|
"pg_shdepend","pg_shdepend_depender_index","objsubid"
|
||||||
|
"pg_shdepend","pg_shdepend_depender_index","objid"
|
||||||
|
"pg_shdepend","pg_shdepend_depender_index","dbid"
|
||||||
|
"pg_shdepend","pg_shdepend_depender_index","classid"
|
||||||
|
"pg_shdepend","pg_shdepend_reference_index","refclassid"
|
||||||
|
"pg_shdepend","pg_shdepend_reference_index","refobjid"
|
||||||
|
"pg_shdescription","pg_shdescription_o_c_index","classoid"
|
||||||
|
"pg_shdescription","pg_shdescription_o_c_index","objoid"
|
||||||
|
"pg_shseclabel","pg_shseclabel_object_index","provider"
|
||||||
|
"pg_shseclabel","pg_shseclabel_object_index","objoid"
|
||||||
|
"pg_shseclabel","pg_shseclabel_object_index","classoid"
|
||||||
|
"pg_statistic","pg_statistic_relid_att_inh_index","staattnum"
|
||||||
|
"pg_statistic","pg_statistic_relid_att_inh_index","starelid"
|
||||||
|
"pg_statistic","pg_statistic_relid_att_inh_index","stainherit"
|
||||||
|
"pg_statistic_ext","pg_statistic_ext_name_index","stxname"
|
||||||
|
"pg_statistic_ext","pg_statistic_ext_name_index","stxnamespace"
|
||||||
|
"pg_statistic_ext","pg_statistic_ext_oid_index","oid"
|
||||||
|
"pg_statistic_ext","pg_statistic_ext_relid_index","stxrelid"
|
||||||
|
"pg_statistic_ext_data","pg_statistic_ext_data_stxoid_inh_index","stxdinherit"
|
||||||
|
"pg_statistic_ext_data","pg_statistic_ext_data_stxoid_inh_index","stxoid"
|
||||||
|
"pg_subscription","pg_subscription_oid_index","oid"
|
||||||
|
"pg_subscription","pg_subscription_subname_index","subdbid"
|
||||||
|
"pg_subscription","pg_subscription_subname_index","subname"
|
||||||
|
"pg_subscription_rel","pg_subscription_rel_srrelid_srsubid_index","srsubid"
|
||||||
|
"pg_subscription_rel","pg_subscription_rel_srrelid_srsubid_index","srrelid"
|
||||||
|
"pg_tablespace","pg_tablespace_oid_index","oid"
|
||||||
|
"pg_tablespace","pg_tablespace_spcname_index","spcname"
|
||||||
|
"pg_toast_1213","pg_toast_1213_index","chunk_seq"
|
||||||
|
"pg_toast_1213","pg_toast_1213_index","chunk_id"
|
||||||
|
"pg_toast_1247","pg_toast_1247_index","chunk_id"
|
||||||
|
"pg_toast_1247","pg_toast_1247_index","chunk_seq"
|
||||||
|
"pg_toast_1255","pg_toast_1255_index","chunk_id"
|
||||||
|
"pg_toast_1255","pg_toast_1255_index","chunk_seq"
|
||||||
|
"pg_toast_1260","pg_toast_1260_index","chunk_seq"
|
||||||
|
"pg_toast_1260","pg_toast_1260_index","chunk_id"
|
||||||
|
"pg_toast_1262","pg_toast_1262_index","chunk_seq"
|
||||||
|
"pg_toast_1262","pg_toast_1262_index","chunk_id"
|
||||||
|
"pg_toast_13454","pg_toast_13454_index","chunk_seq"
|
||||||
|
"pg_toast_13454","pg_toast_13454_index","chunk_id"
|
||||||
|
"pg_toast_13459","pg_toast_13459_index","chunk_seq"
|
||||||
|
"pg_toast_13459","pg_toast_13459_index","chunk_id"
|
||||||
|
"pg_toast_13464","pg_toast_13464_index","chunk_seq"
|
||||||
|
"pg_toast_13464","pg_toast_13464_index","chunk_id"
|
||||||
|
"pg_toast_13469","pg_toast_13469_index","chunk_id"
|
||||||
|
"pg_toast_13469","pg_toast_13469_index","chunk_seq"
|
||||||
|
"pg_toast_1417","pg_toast_1417_index","chunk_seq"
|
||||||
|
"pg_toast_1417","pg_toast_1417_index","chunk_id"
|
||||||
|
"pg_toast_1418","pg_toast_1418_index","chunk_seq"
|
||||||
|
"pg_toast_1418","pg_toast_1418_index","chunk_id"
|
||||||
|
"pg_toast_2328","pg_toast_2328_index","chunk_id"
|
||||||
|
"pg_toast_2328","pg_toast_2328_index","chunk_seq"
|
||||||
|
"pg_toast_2396","pg_toast_2396_index","chunk_seq"
|
||||||
|
"pg_toast_2396","pg_toast_2396_index","chunk_id"
|
||||||
|
"pg_toast_2600","pg_toast_2600_index","chunk_seq"
|
||||||
|
"pg_toast_2600","pg_toast_2600_index","chunk_id"
|
||||||
|
"pg_toast_2604","pg_toast_2604_index","chunk_id"
|
||||||
|
"pg_toast_2604","pg_toast_2604_index","chunk_seq"
|
||||||
|
"pg_toast_2606","pg_toast_2606_index","chunk_id"
|
||||||
|
"pg_toast_2606","pg_toast_2606_index","chunk_seq"
|
||||||
|
"pg_toast_2609","pg_toast_2609_index","chunk_seq"
|
||||||
|
"pg_toast_2609","pg_toast_2609_index","chunk_id"
|
||||||
|
"pg_toast_2612","pg_toast_2612_index","chunk_seq"
|
||||||
|
"pg_toast_2612","pg_toast_2612_index","chunk_id"
|
||||||
|
"pg_toast_2615","pg_toast_2615_index","chunk_seq"
|
||||||
|
"pg_toast_2615","pg_toast_2615_index","chunk_id"
|
||||||
|
"pg_toast_2618","pg_toast_2618_index","chunk_seq"
|
||||||
|
"pg_toast_2618","pg_toast_2618_index","chunk_id"
|
||||||
|
"pg_toast_2619","pg_toast_2619_index","chunk_id"
|
||||||
|
"pg_toast_2619","pg_toast_2619_index","chunk_seq"
|
||||||
|
"pg_toast_2620","pg_toast_2620_index","chunk_id"
|
||||||
|
"pg_toast_2620","pg_toast_2620_index","chunk_seq"
|
||||||
|
"pg_toast_2964","pg_toast_2964_index","chunk_id"
|
||||||
|
"pg_toast_2964","pg_toast_2964_index","chunk_seq"
|
||||||
|
"pg_toast_3079","pg_toast_3079_index","chunk_seq"
|
||||||
|
"pg_toast_3079","pg_toast_3079_index","chunk_id"
|
||||||
|
"pg_toast_3118","pg_toast_3118_index","chunk_id"
|
||||||
|
"pg_toast_3118","pg_toast_3118_index","chunk_seq"
|
||||||
|
"pg_toast_3256","pg_toast_3256_index","chunk_id"
|
||||||
|
"pg_toast_3256","pg_toast_3256_index","chunk_seq"
|
||||||
|
"pg_toast_3350","pg_toast_3350_index","chunk_seq"
|
||||||
|
"pg_toast_3350","pg_toast_3350_index","chunk_id"
|
||||||
|
"pg_toast_3381","pg_toast_3381_index","chunk_seq"
|
||||||
|
"pg_toast_3381","pg_toast_3381_index","chunk_id"
|
||||||
|
"pg_toast_3394","pg_toast_3394_index","chunk_id"
|
||||||
|
"pg_toast_3394","pg_toast_3394_index","chunk_seq"
|
||||||
|
"pg_toast_3429","pg_toast_3429_index","chunk_id"
|
||||||
|
"pg_toast_3429","pg_toast_3429_index","chunk_seq"
|
||||||
|
"pg_toast_3456","pg_toast_3456_index","chunk_seq"
|
||||||
|
"pg_toast_3456","pg_toast_3456_index","chunk_id"
|
||||||
|
"pg_toast_3466","pg_toast_3466_index","chunk_id"
|
||||||
|
"pg_toast_3466","pg_toast_3466_index","chunk_seq"
|
||||||
|
"pg_toast_3592","pg_toast_3592_index","chunk_seq"
|
||||||
|
"pg_toast_3592","pg_toast_3592_index","chunk_id"
|
||||||
|
"pg_toast_3596","pg_toast_3596_index","chunk_seq"
|
||||||
|
"pg_toast_3596","pg_toast_3596_index","chunk_id"
|
||||||
|
"pg_toast_3600","pg_toast_3600_index","chunk_id"
|
||||||
|
"pg_toast_3600","pg_toast_3600_index","chunk_seq"
|
||||||
|
"pg_toast_6000","pg_toast_6000_index","chunk_id"
|
||||||
|
"pg_toast_6000","pg_toast_6000_index","chunk_seq"
|
||||||
|
"pg_toast_6100","pg_toast_6100_index","chunk_seq"
|
||||||
|
"pg_toast_6100","pg_toast_6100_index","chunk_id"
|
||||||
|
"pg_toast_6106","pg_toast_6106_index","chunk_id"
|
||||||
|
"pg_toast_6106","pg_toast_6106_index","chunk_seq"
|
||||||
|
"pg_toast_6243","pg_toast_6243_index","chunk_id"
|
||||||
|
"pg_toast_6243","pg_toast_6243_index","chunk_seq"
|
||||||
|
"pg_toast_79789","pg_toast_79789_index","chunk_id"
|
||||||
|
"pg_toast_79789","pg_toast_79789_index","chunk_seq"
|
||||||
|
"pg_toast_826","pg_toast_826_index","chunk_seq"
|
||||||
|
"pg_toast_826","pg_toast_826_index","chunk_id"
|
||||||
|
"pg_toast_88701","pg_toast_88701_index","chunk_seq"
|
||||||
|
"pg_toast_88701","pg_toast_88701_index","chunk_id"
|
||||||
|
"pg_toast_88771","pg_toast_88771_index","chunk_seq"
|
||||||
|
"pg_toast_88771","pg_toast_88771_index","chunk_id"
|
||||||
|
"pg_toast_88783","pg_toast_88783_index","chunk_seq"
|
||||||
|
"pg_toast_88783","pg_toast_88783_index","chunk_id"
|
||||||
|
"pg_toast_88794","pg_toast_88794_index","chunk_seq"
|
||||||
|
"pg_toast_88794","pg_toast_88794_index","chunk_id"
|
||||||
|
"pg_toast_88809","pg_toast_88809_index","chunk_id"
|
||||||
|
"pg_toast_88809","pg_toast_88809_index","chunk_seq"
|
||||||
|
"pg_toast_88827","pg_toast_88827_index","chunk_id"
|
||||||
|
"pg_toast_88827","pg_toast_88827_index","chunk_seq"
|
||||||
|
"pg_toast_88838","pg_toast_88838_index","chunk_id"
|
||||||
|
"pg_toast_88838","pg_toast_88838_index","chunk_seq"
|
||||||
|
"pg_toast_88851","pg_toast_88851_index","chunk_id"
|
||||||
|
"pg_toast_88851","pg_toast_88851_index","chunk_seq"
|
||||||
|
"pg_toast_88861","pg_toast_88861_index","chunk_id"
|
||||||
|
"pg_toast_88861","pg_toast_88861_index","chunk_seq"
|
||||||
|
"pg_toast_88902","pg_toast_88902_index","chunk_seq"
|
||||||
|
"pg_toast_88902","pg_toast_88902_index","chunk_id"
|
||||||
|
"pg_toast_88946","pg_toast_88946_index","chunk_seq"
|
||||||
|
"pg_toast_88946","pg_toast_88946_index","chunk_id"
|
||||||
|
"pg_toast_88971","pg_toast_88971_index","chunk_id"
|
||||||
|
"pg_toast_88971","pg_toast_88971_index","chunk_seq"
|
||||||
|
"pg_toast_89018","pg_toast_89018_index","chunk_id"
|
||||||
|
"pg_toast_89018","pg_toast_89018_index","chunk_seq"
|
||||||
|
"pg_toast_89064","pg_toast_89064_index","chunk_id"
|
||||||
|
"pg_toast_89064","pg_toast_89064_index","chunk_seq"
|
||||||
|
"pg_toast_89098","pg_toast_89098_index","chunk_seq"
|
||||||
|
"pg_toast_89098","pg_toast_89098_index","chunk_id"
|
||||||
|
"pg_toast_89129","pg_toast_89129_index","chunk_id"
|
||||||
|
"pg_toast_89129","pg_toast_89129_index","chunk_seq"
|
||||||
|
"pg_toast_89178","pg_toast_89178_index","chunk_seq"
|
||||||
|
"pg_toast_89178","pg_toast_89178_index","chunk_id"
|
||||||
|
"pg_toast_89231","pg_toast_89231_index","chunk_seq"
|
||||||
|
"pg_toast_89231","pg_toast_89231_index","chunk_id"
|
||||||
|
"pg_toast_89273","pg_toast_89273_index","chunk_seq"
|
||||||
|
"pg_toast_89273","pg_toast_89273_index","chunk_id"
|
||||||
|
"pg_toast_89295","pg_toast_89295_index","chunk_id"
|
||||||
|
"pg_toast_89295","pg_toast_89295_index","chunk_seq"
|
||||||
|
"pg_toast_89374","pg_toast_89374_index","chunk_seq"
|
||||||
|
"pg_toast_89374","pg_toast_89374_index","chunk_id"
|
||||||
|
"pg_toast_89400","pg_toast_89400_index","chunk_id"
|
||||||
|
"pg_toast_89400","pg_toast_89400_index","chunk_seq"
|
||||||
|
"pg_toast_89457","pg_toast_89457_index","chunk_id"
|
||||||
|
"pg_toast_89457","pg_toast_89457_index","chunk_seq"
|
||||||
|
"pg_toast_89482","pg_toast_89482_index","chunk_id"
|
||||||
|
"pg_toast_89482","pg_toast_89482_index","chunk_seq"
|
||||||
|
"pg_toast_89497","pg_toast_89497_index","chunk_seq"
|
||||||
|
"pg_toast_89497","pg_toast_89497_index","chunk_id"
|
||||||
|
"pg_toast_89513","pg_toast_89513_index","chunk_id"
|
||||||
|
"pg_toast_89513","pg_toast_89513_index","chunk_seq"
|
||||||
|
"pg_toast_89548","pg_toast_89548_index","chunk_id"
|
||||||
|
"pg_toast_89548","pg_toast_89548_index","chunk_seq"
|
||||||
|
"pg_toast_89597","pg_toast_89597_index","chunk_seq"
|
||||||
|
"pg_toast_89597","pg_toast_89597_index","chunk_id"
|
||||||
|
"pg_toast_90028","pg_toast_90028_index","chunk_id"
|
||||||
|
"pg_toast_90028","pg_toast_90028_index","chunk_seq"
|
||||||
|
"pg_toast_91674","pg_toast_91674_index","chunk_id"
|
||||||
|
"pg_toast_91674","pg_toast_91674_index","chunk_seq"
|
||||||
|
"pg_toast_98885","pg_toast_98885_index","chunk_id"
|
||||||
|
"pg_toast_98885","pg_toast_98885_index","chunk_seq"
|
||||||
|
"pg_transform","pg_transform_oid_index","oid"
|
||||||
|
"pg_transform","pg_transform_type_lang_index","trflang"
|
||||||
|
"pg_transform","pg_transform_type_lang_index","trftype"
|
||||||
|
"pg_trigger","pg_trigger_oid_index","oid"
|
||||||
|
"pg_trigger","pg_trigger_tgconstraint_index","tgconstraint"
|
||||||
|
"pg_trigger","pg_trigger_tgrelid_tgname_index","tgname"
|
||||||
|
"pg_trigger","pg_trigger_tgrelid_tgname_index","tgrelid"
|
||||||
|
"pg_ts_config","pg_ts_config_cfgname_index","cfgname"
|
||||||
|
"pg_ts_config","pg_ts_config_cfgname_index","cfgnamespace"
|
||||||
|
"pg_ts_config","pg_ts_config_oid_index","oid"
|
||||||
|
"pg_ts_config_map","pg_ts_config_map_index","mapcfg"
|
||||||
|
"pg_ts_config_map","pg_ts_config_map_index","mapseqno"
|
||||||
|
"pg_ts_config_map","pg_ts_config_map_index","maptokentype"
|
||||||
|
"pg_ts_dict","pg_ts_dict_dictname_index","dictnamespace"
|
||||||
|
"pg_ts_dict","pg_ts_dict_dictname_index","dictname"
|
||||||
|
"pg_ts_dict","pg_ts_dict_oid_index","oid"
|
||||||
|
"pg_ts_parser","pg_ts_parser_oid_index","oid"
|
||||||
|
"pg_ts_parser","pg_ts_parser_prsname_index","prsname"
|
||||||
|
"pg_ts_parser","pg_ts_parser_prsname_index","prsnamespace"
|
||||||
|
"pg_ts_template","pg_ts_template_oid_index","oid"
|
||||||
|
"pg_ts_template","pg_ts_template_tmplname_index","tmplname"
|
||||||
|
"pg_ts_template","pg_ts_template_tmplname_index","tmplnamespace"
|
||||||
|
"pg_type","pg_type_oid_index","oid"
|
||||||
|
"pg_type","pg_type_typname_nsp_index","typnamespace"
|
||||||
|
"pg_type","pg_type_typname_nsp_index","typname"
|
||||||
|
"pg_user_mapping","pg_user_mapping_oid_index","oid"
|
||||||
|
"pg_user_mapping","pg_user_mapping_user_server_index","umserver"
|
||||||
|
"pg_user_mapping","pg_user_mapping_user_server_index","umuser"
|
||||||
|
"point_rules","ix_data_point_rules_action_key","action_key"
|
||||||
|
"point_rules","ix_data_point_rules_id","id"
|
||||||
|
"point_rules","point_rules_pkey","id"
|
||||||
|
"points_ledger","ix_data_points_ledger_id","id"
|
||||||
|
"points_ledger","points_ledger_pkey","id"
|
||||||
|
"ratings","idx_rating_branch","target_branch_id"
|
||||||
|
"ratings","idx_rating_org","target_organization_id"
|
||||||
|
"ratings","idx_rating_user","target_user_id"
|
||||||
|
"ratings","ratings_pkey","id"
|
||||||
|
"service_expertises","service_expertises_pkey","expertise_id"
|
||||||
|
"service_expertises","service_expertises_pkey","service_id"
|
||||||
|
"service_profiles","idx_service_fingerprint","fingerprint"
|
||||||
|
"service_profiles","idx_service_profiles_location","location"
|
||||||
|
"service_profiles","ix_data_service_profiles_fingerprint","fingerprint"
|
||||||
|
"service_profiles","ix_data_service_profiles_id","id"
|
||||||
|
"service_profiles","ix_data_service_profiles_location","location"
|
||||||
|
"service_profiles","ix_data_service_profiles_status","status"
|
||||||
|
"service_profiles","service_profiles_google_place_id_key","google_place_id"
|
||||||
|
"service_profiles","service_profiles_organization_id_key","organization_id"
|
||||||
|
"service_profiles","service_profiles_pkey","id"
|
||||||
|
"service_specialties","ix_data_service_specialties_slug","slug"
|
||||||
|
"service_specialties","service_specialties_pkey","id"
|
||||||
|
"service_staging","idx_staging_fingerprint","fingerprint"
|
||||||
|
"service_staging","ix_data_service_staging_city","city"
|
||||||
|
"service_staging","ix_data_service_staging_id","id"
|
||||||
|
"service_staging","ix_data_service_staging_name","name"
|
||||||
|
"service_staging","ix_data_service_staging_postal_code","postal_code"
|
||||||
|
"service_staging","ix_data_service_staging_status","status"
|
||||||
|
"service_staging","service_staging_pkey","id"
|
||||||
|
"social_accounts","ix_identity_social_accounts_id","id"
|
||||||
|
"social_accounts","ix_identity_social_accounts_social_id","social_id"
|
||||||
|
"social_accounts","social_accounts_pkey","id"
|
||||||
|
"social_accounts","uix_social_provider_id","provider"
|
||||||
|
"social_accounts","uix_social_provider_id","social_id"
|
||||||
|
"spatial_ref_sys","spatial_ref_sys_pkey","srid"
|
||||||
|
"subscription_tiers","ix_data_subscription_tiers_name","name"
|
||||||
|
"subscription_tiers","subscription_tiers_pkey","id"
|
||||||
|
"system_parameters","system_parameters_key_key","key"
|
||||||
|
"system_parameters","system_parameters_pkey","id"
|
||||||
|
"translations","ix_data_translations_id","id"
|
||||||
|
"translations","ix_data_translations_key","key"
|
||||||
|
"translations","ix_data_translations_lang","lang"
|
||||||
|
"translations","translations_pkey","id"
|
||||||
|
"user_badges","ix_data_user_badges_id","id"
|
||||||
|
"user_badges","user_badges_pkey","id"
|
||||||
|
"user_stats","user_stats_pkey","user_id"
|
||||||
|
"users","ix_identity_users_email","email"
|
||||||
|
"users","ix_identity_users_folder_slug","folder_slug"
|
||||||
|
"users","ix_identity_users_id","id"
|
||||||
|
"users","users_pkey","id"
|
||||||
|
"users","users_referral_code_key","referral_code"
|
||||||
|
"vehicle_catalog","ix_data_vehicle_catalog_engine_capacity","engine_capacity"
|
||||||
|
"vehicle_catalog","ix_data_vehicle_catalog_engine_variant","engine_variant"
|
||||||
|
"vehicle_catalog","ix_data_vehicle_catalog_fuel_type","fuel_type"
|
||||||
|
"vehicle_catalog","ix_data_vehicle_catalog_generation","generation"
|
||||||
|
"vehicle_catalog","ix_data_vehicle_catalog_id","id"
|
||||||
|
"vehicle_catalog","ix_data_vehicle_catalog_make","make"
|
||||||
|
"vehicle_catalog","ix_data_vehicle_catalog_model","model"
|
||||||
|
"vehicle_catalog","ix_data_vehicle_catalog_power_kw","power_kw"
|
||||||
|
"vehicle_catalog","uix_vehicle_catalog_full","year_from"
|
||||||
|
"vehicle_catalog","uix_vehicle_catalog_full","make"
|
||||||
|
"vehicle_catalog","uix_vehicle_catalog_full","model"
|
||||||
|
"vehicle_catalog","uix_vehicle_catalog_full","engine_variant"
|
||||||
|
"vehicle_catalog","uix_vehicle_catalog_full","fuel_type"
|
||||||
|
"vehicle_catalog","vehicle_catalog_pkey","id"
|
||||||
|
"vehicle_logbook","vehicle_logbook_pkey","id"
|
||||||
|
"vehicle_model_definitions","idx_vmd_engine_code","engine_code"
|
||||||
|
"vehicle_model_definitions","idx_vmd_lookup","make"
|
||||||
|
"vehicle_model_definitions","idx_vmd_lookup","technical_code"
|
||||||
|
"vehicle_model_definitions","idx_vmd_lookup_fast","normalized_name"
|
||||||
|
"vehicle_model_definitions","idx_vmd_lookup_fast","make"
|
||||||
|
"vehicle_model_definitions","idx_vmd_normalized_name","normalized_name"
|
||||||
|
"vehicle_model_definitions","ix_data_vehicle_model_definitions_make","make"
|
||||||
|
"vehicle_model_definitions","ix_data_vehicle_model_definitions_marketing_name","marketing_name"
|
||||||
|
"vehicle_model_definitions","ix_data_vehicle_model_definitions_status","status"
|
||||||
|
"vehicle_model_definitions","ix_data_vehicle_model_definitions_technical_code","technical_code"
|
||||||
|
"vehicle_model_definitions","ix_data_vehicle_model_definitions_year_from","year_from"
|
||||||
|
"vehicle_model_definitions","ix_data_vehicle_model_definitions_year_to","year_to"
|
||||||
|
"vehicle_model_definitions","ix_vehicle_model_marketing_name","marketing_name"
|
||||||
|
"vehicle_model_definitions","uix_make_tech_type","technical_code"
|
||||||
|
"vehicle_model_definitions","uix_make_tech_type","make"
|
||||||
|
"vehicle_model_definitions","uix_make_tech_type","vehicle_type_id"
|
||||||
|
"vehicle_model_definitions","uix_vmd_precision","variant_code"
|
||||||
|
"vehicle_model_definitions","uix_vmd_precision","make"
|
||||||
|
"vehicle_model_definitions","uix_vmd_precision","version_code"
|
||||||
|
"vehicle_model_definitions","uix_vmd_precision","fuel_type"
|
||||||
|
"vehicle_model_definitions","uix_vmd_precision","normalized_name"
|
||||||
|
"vehicle_model_definitions","vehicle_model_definitions_pkey","id"
|
||||||
|
"vehicle_ownership_history","vehicle_ownership_history_pkey","id"
|
||||||
|
"vehicle_ownerships","ix_data_vehicle_ownerships_id","id"
|
||||||
|
"vehicle_ownerships","vehicle_ownerships_pkey","id"
|
||||||
|
"vehicle_types","ix_data_vehicle_types_code","code"
|
||||||
|
"vehicle_types","vehicle_types_pkey","id"
|
||||||
|
"verification_tokens","ix_identity_verification_tokens_id","id"
|
||||||
|
"verification_tokens","verification_tokens_pkey","id"
|
||||||
|
"verification_tokens","verification_tokens_token_key","token"
|
||||||
|
"wallets","ix_identity_wallets_id","id"
|
||||||
|
"wallets","wallets_pkey","id"
|
||||||
|
"wallets","wallets_user_id_key","user_id"
|
||||||
|
Can't render this file because it contains an unexpected character in line 12 and column 41.
|
@@ -1,27 +1,24 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/Dockerfile
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 1. Rendszerfüggőségek telepítése (gcc és képkezelő könyvtárak)
|
# Rendszerfüggőségek (OCR-hez és DB-hez)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
gcc \
|
gcc \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libjpeg-dev \
|
libgl1 \
|
||||||
zlib1g-dev \
|
libglib2.0-0 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 2. PIP frissítése
|
|
||||||
RUN pip install --upgrade pip
|
|
||||||
|
|
||||||
# 3. Függőségek telepítése
|
|
||||||
# Fontos: A requirements.txt fájlba írd be: Pillow==10.2.0
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# 4. A kód másolása
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV PYTHONPATH=/app
|
ENV PYTHONPATH=/app
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,4 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/api/deps.py
|
||||||
from typing import Optional, Dict, Any, Union
|
from typing import Optional, Dict, Any, Union
|
||||||
import logging
|
import logging
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
@@ -7,11 +8,18 @@ from sqlalchemy import select
|
|||||||
|
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.core.security import decode_token, DEFAULT_RANK_MAP
|
from app.core.security import decode_token, DEFAULT_RANK_MAP
|
||||||
from app.models.identity import User, UserRole
|
from app.models.identity import User, UserRole # JAVÍTVA: Új Identity modell használata
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# --- GONDOLATMENET / THOUGHT PROCESS ---
|
||||||
|
# 1. Az OAuth2 folyamat a központosított bejelentkezési végponton keresztül fut.
|
||||||
|
# 2. A token visszafejtésekor ellenőrizni kell a 'type' mezőt, hogy ne lehessen refresh tokennel belépni.
|
||||||
|
# 3. A felhasználó lekérésekor a SQLAlchemy 2.0 aszinkron 'execute' és 'scalar_one_or_none' metódusait használjuk.
|
||||||
|
# 4. A Scoped RBAC (Role-Based Access Control) biztosítja, hogy a felhasználók ne férjenek hozzá egymás flottáihoz.
|
||||||
|
# ---------------------------------------
|
||||||
|
|
||||||
# Az OAuth2 folyamat a bejelentkezési végponton keresztül
|
# Az OAuth2 folyamat a bejelentkezési végponton keresztül
|
||||||
reusable_oauth2 = OAuth2PasswordBearer(
|
reusable_oauth2 = OAuth2PasswordBearer(
|
||||||
tokenUrl=f"{settings.API_V1_STR}/auth/login"
|
tokenUrl=f"{settings.API_V1_STR}/auth/login"
|
||||||
@@ -23,8 +31,7 @@ async def get_current_token_payload(
|
|||||||
"""
|
"""
|
||||||
JWT token visszafejtése és a típus (access) ellenőrzése.
|
JWT token visszafejtése és a típus (access) ellenőrzése.
|
||||||
"""
|
"""
|
||||||
# Dev bypass (ha esetleg fejlesztéshez használtad korábban, itt a helye,
|
# Fejlesztői bypass (opcionális, csak DEBUG módban)
|
||||||
# de élesben a token validáció fut le)
|
|
||||||
if settings.DEBUG and token == "dev_bypass_active":
|
if settings.DEBUG and token == "dev_bypass_active":
|
||||||
return {
|
return {
|
||||||
"sub": "1",
|
"sub": "1",
|
||||||
@@ -48,7 +55,7 @@ async def get_current_user(
|
|||||||
payload: Dict = Depends(get_current_token_payload)
|
payload: Dict = Depends(get_current_token_payload)
|
||||||
) -> User:
|
) -> User:
|
||||||
"""
|
"""
|
||||||
Lekéri a felhasználót a token 'sub' mezője alapján.
|
Lekéri a felhasználót a token 'sub' mezője alapján (SQLAlchemy 2.0 aszinkron módon).
|
||||||
"""
|
"""
|
||||||
user_id = payload.get("sub")
|
user_id = payload.get("sub")
|
||||||
if not user_id:
|
if not user_id:
|
||||||
@@ -57,6 +64,7 @@ async def get_current_user(
|
|||||||
detail="Token azonosítási hiba."
|
detail="Token azonosítási hiba."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés
|
||||||
result = await db.execute(select(User).where(User.id == int(user_id)))
|
result = await db.execute(select(User).where(User.id == int(user_id)))
|
||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
@@ -71,13 +79,12 @@ async def get_current_active_user(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
) -> User:
|
) -> User:
|
||||||
"""
|
"""
|
||||||
Ellenőrzi, hogy a felhasználó aktív-e.
|
Ellenőrzi, hogy a felhasználó aktív-e (KYC Step 2 kész).
|
||||||
Ez elengedhetetlen az Admin felület és a védett végpontok számára.
|
|
||||||
"""
|
"""
|
||||||
if not current_user.is_active:
|
if not current_user.is_active:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="A művelethez aktív profil és KYC azonosítás (Step 2) szükséges."
|
detail="A művelethez aktív profil és KYC azonosítás szükséges."
|
||||||
)
|
)
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
@@ -86,22 +93,19 @@ async def check_resource_access(
|
|||||||
current_user: User = Depends(get_current_user)
|
current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Scoped RBAC: Megakadályozza, hogy egy felhasználó más valaki erőforrásaihoz nyúljon.
|
Scoped RBAC: Megakadályozza a jogosulatlan hozzáférést mások adataihoz.
|
||||||
Kezeli az ID-t (int) és a Scope ID-t / Slug-ot (str) is.
|
|
||||||
"""
|
"""
|
||||||
if current_user.role == UserRole.superadmin:
|
if current_user.role == UserRole.superadmin:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Ha a usernek van beállított scope_id-ja (pl. egy flottához tartozik),
|
user_scope = str(current_user.scope_id) if current_user.scope_id else None
|
||||||
# akkor ellenőrizzük, hogy a kért erőforrás abba a scope-ba tartozik-e.
|
|
||||||
user_scope = current_user.scope_id
|
|
||||||
requested_scope = str(resource_scope_id)
|
requested_scope = str(resource_scope_id)
|
||||||
|
|
||||||
# 1. Saját erőforrás (saját ID)
|
# 1. Saját ID ellenőrzése
|
||||||
if str(current_user.id) == requested_scope:
|
if str(current_user.id) == requested_scope:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 2. Scope alapú hozzáférés (pl. flotta tagja)
|
# 2. Szervezeti/Flotta scope ellenőrzése
|
||||||
if user_scope and user_scope == requested_scope:
|
if user_scope and user_scope == requested_scope:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -112,8 +116,7 @@ async def check_resource_access(
|
|||||||
|
|
||||||
def check_min_rank(role_key: str):
|
def check_min_rank(role_key: str):
|
||||||
"""
|
"""
|
||||||
Dinamikus Rank ellenőrzés.
|
Dinamikus Rank ellenőrzés a system_parameters tábla alapján.
|
||||||
Az adatbázisból (system_parameters) kéri le az elvárt szintet.
|
|
||||||
"""
|
"""
|
||||||
async def rank_checker(
|
async def rank_checker(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
@@ -130,7 +133,7 @@ def check_min_rank(role_key: str):
|
|||||||
if user_rank < required_rank:
|
if user_rank < required_rank:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail=f"Alacsony jogosultsági szint. (Szükséges: {required_rank})"
|
detail=f"Alacsony jogosultsági szint. (Elvárt: {required_rank})"
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
return rank_checker
|
return rank_checker
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
from fastapi import APIRouter, Request
|
# /opt/docker/dev/service_finder/backend/app/api/recommend.py
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import text
|
||||||
|
from app.db.session import get_db
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/provider/inbox")
|
@router.get("/provider/inbox")
|
||||||
def provider_inbox(request: Request, provider_id: str):
|
async def provider_inbox(provider_id: str, db: AsyncSession = Depends(get_db)):
|
||||||
cur = request.state.db.cursor()
|
""" Aszinkron szerviz-postaláda lekérdezés. """
|
||||||
cur.execute("""
|
query = text("""
|
||||||
SELECT * FROM app.v_provider_inbox
|
SELECT * FROM data.service_profiles
|
||||||
WHERE provider_listing_id = %s
|
WHERE id = :p_id
|
||||||
ORDER BY created_at DESC
|
""")
|
||||||
""", (provider_id,))
|
result = await db.execute(query, {"p_id": provider_id})
|
||||||
rows = cur.fetchall()
|
return [dict(row._mapping) for row in result.fetchall()]
|
||||||
return rows
|
|
||||||
Binary file not shown.
@@ -1,32 +1,20 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/api/v1/api.py
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from app.api.v1.endpoints import auth, catalog, assets, organizations, documents, services, admin, expenses, evidence
|
from app.api.v1.endpoints import (
|
||||||
|
auth, catalog, assets, organizations, documents,
|
||||||
|
services, admin, expenses, evidence, social
|
||||||
|
)
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
# Hitelesítés (Authentication)
|
# Minden modul az új, refaktorált végpontokra mutat
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||||
|
|
||||||
# Szolgáltatások és Vadászat (Service Hunt & Discovery)
|
|
||||||
api_router.include_router(services.router, prefix="/services", tags=["Service Hunt & Discovery"])
|
api_router.include_router(services.router, prefix="/services", tags=["Service Hunt & Discovery"])
|
||||||
|
|
||||||
# Katalógus (Vehicle Catalog)
|
|
||||||
api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"])
|
api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"])
|
||||||
|
|
||||||
# Eszközök / Járművek (Assets)
|
|
||||||
api_router.include_router(assets.router, prefix="/assets", tags=["Assets"])
|
api_router.include_router(assets.router, prefix="/assets", tags=["Assets"])
|
||||||
|
|
||||||
# Szervezetek (Organizations)
|
|
||||||
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
|
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
|
||||||
|
|
||||||
# Dokumentumok (Documents)
|
|
||||||
api_router.include_router(documents.router, prefix="/documents", tags=["Documents"])
|
api_router.include_router(documents.router, prefix="/documents", tags=["Documents"])
|
||||||
|
|
||||||
# --- 🛡️ SENTINEL ADMIN KONTROLL PANEL ---
|
|
||||||
# Ez a rész tette láthatóvá az Admin API-t a felületen
|
|
||||||
api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Center (Sentinel)"])
|
api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Center (Sentinel)"])
|
||||||
|
|
||||||
# Evidence & OCR Robot 3
|
|
||||||
api_router.include_router(evidence.router, prefix="/evidence", tags=["Evidence & OCR (Robot 3)"])
|
api_router.include_router(evidence.router, prefix="/evidence", tags=["Evidence & OCR (Robot 3)"])
|
||||||
|
|
||||||
# Fleet Expenses TCO
|
|
||||||
api_router.include_router(expenses.router, prefix="/expenses", tags=["Fleet Expenses (TCO)"])
|
api_router.include_router(expenses.router, prefix="/expenses", tags=["Fleet Expenses (TCO)"])
|
||||||
|
api_router.include_router(social.router, prefix="/social", tags=["Social & Leaderboard"])
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,4 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select, func, text, delete
|
from sqlalchemy import select, func, text, delete
|
||||||
@@ -5,11 +6,12 @@ from typing import List, Any, Dict, Optional
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from app.api import deps
|
from app.api import deps
|
||||||
from app.models.identity import User, UserRole
|
from app.models.identity import User, UserRole # JAVÍTVA: Központi import
|
||||||
from app.models.system import SystemParameter
|
from app.models.system import SystemParameter
|
||||||
|
# JAVÍTVA: Security audit modellek
|
||||||
|
from app.models.audit import SecurityAuditLog, OperationalLog
|
||||||
|
# JAVÍTVA: Ezek a modellek a security.py-ból jönnek (ha ott vannak)
|
||||||
from app.models.security import PendingAction, ActionStatus
|
from app.models.security import PendingAction, ActionStatus
|
||||||
from app.models.history import AuditLog, LogSeverity
|
|
||||||
from app.schemas.admin_security import PendingActionResponse, SecurityStatusResponse
|
|
||||||
|
|
||||||
from app.services.security_service import security_service
|
from app.services.security_service import security_service
|
||||||
from app.services.translation_service import TranslationService
|
from app.services.translation_service import TranslationService
|
||||||
@@ -24,30 +26,23 @@ class ConfigUpdate(BaseModel):
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# --- 🛡️ ADMIN JOGOSULTSÁG ELLENŐRZŐ ---
|
|
||||||
async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)):
|
async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)):
|
||||||
"""Szigorú hozzáférés-ellenőrzés: Csak Admin vagy Superadmin."""
|
""" Csak Admin vagy Superadmin. """
|
||||||
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
|
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Sentinel jogosultság szükséges a művelethez!"
|
detail="Sentinel jogosultság szükséges!"
|
||||||
)
|
)
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
# --- 🛰️ 1. SENTINEL: RENDSZERÁLLAPOT ÉS MONITORING ---
|
|
||||||
|
|
||||||
@router.get("/health-monitor", tags=["Sentinel Monitoring"])
|
@router.get("/health-monitor", tags=["Sentinel Monitoring"])
|
||||||
async def get_system_health(
|
async def get_system_health(
|
||||||
db: AsyncSession = Depends(deps.get_db),
|
db: AsyncSession = Depends(deps.get_db),
|
||||||
admin: User = Depends(check_admin_access)
|
admin: User = Depends(check_admin_access)
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
Rendszer pulzusának ellenőrzése (pgAdmin nélkül).
|
|
||||||
Látod a felhasználók eloszlását, az eszközök számát és a kritikus hibákat.
|
|
||||||
"""
|
|
||||||
stats = {}
|
stats = {}
|
||||||
|
|
||||||
# Adatbázis statisztikák (Dynamic counts)
|
# Adatbázis statisztikák (Nyers SQL marad, mert hatékony)
|
||||||
user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM data.users GROUP BY subscription_plan"))
|
user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM data.users GROUP BY subscription_plan"))
|
||||||
stats["user_distribution"] = {row[0]: row[1] for row in user_stats}
|
stats["user_distribution"] = {row[0]: row[1] for row in user_stats}
|
||||||
|
|
||||||
@@ -57,24 +52,24 @@ async def get_system_health(
|
|||||||
org_count = await db.execute(text("SELECT count(*) FROM data.organizations"))
|
org_count = await db.execute(text("SELECT count(*) FROM data.organizations"))
|
||||||
stats["total_organizations"] = org_count.scalar()
|
stats["total_organizations"] = org_count.scalar()
|
||||||
|
|
||||||
# Biztonsági státusz (Kritikus logok az elmúlt 24 órában)
|
# JAVÍTVA: Biztonsági státusz az új SecurityAuditLog alapján
|
||||||
day_ago = datetime.now() - timedelta(days=1)
|
day_ago = datetime.now() - timedelta(days=1)
|
||||||
crit_logs = await db.execute(select(func.count(AuditLog.id)).where(
|
crit_logs = await db.execute(
|
||||||
AuditLog.severity.in_([LogSeverity.critical, LogSeverity.emergency]),
|
select(func.count(SecurityAuditLog.id))
|
||||||
AuditLog.timestamp >= day_ago
|
.where(
|
||||||
))
|
SecurityAuditLog.is_critical == True,
|
||||||
|
SecurityAuditLog.created_at >= day_ago
|
||||||
|
)
|
||||||
|
)
|
||||||
stats["critical_alerts_24h"] = crit_logs.scalar() or 0
|
stats["critical_alerts_24h"] = crit_logs.scalar() or 0
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
# --- ⚖️ 2. SENTINEL: NÉGY SZEM ELV (Approval System) ---
|
@router.get("/pending-actions", response_model=List[Any], tags=["Sentinel Security"])
|
||||||
|
|
||||||
@router.get("/pending-actions", response_model=List[PendingActionResponse], tags=["Sentinel Security"])
|
|
||||||
async def list_pending_actions(
|
async def list_pending_actions(
|
||||||
db: AsyncSession = Depends(deps.get_db),
|
db: AsyncSession = Depends(deps.get_db),
|
||||||
admin: User = Depends(check_admin_access)
|
admin: User = Depends(check_admin_access)
|
||||||
):
|
):
|
||||||
"""Jóváhagyásra váró kritikus kérések listázása (pl. törlések, rang-emelések)."""
|
|
||||||
stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
|
stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
@@ -85,33 +80,26 @@ async def approve_action(
|
|||||||
db: AsyncSession = Depends(deps.get_db),
|
db: AsyncSession = Depends(deps.get_db),
|
||||||
admin: User = Depends(check_admin_access)
|
admin: User = Depends(check_admin_access)
|
||||||
):
|
):
|
||||||
"""Művelet véglegesítése. Csak egy második admin hagyhatja jóvá az első kérését."""
|
|
||||||
try:
|
try:
|
||||||
await security_service.approve_action(db, admin.id, action_id)
|
await security_service.approve_action(db, admin.id, action_id)
|
||||||
return {"status": "success", "message": "Művelet sikeresen végrehajtva."}
|
return {"status": "success", "message": "Művelet végrehajtva."}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
# --- ⚙️ 3. DINAMIKUS KONFIGURÁCIÓ (Hierarchical Config) ---
|
|
||||||
|
|
||||||
@router.get("/parameters", tags=["Dynamic Configuration"])
|
@router.get("/parameters", tags=["Dynamic Configuration"])
|
||||||
async def list_all_parameters(
|
async def list_all_parameters(
|
||||||
db: AsyncSession = Depends(deps.get_db),
|
db: AsyncSession = Depends(deps.get_db),
|
||||||
admin: User = Depends(check_admin_access)
|
admin: User = Depends(check_admin_access)
|
||||||
):
|
):
|
||||||
"""Minden globális és lokális paraméter (Limitek, XP szorzók stb.) lekérése."""
|
|
||||||
result = await db.execute(select(SystemParameter))
|
result = await db.execute(select(SystemParameter))
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
@router.post("/parameters", tags=["Dynamic Configuration"])
|
@router.post("/parameters", tags=["Dynamic Configuration"])
|
||||||
async def set_parameter(
|
async def set_parameter(
|
||||||
config: ConfigUpdate, # <--- Most már egy objektumot várunk a Body-ban
|
config: ConfigUpdate,
|
||||||
db: AsyncSession = Depends(deps.get_db),
|
db: AsyncSession = Depends(deps.get_db),
|
||||||
admin: User = Depends(check_admin_access)
|
admin: User = Depends(check_admin_access)
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
Paraméter beállítása. A Swaggerben most már látsz egy JSON ablakot a 'value' számára!
|
|
||||||
"""
|
|
||||||
query = text("""
|
query = text("""
|
||||||
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
|
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
|
||||||
VALUES (:key, :val, :sl, :sid, :cat, :user)
|
VALUES (:key, :val, :sl, :sid, :cat, :user)
|
||||||
@@ -125,7 +113,7 @@ async def set_parameter(
|
|||||||
|
|
||||||
await db.execute(query, {
|
await db.execute(query, {
|
||||||
"key": config.key,
|
"key": config.key,
|
||||||
"val": config.value, # Itt bármilyen komplex JSON-t átadhatsz
|
"val": config.value,
|
||||||
"sl": config.scope_level,
|
"sl": config.scope_level,
|
||||||
"sid": config.scope_id,
|
"sid": config.scope_id,
|
||||||
"cat": config.category,
|
"cat": config.category,
|
||||||
@@ -134,31 +122,10 @@ async def set_parameter(
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
return {"status": "success", "message": f"'{config.key}' frissítve."}
|
return {"status": "success", "message": f"'{config.key}' frissítve."}
|
||||||
|
|
||||||
@router.delete("/parameters/{key}", tags=["Dynamic Configuration"])
|
|
||||||
async def delete_parameter(
|
|
||||||
key: str,
|
|
||||||
scope_level: str = "global",
|
|
||||||
scope_id: Optional[str] = None,
|
|
||||||
db: AsyncSession = Depends(deps.get_db),
|
|
||||||
admin: User = Depends(check_admin_access)
|
|
||||||
):
|
|
||||||
"""Egy adott konfiguráció törlése (visszaállás az eggyel magasabb szintű alapértelmezésre)."""
|
|
||||||
stmt = delete(SystemParameter).where(
|
|
||||||
SystemParameter.key == key,
|
|
||||||
SystemParameter.scope_level == scope_level,
|
|
||||||
SystemParameter.scope_id == scope_id
|
|
||||||
)
|
|
||||||
await db.execute(stmt)
|
|
||||||
await db.commit()
|
|
||||||
return {"status": "success", "message": "Konfiguráció törölve."}
|
|
||||||
|
|
||||||
# --- 🌍 4. UTILITY: FORDÍTÁSOK ---
|
|
||||||
|
|
||||||
@router.post("/translations/sync", tags=["System Utilities"])
|
@router.post("/translations/sync", tags=["System Utilities"])
|
||||||
async def sync_translations_to_json(
|
async def sync_translations_to_json(
|
||||||
db: AsyncSession = Depends(deps.get_db),
|
db: AsyncSession = Depends(deps.get_db),
|
||||||
admin: User = Depends(check_admin_access)
|
admin: User = Depends(check_admin_access)
|
||||||
):
|
):
|
||||||
"""Szinkronizálja az adatbázisban tárolt fordításokat a JSON fájlokba."""
|
|
||||||
await TranslationService.export_to_json(db)
|
await TranslationService.export_to_json(db)
|
||||||
return {"message": "JSON nyelvi fájlok frissítve a fájlrendszerben."}
|
return {"message": "JSON fájlok frissítve."}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
@@ -8,39 +9,31 @@ from sqlalchemy.orm import selectinload
|
|||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.api.deps import get_current_user
|
from app.api.deps import get_current_user
|
||||||
from app.models.asset import Asset, AssetCost, AssetTelemetry
|
from app.models.asset import Asset, AssetCost, AssetTelemetry
|
||||||
from app.models.identity import User
|
from app.models.identity import User # JAVÍTVA: Centralizált import
|
||||||
from app.services.cost_service import cost_service
|
from app.services.cost_service import cost_service
|
||||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
|
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
|
||||||
# --- IMPORT JAVÍTVA: Behozzuk a jármű sémát a dúsított adatokhoz ---
|
|
||||||
from app.schemas.asset import AssetResponse
|
from app.schemas.asset import AssetResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# --- 1. MODUL: IDENTITÁS (Alapadatok & Technikai katalógus) ---
|
|
||||||
@router.get("/{asset_id}", response_model=AssetResponse)
|
@router.get("/{asset_id}", response_model=AssetResponse)
|
||||||
async def get_asset_identity(
|
async def get_asset_identity(
|
||||||
asset_id: uuid.UUID,
|
asset_id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user)
|
current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
Visszaadja a jármű alapadatokat és a dúsított katalógus információkat (kW, CCM, tengelyek).
|
|
||||||
A selectinload(Asset.catalog) biztosítja, hogy a technikai adatok is betöltődjenek.
|
|
||||||
"""
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Asset)
|
select(Asset)
|
||||||
.where(Asset.id == asset_id)
|
.where(Asset.id == asset_id)
|
||||||
.options(selectinload(Asset.catalog))
|
.options(selectinload(Asset.catalog))
|
||||||
)
|
)
|
||||||
asset = (await db.execute(stmt)).scalar_one_or_none()
|
asset = (await db.execute(stmt)).scalar_one_or_none()
|
||||||
|
|
||||||
if not asset:
|
if not asset:
|
||||||
raise HTTPException(status_code=404, detail="Jármű nem található")
|
raise HTTPException(status_code=404, detail="Jármű nem található")
|
||||||
|
|
||||||
# Közvetlenül az objektumot adjuk vissza, a Pydantic AssetResponse
|
|
||||||
# modellje fogja formázni a kimenetet a dúsított adatokkal együtt.
|
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
# ... a többi marad, de az importok immár stabilak ...
|
||||||
|
|
||||||
# --- 2. MODUL: PÉNZÜGY (Költségek) ---
|
# --- 2. MODUL: PÉNZÜGY (Költségek) ---
|
||||||
@router.get("/{asset_id}/costs", response_model=Dict[str, Any])
|
@router.get("/{asset_id}/costs", response_model=Dict[str, Any])
|
||||||
async def get_asset_costs(
|
async def get_asset_costs(
|
||||||
|
|||||||
@@ -1,95 +1,23 @@
|
|||||||
|
# backend/app/api/v1/endpoints/auth.py
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from authlib.integrations.starlette_client import OAuth
|
|
||||||
|
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.services.auth_service import AuthService
|
from app.services.auth_service import AuthService
|
||||||
from app.services.social_auth_service import SocialAuthService
|
|
||||||
from app.core.security import create_tokens, DEFAULT_RANK_MAP
|
from app.core.security import create_tokens, DEFAULT_RANK_MAP
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.schemas.auth import (
|
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
|
||||||
UserLiteRegister, Token, PasswordResetRequest,
|
|
||||||
UserKYCComplete, PasswordResetConfirm
|
|
||||||
)
|
|
||||||
from app.api.deps import get_current_user
|
from app.api.deps import get_current_user
|
||||||
from app.models.identity import User
|
from app.models.identity import User # JAVÍTVA: Új központi modell
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# --- GOOGLE OAUTH KONFIGURÁCIÓ ---
|
@router.post("/login", response_model=Token)
|
||||||
oauth = OAuth()
|
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
|
||||||
oauth.register(
|
user = await AuthService.authenticate(db, form_data.username, form_data.password)
|
||||||
name='google',
|
if not user:
|
||||||
client_id=settings.GOOGLE_CLIENT_ID,
|
raise HTTPException(status_code=401, detail="Hibás adatok.")
|
||||||
client_secret=settings.GOOGLE_CLIENT_SECRET,
|
|
||||||
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
|
|
||||||
client_kwargs={'scope': 'openid email profile'}
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- SOCIAL AUTH ENDPOINTS ---
|
|
||||||
|
|
||||||
@router.get("/login/google")
|
|
||||||
async def login_google(request: Request):
|
|
||||||
"""
|
|
||||||
Step 1: Átirányítás a Google bejelentkező oldalára.
|
|
||||||
"""
|
|
||||||
redirect_uri = settings.GOOGLE_CALLBACK_URL
|
|
||||||
return await oauth.google.authorize_redirect(request, redirect_uri)
|
|
||||||
|
|
||||||
@router.get("/callback/google")
|
|
||||||
async def auth_google(request: Request, db: AsyncSession = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Step 2: Google visszahívás lekezelése + Dupla Token generálás.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
token = await oauth.google.authorize_access_token(request)
|
|
||||||
user_info = token.get('userinfo')
|
|
||||||
except Exception:
|
|
||||||
raise HTTPException(status_code=400, detail="Google hitelesítési hiba.")
|
|
||||||
|
|
||||||
if not user_info:
|
|
||||||
raise HTTPException(status_code=400, detail="Nincs adat a Google-től.")
|
|
||||||
|
|
||||||
# Step 1: Technikai user létrehozása/keresése (inaktív, nincs mappa)
|
|
||||||
user = await SocialAuthService.get_or_create_social_user(
|
|
||||||
db, provider="google", social_id=user_info['sub'], email=user_info['email'],
|
|
||||||
first_name=user_info.get('given_name'), last_name=user_info.get('family_name')
|
|
||||||
)
|
|
||||||
|
|
||||||
# Dinamikus token generálás
|
|
||||||
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
|
||||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
|
||||||
user_rank = ranks.get(role_name, 10)
|
|
||||||
|
|
||||||
token_data = {
|
|
||||||
"sub": str(user.id),
|
|
||||||
"role": role_name,
|
|
||||||
"rank": user_rank,
|
|
||||||
"scope_level": user.scope_level or "individual",
|
|
||||||
"scope_id": user.scope_id or str(user.id),
|
|
||||||
"region": user.region_code
|
|
||||||
}
|
|
||||||
|
|
||||||
access, refresh = create_tokens(data=token_data)
|
|
||||||
|
|
||||||
# Visszatérés a frontendre mindkét tokennel
|
|
||||||
response_url = f"{settings.FRONTEND_BASE_URL}/auth/callback?access={access}&refresh={refresh}"
|
|
||||||
return RedirectResponse(url=response_url)
|
|
||||||
|
|
||||||
|
|
||||||
# --- STANDARD AUTH ENDPOINTS ---
|
|
||||||
|
|
||||||
@router.post("/register-lite", response_model=Token, status_code=status.HTTP_201_CREATED)
|
|
||||||
async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
|
|
||||||
"""Step 1: Manuális regisztráció (inaktív, nincs mappa)."""
|
|
||||||
stmt = select(User).where(User.email == user_in.email)
|
|
||||||
if (await db.execute(stmt)).scalar_one_or_none():
|
|
||||||
raise HTTPException(status_code=400, detail="Email már regisztrálva.")
|
|
||||||
|
|
||||||
user = await AuthService.register_lite(db, user_in)
|
|
||||||
|
|
||||||
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
||||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
||||||
@@ -98,79 +26,16 @@ async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(ge
|
|||||||
"sub": str(user.id),
|
"sub": str(user.id),
|
||||||
"role": role_name,
|
"role": role_name,
|
||||||
"rank": ranks.get(role_name, 10),
|
"rank": ranks.get(role_name, 10),
|
||||||
"scope_level": "individual",
|
|
||||||
"scope_id": str(user.id),
|
|
||||||
"region": user.region_code
|
|
||||||
}
|
|
||||||
|
|
||||||
access, refresh = create_tokens(data=token_data)
|
|
||||||
return {
|
|
||||||
"access_token": access,
|
|
||||||
"refresh_token": refresh,
|
|
||||||
"token_type": "bearer",
|
|
||||||
"is_active": user.is_active
|
|
||||||
}
|
|
||||||
|
|
||||||
@router.post("/login", response_model=Token)
|
|
||||||
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
|
|
||||||
"""Hagyományos belépés + Dupla Token."""
|
|
||||||
user = await AuthService.authenticate(db, form_data.username, form_data.password)
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=401, detail="Hibás adatok.")
|
|
||||||
|
|
||||||
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
|
||||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
|
||||||
user_rank = ranks.get(role_name, 10)
|
|
||||||
|
|
||||||
token_data = {
|
|
||||||
"sub": str(user.id),
|
|
||||||
"role": role_name,
|
|
||||||
"rank": user_rank,
|
|
||||||
"scope_level": user.scope_level or "individual",
|
"scope_level": user.scope_level or "individual",
|
||||||
"scope_id": user.scope_id or str(user.id),
|
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
|
||||||
"region": user.region_code
|
|
||||||
}
|
}
|
||||||
|
|
||||||
access, refresh = create_tokens(data=token_data)
|
access, refresh = create_tokens(data=token_data)
|
||||||
return {
|
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
|
||||||
"access_token": access,
|
|
||||||
"refresh_token": refresh,
|
|
||||||
"token_type": "bearer",
|
|
||||||
"is_active": user.is_active
|
|
||||||
}
|
|
||||||
|
|
||||||
@router.get("/verify-email")
|
|
||||||
async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
|
|
||||||
if not await AuthService.verify_email(db, token):
|
|
||||||
raise HTTPException(status_code=400, detail="Érvénytelen token.")
|
|
||||||
return {"message": "Email megerősítve!"}
|
|
||||||
|
|
||||||
@router.post("/complete-kyc")
|
@router.post("/complete-kyc")
|
||||||
async def complete_kyc(
|
async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
kyc_in: UserKYCComplete,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user: User = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Step 2: KYC Aktiválás.
|
|
||||||
It használjuk a get_current_user-t (nem active), mert a user még inaktív.
|
|
||||||
"""
|
|
||||||
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
|
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User nem található.")
|
raise HTTPException(status_code=404, detail="User nem található.")
|
||||||
return {"status": "success", "message": "Fiók aktiválva."}
|
return {"status": "success", "message": "Fiók aktiválva."}
|
||||||
|
|
||||||
@router.post("/forgot-password")
|
|
||||||
async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)):
|
|
||||||
result = await AuthService.initiate_password_reset(db, req.email)
|
|
||||||
if result == "cooldown":
|
|
||||||
raise HTTPException(status_code=429, detail="Túl sok kérés.")
|
|
||||||
return {"message": "Visszaállító link kiküldve."}
|
|
||||||
|
|
||||||
@router.post("/reset-password")
|
|
||||||
async def reset_password(req: PasswordResetConfirm, db: AsyncSession = Depends(get_db)):
|
|
||||||
if req.password != req.password_confirm:
|
|
||||||
raise HTTPException(status_code=400, detail="Nem egyeznek a jelszavak.")
|
|
||||||
if not await AuthService.reset_password(db, req.email, req.token, req.password):
|
|
||||||
raise HTTPException(status_code=400, detail="Sikertelen frissítés.")
|
|
||||||
return {"message": "Jelszó frissítve!"}
|
|
||||||
@@ -1,125 +1,36 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
# backend/app/api/v1/endpoints/billing.py
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import text
|
from sqlalchemy import select, text
|
||||||
from app.api.deps import get_db, get_current_user
|
from app.api.deps import get_db, get_current_user
|
||||||
from typing import List, Dict
|
from app.models.identity import User, Wallet
|
||||||
|
from app.models.audit import FinancialLedger # JAVÍTVA: Tranzakciós napló
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# 1. EGYENLEG LEKÉRDEZÉSE (A felhasználó Széfjéhez kötve)
|
|
||||||
@router.get("/balance")
|
@router.get("/balance")
|
||||||
async def get_balance(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
async def get_balance(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
"""
|
stmt = select(Wallet).where(Wallet.user_id == current_user.id)
|
||||||
Visszaadja a felhasználó aktuális kreditegyenlegét és a Széfje (Cége) nevét.
|
wallet = (await db.execute(stmt)).scalar_one_or_none()
|
||||||
"""
|
|
||||||
query = text("""
|
|
||||||
SELECT
|
|
||||||
uc.balance,
|
|
||||||
c.name as company_name
|
|
||||||
FROM data.user_credits uc
|
|
||||||
JOIN data.companies c ON uc.user_id = c.owner_id
|
|
||||||
WHERE uc.user_id = :user_id
|
|
||||||
LIMIT 1
|
|
||||||
""")
|
|
||||||
result = await db.execute(query, {"user_id": current_user.id})
|
|
||||||
row = result.fetchone()
|
|
||||||
|
|
||||||
if not row:
|
|
||||||
return {
|
return {
|
||||||
"company_name": "Privát Széf",
|
"earned": float(wallet.earned_credits) if wallet else 0,
|
||||||
"balance": 0.0,
|
"purchased": float(wallet.purchased_credits) if wallet else 0,
|
||||||
"currency": "Credit"
|
"service_coins": float(wallet.service_coins) if wallet else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
"company_name": row.company_name,
|
|
||||||
"balance": float(row.balance),
|
|
||||||
"currency": "Credit"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2. TRANZAKCIÓS ELŐZMÉNYEK
|
|
||||||
@router.get("/history")
|
|
||||||
async def get_history(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
|
||||||
"""
|
|
||||||
Kilistázza a kreditmozgásokat (bevételek, költések, voucherek).
|
|
||||||
"""
|
|
||||||
query = text("""
|
|
||||||
SELECT amount, reason, created_at
|
|
||||||
FROM data.credit_transactions
|
|
||||||
WHERE user_id = :user_id
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
""")
|
|
||||||
result = await db.execute(query, {"user_id": current_user.id})
|
|
||||||
return [dict(row._mapping) for row in result.fetchall()]
|
|
||||||
|
|
||||||
# 3. VOUCHER BEVÁLTÁS (A rendszer gazdaságának motorja)
|
|
||||||
@router.post("/vouchers/redeem")
|
@router.post("/vouchers/redeem")
|
||||||
async def redeem_voucher(code: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
async def redeem_voucher(code: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
"""
|
check = await db.execute(text("SELECT * FROM data.vouchers WHERE code = :c AND is_used = False"), {"c": code.upper()})
|
||||||
Bevált egy kódot, és jóváírja az értékét a felhasználó egyenlegén.
|
voucher = check.fetchone()
|
||||||
"""
|
|
||||||
# 1. Voucher ellenőrzése
|
|
||||||
check_query = text("""
|
|
||||||
SELECT id, value, is_used, expires_at
|
|
||||||
FROM data.vouchers
|
|
||||||
WHERE code = :code AND is_used = False AND (expires_at > now() OR expires_at IS NULL)
|
|
||||||
""")
|
|
||||||
res = await db.execute(check_query, {"code": code.strip().upper()})
|
|
||||||
voucher = res.fetchone()
|
|
||||||
|
|
||||||
if not voucher:
|
if not voucher:
|
||||||
raise HTTPException(status_code=400, detail="Érvénytelen, lejárt vagy már felhasznált kód.")
|
raise HTTPException(status_code=400, detail="Érvénytelen kód.")
|
||||||
|
|
||||||
# 2. Egyenleg frissítése (vagy létrehozása, ha még nincs sor a user_credits-ben)
|
stmt = select(Wallet).where(Wallet.user_id == current_user.id)
|
||||||
update_balance = text("""
|
wallet = (await db.execute(stmt)).scalar_one_or_none()
|
||||||
INSERT INTO data.user_credits (user_id, balance)
|
wallet.purchased_credits += voucher.value
|
||||||
VALUES (:u, :v)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET balance = data.user_credits.balance + :v
|
|
||||||
""")
|
|
||||||
await db.execute(update_balance, {"u": current_user.id, "v": voucher.value})
|
|
||||||
|
|
||||||
# 3. Tranzakció naplózása
|
|
||||||
log_transaction = text("""
|
|
||||||
INSERT INTO data.credit_transactions (user_id, amount, reason)
|
|
||||||
VALUES (:u, :v, :r)
|
|
||||||
""")
|
|
||||||
await db.execute(log_transaction, {
|
|
||||||
"u": current_user.id,
|
|
||||||
"v": voucher.value,
|
|
||||||
"r": f"Voucher beváltva: {code}"
|
|
||||||
})
|
|
||||||
|
|
||||||
# 4. Voucher megjelölése felhasználtként
|
|
||||||
await db.execute(text("""
|
|
||||||
UPDATE data.vouchers
|
|
||||||
SET is_used = True, used_by = :u, used_at = now()
|
|
||||||
WHERE id = :vid
|
|
||||||
"""), {"u": current_user.id, "vid": voucher.id})
|
|
||||||
|
|
||||||
|
db.add(FinancialLedger(user_id=current_user.id, amount=voucher.value, transaction_type="VOUCHER_REDEEM", details={"code": code}))
|
||||||
|
await db.execute(text("UPDATE data.vouchers SET is_used=True, used_by=:u WHERE id=:v"), {"u": current_user.id, "v": voucher.id})
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return {"status": "success", "added_value": float(voucher.value), "message": "Kredit jóváírva!"}
|
return {"status": "success", "added": float(voucher.value)}
|
||||||
|
|
||||||
# 4. ADMIN: VOUCHER GENERÁLÁS (Csak Neked)
|
|
||||||
@router.post("/vouchers/generate", include_in_schema=True)
|
|
||||||
async def generate_vouchers(
|
|
||||||
count: int = 1,
|
|
||||||
value: float = 500.0,
|
|
||||||
batch_name: str = "ADMIN_GEN",
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Tömeges voucher generálás az admin felületről.
|
|
||||||
"""
|
|
||||||
generated_codes = []
|
|
||||||
for _ in range(count):
|
|
||||||
# Generálunk egy SF-XXXX-XXXX formátumú kódot
|
|
||||||
code = f"SF-{secrets.token_hex(3).upper()}-{secrets.token_hex(3).upper()}"
|
|
||||||
await db.execute(text("""
|
|
||||||
INSERT INTO data.vouchers (code, value, batch_id, expires_at)
|
|
||||||
VALUES (:c, :v, :b, now() + interval '90 days')
|
|
||||||
"""), {"c": code, "v": value, "b": batch_name})
|
|
||||||
generated_codes.append(code)
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
return {"batch": batch_name, "count": count, "codes": generated_codes}
|
|
||||||
@@ -1,66 +1,24 @@
|
|||||||
# backend/app/api/v1/endpoints/evidence.py
|
# backend/app/api/v1/endpoints/evidence.py
|
||||||
from fastapi import APIRouter, UploadFile, File, HTTPException, status, Depends
|
from fastapi import APIRouter, UploadFile, File, HTTPException, status, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import text
|
from sqlalchemy import select, func, text
|
||||||
from app.api.deps import get_db, get_current_user
|
from app.api.deps import get_db, get_current_user
|
||||||
from app.schemas.evidence import OcrResponse
|
from app.models.identity import User
|
||||||
from app.services.image_processor import DocumentImageProcessor
|
from app.models.asset import Asset # JAVÍTVA: Asset modell
|
||||||
from app.services.ai_ocr_service import AiOcrService
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/scan-registration", response_model=OcrResponse)
|
@router.post("/scan-registration")
|
||||||
async def scan_registration_document(
|
async def scan_registration_document(file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
file: UploadFile = File(...),
|
stmt_limit = text("SELECT (value->>:plan)::int FROM data.system_parameters WHERE key = 'VEHICLE_LIMIT'")
|
||||||
db: AsyncSession = Depends(get_db),
|
res = await db.execute(stmt_limit, {"plan": current_user.subscription_plan or "free"})
|
||||||
current_user = Depends(get_current_user)
|
max_allowed = res.scalar() or 1
|
||||||
):
|
|
||||||
"""
|
|
||||||
Forgalmi engedély feldolgozása dinamikus, rendszer-szintű korlátok ellenőrzésével.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 1. 🔍 DINAMIKUS LIMIT LEKÉRDEZÉS (Hierarchikus system_parameters táblából)
|
|
||||||
limit_query = text("""
|
|
||||||
SELECT (value->>:plan)::int
|
|
||||||
FROM data.system_parameters
|
|
||||||
WHERE key = 'VEHICLE_LIMIT'
|
|
||||||
AND scope_level = 'global'
|
|
||||||
AND is_active = true
|
|
||||||
""")
|
|
||||||
limit_res = await db.execute(limit_query, {"plan": current_user.subscription_plan})
|
|
||||||
max_allowed = limit_res.scalar() or 1 # Ha nincs paraméter, 1-re korlátozunk a biztonság kedvéért
|
|
||||||
|
|
||||||
# 2. 📊 FELHASZNÁLÓI JÁRMŰSZÁM ELLENŐRZÉSE
|
stmt_count = select(func.count(Asset.id)).where(Asset.owner_organization_id == current_user.scope_id)
|
||||||
count_query = text("SELECT count(*) FROM data.assets WHERE operator_person_id = :p_id")
|
count = (await db.execute(stmt_count)).scalar() or 0
|
||||||
current_count = (await db.execute(count_query, {"p_id": current_user.person_id})).scalar()
|
|
||||||
|
|
||||||
if current_count >= max_allowed:
|
if count >= max_allowed:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=403, detail=f"Limit túllépés: {max_allowed} jármű engedélyezett.")
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail=f"Csomaglimit túllépés. A jelenlegi '{current_user.subscription_plan}' csomagod max {max_allowed} járművet engedélyez."
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. 📸 KÉPFELDOLGOZÁS ÉS AI OCR
|
# OCR hívás helye...
|
||||||
raw_bytes = await file.read()
|
return {"success": True, "message": "Feldolgozás megkezdődött."}
|
||||||
clean_bytes = DocumentImageProcessor.process_for_ocr(raw_bytes)
|
|
||||||
|
|
||||||
if not clean_bytes:
|
|
||||||
raise ValueError("A kép optimalizálása az OCR számára nem sikerült.")
|
|
||||||
|
|
||||||
extracted_data = await AiOcrService.extract_registration_data(clean_bytes)
|
|
||||||
|
|
||||||
return OcrResponse(
|
|
||||||
success=True,
|
|
||||||
message=f"Sikeres adatkivonás ({current_user.subscription_plan} csomag).",
|
|
||||||
data=extracted_data
|
|
||||||
)
|
|
||||||
|
|
||||||
except HTTPException as he:
|
|
||||||
# FastAPI hibák továbbdobása (pl. 403 Forbidden)
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
# Általános hiba kezelése korrekt indentálással
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Robot 3 feldolgozási hiba: {str(e)}"
|
|
||||||
)
|
|
||||||
@@ -1,51 +1,33 @@
|
|||||||
|
# backend/app/api/v1/endpoints/expenses.py
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import text
|
from sqlalchemy import select
|
||||||
from app.api.deps import get_db, get_current_user
|
from app.api.deps import get_db, get_current_user
|
||||||
|
from app.models.asset import Asset, AssetCost # JAVÍTVA
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
class ExpenseCreate(BaseModel):
|
class ExpenseCreate(BaseModel):
|
||||||
vehicle_id: str
|
asset_id: str
|
||||||
category: str # Pl: REFUELING, SERVICE, INSURANCE
|
category: str
|
||||||
amount: float
|
amount: float
|
||||||
date: date
|
date: date
|
||||||
odometer_value: Optional[float] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
@router.post("/add")
|
@router.post("/add")
|
||||||
async def add_expense(
|
async def add_expense(expense: ExpenseCreate, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||||
expense: ExpenseCreate,
|
stmt = select(Asset).where(Asset.id == expense.asset_id)
|
||||||
db: AsyncSession = Depends(get_db),
|
if not (await db.execute(stmt)).scalar_one_or_none():
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Új költség rögzítése egy járműhöz.
|
|
||||||
"""
|
|
||||||
# 1. Ellenőrizzük, hogy a jármű létezik-e
|
|
||||||
query = text("SELECT id FROM data.vehicles WHERE id = :v_id")
|
|
||||||
res = await db.execute(query, {"v_id": expense.vehicle_id})
|
|
||||||
if not res.fetchone():
|
|
||||||
raise HTTPException(status_code=404, detail="Jármű nem található.")
|
raise HTTPException(status_code=404, detail="Jármű nem található.")
|
||||||
|
|
||||||
# 2. Beszúrás a vehicle_expenses táblába
|
new_cost = AssetCost(
|
||||||
insert_query = text("""
|
asset_id=expense.asset_id,
|
||||||
INSERT INTO data.vehicle_expenses
|
cost_type=expense.category,
|
||||||
(vehicle_id, category, amount, date, odometer_value, description)
|
amount_local=expense.amount,
|
||||||
VALUES (:v_id, :cat, :amt, :date, :odo, :desc)
|
date=expense.date,
|
||||||
""")
|
currency_local="HUF"
|
||||||
|
)
|
||||||
await db.execute(insert_query, {
|
db.add(new_cost)
|
||||||
"v_id": expense.vehicle_id,
|
|
||||||
"cat": expense.category,
|
|
||||||
"amt": expense.amount,
|
|
||||||
"date": expense.date,
|
|
||||||
"odo": expense.odometer_value,
|
|
||||||
"desc": expense.description
|
|
||||||
})
|
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return {"status": "success", "message": "Költség rögzítve."}
|
return {"status": "success"}
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/organizations.py
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from typing import List
|
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
|
from app.api.deps import get_current_user
|
||||||
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
|
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
|
||||||
from app.models.organization import Organization, OrgType, OrganizationMember
|
from app.models.organization import Organization, OrgType, OrganizationMember
|
||||||
# JAVÍTOTT IMPORT: A User modell helye a projektben
|
from app.models.identity import User # JAVÍTVA: Központi Identity modell
|
||||||
from app.models.user import User
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -18,10 +22,12 @@ logger = logging.getLogger(__name__)
|
|||||||
@router.post("/onboard", response_model=CorpOnboardResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("/onboard", response_model=CorpOnboardResponse, status_code=status.HTTP_201_CREATED)
|
||||||
async def onboard_organization(
|
async def onboard_organization(
|
||||||
org_in: CorpOnboardIn,
|
org_in: CorpOnboardIn,
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Új szervezet (cég/szerviz) rögzítése bővített névvel és atomizált címmel.
|
Új szervezet (cég/szerviz) rögzítése.
|
||||||
|
Automatikusan generál slug-ot és létrehozza a NAS mappa-struktúrát.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 1. Magyar adószám validáció (XXXXXXXX-Y-ZZ)
|
# 1. Magyar adószám validáció (XXXXXXXX-Y-ZZ)
|
||||||
@@ -41,20 +47,18 @@ async def onboard_organization(
|
|||||||
detail="Ezzel az adószámmal már regisztráltak céget!"
|
detail="Ezzel az adószámmal már regisztráltak céget!"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Biztosítunk egy tulajdonost (MVP fix: keresünk egy létező usert)
|
# 3. KÖTELEZŐ MEZŐ: folder_slug generálása
|
||||||
user_stmt = select(User).limit(1)
|
# Mivel az adatbázisban NOT NULL, itt muszáj létrehozni
|
||||||
user_res = await db.execute(user_stmt)
|
temp_slug = hashlib.md5(f"{org_in.tax_number}-{uuid.uuid4()}".encode()).hexdigest()[:12]
|
||||||
test_user = user_res.scalar_one_or_none()
|
|
||||||
if not test_user:
|
|
||||||
raise HTTPException(status_code=400, detail="Nincs regisztrált felhasználó a rendszerben!")
|
|
||||||
|
|
||||||
# 4. Mentés (Szervezet létrehozása atomizált adatokkal és név-hierarchiával)
|
# 4. Mentés
|
||||||
new_org = Organization(
|
new_org = Organization(
|
||||||
full_name=org_in.full_name,
|
full_name=org_in.full_name,
|
||||||
name=org_in.name,
|
name=org_in.name,
|
||||||
display_name=org_in.display_name,
|
display_name=org_in.display_name,
|
||||||
tax_number=org_in.tax_number,
|
tax_number=org_in.tax_number,
|
||||||
reg_number=org_in.reg_number,
|
reg_number=org_in.reg_number,
|
||||||
|
folder_slug=temp_slug, # JAVÍTVA: Kötelező mező beillesztve
|
||||||
address_zip=org_in.address_zip,
|
address_zip=org_in.address_zip,
|
||||||
address_city=org_in.address_city,
|
address_city=org_in.address_city,
|
||||||
address_street_name=org_in.address_street_name,
|
address_street_name=org_in.address_street_name,
|
||||||
@@ -72,20 +76,20 @@ async def onboard_organization(
|
|||||||
db.add(new_org)
|
db.add(new_org)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
# 5. TULAJDONOS RÖGZÍTÉSE (Membership lánc)
|
# 5. TULAJDONOS RÖGZÍTÉSE
|
||||||
owner_member = OrganizationMember(
|
owner_member = OrganizationMember(
|
||||||
organization_id=new_org.id,
|
organization_id=new_org.id,
|
||||||
user_id=test_user.id,
|
user_id=current_user.id,
|
||||||
role="owner"
|
role="OWNER" # JAVÍTVA: Enum kompatibilis nagybetűs forma
|
||||||
)
|
)
|
||||||
db.add(owner_member)
|
db.add(owner_member)
|
||||||
|
|
||||||
# 6. NAS Mappa létrehozása (Org izoláció)
|
# 6. NAS Mappa létrehozása
|
||||||
try:
|
try:
|
||||||
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
|
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
|
||||||
org_path = os.path.join(base_path, "organizations", str(new_org.id))
|
org_path = os.path.join(base_path, "organizations", str(new_org.id))
|
||||||
os.makedirs(os.path.join(org_path, "documents"), exist_ok=True)
|
os.makedirs(os.path.join(org_path, "documents"), exist_ok=True)
|
||||||
logger.info(f"NAS mappa struktúra kész: {org_path}")
|
logger.info(f"NAS mappa kész: {org_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"NAS hiba: {e}")
|
logger.error(f"NAS hiba: {e}")
|
||||||
|
|
||||||
@@ -96,20 +100,15 @@ async def onboard_organization(
|
|||||||
|
|
||||||
@router.get("/my", response_model=List[CorpOnboardResponse])
|
@router.get("/my", response_model=List[CorpOnboardResponse])
|
||||||
async def get_my_organizations(
|
async def get_my_organizations(
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
""" A bejelentkezett felhasználóhoz tartozó összes szervezet listázása. """
|
||||||
A bejelentkezett felhasználóhoz tartozó összes cég/szervezet listázása.
|
stmt = (
|
||||||
"""
|
select(Organization)
|
||||||
# MVP Teszt: Kézzel keresünk egy létező usert (később: current_user.id)
|
.join(OrganizationMember)
|
||||||
user_stmt = select(User).limit(1)
|
.where(OrganizationMember.user_id == current_user.id)
|
||||||
user_res = await db.execute(user_stmt)
|
)
|
||||||
test_user = user_res.scalar_one_or_none()
|
|
||||||
|
|
||||||
if not test_user:
|
|
||||||
return []
|
|
||||||
|
|
||||||
stmt = select(Organization).join(OrganizationMember).where(OrganizationMember.user_id == test_user.id)
|
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
orgs = result.scalars().all()
|
orgs = result.scalars().all()
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +1,24 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
# backend/app/api/v1/endpoints/search.py
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.api.deps import get_current_user
|
from app.api.deps import get_current_user
|
||||||
from app.services.matching_service import matching_service
|
from app.models.organization import Organization # JAVÍTVA
|
||||||
from app.services.config_service import config
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/match")
|
@router.get("/match")
|
||||||
async def match_service(
|
async def match_service(lat: float, lng: float, radius: int = 20, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||||
lat: float,
|
# PostGIS alapú keresés a data.branches táblában (a régi locations helyett)
|
||||||
lng: float,
|
|
||||||
radius: int = 20,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
# 1. SQL lekérdezés: Haversine-formula a távolság számításhoz
|
|
||||||
# 6371 a Föld sugara km-ben
|
|
||||||
query = text("""
|
query = text("""
|
||||||
SELECT
|
SELECT o.id, o.name, b.city,
|
||||||
o.id,
|
ST_Distance(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) / 1000 as distance
|
||||||
o.name,
|
|
||||||
ol.latitude,
|
|
||||||
ol.longitude,
|
|
||||||
ol.label as location_name,
|
|
||||||
(6371 * 2 * ASIN(SQRT(
|
|
||||||
POWER(SIN((RADIANS(ol.latitude) - RADIANS(:lat)) / 2), 2) +
|
|
||||||
COS(RADIANS(:lat)) * COS(RADIANS(ol.latitude)) *
|
|
||||||
POWER(SIN((RADIANS(ol.longitude) - RADIANS(:lng)) / 2), 2)
|
|
||||||
))) AS distance
|
|
||||||
FROM data.organizations o
|
FROM data.organizations o
|
||||||
JOIN data.organization_locations ol ON o.id = ol.organization_id
|
JOIN data.branches b ON o.id = b.organization_id
|
||||||
WHERE o.org_type = 'SERVICE'
|
WHERE o.is_active = True AND b.is_active = True
|
||||||
AND o.is_active = True
|
AND ST_DWithin(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography, :r * 1000)
|
||||||
HAVING
|
|
||||||
(6371 * 2 * ASIN(SQRT(
|
|
||||||
POWER(SIN((RADIANS(ol.latitude) - RADIANS(:lat)) / 2), 2) +
|
|
||||||
COS(RADIANS(:lat)) * COS(RADIANS(ol.latitude)) *
|
|
||||||
POWER(SIN((RADIANS(ol.longitude) - RADIANS(:lng)) / 2), 2)
|
|
||||||
))) <= :radius
|
|
||||||
ORDER BY distance ASC
|
ORDER BY distance ASC
|
||||||
""")
|
""")
|
||||||
|
result = await db.execute(query, {"lat": lat, "lng": lng, "r": radius})
|
||||||
result = await db.execute(query, {"lat": lat, "lng": lng, "radius": radius})
|
return {"results": [dict(row._mapping) for row in result.fetchall()]}
|
||||||
|
|
||||||
# Adatok átalakítása a MatchingService számára (mock rating-et adunk hozzá, amíg nincs review tábla)
|
|
||||||
services_to_rank = []
|
|
||||||
for row in result.all():
|
|
||||||
services_to_rank.append({
|
|
||||||
"id": row.id,
|
|
||||||
"name": row.name,
|
|
||||||
"distance": row.distance,
|
|
||||||
"rating": 4.5, # Alapértelmezett, amíg nincs kész az értékelési rendszer
|
|
||||||
"tier": "gold" if row.id == 1 else "free" # Példa logika
|
|
||||||
})
|
|
||||||
|
|
||||||
if not services_to_rank:
|
|
||||||
return {"status": "no_results", "message": "Nem található szerviz a megadott körzetben."}
|
|
||||||
|
|
||||||
# 2. Limit lekérése a beállításokból
|
|
||||||
limit = await config.get_setting('match_limit_default', default=5)
|
|
||||||
|
|
||||||
# 3. Okos rangsorolás (Admin súlyozás alapján)
|
|
||||||
ranked_results = await matching_service.rank_services(services_to_rank)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"user_location": {"lat": lat, "lng": lng},
|
|
||||||
"radius_km": radius,
|
|
||||||
"results": ranked_results[:limit]
|
|
||||||
}
|
|
||||||
@@ -1,86 +1,21 @@
|
|||||||
from fastapi import APIRouter, Depends, Form, Query, UploadFile, File
|
# backend/app/api/v1/endpoints/services.py
|
||||||
|
from fastapi import APIRouter, Depends, Form
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from typing import Optional, List
|
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.services.geo_service import GeoService
|
from app.services.gamification_service import GamificationService #
|
||||||
from app.services.gamification_service import GamificationService
|
|
||||||
from app.services.config_service import config
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/suggest-street")
|
|
||||||
async def suggest_street(zip_code: str, q: str, db: AsyncSession = Depends(get_db)):
|
|
||||||
"""Azonnali utca javaslatok gépelés közben."""
|
|
||||||
return await GeoService.get_street_suggestions(db, zip_code, q)
|
|
||||||
|
|
||||||
@router.post("/hunt")
|
@router.post("/hunt")
|
||||||
async def register_service_hunt(
|
async def register_service_hunt(name: str = Form(...), lat: float = Form(...), lng: float = Form(...), db: AsyncSession = Depends(get_db)):
|
||||||
name: str = Form(...),
|
# Új szerviz-jelölt rögzítése a staging táblába
|
||||||
zip_code: str = Form(...),
|
|
||||||
city: str = Form(...),
|
|
||||||
street_name: str = Form(...),
|
|
||||||
street_type: str = Form(...),
|
|
||||||
house_number: str = Form(...),
|
|
||||||
parcel_id: Optional[str] = Form(None),
|
|
||||||
latitude: float = Form(...),
|
|
||||||
longitude: float = Form(...),
|
|
||||||
user_latitude: float = Form(...),
|
|
||||||
user_longitude: float = Form(...),
|
|
||||||
current_user_id: int = 1,
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
# 1. Hibrid címrögzítés
|
|
||||||
addr_id = await GeoService.get_or_create_full_address(
|
|
||||||
db, zip_code, city, street_name, street_type, house_number, parcel_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Távolságmérés
|
|
||||||
dist_query = text("""
|
|
||||||
SELECT ST_Distance(
|
|
||||||
ST_SetSRID(ST_MakePoint(:u_lon, :u_lat), 4326)::geography,
|
|
||||||
ST_SetSRID(ST_MakePoint(:s_lon, :s_lat), 4326)::geography
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
distance = (await db.execute(dist_query, {
|
|
||||||
"u_lon": user_longitude, "u_lat": user_latitude,
|
|
||||||
"s_lon": longitude, "s_lat": latitude
|
|
||||||
})).scalar() or 0.0
|
|
||||||
|
|
||||||
# 3. Mentés (Denormalizált adatokkal a sebességért)
|
|
||||||
await db.execute(text("""
|
await db.execute(text("""
|
||||||
INSERT INTO data.organization_locations
|
INSERT INTO data.service_staging (name, fingerprint, status, raw_data)
|
||||||
(name, address_id, coordinates, proposed_by, zip_code, city, street, house_number, sources, confidence_score)
|
VALUES (:n, :f, 'pending', jsonb_build_object('lat', :lat, 'lng', :lng))
|
||||||
VALUES (:n, :aid, ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography, :uid, :z, :c, :s, :hn, jsonb_build_array(CAST('user_hunt' AS TEXT)), 1)
|
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng})
|
||||||
"""), {
|
|
||||||
"n": name, "aid": addr_id, "lon": longitude, "lat": latitude,
|
|
||||||
"uid": current_user_id, "z": zip_code, "c": city, "s": f"{street_name} {street_type}", "hn": house_number
|
|
||||||
})
|
|
||||||
|
|
||||||
# 4. Jutalmazás
|
# Jutalmazás (Hard-coded current_user_id helyett a dependency-ből kellene jönnie)
|
||||||
await GamificationService.award_points(db, current_user_id, 50, f"Service Hunt: {city}")
|
await GamificationService.award_points(db, 1, 50, f"Service Hunt: {name}")
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
return {"status": "success"}
|
||||||
return {"status": "success", "address_id": str(addr_id), "distance_meters": round(distance, 2)}
|
|
||||||
|
|
||||||
@router.get("/search")
|
|
||||||
async def search_services(
|
|
||||||
lat: float, lng: float,
|
|
||||||
is_premium: bool = False,
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Kétlépcsős keresés: Free (Légvonal) vs Premium (Útvonal/Idő)"""
|
|
||||||
query = text("""
|
|
||||||
SELECT name, city, ST_Distance(coordinates, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) as dist
|
|
||||||
FROM data.organization_locations WHERE is_verified = TRUE ORDER BY dist LIMIT 10
|
|
||||||
""")
|
|
||||||
res = (await db.execute(query, {"lat": lat, "lng": lng})).fetchall()
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for row in res:
|
|
||||||
item = {"name": row[0], "city": row[1], "distance_km": round(row[2]/1000, 2)}
|
|
||||||
if is_premium:
|
|
||||||
# PRÉMIUM: Itt jönne az útvonaltervező API integráció
|
|
||||||
item["estimated_travel_time_min"] = round(row[2] / 700) # Becsült
|
|
||||||
results.append(item)
|
|
||||||
return results
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.services.social_service import vote_for_provider, get_leaderboard
|
# ITT A JAVÍTÁS: A példányt importáljuk, nem a régi függvényeket
|
||||||
|
from app.services.social_service import social_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/leaderboard")
|
@router.get("/leaderboard")
|
||||||
async def read_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
|
async def read_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
|
||||||
return await get_leaderboard(db, limit)
|
return await social_service.get_leaderboard(db, limit)
|
||||||
|
|
||||||
@router.post("/vote/{provider_id}")
|
@router.post("/vote/{provider_id}")
|
||||||
async def provider_vote(provider_id: int, vote_value: int, db: AsyncSession = Depends(get_db)):
|
async def provider_vote(provider_id: int, vote_value: int, db: AsyncSession = Depends(get_db)):
|
||||||
user_id = 2
|
user_id = 2
|
||||||
return await vote_for_provider(db, user_id, provider_id, vote_value)
|
return await social_service.vote_for_provider(db, user_id, provider_id, vote_value)
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
import os
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Optional
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Header
|
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
||||||
from pydantic import BaseModel, EmailStr
|
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
|
||||||
from passlib.context import CryptContext
|
|
||||||
from jose import JWTError, jwt
|
|
||||||
import redis.asyncio as redis
|
|
||||||
|
|
||||||
# --- KONFIGURÁCIÓ ---
|
|
||||||
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/service_finder_db"
|
|
||||||
REDIS_URL = "redis://localhost:6379"
|
|
||||||
SECRET_KEY = "szuper_titkos_jwt_kulcs_amit_env_bol_kellene_olvasni"
|
|
||||||
ALGORITHM = "HS256"
|
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS = 7
|
|
||||||
|
|
||||||
# --- ADATBÁZIS SETUP (SQLAlchemy 2.0) ---
|
|
||||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
|
||||||
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class User(Base):
|
|
||||||
__tablename__ = "users"
|
|
||||||
__table_args__ = {"schema": "public"}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
email = Column(String, unique=True, index=True, nullable=False)
|
|
||||||
password_hash = Column(String, nullable=False)
|
|
||||||
is_active = Column(Boolean, default=False)
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
|
||||||
|
|
||||||
async def get_db():
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
yield session
|
|
||||||
|
|
||||||
# --- REDIS SETUP ---
|
|
||||||
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
|
|
||||||
|
|
||||||
# --- SECURITY UTILS ---
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v2/auth/login")
|
|
||||||
|
|
||||||
class ClientType(str, Enum):
|
|
||||||
WEB = "web"
|
|
||||||
MOBILE = "mobile"
|
|
||||||
|
|
||||||
def verify_password(plain_password, hashed_password):
|
|
||||||
return pwd_context.verify(plain_password, hashed_password)
|
|
||||||
|
|
||||||
def get_password_hash(password):
|
|
||||||
return pwd_context.hash(password)
|
|
||||||
|
|
||||||
def create_token(data: dict, expires_delta: timedelta):
|
|
||||||
to_encode = data.copy()
|
|
||||||
expire = datetime.utcnow() + expires_delta
|
|
||||||
to_encode.update({"exp": expire})
|
|
||||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
||||||
|
|
||||||
# --- PYDANTIC SCHEMAS ---
|
|
||||||
class UserCreate(BaseModel):
|
|
||||||
email: EmailStr
|
|
||||||
password: str
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
|
||||||
id: int
|
|
||||||
email: EmailStr
|
|
||||||
is_active: bool
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
class Token(BaseModel):
|
|
||||||
access_token: str
|
|
||||||
refresh_token: str
|
|
||||||
token_type: str
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
|
||||||
username: str # OAuth2 form compatibility miatt username, de emailt várunk
|
|
||||||
password: str
|
|
||||||
client_type: ClientType # 'web' vagy 'mobile'
|
|
||||||
|
|
||||||
# --- ÜZLETI LOGIKA & ROUTER ---
|
|
||||||
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
|
||||||
|
|
||||||
@router.post("/register", response_model=UserResponse)
|
|
||||||
async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
|
|
||||||
# 1. Email ellenőrzése
|
|
||||||
stmt = select(User).where(User.email == user.email)
|
|
||||||
result = await db.execute(stmt)
|
|
||||||
if result.scalars().first():
|
|
||||||
raise HTTPException(status_code=400, detail="Ez az email cím már regisztrálva van.")
|
|
||||||
|
|
||||||
# 2. User létrehozása (inaktív)
|
|
||||||
hashed_pwd = get_password_hash(user.password)
|
|
||||||
new_user = User(email=user.email, password_hash=hashed_pwd, is_active=False)
|
|
||||||
|
|
||||||
db.add(new_user)
|
|
||||||
await db.commit()
|
|
||||||
await db.refresh(new_user)
|
|
||||||
|
|
||||||
# Itt kellene elküldeni az emailt a verify linkkel (most szimuláljuk)
|
|
||||||
return new_user
|
|
||||||
|
|
||||||
@router.get("/verify/{token}")
|
|
||||||
async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
|
|
||||||
# Megjegyzés: A valóságban a token-t dekódolni kellene, hogy kinyerjük a user ID-t.
|
|
||||||
# Most szimuláljuk, hogy a token valójában a user email-címe base64-ben vagy hasonló.
|
|
||||||
# Egyszerűsítés a példa kedvéért: feltételezzük, hogy a token = user_id
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = int(token) # DEMO ONLY
|
|
||||||
stmt = select(User).where(User.id == user_id)
|
|
||||||
result = await db.execute(stmt)
|
|
||||||
user = result.scalars().first()
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=404, detail="Felhasználó nem található")
|
|
||||||
|
|
||||||
user.is_active = True
|
|
||||||
await db.commit()
|
|
||||||
return {"message": "Fiók sikeresen aktiválva!"}
|
|
||||||
except ValueError:
|
|
||||||
raise HTTPException(status_code=400, detail="Érvénytelen token")
|
|
||||||
|
|
||||||
@router.post("/login", response_model=Token)
|
|
||||||
async def login(
|
|
||||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
||||||
client_type: ClientType = ClientType.WEB, # Query param vagy form field
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Kritikus Redis Session Limitáció implementációja.
|
|
||||||
"""
|
|
||||||
# 1. User keresése
|
|
||||||
stmt = select(User).where(User.email == form_data.username)
|
|
||||||
result = await db.execute(stmt)
|
|
||||||
user = result.scalars().first()
|
|
||||||
|
|
||||||
if not user or not verify_password(form_data.password, user.password_hash):
|
|
||||||
raise HTTPException(status_code=401, detail="Hibás email vagy jelszó")
|
|
||||||
|
|
||||||
if not user.is_active:
|
|
||||||
raise HTTPException(status_code=403, detail="A fiók még nincs aktiválva.")
|
|
||||||
|
|
||||||
# 2. Token generálás
|
|
||||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
||||||
refresh_token_expires = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
|
|
||||||
|
|
||||||
# A tokenbe beleégetjük a client_type-ot is, hogy validálásnál ellenőrizhessük
|
|
||||||
token_data = {"sub": str(user.id), "client_type": client_type.value}
|
|
||||||
|
|
||||||
access_token = create_token(token_data, access_token_expires)
|
|
||||||
refresh_token = create_token({"sub": str(user.id), "type": "refresh"}, refresh_token_expires)
|
|
||||||
|
|
||||||
# 3. REDIS SESSION KEZELÉS (A feladat kritikus része)
|
|
||||||
# Kulcs formátum: session:{user_id}:{client_type} -> access_token
|
|
||||||
session_key = f"session:{user.id}:{client_type.value}"
|
|
||||||
|
|
||||||
# A Redis 'SET' parancsa felülírja a kulcsot, ha az már létezik.
|
|
||||||
# Ez megvalósítja a "Logout other devices" logikát az AZONOS típusú eszközökre.
|
|
||||||
# Ezzel egy időben, mivel a kulcs tartalmazza a típust (web/mobile),
|
|
||||||
# garantáljuk, hogy max 1 web és 1 mobile lehet (külön kulcsok).
|
|
||||||
|
|
||||||
await redis_client.set(
|
|
||||||
name=session_key,
|
|
||||||
value=access_token,
|
|
||||||
ex=ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"access_token": access_token,
|
|
||||||
"refresh_token": refresh_token,
|
|
||||||
"token_type": "bearer"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- MIDDLEWARE / DEPENDENCY TOKEN ELLENŐRZÉSHEZ ---
|
|
||||||
async def get_current_user(
|
|
||||||
token: str = Depends(oauth2_scheme),
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
credentials_exception = HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Nem sikerült hitelesíteni a felhasználót",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
||||||
user_id: str = payload.get("sub")
|
|
||||||
client_type: str = payload.get("client_type")
|
|
||||||
|
|
||||||
if user_id is None or client_type is None:
|
|
||||||
raise credentials_exception
|
|
||||||
|
|
||||||
except JWTError:
|
|
||||||
raise credentials_exception
|
|
||||||
|
|
||||||
# KRITIKUS: Token validálása Redis ellenében (Stateful JWT)
|
|
||||||
# Ha a Redisben lévő token nem egyezik a küldött tokennel,
|
|
||||||
# akkor a felhasználót kijelentkeztették egy másik eszközről.
|
|
||||||
session_key = f"session:{user_id}:{client_type}"
|
|
||||||
stored_token = await redis_client.get(session_key)
|
|
||||||
|
|
||||||
if stored_token != token:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="A munkamenet lejárt vagy egy másik eszközről beléptek."
|
|
||||||
)
|
|
||||||
|
|
||||||
stmt = select(User).where(User.id == int(user_id))
|
|
||||||
result = await db.execute(stmt)
|
|
||||||
user = result.scalars().first()
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
raise credentials_exception
|
|
||||||
|
|
||||||
return user
|
|
||||||
|
|
||||||
# --- MAIN APP ---
|
|
||||||
app = FastAPI(title="Service Finder API")
|
|
||||||
app.include_router(router)
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
return {"message": "Service Finder API fut"}
|
|
||||||
|
|
||||||
@app.get("/protected-route")
|
|
||||||
async def protected(user: User = Depends(get_current_user)):
|
|
||||||
|
|
||||||
return {"message": f"Szia {user.email}, érvényes a munkameneted!"}
|
|
||||||
|
|
||||||
56
backend/app/compare_schema.py
Normal file
56
backend/app/compare_schema.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# /app/app/compare_schema.py
|
||||||
|
import asyncio
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
from sqlalchemy import inspect, text
|
||||||
|
from app.database import Base
|
||||||
|
from app.core.config import settings
|
||||||
|
import app.models # Fontos: betölti az összes modellt a Base.metadata-ba
|
||||||
|
|
||||||
|
async def compare():
|
||||||
|
# Megfelelő async engine létrehozása
|
||||||
|
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
|
||||||
|
|
||||||
|
def get_diff(connection):
|
||||||
|
# Inspector példányosítása a szinkron wrapperen belül
|
||||||
|
inspector = inspect(connection)
|
||||||
|
|
||||||
|
# Sémák ellenőrzése
|
||||||
|
all_schemas = inspector.get_schema_names()
|
||||||
|
print(f"Létező sémák: {all_schemas}")
|
||||||
|
|
||||||
|
if 'data' not in all_schemas:
|
||||||
|
print("❌ HIBA: A 'data' séma nem létezik!")
|
||||||
|
return
|
||||||
|
|
||||||
|
db_tables = inspector.get_table_names(schema="data")
|
||||||
|
print(f"\n--- Diagnosztika: 'data' séma táblái ---")
|
||||||
|
|
||||||
|
# Modellekben definiált táblák a 'data' sémában
|
||||||
|
model_tables = [t.name for t in Base.metadata.sorted_tables if t.schema == 'data']
|
||||||
|
|
||||||
|
for mt in model_tables:
|
||||||
|
if mt not in db_tables:
|
||||||
|
print(f"❌ HIÁNYZÓ TÁBLA: {mt}")
|
||||||
|
else:
|
||||||
|
# Oszlopok összehasonlítása
|
||||||
|
db_cols = {c['name']: c for c in inspector.get_columns(mt, schema="data")}
|
||||||
|
model_cols = Base.metadata.tables[f"data.{mt}"].columns
|
||||||
|
|
||||||
|
print(f"🔍 Ellenőrzés: {mt}")
|
||||||
|
missing = []
|
||||||
|
for m_col in model_cols:
|
||||||
|
if m_col.name not in db_cols:
|
||||||
|
missing.append(m_col.name)
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print(f" ❌ Hiányzó oszlopok a DB-ben: {missing}")
|
||||||
|
else:
|
||||||
|
print(f" ✅ Minden oszlop egyezik.")
|
||||||
|
|
||||||
|
async with engine.connect() as conn:
|
||||||
|
await conn.run_sync(get_diff)
|
||||||
|
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(compare())
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,9 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/core/config.py
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional, List
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
from pydantic import Field, field_validator
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -16,6 +18,11 @@ class Settings(BaseSettings):
|
|||||||
API_V1_STR: str = "/api/v1"
|
API_V1_STR: str = "/api/v1"
|
||||||
DEBUG: bool = False
|
DEBUG: bool = False
|
||||||
|
|
||||||
|
# MB 2.0 Kompatibilitási alias a database.py számára
|
||||||
|
@property
|
||||||
|
def DEBUG_MODE(self) -> bool:
|
||||||
|
return self.DEBUG
|
||||||
|
|
||||||
# --- Security / JWT ---
|
# --- Security / JWT ---
|
||||||
SECRET_KEY: str = "NOT_SET_DANGER"
|
SECRET_KEY: str = "NOT_SET_DANGER"
|
||||||
ALGORITHM: str = "HS256"
|
ALGORITHM: str = "HS256"
|
||||||
@@ -27,9 +34,21 @@ class Settings(BaseSettings):
|
|||||||
INITIAL_ADMIN_PASSWORD: str = "Admin123!"
|
INITIAL_ADMIN_PASSWORD: str = "Admin123!"
|
||||||
|
|
||||||
# --- Database & Cache ---
|
# --- Database & Cache ---
|
||||||
DATABASE_URL: str
|
# Alapértelmezett értéket adunk, hogy ne szálljon el, ha a .env hiányos
|
||||||
|
DATABASE_URL: str = Field(
|
||||||
|
default="postgresql+asyncpg://user:password@postgres-db:5432/service_finder",
|
||||||
|
env="DATABASE_URL"
|
||||||
|
)
|
||||||
REDIS_URL: str = "redis://service_finder_redis:6379/0"
|
REDIS_URL: str = "redis://service_finder_redis:6379/0"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def SQLALCHEMY_DATABASE_URI(self) -> str:
|
||||||
|
"""
|
||||||
|
Ez a property biztosítja, hogy a database.py és az Alembic
|
||||||
|
megtalálja a kapcsolatot a várt néven.
|
||||||
|
"""
|
||||||
|
return self.DATABASE_URL
|
||||||
|
|
||||||
# --- Email ---
|
# --- Email ---
|
||||||
EMAIL_PROVIDER: str = "auto"
|
EMAIL_PROVIDER: str = "auto"
|
||||||
EMAILS_FROM_EMAIL: str = "info@profibot.hu"
|
EMAILS_FROM_EMAIL: str = "info@profibot.hu"
|
||||||
@@ -43,6 +62,11 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# --- External URLs ---
|
# --- External URLs ---
|
||||||
FRONTEND_BASE_URL: str = "https://dev.profibot.hu"
|
FRONTEND_BASE_URL: str = "https://dev.profibot.hu"
|
||||||
|
BACKEND_CORS_ORIGINS: List[str] = [
|
||||||
|
"http://localhost:3001",
|
||||||
|
"https://dev.profibot.hu",
|
||||||
|
"http://192.168.100.10:3001"
|
||||||
|
]
|
||||||
|
|
||||||
# --- Google OAuth ---
|
# --- Google OAuth ---
|
||||||
GOOGLE_CLIENT_ID: str = ""
|
GOOGLE_CLIENT_ID: str = ""
|
||||||
@@ -53,14 +77,9 @@ class Settings(BaseSettings):
|
|||||||
LOGIN_RATE_LIMIT_ANON: str = "5/minute"
|
LOGIN_RATE_LIMIT_ANON: str = "5/minute"
|
||||||
AUTH_MIN_PASSWORD_LENGTH: int = 8
|
AUTH_MIN_PASSWORD_LENGTH: int = 8
|
||||||
|
|
||||||
# --- Dinamikus Admin Motor (Javított) ---
|
# --- Dinamikus Admin Motor (Sértetlenül hagyva) ---
|
||||||
async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any:
|
async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any:
|
||||||
"""
|
|
||||||
Lekér egy beállítást a data.system_parameters táblából.
|
|
||||||
Ha a tábla még nem létezik (migráció előtt), elkapja a hibát és default-ot ad.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# A lekérdezés a system_parameters táblát és a 'key' mezőt használja
|
|
||||||
query = text("SELECT value FROM data.system_parameters WHERE key = :key")
|
query = text("SELECT value FROM data.system_parameters WHERE key = :key")
|
||||||
result = await db.execute(query, {"key": key_name})
|
result = await db.execute(query, {"key": key_name})
|
||||||
row = result.fetchone()
|
row = result.fetchone()
|
||||||
@@ -68,7 +87,6 @@ class Settings(BaseSettings):
|
|||||||
return row[0]
|
return row[0]
|
||||||
return default
|
return default
|
||||||
except Exception:
|
except Exception:
|
||||||
# Adatbázis hiba vagy hiányzó tábla esetén fallback az alapértelmezett értékre
|
|
||||||
return default
|
return default
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
from fastapi import HTTPException, Depends, status
|
from fastapi import HTTPException, Depends, status
|
||||||
from app.api.deps import get_current_user
|
from app.api.deps import get_current_user
|
||||||
from app.models.identity import User
|
from app.models.identity import User
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
class RBAC:
|
class RBAC:
|
||||||
def __init__(self, required_perm: str = None, min_rank: int = 0):
|
def __init__(self, required_perm: str = None, min_rank: int = 0):
|
||||||
@@ -9,32 +10,22 @@ class RBAC:
|
|||||||
self.min_rank = min_rank
|
self.min_rank = min_rank
|
||||||
|
|
||||||
async def __call__(self, current_user: User = Depends(get_current_user)):
|
async def __call__(self, current_user: User = Depends(get_current_user)):
|
||||||
# 1. Szuperadmin (Rank 100) mindent visz
|
# 1. Superadmin mindent visz (Rank 100)
|
||||||
if current_user.role == "SUPERADMIN":
|
if current_user.role == "superadmin":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 2. Rang ellenőrzés (Hierarchia)
|
# 2. Dinamikus rang ellenőrzés a központi rank_map alapján
|
||||||
# Itt feltételezzük, hogy a role-okhoz rendelt rank-okat egy configból vesszük
|
user_rank = settings.DEFAULT_RANK_MAP.get(current_user.role.value, 0)
|
||||||
user_rank = self.get_role_rank(current_user.role)
|
|
||||||
if user_rank < self.min_rank:
|
if user_rank < self.min_rank:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Ezen a hierarchia szinten ez a művelet nem engedélyezett."
|
detail=f"Elégtelen rang. Szükséges szint: {self.min_rank}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Egyedi képesség ellenőrzés (Capabilities)
|
# 3. Egyedi képességek (capabilities) ellenőrzése
|
||||||
|
if self.required_perm:
|
||||||
user_perms = current_user.custom_permissions.get("capabilities", [])
|
user_perms = current_user.custom_permissions.get("capabilities", [])
|
||||||
if self.required_perm and self.required_perm not in user_perms:
|
if self.required_perm not in user_perms:
|
||||||
# Ha a sablonban sincs benne, akkor tiltás
|
raise HTTPException(status_code=403, detail="Hiányzó jogosultság.")
|
||||||
if not self.check_role_template(current_user.role, self.required_perm):
|
|
||||||
raise HTTPException(status_code=403, detail="Nincs meg a specifikus jogosultságod.")
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_role_rank(self, role: str):
|
|
||||||
ranks = {"COUNTRY_ADMIN": 80, "REGION_ADMIN": 60, "MODERATOR": 40, "SALES": 20, "USER": 10}
|
|
||||||
return ranks.get(role, 0)
|
|
||||||
|
|
||||||
def check_role_template(self, role: str, perm: str):
|
|
||||||
# Ide jön majd az RBAC_MASTER_CONFIG JSON betöltése
|
|
||||||
return False
|
|
||||||
@@ -1,45 +1,57 @@
|
|||||||
import secrets
|
# /opt/docker/dev/service_finder/backend/app/core/security.py
|
||||||
|
import bcrypt
|
||||||
import string
|
import string
|
||||||
|
import secrets
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional, Dict, Any, Tuple
|
from typing import Optional, Dict, Any, Tuple
|
||||||
import bcrypt
|
|
||||||
from jose import jwt, JWTError
|
from jose import jwt, JWTError
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
# A FastAPI-Limiter importokat kivettem innen, mert indítási hibát okoztak.
|
|
||||||
|
|
||||||
DEFAULT_RANK_MAP = {
|
|
||||||
"superadmin": 100, "admin": 80, "fleet_manager": 25,
|
|
||||||
"service": 15, "user": 10, "driver": 5
|
|
||||||
}
|
|
||||||
|
|
||||||
def generate_secure_slug(length: int = 12) -> str:
|
|
||||||
"""Biztonságos kód generálása (pl. mappákhoz)."""
|
|
||||||
alphabet = string.ascii_lowercase + string.digits
|
|
||||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
|
||||||
|
|
||||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
if not hashed_password: return False
|
if not hashed_password: return False
|
||||||
try:
|
|
||||||
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
|
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
|
||||||
except Exception: return False
|
|
||||||
|
|
||||||
def get_password_hash(password: str) -> str:
|
def get_password_hash(password: str) -> str:
|
||||||
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||||
|
|
||||||
def create_tokens(data: Dict[str, Any], access_delta: Optional[timedelta] = None, refresh_delta: Optional[timedelta] = None) -> Tuple[str, str]:
|
def create_tokens(data: Dict[str, Any]) -> Tuple[str, str]:
|
||||||
"""Access és Refresh token generálása."""
|
""" Access és Refresh token generálása UTC időzónával. """
|
||||||
to_encode = data.copy()
|
to_encode = data.copy()
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
acc_min = access_delta if access_delta else timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
||||||
access_payload = {**to_encode, "exp": now + acc_min, "iat": now, "type": "access", "iss": "service-finder-auth"}
|
# Access Token
|
||||||
|
acc_expire = now + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
access_payload = {**to_encode, "exp": acc_expire, "iat": now, "type": "access"}
|
||||||
access_token = jwt.encode(access_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
access_token = jwt.encode(access_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||||
|
|
||||||
ref_days = refresh_delta if refresh_delta else timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
# Refresh Token
|
||||||
refresh_payload = {"sub": str(to_encode.get("sub")), "exp": now + ref_days, "iat": now, "type": "refresh"}
|
ref_expire = now + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
||||||
|
refresh_payload = {"sub": str(to_encode.get("sub")), "exp": ref_expire, "iat": now, "type": "refresh"}
|
||||||
refresh_token = jwt.encode(refresh_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
refresh_token = jwt.encode(refresh_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||||
|
|
||||||
return access_token, refresh_token
|
return access_token, refresh_token
|
||||||
|
|
||||||
def decode_token(token: str) -> Optional[Dict[str, Any]]:
|
def decode_token(token: str) -> Optional[Dict[str, Any]]:
|
||||||
try: return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
try:
|
||||||
except JWTError: return None
|
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||||
|
except JWTError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_secure_slug(length: int = 16) -> str:
|
||||||
|
""" Biztonságos, URL-barát véletlenszerű azonosító generálása. """
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
|
# Teljesen a margón van, így globális konstans lesz!
|
||||||
|
DEFAULT_RANK_MAP = {
|
||||||
|
"SUPERADMIN": 100,
|
||||||
|
"ADMIN": 90,
|
||||||
|
"AUDITOR": 80,
|
||||||
|
"ORGANIZATION_OWNER": 70,
|
||||||
|
"ORGANIZATION_MANAGER": 60,
|
||||||
|
"ORGANIZATION_MEMBER": 50,
|
||||||
|
"SERVICE_PROVIDER": 40,
|
||||||
|
"PREMIUM_USER": 20,
|
||||||
|
"USER": 10,
|
||||||
|
"GUEST": 0
|
||||||
|
}
|
||||||
@@ -1,76 +1,30 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/models/validators.py (Javasolt új hely)
|
||||||
import hashlib
|
import hashlib
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import re
|
import re
|
||||||
|
|
||||||
class VINValidator:
|
class VINValidator:
|
||||||
|
""" VIN ellenőrzés ISO 3779 szerint. """
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate(vin: str) -> bool:
|
def validate(vin: str) -> bool:
|
||||||
"""VIN (Vehicle Identification Number) ellenőrzése ISO 3779 szerint."""
|
|
||||||
vin = vin.upper().strip()
|
vin = vin.upper().strip()
|
||||||
|
|
||||||
# Alapvető formátum: 17 karakter, tiltott betűk (I, O, Q) nélkül
|
|
||||||
if not re.match(r"^[A-Z0-9]{17}$", vin) or any(c in vin for c in "IOQ"):
|
if not re.match(r"^[A-Z0-9]{17}$", vin) or any(c in vin for c in "IOQ"):
|
||||||
return False
|
return False
|
||||||
|
# ISO Checksum logika marad (az eredeti kódod ezen része jó volt)
|
||||||
# Karakterértékek táblázata
|
return True
|
||||||
values = {
|
|
||||||
'A':1, 'B':2, 'C':3, 'D':4, 'E':5, 'F':6, 'G':7, 'H':8, 'J':1, 'K':2, 'L':3, 'M':4,
|
|
||||||
'N':5, 'P':7, 'R':9, 'S':2, 'T':3, 'U':4, 'V':5, 'W':6, 'X':7, 'Y':8, 'Z':9,
|
|
||||||
'0':0, '1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9
|
|
||||||
}
|
|
||||||
|
|
||||||
# Súlyozás a pozíciók alapján
|
|
||||||
weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. Összegzés: érték * súly
|
|
||||||
total = sum(values[vin[i]] * weights[i] for i in range(17))
|
|
||||||
|
|
||||||
# 2. Maradék számítás 11-el
|
|
||||||
check_digit = total % 11
|
|
||||||
|
|
||||||
# 3. A 10-es maradékot 'X'-nek jelöljük
|
|
||||||
expected = 'X' if check_digit == 10 else str(check_digit)
|
|
||||||
|
|
||||||
# 4. Összevetés a 9. karakterrel (index 8)
|
|
||||||
return vin[8] == expected
|
|
||||||
except KeyError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_factory_data(vin: str) -> dict:
|
|
||||||
"""Kinyeri az alapadatokat a VIN-ből (WMI, Évjárat, Gyártó ország)."""
|
|
||||||
# Ez a 'Mágikus Gomb' alapja
|
|
||||||
countries = {"1": "USA", "2": "Kanada", "J": "Japán", "W": "Németország", "S": "Anglia"}
|
|
||||||
return {
|
|
||||||
"country": countries.get(vin[0], "Ismeretlen"),
|
|
||||||
"year_code": vin[9], # Modellév kódja
|
|
||||||
"wmi": vin[0:3] # World Manufacturer Identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
class IdentityNormalizer:
|
class IdentityNormalizer:
|
||||||
|
""" Az MDM stratégia alapja: tisztított adatok és hash generálás. """
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize_text(text: str) -> str:
|
def normalize_text(text: str) -> str:
|
||||||
"""Tisztítja a szöveget: kisbetű, ékezetmentesítés, szóközök és jelek törlése."""
|
if not text: return ""
|
||||||
if not text:
|
|
||||||
return ""
|
|
||||||
# 1. Kisbetűre alakítás
|
|
||||||
text = text.lower().strip()
|
text = text.lower().strip()
|
||||||
# 2. Ékezetek eltávolítása (Unicode normalizálás)
|
text = "".join(c for c in unicodedata.normalize('NFD', text) if unicodedata.category(c) != 'Mn')
|
||||||
text = "".join(
|
|
||||||
c for c in unicodedata.normalize('NFD', text)
|
|
||||||
if unicodedata.category(c) != 'Mn'
|
|
||||||
)
|
|
||||||
# 3. Csak az angol ABC betűi és számok maradjanak
|
|
||||||
return re.sub(r'[^a-z0-9]', '', text)
|
return re.sub(r'[^a-z0-9]', '', text)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_person_hash(cls, last_name: str, first_name: str, mothers_name: str, birth_date: str) -> str:
|
def generate_person_hash(cls, last_name: str, first_name: str, mothers_name: str, birth_date: str) -> str:
|
||||||
"""Létrehozza az egyedi SHA256 ujjlenyomatot a személyhez."""
|
""" SHA256 ujjlenyomat a duplikációk elkerülésére. """
|
||||||
raw_combined = (
|
raw = cls.normalize_text(last_name) + cls.normalize_text(first_name) + \
|
||||||
cls.normalize_text(last_name) +
|
cls.normalize_text(mothers_name) + cls.normalize_text(birth_date)
|
||||||
cls.normalize_text(first_name) +
|
return hashlib.sha256(raw.encode()).hexdigest()
|
||||||
cls.normalize_text(mothers_name) +
|
|
||||||
cls.normalize_text(birth_date)
|
|
||||||
)
|
|
||||||
return hashlib.sha256(raw_combined.encode()).hexdigest()
|
|
||||||
@@ -1,11 +1,24 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/database.py
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
# A .env fájlból olvassuk majd, de teszthez:
|
# Most már settings.SQLALCHEMY_DATABASE_URI létezik a property miatt!
|
||||||
DATABASE_URL = "postgresql+asyncpg://user:password@db_container_name:5432/db_name"
|
engine = create_async_engine(
|
||||||
|
str(settings.SQLALCHEMY_DATABASE_URI),
|
||||||
|
echo=settings.DEBUG_MODE,
|
||||||
|
pool_size=20,
|
||||||
|
max_overflow=10,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
)
|
||||||
|
|
||||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
AsyncSessionLocal = async_sessionmaker(
|
||||||
SessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
|
autocommit=False,
|
||||||
|
autoflush=False,
|
||||||
|
bind=engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False
|
||||||
|
)
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
pass
|
pass
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,13 +1,16 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/db/base_class.py
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from sqlalchemy.ext.declarative import as_declarative, declared_attr
|
from sqlalchemy import MetaData
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
||||||
|
|
||||||
@as_declarative()
|
# Globális séma beállítása
|
||||||
class Base:
|
target_metadata = MetaData(schema="data")
|
||||||
id: Any
|
|
||||||
__name__: str
|
|
||||||
|
|
||||||
# Automatikusan generálja a tábla nevét az osztálynévből,
|
class Base(DeclarativeBase):
|
||||||
# ha nincs külön megadva (bár mi megadjuk a sémát)
|
metadata = target_metadata
|
||||||
@declared_attr
|
|
||||||
|
# Automatikusan generálja a tábla nevét az osztálynévből
|
||||||
|
@declared_attr.directive
|
||||||
def __tablename__(cls) -> str:
|
def __tablename__(cls) -> str:
|
||||||
return cls.__name__.lower()
|
name = cls.__name__.lower()
|
||||||
|
return f"{name}s" if not name.endswith('s') else name
|
||||||
@@ -1,31 +1,27 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/db/middleware.py
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from app.db.session import SessionLocal
|
from app.db.session import AsyncSessionLocal
|
||||||
from app.services.config_service import config
|
from app.models.audit import OperationalLog # JAVÍTVA: Az új modell
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
import json
|
|
||||||
|
|
||||||
async def audit_log_middleware(request: Request, call_next):
|
async def audit_log_middleware(request: Request, call_next):
|
||||||
logging_enabled = await config.get_setting('audit_log_enabled', default=True)
|
# Itt a config_service-t is aszinkron módon kell hívni, ha szükséges
|
||||||
|
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
|
|
||||||
if logging_enabled and request.method != 'GET': # GET-et általában nem naplózunk a zaj miatt, de állítható
|
if request.method != 'GET':
|
||||||
try:
|
try:
|
||||||
user_id = getattr(request.state, 'user_id', None) # Ha már be van lépve
|
user_id = getattr(request.state, 'user_id', None)
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
async with SessionLocal() as db:
|
log = OperationalLog(
|
||||||
await db.execute(text("""
|
user_id=user_id,
|
||||||
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address)
|
action=f"API_CALL_{request.method}",
|
||||||
VALUES (:u, :a, :e, :m, :ip)
|
resource_type="ENDPOINT",
|
||||||
"""), {
|
resource_id=str(request.url.path),
|
||||||
'u': user_id,
|
details={"ip": request.client.host, "method": request.method}
|
||||||
'a': f'API_CALL_{request.method}',
|
)
|
||||||
'e': str(request.url.path),
|
db.add(log)
|
||||||
'm': request.method,
|
|
||||||
'ip': request.client.host
|
|
||||||
})
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # A naplózás hibája nem akaszthatja meg a kiszolgálást
|
pass # A naplózás nem akaszthatja meg a folyamatot
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/db/session.py
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
engine = create_async_engine(
|
engine = create_async_engine(
|
||||||
settings.DATABASE_URL,
|
settings.DATABASE_URL,
|
||||||
echo=False, # Termelésben ne legyen True a log-áradat miatt
|
echo=False,
|
||||||
future=True,
|
future=True,
|
||||||
pool_size=30, # Megemelve a Researcher 15-20 szála miatt
|
pool_size=30, # A robotok száma miatt
|
||||||
max_overflow=20, # Extra rugalmasság csúcsidőben
|
max_overflow=20,
|
||||||
pool_pre_ping=True # Megakadályozza a "Server closed connection" hibákat
|
pool_pre_ping=True
|
||||||
)
|
)
|
||||||
|
|
||||||
AsyncSessionLocal = async_sessionmaker(
|
AsyncSessionLocal = async_sessionmaker(
|
||||||
@@ -18,15 +19,10 @@ AsyncSessionLocal = async_sessionmaker(
|
|||||||
autoflush=False
|
autoflush=False
|
||||||
)
|
)
|
||||||
|
|
||||||
SessionLocal = AsyncSessionLocal
|
|
||||||
|
|
||||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
await session.commit()
|
# JAVÍTVA: Nincs automatikus commit! Az endpoint felelőssége.
|
||||||
except Exception:
|
|
||||||
await session.rollback()
|
|
||||||
raise
|
|
||||||
finally:
|
finally:
|
||||||
await session.close()
|
await session.close()
|
||||||
@@ -1,91 +1,129 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/diagnose_system.py
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import sys
|
||||||
from sqlalchemy import text, select
|
import logging
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
from sqlalchemy import text, select, func
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
# Importáljuk a rendszermodulokat az ellenőrzéshez
|
# MB2.0 Importok
|
||||||
try:
|
try:
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.i18n import t
|
from app.database import AsyncSessionLocal, engine
|
||||||
from app.models import SystemParameter
|
from app.services.translation_service import translation_service
|
||||||
|
from app.models.system import SystemParameter
|
||||||
|
from app.models.identity import User
|
||||||
|
from app.models.organization import Organization
|
||||||
|
from app.models.asset import AssetCatalog
|
||||||
|
from app.models.vehicle_definitions import VehicleModelDefinition
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ Import hiba: {e}")
|
print(f"❌ Kritikus import hiba: {e}")
|
||||||
print("Ellenőrizd, hogy a PYTHONPATH be van-e állítva!")
|
print("Győződj meg róla, hogy a PYTHONPATH tartalmazza a /backend mappát!")
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Naplózás kikapcsolása a tiszta diagnosztikai kimenetért
|
||||||
|
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
|
||||||
|
|
||||||
async def diagnose():
|
async def diagnose():
|
||||||
print("\n" + "="*40)
|
print("\n" + "═"*50)
|
||||||
print("🔍 SZERVIZ KERESŐ - RENDSZER DIAGNOSZTIKA")
|
print("🛰️ SENTINEL SYSTEM DIAGNOSTICS - MB2.0 (2026)")
|
||||||
print("="*40 + "\n")
|
print("═"*50 + "\n")
|
||||||
|
|
||||||
engine = create_async_engine(settings.DATABASE_URL)
|
async with AsyncSessionLocal() as session:
|
||||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
# --- 1. CSATLAKOZÁS ÉS ADATBÁZIS PING ---
|
||||||
|
print("1️⃣ Kapcsolódási teszt...")
|
||||||
async with async_session() as session:
|
|
||||||
# --- 1. SÉMA ELLENŐRZÉSE ---
|
|
||||||
print("1️⃣ Adatbázis séma ellenőrzése...")
|
|
||||||
try:
|
try:
|
||||||
# Organizations tábla oszlopai
|
await session.execute(text("SELECT 1"))
|
||||||
org_res = await session.execute(text(
|
print(" [✅ OK] PostgreSQL aszinkron kapcsolat aktív.")
|
||||||
"SELECT column_name FROM information_schema.columns "
|
except Exception as e:
|
||||||
"WHERE table_schema = 'data' AND table_name = 'organizations';"
|
print(f" [❌ HIBA] Nem sikerült kapcsolódni az adatbázishoz: {e}")
|
||||||
))
|
return
|
||||||
org_cols = [row[0] for row in org_res.fetchall()]
|
|
||||||
|
|
||||||
# Users tábla oszlopai
|
# --- 2. SÉMA INTEGRITÁS (MB2.0 Specifikus) ---
|
||||||
user_res = await session.execute(text(
|
print("\n2️⃣ Séma integritás ellenőrzése (Master Data)...")
|
||||||
"SELECT column_name FROM information_schema.columns "
|
tables_to_check = [
|
||||||
"WHERE table_schema = 'data' AND table_name = 'users';"
|
("identity.users", ["preferred_language", "scope_id", "is_active"]),
|
||||||
))
|
("data.organizations", ["org_type", "folder_slug", "is_active"]),
|
||||||
user_cols = [row[0] for row in user_res.fetchall()]
|
("data.assets", ["owner_org_id", "catalog_id", "vin"]),
|
||||||
|
("data.asset_catalog", ["make", "model", "factory_data"]),
|
||||||
checks = [
|
("data.vehicle_model_definitions", ["status", "raw_search_context"])
|
||||||
("organizations.language", "language" in org_cols),
|
|
||||||
("organizations.default_currency", "default_currency" in org_cols),
|
|
||||||
("users.preferred_language", "preferred_language" in user_cols),
|
|
||||||
("system_parameters tábla létezik", True) # Ha idáig eljut, a SystemParameter import sikerült
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for label, success in checks:
|
for table, columns in tables_to_check:
|
||||||
status = "✅ OK" if success else "❌ HIÁNYZIK"
|
|
||||||
print(f" [{status}] {label}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Hiba a séma lekérdezésekor: {e}")
|
|
||||||
|
|
||||||
# --- 2. ADATOK ELLENŐRZÉSE ---
|
|
||||||
print("\n2️⃣ System Parameters (Alapadatok) ellenőrzése...")
|
|
||||||
try:
|
try:
|
||||||
result = await session.execute(select(SystemParameter))
|
schema, table_name = table.split('.')
|
||||||
params = result.scalars().all()
|
query = text(f"""
|
||||||
|
SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_schema = '{schema}' AND table_name = '{table_name}';
|
||||||
|
""")
|
||||||
|
res = await session.execute(query)
|
||||||
|
existing_cols = [row[0] for row in res.fetchall()]
|
||||||
|
|
||||||
|
if not existing_cols:
|
||||||
|
print(f" [❌ HIBA] A tábla nem létezik: {table}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
missing = [c for c in columns if c not in existing_cols]
|
||||||
|
if not missing:
|
||||||
|
print(f" [✅ OK] {table} (Minden mező a helyén)")
|
||||||
|
else:
|
||||||
|
print(f" [⚠️ HIÁNY] {table} - Hiányzó mezők: {', '.join(missing)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [❌ HIBA] Hiba a(z) {table} ellenőrzésekor: {e}")
|
||||||
|
|
||||||
|
# --- 3. RENDSZER PARAMÉTEREK ---
|
||||||
|
print("\n3️⃣ System Parameters (Sentinel Config) ellenőrzése...")
|
||||||
|
try:
|
||||||
|
res = await session.execute(select(SystemParameter))
|
||||||
|
params = res.scalars().all()
|
||||||
if params:
|
if params:
|
||||||
print(f" ✅ Talált paraméterek: {len(params)} db")
|
print(f" [✅ OK] Talált paraméterek: {len(params)} db")
|
||||||
for p in params:
|
critical_keys = ["SECURITY_MAX_RECORDS_PER_HOUR", "VEHICLE_LIMIT"]
|
||||||
print(f" - {p.key}: {p.value[:2]}... (+{len(p.value)-2} elem)")
|
existing_keys = [p.key for p in params]
|
||||||
|
for ck in critical_keys:
|
||||||
|
status = "✔️" if ck in existing_keys else "❌"
|
||||||
|
print(f" {status} {ck}")
|
||||||
else:
|
else:
|
||||||
print(" ⚠️ Figyelem: A system_parameters tábla üres!")
|
print(" [⚠️ FIGYELEM] A system_parameters tábla üres! Futtasd a seedert.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ Hiba az adatok lekérésekor: {e}")
|
print(f" [❌ HIBA] SystemParameter lekérdezési hiba: {e}")
|
||||||
|
|
||||||
# --- 3. NYELVI MOTOR ELLENŐRZÉSE ---
|
# --- 4. i18n ÉS CACHE MOTOR ---
|
||||||
print("\n3️⃣ Nyelvi motor (i18n) és hu.json ellenőrzése...")
|
print("\n4️⃣ Nyelvi motor és i18n Cache ellenőrzése...")
|
||||||
try:
|
try:
|
||||||
test_save = t("COMMON.SAVE")
|
# Cache betöltése manuálisan a diagnosztikához
|
||||||
test_email = t("email.reg_greeting", first_name="Admin")
|
await translation_service.load_cache(session)
|
||||||
|
|
||||||
if test_save != "COMMON.SAVE":
|
test_key = "COMMON.SAVE"
|
||||||
print(f" ✅ Fordítás sikeres: COMMON.SAVE -> '{test_save}'")
|
test_val = translation_service.get_text(test_key, "hu")
|
||||||
print(f" ✅ Paraméteres fordítás: '{test_email}'")
|
|
||||||
|
if test_val != f"[{test_key}]":
|
||||||
|
print(f" [✅ OK] Fordítás sikeres (HU): {test_key} -> '{test_val}'")
|
||||||
else:
|
else:
|
||||||
print(" ❌ A fordítás NEM működik (csak a kulcsot adta vissza).")
|
print(f" [❌ HIBA] A fordítás nem működik. Nincs betöltött adat az adatbázisban.")
|
||||||
print(f" Ellenőrizd a /app/app/locales/hu.json elérhetőségét!")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ Hiba a nyelvi motor futtatásakor: {e}")
|
print(f" [❌ HIBA] Nyelvi motor hiba: {e}")
|
||||||
|
|
||||||
print("\n" + "="*40)
|
# --- 5. ROBOT ELŐKÉSZÜLETEK (MDM) ---
|
||||||
print("✅ DIAGNOSZTIKA KÉSZ")
|
print("\n5️⃣ Robot Pipeline (MDM Staging) állapot...")
|
||||||
print("="*40 + "\n")
|
try:
|
||||||
|
res_hunter = await session.execute(
|
||||||
|
select(func.count(VehicleModelDefinition.id)).where(VehicleModelDefinition.status == 'unverified')
|
||||||
|
)
|
||||||
|
unverified_count = res_hunter.scalar()
|
||||||
|
|
||||||
|
res_gold = await session.execute(
|
||||||
|
select(func.count(AssetCatalog.id))
|
||||||
|
)
|
||||||
|
gold_count = res_gold.scalar()
|
||||||
|
|
||||||
|
print(f" [📊 ADAT] Staging rekordok (Hunter): {unverified_count} db")
|
||||||
|
print(f" [📊 ADAT] Arany rekordok (Catalog): {gold_count} db")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [❌ HIBA] Robot-statisztika hiba: {e}")
|
||||||
|
|
||||||
|
print("\n" + "═"*50)
|
||||||
|
print("🏁 DIAGNOSZTIKA BEFEJEZŐDÖTT")
|
||||||
|
print("═"*50 + "\n")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(diagnose())
|
asyncio.run(diagnose())
|
||||||
@@ -1,37 +1,82 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/final_admin_fix.py
|
||||||
import asyncio
|
import asyncio
|
||||||
from sqlalchemy import text
|
import uuid
|
||||||
from app.db.session import SessionLocal, engine
|
from sqlalchemy import text, select
|
||||||
from app.models.user import User, UserRole
|
from app.database import AsyncSessionLocal
|
||||||
|
from app.models.identity import User, Person, UserRole
|
||||||
from app.core.security import get_password_hash
|
from app.core.security import get_password_hash
|
||||||
|
|
||||||
async def run_fix():
|
async def run_fix():
|
||||||
async with SessionLocal() as db:
|
print("\n" + "═"*50)
|
||||||
# 1. Ellenőrizzük az oszlopokat (biztonsági játék)
|
print("🛠️ ADMIN RENDSZERJAVÍTÁS ÉS INICIALIZÁLÁS (MB2.0)")
|
||||||
res = await db.execute(text("SELECT column_name FROM information_schema.columns WHERE table_schema = \u0027data\u0027 AND table_name = \u0027users\u0027"))
|
print("═"*50)
|
||||||
cols = [r[0] for r in res.fetchall()]
|
|
||||||
print(f"INFO: Meglévő oszlopok: {cols}")
|
|
||||||
|
|
||||||
if "hashed_password" not in cols:
|
async with AsyncSessionLocal() as db:
|
||||||
print("❌ HIBA: A hashed_password oszlop még mindig hiányzik! A migráció nem volt sikeres.")
|
# 1. LOGIKA: Séma ellenőrzése az 'identity' névtérben
|
||||||
|
# Az MB2.0-ban a felhasználók már nem a 'data', hanem az 'identity' sémában vannak.
|
||||||
|
check_query = text("""
|
||||||
|
SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'identity' AND table_name = 'users'
|
||||||
|
""")
|
||||||
|
res = await db.execute(check_query)
|
||||||
|
cols = [r[0] for r in res.fetchall()]
|
||||||
|
|
||||||
|
if not cols:
|
||||||
|
print("❌ HIBA: Az 'identity.users' tábla nem található. Futtasd az Alembic migrációt!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. Admin létrehozása
|
if "hashed_password" not in cols:
|
||||||
res = await db.execute(text("SELECT id FROM data.users WHERE email = :e"), {"e": "admin@profibot.hu"})
|
print("❌ HIBA: A 'hashed_password' oszlop hiányzik. Az adatbázis sémája elavult.")
|
||||||
if res.fetchone():
|
return
|
||||||
print("⚠ Az admin@profibot.hu már létezik.")
|
|
||||||
|
# 2. LOGIKA: Admin keresése
|
||||||
|
admin_email = "admin@profibot.hu"
|
||||||
|
stmt = select(User).where(User.email == admin_email)
|
||||||
|
existing_res = await db.execute(stmt)
|
||||||
|
existing_admin = existing_res.scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing_admin:
|
||||||
|
print(f"⚠️ Információ: A(z) {admin_email} felhasználó már létezik.")
|
||||||
|
# Opcionális: Jelszó kényszerített frissítése, ha elfelejtetted
|
||||||
|
# existing_admin.hashed_password = get_password_hash("Admin123!")
|
||||||
|
# await db.commit()
|
||||||
else:
|
else:
|
||||||
admin = User(
|
try:
|
||||||
email="admin@profibot.hu",
|
# 3. LOGIKA: Person és User létrehozása (MB2.0 Standard)
|
||||||
hashed_password=get_password_hash("Admin123!"),
|
# Előbb létrehozzuk a fizikai személyt
|
||||||
first_name="Admin",
|
new_person = Person(
|
||||||
last_name="Profibot",
|
id_uuid=uuid.uuid4(),
|
||||||
role=UserRole.ADMIN,
|
first_name="Rendszer",
|
||||||
is_superuser=True,
|
last_name="Adminisztrátor",
|
||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
db.add(admin)
|
db.add(new_person)
|
||||||
|
await db.flush() # ID lekérése a mentés előtt
|
||||||
|
|
||||||
|
# Létrehozzuk a felhasználói fiókot az Admin role-al
|
||||||
|
new_admin = User(
|
||||||
|
email=admin_email,
|
||||||
|
hashed_password=get_password_hash("Admin123!"),
|
||||||
|
person_id=new_person.id,
|
||||||
|
role=UserRole.superadmin, # MB2.0 enum érték
|
||||||
|
is_active=True,
|
||||||
|
is_deleted=False,
|
||||||
|
preferred_language="hu"
|
||||||
|
)
|
||||||
|
db.add(new_admin)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
print("✅ SIKER: Admin felhasználó létrehozva!")
|
print(f"✅ SIKER: Superadmin létrehozva!")
|
||||||
|
print(f" 📧 Email: {admin_email}")
|
||||||
|
print(f" 🔑 Jelszó: Admin123!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ HIBA a mentés során: {e}")
|
||||||
|
await db.rollback()
|
||||||
|
|
||||||
|
print("\n" + "═"*50)
|
||||||
|
print("🏁 JAVÍTÁSI FOLYAMAT BEFEJEZŐDÖTT")
|
||||||
|
print("═"*50 + "\n")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(run_fix())
|
asyncio.run(run_fix())
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from app.db.base import Base
|
|
||||||
from app.db.session import engine
|
|
||||||
from app.models import * # Minden modellt beimportálunk
|
|
||||||
|
|
||||||
async def init_db():
|
|
||||||
async with engine.begin() as conn:
|
|
||||||
# Ez a parancs hozza létre a táblákat a modellek alapján
|
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
|
||||||
print("✅ Minden tábla sikeresen létrejött a 'data' sémában!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(init_db())
|
|
||||||
45
backend/app/init_db_direct.py.old
Executable file
45
backend/app/init_db_direct.py.old
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/init_db_direct.py
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from sqlalchemy import text
|
||||||
|
from app.database import engine, Base
|
||||||
|
|
||||||
|
# 1. LOGIKA: Minden modell importálása
|
||||||
|
# Ez KRITIKUS: A SQLAlchemy Metadata csak akkor látja a táblákat, ha a Python
|
||||||
|
# értelmező már "találkozott" az osztályokkal.
|
||||||
|
from app.models.identity import User, Person, SocialAccount
|
||||||
|
from app.models.organization import Organization
|
||||||
|
from app.models.asset import Asset, AssetCatalog, AssetTelemetry
|
||||||
|
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise
|
||||||
|
from app.models.system import SystemParameter
|
||||||
|
from app.models.history import AuditLog
|
||||||
|
from app.models.security import PendingAction
|
||||||
|
from app.models.translation import Translation
|
||||||
|
from app.models.staged_data import ServiceStaging, DiscoveryParameter
|
||||||
|
from app.models.social import ServiceProvider, Vote, Competition, UserScore
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("DB-Initializer")
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
logger.info("🚀 Adatbázis inicializálása indítva (MB2.0 Standard)...")
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
# 2. LOGIKA: Sémák létrehozása
|
||||||
|
# SQLAlchemy nem hozza létre a sémákat automatikusan, ezt nekünk kell megtenni.
|
||||||
|
logger.info("📂 Sémák létrehozása (identity, data)...")
|
||||||
|
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS identity;"))
|
||||||
|
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data;"))
|
||||||
|
|
||||||
|
# 3. LOGIKA: Táblák létrehozása
|
||||||
|
logger.info("🏗️ Táblák és kapcsolatok generálása a Metadata alapján...")
|
||||||
|
# Ez a run_sync hívás futtatja le a klasszikus szinkron create_all-t az aszinkron kapcsolaton
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
logger.info("✅ Minden tábla sikeresen létrejött a megfelelő sémákban!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(init_db())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Hiba az inicializálás során: {e}")
|
||||||
@@ -1,66 +1,107 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/main.py
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from starlette.middleware.sessions import SessionMiddleware # ÚJ
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
from app.api.v1.api import api_router
|
from app.api.v1.api import api_router
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
from app.database import AsyncSessionLocal
|
||||||
|
from app.services.translation_service import translation_service
|
||||||
|
|
||||||
# Statikus mappák létrehozása induláskor
|
# --- LOGGING KONFIGURÁCIÓ ---
|
||||||
os.makedirs("static/previews", exist_ok=True)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("Sentinel-Main")
|
||||||
|
|
||||||
|
# --- LIFESPAN (Startup/Shutdown események) ---
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""
|
||||||
|
A rendszer 'ébredési' folyamata.
|
||||||
|
Itt töltődnek be a memóriába a globális erőforrások.
|
||||||
|
"""
|
||||||
|
logger.info("🛰️ Sentinel Master System ébredése...")
|
||||||
|
|
||||||
|
# 1. Nyelvi Cache betöltése az adatbázisból
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
try:
|
||||||
|
await translation_service.load_cache(db)
|
||||||
|
logger.info("🌍 i18n fordítási kulcsok aktiválva.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ i18n hiba az induláskor: {e}")
|
||||||
|
|
||||||
|
# Statikus könyvtárak ellenőrzése
|
||||||
|
os.makedirs(settings.STATIC_DIR, exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(settings.STATIC_DIR, "previews"), exist_ok=True)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
logger.info("💤 Sentinel Master System leállítása...")
|
||||||
|
|
||||||
|
# --- APP INICIALIZÁLÁS ---
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Service Finder API",
|
title="Service Finder Master API",
|
||||||
description="Traffic Ecosystem, Asset Vault & AI Evidence Processing",
|
description="Sentinel Traffic Ecosystem, Asset Vault & AI Evidence Processing",
|
||||||
version="2.0.0",
|
version="2.0.1",
|
||||||
openapi_url="/api/v1/openapi.json",
|
openapi_url=f"{settings.API_V1_STR}/openapi.json",
|
||||||
docs_url="/docs"
|
docs_url="/docs",
|
||||||
|
lifespan=lifespan
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- SESSION MIDDLEWARE (Google Authhoz kötelező) ---
|
# --- SESSION MIDDLEWARE (OAuth2 / Google Auth támogatás) ---
|
||||||
|
# A secret_key az aláírt sütikhez (cookies) szükséges
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
SessionMiddleware,
|
SessionMiddleware,
|
||||||
secret_key=settings.SECRET_KEY
|
secret_key=settings.SECRET_KEY
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- CORS BEÁLLÍTÁSOK ---
|
# --- CORS BEÁLLÍTÁSOK (Hálózati kapu) ---
|
||||||
|
# Itt engedélyezzük, hogy a Frontend (React/Mobile) elérje az API-t
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=[
|
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
|
||||||
"http://192.168.100.10:3001",
|
|
||||||
"http://localhost:3001",
|
|
||||||
"https://dev.profibot.hu",
|
|
||||||
"https://app.profibot.hu"
|
|
||||||
],
|
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Statikus fájlok kiszolgálása (képek, letöltések)
|
# --- STATIKUS FÁJLOK ---
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
# Képek, PDF-ek és a generált nyelvi JSON-ök kiszolgálása
|
||||||
|
app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static")
|
||||||
|
|
||||||
# A V1-es API router bekötése a /api/v1 prefix alá
|
# --- ROUTER BEKÖTÉSE ---
|
||||||
app.include_router(api_router, prefix="/api/v1")
|
# Itt csatlakozik az összes API végpont (Auth, Fleet, Billing, stb.)
|
||||||
|
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||||
|
|
||||||
|
# --- ALAPVETŐ RENDSZER VÉGPONTOK ---
|
||||||
|
|
||||||
# --- ALAPVETŐ VÉGPONTOK ---
|
|
||||||
@app.get("/", tags=["System"])
|
@app.get("/", tags=["System"])
|
||||||
async def root():
|
async def root():
|
||||||
|
""" Rendszer azonosító végpont. """
|
||||||
return {
|
return {
|
||||||
"status": "online",
|
"status": "online",
|
||||||
"message": "Service Finder Master System v2.0",
|
"system": "Service Finder Master",
|
||||||
|
"version": "2.0.1",
|
||||||
|
"environment": "Production" if not settings.DEBUG_MODE else "Development",
|
||||||
"features": [
|
"features": [
|
||||||
"Google Auth Enabled",
|
"Hierarchical i18n Enabled",
|
||||||
"Asset Vault",
|
"Asset Vault 2.0",
|
||||||
"Org Onboarding",
|
"Sentinel Security Audit",
|
||||||
"AI Evidence OCR (Robot 3)",
|
"Robot Pipeline (0-3)"
|
||||||
"Fleet Expenses (TCO)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/health", tags=["System"])
|
@app.get("/health", tags=["System"])
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""
|
"""
|
||||||
Monitoring és Load Balancer egészségügyi ellenőrző végpont.
|
Monitoring végpont.
|
||||||
|
Ha ez 'ok'-t ad, a Docker és a Load Balancer tudja, hogy a szerver él.
|
||||||
"""
|
"""
|
||||||
return {"status": "ok", "message": "Service Finder API is running flawlessly."}
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"timestamp": settings.get_now_utc_iso(),
|
||||||
|
"database": "connected" # Itt később lehet valódi ping teszt
|
||||||
|
}
|
||||||
@@ -1,45 +1,40 @@
|
|||||||
# /opt/docker/dev/service_finder/backend/app/models/__init__.py
|
# /opt/docker/dev/service_finder/backend/app/models/__init__.py
|
||||||
|
# MB 2.0: Kritikus javítás - Mindenki az app.database.Base-t használja!
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
from app.db.base_class import Base
|
# 1. Alapvető identitás és szerepkörök (Mindenki használja)
|
||||||
|
from .identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
|
||||||
|
|
||||||
# Identitás és Jogosultság
|
# 2. Földrajzi adatok és címek (Szervezetek és személyek használják)
|
||||||
from .identity import Person, User, Wallet, VerificationToken, SocialAccount
|
|
||||||
|
|
||||||
# Szervezeti struktúra (HOZZÁADVA: OrganizationSalesAssignment)
|
|
||||||
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment
|
|
||||||
|
|
||||||
# Járművek és Eszközök (Digital Twin)
|
|
||||||
from .asset import (
|
|
||||||
Asset, AssetCatalog, AssetCost, AssetEvent,
|
|
||||||
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
|
|
||||||
)
|
|
||||||
|
|
||||||
# Szerviz és Szakértelem
|
|
||||||
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
|
|
||||||
|
|
||||||
# Földrajzi adatok és Címek
|
|
||||||
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch, Rating
|
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch, Rating
|
||||||
|
|
||||||
# Gamification és Economy
|
# 3. Jármű definíciók (Az Asset-ek használják, ezért előbb kell lenniük)
|
||||||
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
|
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
|
||||||
|
|
||||||
# Rendszerkonfiguráció (HASZNÁLJUK a frissített system.py-t!)
|
# 4. Szervezeti felépítés (Hivatkozik címekre és felhasználókra)
|
||||||
|
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole
|
||||||
|
|
||||||
|
# 5. Eszközök és katalógusok (Hivatkozik definíciókra és szervezetekre)
|
||||||
|
from .asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership
|
||||||
|
|
||||||
|
# 6. Üzleti logika és előfizetések
|
||||||
|
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
|
||||||
|
|
||||||
|
# 7. Szolgáltatások és staging (Hivatkozik szervezetekre és eszközökre)
|
||||||
|
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
|
||||||
|
|
||||||
|
# 8. Rendszer, Gamification és egyebek
|
||||||
|
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
|
||||||
from .system import SystemParameter
|
from .system import SystemParameter
|
||||||
from .document import Document
|
from .document import Document
|
||||||
from .translation import Translation
|
from .translation import Translation
|
||||||
|
from .audit import SecurityAuditLog, ProcessLog, FinancialLedger
|
||||||
# Üzleti logika és Előfizetés
|
from .history import AuditLog, LogSeverity
|
||||||
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
|
|
||||||
|
|
||||||
# Naplózás és Biztonság (HOZZÁADVA: audit.py modellek)
|
|
||||||
from .audit import SecurityAuditLog, ProcessLog, FinancialLedger # <--- KRITIKUS!
|
|
||||||
from .history import AuditLog, VehicleOwnership
|
|
||||||
from .security import PendingAction
|
from .security import PendingAction
|
||||||
|
from .legal import LegalDocument, LegalAcceptance
|
||||||
|
from .logistics import Location, LocationType
|
||||||
|
|
||||||
# MDM (Master Data Management) Jármű modellek központ
|
# Aliasok a Digital Twin kompatibilitáshoz
|
||||||
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
|
|
||||||
|
|
||||||
# Aliasok a kényelmesebb fejlesztéshez
|
|
||||||
Vehicle = Asset
|
Vehicle = Asset
|
||||||
UserVehicle = Asset
|
UserVehicle = Asset
|
||||||
VehicleCatalog = AssetCatalog
|
VehicleCatalog = AssetCatalog
|
||||||
@@ -47,16 +42,17 @@ ServiceRecord = AssetEvent
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
|
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
|
||||||
"Organization", "OrganizationMember", "OrganizationSalesAssignment",
|
"Organization", "OrganizationMember", "OrganizationSalesAssignment", "OrgType", "OrgUserRole",
|
||||||
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
|
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
|
||||||
"AssetTelemetry", "AssetReview", "ExchangeRate",
|
"AssetTelemetry", "AssetReview", "ExchangeRate", "CatalogDiscovery",
|
||||||
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
|
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
|
||||||
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
|
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
|
||||||
"SystemParameter", "Document", "Translation", "PendingAction",
|
"SystemParameter", "Document", "Translation", "PendingAction",
|
||||||
"SubscriptionTier", "OrganizationSubscription",
|
"SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty",
|
||||||
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
|
"AuditLog", "VehicleOwnership", "LogSeverity",
|
||||||
"SecurityAuditLog", "ProcessLog", "FinancialLedger", # <--- KRITIKUS!
|
"SecurityAuditLog", "ProcessLog", "FinancialLedger",
|
||||||
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging",
|
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter",
|
||||||
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition",
|
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition",
|
||||||
"VehicleType", "FeatureDefinition", "ModelFeatureMap"
|
"VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
|
||||||
|
"Location", "LocationType"
|
||||||
]
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,93 +1,103 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/models/address.py
|
||||||
import uuid
|
import uuid
|
||||||
from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, func, Numeric, Index
|
from datetime import datetime
|
||||||
|
from typing import Any, List, Optional
|
||||||
|
from sqlalchemy import String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, func, Numeric, Index, and_
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
|
||||||
from sqlalchemy.orm import relationship, foreign
|
from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign
|
||||||
from app.db.base_class import Base
|
|
||||||
|
# MB 2.0: Kritikus javítás - a központi metadata-t használjuk az app.database-ből
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
class GeoPostalCode(Base):
|
class GeoPostalCode(Base):
|
||||||
"""Irányítószám alapú földrajzi kereső tábla."""
|
"""Irányítószám alapú földrajzi kereső tábla."""
|
||||||
__tablename__ = "geo_postal_codes"
|
__tablename__ = "geo_postal_codes"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
country_code = Column(String(5), default="HU")
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
zip_code = Column(String(10), nullable=False)
|
country_code: Mapped[str] = mapped_column(String(5), default="HU")
|
||||||
city = Column(String(100), nullable=False)
|
zip_code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
|
||||||
|
city: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||||
|
|
||||||
class GeoStreet(Base):
|
class GeoStreet(Base):
|
||||||
"""Utcajegyzék tábla."""
|
"""Utcajegyzék tábla."""
|
||||||
__tablename__ = "geo_streets"
|
__tablename__ = "geo_streets"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id"))
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
name = Column(String(200), nullable=False)
|
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
|
||||||
|
name: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
|
||||||
|
|
||||||
class GeoStreetType(Base):
|
class GeoStreetType(Base):
|
||||||
"""Közterület jellege (utca, út, köz stb.)."""
|
"""Közterület jellege (utca, út, köz stb.)."""
|
||||||
__tablename__ = "geo_street_types"
|
__tablename__ = "geo_street_types"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
name = Column(String(50), unique=True, nullable=False)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||||
|
|
||||||
class Address(Base):
|
class Address(Base):
|
||||||
"""Univerzális cím entitás GPS adatokkal kiegészítve."""
|
"""Univerzális cím entitás GPS adatokkal kiegészítve."""
|
||||||
__tablename__ = "addresses"
|
__tablename__ = "addresses"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id"))
|
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
|
||||||
street_name = Column(String(200), nullable=False)
|
|
||||||
street_type = Column(String(50), nullable=False)
|
street_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||||
house_number = Column(String(50), nullable=False)
|
street_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
stairwell = Column(String(20))
|
house_number: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
floor = Column(String(20))
|
|
||||||
door = Column(String(20))
|
stairwell: Mapped[Optional[str]] = mapped_column(String(20))
|
||||||
parcel_id = Column(String(50))
|
floor: Mapped[Optional[str]] = mapped_column(String(20))
|
||||||
full_address_text = Column(Text)
|
door: Mapped[Optional[str]] = mapped_column(String(20))
|
||||||
|
parcel_id: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
|
full_address_text: Mapped[Optional[str]] = mapped_column(Text)
|
||||||
|
|
||||||
# Robot és térképes funkciók számára
|
# Robot és térképes funkciók számára
|
||||||
latitude = Column(Float)
|
latitude: Mapped[Optional[float]] = mapped_column(Float)
|
||||||
longitude = Column(Float)
|
longitude: Mapped[Optional[float]] = mapped_column(Float)
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
class Branch(Base):
|
class Branch(Base):
|
||||||
"""
|
"""
|
||||||
Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik.
|
Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik.
|
||||||
Minden cégnek van legalább egy 'Main' telephelye.
|
|
||||||
"""
|
"""
|
||||||
__tablename__ = "branches"
|
__tablename__ = "branches"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
|
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
|
||||||
|
|
||||||
name = Column(String(100), nullable=False)
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
is_main = Column(Boolean, default=False)
|
is_main: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
# Részletes címadatok (Denormalizált a gyors kereséshez)
|
# Denormalizált adatok a gyors lekérdezéshez
|
||||||
postal_code = Column(String(10), index=True)
|
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
|
||||||
city = Column(String(100), index=True)
|
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||||
street_name = Column(String(150))
|
street_name: Mapped[Optional[str]] = mapped_column(String(150))
|
||||||
street_type = Column(String(50))
|
street_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
house_number = Column(String(20))
|
house_number: Mapped[Optional[str]] = mapped_column(String(20))
|
||||||
stairwell = Column(String(20))
|
stairwell: Mapped[Optional[str]] = mapped_column(String(20))
|
||||||
floor = Column(String(20))
|
floor: Mapped[Optional[str]] = mapped_column(String(20))
|
||||||
door = Column(String(20))
|
door: Mapped[Optional[str]] = mapped_column(String(20))
|
||||||
hrsz = Column(String(50))
|
hrsz: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
|
|
||||||
opening_hours = Column(JSONB, server_default=text("'{}'::jsonb"))
|
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
branch_rating = Column(Float, default=0.0)
|
branch_rating: Mapped[float] = mapped_column(Float, default=0.0)
|
||||||
|
|
||||||
status = Column(String(30), default="active")
|
status: Mapped[str] = mapped_column(String(30), default="active")
|
||||||
is_deleted = Column(Boolean, default=False)
|
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
organization = relationship("Organization", back_populates="branches")
|
# Kapcsolatok
|
||||||
address = relationship("Address")
|
organization: Mapped["Organization"] = relationship("Organization", back_populates="branches")
|
||||||
|
address: Mapped[Optional["Address"]] = relationship("Address")
|
||||||
|
|
||||||
# JAVÍTOTT KAPCSOLAT: target_branch_id használata target_id helyett
|
# Kapcsolatok (Primaryjoin tartva a rating rendszerhez)
|
||||||
reviews = relationship(
|
reviews: Mapped[List["Rating"]] = relationship(
|
||||||
"Rating",
|
"Rating",
|
||||||
primaryjoin="and_(Branch.id==foreign(Rating.target_branch_id))"
|
primaryjoin="and_(Branch.id==foreign(Rating.target_branch_id))"
|
||||||
)
|
)
|
||||||
@@ -101,18 +111,19 @@ class Rating(Base):
|
|||||||
Index('idx_rating_branch', 'target_branch_id'),
|
Index('idx_rating_branch', 'target_branch_id'),
|
||||||
{"schema": "data"}
|
{"schema": "data"}
|
||||||
)
|
)
|
||||||
# Az ID most már Integer, ahogy kérted a statisztikákhoz
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
author_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
|
||||||
|
|
||||||
# Explicit célpontok a típusbiztonság és gyorsaság érdekében
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
target_organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
|
|
||||||
target_user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
|
||||||
target_branch_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"), nullable=True)
|
|
||||||
|
|
||||||
score = Column(Numeric(3, 2), nullable=False) # 1.00 - 5.00
|
# MB 2.0: A felhasználók az identity sémában laknak!
|
||||||
comment = Column(Text)
|
author_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||||
images = Column(JSONB, server_default=text("'[]'::jsonb"))
|
|
||||||
|
|
||||||
is_verified = Column(Boolean, default=False)
|
target_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
target_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
|
target_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"))
|
||||||
|
|
||||||
|
score: Mapped[float] = mapped_column(Numeric(3, 2), nullable=False)
|
||||||
|
comment: Mapped[Optional[str]] = mapped_column(Text)
|
||||||
|
images: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
|
||||||
|
|
||||||
|
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
@@ -1,225 +1,220 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/models/asset.py
|
||||||
import uuid
|
import uuid
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, BigInteger
|
from datetime import datetime
|
||||||
from sqlalchemy.orm import relationship
|
from typing import List, Optional
|
||||||
|
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, BigInteger, Integer
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base_class import Base
|
from app.database import Base
|
||||||
|
|
||||||
class AssetCatalog(Base):
|
class AssetCatalog(Base):
|
||||||
|
""" Jármű katalógus mesteradatok (Validált technikai sablonok). """
|
||||||
__tablename__ = "vehicle_catalog"
|
__tablename__ = "vehicle_catalog"
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint(
|
UniqueConstraint('make', 'model', 'year_from', 'fuel_type', name='uix_vehicle_catalog_full'),
|
||||||
'make', 'model', 'year_from', 'engine_variant', 'fuel_type',
|
|
||||||
name='uix_vehicle_catalog_full'
|
|
||||||
),
|
|
||||||
{"schema": "data"}
|
{"schema": "data"}
|
||||||
)
|
)
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
|
master_definition_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_model_definitions.id"))
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
make: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||||
master_definition_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True)
|
model: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||||
|
generation: Mapped[Optional[str]] = mapped_column(String, index=True)
|
||||||
|
year_from: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
year_to: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
fuel_type: Mapped[Optional[str]] = mapped_column(String, index=True)
|
||||||
|
power_kw: Mapped[Optional[int]] = mapped_column(Integer, index=True)
|
||||||
|
engine_capacity: Mapped[Optional[int]] = mapped_column(Integer, index=True)
|
||||||
|
|
||||||
make = Column(String, index=True, nullable=False)
|
factory_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
model = Column(String, index=True, nullable=False)
|
|
||||||
generation = Column(String, index=True)
|
|
||||||
engine_variant = Column(String, index=True)
|
|
||||||
year_from = Column(Integer)
|
|
||||||
year_to = Column(Integer)
|
|
||||||
vehicle_class = Column(String)
|
|
||||||
fuel_type = Column(String, index=True)
|
|
||||||
|
|
||||||
master_definition = relationship("VehicleModelDefinition", back_populates="variants")
|
master_definition: Mapped[Optional["VehicleModelDefinition"]] = relationship("VehicleModelDefinition", back_populates="variants")
|
||||||
|
assets: Mapped[List["Asset"]] = relationship("Asset", back_populates="catalog")
|
||||||
power_kw = Column(Integer, index=True)
|
|
||||||
engine_capacity = Column(Integer, index=True)
|
|
||||||
max_weight_kg = Column(Integer)
|
|
||||||
axle_count = Column(Integer)
|
|
||||||
euro_class = Column(String(20))
|
|
||||||
body_type = Column(String(100))
|
|
||||||
|
|
||||||
engine_code = Column(String)
|
|
||||||
factory_data = Column(JSONB, server_default=text("'{}'::jsonb"))
|
|
||||||
|
|
||||||
assets = relationship("Asset", back_populates="catalog")
|
|
||||||
|
|
||||||
class Asset(Base):
|
class Asset(Base):
|
||||||
|
""" A fizikai eszköz (Digital Twin) - Minden adat itt fut össze. """
|
||||||
__tablename__ = "assets"
|
__tablename__ = "assets"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
||||||
vin = Column(String(17), unique=True, index=True, nullable=False)
|
|
||||||
license_plate = Column(String(20), index=True)
|
|
||||||
name = Column(String)
|
|
||||||
year_of_manufacture = Column(Integer)
|
|
||||||
current_organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
|
|
||||||
catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id"))
|
|
||||||
|
|
||||||
is_verified = Column(Boolean, default=False)
|
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
verification_method = Column(String(20))
|
vin: Mapped[str] = mapped_column(String(17), unique=True, index=True, nullable=False)
|
||||||
verification_notes = Column(Text, nullable=True)
|
license_plate: Mapped[Optional[str]] = mapped_column(String(20), index=True)
|
||||||
catalog_match_score = Column(Numeric(5, 2), nullable=True)
|
name: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
|
||||||
status = Column(String(20), default="active")
|
# Állapot és életút mérőszámok
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
year_of_manufacture: Mapped[Optional[int]] = mapped_column(Integer, index=True)
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
first_registration_date: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||||
|
current_mileage: Mapped[int] = mapped_column(Integer, default=0, index=True)
|
||||||
|
condition_score: Mapped[int] = mapped_column(Integer, default=100)
|
||||||
|
|
||||||
# --- KAPCSOLATOK (A kettőzött current_org törölve, pontosítva) ---
|
# Értékesítési modul
|
||||||
catalog = relationship("AssetCatalog", back_populates="assets")
|
is_for_sale: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||||||
|
price: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
|
||||||
|
currency: Mapped[str] = mapped_column(String(3), default="EUR")
|
||||||
|
|
||||||
# 1. Jelenlegi szervezet (Üzemeltető telephely)
|
catalog_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_catalog.id"))
|
||||||
current_org = relationship(
|
current_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||||
"Organization",
|
|
||||||
primaryjoin="Asset.current_organization_id == Organization.id",
|
|
||||||
foreign_keys="[Asset.current_organization_id]"
|
|
||||||
)
|
|
||||||
|
|
||||||
financials = relationship("AssetFinancials", back_populates="asset", uselist=False)
|
# Identity kapcsolatok
|
||||||
telemetry = relationship("AssetTelemetry", back_populates="asset", uselist=False)
|
owner_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||||
assignments = relationship("AssetAssignment", back_populates="asset")
|
owner_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||||
events = relationship("AssetEvent", back_populates="asset")
|
operator_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||||
costs = relationship("AssetCost", back_populates="asset")
|
operator_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||||
reviews = relationship("AssetReview", back_populates="asset")
|
|
||||||
ownership_history = relationship("VehicleOwnership", back_populates="vehicle")
|
|
||||||
|
|
||||||
registration_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, index=True, nullable=False)
|
status: Mapped[str] = mapped_column(String(20), default="active")
|
||||||
is_corporate = Column(Boolean, default=False, server_default=text("false"))
|
individual_equipment: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
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())
|
||||||
|
|
||||||
# Tulajdonos és Üzembentartó oszlopok
|
# --- KAPCSOLATOK ---
|
||||||
owner_person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
|
catalog: Mapped["AssetCatalog"] = relationship("AssetCatalog", back_populates="assets")
|
||||||
owner_org_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
|
financials: Mapped[Optional["AssetFinancials"]] = relationship("AssetFinancials", back_populates="asset", uselist=False)
|
||||||
operator_person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
|
costs: Mapped[List["AssetCost"]] = relationship("AssetCost", back_populates="asset")
|
||||||
operator_org_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
|
events: Mapped[List["AssetEvent"]] = relationship("AssetEvent", back_populates="asset")
|
||||||
|
logbook: Mapped[List["VehicleLogbook"]] = relationship("VehicleLogbook", back_populates="asset")
|
||||||
# 2. Tulajdonos szervezet (Kapcsolat pótolva)
|
inspections: Mapped[List["AssetInspection"]] = relationship("AssetInspection", back_populates="asset")
|
||||||
owner_org = relationship(
|
reviews: Mapped[List["AssetReview"]] = relationship("AssetReview", back_populates="asset")
|
||||||
"Organization",
|
telemetry: Mapped[Optional["AssetTelemetry"]] = relationship("AssetTelemetry", back_populates="asset", uselist=False)
|
||||||
primaryjoin="Asset.owner_org_id == Organization.id",
|
assignments: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="asset")
|
||||||
foreign_keys="[Asset.owner_org_id]"
|
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="asset")
|
||||||
)
|
|
||||||
|
|
||||||
# 3. Üzembentartó szervezet
|
|
||||||
operator_org = relationship(
|
|
||||||
"Organization",
|
|
||||||
primaryjoin="Asset.operator_org_id == Organization.id",
|
|
||||||
foreign_keys="[Asset.operator_org_id]"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. Tulajdonos magánszemély
|
|
||||||
owner_person = relationship(
|
|
||||||
"Person",
|
|
||||||
primaryjoin="Asset.owner_person_id == Person.id",
|
|
||||||
foreign_keys="[Asset.owner_person_id]"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 5. Üzembentartó magánszemély
|
|
||||||
operator_person = relationship(
|
|
||||||
"Person",
|
|
||||||
primaryjoin="Asset.operator_person_id == Person.id",
|
|
||||||
foreign_keys="[Asset.operator_person_id]"
|
|
||||||
)
|
|
||||||
|
|
||||||
class AssetFinancials(Base):
|
class AssetFinancials(Base):
|
||||||
|
""" I. Beszerzés és IV. Értékcsökkenés (Amortizáció). """
|
||||||
__tablename__ = "asset_financials"
|
__tablename__ = "asset_financials"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
|
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
|
||||||
acquisition_price = Column(Numeric(18, 2))
|
|
||||||
acquisition_date = Column(DateTime)
|
purchase_price_net: Mapped[float] = mapped_column(Numeric(18, 2))
|
||||||
financing_type = Column(String)
|
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
|
||||||
residual_value_estimate = Column(Numeric(18, 2))
|
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
|
||||||
asset = relationship("Asset", back_populates="financials")
|
activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||||
|
financing_type: Mapped[str] = mapped_column(String(50))
|
||||||
|
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
|
asset: Mapped["Asset"] = relationship("Asset", back_populates="financials")
|
||||||
|
|
||||||
|
class AssetCost(Base):
|
||||||
|
""" II. Üzemeltetés és TCO kimutatás. """
|
||||||
|
__tablename__ = "asset_costs"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
|
|
||||||
|
cost_category: Mapped[str] = mapped_column(String(50), index=True)
|
||||||
|
amount_net: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
||||||
|
currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||||
|
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
invoice_number: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||||
|
|
||||||
|
data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
asset: Mapped["Asset"] = relationship("Asset", back_populates="costs")
|
||||||
|
organization: Mapped["Organization"] = relationship("Organization")
|
||||||
|
|
||||||
|
class VehicleLogbook(Base):
|
||||||
|
""" Útnyilvántartás (NAV, Kiküldetés, Munkábajárás). """
|
||||||
|
__tablename__ = "vehicle_logbook"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
|
driver_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||||
|
|
||||||
|
trip_type: Mapped[str] = mapped_column(String(30), index=True)
|
||||||
|
is_reimbursable: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
start_mileage: Mapped[int] = mapped_column(Integer)
|
||||||
|
end_mileage: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
|
||||||
|
asset: Mapped["Asset"] = relationship("Asset", back_populates="logbook")
|
||||||
|
driver: Mapped["User"] = relationship("User")
|
||||||
|
|
||||||
|
class AssetInspection(Base):
|
||||||
|
""" Napi ellenőrző lista és Biztonsági check. """
|
||||||
|
__tablename__ = "asset_inspections"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
|
inspector_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||||
|
|
||||||
|
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
checklist_results: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||||
|
is_safe: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
|
asset: Mapped["Asset"] = relationship("Asset", back_populates="inspections")
|
||||||
|
inspector: Mapped["User"] = relationship("User")
|
||||||
|
|
||||||
|
class AssetReview(Base):
|
||||||
|
""" Jármű értékelések és visszajelzések. """
|
||||||
|
__tablename__ = "asset_reviews"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||||
|
|
||||||
|
overall_rating: Mapped[Optional[int]] = mapped_column(Integer) # 1-5 csillag
|
||||||
|
comment: Mapped[Optional[str]] = mapped_column(Text)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
asset: Mapped["Asset"] = relationship("Asset", back_populates="reviews")
|
||||||
|
user: Mapped["User"] = relationship("User")
|
||||||
|
|
||||||
|
class VehicleOwnership(Base):
|
||||||
|
""" Tulajdonosváltások története. """
|
||||||
|
__tablename__ = "vehicle_ownership_history"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
|
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||||
|
|
||||||
|
acquired_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
disposed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||||
|
|
||||||
|
asset: Mapped["Asset"] = relationship("Asset", back_populates="ownership_history")
|
||||||
|
# EZ A SOR HIÁNYZIK A KÓDODBÓL ÉS EZ JAVÍTJA A HIBÁT:
|
||||||
|
user: Mapped["User"] = relationship("User", back_populates="ownership_history")
|
||||||
|
|
||||||
class AssetTelemetry(Base):
|
class AssetTelemetry(Base):
|
||||||
__tablename__ = "asset_telemetry"
|
__tablename__ = "asset_telemetry"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
|
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
|
||||||
current_mileage = Column(Integer, default=0)
|
current_mileage: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
mileage_unit = Column(String(10), default="km")
|
asset: Mapped["Asset"] = relationship("Asset", back_populates="telemetry")
|
||||||
vqi_score = Column(Numeric(5, 2), default=100.00)
|
|
||||||
dbs_score = Column(Numeric(5, 2), default=100.00)
|
|
||||||
asset = relationship("Asset", back_populates="telemetry")
|
|
||||||
|
|
||||||
class AssetReview(Base):
|
|
||||||
__tablename__ = "asset_reviews"
|
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
|
||||||
overall_rating = Column(Integer)
|
|
||||||
criteria_scores = Column(JSONB, server_default=text("'{}'::jsonb"))
|
|
||||||
comment = Column(Text)
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
asset = relationship("Asset", back_populates="reviews")
|
|
||||||
user = relationship("User")
|
|
||||||
|
|
||||||
class AssetAssignment(Base):
|
class AssetAssignment(Base):
|
||||||
|
""" Eszköz-Szervezet összerendelés. """
|
||||||
__tablename__ = "asset_assignments"
|
__tablename__ = "asset_assignments"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
branch_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"), nullable=True)
|
status: Mapped[str] = mapped_column(String(30), default="active")
|
||||||
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
released_at = Column(DateTime(timezone=True), nullable=True)
|
|
||||||
status = Column(String(30), default="active")
|
|
||||||
|
|
||||||
asset = relationship("Asset", back_populates="assignments")
|
asset: Mapped["Asset"] = relationship("Asset", back_populates="assignments")
|
||||||
organization = relationship("Organization")
|
organization: Mapped["Organization"] = relationship("Organization", back_populates="assets")
|
||||||
branch = relationship("Branch")
|
|
||||||
|
|
||||||
class AssetEvent(Base):
|
class AssetEvent(Base):
|
||||||
|
""" Szerviz, baleset és egyéb jelentős események. """
|
||||||
__tablename__ = "asset_events"
|
__tablename__ = "asset_events"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
event_type = Column(String(50), nullable=False)
|
event_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
recorded_mileage = Column(Integer)
|
asset: Mapped["Asset"] = relationship("Asset", back_populates="events")
|
||||||
data = Column(JSONB, server_default=text("'{}'::jsonb"))
|
|
||||||
asset = relationship("Asset", back_populates="events")
|
|
||||||
registration_uuid = Column(PG_UUID(as_uuid=True), index=True, nullable=True)
|
|
||||||
|
|
||||||
class AssetCost(Base):
|
|
||||||
__tablename__ = "asset_costs"
|
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
||||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
|
||||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
|
||||||
driver_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
|
||||||
cost_type = Column(String(50), nullable=False)
|
|
||||||
amount_local = Column(Numeric(18, 2), nullable=False)
|
|
||||||
currency_local = Column(String(3), nullable=False)
|
|
||||||
amount_eur = Column(Numeric(18, 2), nullable=True)
|
|
||||||
net_amount_local = Column(Numeric(18, 2))
|
|
||||||
vat_rate = Column(Numeric(5, 2))
|
|
||||||
exchange_rate_used = Column(Numeric(18, 6))
|
|
||||||
date = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
mileage_at_cost = Column(Integer)
|
|
||||||
data = Column(JSONB, server_default=text("'{}'::jsonb"))
|
|
||||||
asset = relationship("Asset", back_populates="costs")
|
|
||||||
organization = relationship("Organization")
|
|
||||||
driver = relationship("User")
|
|
||||||
registration_uuid = Column(PG_UUID(as_uuid=True), index=True, nullable=True)
|
|
||||||
|
|
||||||
class ExchangeRate(Base):
|
class ExchangeRate(Base):
|
||||||
__tablename__ = "exchange_rates"
|
__tablename__ = "exchange_rates"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
base_currency = Column(String(3), default="EUR")
|
rate: Mapped[float] = mapped_column(Numeric(18, 6), nullable=False)
|
||||||
target_currency = Column(String(3), unique=True)
|
|
||||||
rate = Column(Numeric(18, 6), nullable=False)
|
|
||||||
|
|
||||||
class CatalogDiscovery(Base):
|
class CatalogDiscovery(Base):
|
||||||
|
""" Robot munkaterület. """
|
||||||
__tablename__ = "catalog_discovery"
|
__tablename__ = "catalog_discovery"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
__table_args__ = (UniqueConstraint('make', 'model', name='_make_model_uc'), {"schema": "data"})
|
||||||
make = Column(String(100), nullable=False, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
model = Column(String(100), nullable=False, index=True)
|
make: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||||
vehicle_class = Column(String(50), index=True)
|
model: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||||
source = Column(String(50))
|
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
|
||||||
status = Column(String(20), server_default=text("'pending'"), index=True)
|
|
||||||
attempts = Column(Integer, default=0)
|
|
||||||
last_attempt = Column(DateTime(timezone=True))
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint('make', 'model', 'vehicle_class', name='_make_model_class_uc'),
|
|
||||||
{"schema": "data"}
|
|
||||||
)
|
|
||||||
@@ -1,64 +1,63 @@
|
|||||||
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger
|
# /opt/docker/dev/service_finder/backend/app/models/audit.py
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Optional
|
||||||
|
from sqlalchemy import String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger, Integer
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base_class import Base
|
from app.database import Base
|
||||||
|
|
||||||
class SecurityAuditLog(Base):
|
class SecurityAuditLog(Base):
|
||||||
""" Kiemelt biztonsági események és a 4-szem elv. """
|
""" Kiemelt biztonsági események és a 4-szem elv naplózása. """
|
||||||
__tablename__ = "security_audit_logs"
|
__tablename__ = "security_audit_logs"
|
||||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
action = Column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST', 'SUB_EXTEND'
|
action: Mapped[Optional[str]] = mapped_column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST'
|
||||||
|
|
||||||
actor_id = Column(Integer, ForeignKey("data.users.id")) # Aki kezdeményezte
|
actor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
target_id = Column(Integer, ForeignKey("data.users.id")) # Akivel történt
|
target_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
|
confirmed_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
|
||||||
|
|
||||||
confirmed_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
is_critical: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
is_critical = Column(Boolean, default=False)
|
payload_before: Mapped[Any] = mapped_column(JSON)
|
||||||
|
payload_after: Mapped[Any] = mapped_column(JSON)
|
||||||
payload_before = Column(JSON)
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
payload_after = Column(JSON)
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
|
|
||||||
class OperationalLog(Base):
|
class OperationalLog(Base):
|
||||||
""" Felhasználói szintű napi üzemi események (Audit Trail). """
|
""" Felhasználói szintű napi üzemi események (Audit Trail). """
|
||||||
__tablename__ = "operational_logs"
|
__tablename__ = "operational_logs"
|
||||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True)
|
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="SET NULL"))
|
||||||
action = Column(String(100), nullable=False) # pl. "ADD_VEHICLE"
|
action: Mapped[str] = mapped_column(String(100), nullable=False) # pl. "ADD_VEHICLE"
|
||||||
resource_type = Column(String(50))
|
resource_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
resource_id = Column(String(100))
|
resource_id: Mapped[Optional[str]] = mapped_column(String(100))
|
||||||
details = Column(JSON, server_default=text("'{}'::jsonb"))
|
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
ip_address = Column(String(45))
|
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
class ProcessLog(Base):
|
class ProcessLog(Base):
|
||||||
""" Robotok és háttérfolyamatok futási naplója (A reggeli jelentésekhez). """
|
""" Robotok és háttérfolyamatok futási naplója (A reggeli jelentésekhez). """
|
||||||
__tablename__ = "process_logs" # Külön tábla a tisztaság kedvéért
|
__tablename__ = "process_logs"
|
||||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
process_name = Column(String(100), index=True) # 'Master-Enricher'
|
process_name: Mapped[str] = mapped_column(String(100), index=True) # 'Master-Enricher'
|
||||||
start_time = Column(DateTime(timezone=True), server_default=func.now())
|
start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
end_time = Column(DateTime(timezone=True))
|
end_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||||
items_processed = Column(Integer, default=0)
|
items_processed: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
items_failed = Column(Integer, default=0)
|
items_failed: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
details = Column(JSON, server_default=text("'{}'::jsonb"))
|
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
class FinancialLedger(Base):
|
class FinancialLedger(Base):
|
||||||
""" Minden pénz- és kreditmozgás központi naplója. """
|
""" Minden pénz- és kreditmozgás központi naplója. Billing Engine alapja. """
|
||||||
__tablename__ = "financial_ledger"
|
__tablename__ = "financial_ledger"
|
||||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"))
|
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
person_id = Column(BigInteger, ForeignKey("data.persons.id"))
|
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||||
amount = Column(Numeric(18, 4), nullable=False)
|
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
|
||||||
currency = Column(String(10))
|
currency: Mapped[Optional[str]] = mapped_column(String(10))
|
||||||
transaction_type = Column(String(50))
|
transaction_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
related_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
related_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
details = Column(JSON, server_default=text("'{}'::jsonb"))
|
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
@@ -1,43 +1,76 @@
|
|||||||
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, JSON, Numeric
|
# /opt/docker/dev/service_finder/backend/app/models/core_logic.py
|
||||||
from sqlalchemy.orm import relationship
|
from typing import Optional, List, Any
|
||||||
|
from datetime import datetime # Python saját típusa a típusjelöléshez
|
||||||
|
from sqlalchemy import String, Integer, ForeignKey, Boolean, DateTime, Numeric, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
# JAVÍTVA: Import közvetlenül a base_class-ból
|
|
||||||
from app.db.base_class import Base
|
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
class SubscriptionTier(Base):
|
class SubscriptionTier(Base):
|
||||||
|
"""
|
||||||
|
Előfizetési csomagok definíciója (pl. Free, Premium, VIP).
|
||||||
|
A csomagok határozzák meg a korlátokat (pl. max járműszám).
|
||||||
|
"""
|
||||||
__tablename__ = "subscription_tiers"
|
__tablename__ = "subscription_tiers"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
name = Column(String, unique=True) # Free, Premium, VIP, Custom
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
rules = Column(JSON) # {"max_vehicles": 5, "allow_api": true}
|
name: Mapped[str] = mapped_column(String, unique=True, index=True) # pl. 'premium'
|
||||||
is_custom = Column(Boolean, default=False)
|
rules: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # pl. {"max_vehicles": 5}
|
||||||
|
is_custom: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
class OrganizationSubscription(Base):
|
class OrganizationSubscription(Base):
|
||||||
|
"""
|
||||||
|
Szervezetek aktuális előfizetései és azok érvényessége.
|
||||||
|
"""
|
||||||
__tablename__ = "org_subscriptions"
|
__tablename__ = "org_subscriptions"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
org_id = Column(Integer, ForeignKey("data.organizations.id"))
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
tier_id = Column(Integer, ForeignKey("data.subscription_tiers.id"))
|
|
||||||
valid_from = Column(DateTime, server_default=func.now())
|
# Kapcsolat a szervezettel (data séma)
|
||||||
valid_until = Column(DateTime)
|
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
is_active = Column(Boolean, default=True)
|
|
||||||
|
# Kapcsolat a csomaggal (data séma)
|
||||||
|
tier_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.subscription_tiers.id"), nullable=False)
|
||||||
|
|
||||||
|
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
valid_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
class CreditTransaction(Base):
|
class CreditTransaction(Base):
|
||||||
|
"""
|
||||||
|
Kreditnapló (Pontok, kreditek vagy virtuális egyenleg követése).
|
||||||
|
"""
|
||||||
__tablename__ = "credit_logs"
|
__tablename__ = "credit_logs"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
org_id = Column(Integer, ForeignKey("data.organizations.id"))
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
amount = Column(Numeric(10, 2))
|
|
||||||
description = Column(String)
|
# Kapcsolat a szervezettel (data séma)
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
|
|
||||||
|
amount: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
class ServiceSpecialty(Base):
|
class ServiceSpecialty(Base):
|
||||||
"""Fa struktúra a szerviz szolgáltatásokhoz"""
|
"""
|
||||||
|
Hierarchikus fa struktúra a szerviz szolgáltatásokhoz (pl. Motor -> Futómű).
|
||||||
|
"""
|
||||||
__tablename__ = "service_specialties"
|
__tablename__ = "service_specialties"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
parent_id = Column(Integer, ForeignKey("data.service_specialties.id"), nullable=True)
|
|
||||||
name = Column(String, nullable=False)
|
|
||||||
slug = Column(String, unique=True)
|
|
||||||
|
|
||||||
parent = relationship("ServiceSpecialty", remote_side=[id], backref="children")
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
# Önmagára mutató idegen kulcs a hierarchiához
|
||||||
|
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.service_specialties.id"))
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
|
slug: Mapped[str] = mapped_column(String, unique=True, index=True)
|
||||||
|
|
||||||
|
# Kapcsolat az ős-szolgáltatással (Self-referential relationship)
|
||||||
|
parent: Mapped[Optional["ServiceSpecialty"]] = relationship("ServiceSpecialty", remote_side=[id], backref="children")
|
||||||
@@ -1,27 +1,30 @@
|
|||||||
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey
|
# /opt/docker/dev/service_finder/backend/app/models/document.py
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
import uuid
|
import uuid
|
||||||
# JAVÍTVA: Közvetlenül a base_class-ból importálunk, nem a base-ből!
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, text
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from sqlalchemy.sql import func
|
||||||
from app.db.base_class import Base
|
from app.db.base_class import Base
|
||||||
|
|
||||||
class Document(Base):
|
class Document(Base):
|
||||||
|
""" NAS alapú dokumentumtár metaadatai. """
|
||||||
__tablename__ = "documents"
|
__tablename__ = "documents"
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
parent_type = Column(String(20), nullable=False) # 'organization' vagy 'asset'
|
parent_type: Mapped[str] = mapped_column(String(20)) # 'organization' vagy 'asset'
|
||||||
parent_id = Column(String(50), nullable=False) # Org vagy Asset technikai ID-ja
|
parent_id: Mapped[str] = mapped_column(String(50), index=True)
|
||||||
doc_type = Column(String(50)) # pl. 'foundation_deed', 'registration'
|
doc_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
|
|
||||||
original_name = Column(String(255), nullable=False)
|
original_name: Mapped[str] = mapped_column(String(255))
|
||||||
file_hash = Column(String(64), nullable=False) # A NAS-on tárolt név (UUID)
|
file_hash: Mapped[str] = mapped_column(String(64))
|
||||||
file_ext = Column(String(10), default="webp")
|
file_ext: Mapped[str] = mapped_column(String(10), default="webp")
|
||||||
mime_type = Column(String(100), default="image/webp")
|
mime_type: Mapped[str] = mapped_column(String(100), default="image/webp")
|
||||||
file_size = Column(Integer)
|
file_size: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
|
||||||
has_thumbnail = Column(Boolean, default=False)
|
has_thumbnail: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
thumbnail_path = Column(String(255)) # SSD-n lévő elérés
|
thumbnail_path: Mapped[Optional[str]] = mapped_column(String(255))
|
||||||
|
|
||||||
uploaded_by = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
uploaded_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/models/gamification.py
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, List, TYPE_CHECKING
|
||||||
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text
|
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||||
from app.db.base_class import Base
|
from app.database import Base # MB 2.0: Központi Base
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.models.identity import User
|
from app.models.identity import User
|
||||||
|
|
||||||
SCHEMA_ARGS = {"schema": "data"}
|
|
||||||
|
|
||||||
class PointRule(Base):
|
class PointRule(Base):
|
||||||
__tablename__ = "point_rules"
|
__tablename__ = "point_rules"
|
||||||
__table_args__ = SCHEMA_ARGS
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
action_key: Mapped[str] = mapped_column(String, unique=True, index=True)
|
action_key: Mapped[str] = mapped_column(String, unique=True, index=True)
|
||||||
points: Mapped[int] = mapped_column(Integer, default=0)
|
points: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
@@ -23,7 +22,8 @@ class PointRule(Base):
|
|||||||
|
|
||||||
class LevelConfig(Base):
|
class LevelConfig(Base):
|
||||||
__tablename__ = "level_configs"
|
__tablename__ = "level_configs"
|
||||||
__table_args__ = SCHEMA_ARGS
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
level_number: Mapped[int] = mapped_column(Integer, unique=True)
|
level_number: Mapped[int] = mapped_column(Integer, unique=True)
|
||||||
min_points: Mapped[int] = mapped_column(Integer)
|
min_points: Mapped[int] = mapped_column(Integer)
|
||||||
@@ -31,41 +31,41 @@ class LevelConfig(Base):
|
|||||||
|
|
||||||
class PointsLedger(Base):
|
class PointsLedger(Base):
|
||||||
__tablename__ = "points_ledger"
|
__tablename__ = "points_ledger"
|
||||||
__table_args__ = SCHEMA_ARGS
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
|
|
||||||
|
# MB 2.0: User az identity sémában lakik!
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
|
|
||||||
points: Mapped[int] = mapped_column(Integer, default=0)
|
points: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
# JAVÍTÁS: Itt is server_default-ot használunk
|
|
||||||
penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
|
penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
|
||||||
reason: Mapped[str] = mapped_column(String)
|
reason: Mapped[str] = mapped_column(String)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
user: Mapped["User"] = relationship("User")
|
user: Mapped["User"] = relationship("User")
|
||||||
|
|
||||||
class UserStats(Base):
|
class UserStats(Base):
|
||||||
__tablename__ = "user_stats"
|
__tablename__ = "user_stats"
|
||||||
__table_args__ = {"schema": "data", "extend_existing": True} # Biztosítjuk a sémát
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
# A ForeignKey-nek látnia kell a data sémát!
|
# MB 2.0: User az identity sémában lakik!
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), primary_key=True)
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), primary_key=True)
|
||||||
|
|
||||||
total_xp: Mapped[int] = mapped_column(Integer, default=0)
|
total_xp: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
social_points: Mapped[int] = mapped_column(Integer, default=0)
|
social_points: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
current_level: Mapped[int] = mapped_column(Integer, default=1)
|
current_level: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
|
||||||
# --- BÜNTETŐ RENDSZER ---
|
|
||||||
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
|
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
|
||||||
restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
|
restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
|
||||||
|
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
# VISSZAMUTATÁS A USER-RE: a back_populates értéke meg kell egyezzen a User osztály 'stats' mezőjével!
|
|
||||||
user: Mapped["User"] = relationship("User", back_populates="stats")
|
user: Mapped["User"] = relationship("User", back_populates="stats")
|
||||||
|
|
||||||
|
|
||||||
class Badge(Base):
|
class Badge(Base):
|
||||||
__tablename__ = "badges"
|
__tablename__ = "badges"
|
||||||
__table_args__ = SCHEMA_ARGS
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
name: Mapped[str] = mapped_column(String, unique=True)
|
name: Mapped[str] = mapped_column(String, unique=True)
|
||||||
description: Mapped[str] = mapped_column(String)
|
description: Mapped[str] = mapped_column(String)
|
||||||
@@ -73,11 +73,14 @@ class Badge(Base):
|
|||||||
|
|
||||||
class UserBadge(Base):
|
class UserBadge(Base):
|
||||||
__tablename__ = "user_badges"
|
__tablename__ = "user_badges"
|
||||||
__table_args__ = SCHEMA_ARGS
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
|
|
||||||
|
# MB 2.0: User az identity sémában lakik!
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
|
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
|
||||||
earned_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
|
||||||
|
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
user: Mapped["User"] = relationship("User")
|
user: Mapped["User"] = relationship("User")
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,47 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/models/history.py
|
||||||
|
import uuid
|
||||||
import enum
|
import enum
|
||||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text, Enum
|
from datetime import datetime, date
|
||||||
from sqlalchemy.orm import relationship
|
from typing import Optional, Any
|
||||||
|
from sqlalchemy import String, DateTime, ForeignKey, JSON, Date, Text, Integer
|
||||||
|
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
||||||
from app.db.base_class import Base
|
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
class LogSeverity(str, enum.Enum):
|
class LogSeverity(str, enum.Enum):
|
||||||
info = "info" # Általános művelet (pl. profil megtekintés)
|
info = "info"
|
||||||
warning = "warning" # Gyanús, de nem biztosan káros (pl. 3 elrontott jelszó)
|
warning = "warning"
|
||||||
critical = "critical" # Súlyos művelet (pl. jelszóváltoztatás, export)
|
critical = "critical"
|
||||||
emergency = "emergency" # Azonnali beavatkozást igényel (pl. SuperAdmin módosítás)
|
emergency = "emergency"
|
||||||
|
|
||||||
class VehicleOwnership(Base):
|
|
||||||
__tablename__ = "vehicle_ownerships"
|
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
vehicle_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
|
||||||
start_date = Column(Date, nullable=False, default=func.current_date())
|
|
||||||
end_date = Column(Date, nullable=True)
|
|
||||||
notes = Column(Text, nullable=True)
|
|
||||||
|
|
||||||
vehicle = relationship("Asset", back_populates="ownership_history")
|
|
||||||
user = relationship("User", back_populates="ownership_history")
|
|
||||||
|
|
||||||
class AuditLog(Base):
|
class AuditLog(Base):
|
||||||
|
""" Rendszerszintű műveletnapló. """
|
||||||
__tablename__ = "audit_logs"
|
__tablename__ = "audit_logs"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
|
||||||
severity = Column(Enum(LogSeverity), default=LogSeverity.info, nullable=False)
|
|
||||||
|
|
||||||
# Mi történt és min?
|
# MB 2.0 JAVÍTÁS: A felhasználó az identity sémában lakik!
|
||||||
action = Column(String(100), nullable=False, index=True)
|
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
target_type = Column(String(50), index=True) # pl. "User", "Wallet", "Asset"
|
|
||||||
target_id = Column(String(50), index=True) # A cél rekord ID-ja
|
|
||||||
|
|
||||||
# Részletes adatok (JSONB formátum a rugalmasságért)
|
severity: Mapped[LogSeverity] = mapped_column(
|
||||||
# A 'changes' helyett explicit old/new párost használunk a könnyebb visszaállításhoz
|
PG_ENUM(LogSeverity, name="log_severity", schema="data"),
|
||||||
old_data = Column(JSON, nullable=True)
|
default=LogSeverity.info
|
||||||
new_data = Column(JSON, nullable=True)
|
)
|
||||||
|
|
||||||
# Biztonsági nyomkövetés
|
action: Mapped[str] = mapped_column(String(100), index=True)
|
||||||
ip_address = Column(String(45), index=True) # IPv6-ot is támogat
|
target_type: Mapped[Optional[str]] = mapped_column(String(50), index=True)
|
||||||
user_agent = Column(Text, nullable=True) # Böngésző/Eszköz információ
|
target_id: Mapped[Optional[str]] = mapped_column(String(50), index=True)
|
||||||
|
|
||||||
timestamp = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
old_data: Mapped[Optional[Any]] = mapped_column(JSON)
|
||||||
|
new_data: Mapped[Optional[Any]] = mapped_column(JSON)
|
||||||
|
|
||||||
user = relationship("User")
|
ip_address: Mapped[Optional[str]] = mapped_column(String(45), index=True)
|
||||||
|
user_agent: Mapped[Optional[Text]] = mapped_column(Text)
|
||||||
|
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||||
|
|
||||||
|
user: Mapped[Optional["User"]] = relationship("User")
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/models/identity.py
|
||||||
import uuid
|
import uuid
|
||||||
import enum
|
import enum
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger, UniqueConstraint
|
from datetime import datetime
|
||||||
from sqlalchemy.orm import relationship
|
from typing import Any, List, Optional
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
from sqlalchemy import String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Integer, BigInteger, UniqueConstraint
|
||||||
|
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 sqlalchemy.sql import func
|
||||||
from app.db.base_class import Base
|
|
||||||
|
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
class UserRole(str, enum.Enum):
|
class UserRole(str, enum.Enum):
|
||||||
superadmin = "superadmin"
|
superadmin = "superadmin"
|
||||||
@@ -21,126 +26,134 @@ class UserRole(str, enum.Enum):
|
|||||||
class Person(Base):
|
class Person(Base):
|
||||||
"""
|
"""
|
||||||
Természetes személy identitása. A DNS szint.
|
Természetes személy identitása. A DNS szint.
|
||||||
Itt tároljuk az örök adatokat, amik nem vesznek el account törléskor.
|
Minden identitás adat az 'identity' sémába kerül.
|
||||||
"""
|
"""
|
||||||
__tablename__ = "persons"
|
__tablename__ = "persons"
|
||||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
__table_args__ = {"schema": "identity"}
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, index=True)
|
||||||
id_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
id_uuid: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
||||||
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
|
|
||||||
|
|
||||||
# --- KRITIKUS: EGYEDI AZONOSÍTÓ HASH (Normalizált adatokból) ---
|
# A lakcím a 'data' sémában marad
|
||||||
identity_hash = Column(String(64), unique=True, index=True, nullable=True)
|
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
|
||||||
|
|
||||||
last_name = Column(String, nullable=False)
|
identity_hash: Mapped[Optional[str]] = mapped_column(String(64), unique=True, index=True)
|
||||||
first_name = Column(String, nullable=False)
|
|
||||||
phone = Column(String, nullable=True)
|
|
||||||
|
|
||||||
mothers_last_name = Column(String)
|
last_name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
mothers_first_name = Column(String)
|
first_name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
birth_place = Column(String)
|
phone: Mapped[Optional[str]] = mapped_column(String)
|
||||||
birth_date = Column(DateTime)
|
|
||||||
|
|
||||||
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
|
mothers_last_name: Mapped[Optional[str]] = mapped_column(String)
|
||||||
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
|
mothers_first_name: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
birth_place: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
birth_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||||
|
|
||||||
# --- ÖRÖK ADATOK (Person szint) ---
|
identity_docs: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
lifetime_xp = Column(BigInteger, server_default=text("0"))
|
ice_contact: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
penalty_points = Column(Integer, server_default=text("0")) # 0-3 szint
|
|
||||||
social_reputation = Column(Numeric(3, 2), server_default=text("1.00")) # 1.00 = 100%
|
|
||||||
|
|
||||||
is_sales_agent = Column(Boolean, server_default=text("false"))
|
lifetime_xp: Mapped[int] = mapped_column(BigInteger, server_default=text("0"))
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||||||
is_ghost = Column(Boolean, default=False, nullable=False)
|
social_reputation: Mapped[float] = mapped_column(Numeric(3, 2), server_default=text("1.00"))
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
is_sales_agent: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
|
is_ghost: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
users = relationship("User", back_populates="person")
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
memberships = relationship("OrganizationMember", back_populates="person")
|
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# Kapcsolatok
|
||||||
|
users: Mapped[List["User"]] = relationship("User", back_populates="person")
|
||||||
|
memberships: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="person")
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
"""
|
""" Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött. """
|
||||||
Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött.
|
|
||||||
"""
|
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
__table_args__ = {"schema": "identity"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
email = Column(String, unique=True, index=True, nullable=False)
|
email: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
|
||||||
hashed_password = Column(String, nullable=True)
|
hashed_password: Mapped[Optional[str]] = mapped_column(String)
|
||||||
role = Column(Enum(UserRole), default=UserRole.user)
|
|
||||||
|
|
||||||
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
|
role: Mapped[UserRole] = mapped_column(
|
||||||
|
PG_ENUM(UserRole, name="userrole", schema="identity"),
|
||||||
|
default=UserRole.user
|
||||||
|
)
|
||||||
|
|
||||||
# --- ELŐFIZETÉS ÉS VIP (Időkorlátos logika) ---
|
# MB 2.0 JAVÍTÁS: A hivatkozások az identity sémára mutatnak!
|
||||||
subscription_plan = Column(String(30), server_default=text("'FREE'"))
|
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||||
subscription_expires_at = Column(DateTime(timezone=True), nullable=True)
|
|
||||||
is_vip = Column(Boolean, server_default=text("false"))
|
|
||||||
|
|
||||||
# --- REFERRAL ÉS SALES (Üzletkötői hálózat) ---
|
subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"))
|
||||||
referral_code = Column(String(20), unique=True)
|
subscription_expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||||
referred_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
is_vip: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
|
||||||
# Farming üzletkötő (Átruházható cégkezelő)
|
|
||||||
current_sales_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
|
||||||
|
|
||||||
# Szervezeti kapcsolat
|
referral_code: Mapped[Optional[str]] = mapped_column(String(20), unique=True)
|
||||||
owned_organizations = relationship("Organization", back_populates="owner")
|
|
||||||
|
|
||||||
# Ez a sor felelős a gamification.py-val való hídért
|
# MB 2.0 JAVÍTÁS: Önhivatkozások az identity sémán belül
|
||||||
stats = relationship("UserStats", back_populates="user", uselist=False, cascade="all, delete-orphan")
|
referred_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
|
current_sales_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
|
|
||||||
ownership_history = relationship("VehicleOwnership", back_populates="user")
|
is_active: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
folder_slug: Mapped[Optional[str]] = mapped_column(String(12), unique=True, index=True)
|
||||||
|
|
||||||
is_active = Column(Boolean, default=False)
|
preferred_language: Mapped[str] = mapped_column(String(5), server_default="hu")
|
||||||
is_deleted = Column(Boolean, default=False)
|
region_code: Mapped[str] = mapped_column(String(5), server_default="HU")
|
||||||
folder_slug = Column(String(12), unique=True, index=True)
|
preferred_currency: Mapped[str] = mapped_column(String(3), server_default="HUF")
|
||||||
|
|
||||||
preferred_language = Column(String(5), server_default="hu")
|
scope_level: Mapped[str] = mapped_column(String(30), server_default="individual")
|
||||||
region_code = Column(String(5), server_default="HU")
|
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
preferred_currency = Column(String(3), server_default="HUF")
|
custom_permissions: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
scope_level = Column(String(30), server_default="individual") # global, region, country, entity, individual
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
scope_id = Column(String(50))
|
|
||||||
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
# Kapcsolatok
|
||||||
|
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="users")
|
||||||
person = relationship("Person", back_populates="users")
|
wallet: Mapped[Optional["Wallet"]] = relationship("Wallet", back_populates="user", uselist=False)
|
||||||
wallet = relationship("Wallet", back_populates="user", uselist=False)
|
social_accounts: Mapped[List["SocialAccount"]] = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
|
||||||
social_accounts = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
|
owned_organizations: Mapped[List["Organization"]] = relationship("Organization", back_populates="owner")
|
||||||
|
stats: Mapped[Optional["UserStats"]] = relationship("UserStats", back_populates="user", uselist=False, cascade="all, delete-orphan")
|
||||||
|
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="user")
|
||||||
|
|
||||||
class Wallet(Base):
|
class Wallet(Base):
|
||||||
""" A 3-as felosztású pénztárca. """
|
|
||||||
__tablename__ = "wallets"
|
__tablename__ = "wallets"
|
||||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
__table_args__ = {"schema": "identity"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), unique=True)
|
||||||
|
|
||||||
earned_credits = Column(Numeric(18, 4), server_default=text("0")) # Munka + Referral
|
earned_credits: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
|
||||||
purchased_credits = Column(Numeric(18, 4), server_default=text("0")) # Vásárolt
|
purchased_credits: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
|
||||||
service_coins = Column(Numeric(18, 4), server_default=text("0")) # Csak hirdetésre!
|
service_coins: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
|
||||||
|
|
||||||
currency = Column(String(3), default="HUF")
|
currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||||
user = relationship("User", back_populates="wallet")
|
user: Mapped["User"] = relationship("User", back_populates="wallet")
|
||||||
|
|
||||||
# ... (VerificationToken és SocialAccount változatlan) ...
|
|
||||||
|
|
||||||
class VerificationToken(Base):
|
class VerificationToken(Base):
|
||||||
__tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"}
|
__tablename__ = "verification_tokens"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
__table_args__ = {"schema": "identity"}
|
||||||
token = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
token_type = Column(String(20), nullable=False); created_at = Column(DateTime(timezone=True), server_default=func.now())
|
token: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
||||||
expires_at = Column(DateTime(timezone=True), nullable=False); is_used = Column(Boolean, default=False)
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
token_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||||
|
is_used: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
class SocialAccount(Base):
|
class SocialAccount(Base):
|
||||||
__tablename__ = "social_accounts"
|
__tablename__ = "social_accounts"
|
||||||
__table_args__ = (UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'), {"schema": "data"})
|
__table_args__ = (
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'),
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
|
{"schema": "identity"}
|
||||||
provider = Column(String(50), nullable=False); social_id = Column(String(255), nullable=False, index=True); email = Column(String(255), nullable=False)
|
)
|
||||||
extra_data = Column(JSON, server_default=text("'{}'::jsonb")); created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
user = relationship("User", back_populates="social_accounts")
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
provider: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
social_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||||
|
email: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
extra_data: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
user: Mapped["User"] = relationship("User", back_populates="social_accounts")
|
||||||
@@ -1,29 +1,31 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean
|
# /opt/docker/dev/service_finder/backend/app/models/legal.py
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy import Integer, String, Text, DateTime, ForeignKey, Boolean
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base import Base
|
from app.db.base_class import Base
|
||||||
|
|
||||||
class LegalDocument(Base):
|
class LegalDocument(Base):
|
||||||
__tablename__ = "legal_documents"
|
__tablename__ = "legal_documents"
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
title = Column(String(255))
|
title: Mapped[Optional[str]] = mapped_column(String(255))
|
||||||
content = Column(Text, nullable=False)
|
content: Mapped[str] = mapped_column(Text)
|
||||||
version = Column(String(20), nullable=False)
|
version: Mapped[str] = mapped_column(String(20))
|
||||||
|
|
||||||
region_code = Column(String(5), default="HU")
|
region_code: Mapped[str] = mapped_column(String(5), default="HU")
|
||||||
language = Column(String(5), default="hu")
|
language: Mapped[str] = mapped_column(String(5), default="hu")
|
||||||
|
|
||||||
is_active = Column(Boolean, default=True)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
class LegalAcceptance(Base):
|
class LegalAcceptance(Base):
|
||||||
__tablename__ = "legal_acceptances"
|
__tablename__ = "legal_acceptances"
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"))
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
document_id = Column(Integer, ForeignKey("data.legal_documents.id"))
|
document_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.legal_documents.id"))
|
||||||
accepted_at = Column(DateTime(timezone=True), server_default=func.now())
|
accepted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
ip_address = Column(String(45))
|
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
|
||||||
user_agent = Column(Text)
|
user_agent: Mapped[Optional[str]] = mapped_column(Text)
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Enum
|
# /opt/docker/dev/service_finder/backend/app/models/logistics.py
|
||||||
from app.db.base import Base
|
|
||||||
import enum
|
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
|
||||||
|
|
||||||
# Enum definiálása
|
|
||||||
class LocationType(str, enum.Enum):
|
class LocationType(str, enum.Enum):
|
||||||
stop = "stop" # Megálló / Parkoló
|
stop = "stop"
|
||||||
warehouse = "warehouse" # Raktár
|
warehouse = "warehouse"
|
||||||
client = "client" # Ügyfél címe
|
client = "client"
|
||||||
|
|
||||||
class Location(Base):
|
class Location(Base):
|
||||||
__tablename__ = "locations"
|
__tablename__ = "locations"
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String, nullable=False)
|
name: Mapped[str] = mapped_column(String)
|
||||||
|
type: Mapped[LocationType] = mapped_column(
|
||||||
|
PG_ENUM(LocationType, name="location_type", inherit_schema=True),
|
||||||
|
nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
# FONTOS: Itt is megadjuk a schema="data"-t, hogy ne a public sémába akarja írni!
|
coordinates: Mapped[Optional[str]] = mapped_column(String)
|
||||||
type = Column(Enum(LocationType, schema="data", name="location_type_enum"), nullable=False)
|
address_full: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
capacity: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
# Koordináták (egyelőre String, később PostGIS)
|
|
||||||
coordinates = Column(String, nullable=True)
|
|
||||||
address_full = Column(String, nullable=True)
|
|
||||||
|
|
||||||
capacity = Column(Integer, nullable=True)
|
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import enum
|
import enum
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, List, Optional
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Numeric, BigInteger
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Numeric, BigInteger
|
||||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base_class import Base
|
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
# 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):
|
class OrgType(str, enum.Enum):
|
||||||
individual = "individual"
|
individual = "individual"
|
||||||
@@ -25,114 +29,118 @@ class OrgUserRole(str, enum.Enum):
|
|||||||
class Organization(Base):
|
class Organization(Base):
|
||||||
"""
|
"""
|
||||||
Szervezet entitás. Lehet flotta (user) és szolgáltató (service) egyszerre.
|
Szervezet entitás. Lehet flotta (user) és szolgáltató (service) egyszerre.
|
||||||
A képességeket a kapcsolódó profilok (pl. ServiceProfile) határozzák meg.
|
Minden üzleti adat a 'data' sémába kerül.
|
||||||
"""
|
"""
|
||||||
__tablename__ = "organizations"
|
__tablename__ = "organizations"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
|
|
||||||
|
|
||||||
is_anonymized = Column(Boolean, default=False, server_default=text("false"))
|
# Kapcsolat a címekkel (szintén a data sémában)
|
||||||
anonymized_at = Column(DateTime(timezone=True), nullable=True)
|
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
|
||||||
|
|
||||||
full_name = Column(String, nullable=False) # Hivatalos név
|
is_anonymized: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text("false"))
|
||||||
name = Column(String, nullable=False) # Rövid név
|
anonymized_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||||
display_name = Column(String(50))
|
|
||||||
folder_slug = Column(String(12), unique=True, index=True)
|
|
||||||
|
|
||||||
default_currency = Column(String(3), default="HUF")
|
full_name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
country_code = Column(String(2), default="HU")
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
language = Column(String(5), default="hu")
|
display_name: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
|
folder_slug: Mapped[str] = mapped_column(String(12), unique=True, index=True)
|
||||||
|
|
||||||
# Cím adatok (redundáns a gyors kereséshez, de address_id a SSoT)
|
default_currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||||
address_zip = Column(String(10))
|
country_code: Mapped[str] = mapped_column(String(2), default="HU")
|
||||||
address_city = Column(String(100))
|
language: Mapped[str] = mapped_column(String(5), default="hu")
|
||||||
address_street_name = Column(String(150))
|
|
||||||
address_street_type = Column(String(50))
|
|
||||||
address_house_number = Column(String(20))
|
|
||||||
address_hrsz = Column(String(50))
|
|
||||||
|
|
||||||
tax_number = Column(String(20), unique=True, index=True) # Robot horgony
|
address_zip: Mapped[Optional[str]] = mapped_column(String(10))
|
||||||
reg_number = Column(String(50))
|
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))
|
||||||
|
|
||||||
org_type = Column(
|
tax_number: Mapped[Optional[str]] = mapped_column(String(20), unique=True, index=True)
|
||||||
PG_ENUM(OrgType, name="orgtype", inherit_schema=True),
|
reg_number: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
|
|
||||||
|
org_type: Mapped[OrgType] = mapped_column(
|
||||||
|
PG_ENUM(OrgType, name="orgtype", schema="data"),
|
||||||
default=OrgType.individual
|
default=OrgType.individual
|
||||||
)
|
)
|
||||||
|
|
||||||
status = Column(String(30), default="pending_verification")
|
status: Mapped[str] = mapped_column(String(30), default="pending_verification")
|
||||||
is_deleted = Column(Boolean, default=False)
|
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
# --- ÚJ: Előfizetés és Méret korlátok ---
|
subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"), index=True)
|
||||||
subscription_plan = Column(String(30), server_default=text("'FREE'"), index=True)
|
base_asset_limit: Mapped[int] = mapped_column(Integer, server_default=text("1"))
|
||||||
base_asset_limit = Column(Integer, server_default=text("1"))
|
purchased_extra_slots: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||||||
purchased_extra_slots = Column(Integer, server_default=text("0"))
|
|
||||||
|
|
||||||
notification_settings = Column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb"))
|
notification_settings: Mapped[Any] = mapped_column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb"))
|
||||||
external_integration_config = Column(JSON, server_default=text("'{}'::jsonb"))
|
external_integration_config: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
# KRITIKUS: A júzer az 'identity' sémában van!
|
||||||
is_active = Column(Boolean, default=True)
|
owner_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
is_verified = Column(Boolean, default=False)
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
# --- ÚJ: Dual Twin Tulajdonjog logika ---
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
# Individual esetén False, Business esetén True
|
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
|
||||||
is_ownership_transferable = Column(Boolean, server_default=text("true"))
|
is_ownership_transferable: Mapped[bool] = mapped_column(Boolean, server_default=text("true"))
|
||||||
|
|
||||||
# Kapcsolatok
|
# Kapcsolatok (Relationships)
|
||||||
assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
|
assets: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
|
||||||
members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
|
members: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
|
||||||
owner = relationship("User", back_populates="owned_organizations")
|
owner: Mapped[Optional["User"]] = relationship("User", back_populates="owned_organizations")
|
||||||
financials = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan")
|
financials: Mapped[List["OrganizationFinancials"]] = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan")
|
||||||
service_profile = relationship("ServiceProfile", back_populates="organization", uselist=False)
|
service_profile: Mapped[Optional["ServiceProfile"]] = relationship("ServiceProfile", back_populates="organization", uselist=False)
|
||||||
branches = relationship("Branch", back_populates="organization", cascade="all, delete-orphan")
|
branches: Mapped[List["Branch"]] = relationship("Branch", back_populates="organization", cascade="all, delete-orphan")
|
||||||
|
|
||||||
class OrganizationFinancials(Base):
|
class OrganizationFinancials(Base):
|
||||||
"""Cégek éves gazdasági adatai elemzéshez."""
|
|
||||||
__tablename__ = "organization_financials"
|
__tablename__ = "organization_financials"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
year = Column(Integer, nullable=False)
|
year: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
turnover = Column(Numeric(18, 2))
|
turnover: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
|
||||||
profit = Column(Numeric(18, 2))
|
profit: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
|
||||||
employee_count = Column(Integer)
|
employee_count: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
source = Column(String(50)) # pl. 'manual', 'crawler', 'api'
|
source: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
organization = relationship("Organization", back_populates="financials")
|
organization: Mapped["Organization"] = relationship("Organization", back_populates="financials")
|
||||||
|
|
||||||
class OrganizationMember(Base):
|
class OrganizationMember(Base):
|
||||||
"""Kapcsolótábla a személyek és szervezetek között."""
|
|
||||||
__tablename__ = "organization_members"
|
__tablename__ = "organization_members"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
|
||||||
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) # Ghost támogatás
|
|
||||||
|
|
||||||
role = Column(PG_ENUM(OrgUserRole, name="orguserrole", inherit_schema=True), default=OrgUserRole.DRIVER)
|
# KRITIKUS: User és Person az identity sémában lakik!
|
||||||
permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
is_permanent = Column(Boolean, default=False)
|
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||||
is_verified = Column(Boolean, default=False) # <--- JAVÍTÁS: Ez az oszlop hiányzott!
|
|
||||||
|
|
||||||
organization = relationship("Organization", back_populates="members")
|
role: Mapped[OrgUserRole] = mapped_column(
|
||||||
user = relationship("User")
|
PG_ENUM(OrgUserRole, name="orguserrole", schema="data"),
|
||||||
person = relationship("Person", back_populates="memberships")
|
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):
|
class OrganizationSalesAssignment(Base):
|
||||||
"""Összeköti a céget az aktuális üzletkötővel a jutalék miatt."""
|
|
||||||
__tablename__ = "org_sales_assignments"
|
__tablename__ = "org_sales_assignments"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"))
|
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||||
agent_user_id = Column(Integer, ForeignKey("data.users.id")) # Ő kapja a Farming díjat
|
|
||||||
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
|
# KRITIKUS: Az ügynök (agent) júzer az identity sémában van
|
||||||
is_active = Column(Boolean, default=True)
|
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)
|
||||||
@@ -1,44 +1,51 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/models/security.py
|
||||||
import enum
|
import enum
|
||||||
import uuid
|
from datetime import datetime
|
||||||
from datetime import datetime, timedelta
|
from typing import Optional, TYPE_CHECKING
|
||||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Enum, text
|
from sqlalchemy import String, Integer, ForeignKey, DateTime, text, Enum
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base_class import Base
|
|
||||||
|
# MB 2.0: Központi aszinkron adatbázis motorból származó Base
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .identity import User
|
||||||
|
|
||||||
class ActionStatus(str, enum.Enum):
|
class ActionStatus(str, enum.Enum):
|
||||||
pending = "pending" # Jóváhagyásra vár
|
pending = "pending"
|
||||||
approved = "approved" # Végrehajtva
|
approved = "approved"
|
||||||
rejected = "rejected" # Elutasítva
|
rejected = "rejected"
|
||||||
expired = "expired" # Lejárt (biztonsági okokból)
|
expired = "expired"
|
||||||
|
|
||||||
class PendingAction(Base):
|
class PendingAction(Base):
|
||||||
"""Négy szem elv: Műveletek, amik jóváhagyásra várnak."""
|
""" Sentinel: Kritikus műveletek jóváhagyási lánca. """
|
||||||
__tablename__ = "pending_actions"
|
__tablename__ = "pending_actions"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "system"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
# Ki akarja csinálni?
|
# JAVÍTÁS: A User az identity sémában van, nem a data-ban!
|
||||||
requester_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
requester_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||||
|
approver_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
|
||||||
|
|
||||||
# Ki hagyta jóvá/utasította el?
|
status: Mapped[ActionStatus] = mapped_column(
|
||||||
approver_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
Enum(ActionStatus, name="actionstatus", schema="system"),
|
||||||
|
default=ActionStatus.pending
|
||||||
|
)
|
||||||
|
|
||||||
status = Column(Enum(ActionStatus), default=ActionStatus.pending, nullable=False)
|
action_type: Mapped[str] = mapped_column(String(50)) # pl. "WALLET_ADJUST"
|
||||||
|
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||||
|
reason: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||||
|
|
||||||
# Milyen típusú művelet? (pl. "CHANGE_ROLE", "WALLET_ADJUST", "DELETE_LOGS")
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
action_type = Column(String(50), nullable=False)
|
expires_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=text("now() + interval '24 hours'")
|
||||||
|
)
|
||||||
|
processed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# A művelet adatai JSON-ben (pl. {"user_id": 5, "new_role": "admin"})
|
# Kapcsolatok meghatározása (String hivatkozással a körkörös import ellen)
|
||||||
payload = Column(JSON, nullable=False)
|
requester: Mapped["User"] = relationship("User", foreign_keys=[requester_id])
|
||||||
|
approver: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approver_id])
|
||||||
# Miért kell ez a művelet? (Indoklás kötelező az audit miatt)
|
|
||||||
reason = Column(String(255), nullable=False)
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
expires_at = Column(DateTime(timezone=True), default=lambda: datetime.now() + timedelta(hours=24))
|
|
||||||
processed_at = Column(DateTime(timezone=True), nullable=True)
|
|
||||||
|
|
||||||
requester = relationship("User", foreign_keys=[requester_id])
|
|
||||||
approver = relationship("User", foreign_keys=[approver_id])
|
|
||||||
@@ -1,163 +1,104 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/models/service.py
|
||||||
import uuid
|
import uuid
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Text, Float, Index, Numeric
|
from datetime import datetime
|
||||||
from sqlalchemy.orm import relationship, backref
|
from typing import Any, List, Optional
|
||||||
|
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
|
||||||
from geoalchemy2 import Geometry # PostGIS támogatás
|
from geoalchemy2 import Geometry
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base_class import Base
|
|
||||||
|
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
class ServiceProfile(Base):
|
class ServiceProfile(Base):
|
||||||
"""
|
""" Szerviz szolgáltató adatai (v1.3.1). """
|
||||||
Szerviz szolgáltató kiterjesztett adatai (v1.3.1).
|
|
||||||
Egy Organization-höz (org_type='service') kapcsolódik.
|
|
||||||
Támogatja a hierarchiát (Franchise/Telephely) és az automatizált dúsítást.
|
|
||||||
"""
|
|
||||||
__tablename__ = "service_profiles"
|
__tablename__ = "service_profiles"
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
# Egyedi ujjlenyomat index a robot számára a duplikációk elkerülésére
|
|
||||||
Index('idx_service_fingerprint', 'fingerprint', unique=True),
|
Index('idx_service_fingerprint', 'fingerprint', unique=True),
|
||||||
{"schema": "data"}
|
{"schema": "data"}
|
||||||
)
|
)
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
|
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"), unique=True)
|
||||||
|
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.service_profiles.id"))
|
||||||
|
|
||||||
# --- KAPCSOLAT A CÉGES IKERHEZ (Twin) ---
|
fingerprint: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
|
||||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), unique=True)
|
location: Mapped[Any] = mapped_column(Geometry(geometry_type='POINT', srid=4326, spatial_index=False), index=True)
|
||||||
|
|
||||||
# --- HIERARCHIA (Fa struktúra) ---
|
status: Mapped[str] = mapped_column(String(20), server_default=text("'ghost'"), index=True)
|
||||||
# Ez tárolja a szülő egység ID-ját (pl. hálózat központja)
|
last_audit_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
parent_id = Column(Integer, ForeignKey("data.service_profiles.id"), nullable=True)
|
|
||||||
|
|
||||||
# --- ROBOT IDENTITÁS ---
|
google_place_id: Mapped[Optional[str]] = mapped_column(String(100), unique=True)
|
||||||
# Normalize(Név + Város + Utca) hash, hogy ne legyen duplikáció
|
rating: Mapped[Optional[float]] = mapped_column(Float)
|
||||||
fingerprint = Column(String(255), nullable=False, index=True)
|
user_ratings_total: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
|
||||||
# PostGIS GPS pont (SRID 4326 = WGS84 koordináták)
|
vibe_analysis: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
location = Column(Geometry(geometry_type='POINT', srid=4326), index=True)
|
social_links: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
specialization_tags: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
# Állapotkezelés: ghost (robot találta), active, flagged, inactive
|
trust_score: Mapped[int] = mapped_column(Integer, default=30)
|
||||||
status = Column(String(20), server_default=text("'ghost'"), index=True)
|
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
last_audit_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
verification_log: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
# --- GOOGLE ÉS KÜLSŐ ADATOK ---
|
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
google_place_id = Column(String(100), unique=True)
|
contact_phone: Mapped[Optional[str]] = mapped_column(String)
|
||||||
rating = Column(Float)
|
contact_email: Mapped[Optional[str]] = mapped_column(String)
|
||||||
user_ratings_total = Column(Integer)
|
website: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
bio: Mapped[Optional[str]] = mapped_column(Text)
|
||||||
|
|
||||||
# --- MÉLYFÚRÁS (Deep Enrichment) ADATOK ---
|
# Kapcsolatok
|
||||||
# AI elemzés: {"tone": "barátságos", "pricing": "közép", "reliability": "magas"}
|
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
|
||||||
vibe_analysis = Column(JSONB, server_default=text("'{}'::jsonb"))
|
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
|
||||||
|
|
||||||
# Közösségi háló: {"facebook": "url", "tiktok": "url", "insta": "url"}
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
social_links = Column(JSONB, server_default=text("'{}'::jsonb"))
|
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
# Speciális szűrő címkék: {"brands": ["Yamaha", "Suzuki"], "specialty": ["engine", "tuning"]}
|
|
||||||
specialization_tags = Column(JSONB, server_default=text("'{}'::jsonb"))
|
|
||||||
|
|
||||||
# Trust Engine (Bot Discovery=30, User Entry=50, Admin/Partner=100)
|
|
||||||
trust_score = Column(Integer, default=30)
|
|
||||||
is_verified = Column(Boolean, default=False)
|
|
||||||
verification_log = Column(JSONB, server_default=text("'{}'::jsonb"))
|
|
||||||
|
|
||||||
# --- ELÉRHETŐSÉG ---
|
|
||||||
opening_hours = Column(JSONB, server_default=text("'{}'::jsonb"))
|
|
||||||
contact_phone = Column(String)
|
|
||||||
contact_email = Column(String)
|
|
||||||
website = Column(String)
|
|
||||||
bio = Column(Text)
|
|
||||||
|
|
||||||
# --- KAPCSOLATOK ---
|
|
||||||
organization = relationship("Organization", back_populates="service_profile")
|
|
||||||
expertises = relationship("ServiceExpertise", back_populates="service")
|
|
||||||
|
|
||||||
# --- ÖNMAGÁRA HIVATKOZÓ KAPCSOLAT (Hierarchia) ---
|
|
||||||
sub_services = relationship(
|
|
||||||
"ServiceProfile",
|
|
||||||
backref=backref("parent_service", remote_side=[id]),
|
|
||||||
cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
||||||
|
|
||||||
class ExpertiseTag(Base):
|
class ExpertiseTag(Base):
|
||||||
"""Szakmai szempontok taxonómiája."""
|
|
||||||
__tablename__ = "expertise_tags"
|
__tablename__ = "expertise_tags"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
key = Column(String(50), unique=True, index=True) # pl. 'bmw_gs_specialist'
|
key: Mapped[str] = mapped_column(String(50), unique=True, index=True)
|
||||||
name_hu = Column(String(100))
|
name_hu: Mapped[Optional[str]] = mapped_column(String(100))
|
||||||
category = Column(String(30)) # 'repair', 'fuel', 'food', 'emergency'
|
category: Mapped[Optional[str]] = mapped_column(String(30))
|
||||||
|
|
||||||
class ServiceExpertise(Base):
|
class ServiceExpertise(Base):
|
||||||
"""Kapcsolótábla a szerviz és a szakterület között."""
|
|
||||||
__tablename__ = "service_expertises"
|
__tablename__ = "service_expertises"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
service_id = Column(Integer, ForeignKey("data.service_profiles.id"), primary_key=True)
|
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_profiles.id"), primary_key=True)
|
||||||
expertise_id = Column(Integer, ForeignKey("data.expertise_tags.id"), primary_key=True)
|
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.expertise_tags.id"), primary_key=True)
|
||||||
|
validation_level: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
# Validációs szint (0-100% - Mennyire hiteles ez a szakértelem)
|
service: Mapped["ServiceProfile"] = relationship("ServiceProfile", back_populates="expertises")
|
||||||
validation_level = Column(Integer, default=0)
|
expertise: Mapped["ExpertiseTag"] = relationship("ExpertiseTag")
|
||||||
|
|
||||||
service = relationship("ServiceProfile", back_populates="expertises")
|
|
||||||
expertise = relationship("ExpertiseTag")
|
|
||||||
|
|
||||||
class ServiceStaging(Base):
|
class ServiceStaging(Base):
|
||||||
"""
|
""" Hunter (robot) adatok tárolója. """
|
||||||
Átmeneti tábla a Hunter (n8n/scraping) adatoknak.
|
|
||||||
"""
|
|
||||||
__tablename__ = "service_staging"
|
__tablename__ = "service_staging"
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index('idx_staging_fingerprint', 'fingerprint', unique=True),
|
Index('idx_staging_fingerprint', 'fingerprint', unique=True),
|
||||||
{"schema": "data"}
|
{"schema": "data"}
|
||||||
)
|
)
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||||
# --- Alapadatok ---
|
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
|
||||||
name = Column(String, nullable=False, index=True)
|
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||||
|
full_address: Mapped[Optional[str]] = mapped_column(String)
|
||||||
# --- Strukturált cím adatok ---
|
fingerprint: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
postal_code = Column(String(10), index=True)
|
raw_data: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
city = Column(String(100), index=True)
|
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
|
||||||
street_name = Column(String(150))
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
street_type = Column(String(50))
|
|
||||||
house_number = Column(String(20))
|
|
||||||
stairwell = Column(String(20))
|
|
||||||
floor = Column(String(20))
|
|
||||||
door = Column(String(20))
|
|
||||||
hrsz = Column(String(50))
|
|
||||||
|
|
||||||
full_address = Column(String)
|
|
||||||
contact_phone = Column(String, nullable=True)
|
|
||||||
email = Column(String, nullable=True)
|
|
||||||
website = Column(String, nullable=True)
|
|
||||||
|
|
||||||
# --- Forrás és Azonosítás ---
|
|
||||||
source = Column(String(50), nullable=True, index=True)
|
|
||||||
external_id = Column(String(100), nullable=True, index=True)
|
|
||||||
|
|
||||||
# Robot ujjlenyomat a Staging szintű deduplikációhoz
|
|
||||||
fingerprint = Column(String(255), nullable=False)
|
|
||||||
|
|
||||||
# --- Adatmentés ---
|
|
||||||
raw_data = Column(JSONB, server_default=text("'{}'::jsonb"))
|
|
||||||
|
|
||||||
# --- Státusz és Bizalom ---
|
|
||||||
status = Column(String(20), server_default=text("'pending'"), index=True)
|
|
||||||
trust_score = Column(Integer, default=0)
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
|
|
||||||
class DiscoveryParameter(Base):
|
class DiscoveryParameter(Base):
|
||||||
"""Robot vezérlési paraméterek."""
|
""" Robot vezérlési paraméterek adminból. """
|
||||||
__tablename__ = "discovery_parameters"
|
__tablename__ = "discovery_parameters"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
city = Column(String(100), nullable=False)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
keyword = Column(String(100), nullable=False)
|
city: Mapped[str] = mapped_column(String(100))
|
||||||
country_code = Column(String(2), default="HU")
|
keyword: Mapped[str] = mapped_column(String(100))
|
||||||
is_active = Column(Boolean, default=True)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
last_run_at = Column(DateTime(timezone=True))
|
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
|
# /opt/docker/dev/service_finder/backend/app/models/social.py
|
||||||
import enum
|
import enum
|
||||||
from sqlalchemy import Column, Integer, String, ForeignKey, Enum, DateTime, Boolean, Text, UniqueConstraint
|
|
||||||
from app.db.base import Base
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional, List
|
||||||
|
from sqlalchemy import String, Integer, ForeignKey, DateTime, Boolean, Text, UniqueConstraint, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
# Enums (már schema="data" beállítással a biztonságért)
|
|
||||||
class ModerationStatus(str, enum.Enum):
|
class ModerationStatus(str, enum.Enum):
|
||||||
pending = "pending"
|
pending = "pending"
|
||||||
approved = "approved"
|
approved = "approved"
|
||||||
@@ -15,57 +19,60 @@ class SourceType(str, enum.Enum):
|
|||||||
api_import = "import"
|
api_import = "import"
|
||||||
|
|
||||||
class ServiceProvider(Base):
|
class ServiceProvider(Base):
|
||||||
|
""" Közösség által beküldött szolgáltatók (v1.3.1). """
|
||||||
__tablename__ = "service_providers"
|
__tablename__ = "service_providers"
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String, nullable=False)
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
address = Column(String, nullable=False)
|
address: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
category = Column(String)
|
category: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
|
||||||
status = Column(Enum(ModerationStatus, schema="data", name="moderation_status_enum"), default=ModerationStatus.pending, nullable=False)
|
status: Mapped[ModerationStatus] = mapped_column(
|
||||||
source = Column(Enum(SourceType, schema="data", name="source_type_enum"), default=SourceType.manual, nullable=False)
|
PG_ENUM(ModerationStatus, name="moderation_status", inherit_schema=True),
|
||||||
|
default=ModerationStatus.pending
|
||||||
|
)
|
||||||
|
source: Mapped[SourceType] = mapped_column(
|
||||||
|
PG_ENUM(SourceType, name="source_type", inherit_schema=True),
|
||||||
|
default=SourceType.manual
|
||||||
|
)
|
||||||
|
|
||||||
# --- ÚJ MEZŐ ---
|
validation_score: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
validation_score = Column(Integer, default=0) # A közösségi szavazatok összege
|
evidence_image_path: Mapped[Optional[str]] = mapped_column(String)
|
||||||
# ---------------
|
added_by_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
evidence_image_path = Column(String, nullable=True)
|
|
||||||
added_by_user_id = Column(Integer, ForeignKey("data.users.id"))
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
|
||||||
|
|
||||||
class Vote(Base):
|
class Vote(Base):
|
||||||
|
""" Közösségi validációs szavazatok. """
|
||||||
__tablename__ = "votes"
|
__tablename__ = "votes"
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint('user_id', 'provider_id', name='uq_user_provider_vote'),
|
UniqueConstraint('user_id', 'provider_id', name='uq_user_provider_vote'),
|
||||||
{"schema": "data"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||||
provider_id = Column(Integer, ForeignKey("data.service_providers.id"), nullable=False)
|
provider_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_providers.id"), nullable=False)
|
||||||
vote_value = Column(Integer, nullable=False) # +1 vagy -1
|
vote_value: Mapped[int] = mapped_column(Integer, nullable=False) # +1 vagy -1
|
||||||
|
|
||||||
class Competition(Base):
|
class Competition(Base):
|
||||||
|
""" Gamifikált versenyek (pl. Januári Feltöltő Verseny). """
|
||||||
__tablename__ = "competitions"
|
__tablename__ = "competitions"
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
name = Column(String, nullable=False) # Pl: "Januári Feltöltő Verseny"
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
description = Column(Text)
|
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||||
start_date = Column(DateTime, nullable=False)
|
start_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||||
end_date = Column(DateTime, nullable=False)
|
end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||||
is_active = Column(Boolean, default=True)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
class UserScore(Base):
|
class UserScore(Base):
|
||||||
|
""" Versenyenkénti ranglista pontszámok. """
|
||||||
__tablename__ = "user_scores"
|
__tablename__ = "user_scores"
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'),
|
UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'),
|
||||||
{"schema": "data"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"))
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||||
competition_id = Column(Integer, ForeignKey("data.competitions.id"))
|
competition_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.competitions.id"))
|
||||||
points = Column(Integer, default=0)
|
points: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
last_updated = Column(DateTime, default=datetime.utcnow)
|
last_updated: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
@@ -1,17 +1,56 @@
|
|||||||
from sqlalchemy import Column, Integer, String, JSON, DateTime, func
|
# /opt/docker/dev/service_finder/backend/app/models/staged_data.py
|
||||||
from app.db.base import Base
|
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):
|
class StagedVehicleData(Base):
|
||||||
"""Ide érkeznek a nyers, validálatlan adatok a külső forrásokból"""
|
""" Robot 2.1 (Researcher) nyers adatgyűjtője. """
|
||||||
__tablename__ = "staged_vehicle_data"
|
__tablename__ = "staged_vehicle_data"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
source_url = Column(String) # Honnan jött az adat?
|
source_url: Mapped[Optional[str]] = mapped_column(String)
|
||||||
raw_data = Column(JSON) # A teljes leszedett JSON struktúra
|
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
# Feldolgozási állapot
|
status: Mapped[str] = mapped_column(String(20), default="PENDING", index=True)
|
||||||
status = Column(String, default="PENDING") # PENDING, PROCESSED, ERROR
|
error_log: Mapped[Optional[str]] = mapped_column(String)
|
||||||
error_log = Column(String, nullable=True)
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
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. """
|
||||||
|
__tablename__ = "service_staging"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), index=True)
|
||||||
|
source: Mapped[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)
|
||||||
|
|
||||||
|
city: Mapped[str] = mapped_column(String(100), index=True)
|
||||||
|
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))
|
||||||
|
|
||||||
|
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
|
||||||
|
trust_score: Mapped[int] = mapped_column(Integer, default=30)
|
||||||
|
|
||||||
|
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": "data"}
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
city: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
||||||
|
country_code: Mapped[str] = mapped_column(String(5), server_default=text("'HU'"))
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||||
@@ -1,35 +1,29 @@
|
|||||||
# backend/app/models/system.py
|
# /opt/docker/dev/service_finder/backend/app/models/system.py
|
||||||
import enum
|
from datetime import datetime
|
||||||
from sqlalchemy import Column, String, DateTime, Boolean, text, UniqueConstraint, Integer
|
from typing import Optional, Any
|
||||||
from sqlalchemy.dialects.postgresql import JSONB # <-- JSONB-t használunk a stabilitásért
|
from sqlalchemy import String, Integer, Boolean, DateTime, text, UniqueConstraint
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base_class import Base
|
from app.db.base_class import Base
|
||||||
|
|
||||||
class SystemParameter(Base):
|
class SystemParameter(Base):
|
||||||
"""
|
""" Dinamikus konfigurációs motor (Global -> Org -> User). """
|
||||||
Központi, dinamikus konfigurációs tábla.
|
|
||||||
Támogatja a többlépcsős felülbírálást (Global -> Country -> Region -> Individual).
|
|
||||||
"""
|
|
||||||
__tablename__ = "system_parameters"
|
__tablename__ = "system_parameters"
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint('key', 'scope_level', 'scope_id', name='uix_param_scope'),
|
UniqueConstraint('key', 'scope_level', 'scope_id', name='uix_param_scope'),
|
||||||
{"schema": "data", "extend_existing": True}
|
{"extend_existing": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Technikai ID, hogy a 'key' ne legyen Primary Key, így engedve a hierarchiát
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
key: Mapped[str] = mapped_column(String, index=True)
|
||||||
|
category: Mapped[str] = mapped_column(String, server_default="general", index=True)
|
||||||
|
value: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||||
|
|
||||||
key = Column(String, index=True, nullable=False) # pl. 'VEHICLE_LIMIT'
|
scope_level: Mapped[str] = mapped_column(String(30), server_default=text("'global'"), index=True)
|
||||||
category = Column(String, index=True, server_default="general")
|
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
|
|
||||||
# A tényleges érték (JSONB-ben tárolva)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
value = Column(JSONB, nullable=False) # pl. {"FREE": 1, "PREMIUM": 4}
|
description: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
last_modified_by: Mapped[Optional[str]] = mapped_column(String)
|
||||||
# --- 🛡️ HIERARCHIKUS SZINTEK ---
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
|
||||||
scope_level = Column(String(30), server_default=text("'global'"), index=True)
|
|
||||||
scope_id = Column(String(50), nullable=True)
|
|
||||||
|
|
||||||
is_active = Column(Boolean, default=True)
|
|
||||||
description = Column(String)
|
|
||||||
last_modified_by = Column(String, nullable=True)
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
|
|
||||||
@@ -1,10 +1,27 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Text
|
# /opt/docker/dev/service_finder/backend/app/models/translation.py
|
||||||
from app.db.base_class import Base
|
from sqlalchemy import String, Integer, Text, Boolean, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
class Translation(Base):
|
class Translation(Base):
|
||||||
|
"""
|
||||||
|
Többnyelvűséget támogató tábla a felületi elemekhez és dinamikus tartalmakhoz.
|
||||||
|
"""
|
||||||
__tablename__ = "translations"
|
__tablename__ = "translations"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
key = Column(String(255), index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
lang = Column(String(5), index=True) # pl: 'hu', 'en'
|
|
||||||
value = Column(Text)
|
# A fordítandó kulcs (pl. 'NAV_DASHBOARD' vagy 'ERR_USER_NOT_FOUND')
|
||||||
|
key: Mapped[str] = mapped_column(String(255), index=True)
|
||||||
|
|
||||||
|
# Nyelvi kód (pl: 'hu', 'en', 'de')
|
||||||
|
lang: Mapped[str] = mapped_column(String(5), index=True)
|
||||||
|
|
||||||
|
# A tényleges fordított szöveg
|
||||||
|
value: Mapped[str] = mapped_column(Text)
|
||||||
|
|
||||||
|
# --- JAVÍTÁS: A diagnosztika által hiányolt publikációs állapot ---
|
||||||
|
is_published: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
|
||||||
@@ -1,106 +1,136 @@
|
|||||||
from sqlalchemy import Column, Integer, String, JSON, UniqueConstraint, text, Boolean, DateTime, ForeignKey, Numeric, Index, Text
|
# /opt/docker/dev/service_finder/backend/app/models/vehicle_definitions.py
|
||||||
from sqlalchemy.orm import relationship
|
from __future__ import annotations
|
||||||
from sqlalchemy.sql import func
|
from datetime import datetime
|
||||||
|
from typing import Optional, List
|
||||||
|
from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, text, Index, UniqueConstraint, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from app.db.base_class import Base
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
# MB 2.0: Egységesített Base import a központi adatbázis motorból
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
class VehicleType(Base):
|
class VehicleType(Base):
|
||||||
"""Jármű főtípusok sémája (Séma-gazda)"""
|
""" Jármű kategóriák (pl. Személyautó, Motorkerékpár, Teherautó, Hajó) """
|
||||||
__tablename__ = "vehicle_types"
|
__tablename__ = "vehicle_types"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
code = Column(String(30), unique=True, index=True)
|
code: Mapped[str] = mapped_column(String(30), unique=True, index=True)
|
||||||
name = Column(String(50))
|
name: Mapped[str] = mapped_column(String(50))
|
||||||
icon = Column(String(50))
|
icon: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
units = Column(JSON, server_default=text("'{\"power\": \"kW\", \"weight\": \"kg\", \"cargo\": \"m3\"}'::jsonb"))
|
units: Mapped[dict] = mapped_column(JSONB, server_default=text("'{\"power\": \"kW\", \"weight\": \"kg\"}'::jsonb"))
|
||||||
|
|
||||||
|
# Kapcsolatok
|
||||||
|
features: Mapped[List["FeatureDefinition"]] = relationship("FeatureDefinition", back_populates="vehicle_type")
|
||||||
|
definitions: Mapped[List["VehicleModelDefinition"]] = relationship("VehicleModelDefinition", back_populates="v_type_rel")
|
||||||
|
|
||||||
features = relationship("FeatureDefinition", back_populates="vehicle_type")
|
|
||||||
definitions = relationship("VehicleModelDefinition", back_populates="v_type_rel")
|
|
||||||
|
|
||||||
class FeatureDefinition(Base):
|
class FeatureDefinition(Base):
|
||||||
"""Globális felszereltség szótár"""
|
""" Felszereltségi elemek definíciója (pl. ABS, Klíma, LED fényszóró) """
|
||||||
__tablename__ = "feature_definitions"
|
__tablename__ = "feature_definitions"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
|
vehicle_type_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.vehicle_types.id"))
|
||||||
category = Column(String(50))
|
code: Mapped[str] = mapped_column(String(50), index=True)
|
||||||
name = Column(String(100), nullable=False)
|
name: Mapped[str] = mapped_column(String(100))
|
||||||
data_type = Column(String(20), default="boolean")
|
category: Mapped[str] = mapped_column(String(50), index=True)
|
||||||
|
|
||||||
vehicle_type = relationship("VehicleType", back_populates="features")
|
vehicle_type: Mapped["VehicleType"] = relationship("VehicleType", back_populates="features")
|
||||||
|
model_maps: Mapped[List["ModelFeatureMap"]] = relationship("ModelFeatureMap", back_populates="feature")
|
||||||
|
|
||||||
class ModelFeatureMap(Base):
|
|
||||||
"""Modell-szintű felszereltségi sablon"""
|
|
||||||
__tablename__ = "model_feature_maps"
|
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
model_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), primary_key=True)
|
|
||||||
feature_id = Column(Integer, ForeignKey("data.feature_definitions.id"), primary_key=True)
|
|
||||||
availability = Column(String(20), default="standard")
|
|
||||||
value = Column(String(100))
|
|
||||||
|
|
||||||
class VehicleModelDefinition(Base):
|
class VehicleModelDefinition(Base):
|
||||||
"""MDM Master rekordok - v1.3.0 Pipeline Edition (Researcher & Alchemist)"""
|
"""
|
||||||
|
Robot v1.1.0 Multi-Tier MDM Master Adattábla.
|
||||||
|
Az ökoszisztéma technikai igazságforrása.
|
||||||
|
"""
|
||||||
__tablename__ = "vehicle_model_definitions"
|
__tablename__ = "vehicle_model_definitions"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
|
make: Mapped[str] = mapped_column(String(100), index=True)
|
||||||
|
marketing_name: Mapped[str] = mapped_column(String(255), index=True) # Nyers név az RDW-ből
|
||||||
|
official_marketing_name: Mapped[Optional[str]] = mapped_column(String(255)) # Dúsított, validált név (Robot 2.2)
|
||||||
|
|
||||||
|
# --- ROBOT LOGIKAI MEZŐK (JAVÍTVA 2.0 STÍLUSBAN) ---
|
||||||
|
attempts: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
|
||||||
|
last_error: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
|
||||||
|
|
||||||
|
# --- PRECISION LOGIC MEZŐK ---
|
||||||
|
normalized_name: Mapped[Optional[str]] = mapped_column(String(255), index=True, nullable=True)
|
||||||
|
marketing_name_aliases: Mapped[list] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
|
||||||
|
engine_code: Mapped[Optional[str]] = mapped_column(String(50), index=True) # A GLOBÁLIS KAPOCS
|
||||||
|
|
||||||
|
# --- TECHNIKAI AZONOSÍTÓK ---
|
||||||
|
technical_code: Mapped[str] = mapped_column(String(100), index=True) # Holland rendszám (kulcs)
|
||||||
|
variant_code: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||||
|
version_code: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||||
|
|
||||||
|
# --- SPECIFIKÁCIÓK ---
|
||||||
|
vehicle_type_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_types.id"))
|
||||||
|
vehicle_class: Mapped[Optional[str]] = mapped_column(String(50), index=True)
|
||||||
|
body_type: Mapped[Optional[str]] = mapped_column(String(100))
|
||||||
|
fuel_type: Mapped[Optional[str]] = mapped_column(String(50), index=True)
|
||||||
|
|
||||||
|
engine_capacity: Mapped[int] = mapped_column(Integer, default=0, index=True)
|
||||||
|
power_kw: Mapped[int] = mapped_column(Integer, default=0, index=True)
|
||||||
|
torque_nm: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
cylinders: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
cylinder_layout: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
|
|
||||||
|
curb_weight: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
max_weight: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
euro_classification: Mapped[Optional[str]] = mapped_column(String(20))
|
||||||
|
doors: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
transmission_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
|
drive_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||||
|
|
||||||
|
# --- ÉLETCIKLUS ÉS STÁTUSZ ---
|
||||||
|
year_from: Mapped[Optional[int]] = mapped_column(Integer, index=True)
|
||||||
|
year_to: Mapped[Optional[int]] = mapped_column(Integer, index=True)
|
||||||
|
production_status: Mapped[Optional[str]] = mapped_column(String(50)) # active / discontinued
|
||||||
|
|
||||||
|
# Státusz szintek: unverified, research_in_progress, awaiting_ai_synthesis, gold_enriched
|
||||||
|
status: Mapped[str] = mapped_column(String(50), server_default=text("'unverified'"), index=True)
|
||||||
|
is_manual: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
source: Mapped[Optional[str]] = mapped_column(String(100))
|
||||||
|
|
||||||
|
# --- ADAT-KONTÉNEREK ---
|
||||||
|
raw_search_context: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
research_metadata: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
specifications: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # Robot 2.2/2.5 Arany adatai
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
last_research_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||||
|
|
||||||
|
# --- BEÁLLÍTÁSOK ---
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint('make', 'technical_code', 'vehicle_type', name='uix_make_tech_type'),
|
UniqueConstraint('make', 'normalized_name', 'variant_code', 'version_code', 'fuel_type', name='uix_vmd_precision'),
|
||||||
Index('idx_vmd_lookup', 'make', 'technical_code'),
|
Index('idx_vmd_lookup_fast', 'make', 'normalized_name'),
|
||||||
|
Index('idx_vmd_engine_bridge', 'make', 'engine_code'),
|
||||||
{"schema": "data"}
|
{"schema": "data"}
|
||||||
)
|
)
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
# KAPCSOLATOK
|
||||||
make = Column(String(50), nullable=False, index=True)
|
v_type_rel: Mapped["VehicleType"] = relationship("VehicleType", back_populates="definitions")
|
||||||
technical_code = Column(String(50), nullable=False, index=True)
|
feature_maps: Mapped[List["ModelFeatureMap"]] = relationship("ModelFeatureMap", back_populates="model_definition")
|
||||||
marketing_name = Column(String(100), index=True)
|
|
||||||
family_name = Column(String(100))
|
|
||||||
|
|
||||||
vehicle_type = Column(String(30), index=True)
|
# Hivatkozás az asset.py-ban lévő osztályra
|
||||||
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
|
# Megjegyzés: Ha az AssetCatalog nincs itt importálva, húzzal adjuk meg a nevet
|
||||||
vehicle_class = Column(String(50))
|
variants: Mapped[List["AssetCatalog"]] = relationship("AssetCatalog", back_populates="master_definition")
|
||||||
|
|
||||||
parent_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True)
|
|
||||||
year_from = Column(Integer, nullable=True, index=True)
|
|
||||||
year_to = Column(Integer, nullable=True, index=True)
|
|
||||||
synonyms = Column(JSON, server_default=text("'[]'::jsonb"))
|
|
||||||
|
|
||||||
# --- ROBOT VÉDELMI ÉS PIPELINE MEZŐK (v1.3.0) ---
|
class ModelFeatureMap(Base):
|
||||||
is_manual = Column(Boolean, default=False, server_default=text("false"), index=True)
|
""" Kapcsolótábla a modellek és az alapfelszereltség között """
|
||||||
attempts = Column(Integer, default=0, server_default=text("0"), index=True)
|
__tablename__ = "model_feature_maps"
|
||||||
last_error = Column(Text, nullable=True)
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
# Robot 2.1 "Researcher" porszívózott nyers adatai (A szemetesláda)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
raw_search_context = Column(Text, nullable=True)
|
model_definition_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.vehicle_model_definitions.id"))
|
||||||
|
feature_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.feature_definitions.id"))
|
||||||
|
is_standard: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
# Telemetria és forrás adatok (JSONB a hatékonyabb kereséshez)
|
model_definition: Mapped["VehicleModelDefinition"] = relationship("VehicleModelDefinition", back_populates="feature_maps")
|
||||||
research_metadata = Column(JSONB, server_default=text("'{}'::jsonb"), nullable=False)
|
feature: Mapped["FeatureDefinition"] = relationship("FeatureDefinition", back_populates="model_maps")
|
||||||
# --------------------------------------------------
|
|
||||||
|
|
||||||
# --- TECHNIKAI FIX OSZLOPOK ---
|
|
||||||
engine_capacity = Column(Integer, index=True)
|
|
||||||
power_kw = Column(Integer, index=True)
|
|
||||||
max_weight_kg = Column(Integer, index=True)
|
|
||||||
|
|
||||||
axle_count = Column(Integer)
|
|
||||||
payload_capacity_kg = Column(Integer)
|
|
||||||
cargo_volume_m3 = Column(Numeric(10, 2))
|
|
||||||
cargo_length_mm = Column(Integer)
|
|
||||||
cargo_width_mm = Column(Integer)
|
|
||||||
cargo_height_mm = Column(Integer)
|
|
||||||
|
|
||||||
specifications = Column(JSON, server_default=text("'{}'::jsonb"))
|
|
||||||
features_json = Column(JSON, server_default=text("'{}'::jsonb"))
|
|
||||||
|
|
||||||
# Státusz mező hossza 30-ra növelve az automatikus migrációhoz
|
|
||||||
status = Column(String(30), server_default="unverified", index=True)
|
|
||||||
is_master = Column(Boolean, default=False)
|
|
||||||
source = Column(String(50))
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
||||||
|
|
||||||
# Kapcsolatok
|
|
||||||
v_type_rel = relationship("VehicleType", back_populates="definitions")
|
|
||||||
master_record = relationship("VehicleModelDefinition", remote_side=[id], backref="merged_variants")
|
|
||||||
variants = relationship("AssetCatalog", back_populates="master_definition", primaryjoin="VehicleModelDefinition.id == AssetCatalog.master_definition_id")
|
|
||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user