STABLE: Final schema sync, optimized gitignore
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,15 +1,18 @@
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
backend/__pycache__/
|
||||
backend/app/scripts/__pycache__/
|
||||
|
||||
# Docker & Data (Master Book 2.0 izoláció)
|
||||
ollama_data/
|
||||
n8n/data/*.log
|
||||
n8n/data/*.json
|
||||
n8n/
|
||||
temp/
|
||||
infra/postgres/data/
|
||||
|
||||
# Logs
|
||||
logs/*.log
|
||||
*.log
|
||||
|
||||
# IDE & AI Config
|
||||
.continue/
|
||||
@@ -18,4 +21,4 @@ vscode_config/
|
||||
|
||||
# Backup files
|
||||
*.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 logging
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -8,12 +8,13 @@ from app.models.asset import AssetCatalog
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BaseHarvester:
|
||||
""" MDM Adatgyűjtő Alaposztály. """
|
||||
def __init__(self, category: str):
|
||||
self.category = category # car, bike, truck
|
||||
self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/2.0"}
|
||||
self.category = category # 'car', 'motorcycle', 'truck'
|
||||
self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/2.1"}
|
||||
|
||||
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(
|
||||
AssetCatalog.make == brand,
|
||||
AssetCatalog.model == model,
|
||||
@@ -26,7 +27,7 @@ class BaseHarvester:
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
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"))
|
||||
if not existing:
|
||||
new_v = AssetCatalog(
|
||||
@@ -37,9 +38,11 @@ class BaseHarvester:
|
||||
year_to=specs.get("year_to"),
|
||||
vehicle_class=self.category,
|
||||
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)
|
||||
logger.info(f"🆕 Új katalógus elem: {brand} {model}")
|
||||
logger.info(f"🆕 Új katalógus elem rögzítve: {brand} {model}")
|
||||
return True
|
||||
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
|
||||
|
||||
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 \
|
||||
gcc \
|
||||
python3-dev \
|
||||
libpq-dev \
|
||||
libjpeg-dev \
|
||||
zlib1g-dev \
|
||||
libgl1 \
|
||||
libglib2.0-0 \
|
||||
&& 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 .
|
||||
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 . .
|
||||
|
||||
ENV PYTHONPATH=/app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
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
|
||||
import logging
|
||||
from fastapi import Depends, HTTPException, status
|
||||
@@ -7,11 +8,18 @@ from sqlalchemy import select
|
||||
|
||||
from app.db.session import get_db
|
||||
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
|
||||
|
||||
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
|
||||
reusable_oauth2 = OAuth2PasswordBearer(
|
||||
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.
|
||||
"""
|
||||
# Dev bypass (ha esetleg fejlesztéshez használtad korábban, itt a helye,
|
||||
# de élesben a token validáció fut le)
|
||||
# Fejlesztői bypass (opcionális, csak DEBUG módban)
|
||||
if settings.DEBUG and token == "dev_bypass_active":
|
||||
return {
|
||||
"sub": "1",
|
||||
@@ -48,7 +55,7 @@ async def get_current_user(
|
||||
payload: Dict = Depends(get_current_token_payload)
|
||||
) -> 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")
|
||||
if not user_id:
|
||||
@@ -57,6 +64,7 @@ async def get_current_user(
|
||||
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)))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
@@ -71,13 +79,12 @@ async def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
"""
|
||||
Ellenőrzi, hogy a felhasználó aktív-e.
|
||||
Ez elengedhetetlen az Admin felület és a védett végpontok számára.
|
||||
Ellenőrzi, hogy a felhasználó aktív-e (KYC Step 2 kész).
|
||||
"""
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(
|
||||
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
|
||||
|
||||
@@ -86,22 +93,19 @@ async def check_resource_access(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Scoped RBAC: Megakadályozza, hogy egy felhasználó más valaki erőforrásaihoz nyúljon.
|
||||
Kezeli az ID-t (int) és a Scope ID-t / Slug-ot (str) is.
|
||||
Scoped RBAC: Megakadályozza a jogosulatlan hozzáférést mások adataihoz.
|
||||
"""
|
||||
if current_user.role == UserRole.superadmin:
|
||||
return True
|
||||
|
||||
# Ha a usernek van beállított scope_id-ja (pl. egy flottához tartozik),
|
||||
# akkor ellenőrizzük, hogy a kért erőforrás abba a scope-ba tartozik-e.
|
||||
user_scope = current_user.scope_id
|
||||
user_scope = str(current_user.scope_id) if current_user.scope_id else None
|
||||
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:
|
||||
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:
|
||||
return True
|
||||
|
||||
@@ -112,8 +116,7 @@ async def check_resource_access(
|
||||
|
||||
def check_min_rank(role_key: str):
|
||||
"""
|
||||
Dinamikus Rank ellenőrzés.
|
||||
Az adatbázisból (system_parameters) kéri le az elvárt szintet.
|
||||
Dinamikus Rank ellenőrzés a system_parameters tábla alapján.
|
||||
"""
|
||||
async def rank_checker(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -130,7 +133,7 @@ def check_min_rank(role_key: str):
|
||||
if user_rank < required_rank:
|
||||
raise HTTPException(
|
||||
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 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.get("/provider/inbox")
|
||||
def provider_inbox(request: Request, provider_id: str):
|
||||
cur = request.state.db.cursor()
|
||||
cur.execute("""
|
||||
SELECT * FROM app.v_provider_inbox
|
||||
WHERE provider_listing_id = %s
|
||||
ORDER BY created_at DESC
|
||||
""", (provider_id,))
|
||||
rows = cur.fetchall()
|
||||
return rows
|
||||
async def provider_inbox(provider_id: str, db: AsyncSession = Depends(get_db)):
|
||||
""" Aszinkron szerviz-postaláda lekérdezés. """
|
||||
query = text("""
|
||||
SELECT * FROM data.service_profiles
|
||||
WHERE id = :p_id
|
||||
""")
|
||||
result = await db.execute(query, {"p_id": provider_id})
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
Binary file not shown.
@@ -1,32 +1,20 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/api.py
|
||||
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()
|
||||
|
||||
# Hitelesítés (Authentication)
|
||||
# Minden modul az új, refaktorált végpontokra mutat
|
||||
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"])
|
||||
|
||||
# Katalógus (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"])
|
||||
|
||||
# Szervezetek (Organizations)
|
||||
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
|
||||
|
||||
# Dokumentumok (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)"])
|
||||
|
||||
# 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(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 sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, text, delete
|
||||
@@ -5,11 +6,12 @@ from typing import List, Any, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
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
|
||||
# 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.history import AuditLog, LogSeverity
|
||||
from app.schemas.admin_security import PendingActionResponse, SecurityStatusResponse
|
||||
|
||||
from app.services.security_service import security_service
|
||||
from app.services.translation_service import TranslationService
|
||||
@@ -24,30 +26,23 @@ class ConfigUpdate(BaseModel):
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# --- 🛡️ ADMIN JOGOSULTSÁG ELLENŐRZŐ ---
|
||||
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]:
|
||||
raise HTTPException(
|
||||
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
|
||||
|
||||
# --- 🛰️ 1. SENTINEL: RENDSZERÁLLAPOT ÉS MONITORING ---
|
||||
|
||||
@router.get("/health-monitor", tags=["Sentinel Monitoring"])
|
||||
async def get_system_health(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
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 = {}
|
||||
|
||||
# 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"))
|
||||
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"))
|
||||
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)
|
||||
crit_logs = await db.execute(select(func.count(AuditLog.id)).where(
|
||||
AuditLog.severity.in_([LogSeverity.critical, LogSeverity.emergency]),
|
||||
AuditLog.timestamp >= day_ago
|
||||
))
|
||||
crit_logs = await db.execute(
|
||||
select(func.count(SecurityAuditLog.id))
|
||||
.where(
|
||||
SecurityAuditLog.is_critical == True,
|
||||
SecurityAuditLog.created_at >= day_ago
|
||||
)
|
||||
)
|
||||
stats["critical_alerts_24h"] = crit_logs.scalar() or 0
|
||||
|
||||
return stats
|
||||
|
||||
# --- ⚖️ 2. SENTINEL: NÉGY SZEM ELV (Approval System) ---
|
||||
|
||||
@router.get("/pending-actions", response_model=List[PendingActionResponse], tags=["Sentinel Security"])
|
||||
@router.get("/pending-actions", response_model=List[Any], tags=["Sentinel Security"])
|
||||
async def list_pending_actions(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
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)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
@@ -85,33 +80,26 @@ async def approve_action(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
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:
|
||||
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:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# --- ⚙️ 3. DINAMIKUS KONFIGURÁCIÓ (Hierarchical Config) ---
|
||||
|
||||
@router.get("/parameters", tags=["Dynamic Configuration"])
|
||||
async def list_all_parameters(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
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))
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/parameters", tags=["Dynamic Configuration"])
|
||||
async def set_parameter(
|
||||
config: ConfigUpdate, # <--- Most már egy objektumot várunk a Body-ban
|
||||
config: ConfigUpdate,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
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("""
|
||||
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
|
||||
VALUES (:key, :val, :sl, :sid, :cat, :user)
|
||||
@@ -125,7 +113,7 @@ async def set_parameter(
|
||||
|
||||
await db.execute(query, {
|
||||
"key": config.key,
|
||||
"val": config.value, # Itt bármilyen komplex JSON-t átadhatsz
|
||||
"val": config.value,
|
||||
"sl": config.scope_level,
|
||||
"sid": config.scope_id,
|
||||
"cat": config.category,
|
||||
@@ -134,31 +122,10 @@ async def set_parameter(
|
||||
await db.commit()
|
||||
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"])
|
||||
async def sync_translations_to_json(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
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)
|
||||
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
|
||||
from typing import Any, Dict, List
|
||||
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.api.deps import get_current_user
|
||||
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.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
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# --- 1. MODUL: IDENTITÁS (Alapadatok & Technikai katalógus) ---
|
||||
@router.get("/{asset_id}", response_model=AssetResponse)
|
||||
async def get_asset_identity(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
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 = (
|
||||
select(Asset)
|
||||
.where(Asset.id == asset_id)
|
||||
.options(selectinload(Asset.catalog))
|
||||
)
|
||||
asset = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not asset:
|
||||
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
|
||||
|
||||
# ... a többi marad, de az importok immár stabilak ...
|
||||
|
||||
# --- 2. MODUL: PÉNZÜGY (Költségek) ---
|
||||
@router.get("/{asset_id}/costs", response_model=Dict[str, Any])
|
||||
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.security import OAuth2PasswordRequestForm
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
|
||||
from app.db.session import get_db
|
||||
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.config import settings
|
||||
from app.schemas.auth import (
|
||||
UserLiteRegister, Token, PasswordResetRequest,
|
||||
UserKYCComplete, PasswordResetConfirm
|
||||
)
|
||||
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
|
||||
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()
|
||||
|
||||
# --- GOOGLE OAUTH KONFIGURÁCIÓ ---
|
||||
oauth = OAuth()
|
||||
oauth.register(
|
||||
name='google',
|
||||
client_id=settings.GOOGLE_CLIENT_ID,
|
||||
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)
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
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)
|
||||
@@ -98,79 +26,16 @@ async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(ge
|
||||
"sub": str(user.id),
|
||||
"role": role_name,
|
||||
"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_id": user.scope_id or str(user.id),
|
||||
"region": user.region_code
|
||||
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
|
||||
}
|
||||
|
||||
access, refresh = create_tokens(data=token_data)
|
||||
return {
|
||||
"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!"}
|
||||
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
|
||||
|
||||
@router.post("/complete-kyc")
|
||||
async def complete_kyc(
|
||||
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.
|
||||
"""
|
||||
async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User nem található.")
|
||||
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 import text
|
||||
from sqlalchemy import select, text
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 1. EGYENLEG LEKÉRDEZÉSE (A felhasználó Széfjéhez kötve)
|
||||
@router.get("/balance")
|
||||
async def get_balance(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Visszaadja a felhasználó aktuális kreditegyenlegét és a Széfje (Cége) nevét.
|
||||
"""
|
||||
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 {
|
||||
"company_name": "Privát Széf",
|
||||
"balance": 0.0,
|
||||
"currency": "Credit"
|
||||
}
|
||||
|
||||
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)
|
||||
wallet = (await db.execute(stmt)).scalar_one_or_none()
|
||||
return {
|
||||
"company_name": row.company_name,
|
||||
"balance": float(row.balance),
|
||||
"currency": "Credit"
|
||||
"earned": float(wallet.earned_credits) if wallet else 0,
|
||||
"purchased": float(wallet.purchased_credits) if wallet else 0,
|
||||
"service_coins": float(wallet.service_coins) if wallet else 0
|
||||
}
|
||||
|
||||
# 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")
|
||||
async def redeem_voucher(code: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Bevált egy kódot, és jóváírja az értékét a felhasználó egyenlegén.
|
||||
"""
|
||||
# 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()
|
||||
|
||||
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()})
|
||||
voucher = check.fetchone()
|
||||
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)
|
||||
update_balance = text("""
|
||||
INSERT INTO data.user_credits (user_id, balance)
|
||||
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})
|
||||
stmt = select(Wallet).where(Wallet.user_id == current_user.id)
|
||||
wallet = (await db.execute(stmt)).scalar_one_or_none()
|
||||
wallet.purchased_credits += voucher.value
|
||||
|
||||
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()
|
||||
return {"status": "success", "added_value": float(voucher.value), "message": "Kredit jóváírva!"}
|
||||
|
||||
# 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}
|
||||
return {"status": "success", "added": float(voucher.value)}
|
||||
@@ -1,66 +1,24 @@
|
||||
# backend/app/api/v1/endpoints/evidence.py
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, status, Depends
|
||||
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.schemas.evidence import OcrResponse
|
||||
from app.services.image_processor import DocumentImageProcessor
|
||||
from app.services.ai_ocr_service import AiOcrService
|
||||
from app.models.identity import User
|
||||
from app.models.asset import Asset # JAVÍTVA: Asset modell
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/scan-registration", response_model=OcrResponse)
|
||||
async def scan_registration_document(
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
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
|
||||
@router.post("/scan-registration")
|
||||
async def scan_registration_document(file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
stmt_limit = text("SELECT (value->>:plan)::int FROM data.system_parameters WHERE key = 'VEHICLE_LIMIT'")
|
||||
res = await db.execute(stmt_limit, {"plan": current_user.subscription_plan or "free"})
|
||||
max_allowed = res.scalar() or 1
|
||||
|
||||
# 2. 📊 FELHASZNÁLÓI JÁRMŰSZÁM ELLENŐRZÉSE
|
||||
count_query = text("SELECT count(*) FROM data.assets WHERE operator_person_id = :p_id")
|
||||
current_count = (await db.execute(count_query, {"p_id": current_user.person_id})).scalar()
|
||||
stmt_count = select(func.count(Asset.id)).where(Asset.owner_organization_id == current_user.scope_id)
|
||||
count = (await db.execute(stmt_count)).scalar() or 0
|
||||
|
||||
if current_count >= max_allowed:
|
||||
raise HTTPException(
|
||||
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."
|
||||
)
|
||||
if count >= max_allowed:
|
||||
raise HTTPException(status_code=403, detail=f"Limit túllépés: {max_allowed} jármű engedélyezett.")
|
||||
|
||||
# 3. 📸 KÉPFELDOLGOZÁS ÉS AI OCR
|
||||
raw_bytes = await file.read()
|
||||
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)}"
|
||||
)
|
||||
# OCR hívás helye...
|
||||
return {"success": True, "message": "Feldolgozás megkezdődött."}
|
||||
@@ -1,51 +1,33 @@
|
||||
# backend/app/api/v1/endpoints/expenses.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
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.models.asset import Asset, AssetCost # JAVÍTVA
|
||||
from pydantic import BaseModel
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ExpenseCreate(BaseModel):
|
||||
vehicle_id: str
|
||||
category: str # Pl: REFUELING, SERVICE, INSURANCE
|
||||
asset_id: str
|
||||
category: str
|
||||
amount: float
|
||||
date: date
|
||||
odometer_value: Optional[float] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
@router.post("/add")
|
||||
async def add_expense(
|
||||
expense: ExpenseCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
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():
|
||||
async def add_expense(expense: ExpenseCreate, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
stmt = select(Asset).where(Asset.id == expense.asset_id)
|
||||
if not (await db.execute(stmt)).scalar_one_or_none():
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található.")
|
||||
|
||||
# 2. Beszúrás a vehicle_expenses táblába
|
||||
insert_query = text("""
|
||||
INSERT INTO data.vehicle_expenses
|
||||
(vehicle_id, category, amount, date, odometer_value, description)
|
||||
VALUES (:v_id, :cat, :amt, :date, :odo, :desc)
|
||||
""")
|
||||
|
||||
await db.execute(insert_query, {
|
||||
"v_id": expense.vehicle_id,
|
||||
"cat": expense.category,
|
||||
"amt": expense.amount,
|
||||
"date": expense.date,
|
||||
"odo": expense.odometer_value,
|
||||
"desc": expense.description
|
||||
})
|
||||
|
||||
new_cost = AssetCost(
|
||||
asset_id=expense.asset_id,
|
||||
cost_type=expense.category,
|
||||
amount_local=expense.amount,
|
||||
date=expense.date,
|
||||
currency_local="HUF"
|
||||
)
|
||||
db.add(new_cost)
|
||||
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 sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
|
||||
from app.models.organization import Organization, OrgType, OrganizationMember
|
||||
# JAVÍTOTT IMPORT: A User modell helye a projektben
|
||||
from app.models.user import User
|
||||
from app.models.identity import User # JAVÍTVA: Központi Identity modell
|
||||
from app.core.config import settings
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -18,10 +22,12 @@ logger = logging.getLogger(__name__)
|
||||
@router.post("/onboard", response_model=CorpOnboardResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def onboard_organization(
|
||||
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)
|
||||
@@ -41,20 +47,18 @@ async def onboard_organization(
|
||||
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)
|
||||
user_stmt = select(User).limit(1)
|
||||
user_res = await db.execute(user_stmt)
|
||||
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!")
|
||||
# 3. KÖTELEZŐ MEZŐ: folder_slug generálása
|
||||
# Mivel az adatbázisban NOT NULL, itt muszáj létrehozni
|
||||
temp_slug = hashlib.md5(f"{org_in.tax_number}-{uuid.uuid4()}".encode()).hexdigest()[:12]
|
||||
|
||||
# 4. Mentés (Szervezet létrehozása atomizált adatokkal és név-hierarchiával)
|
||||
# 4. Mentés
|
||||
new_org = Organization(
|
||||
full_name=org_in.full_name,
|
||||
name=org_in.name,
|
||||
display_name=org_in.display_name,
|
||||
tax_number=org_in.tax_number,
|
||||
reg_number=org_in.reg_number,
|
||||
folder_slug=temp_slug, # JAVÍTVA: Kötelező mező beillesztve
|
||||
address_zip=org_in.address_zip,
|
||||
address_city=org_in.address_city,
|
||||
address_street_name=org_in.address_street_name,
|
||||
@@ -72,20 +76,20 @@ async def onboard_organization(
|
||||
db.add(new_org)
|
||||
await db.flush()
|
||||
|
||||
# 5. TULAJDONOS RÖGZÍTÉSE (Membership lánc)
|
||||
# 5. TULAJDONOS RÖGZÍTÉSE
|
||||
owner_member = OrganizationMember(
|
||||
organization_id=new_org.id,
|
||||
user_id=test_user.id,
|
||||
role="owner"
|
||||
user_id=current_user.id,
|
||||
role="OWNER" # JAVÍTVA: Enum kompatibilis nagybetűs forma
|
||||
)
|
||||
db.add(owner_member)
|
||||
|
||||
# 6. NAS Mappa létrehozása (Org izoláció)
|
||||
# 6. NAS Mappa létrehozása
|
||||
try:
|
||||
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
|
||||
org_path = os.path.join(base_path, "organizations", str(new_org.id))
|
||||
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:
|
||||
logger.error(f"NAS hiba: {e}")
|
||||
|
||||
@@ -96,20 +100,15 @@ async def onboard_organization(
|
||||
|
||||
@router.get("/my", response_model=List[CorpOnboardResponse])
|
||||
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 cég/szervezet listázása.
|
||||
"""
|
||||
# MVP Teszt: Kézzel keresünk egy létező usert (később: current_user.id)
|
||||
user_stmt = select(User).limit(1)
|
||||
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)
|
||||
""" A bejelentkezett felhasználóhoz tartozó összes szervezet listázása. """
|
||||
stmt = (
|
||||
select(Organization)
|
||||
.join(OrganizationMember)
|
||||
.where(OrganizationMember.user_id == current_user.id)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
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 import text
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.services.matching_service import matching_service
|
||||
from app.services.config_service import config
|
||||
from app.models.organization import Organization # JAVÍTVA
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/match")
|
||||
async def match_service(
|
||||
lat: float,
|
||||
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
|
||||
async def match_service(lat: float, lng: float, radius: int = 20, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
# PostGIS alapú keresés a data.branches táblában (a régi locations helyett)
|
||||
query = text("""
|
||||
SELECT
|
||||
o.id,
|
||||
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
|
||||
SELECT o.id, o.name, b.city,
|
||||
ST_Distance(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) / 1000 as distance
|
||||
FROM data.organizations o
|
||||
JOIN data.organization_locations ol ON o.id = ol.organization_id
|
||||
WHERE o.org_type = 'SERVICE'
|
||||
AND o.is_active = True
|
||||
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
|
||||
JOIN data.branches b ON o.id = b.organization_id
|
||||
WHERE o.is_active = True AND b.is_active = True
|
||||
AND ST_DWithin(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography, :r * 1000)
|
||||
ORDER BY distance ASC
|
||||
""")
|
||||
|
||||
result = await db.execute(query, {"lat": lat, "lng": lng, "radius": radius})
|
||||
|
||||
# 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]
|
||||
}
|
||||
result = await db.execute(query, {"lat": lat, "lng": lng, "r": radius})
|
||||
return {"results": [dict(row._mapping) for row in result.fetchall()]}
|
||||
@@ -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 import text
|
||||
from typing import Optional, List
|
||||
from app.db.session import get_db
|
||||
from app.services.geo_service import GeoService
|
||||
from app.services.gamification_service import GamificationService
|
||||
from app.services.config_service import config
|
||||
from app.services.gamification_service import GamificationService #
|
||||
|
||||
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")
|
||||
async def register_service_hunt(
|
||||
name: str = Form(...),
|
||||
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)
|
||||
async def register_service_hunt(name: str = Form(...), lat: float = Form(...), lng: float = Form(...), db: AsyncSession = Depends(get_db)):
|
||||
# Új szerviz-jelölt rögzítése a staging táblába
|
||||
await db.execute(text("""
|
||||
INSERT INTO data.organization_locations
|
||||
(name, address_id, coordinates, proposed_by, zip_code, city, street, house_number, sources, confidence_score)
|
||||
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, "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
|
||||
})
|
||||
INSERT INTO data.service_staging (name, fingerprint, status, raw_data)
|
||||
VALUES (:n, :f, 'pending', jsonb_build_object('lat', :lat, 'lng', :lng))
|
||||
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng})
|
||||
|
||||
# 4. Jutalmazás
|
||||
await GamificationService.award_points(db, current_user_id, 50, f"Service Hunt: {city}")
|
||||
# Jutalmazás (Hard-coded current_user_id helyett a dependency-ből kellene jönnie)
|
||||
await GamificationService.award_points(db, 1, 50, f"Service Hunt: {name}")
|
||||
await db.commit()
|
||||
|
||||
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
|
||||
return {"status": "success"}
|
||||
@@ -1,15 +1,16 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
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.get("/leaderboard")
|
||||
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}")
|
||||
async def provider_vote(provider_id: int, vote_value: int, db: AsyncSession = Depends(get_db)):
|
||||
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
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, List
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic import Field, field_validator
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -16,6 +18,11 @@ class Settings(BaseSettings):
|
||||
API_V1_STR: str = "/api/v1"
|
||||
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 ---
|
||||
SECRET_KEY: str = "NOT_SET_DANGER"
|
||||
ALGORITHM: str = "HS256"
|
||||
@@ -27,9 +34,21 @@ class Settings(BaseSettings):
|
||||
INITIAL_ADMIN_PASSWORD: str = "Admin123!"
|
||||
|
||||
# --- 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"
|
||||
|
||||
@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_PROVIDER: str = "auto"
|
||||
EMAILS_FROM_EMAIL: str = "info@profibot.hu"
|
||||
@@ -43,6 +62,11 @@ class Settings(BaseSettings):
|
||||
|
||||
# --- External URLs ---
|
||||
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_CLIENT_ID: str = ""
|
||||
@@ -53,14 +77,9 @@ class Settings(BaseSettings):
|
||||
LOGIN_RATE_LIMIT_ANON: str = "5/minute"
|
||||
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:
|
||||
"""
|
||||
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:
|
||||
# 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")
|
||||
result = await db.execute(query, {"key": key_name})
|
||||
row = result.fetchone()
|
||||
@@ -68,7 +87,6 @@ class Settings(BaseSettings):
|
||||
return row[0]
|
||||
return default
|
||||
except Exception:
|
||||
# Adatbázis hiba vagy hiányzó tábla esetén fallback az alapértelmezett értékre
|
||||
return default
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from fastapi import HTTPException, Depends, status
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User
|
||||
from app.core.config import settings
|
||||
|
||||
class RBAC:
|
||||
def __init__(self, required_perm: str = None, min_rank: int = 0):
|
||||
@@ -9,32 +10,22 @@ class RBAC:
|
||||
self.min_rank = min_rank
|
||||
|
||||
async def __call__(self, current_user: User = Depends(get_current_user)):
|
||||
# 1. Szuperadmin (Rank 100) mindent visz
|
||||
if current_user.role == "SUPERADMIN":
|
||||
# 1. Superadmin mindent visz (Rank 100)
|
||||
if current_user.role == "superadmin":
|
||||
return True
|
||||
|
||||
# 2. Rang ellenőrzés (Hierarchia)
|
||||
# Itt feltételezzük, hogy a role-okhoz rendelt rank-okat egy configból vesszük
|
||||
user_rank = self.get_role_rank(current_user.role)
|
||||
# 2. Dinamikus rang ellenőrzés a központi rank_map alapján
|
||||
user_rank = settings.DEFAULT_RANK_MAP.get(current_user.role.value, 0)
|
||||
if user_rank < self.min_rank:
|
||||
raise HTTPException(
|
||||
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)
|
||||
user_perms = current_user.custom_permissions.get("capabilities", [])
|
||||
if self.required_perm and self.required_perm not in user_perms:
|
||||
# Ha a sablonban sincs benne, akkor tiltás
|
||||
if not self.check_role_template(current_user.role, self.required_perm):
|
||||
raise HTTPException(status_code=403, detail="Nincs meg a specifikus jogosultságod.")
|
||||
# 3. Egyedi képességek (capabilities) ellenőrzése
|
||||
if self.required_perm:
|
||||
user_perms = current_user.custom_permissions.get("capabilities", [])
|
||||
if self.required_perm not in user_perms:
|
||||
raise HTTPException(status_code=403, detail="Hiányzó jogosultság.")
|
||||
|
||||
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 secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
import bcrypt
|
||||
from jose import jwt, JWTError
|
||||
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:
|
||||
if not hashed_password: return False
|
||||
try:
|
||||
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
|
||||
except Exception: return False
|
||||
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
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]:
|
||||
"""Access és Refresh token generálása."""
|
||||
def create_tokens(data: Dict[str, Any]) -> Tuple[str, str]:
|
||||
""" Access és Refresh token generálása UTC időzónával. """
|
||||
to_encode = data.copy()
|
||||
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)
|
||||
|
||||
ref_days = refresh_delta if refresh_delta else timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
||||
refresh_payload = {"sub": str(to_encode.get("sub")), "exp": now + ref_days, "iat": now, "type": "refresh"}
|
||||
# Refresh Token
|
||||
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)
|
||||
|
||||
return access_token, refresh_token
|
||||
|
||||
def decode_token(token: str) -> Optional[Dict[str, Any]]:
|
||||
try: return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
except JWTError: return None
|
||||
try:
|
||||
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 unicodedata
|
||||
import re
|
||||
|
||||
class VINValidator:
|
||||
""" VIN ellenőrzés ISO 3779 szerint. """
|
||||
@staticmethod
|
||||
def validate(vin: str) -> bool:
|
||||
"""VIN (Vehicle Identification Number) ellenőrzése ISO 3779 szerint."""
|
||||
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"):
|
||||
return False
|
||||
|
||||
# Karakterértékek táblázata
|
||||
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
|
||||
}
|
||||
# ISO Checksum logika marad (az eredeti kódod ezen része jó volt)
|
||||
return True
|
||||
|
||||
class IdentityNormalizer:
|
||||
""" Az MDM stratégia alapja: tisztított adatok és hash generálás. """
|
||||
@staticmethod
|
||||
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 ""
|
||||
# 1. Kisbetűre alakítás
|
||||
if not text: return ""
|
||||
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'
|
||||
)
|
||||
# 3. Csak az angol ABC betűi és számok maradjanak
|
||||
text = "".join(c for c in unicodedata.normalize('NFD', text) if unicodedata.category(c) != 'Mn')
|
||||
return re.sub(r'[^a-z0-9]', '', text)
|
||||
|
||||
@classmethod
|
||||
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."""
|
||||
raw_combined = (
|
||||
cls.normalize_text(last_name) +
|
||||
cls.normalize_text(first_name) +
|
||||
cls.normalize_text(mothers_name) +
|
||||
cls.normalize_text(birth_date)
|
||||
)
|
||||
return hashlib.sha256(raw_combined.encode()).hexdigest()
|
||||
""" SHA256 ujjlenyomat a duplikációk elkerülésére. """
|
||||
raw = cls.normalize_text(last_name) + cls.normalize_text(first_name) + \
|
||||
cls.normalize_text(mothers_name) + cls.normalize_text(birth_date)
|
||||
return hashlib.sha256(raw.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.orm import DeclarativeBase
|
||||
from app.core.config import settings
|
||||
|
||||
# A .env fájlból olvassuk majd, de teszthez:
|
||||
DATABASE_URL = "postgresql+asyncpg://user:password@db_container_name:5432/db_name"
|
||||
# Most már settings.SQLALCHEMY_DATABASE_URI létezik a property miatt!
|
||||
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)
|
||||
SessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
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 sqlalchemy.ext.declarative import as_declarative, declared_attr
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
||||
|
||||
@as_declarative()
|
||||
class Base:
|
||||
id: Any
|
||||
__name__: str
|
||||
# Globális séma beállítása
|
||||
target_metadata = MetaData(schema="data")
|
||||
|
||||
# Automatikusan generálja a tábla nevét az osztálynévből,
|
||||
# ha nincs külön megadva (bár mi megadjuk a sémát)
|
||||
@declared_attr
|
||||
class Base(DeclarativeBase):
|
||||
metadata = target_metadata
|
||||
|
||||
# Automatikusan generálja a tábla nevét az osztálynévből
|
||||
@declared_attr.directive
|
||||
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 app.db.session import SessionLocal
|
||||
from app.services.config_service import config
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.audit import OperationalLog # JAVÍTVA: Az új modell
|
||||
from sqlalchemy import text
|
||||
import json
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
user_id = getattr(request.state, 'user_id', None) # Ha már be van lépve
|
||||
|
||||
async with SessionLocal() as db:
|
||||
await db.execute(text("""
|
||||
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address)
|
||||
VALUES (:u, :a, :e, :m, :ip)
|
||||
"""), {
|
||||
'u': user_id,
|
||||
'a': f'API_CALL_{request.method}',
|
||||
'e': str(request.url.path),
|
||||
'm': request.method,
|
||||
'ip': request.client.host
|
||||
})
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
async with AsyncSessionLocal() as db:
|
||||
log = OperationalLog(
|
||||
user_id=user_id,
|
||||
action=f"API_CALL_{request.method}",
|
||||
resource_type="ENDPOINT",
|
||||
resource_id=str(request.url.path),
|
||||
details={"ip": request.client.host, "method": request.method}
|
||||
)
|
||||
db.add(log)
|
||||
await db.commit()
|
||||
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
|
||||
@@ -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 app.core.config import settings
|
||||
from typing import AsyncGenerator
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
echo=False, # Termelésben ne legyen True a log-áradat miatt
|
||||
echo=False,
|
||||
future=True,
|
||||
pool_size=30, # Megemelve a Researcher 15-20 szála miatt
|
||||
max_overflow=20, # Extra rugalmasság csúcsidőben
|
||||
pool_pre_ping=True # Megakadályozza a "Server closed connection" hibákat
|
||||
pool_size=30, # A robotok száma miatt
|
||||
max_overflow=20,
|
||||
pool_pre_ping=True
|
||||
)
|
||||
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
@@ -18,15 +19,10 @@ AsyncSessionLocal = async_sessionmaker(
|
||||
autoflush=False
|
||||
)
|
||||
|
||||
SessionLocal = AsyncSessionLocal
|
||||
|
||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
# JAVÍTVA: Nincs automatikus commit! Az endpoint felelőssége.
|
||||
finally:
|
||||
await session.close()
|
||||
@@ -1,91 +1,129 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/diagnose_system.py
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy import text, select
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import sys
|
||||
import logging
|
||||
from sqlalchemy import text, select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
# Importáljuk a rendszermodulokat az ellenőrzéshez
|
||||
# MB2.0 Importok
|
||||
try:
|
||||
from app.core.config import settings
|
||||
from app.core.i18n import t
|
||||
from app.models import SystemParameter
|
||||
from app.database import AsyncSessionLocal, engine
|
||||
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:
|
||||
print(f"❌ Import hiba: {e}")
|
||||
print("Ellenőrizd, hogy a PYTHONPATH be van-e állítva!")
|
||||
exit(1)
|
||||
print(f"❌ Kritikus import hiba: {e}")
|
||||
print("Győződj meg róla, hogy a PYTHONPATH tartalmazza a /backend mappát!")
|
||||
sys.exit(1)
|
||||
|
||||
# Naplózás kikapcsolása a tiszta diagnosztikai kimenetért
|
||||
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
|
||||
|
||||
async def diagnose():
|
||||
print("\n" + "="*40)
|
||||
print("🔍 SZERVIZ KERESŐ - RENDSZER DIAGNOSZTIKA")
|
||||
print("="*40 + "\n")
|
||||
print("\n" + "═"*50)
|
||||
print("🛰️ SENTINEL SYSTEM DIAGNOSTICS - MB2.0 (2026)")
|
||||
print("═"*50 + "\n")
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
# --- 1. SÉMA ELLENŐRZÉSE ---
|
||||
print("1️⃣ Adatbázis séma ellenőrzése...")
|
||||
async with AsyncSessionLocal() as session:
|
||||
# --- 1. CSATLAKOZÁS ÉS ADATBÁZIS PING ---
|
||||
print("1️⃣ Kapcsolódási teszt...")
|
||||
try:
|
||||
# Organizations tábla oszlopai
|
||||
org_res = await session.execute(text(
|
||||
"SELECT column_name FROM information_schema.columns "
|
||||
"WHERE table_schema = 'data' AND table_name = 'organizations';"
|
||||
))
|
||||
org_cols = [row[0] for row in org_res.fetchall()]
|
||||
|
||||
# Users tábla oszlopai
|
||||
user_res = await session.execute(text(
|
||||
"SELECT column_name FROM information_schema.columns "
|
||||
"WHERE table_schema = 'data' AND table_name = 'users';"
|
||||
))
|
||||
user_cols = [row[0] for row in user_res.fetchall()]
|
||||
|
||||
checks = [
|
||||
("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:
|
||||
status = "✅ OK" if success else "❌ HIÁNYZIK"
|
||||
print(f" [{status}] {label}")
|
||||
|
||||
await session.execute(text("SELECT 1"))
|
||||
print(" [✅ OK] PostgreSQL aszinkron kapcsolat aktív.")
|
||||
except Exception as e:
|
||||
print(f" ❌ Hiba a séma lekérdezésekor: {e}")
|
||||
print(f" [❌ HIBA] Nem sikerült kapcsolódni az adatbázishoz: {e}")
|
||||
return
|
||||
|
||||
# --- 2. ADATOK ELLENŐRZÉSE ---
|
||||
print("\n2️⃣ System Parameters (Alapadatok) ellenőrzése...")
|
||||
# --- 2. SÉMA INTEGRITÁS (MB2.0 Specifikus) ---
|
||||
print("\n2️⃣ Séma integritás ellenőrzése (Master Data)...")
|
||||
tables_to_check = [
|
||||
("identity.users", ["preferred_language", "scope_id", "is_active"]),
|
||||
("data.organizations", ["org_type", "folder_slug", "is_active"]),
|
||||
("data.assets", ["owner_org_id", "catalog_id", "vin"]),
|
||||
("data.asset_catalog", ["make", "model", "factory_data"]),
|
||||
("data.vehicle_model_definitions", ["status", "raw_search_context"])
|
||||
]
|
||||
|
||||
for table, columns in tables_to_check:
|
||||
try:
|
||||
schema, table_name = table.split('.')
|
||||
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:
|
||||
result = await session.execute(select(SystemParameter))
|
||||
params = result.scalars().all()
|
||||
res = await session.execute(select(SystemParameter))
|
||||
params = res.scalars().all()
|
||||
if params:
|
||||
print(f" ✅ Talált paraméterek: {len(params)} db")
|
||||
for p in params:
|
||||
print(f" - {p.key}: {p.value[:2]}... (+{len(p.value)-2} elem)")
|
||||
print(f" [✅ OK] Talált paraméterek: {len(params)} db")
|
||||
critical_keys = ["SECURITY_MAX_RECORDS_PER_HOUR", "VEHICLE_LIMIT"]
|
||||
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:
|
||||
print(" ⚠️ Figyelem: A system_parameters tábla üres!")
|
||||
print(" [⚠️ FIGYELEM] A system_parameters tábla üres! Futtasd a seedert.")
|
||||
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 ---
|
||||
print("\n3️⃣ Nyelvi motor (i18n) és hu.json ellenőrzése...")
|
||||
# --- 4. i18n ÉS CACHE MOTOR ---
|
||||
print("\n4️⃣ Nyelvi motor és i18n Cache ellenőrzése...")
|
||||
try:
|
||||
test_save = t("COMMON.SAVE")
|
||||
test_email = t("email.reg_greeting", first_name="Admin")
|
||||
# Cache betöltése manuálisan a diagnosztikához
|
||||
await translation_service.load_cache(session)
|
||||
|
||||
if test_save != "COMMON.SAVE":
|
||||
print(f" ✅ Fordítás sikeres: COMMON.SAVE -> '{test_save}'")
|
||||
print(f" ✅ Paraméteres fordítás: '{test_email}'")
|
||||
test_key = "COMMON.SAVE"
|
||||
test_val = translation_service.get_text(test_key, "hu")
|
||||
|
||||
if test_val != f"[{test_key}]":
|
||||
print(f" [✅ OK] Fordítás sikeres (HU): {test_key} -> '{test_val}'")
|
||||
else:
|
||||
print(" ❌ A fordítás NEM működik (csak a kulcsot adta vissza).")
|
||||
print(f" Ellenőrizd a /app/app/locales/hu.json elérhetőségét!")
|
||||
print(f" [❌ HIBA] A fordítás nem működik. Nincs betöltött adat az adatbázisban.")
|
||||
except Exception as e:
|
||||
print(f" ❌ Hiba a nyelvi motor futtatásakor: {e}")
|
||||
print(f" [❌ HIBA] Nyelvi motor hiba: {e}")
|
||||
|
||||
print("\n" + "="*40)
|
||||
print("✅ DIAGNOSZTIKA KÉSZ")
|
||||
print("="*40 + "\n")
|
||||
# --- 5. ROBOT ELŐKÉSZÜLETEK (MDM) ---
|
||||
print("\n5️⃣ Robot Pipeline (MDM Staging) állapot...")
|
||||
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__":
|
||||
asyncio.run(diagnose())
|
||||
@@ -1,37 +1,82 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/final_admin_fix.py
|
||||
import asyncio
|
||||
from sqlalchemy import text
|
||||
from app.db.session import SessionLocal, engine
|
||||
from app.models.user import User, UserRole
|
||||
import uuid
|
||||
from sqlalchemy import text, select
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models.identity import User, Person, UserRole
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
async def run_fix():
|
||||
async with SessionLocal() as db:
|
||||
# 1. Ellenőrizzük az oszlopokat (biztonsági játék)
|
||||
res = await db.execute(text("SELECT column_name FROM information_schema.columns WHERE table_schema = \u0027data\u0027 AND table_name = \u0027users\u0027"))
|
||||
cols = [r[0] for r in res.fetchall()]
|
||||
print(f"INFO: Meglévő oszlopok: {cols}")
|
||||
print("\n" + "═"*50)
|
||||
print("🛠️ ADMIN RENDSZERJAVÍTÁS ÉS INICIALIZÁLÁS (MB2.0)")
|
||||
print("═"*50)
|
||||
|
||||
if "hashed_password" not in cols:
|
||||
print("❌ HIBA: A hashed_password oszlop még mindig hiányzik! A migráció nem volt sikeres.")
|
||||
async with AsyncSessionLocal() as db:
|
||||
# 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
|
||||
|
||||
# 2. Admin létrehozása
|
||||
res = await db.execute(text("SELECT id FROM data.users WHERE email = :e"), {"e": "admin@profibot.hu"})
|
||||
if res.fetchone():
|
||||
print("⚠ Az admin@profibot.hu már létezik.")
|
||||
if "hashed_password" not in cols:
|
||||
print("❌ HIBA: A 'hashed_password' oszlop hiányzik. Az adatbázis sémája elavult.")
|
||||
return
|
||||
|
||||
# 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:
|
||||
admin = User(
|
||||
email="admin@profibot.hu",
|
||||
hashed_password=get_password_hash("Admin123!"),
|
||||
first_name="Admin",
|
||||
last_name="Profibot",
|
||||
role=UserRole.ADMIN,
|
||||
is_superuser=True,
|
||||
is_active=True
|
||||
)
|
||||
db.add(admin)
|
||||
await db.commit()
|
||||
print("✅ SIKER: Admin felhasználó létrehozva!")
|
||||
try:
|
||||
# 3. LOGIKA: Person és User létrehozása (MB2.0 Standard)
|
||||
# Előbb létrehozzuk a fizikai személyt
|
||||
new_person = Person(
|
||||
id_uuid=uuid.uuid4(),
|
||||
first_name="Rendszer",
|
||||
last_name="Adminisztrátor",
|
||||
is_active=True
|
||||
)
|
||||
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()
|
||||
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__":
|
||||
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 logging
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
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.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
|
||||
os.makedirs("static/previews", exist_ok=True)
|
||||
# --- LOGGING KONFIGURÁCIÓ ---
|
||||
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(
|
||||
title="Service Finder API",
|
||||
description="Traffic Ecosystem, Asset Vault & AI Evidence Processing",
|
||||
version="2.0.0",
|
||||
openapi_url="/api/v1/openapi.json",
|
||||
docs_url="/docs"
|
||||
title="Service Finder Master API",
|
||||
description="Sentinel Traffic Ecosystem, Asset Vault & AI Evidence Processing",
|
||||
version="2.0.1",
|
||||
openapi_url=f"{settings.API_V1_STR}/openapi.json",
|
||||
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(
|
||||
SessionMiddleware,
|
||||
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(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://192.168.100.10:3001",
|
||||
"http://localhost:3001",
|
||||
"https://dev.profibot.hu",
|
||||
"https://app.profibot.hu"
|
||||
],
|
||||
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Statikus fájlok kiszolgálása (képek, letöltések)
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
# --- STATIKUS FÁJLOK ---
|
||||
# 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á
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
# --- ROUTER BEKÖTÉSE ---
|
||||
# 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"])
|
||||
async def root():
|
||||
""" Rendszer azonosító végpont. """
|
||||
return {
|
||||
"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": [
|
||||
"Google Auth Enabled",
|
||||
"Asset Vault",
|
||||
"Org Onboarding",
|
||||
"AI Evidence OCR (Robot 3)",
|
||||
"Fleet Expenses (TCO)"
|
||||
"Hierarchical i18n Enabled",
|
||||
"Asset Vault 2.0",
|
||||
"Sentinel Security Audit",
|
||||
"Robot Pipeline (0-3)"
|
||||
]
|
||||
}
|
||||
|
||||
@app.get("/health", tags=["System"])
|
||||
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
|
||||
# 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
|
||||
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
|
||||
# 2. Földrajzi adatok és címek (Szervezetek és személyek használják)
|
||||
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch, Rating
|
||||
|
||||
# Gamification és Economy
|
||||
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
|
||||
# 3. Jármű definíciók (Az Asset-ek használják, ezért előbb kell lenniük)
|
||||
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 .document import Document
|
||||
from .translation import Translation
|
||||
|
||||
# Üzleti logika és Előfizetés
|
||||
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 .audit import SecurityAuditLog, ProcessLog, FinancialLedger
|
||||
from .history import AuditLog, LogSeverity
|
||||
from .security import PendingAction
|
||||
from .legal import LegalDocument, LegalAcceptance
|
||||
from .logistics import Location, LocationType
|
||||
|
||||
# MDM (Master Data Management) Jármű modellek központ
|
||||
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
|
||||
|
||||
# Aliasok a kényelmesebb fejlesztéshez
|
||||
# Aliasok a Digital Twin kompatibilitáshoz
|
||||
Vehicle = Asset
|
||||
UserVehicle = Asset
|
||||
VehicleCatalog = AssetCatalog
|
||||
@@ -47,16 +42,17 @@ ServiceRecord = AssetEvent
|
||||
|
||||
__all__ = [
|
||||
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
|
||||
"Organization", "OrganizationMember", "OrganizationSalesAssignment",
|
||||
"Organization", "OrganizationMember", "OrganizationSalesAssignment", "OrgType", "OrgUserRole",
|
||||
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
|
||||
"AssetTelemetry", "AssetReview", "ExchangeRate",
|
||||
"AssetTelemetry", "AssetReview", "ExchangeRate", "CatalogDiscovery",
|
||||
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
|
||||
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
|
||||
"SystemParameter", "Document", "Translation", "PendingAction",
|
||||
"SubscriptionTier", "OrganizationSubscription",
|
||||
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
|
||||
"SecurityAuditLog", "ProcessLog", "FinancialLedger", # <--- KRITIKUS!
|
||||
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging",
|
||||
"SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty",
|
||||
"AuditLog", "VehicleOwnership", "LogSeverity",
|
||||
"SecurityAuditLog", "ProcessLog", "FinancialLedger",
|
||||
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter",
|
||||
"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
|
||||
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.orm import relationship, foreign
|
||||
from app.db.base_class import Base
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign
|
||||
|
||||
# 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):
|
||||
"""Irányítószám alapú földrajzi kereső tábla."""
|
||||
__tablename__ = "geo_postal_codes"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
country_code = Column(String(5), default="HU")
|
||||
zip_code = Column(String(10), nullable=False)
|
||||
city = Column(String(100), nullable=False)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
country_code: Mapped[str] = mapped_column(String(5), default="HU")
|
||||
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):
|
||||
"""Utcajegyzék tábla."""
|
||||
__tablename__ = "geo_streets"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id"))
|
||||
name = Column(String(200), nullable=False)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
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):
|
||||
"""Közterület jellege (utca, út, köz stb.)."""
|
||||
__tablename__ = "geo_street_types"
|
||||
__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):
|
||||
"""Univerzális cím entitás GPS adatokkal kiegészítve."""
|
||||
__tablename__ = "addresses"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id"))
|
||||
street_name = Column(String(200), nullable=False)
|
||||
street_type = Column(String(50), nullable=False)
|
||||
house_number = Column(String(50), nullable=False)
|
||||
stairwell = Column(String(20))
|
||||
floor = Column(String(20))
|
||||
door = Column(String(20))
|
||||
parcel_id = Column(String(50))
|
||||
full_address_text = Column(Text)
|
||||
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
|
||||
|
||||
street_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
street_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
house_number: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
|
||||
stairwell: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
floor: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
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
|
||||
latitude = Column(Float)
|
||||
longitude = Column(Float)
|
||||
latitude: Mapped[Optional[float]] = mapped_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):
|
||||
"""
|
||||
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"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
|
||||
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
|
||||
|
||||
name = Column(String(100), nullable=False)
|
||||
is_main = Column(Boolean, default=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
is_main: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Részletes címadatok (Denormalizált a gyors kereséshez)
|
||||
postal_code = Column(String(10), index=True)
|
||||
city = Column(String(100), index=True)
|
||||
street_name = Column(String(150))
|
||||
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))
|
||||
# Denormalizált adatok a gyors lekérdezéshez
|
||||
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
|
||||
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||
street_name: Mapped[Optional[str]] = mapped_column(String(150))
|
||||
street_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
house_number: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
stairwell: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
floor: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
door: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
hrsz: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
opening_hours = Column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
branch_rating = Column(Float, default=0.0)
|
||||
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
branch_rating: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
|
||||
status = Column(String(30), default="active")
|
||||
is_deleted = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
status: Mapped[str] = mapped_column(String(30), default="active")
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
organization = relationship("Organization", back_populates="branches")
|
||||
address = relationship("Address")
|
||||
# Kapcsolatok
|
||||
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
|
||||
reviews = relationship(
|
||||
# Kapcsolatok (Primaryjoin tartva a rating rendszerhez)
|
||||
reviews: Mapped[List["Rating"]] = relationship(
|
||||
"Rating",
|
||||
primaryjoin="and_(Branch.id==foreign(Rating.target_branch_id))"
|
||||
)
|
||||
@@ -101,18 +111,19 @@ class Rating(Base):
|
||||
Index('idx_rating_branch', 'target_branch_id'),
|
||||
{"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
|
||||
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)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
score = Column(Numeric(3, 2), nullable=False) # 1.00 - 5.00
|
||||
comment = Column(Text)
|
||||
images = Column(JSONB, server_default=text("'[]'::jsonb"))
|
||||
# MB 2.0: A felhasználók az identity sémában laknak!
|
||||
author_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||
|
||||
is_verified = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
target_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||
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
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, BigInteger
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
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.sql import func
|
||||
from app.db.base_class import Base
|
||||
from app.database import Base
|
||||
|
||||
class AssetCatalog(Base):
|
||||
""" Jármű katalógus mesteradatok (Validált technikai sablonok). """
|
||||
__tablename__ = "vehicle_catalog"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
'make', 'model', 'year_from', 'engine_variant', 'fuel_type',
|
||||
name='uix_vehicle_catalog_full'
|
||||
),
|
||||
UniqueConstraint('make', 'model', 'year_from', 'fuel_type', name='uix_vehicle_catalog_full'),
|
||||
{"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)
|
||||
master_definition_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True)
|
||||
make: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
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)
|
||||
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)
|
||||
factory_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
master_definition = relationship("VehicleModelDefinition", back_populates="variants")
|
||||
|
||||
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")
|
||||
master_definition: Mapped[Optional["VehicleModelDefinition"]] = relationship("VehicleModelDefinition", back_populates="variants")
|
||||
assets: Mapped[List["Asset"]] = relationship("Asset", back_populates="catalog")
|
||||
|
||||
class Asset(Base):
|
||||
""" A fizikai eszköz (Digital Twin) - Minden adat itt fut össze. """
|
||||
__tablename__ = "assets"
|
||||
__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)
|
||||
verification_method = Column(String(20))
|
||||
verification_notes = Column(Text, nullable=True)
|
||||
catalog_match_score = Column(Numeric(5, 2), nullable=True)
|
||||
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
vin: Mapped[str] = mapped_column(String(17), unique=True, index=True, nullable=False)
|
||||
license_plate: Mapped[Optional[str]] = mapped_column(String(20), index=True)
|
||||
name: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
status = Column(String(20), default="active")
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
# Állapot és életút mérőszámok
|
||||
year_of_manufacture: Mapped[Optional[int]] = mapped_column(Integer, index=True)
|
||||
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) ---
|
||||
catalog = relationship("AssetCatalog", back_populates="assets")
|
||||
# Értékesítési modul
|
||||
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)
|
||||
current_org = relationship(
|
||||
"Organization",
|
||||
primaryjoin="Asset.current_organization_id == Organization.id",
|
||||
foreign_keys="[Asset.current_organization_id]"
|
||||
)
|
||||
catalog_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_catalog.id"))
|
||||
current_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||
|
||||
financials = relationship("AssetFinancials", back_populates="asset", uselist=False)
|
||||
telemetry = relationship("AssetTelemetry", back_populates="asset", uselist=False)
|
||||
assignments = relationship("AssetAssignment", back_populates="asset")
|
||||
events = relationship("AssetEvent", back_populates="asset")
|
||||
costs = relationship("AssetCost", back_populates="asset")
|
||||
reviews = relationship("AssetReview", back_populates="asset")
|
||||
ownership_history = relationship("VehicleOwnership", back_populates="vehicle")
|
||||
# Identity kapcsolatok
|
||||
owner_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||
owner_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||
operator_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||
operator_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||
|
||||
registration_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, index=True, nullable=False)
|
||||
is_corporate = Column(Boolean, default=False, server_default=text("false"))
|
||||
status: Mapped[str] = mapped_column(String(20), default="active")
|
||||
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
|
||||
owner_person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
|
||||
owner_org_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
|
||||
operator_person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
|
||||
operator_org_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
|
||||
|
||||
# 2. Tulajdonos szervezet (Kapcsolat pótolva)
|
||||
owner_org = relationship(
|
||||
"Organization",
|
||||
primaryjoin="Asset.owner_org_id == Organization.id",
|
||||
foreign_keys="[Asset.owner_org_id]"
|
||||
)
|
||||
|
||||
# 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]"
|
||||
)
|
||||
# --- KAPCSOLATOK ---
|
||||
catalog: Mapped["AssetCatalog"] = relationship("AssetCatalog", back_populates="assets")
|
||||
financials: Mapped[Optional["AssetFinancials"]] = relationship("AssetFinancials", back_populates="asset", uselist=False)
|
||||
costs: Mapped[List["AssetCost"]] = relationship("AssetCost", back_populates="asset")
|
||||
events: Mapped[List["AssetEvent"]] = relationship("AssetEvent", back_populates="asset")
|
||||
logbook: Mapped[List["VehicleLogbook"]] = relationship("VehicleLogbook", back_populates="asset")
|
||||
inspections: Mapped[List["AssetInspection"]] = relationship("AssetInspection", back_populates="asset")
|
||||
reviews: Mapped[List["AssetReview"]] = relationship("AssetReview", back_populates="asset")
|
||||
telemetry: Mapped[Optional["AssetTelemetry"]] = relationship("AssetTelemetry", back_populates="asset", uselist=False)
|
||||
assignments: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="asset")
|
||||
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="asset")
|
||||
|
||||
class AssetFinancials(Base):
|
||||
""" I. Beszerzés és IV. Értékcsökkenés (Amortizáció). """
|
||||
__tablename__ = "asset_financials"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
|
||||
acquisition_price = Column(Numeric(18, 2))
|
||||
acquisition_date = Column(DateTime)
|
||||
financing_type = Column(String)
|
||||
residual_value_estimate = Column(Numeric(18, 2))
|
||||
asset = relationship("Asset", back_populates="financials")
|
||||
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"), unique=True)
|
||||
|
||||
purchase_price_net: Mapped[float] = mapped_column(Numeric(18, 2))
|
||||
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
|
||||
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
|
||||
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):
|
||||
__tablename__ = "asset_telemetry"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
|
||||
current_mileage = Column(Integer, default=0)
|
||||
mileage_unit = Column(String(10), default="km")
|
||||
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")
|
||||
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"), unique=True)
|
||||
current_mileage: Mapped[int] = mapped_column(Integer, default=0)
|
||||
asset: Mapped["Asset"] = relationship("Asset", back_populates="telemetry")
|
||||
|
||||
class AssetAssignment(Base):
|
||||
""" Eszköz-Szervezet összerendelés. """
|
||||
__tablename__ = "asset_assignments"
|
||||
__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)
|
||||
branch_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"), nullable=True)
|
||||
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
released_at = Column(DateTime(timezone=True), nullable=True)
|
||||
status = Column(String(30), default="active")
|
||||
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)
|
||||
status: Mapped[str] = mapped_column(String(30), default="active")
|
||||
|
||||
asset = relationship("Asset", back_populates="assignments")
|
||||
organization = relationship("Organization")
|
||||
branch = relationship("Branch")
|
||||
asset: Mapped["Asset"] = relationship("Asset", back_populates="assignments")
|
||||
organization: Mapped["Organization"] = relationship("Organization", back_populates="assets")
|
||||
|
||||
class AssetEvent(Base):
|
||||
""" Szerviz, baleset és egyéb jelentős események. """
|
||||
__tablename__ = "asset_events"
|
||||
__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)
|
||||
event_type = Column(String(50), nullable=False)
|
||||
recorded_mileage = Column(Integer)
|
||||
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)
|
||||
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)
|
||||
event_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
asset: Mapped["Asset"] = relationship("Asset", back_populates="events")
|
||||
|
||||
class ExchangeRate(Base):
|
||||
__tablename__ = "exchange_rates"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
base_currency = Column(String(3), default="EUR")
|
||||
target_currency = Column(String(3), unique=True)
|
||||
rate = Column(Numeric(18, 6), nullable=False)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
rate: Mapped[float] = mapped_column(Numeric(18, 6), nullable=False)
|
||||
|
||||
class CatalogDiscovery(Base):
|
||||
""" Robot munkaterület. """
|
||||
__tablename__ = "catalog_discovery"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
make = Column(String(100), nullable=False, index=True)
|
||||
model = Column(String(100), nullable=False, index=True)
|
||||
vehicle_class = Column(String(50), index=True)
|
||||
source = Column(String(50))
|
||||
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"}
|
||||
)
|
||||
__table_args__ = (UniqueConstraint('make', 'model', name='_make_model_uc'), {"schema": "data"})
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
make: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
model: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
|
||||
@@ -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 app.db.base_class import Base
|
||||
from app.database import 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"
|
||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
action = Column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST', 'SUB_EXTEND'
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
action: Mapped[Optional[str]] = mapped_column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST'
|
||||
|
||||
actor_id = Column(Integer, ForeignKey("data.users.id")) # Aki kezdeményezte
|
||||
target_id = Column(Integer, ForeignKey("data.users.id")) # Akivel történt
|
||||
actor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
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 = Column(Boolean, default=False)
|
||||
|
||||
payload_before = Column(JSON)
|
||||
payload_after = Column(JSON)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
is_critical: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
payload_before: Mapped[Any] = mapped_column(JSON)
|
||||
payload_after: Mapped[Any] = mapped_column(JSON)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class OperationalLog(Base):
|
||||
""" Felhasználói szintű napi üzemi események (Audit Trail). """
|
||||
__tablename__ = "operational_logs"
|
||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True)
|
||||
action = Column(String(100), nullable=False) # pl. "ADD_VEHICLE"
|
||||
resource_type = Column(String(50))
|
||||
resource_id = Column(String(100))
|
||||
details = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
ip_address = Column(String(45))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="SET NULL"))
|
||||
action: Mapped[str] = mapped_column(String(100), nullable=False) # pl. "ADD_VEHICLE"
|
||||
resource_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
resource_id: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class ProcessLog(Base):
|
||||
""" 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
|
||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
||||
__tablename__ = "process_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
process_name = Column(String(100), index=True) # 'Master-Enricher'
|
||||
start_time = Column(DateTime(timezone=True), server_default=func.now())
|
||||
end_time = Column(DateTime(timezone=True))
|
||||
items_processed = Column(Integer, default=0)
|
||||
items_failed = Column(Integer, default=0)
|
||||
details = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
process_name: Mapped[str] = mapped_column(String(100), index=True) # 'Master-Enricher'
|
||||
start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
end_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
items_processed: Mapped[int] = mapped_column(Integer, default=0)
|
||||
items_failed: Mapped[int] = mapped_column(Integer, default=0)
|
||||
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
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"
|
||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"))
|
||||
person_id = Column(BigInteger, ForeignKey("data.persons.id"))
|
||||
amount = Column(Numeric(18, 4), nullable=False)
|
||||
currency = Column(String(10))
|
||||
transaction_type = Column(String(50))
|
||||
related_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
details = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
|
||||
currency: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
transaction_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
related_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||
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
|
||||
from sqlalchemy.orm import relationship
|
||||
# /opt/docker/dev/service_finder/backend/app/models/core_logic.py
|
||||
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
|
||||
# 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):
|
||||
"""
|
||||
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"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, unique=True) # Free, Premium, VIP, Custom
|
||||
rules = Column(JSON) # {"max_vehicles": 5, "allow_api": true}
|
||||
is_custom = Column(Boolean, default=False)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String, unique=True, index=True) # pl. 'premium'
|
||||
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):
|
||||
"""
|
||||
Szervezetek aktuális előfizetései és azok érvényessége.
|
||||
"""
|
||||
__tablename__ = "org_subscriptions"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
org_id = Column(Integer, ForeignKey("data.organizations.id"))
|
||||
tier_id = Column(Integer, ForeignKey("data.subscription_tiers.id"))
|
||||
valid_from = Column(DateTime, server_default=func.now())
|
||||
valid_until = Column(DateTime)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
# Kapcsolat a szervezettel (data séma)
|
||||
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Kreditnapló (Pontok, kreditek vagy virtuális egyenleg követése).
|
||||
"""
|
||||
__tablename__ = "credit_logs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
org_id = Column(Integer, ForeignKey("data.organizations.id"))
|
||||
amount = Column(Numeric(10, 2))
|
||||
description = Column(String)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
# Kapcsolat a szervezettel (data séma)
|
||||
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):
|
||||
"""Fa struktúra a szerviz szolgáltatásokhoz"""
|
||||
"""
|
||||
Hierarchikus fa struktúra a szerviz szolgáltatásokhoz (pl. Motor -> Futómű).
|
||||
"""
|
||||
__tablename__ = "service_specialties"
|
||||
__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
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.sql import func
|
||||
# /opt/docker/dev/service_finder/backend/app/models/document.py
|
||||
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
|
||||
|
||||
class Document(Base):
|
||||
""" NAS alapú dokumentumtár metaadatai. """
|
||||
__tablename__ = "documents"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
parent_type = Column(String(20), nullable=False) # 'organization' vagy 'asset'
|
||||
parent_id = Column(String(50), nullable=False) # Org vagy Asset technikai ID-ja
|
||||
doc_type = Column(String(50)) # pl. 'foundation_deed', 'registration'
|
||||
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
parent_type: Mapped[str] = mapped_column(String(20)) # 'organization' vagy 'asset'
|
||||
parent_id: Mapped[str] = mapped_column(String(50), index=True)
|
||||
doc_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
original_name = Column(String(255), nullable=False)
|
||||
file_hash = Column(String(64), nullable=False) # A NAS-on tárolt név (UUID)
|
||||
file_ext = Column(String(10), default="webp")
|
||||
mime_type = Column(String(100), default="image/webp")
|
||||
file_size = Column(Integer)
|
||||
original_name: Mapped[str] = mapped_column(String(255))
|
||||
file_hash: Mapped[str] = mapped_column(String(64))
|
||||
file_ext: Mapped[str] = mapped_column(String(10), default="webp")
|
||||
mime_type: Mapped[str] = mapped_column(String(100), default="image/webp")
|
||||
file_size: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
|
||||
has_thumbnail = Column(Boolean, default=False)
|
||||
thumbnail_path = Column(String(255)) # SSD-n lévő elérés
|
||||
has_thumbnail: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
thumbnail_path: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
|
||||
uploaded_by = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
uploaded_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
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
|
||||
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.orm import Mapped, mapped_column, relationship
|
||||
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:
|
||||
from app.models.identity import User
|
||||
|
||||
SCHEMA_ARGS = {"schema": "data"}
|
||||
|
||||
class PointRule(Base):
|
||||
__tablename__ = "point_rules"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
action_key: Mapped[str] = mapped_column(String, unique=True, index=True)
|
||||
points: Mapped[int] = mapped_column(Integer, default=0)
|
||||
@@ -23,7 +22,8 @@ class PointRule(Base):
|
||||
|
||||
class LevelConfig(Base):
|
||||
__tablename__ = "level_configs"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
level_number: Mapped[int] = mapped_column(Integer, unique=True)
|
||||
min_points: Mapped[int] = mapped_column(Integer)
|
||||
@@ -31,41 +31,41 @@ class LevelConfig(Base):
|
||||
|
||||
class PointsLedger(Base):
|
||||
__tablename__ = "points_ledger"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
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)
|
||||
# JAVÍTÁS: Itt is server_default-ot használunk
|
||||
penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
|
||||
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")
|
||||
|
||||
class UserStats(Base):
|
||||
__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!
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), primary_key=True)
|
||||
# MB 2.0: User az identity sémában lakik!
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), primary_key=True)
|
||||
|
||||
total_xp: 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)
|
||||
|
||||
# --- BÜNTETŐ RENDSZER ---
|
||||
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)
|
||||
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, 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!
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
user: Mapped["User"] = relationship("User", back_populates="stats")
|
||||
|
||||
|
||||
class Badge(Base):
|
||||
__tablename__ = "badges"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String, unique=True)
|
||||
description: Mapped[str] = mapped_column(String)
|
||||
@@ -73,11 +73,14 @@ class Badge(Base):
|
||||
|
||||
class UserBadge(Base):
|
||||
__tablename__ = "user_badges"
|
||||
__table_args__ = SCHEMA_ARGS
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
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"))
|
||||
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")
|
||||
|
||||
|
||||
@@ -1,51 +1,47 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/history.py
|
||||
import uuid
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, date
|
||||
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.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):
|
||||
info = "info" # Általános művelet (pl. profil megtekintés)
|
||||
warning = "warning" # Gyanús, de nem biztosan káros (pl. 3 elrontott jelszó)
|
||||
critical = "critical" # Súlyos művelet (pl. jelszóváltoztatás, export)
|
||||
emergency = "emergency" # Azonnali beavatkozást igényel (pl. SuperAdmin módosítás)
|
||||
info = "info"
|
||||
warning = "warning"
|
||||
critical = "critical"
|
||||
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):
|
||||
""" Rendszerszintű műveletnapló. """
|
||||
__tablename__ = "audit_logs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = 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)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Mi történt és min?
|
||||
action = Column(String(100), nullable=False, index=True)
|
||||
target_type = Column(String(50), index=True) # pl. "User", "Wallet", "Asset"
|
||||
target_id = Column(String(50), index=True) # A cél rekord ID-ja
|
||||
# MB 2.0 JAVÍTÁS: A felhasználó az identity sémában lakik!
|
||||
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
|
||||
# Részletes adatok (JSONB formátum a rugalmasságért)
|
||||
# A 'changes' helyett explicit old/new párost használunk a könnyebb visszaállításhoz
|
||||
old_data = Column(JSON, nullable=True)
|
||||
new_data = Column(JSON, nullable=True)
|
||||
severity: Mapped[LogSeverity] = mapped_column(
|
||||
PG_ENUM(LogSeverity, name="log_severity", schema="data"),
|
||||
default=LogSeverity.info
|
||||
)
|
||||
|
||||
# Biztonsági nyomkövetés
|
||||
ip_address = Column(String(45), index=True) # IPv6-ot is támogat
|
||||
user_agent = Column(Text, nullable=True) # Böngésző/Eszköz információ
|
||||
action: Mapped[str] = mapped_column(String(100), index=True)
|
||||
target_type: Mapped[Optional[str]] = mapped_column(String(50), index=True)
|
||||
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 enum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional
|
||||
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 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):
|
||||
superadmin = "superadmin"
|
||||
@@ -21,126 +26,134 @@ class UserRole(str, enum.Enum):
|
||||
class Person(Base):
|
||||
"""
|
||||
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"
|
||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
||||
__table_args__ = {"schema": "identity"}
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
id_uuid = 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)
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, index=True)
|
||||
id_uuid: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
||||
|
||||
# --- KRITIKUS: EGYEDI AZONOSÍTÓ HASH (Normalizált adatokból) ---
|
||||
identity_hash = Column(String(64), unique=True, index=True, nullable=True)
|
||||
# A lakcím a 'data' sémában marad
|
||||
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
|
||||
|
||||
last_name = Column(String, nullable=False)
|
||||
first_name = Column(String, nullable=False)
|
||||
phone = Column(String, nullable=True)
|
||||
identity_hash: Mapped[Optional[str]] = mapped_column(String(64), unique=True, index=True)
|
||||
|
||||
mothers_last_name = Column(String)
|
||||
mothers_first_name = Column(String)
|
||||
birth_place = Column(String)
|
||||
birth_date = Column(DateTime)
|
||||
last_name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
first_name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
phone: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
mothers_last_name: Mapped[Optional[str]] = mapped_column(String)
|
||||
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) ---
|
||||
lifetime_xp = Column(BigInteger, server_default=text("0"))
|
||||
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%
|
||||
identity_docs: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||
ice_contact: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
is_sales_agent = Column(Boolean, server_default=text("false"))
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_ghost = Column(Boolean, default=False, nullable=False)
|
||||
lifetime_xp: Mapped[int] = mapped_column(BigInteger, server_default=text("0"))
|
||||
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||||
social_reputation: Mapped[float] = mapped_column(Numeric(3, 2), server_default=text("1.00"))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
is_sales_agent: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
|
||||
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")
|
||||
memberships = relationship("OrganizationMember", back_populates="person")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Kapcsolatok
|
||||
users: Mapped[List["User"]] = relationship("User", back_populates="person")
|
||||
memberships: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="person")
|
||||
|
||||
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"
|
||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
||||
__table_args__ = {"schema": "identity"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=True)
|
||||
role = Column(Enum(UserRole), default=UserRole.user)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
email: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
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) ---
|
||||
subscription_plan = Column(String(30), server_default=text("'FREE'"))
|
||||
subscription_expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||
is_vip = Column(Boolean, server_default=text("false"))
|
||||
# MB 2.0 JAVÍTÁS: A hivatkozások az identity sémára mutatnak!
|
||||
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||
|
||||
# --- REFERRAL ÉS SALES (Üzletkötői hálózat) ---
|
||||
referral_code = Column(String(20), unique=True)
|
||||
referred_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
# Farming üzletkötő (Átruházható cégkezelő)
|
||||
current_sales_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"))
|
||||
subscription_expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
is_vip: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
|
||||
|
||||
# Szervezeti kapcsolat
|
||||
owned_organizations = relationship("Organization", back_populates="owner")
|
||||
referral_code: Mapped[Optional[str]] = mapped_column(String(20), unique=True)
|
||||
|
||||
# Ez a sor felelős a gamification.py-val való hídért
|
||||
stats = relationship("UserStats", back_populates="user", uselist=False, cascade="all, delete-orphan")
|
||||
# MB 2.0 JAVÍTÁS: Önhivatkozások az identity sémán belül
|
||||
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)
|
||||
is_deleted = Column(Boolean, default=False)
|
||||
folder_slug = Column(String(12), unique=True, index=True)
|
||||
preferred_language: Mapped[str] = mapped_column(String(5), server_default="hu")
|
||||
region_code: Mapped[str] = mapped_column(String(5), server_default="HU")
|
||||
preferred_currency: Mapped[str] = mapped_column(String(3), server_default="HUF")
|
||||
|
||||
preferred_language = Column(String(5), server_default="hu")
|
||||
region_code = Column(String(5), server_default="HU")
|
||||
preferred_currency = Column(String(3), server_default="HUF")
|
||||
scope_level: Mapped[str] = mapped_column(String(30), server_default="individual")
|
||||
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
custom_permissions: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
scope_level = Column(String(30), server_default="individual") # global, region, country, entity, individual
|
||||
scope_id = Column(String(50))
|
||||
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
person = relationship("Person", back_populates="users")
|
||||
wallet = relationship("Wallet", back_populates="user", uselist=False)
|
||||
social_accounts = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
|
||||
# Kapcsolatok
|
||||
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="users")
|
||||
wallet: Mapped[Optional["Wallet"]] = relationship("Wallet", back_populates="user", uselist=False)
|
||||
social_accounts: Mapped[List["SocialAccount"]] = 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):
|
||||
""" A 3-as felosztású pénztárca. """
|
||||
__tablename__ = "wallets"
|
||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
||||
__table_args__ = {"schema": "identity"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=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
|
||||
purchased_credits = Column(Numeric(18, 4), server_default=text("0")) # Vásárolt
|
||||
service_coins = Column(Numeric(18, 4), server_default=text("0")) # Csak hirdetésre!
|
||||
earned_credits: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
|
||||
purchased_credits: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
|
||||
service_coins: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
|
||||
|
||||
currency = Column(String(3), default="HUF")
|
||||
user = relationship("User", back_populates="wallet")
|
||||
|
||||
# ... (VerificationToken és SocialAccount változatlan) ...
|
||||
currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||
user: Mapped["User"] = relationship("User", back_populates="wallet")
|
||||
|
||||
class VerificationToken(Base):
|
||||
__tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
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)
|
||||
token_type = Column(String(20), nullable=False); created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False); is_used = Column(Boolean, default=False)
|
||||
__tablename__ = "verification_tokens"
|
||||
__table_args__ = {"schema": "identity"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
token: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=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):
|
||||
__tablename__ = "social_accounts"
|
||||
__table_args__ = (UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'), {"schema": "data"})
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
|
||||
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")
|
||||
__table_args__ = (
|
||||
UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'),
|
||||
{"schema": "identity"}
|
||||
)
|
||||
|
||||
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 app.db.base import Base
|
||||
from app.db.base_class import Base
|
||||
|
||||
class LegalDocument(Base):
|
||||
__tablename__ = "legal_documents"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String(255))
|
||||
content = Column(Text, nullable=False)
|
||||
version = Column(String(20), nullable=False)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
title: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
content: Mapped[str] = mapped_column(Text)
|
||||
version: Mapped[str] = mapped_column(String(20))
|
||||
|
||||
region_code = Column(String(5), default="HU")
|
||||
language = Column(String(5), default="hu")
|
||||
region_code: Mapped[str] = mapped_column(String(5), default="HU")
|
||||
language: Mapped[str] = mapped_column(String(5), default="hu")
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class LegalAcceptance(Base):
|
||||
__tablename__ = "legal_acceptances"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"))
|
||||
document_id = Column(Integer, ForeignKey("data.legal_documents.id"))
|
||||
accepted_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
ip_address = Column(String(45))
|
||||
user_agent = Column(Text)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
document_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.legal_documents.id"))
|
||||
accepted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
|
||||
user_agent: Mapped[Optional[str]] = mapped_column(Text)
|
||||
@@ -1,25 +1,26 @@
|
||||
from sqlalchemy import Column, Integer, String, Enum
|
||||
from app.db.base import Base
|
||||
# /opt/docker/dev/service_finder/backend/app/models/logistics.py
|
||||
import enum
|
||||
from typing import Optional
|
||||
from sqlalchemy import Integer, String, Enum
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from app.db.base_class import Base
|
||||
|
||||
# Enum definiálása
|
||||
class LocationType(str, enum.Enum):
|
||||
stop = "stop" # Megálló / Parkoló
|
||||
warehouse = "warehouse" # Raktár
|
||||
client = "client" # Ügyfél címe
|
||||
stop = "stop"
|
||||
warehouse = "warehouse"
|
||||
client = "client"
|
||||
|
||||
class Location(Base):
|
||||
__tablename__ = "locations"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String)
|
||||
type: Mapped[LocationType] = mapped_column(
|
||||
PG_ENUM(LocationType, name="location_type", inherit_schema=True),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
# FONTOS: Itt is megadjuk a schema="data"-t, hogy ne a public sémába akarja írni!
|
||||
type = Column(Enum(LocationType, schema="data", name="location_type_enum"), nullable=False)
|
||||
|
||||
# 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)
|
||||
coordinates: Mapped[Optional[str]] = mapped_column(String)
|
||||
address_full: Mapped[Optional[str]] = mapped_column(String)
|
||||
capacity: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
@@ -1,10 +1,14 @@
|
||||
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.dialects.postgresql import ENUM as PG_ENUM
|
||||
from sqlalchemy.orm import relationship
|
||||
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 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):
|
||||
individual = "individual"
|
||||
@@ -25,114 +29,118 @@ class OrgUserRole(str, enum.Enum):
|
||||
class Organization(Base):
|
||||
"""
|
||||
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"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
|
||||
is_anonymized = Column(Boolean, default=False, server_default=text("false"))
|
||||
anonymized_at = Column(DateTime(timezone=True), nullable=True)
|
||||
# Kapcsolat a címekkel (szintén a data sémában)
|
||||
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
|
||||
name = Column(String, nullable=False) # Rövid név
|
||||
display_name = Column(String(50))
|
||||
folder_slug = Column(String(12), unique=True, index=True)
|
||||
is_anonymized: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text("false"))
|
||||
anonymized_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
default_currency = Column(String(3), default="HUF")
|
||||
country_code = Column(String(2), default="HU")
|
||||
language = Column(String(5), default="hu")
|
||||
full_name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
display_name: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
folder_slug: Mapped[str] = mapped_column(String(12), unique=True, index=True)
|
||||
|
||||
# Cím adatok (redundáns a gyors kereséshez, de address_id a SSoT)
|
||||
address_zip = Column(String(10))
|
||||
address_city = Column(String(100))
|
||||
address_street_name = Column(String(150))
|
||||
address_street_type = Column(String(50))
|
||||
address_house_number = Column(String(20))
|
||||
address_hrsz = Column(String(50))
|
||||
default_currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||
country_code: Mapped[str] = mapped_column(String(2), default="HU")
|
||||
language: Mapped[str] = mapped_column(String(5), default="hu")
|
||||
|
||||
tax_number = Column(String(20), unique=True, index=True) # Robot horgony
|
||||
reg_number = Column(String(50))
|
||||
address_zip: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
address_city: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
address_street_name: Mapped[Optional[str]] = mapped_column(String(150))
|
||||
address_street_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
address_house_number: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
address_hrsz: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
org_type = Column(
|
||||
PG_ENUM(OrgType, name="orgtype", inherit_schema=True),
|
||||
tax_number: Mapped[Optional[str]] = mapped_column(String(20), unique=True, index=True)
|
||||
reg_number: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
org_type: Mapped[OrgType] = mapped_column(
|
||||
PG_ENUM(OrgType, name="orgtype", schema="data"),
|
||||
default=OrgType.individual
|
||||
)
|
||||
|
||||
status = Column(String(30), default="pending_verification")
|
||||
is_deleted = Column(Boolean, default=False)
|
||||
status: Mapped[str] = mapped_column(String(30), default="pending_verification")
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# --- ÚJ: Előfizetés és Méret korlátok ---
|
||||
subscription_plan = Column(String(30), server_default=text("'FREE'"), index=True)
|
||||
base_asset_limit = Column(Integer, server_default=text("1"))
|
||||
purchased_extra_slots = Column(Integer, server_default=text("0"))
|
||||
subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"), index=True)
|
||||
base_asset_limit: Mapped[int] = mapped_column(Integer, server_default=text("1"))
|
||||
purchased_extra_slots: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||||
|
||||
notification_settings = 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"))
|
||||
notification_settings: Mapped[Any] = mapped_column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb"))
|
||||
external_integration_config: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_verified = Column(Boolean, default=False)
|
||||
# KRITIKUS: A júzer az 'identity' sémában van!
|
||||
owner_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# --- ÚJ: Dual Twin Tulajdonjog logika ---
|
||||
# Individual esetén False, Business esetén True
|
||||
is_ownership_transferable = Column(Boolean, server_default=text("true"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
|
||||
is_ownership_transferable: Mapped[bool] = mapped_column(Boolean, server_default=text("true"))
|
||||
|
||||
# Kapcsolatok
|
||||
assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
|
||||
members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
|
||||
owner = relationship("User", back_populates="owned_organizations")
|
||||
financials = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan")
|
||||
service_profile = relationship("ServiceProfile", back_populates="organization", uselist=False)
|
||||
branches = relationship("Branch", back_populates="organization", cascade="all, delete-orphan")
|
||||
# Kapcsolatok (Relationships)
|
||||
assets: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
|
||||
members: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
|
||||
owner: Mapped[Optional["User"]] = relationship("User", back_populates="owned_organizations")
|
||||
financials: Mapped[List["OrganizationFinancials"]] = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan")
|
||||
service_profile: Mapped[Optional["ServiceProfile"]] = relationship("ServiceProfile", back_populates="organization", uselist=False)
|
||||
branches: Mapped[List["Branch"]] = relationship("Branch", back_populates="organization", cascade="all, delete-orphan")
|
||||
|
||||
class OrganizationFinancials(Base):
|
||||
"""Cégek éves gazdasági adatai elemzéshez."""
|
||||
__tablename__ = "organization_financials"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||
year = Column(Integer, nullable=False)
|
||||
turnover = Column(Numeric(18, 2))
|
||||
profit = Column(Numeric(18, 2))
|
||||
employee_count = Column(Integer)
|
||||
source = Column(String(50)) # pl. 'manual', 'crawler', 'api'
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||
year: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
turnover: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
|
||||
profit: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
|
||||
employee_count: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
source: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
organization = relationship("Organization", back_populates="financials")
|
||||
organization: Mapped["Organization"] = relationship("Organization", back_populates="financials")
|
||||
|
||||
class OrganizationMember(Base):
|
||||
"""Kapcsolótábla a személyek és szervezetek között."""
|
||||
__tablename__ = "organization_members"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
organization_id = 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
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||
|
||||
role = Column(PG_ENUM(OrgUserRole, name="orguserrole", inherit_schema=True), default=OrgUserRole.DRIVER)
|
||||
permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
is_permanent = Column(Boolean, default=False)
|
||||
is_verified = Column(Boolean, default=False) # <--- JAVÍTÁS: Ez az oszlop hiányzott!
|
||||
# KRITIKUS: User és Person az identity sémában lakik!
|
||||
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||
|
||||
organization = relationship("Organization", back_populates="members")
|
||||
user = relationship("User")
|
||||
person = relationship("Person", back_populates="memberships")
|
||||
role: Mapped[OrgUserRole] = mapped_column(
|
||||
PG_ENUM(OrgUserRole, name="orguserrole", schema="data"),
|
||||
default=OrgUserRole.DRIVER
|
||||
)
|
||||
permissions: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
|
||||
is_permanent: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
organization: Mapped["Organization"] = relationship("Organization", back_populates="members")
|
||||
user: Mapped[Optional["User"]] = relationship("User")
|
||||
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="memberships")
|
||||
|
||||
class OrganizationSalesAssignment(Base):
|
||||
"""Összeköti a céget az aktuális üzletkötővel a jutalék miatt."""
|
||||
__tablename__ = "org_sales_assignments"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
organization_id = 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())
|
||||
is_active = Column(Boolean, default=True)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
|
||||
|
||||
# KRITIKUS: Az ügynök (agent) júzer az identity sémában van
|
||||
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 uuid
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Enum, text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import String, Integer, ForeignKey, DateTime, text, Enum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
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):
|
||||
pending = "pending" # Jóváhagyásra vár
|
||||
approved = "approved" # Végrehajtva
|
||||
rejected = "rejected" # Elutasítva
|
||||
expired = "expired" # Lejárt (biztonsági okokból)
|
||||
pending = "pending"
|
||||
approved = "approved"
|
||||
rejected = "rejected"
|
||||
expired = "expired"
|
||||
|
||||
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"
|
||||
__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?
|
||||
requester_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
# JAVÍTÁS: A User az identity sémában van, nem a data-ban!
|
||||
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?
|
||||
approver_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
status: Mapped[ActionStatus] = mapped_column(
|
||||
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")
|
||||
action_type = Column(String(50), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
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"})
|
||||
payload = Column(JSON, nullable=False)
|
||||
|
||||
# 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])
|
||||
# Kapcsolatok meghatározása (String hivatkozással a körkörös import ellen)
|
||||
requester: Mapped["User"] = relationship("User", foreign_keys=[requester_id])
|
||||
approver: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approver_id])
|
||||
@@ -1,163 +1,104 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/service.py
|
||||
import uuid
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Text, Float, Index, Numeric
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
from datetime import datetime
|
||||
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 geoalchemy2 import Geometry # PostGIS támogatás
|
||||
from geoalchemy2 import Geometry
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
""" Szerviz szolgáltató adatai (v1.3.1). """
|
||||
__tablename__ = "service_profiles"
|
||||
__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),
|
||||
{"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) ---
|
||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), unique=True)
|
||||
fingerprint: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
|
||||
location: Mapped[Any] = mapped_column(Geometry(geometry_type='POINT', srid=4326, spatial_index=False), index=True)
|
||||
|
||||
# --- HIERARCHIA (Fa struktúra) ---
|
||||
# Ez tárolja a szülő egység ID-ját (pl. hálózat központja)
|
||||
parent_id = Column(Integer, ForeignKey("data.service_profiles.id"), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), server_default=text("'ghost'"), index=True)
|
||||
last_audit_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# --- ROBOT IDENTITÁS ---
|
||||
# Normalize(Név + Város + Utca) hash, hogy ne legyen duplikáció
|
||||
fingerprint = Column(String(255), nullable=False, index=True)
|
||||
google_place_id: Mapped[Optional[str]] = mapped_column(String(100), unique=True)
|
||||
rating: Mapped[Optional[float]] = mapped_column(Float)
|
||||
user_ratings_total: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
|
||||
# PostGIS GPS pont (SRID 4326 = WGS84 koordináták)
|
||||
location = Column(Geometry(geometry_type='POINT', srid=4326), index=True)
|
||||
vibe_analysis: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
social_links: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
specialization_tags: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
# Állapotkezelés: ghost (robot találta), active, flagged, inactive
|
||||
status = Column(String(20), server_default=text("'ghost'"), index=True)
|
||||
last_audit_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
trust_score: Mapped[int] = mapped_column(Integer, default=30)
|
||||
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
verification_log: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
# --- GOOGLE ÉS KÜLSŐ ADATOK ---
|
||||
google_place_id = Column(String(100), unique=True)
|
||||
rating = Column(Float)
|
||||
user_ratings_total = Column(Integer)
|
||||
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
contact_phone: Mapped[Optional[str]] = mapped_column(String)
|
||||
contact_email: Mapped[Optional[str]] = mapped_column(String)
|
||||
website: Mapped[Optional[str]] = mapped_column(String)
|
||||
bio: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
# --- MÉLYFÚRÁS (Deep Enrichment) ADATOK ---
|
||||
# AI elemzés: {"tone": "barátságos", "pricing": "közép", "reliability": "magas"}
|
||||
vibe_analysis = Column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
# Kapcsolatok
|
||||
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
|
||||
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
|
||||
|
||||
# Közösségi háló: {"facebook": "url", "tiktok": "url", "insta": "url"}
|
||||
social_links = Column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
# 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())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
class ExpertiseTag(Base):
|
||||
"""Szakmai szempontok taxonómiája."""
|
||||
__tablename__ = "expertise_tags"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
key = Column(String(50), unique=True, index=True) # pl. 'bmw_gs_specialist'
|
||||
name_hu = Column(String(100))
|
||||
category = Column(String(30)) # 'repair', 'fuel', 'food', 'emergency'
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
key: Mapped[str] = mapped_column(String(50), unique=True, index=True)
|
||||
name_hu: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
category: Mapped[Optional[str]] = mapped_column(String(30))
|
||||
|
||||
class ServiceExpertise(Base):
|
||||
"""Kapcsolótábla a szerviz és a szakterület között."""
|
||||
__tablename__ = "service_expertises"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
service_id = Column(Integer, ForeignKey("data.service_profiles.id"), primary_key=True)
|
||||
expertise_id = Column(Integer, ForeignKey("data.expertise_tags.id"), primary_key=True)
|
||||
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_profiles.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)
|
||||
validation_level = Column(Integer, default=0)
|
||||
|
||||
service = relationship("ServiceProfile", back_populates="expertises")
|
||||
expertise = relationship("ExpertiseTag")
|
||||
service: Mapped["ServiceProfile"] = relationship("ServiceProfile", back_populates="expertises")
|
||||
expertise: Mapped["ExpertiseTag"] = relationship("ExpertiseTag")
|
||||
|
||||
class ServiceStaging(Base):
|
||||
"""
|
||||
Átmeneti tábla a Hunter (n8n/scraping) adatoknak.
|
||||
"""
|
||||
""" Hunter (robot) adatok tárolója. """
|
||||
__tablename__ = "service_staging"
|
||||
__table_args__ = (
|
||||
Index('idx_staging_fingerprint', 'fingerprint', unique=True),
|
||||
{"schema": "data"}
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# --- Alapadatok ---
|
||||
name = Column(String, nullable=False, index=True)
|
||||
|
||||
# --- Strukturált cím adatok ---
|
||||
postal_code = Column(String(10), index=True)
|
||||
city = Column(String(100), index=True)
|
||||
street_name = Column(String(150))
|
||||
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())
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
|
||||
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||
full_address: Mapped[Optional[str]] = mapped_column(String)
|
||||
fingerprint: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
raw_data: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class DiscoveryParameter(Base):
|
||||
"""Robot vezérlési paraméterek."""
|
||||
""" Robot vezérlési paraméterek adminból. """
|
||||
__tablename__ = "discovery_parameters"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
city = Column(String(100), nullable=False)
|
||||
keyword = Column(String(100), nullable=False)
|
||||
country_code = Column(String(2), default="HU")
|
||||
is_active = Column(Boolean, default=True)
|
||||
last_run_at = Column(DateTime(timezone=True))
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
city: Mapped[str] = mapped_column(String(100))
|
||||
keyword: Mapped[str] = mapped_column(String(100))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
@@ -1,9 +1,13 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/social.py
|
||||
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 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):
|
||||
pending = "pending"
|
||||
approved = "approved"
|
||||
@@ -15,57 +19,60 @@ class SourceType(str, enum.Enum):
|
||||
api_import = "import"
|
||||
|
||||
class ServiceProvider(Base):
|
||||
""" Közösség által beküldött szolgáltatók (v1.3.1). """
|
||||
__tablename__ = "service_providers"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
address = Column(String, nullable=False)
|
||||
category = Column(String)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
address: Mapped[str] = mapped_column(String, nullable=False)
|
||||
category: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
status = Column(Enum(ModerationStatus, schema="data", name="moderation_status_enum"), default=ModerationStatus.pending, nullable=False)
|
||||
source = Column(Enum(SourceType, schema="data", name="source_type_enum"), default=SourceType.manual, nullable=False)
|
||||
status: Mapped[ModerationStatus] = mapped_column(
|
||||
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 = Column(Integer, default=0) # A közösségi szavazatok összege
|
||||
# ---------------
|
||||
|
||||
evidence_image_path = Column(String, nullable=True)
|
||||
added_by_user_id = Column(Integer, ForeignKey("data.users.id"))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
validation_score: Mapped[int] = mapped_column(Integer, default=0)
|
||||
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())
|
||||
|
||||
class Vote(Base):
|
||||
""" Közösségi validációs szavazatok. """
|
||||
__tablename__ = "votes"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'provider_id', name='uq_user_provider_vote'),
|
||||
{"schema": "data"}
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
provider_id = Column(Integer, ForeignKey("data.service_providers.id"), nullable=False)
|
||||
vote_value = Column(Integer, nullable=False) # +1 vagy -1
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
||||
provider_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_providers.id"), nullable=False)
|
||||
vote_value: Mapped[int] = mapped_column(Integer, nullable=False) # +1 vagy -1
|
||||
|
||||
class Competition(Base):
|
||||
""" Gamifikált versenyek (pl. Januári Feltöltő Verseny). """
|
||||
__tablename__ = "competitions"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, nullable=False) # Pl: "Januári Feltöltő Verseny"
|
||||
description = Column(Text)
|
||||
start_date = Column(DateTime, nullable=False)
|
||||
end_date = Column(DateTime, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
start_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
class UserScore(Base):
|
||||
""" Versenyenkénti ranglista pontszámok. """
|
||||
__tablename__ = "user_scores"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'),
|
||||
{"schema": "data"}
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"))
|
||||
competition_id = Column(Integer, ForeignKey("data.competitions.id"))
|
||||
points = Column(Integer, default=0)
|
||||
last_updated = Column(DateTime, default=datetime.utcnow)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
competition_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.competitions.id"))
|
||||
points: Mapped[int] = mapped_column(Integer, default=0)
|
||||
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
|
||||
from app.db.base import Base
|
||||
# /opt/docker/dev/service_finder/backend/app/models/staged_data.py
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any
|
||||
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class StagedVehicleData(Base):
|
||||
"""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"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
source_url = Column(String) # Honnan jött az adat?
|
||||
raw_data = Column(JSON) # A teljes leszedett JSON struktúra
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
source_url: Mapped[Optional[str]] = mapped_column(String)
|
||||
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
# Feldolgozási állapot
|
||||
status = Column(String, default="PENDING") # PENDING, PROCESSED, ERROR
|
||||
error_log = Column(String, nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), default="PENDING", index=True)
|
||||
error_log: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
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
|
||||
import enum
|
||||
from sqlalchemy import Column, String, DateTime, Boolean, text, UniqueConstraint, Integer
|
||||
from sqlalchemy.dialects.postgresql import JSONB # <-- JSONB-t használunk a stabilitásért
|
||||
# /opt/docker/dev/service_finder/backend/app/models/system.py
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any
|
||||
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 app.db.base_class import Base
|
||||
|
||||
class SystemParameter(Base):
|
||||
"""
|
||||
Központi, dinamikus konfigurációs tábla.
|
||||
Támogatja a többlépcsős felülbírálást (Global -> Country -> Region -> Individual).
|
||||
"""
|
||||
""" Dinamikus konfigurációs motor (Global -> Org -> User). """
|
||||
__tablename__ = "system_parameters"
|
||||
__table_args__ = (
|
||||
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 = Column(Integer, primary_key=True, autoincrement=True)
|
||||
id: Mapped[int] = mapped_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'
|
||||
category = Column(String, index=True, server_default="general")
|
||||
scope_level: Mapped[str] = mapped_column(String(30), server_default=text("'global'"), index=True)
|
||||
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
# A tényleges érték (JSONB-ben tárolva)
|
||||
value = Column(JSONB, nullable=False) # pl. {"FREE": 1, "PREMIUM": 4}
|
||||
|
||||
# --- 🛡️ HIERARCHIKUS SZINTEK ---
|
||||
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())
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
description: Mapped[Optional[str]] = mapped_column(String)
|
||||
last_modified_by: Mapped[Optional[str]] = mapped_column(String)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
|
||||
@@ -1,10 +1,27 @@
|
||||
from sqlalchemy import Column, Integer, String, Text
|
||||
from app.db.base_class import Base
|
||||
# /opt/docker/dev/service_finder/backend/app/models/translation.py
|
||||
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):
|
||||
"""
|
||||
Többnyelvűséget támogató tábla a felületi elemekhez és dinamikus tartalmakhoz.
|
||||
"""
|
||||
__tablename__ = "translations"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(255), index=True)
|
||||
lang = Column(String(5), index=True) # pl: 'hu', 'en'
|
||||
value = Column(Text)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
|
||||
# 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
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
# /opt/docker/dev/service_finder/backend/app/models/vehicle_definitions.py
|
||||
from __future__ import annotations
|
||||
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 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):
|
||||
"""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"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
code = Column(String(30), unique=True, index=True)
|
||||
name = Column(String(50))
|
||||
icon = Column(String(50))
|
||||
units = Column(JSON, server_default=text("'{\"power\": \"kW\", \"weight\": \"kg\", \"cargo\": \"m3\"}'::jsonb"))
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
code: Mapped[str] = mapped_column(String(30), unique=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String(50))
|
||||
icon: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
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):
|
||||
"""Globális felszereltség szótár"""
|
||||
""" Felszereltségi elemek definíciója (pl. ABS, Klíma, LED fényszóró) """
|
||||
__tablename__ = "feature_definitions"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
|
||||
category = Column(String(50))
|
||||
name = Column(String(100), nullable=False)
|
||||
data_type = Column(String(20), default="boolean")
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
vehicle_type_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.vehicle_types.id"))
|
||||
code: Mapped[str] = mapped_column(String(50), index=True)
|
||||
name: Mapped[str] = mapped_column(String(100))
|
||||
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):
|
||||
"""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"
|
||||
|
||||
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__ = (
|
||||
UniqueConstraint('make', 'technical_code', 'vehicle_type', name='uix_make_tech_type'),
|
||||
Index('idx_vmd_lookup', 'make', 'technical_code'),
|
||||
UniqueConstraint('make', 'normalized_name', 'variant_code', 'version_code', 'fuel_type', name='uix_vmd_precision'),
|
||||
Index('idx_vmd_lookup_fast', 'make', 'normalized_name'),
|
||||
Index('idx_vmd_engine_bridge', 'make', 'engine_code'),
|
||||
{"schema": "data"}
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
make = Column(String(50), nullable=False, index=True)
|
||||
technical_code = Column(String(50), nullable=False, index=True)
|
||||
marketing_name = Column(String(100), index=True)
|
||||
family_name = Column(String(100))
|
||||
# KAPCSOLATOK
|
||||
v_type_rel: Mapped["VehicleType"] = relationship("VehicleType", back_populates="definitions")
|
||||
feature_maps: Mapped[List["ModelFeatureMap"]] = relationship("ModelFeatureMap", back_populates="model_definition")
|
||||
|
||||
vehicle_type = Column(String(30), index=True)
|
||||
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
|
||||
vehicle_class = Column(String(50))
|
||||
# Hivatkozás az asset.py-ban lévő osztályra
|
||||
# Megjegyzés: Ha az AssetCatalog nincs itt importálva, húzzal adjuk meg a nevet
|
||||
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) ---
|
||||
is_manual = Column(Boolean, default=False, server_default=text("false"), index=True)
|
||||
attempts = Column(Integer, default=0, server_default=text("0"), index=True)
|
||||
last_error = Column(Text, nullable=True)
|
||||
class ModelFeatureMap(Base):
|
||||
""" Kapcsolótábla a modellek és az alapfelszereltség között """
|
||||
__tablename__ = "model_feature_maps"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
# Robot 2.1 "Researcher" porszívózott nyers adatai (A szemetesláda)
|
||||
raw_search_context = Column(Text, nullable=True)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=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)
|
||||
research_metadata = Column(JSONB, server_default=text("'{}'::jsonb"), nullable=False)
|
||||
# --------------------------------------------------
|
||||
|
||||
# --- 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")
|
||||
model_definition: Mapped["VehicleModelDefinition"] = relationship("VehicleModelDefinition", back_populates="feature_maps")
|
||||
feature: Mapped["FeatureDefinition"] = relationship("FeatureDefinition", back_populates="model_maps")
|
||||
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