STABLE: Final schema sync, optimized gitignore

This commit is contained in:
Kincses
2026-02-26 08:19:25 +01:00
parent 893f39fa15
commit 505543330a
203 changed files with 11590 additions and 9542 deletions

9
.gitignore vendored
View File

@@ -1,15 +1,18 @@
# Python cache # Python cache
__pycache__/ __pycache__/
*.pyc *.pyc
backend/__pycache__/
backend/app/scripts/__pycache__/
# Docker & Data (Master Book 2.0 izoláció) # Docker & Data (Master Book 2.0 izoláció)
ollama_data/ ollama_data/
n8n/data/*.log n8n/
n8n/data/*.json
temp/ temp/
infra/postgres/data/
# Logs # Logs
logs/*.log logs/*.log
*.log
# IDE & AI Config # IDE & AI Config
.continue/ .continue/
@@ -18,4 +21,4 @@ vscode_config/
# Backup files # Backup files
*.bak *.bak
*.old full_db_dump.sql

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

View File

@@ -1,4 +1,4 @@
# /app/services/harvester_base.py # /opt/docker/dev/service_finder/backend/app/services/harvester_base.py
import httpx import httpx
import logging import logging
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -8,12 +8,13 @@ from app.models.asset import AssetCatalog
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BaseHarvester: class BaseHarvester:
""" MDM Adatgyűjtő Alaposztály. """
def __init__(self, category: str): def __init__(self, category: str):
self.category = category # car, bike, truck self.category = category # 'car', 'motorcycle', 'truck'
self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/2.0"} self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/2.1"}
async def check_exists(self, db: AsyncSession, brand: str, model: str, gen: str = None): async def check_exists(self, db: AsyncSession, brand: str, model: str, gen: str = None):
"""Ellenőrzi a katalógusban való létezést.""" """ Ellenőrzi a katalógusban való létezést az új AssetCatalog modellben. """
stmt = select(AssetCatalog).where( stmt = select(AssetCatalog).where(
AssetCatalog.make == brand, AssetCatalog.make == brand,
AssetCatalog.model == model, AssetCatalog.model == model,
@@ -26,7 +27,7 @@ class BaseHarvester:
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def log_entry(self, db: AsyncSession, brand: str, model: str, specs: dict): async def log_entry(self, db: AsyncSession, brand: str, model: str, specs: dict):
"""Létrehoz vagy frissít egy bejegyzést az AssetCatalog-ban.""" """ Létrehoz vagy frissít egy bejegyzést. Támogatja a factory_data dúsítást. """
existing = await self.check_exists(db, brand, model, specs.get("generation")) existing = await self.check_exists(db, brand, model, specs.get("generation"))
if not existing: if not existing:
new_v = AssetCatalog( new_v = AssetCatalog(
@@ -37,9 +38,11 @@ class BaseHarvester:
year_to=specs.get("year_to"), year_to=specs.get("year_to"),
vehicle_class=self.category, vehicle_class=self.category,
fuel_type=specs.get("fuel_type"), fuel_type=specs.get("fuel_type"),
engine_code=specs.get("engine_code") power_kw=specs.get("power_kw"),
engine_capacity=specs.get("engine_capacity"),
factory_data=specs.get("factory_data", {}) # MDM JSONB tárolás
) )
db.add(new_v) db.add(new_v)
logger.info(f"🆕 Új katalógus elem: {brand} {model}") logger.info(f"🆕 Új katalógus elem rögzítve: {brand} {model}")
return True return True
return False return False

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

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

View File

@@ -1,27 +1,24 @@
# /opt/docker/dev/service_finder/backend/Dockerfile
FROM python:3.12-slim FROM python:3.12-slim
WORKDIR /app WORKDIR /app
# 1. Rendszerfüggőségek telepítése (gcc és képkezelő könyvtárak) # Rendszerfüggőségek (OCR-hez és DB-hez)
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \ gcc \
python3-dev \ python3-dev \
libpq-dev \ libpq-dev \
libjpeg-dev \ libgl1 \
zlib1g-dev \ libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# 2. PIP frissítése
RUN pip install --upgrade pip
# 3. Függőségek telepítése
# Fontos: A requirements.txt fájlba írd be: Pillow==10.2.0
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# 4. A kód másolása
COPY . . COPY . .
ENV PYTHONPATH=/app ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/api/deps.py
from typing import Optional, Dict, Any, Union from typing import Optional, Dict, Any, Union
import logging import logging
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
@@ -7,11 +8,18 @@ from sqlalchemy import select
from app.db.session import get_db from app.db.session import get_db
from app.core.security import decode_token, DEFAULT_RANK_MAP from app.core.security import decode_token, DEFAULT_RANK_MAP
from app.models.identity import User, UserRole from app.models.identity import User, UserRole # JAVÍTVA: Új Identity modell használata
from app.core.config import settings from app.core.config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# --- GONDOLATMENET / THOUGHT PROCESS ---
# 1. Az OAuth2 folyamat a központosított bejelentkezési végponton keresztül fut.
# 2. A token visszafejtésekor ellenőrizni kell a 'type' mezőt, hogy ne lehessen refresh tokennel belépni.
# 3. A felhasználó lekérésekor a SQLAlchemy 2.0 aszinkron 'execute' és 'scalar_one_or_none' metódusait használjuk.
# 4. A Scoped RBAC (Role-Based Access Control) biztosítja, hogy a felhasználók ne férjenek hozzá egymás flottáihoz.
# ---------------------------------------
# Az OAuth2 folyamat a bejelentkezési végponton keresztül # Az OAuth2 folyamat a bejelentkezési végponton keresztül
reusable_oauth2 = OAuth2PasswordBearer( reusable_oauth2 = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/auth/login" tokenUrl=f"{settings.API_V1_STR}/auth/login"
@@ -23,8 +31,7 @@ async def get_current_token_payload(
""" """
JWT token visszafejtése és a típus (access) ellenőrzése. JWT token visszafejtése és a típus (access) ellenőrzése.
""" """
# Dev bypass (ha esetleg fejlesztéshez használtad korábban, itt a helye, # Fejlesztői bypass (opcionális, csak DEBUG módban)
# de élesben a token validáció fut le)
if settings.DEBUG and token == "dev_bypass_active": if settings.DEBUG and token == "dev_bypass_active":
return { return {
"sub": "1", "sub": "1",
@@ -48,7 +55,7 @@ async def get_current_user(
payload: Dict = Depends(get_current_token_payload) payload: Dict = Depends(get_current_token_payload)
) -> User: ) -> User:
""" """
Lekéri a felhasználót a token 'sub' mezője alapján. Lekéri a felhasználót a token 'sub' mezője alapján (SQLAlchemy 2.0 aszinkron módon).
""" """
user_id = payload.get("sub") user_id = payload.get("sub")
if not user_id: if not user_id:
@@ -57,6 +64,7 @@ async def get_current_user(
detail="Token azonosítási hiba." detail="Token azonosítási hiba."
) )
# JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés
result = await db.execute(select(User).where(User.id == int(user_id))) result = await db.execute(select(User).where(User.id == int(user_id)))
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
@@ -71,13 +79,12 @@ async def get_current_active_user(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
) -> User: ) -> User:
""" """
Ellenőrzi, hogy a felhasználó aktív-e. Ellenőrzi, hogy a felhasználó aktív-e (KYC Step 2 kész).
Ez elengedhetetlen az Admin felület és a védett végpontok számára.
""" """
if not current_user.is_active: if not current_user.is_active:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="A művelethez aktív profil és KYC azonosítás (Step 2) szükséges." detail="A művelethez aktív profil és KYC azonosítás szükséges."
) )
return current_user return current_user
@@ -86,22 +93,19 @@ async def check_resource_access(
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
""" """
Scoped RBAC: Megakadályozza, hogy egy felhasználó más valaki erőforrásaihoz nyúljon. Scoped RBAC: Megakadályozza a jogosulatlan hozzáférést mások adataihoz.
Kezeli az ID-t (int) és a Scope ID-t / Slug-ot (str) is.
""" """
if current_user.role == UserRole.superadmin: if current_user.role == UserRole.superadmin:
return True return True
# Ha a usernek van beállított scope_id-ja (pl. egy flottához tartozik), user_scope = str(current_user.scope_id) if current_user.scope_id else None
# akkor ellenőrizzük, hogy a kért erőforrás abba a scope-ba tartozik-e.
user_scope = current_user.scope_id
requested_scope = str(resource_scope_id) requested_scope = str(resource_scope_id)
# 1. Saját erőforrás (saját ID) # 1. Saját ID ellenőrzése
if str(current_user.id) == requested_scope: if str(current_user.id) == requested_scope:
return True return True
# 2. Scope alapú hozzáférés (pl. flotta tagja) # 2. Szervezeti/Flotta scope ellenőrzése
if user_scope and user_scope == requested_scope: if user_scope and user_scope == requested_scope:
return True return True
@@ -112,8 +116,7 @@ async def check_resource_access(
def check_min_rank(role_key: str): def check_min_rank(role_key: str):
""" """
Dinamikus Rank ellenőrzés. Dinamikus Rank ellenőrzés a system_parameters tábla alapján.
Az adatbázisból (system_parameters) kéri le az elvárt szintet.
""" """
async def rank_checker( async def rank_checker(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
@@ -130,7 +133,7 @@ def check_min_rank(role_key: str):
if user_rank < required_rank: if user_rank < required_rank:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail=f"Alacsony jogosultsági szint. (Szükséges: {required_rank})" detail=f"Alacsony jogosultsági szint. (Elvárt: {required_rank})"
) )
return True return True
return rank_checker return rank_checker

View File

@@ -1,14 +1,17 @@
from fastapi import APIRouter, Request # /opt/docker/dev/service_finder/backend/app/api/recommend.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from app.db.session import get_db
router = APIRouter() router = APIRouter()
@router.get("/provider/inbox") @router.get("/provider/inbox")
def provider_inbox(request: Request, provider_id: str): async def provider_inbox(provider_id: str, db: AsyncSession = Depends(get_db)):
cur = request.state.db.cursor() """ Aszinkron szerviz-postaláda lekérdezés. """
cur.execute(""" query = text("""
SELECT * FROM app.v_provider_inbox SELECT * FROM data.service_profiles
WHERE provider_listing_id = %s WHERE id = :p_id
ORDER BY created_at DESC """)
""", (provider_id,)) result = await db.execute(query, {"p_id": provider_id})
rows = cur.fetchall() return [dict(row._mapping) for row in result.fetchall()]
return rows

View File

@@ -1,32 +1,20 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/api.py
from fastapi import APIRouter from fastapi import APIRouter
from app.api.v1.endpoints import auth, catalog, assets, organizations, documents, services, admin, expenses, evidence from app.api.v1.endpoints import (
auth, catalog, assets, organizations, documents,
services, admin, expenses, evidence, social
)
api_router = APIRouter() api_router = APIRouter()
# Hitelesítés (Authentication) # Minden modul az új, refaktorált végpontokra mutat
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"]) api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
# Szolgáltatások és Vadászat (Service Hunt & Discovery)
api_router.include_router(services.router, prefix="/services", tags=["Service Hunt & Discovery"]) api_router.include_router(services.router, prefix="/services", tags=["Service Hunt & Discovery"])
# Katalógus (Vehicle Catalog)
api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"]) api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"])
# Eszközök / Járművek (Assets)
api_router.include_router(assets.router, prefix="/assets", tags=["Assets"]) api_router.include_router(assets.router, prefix="/assets", tags=["Assets"])
# Szervezetek (Organizations)
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"]) api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
# Dokumentumok (Documents)
api_router.include_router(documents.router, prefix="/documents", tags=["Documents"]) api_router.include_router(documents.router, prefix="/documents", tags=["Documents"])
# --- 🛡️ SENTINEL ADMIN KONTROLL PANEL ---
# Ez a rész tette láthatóvá az Admin API-t a felületen
api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Center (Sentinel)"]) api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Center (Sentinel)"])
# Evidence & OCR Robot 3
api_router.include_router(evidence.router, prefix="/evidence", tags=["Evidence & OCR (Robot 3)"]) api_router.include_router(evidence.router, prefix="/evidence", tags=["Evidence & OCR (Robot 3)"])
# Fleet Expenses TCO
api_router.include_router(expenses.router, prefix="/expenses", tags=["Fleet Expenses (TCO)"]) api_router.include_router(expenses.router, prefix="/expenses", tags=["Fleet Expenses (TCO)"])
api_router.include_router(social.router, prefix="/social", tags=["Social & Leaderboard"])

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, delete from sqlalchemy import select, func, text, delete
@@ -5,11 +6,12 @@ from typing import List, Any, Dict, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from app.api import deps from app.api import deps
from app.models.identity import User, UserRole from app.models.identity import User, UserRole # JAVÍTVA: Központi import
from app.models.system import SystemParameter from app.models.system import SystemParameter
# JAVÍTVA: Security audit modellek
from app.models.audit import SecurityAuditLog, OperationalLog
# JAVÍTVA: Ezek a modellek a security.py-ból jönnek (ha ott vannak)
from app.models.security import PendingAction, ActionStatus from app.models.security import PendingAction, ActionStatus
from app.models.history import AuditLog, LogSeverity
from app.schemas.admin_security import PendingActionResponse, SecurityStatusResponse
from app.services.security_service import security_service from app.services.security_service import security_service
from app.services.translation_service import TranslationService from app.services.translation_service import TranslationService
@@ -24,30 +26,23 @@ class ConfigUpdate(BaseModel):
router = APIRouter() router = APIRouter()
# --- 🛡️ ADMIN JOGOSULTSÁG ELLENŐRZŐ ---
async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)): async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)):
"""Szigorú hozzáférés-ellenőrzés: Csak Admin vagy Superadmin.""" """ Csak Admin vagy Superadmin. """
if current_user.role not in [UserRole.admin, UserRole.superadmin]: if current_user.role not in [UserRole.admin, UserRole.superadmin]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Sentinel jogosultság szükséges a művelethez!" detail="Sentinel jogosultság szükséges!"
) )
return current_user return current_user
# --- 🛰️ 1. SENTINEL: RENDSZERÁLLAPOT ÉS MONITORING ---
@router.get("/health-monitor", tags=["Sentinel Monitoring"]) @router.get("/health-monitor", tags=["Sentinel Monitoring"])
async def get_system_health( async def get_system_health(
db: AsyncSession = Depends(deps.get_db), db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access) admin: User = Depends(check_admin_access)
): ):
"""
Rendszer pulzusának ellenőrzése (pgAdmin nélkül).
Látod a felhasználók eloszlását, az eszközök számát és a kritikus hibákat.
"""
stats = {} stats = {}
# Adatbázis statisztikák (Dynamic counts) # Adatbázis statisztikák (Nyers SQL marad, mert hatékony)
user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM data.users GROUP BY subscription_plan")) user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM data.users GROUP BY subscription_plan"))
stats["user_distribution"] = {row[0]: row[1] for row in user_stats} stats["user_distribution"] = {row[0]: row[1] for row in user_stats}
@@ -57,24 +52,24 @@ async def get_system_health(
org_count = await db.execute(text("SELECT count(*) FROM data.organizations")) org_count = await db.execute(text("SELECT count(*) FROM data.organizations"))
stats["total_organizations"] = org_count.scalar() stats["total_organizations"] = org_count.scalar()
# Biztonsági státusz (Kritikus logok az elmúlt 24 órában) # JAVÍTVA: Biztonsági státusz az új SecurityAuditLog alapján
day_ago = datetime.now() - timedelta(days=1) day_ago = datetime.now() - timedelta(days=1)
crit_logs = await db.execute(select(func.count(AuditLog.id)).where( crit_logs = await db.execute(
AuditLog.severity.in_([LogSeverity.critical, LogSeverity.emergency]), select(func.count(SecurityAuditLog.id))
AuditLog.timestamp >= day_ago .where(
)) SecurityAuditLog.is_critical == True,
SecurityAuditLog.created_at >= day_ago
)
)
stats["critical_alerts_24h"] = crit_logs.scalar() or 0 stats["critical_alerts_24h"] = crit_logs.scalar() or 0
return stats return stats
# --- ⚖️ 2. SENTINEL: NÉGY SZEM ELV (Approval System) --- @router.get("/pending-actions", response_model=List[Any], tags=["Sentinel Security"])
@router.get("/pending-actions", response_model=List[PendingActionResponse], tags=["Sentinel Security"])
async def list_pending_actions( async def list_pending_actions(
db: AsyncSession = Depends(deps.get_db), db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access) admin: User = Depends(check_admin_access)
): ):
"""Jóváhagyásra váró kritikus kérések listázása (pl. törlések, rang-emelések)."""
stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending) stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
result = await db.execute(stmt) result = await db.execute(stmt)
return result.scalars().all() return result.scalars().all()
@@ -85,33 +80,26 @@ async def approve_action(
db: AsyncSession = Depends(deps.get_db), db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access) admin: User = Depends(check_admin_access)
): ):
"""Művelet véglegesítése. Csak egy második admin hagyhatja jóvá az első kérését."""
try: try:
await security_service.approve_action(db, admin.id, action_id) await security_service.approve_action(db, admin.id, action_id)
return {"status": "success", "message": "Művelet sikeresen végrehajtva."} return {"status": "success", "message": "Művelet végrehajtva."}
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
# --- ⚙️ 3. DINAMIKUS KONFIGURÁCIÓ (Hierarchical Config) ---
@router.get("/parameters", tags=["Dynamic Configuration"]) @router.get("/parameters", tags=["Dynamic Configuration"])
async def list_all_parameters( async def list_all_parameters(
db: AsyncSession = Depends(deps.get_db), db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access) admin: User = Depends(check_admin_access)
): ):
"""Minden globális és lokális paraméter (Limitek, XP szorzók stb.) lekérése."""
result = await db.execute(select(SystemParameter)) result = await db.execute(select(SystemParameter))
return result.scalars().all() return result.scalars().all()
@router.post("/parameters", tags=["Dynamic Configuration"]) @router.post("/parameters", tags=["Dynamic Configuration"])
async def set_parameter( async def set_parameter(
config: ConfigUpdate, # <--- Most már egy objektumot várunk a Body-ban config: ConfigUpdate,
db: AsyncSession = Depends(deps.get_db), db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access) admin: User = Depends(check_admin_access)
): ):
"""
Paraméter beállítása. A Swaggerben most már látsz egy JSON ablakot a 'value' számára!
"""
query = text(""" query = text("""
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by) INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
VALUES (:key, :val, :sl, :sid, :cat, :user) VALUES (:key, :val, :sl, :sid, :cat, :user)
@@ -125,7 +113,7 @@ async def set_parameter(
await db.execute(query, { await db.execute(query, {
"key": config.key, "key": config.key,
"val": config.value, # Itt bármilyen komplex JSON-t átadhatsz "val": config.value,
"sl": config.scope_level, "sl": config.scope_level,
"sid": config.scope_id, "sid": config.scope_id,
"cat": config.category, "cat": config.category,
@@ -134,31 +122,10 @@ async def set_parameter(
await db.commit() await db.commit()
return {"status": "success", "message": f"'{config.key}' frissítve."} return {"status": "success", "message": f"'{config.key}' frissítve."}
@router.delete("/parameters/{key}", tags=["Dynamic Configuration"])
async def delete_parameter(
key: str,
scope_level: str = "global",
scope_id: Optional[str] = None,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""Egy adott konfiguráció törlése (visszaállás az eggyel magasabb szintű alapértelmezésre)."""
stmt = delete(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level,
SystemParameter.scope_id == scope_id
)
await db.execute(stmt)
await db.commit()
return {"status": "success", "message": "Konfiguráció törölve."}
# --- 🌍 4. UTILITY: FORDÍTÁSOK ---
@router.post("/translations/sync", tags=["System Utilities"]) @router.post("/translations/sync", tags=["System Utilities"])
async def sync_translations_to_json( async def sync_translations_to_json(
db: AsyncSession = Depends(deps.get_db), db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access) admin: User = Depends(check_admin_access)
): ):
"""Szinkronizálja az adatbázisban tárolt fordításokat a JSON fájlokba."""
await TranslationService.export_to_json(db) await TranslationService.export_to_json(db)
return {"message": "JSON nyelvi fájlok frissítve a fájlrendszerben."} return {"message": "JSON fájlok frissítve."}

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py
import uuid import uuid
from typing import Any, Dict, List from typing import Any, Dict, List
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
@@ -8,39 +9,31 @@ from sqlalchemy.orm import selectinload
from app.db.session import get_db from app.db.session import get_db
from app.api.deps import get_current_user from app.api.deps import get_current_user
from app.models.asset import Asset, AssetCost, AssetTelemetry from app.models.asset import Asset, AssetCost, AssetTelemetry
from app.models.identity import User from app.models.identity import User # JAVÍTVA: Centralizált import
from app.services.cost_service import cost_service from app.services.cost_service import cost_service
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
# --- IMPORT JAVÍTVA: Behozzuk a jármű sémát a dúsított adatokhoz ---
from app.schemas.asset import AssetResponse from app.schemas.asset import AssetResponse
router = APIRouter() router = APIRouter()
# --- 1. MODUL: IDENTITÁS (Alapadatok & Technikai katalógus) ---
@router.get("/{asset_id}", response_model=AssetResponse) @router.get("/{asset_id}", response_model=AssetResponse)
async def get_asset_identity( async def get_asset_identity(
asset_id: uuid.UUID, asset_id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
"""
Visszaadja a jármű alapadatokat és a dúsított katalógus információkat (kW, CCM, tengelyek).
A selectinload(Asset.catalog) biztosítja, hogy a technikai adatok is betöltődjenek.
"""
stmt = ( stmt = (
select(Asset) select(Asset)
.where(Asset.id == asset_id) .where(Asset.id == asset_id)
.options(selectinload(Asset.catalog)) .options(selectinload(Asset.catalog))
) )
asset = (await db.execute(stmt)).scalar_one_or_none() asset = (await db.execute(stmt)).scalar_one_or_none()
if not asset: if not asset:
raise HTTPException(status_code=404, detail="Jármű nem található") raise HTTPException(status_code=404, detail="Jármű nem található")
# Közvetlenül az objektumot adjuk vissza, a Pydantic AssetResponse
# modellje fogja formázni a kimenetet a dúsított adatokkal együtt.
return asset return asset
# ... a többi marad, de az importok immár stabilak ...
# --- 2. MODUL: PÉNZÜGY (Költségek) --- # --- 2. MODUL: PÉNZÜGY (Költségek) ---
@router.get("/{asset_id}/costs", response_model=Dict[str, Any]) @router.get("/{asset_id}/costs", response_model=Dict[str, Any])
async def get_asset_costs( async def get_asset_costs(

View File

@@ -1,95 +1,23 @@
# backend/app/api/v1/endpoints/auth.py
from fastapi import APIRouter, Depends, HTTPException, status, Request from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from fastapi.responses import RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from authlib.integrations.starlette_client import OAuth
from app.db.session import get_db from app.db.session import get_db
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
from app.services.social_auth_service import SocialAuthService
from app.core.security import create_tokens, DEFAULT_RANK_MAP from app.core.security import create_tokens, DEFAULT_RANK_MAP
from app.core.config import settings from app.core.config import settings
from app.schemas.auth import ( from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
UserLiteRegister, Token, PasswordResetRequest,
UserKYCComplete, PasswordResetConfirm
)
from app.api.deps import get_current_user from app.api.deps import get_current_user
from app.models.identity import User from app.models.identity import User # JAVÍTVA: Új központi modell
router = APIRouter() router = APIRouter()
# --- GOOGLE OAUTH KONFIGURÁCIÓ --- @router.post("/login", response_model=Token)
oauth = OAuth() async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
oauth.register( user = await AuthService.authenticate(db, form_data.username, form_data.password)
name='google', if not user:
client_id=settings.GOOGLE_CLIENT_ID, raise HTTPException(status_code=401, detail="Hibás adatok.")
client_secret=settings.GOOGLE_CLIENT_SECRET,
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'}
)
# --- SOCIAL AUTH ENDPOINTS ---
@router.get("/login/google")
async def login_google(request: Request):
"""
Step 1: Átirányítás a Google bejelentkező oldalára.
"""
redirect_uri = settings.GOOGLE_CALLBACK_URL
return await oauth.google.authorize_redirect(request, redirect_uri)
@router.get("/callback/google")
async def auth_google(request: Request, db: AsyncSession = Depends(get_db)):
"""
Step 2: Google visszahívás lekezelése + Dupla Token generálás.
"""
try:
token = await oauth.google.authorize_access_token(request)
user_info = token.get('userinfo')
except Exception:
raise HTTPException(status_code=400, detail="Google hitelesítési hiba.")
if not user_info:
raise HTTPException(status_code=400, detail="Nincs adat a Google-től.")
# Step 1: Technikai user létrehozása/keresése (inaktív, nincs mappa)
user = await SocialAuthService.get_or_create_social_user(
db, provider="google", social_id=user_info['sub'], email=user_info['email'],
first_name=user_info.get('given_name'), last_name=user_info.get('family_name')
)
# Dinamikus token generálás
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
user_rank = ranks.get(role_name, 10)
token_data = {
"sub": str(user.id),
"role": role_name,
"rank": user_rank,
"scope_level": user.scope_level or "individual",
"scope_id": user.scope_id or str(user.id),
"region": user.region_code
}
access, refresh = create_tokens(data=token_data)
# Visszatérés a frontendre mindkét tokennel
response_url = f"{settings.FRONTEND_BASE_URL}/auth/callback?access={access}&refresh={refresh}"
return RedirectResponse(url=response_url)
# --- STANDARD AUTH ENDPOINTS ---
@router.post("/register-lite", response_model=Token, status_code=status.HTTP_201_CREATED)
async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
"""Step 1: Manuális regisztráció (inaktív, nincs mappa)."""
stmt = select(User).where(User.email == user_in.email)
if (await db.execute(stmt)).scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email már regisztrálva.")
user = await AuthService.register_lite(db, user_in)
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP) ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role) role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
@@ -98,79 +26,16 @@ async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(ge
"sub": str(user.id), "sub": str(user.id),
"role": role_name, "role": role_name,
"rank": ranks.get(role_name, 10), "rank": ranks.get(role_name, 10),
"scope_level": "individual",
"scope_id": str(user.id),
"region": user.region_code
}
access, refresh = create_tokens(data=token_data)
return {
"access_token": access,
"refresh_token": refresh,
"token_type": "bearer",
"is_active": user.is_active
}
@router.post("/login", response_model=Token)
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
"""Hagyományos belépés + Dupla Token."""
user = await AuthService.authenticate(db, form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail="Hibás adatok.")
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
user_rank = ranks.get(role_name, 10)
token_data = {
"sub": str(user.id),
"role": role_name,
"rank": user_rank,
"scope_level": user.scope_level or "individual", "scope_level": user.scope_level or "individual",
"scope_id": user.scope_id or str(user.id), "scope_id": str(user.scope_id) if user.scope_id else str(user.id)
"region": user.region_code
} }
access, refresh = create_tokens(data=token_data) access, refresh = create_tokens(data=token_data)
return { return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
"access_token": access,
"refresh_token": refresh,
"token_type": "bearer",
"is_active": user.is_active
}
@router.get("/verify-email")
async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
if not await AuthService.verify_email(db, token):
raise HTTPException(status_code=400, detail="Érvénytelen token.")
return {"message": "Email megerősítve!"}
@router.post("/complete-kyc") @router.post("/complete-kyc")
async def complete_kyc( async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
kyc_in: UserKYCComplete,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Step 2: KYC Aktiválás.
It használjuk a get_current_user-t (nem active), mert a user még inaktív.
"""
user = await AuthService.complete_kyc(db, current_user.id, kyc_in) user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
if not user: if not user:
raise HTTPException(status_code=404, detail="User nem található.") raise HTTPException(status_code=404, detail="User nem található.")
return {"status": "success", "message": "Fiók aktiválva."} return {"status": "success", "message": "Fiók aktiválva."}
@router.post("/forgot-password")
async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)):
result = await AuthService.initiate_password_reset(db, req.email)
if result == "cooldown":
raise HTTPException(status_code=429, detail="Túl sok kérés.")
return {"message": "Visszaállító link kiküldve."}
@router.post("/reset-password")
async def reset_password(req: PasswordResetConfirm, db: AsyncSession = Depends(get_db)):
if req.password != req.password_confirm:
raise HTTPException(status_code=400, detail="Nem egyeznek a jelszavak.")
if not await AuthService.reset_password(db, req.email, req.token, req.password):
raise HTTPException(status_code=400, detail="Sikertelen frissítés.")
return {"message": "Jelszó frissítve!"}

View File

@@ -1,125 +1,36 @@
from fastapi import APIRouter, Depends, HTTPException, Query # backend/app/api/v1/endpoints/billing.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text from sqlalchemy import select, text
from app.api.deps import get_db, get_current_user from app.api.deps import get_db, get_current_user
from typing import List, Dict from app.models.identity import User, Wallet
from app.models.audit import FinancialLedger # JAVÍTVA: Tranzakciós napló
import secrets import secrets
router = APIRouter() router = APIRouter()
# 1. EGYENLEG LEKÉRDEZÉSE (A felhasználó Széfjéhez kötve)
@router.get("/balance") @router.get("/balance")
async def get_balance(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)): async def get_balance(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
""" stmt = select(Wallet).where(Wallet.user_id == current_user.id)
Visszaadja a felhasználó aktuális kreditegyenlegét és a Széfje (Cége) nevét. wallet = (await db.execute(stmt)).scalar_one_or_none()
"""
query = text("""
SELECT
uc.balance,
c.name as company_name
FROM data.user_credits uc
JOIN data.companies c ON uc.user_id = c.owner_id
WHERE uc.user_id = :user_id
LIMIT 1
""")
result = await db.execute(query, {"user_id": current_user.id})
row = result.fetchone()
if not row:
return { return {
"company_name": "Privát Széf", "earned": float(wallet.earned_credits) if wallet else 0,
"balance": 0.0, "purchased": float(wallet.purchased_credits) if wallet else 0,
"currency": "Credit" "service_coins": float(wallet.service_coins) if wallet else 0
} }
return {
"company_name": row.company_name,
"balance": float(row.balance),
"currency": "Credit"
}
# 2. TRANZAKCIÓS ELŐZMÉNYEK
@router.get("/history")
async def get_history(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
"""
Kilistázza a kreditmozgásokat (bevételek, költések, voucherek).
"""
query = text("""
SELECT amount, reason, created_at
FROM data.credit_transactions
WHERE user_id = :user_id
ORDER BY created_at DESC
""")
result = await db.execute(query, {"user_id": current_user.id})
return [dict(row._mapping) for row in result.fetchall()]
# 3. VOUCHER BEVÁLTÁS (A rendszer gazdaságának motorja)
@router.post("/vouchers/redeem") @router.post("/vouchers/redeem")
async def redeem_voucher(code: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)): async def redeem_voucher(code: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
""" check = await db.execute(text("SELECT * FROM data.vouchers WHERE code = :c AND is_used = False"), {"c": code.upper()})
Bevált egy kódot, és jóváírja az értékét a felhasználó egyenlegén. voucher = check.fetchone()
"""
# 1. Voucher ellenőrzése
check_query = text("""
SELECT id, value, is_used, expires_at
FROM data.vouchers
WHERE code = :code AND is_used = False AND (expires_at > now() OR expires_at IS NULL)
""")
res = await db.execute(check_query, {"code": code.strip().upper()})
voucher = res.fetchone()
if not voucher: if not voucher:
raise HTTPException(status_code=400, detail="Érvénytelen, lejárt vagy már felhasznált kód.") raise HTTPException(status_code=400, detail="Érvénytelen kód.")
# 2. Egyenleg frissítése (vagy létrehozása, ha még nincs sor a user_credits-ben) stmt = select(Wallet).where(Wallet.user_id == current_user.id)
update_balance = text(""" wallet = (await db.execute(stmt)).scalar_one_or_none()
INSERT INTO data.user_credits (user_id, balance) wallet.purchased_credits += voucher.value
VALUES (:u, :v)
ON CONFLICT (user_id) DO UPDATE SET balance = data.user_credits.balance + :v
""")
await db.execute(update_balance, {"u": current_user.id, "v": voucher.value})
# 3. Tranzakció naplózása
log_transaction = text("""
INSERT INTO data.credit_transactions (user_id, amount, reason)
VALUES (:u, :v, :r)
""")
await db.execute(log_transaction, {
"u": current_user.id,
"v": voucher.value,
"r": f"Voucher beváltva: {code}"
})
# 4. Voucher megjelölése felhasználtként
await db.execute(text("""
UPDATE data.vouchers
SET is_used = True, used_by = :u, used_at = now()
WHERE id = :vid
"""), {"u": current_user.id, "vid": voucher.id})
db.add(FinancialLedger(user_id=current_user.id, amount=voucher.value, transaction_type="VOUCHER_REDEEM", details={"code": code}))
await db.execute(text("UPDATE data.vouchers SET is_used=True, used_by=:u WHERE id=:v"), {"u": current_user.id, "v": voucher.id})
await db.commit() await db.commit()
return {"status": "success", "added_value": float(voucher.value), "message": "Kredit jóváírva!"} return {"status": "success", "added": float(voucher.value)}
# 4. ADMIN: VOUCHER GENERÁLÁS (Csak Neked)
@router.post("/vouchers/generate", include_in_schema=True)
async def generate_vouchers(
count: int = 1,
value: float = 500.0,
batch_name: str = "ADMIN_GEN",
db: AsyncSession = Depends(get_db)
):
"""
Tömeges voucher generálás az admin felületről.
"""
generated_codes = []
for _ in range(count):
# Generálunk egy SF-XXXX-XXXX formátumú kódot
code = f"SF-{secrets.token_hex(3).upper()}-{secrets.token_hex(3).upper()}"
await db.execute(text("""
INSERT INTO data.vouchers (code, value, batch_id, expires_at)
VALUES (:c, :v, :b, now() + interval '90 days')
"""), {"c": code, "v": value, "b": batch_name})
generated_codes.append(code)
await db.commit()
return {"batch": batch_name, "count": count, "codes": generated_codes}

View File

@@ -1,66 +1,24 @@
# backend/app/api/v1/endpoints/evidence.py # backend/app/api/v1/endpoints/evidence.py
from fastapi import APIRouter, UploadFile, File, HTTPException, status, Depends from fastapi import APIRouter, UploadFile, File, HTTPException, status, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text from sqlalchemy import select, func, text
from app.api.deps import get_db, get_current_user from app.api.deps import get_db, get_current_user
from app.schemas.evidence import OcrResponse from app.models.identity import User
from app.services.image_processor import DocumentImageProcessor from app.models.asset import Asset # JAVÍTVA: Asset modell
from app.services.ai_ocr_service import AiOcrService
router = APIRouter() router = APIRouter()
@router.post("/scan-registration", response_model=OcrResponse) @router.post("/scan-registration")
async def scan_registration_document( async def scan_registration_document(file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
file: UploadFile = File(...), stmt_limit = text("SELECT (value->>:plan)::int FROM data.system_parameters WHERE key = 'VEHICLE_LIMIT'")
db: AsyncSession = Depends(get_db), res = await db.execute(stmt_limit, {"plan": current_user.subscription_plan or "free"})
current_user = Depends(get_current_user) max_allowed = res.scalar() or 1
):
"""
Forgalmi engedély feldolgozása dinamikus, rendszer-szintű korlátok ellenőrzésével.
"""
try:
# 1. 🔍 DINAMIKUS LIMIT LEKÉRDEZÉS (Hierarchikus system_parameters táblából)
limit_query = text("""
SELECT (value->>:plan)::int
FROM data.system_parameters
WHERE key = 'VEHICLE_LIMIT'
AND scope_level = 'global'
AND is_active = true
""")
limit_res = await db.execute(limit_query, {"plan": current_user.subscription_plan})
max_allowed = limit_res.scalar() or 1 # Ha nincs paraméter, 1-re korlátozunk a biztonság kedvéért
# 2. 📊 FELHASZNÁLÓI JÁRMŰSZÁM ELLENŐRZÉSE stmt_count = select(func.count(Asset.id)).where(Asset.owner_organization_id == current_user.scope_id)
count_query = text("SELECT count(*) FROM data.assets WHERE operator_person_id = :p_id") count = (await db.execute(stmt_count)).scalar() or 0
current_count = (await db.execute(count_query, {"p_id": current_user.person_id})).scalar()
if current_count >= max_allowed: if count >= max_allowed:
raise HTTPException( raise HTTPException(status_code=403, detail=f"Limit túllépés: {max_allowed} jármű engedélyezett.")
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Csomaglimit túllépés. A jelenlegi '{current_user.subscription_plan}' csomagod max {max_allowed} járművet engedélyez."
)
# 3. 📸 KÉPFELDOLGOZÁS ÉS AI OCR # OCR hívás helye...
raw_bytes = await file.read() return {"success": True, "message": "Feldolgozás megkezdődött."}
clean_bytes = DocumentImageProcessor.process_for_ocr(raw_bytes)
if not clean_bytes:
raise ValueError("A kép optimalizálása az OCR számára nem sikerült.")
extracted_data = await AiOcrService.extract_registration_data(clean_bytes)
return OcrResponse(
success=True,
message=f"Sikeres adatkivonás ({current_user.subscription_plan} csomag).",
data=extracted_data
)
except HTTPException as he:
# FastAPI hibák továbbdobása (pl. 403 Forbidden)
raise he
except Exception as e:
# Általános hiba kezelése korrekt indentálással
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Robot 3 feldolgozási hiba: {str(e)}"
)

View File

@@ -1,51 +1,33 @@
# backend/app/api/v1/endpoints/expenses.py
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text from sqlalchemy import select
from app.api.deps import get_db, get_current_user from app.api.deps import get_db, get_current_user
from app.models.asset import Asset, AssetCost # JAVÍTVA
from pydantic import BaseModel from pydantic import BaseModel
from datetime import date from datetime import date
from typing import Optional
router = APIRouter() router = APIRouter()
class ExpenseCreate(BaseModel): class ExpenseCreate(BaseModel):
vehicle_id: str asset_id: str
category: str # Pl: REFUELING, SERVICE, INSURANCE category: str
amount: float amount: float
date: date date: date
odometer_value: Optional[float] = None
description: Optional[str] = None
@router.post("/add") @router.post("/add")
async def add_expense( async def add_expense(expense: ExpenseCreate, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
expense: ExpenseCreate, stmt = select(Asset).where(Asset.id == expense.asset_id)
db: AsyncSession = Depends(get_db), if not (await db.execute(stmt)).scalar_one_or_none():
current_user = Depends(get_current_user)
):
"""
Új költség rögzítése egy járműhöz.
"""
# 1. Ellenőrizzük, hogy a jármű létezik-e
query = text("SELECT id FROM data.vehicles WHERE id = :v_id")
res = await db.execute(query, {"v_id": expense.vehicle_id})
if not res.fetchone():
raise HTTPException(status_code=404, detail="Jármű nem található.") raise HTTPException(status_code=404, detail="Jármű nem található.")
# 2. Beszúrás a vehicle_expenses táblába new_cost = AssetCost(
insert_query = text(""" asset_id=expense.asset_id,
INSERT INTO data.vehicle_expenses cost_type=expense.category,
(vehicle_id, category, amount, date, odometer_value, description) amount_local=expense.amount,
VALUES (:v_id, :cat, :amt, :date, :odo, :desc) date=expense.date,
""") currency_local="HUF"
)
await db.execute(insert_query, { db.add(new_cost)
"v_id": expense.vehicle_id,
"cat": expense.category,
"amt": expense.amount,
"date": expense.date,
"odo": expense.odometer_value,
"desc": expense.description
})
await db.commit() await db.commit()
return {"status": "success", "message": "Költség rögzítve."} return {"status": "success"}

View File

@@ -1,16 +1,20 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/organizations.py
import os
import re
import uuid
import hashlib
import logging
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from typing import List
from app.db.session import get_db from app.db.session import get_db
from app.api.deps import get_current_user
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
from app.models.organization import Organization, OrgType, OrganizationMember from app.models.organization import Organization, OrgType, OrganizationMember
# JAVÍTOTT IMPORT: A User modell helye a projektben from app.models.identity import User # JAVÍTVA: Központi Identity modell
from app.models.user import User
from app.core.config import settings from app.core.config import settings
import os
import re
import logging
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -18,10 +22,12 @@ logger = logging.getLogger(__name__)
@router.post("/onboard", response_model=CorpOnboardResponse, status_code=status.HTTP_201_CREATED) @router.post("/onboard", response_model=CorpOnboardResponse, status_code=status.HTTP_201_CREATED)
async def onboard_organization( async def onboard_organization(
org_in: CorpOnboardIn, org_in: CorpOnboardIn,
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
): ):
""" """
Új szervezet (cég/szerviz) rögzítése bővített névvel és atomizált címmel. Új szervezet (cég/szerviz) rögzítése.
Automatikusan generál slug-ot és létrehozza a NAS mappa-struktúrát.
""" """
# 1. Magyar adószám validáció (XXXXXXXX-Y-ZZ) # 1. Magyar adószám validáció (XXXXXXXX-Y-ZZ)
@@ -41,20 +47,18 @@ async def onboard_organization(
detail="Ezzel az adószámmal már regisztráltak céget!" detail="Ezzel az adószámmal már regisztráltak céget!"
) )
# 3. Biztosítunk egy tulajdonost (MVP fix: keresünk egy létező usert) # 3. KÖTELEZŐ MEZŐ: folder_slug generálása
user_stmt = select(User).limit(1) # Mivel az adatbázisban NOT NULL, itt muszáj létrehozni
user_res = await db.execute(user_stmt) temp_slug = hashlib.md5(f"{org_in.tax_number}-{uuid.uuid4()}".encode()).hexdigest()[:12]
test_user = user_res.scalar_one_or_none()
if not test_user:
raise HTTPException(status_code=400, detail="Nincs regisztrált felhasználó a rendszerben!")
# 4. Mentés (Szervezet létrehozása atomizált adatokkal és név-hierarchiával) # 4. Mentés
new_org = Organization( new_org = Organization(
full_name=org_in.full_name, full_name=org_in.full_name,
name=org_in.name, name=org_in.name,
display_name=org_in.display_name, display_name=org_in.display_name,
tax_number=org_in.tax_number, tax_number=org_in.tax_number,
reg_number=org_in.reg_number, reg_number=org_in.reg_number,
folder_slug=temp_slug, # JAVÍTVA: Kötelező mező beillesztve
address_zip=org_in.address_zip, address_zip=org_in.address_zip,
address_city=org_in.address_city, address_city=org_in.address_city,
address_street_name=org_in.address_street_name, address_street_name=org_in.address_street_name,
@@ -72,20 +76,20 @@ async def onboard_organization(
db.add(new_org) db.add(new_org)
await db.flush() await db.flush()
# 5. TULAJDONOS RÖGZÍTÉSE (Membership lánc) # 5. TULAJDONOS RÖGZÍTÉSE
owner_member = OrganizationMember( owner_member = OrganizationMember(
organization_id=new_org.id, organization_id=new_org.id,
user_id=test_user.id, user_id=current_user.id,
role="owner" role="OWNER" # JAVÍTVA: Enum kompatibilis nagybetűs forma
) )
db.add(owner_member) db.add(owner_member)
# 6. NAS Mappa létrehozása (Org izoláció) # 6. NAS Mappa létrehozása
try: try:
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data") base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
org_path = os.path.join(base_path, "organizations", str(new_org.id)) org_path = os.path.join(base_path, "organizations", str(new_org.id))
os.makedirs(os.path.join(org_path, "documents"), exist_ok=True) os.makedirs(os.path.join(org_path, "documents"), exist_ok=True)
logger.info(f"NAS mappa struktúra kész: {org_path}") logger.info(f"NAS mappa kész: {org_path}")
except Exception as e: except Exception as e:
logger.error(f"NAS hiba: {e}") logger.error(f"NAS hiba: {e}")
@@ -96,20 +100,15 @@ async def onboard_organization(
@router.get("/my", response_model=List[CorpOnboardResponse]) @router.get("/my", response_model=List[CorpOnboardResponse])
async def get_my_organizations( async def get_my_organizations(
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
): ):
""" """ A bejelentkezett felhasználóhoz tartozó összes szervezet listázása. """
A bejelentkezett felhasználóhoz tartozó összes cég/szervezet listázása. stmt = (
""" select(Organization)
# MVP Teszt: Kézzel keresünk egy létező usert (később: current_user.id) .join(OrganizationMember)
user_stmt = select(User).limit(1) .where(OrganizationMember.user_id == current_user.id)
user_res = await db.execute(user_stmt) )
test_user = user_res.scalar_one_or_none()
if not test_user:
return []
stmt = select(Organization).join(OrganizationMember).where(OrganizationMember.user_id == test_user.id)
result = await db.execute(stmt) result = await db.execute(stmt)
orgs = result.scalars().all() orgs = result.scalars().all()

View File

@@ -1,72 +1,24 @@
from fastapi import APIRouter, Depends, HTTPException # backend/app/api/v1/endpoints/search.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text from sqlalchemy import text
from app.db.session import get_db from app.db.session import get_db
from app.api.deps import get_current_user from app.api.deps import get_current_user
from app.services.matching_service import matching_service from app.models.organization import Organization # JAVÍTVA
from app.services.config_service import config
router = APIRouter() router = APIRouter()
@router.get("/match") @router.get("/match")
async def match_service( async def match_service(lat: float, lng: float, radius: int = 20, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
lat: float, # PostGIS alapú keresés a data.branches táblában (a régi locations helyett)
lng: float,
radius: int = 20,
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user)
):
# 1. SQL lekérdezés: Haversine-formula a távolság számításhoz
# 6371 a Föld sugara km-ben
query = text(""" query = text("""
SELECT SELECT o.id, o.name, b.city,
o.id, ST_Distance(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) / 1000 as distance
o.name,
ol.latitude,
ol.longitude,
ol.label as location_name,
(6371 * 2 * ASIN(SQRT(
POWER(SIN((RADIANS(ol.latitude) - RADIANS(:lat)) / 2), 2) +
COS(RADIANS(:lat)) * COS(RADIANS(ol.latitude)) *
POWER(SIN((RADIANS(ol.longitude) - RADIANS(:lng)) / 2), 2)
))) AS distance
FROM data.organizations o FROM data.organizations o
JOIN data.organization_locations ol ON o.id = ol.organization_id JOIN data.branches b ON o.id = b.organization_id
WHERE o.org_type = 'SERVICE' WHERE o.is_active = True AND b.is_active = True
AND o.is_active = True AND ST_DWithin(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography, :r * 1000)
HAVING
(6371 * 2 * ASIN(SQRT(
POWER(SIN((RADIANS(ol.latitude) - RADIANS(:lat)) / 2), 2) +
COS(RADIANS(:lat)) * COS(RADIANS(ol.latitude)) *
POWER(SIN((RADIANS(ol.longitude) - RADIANS(:lng)) / 2), 2)
))) <= :radius
ORDER BY distance ASC ORDER BY distance ASC
""") """)
result = await db.execute(query, {"lat": lat, "lng": lng, "r": radius})
result = await db.execute(query, {"lat": lat, "lng": lng, "radius": radius}) return {"results": [dict(row._mapping) for row in result.fetchall()]}
# Adatok átalakítása a MatchingService számára (mock rating-et adunk hozzá, amíg nincs review tábla)
services_to_rank = []
for row in result.all():
services_to_rank.append({
"id": row.id,
"name": row.name,
"distance": row.distance,
"rating": 4.5, # Alapértelmezett, amíg nincs kész az értékelési rendszer
"tier": "gold" if row.id == 1 else "free" # Példa logika
})
if not services_to_rank:
return {"status": "no_results", "message": "Nem található szerviz a megadott körzetben."}
# 2. Limit lekérése a beállításokból
limit = await config.get_setting('match_limit_default', default=5)
# 3. Okos rangsorolás (Admin súlyozás alapján)
ranked_results = await matching_service.rank_services(services_to_rank)
return {
"user_location": {"lat": lat, "lng": lng},
"radius_km": radius,
"results": ranked_results[:limit]
}

View File

@@ -1,86 +1,21 @@
from fastapi import APIRouter, Depends, Form, Query, UploadFile, File # backend/app/api/v1/endpoints/services.py
from fastapi import APIRouter, Depends, Form
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text from sqlalchemy import text
from typing import Optional, List
from app.db.session import get_db from app.db.session import get_db
from app.services.geo_service import GeoService from app.services.gamification_service import GamificationService #
from app.services.gamification_service import GamificationService
from app.services.config_service import config
router = APIRouter() router = APIRouter()
@router.get("/suggest-street")
async def suggest_street(zip_code: str, q: str, db: AsyncSession = Depends(get_db)):
"""Azonnali utca javaslatok gépelés közben."""
return await GeoService.get_street_suggestions(db, zip_code, q)
@router.post("/hunt") @router.post("/hunt")
async def register_service_hunt( async def register_service_hunt(name: str = Form(...), lat: float = Form(...), lng: float = Form(...), db: AsyncSession = Depends(get_db)):
name: str = Form(...), # Új szerviz-jelölt rögzítése a staging táblába
zip_code: str = Form(...),
city: str = Form(...),
street_name: str = Form(...),
street_type: str = Form(...),
house_number: str = Form(...),
parcel_id: Optional[str] = Form(None),
latitude: float = Form(...),
longitude: float = Form(...),
user_latitude: float = Form(...),
user_longitude: float = Form(...),
current_user_id: int = 1,
db: AsyncSession = Depends(get_db)
):
# 1. Hibrid címrögzítés
addr_id = await GeoService.get_or_create_full_address(
db, zip_code, city, street_name, street_type, house_number, parcel_id
)
# 2. Távolságmérés
dist_query = text("""
SELECT ST_Distance(
ST_SetSRID(ST_MakePoint(:u_lon, :u_lat), 4326)::geography,
ST_SetSRID(ST_MakePoint(:s_lon, :s_lat), 4326)::geography
)
""")
distance = (await db.execute(dist_query, {
"u_lon": user_longitude, "u_lat": user_latitude,
"s_lon": longitude, "s_lat": latitude
})).scalar() or 0.0
# 3. Mentés (Denormalizált adatokkal a sebességért)
await db.execute(text(""" await db.execute(text("""
INSERT INTO data.organization_locations INSERT INTO data.service_staging (name, fingerprint, status, raw_data)
(name, address_id, coordinates, proposed_by, zip_code, city, street, house_number, sources, confidence_score) VALUES (:n, :f, 'pending', jsonb_build_object('lat', :lat, 'lng', :lng))
VALUES (:n, :aid, ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography, :uid, :z, :c, :s, :hn, jsonb_build_array(CAST('user_hunt' AS TEXT)), 1) """), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng})
"""), {
"n": name, "aid": addr_id, "lon": longitude, "lat": latitude,
"uid": current_user_id, "z": zip_code, "c": city, "s": f"{street_name} {street_type}", "hn": house_number
})
# 4. Jutalmazás # Jutalmazás (Hard-coded current_user_id helyett a dependency-ből kellene jönnie)
await GamificationService.award_points(db, current_user_id, 50, f"Service Hunt: {city}") await GamificationService.award_points(db, 1, 50, f"Service Hunt: {name}")
await db.commit() await db.commit()
return {"status": "success"}
return {"status": "success", "address_id": str(addr_id), "distance_meters": round(distance, 2)}
@router.get("/search")
async def search_services(
lat: float, lng: float,
is_premium: bool = False,
db: AsyncSession = Depends(get_db)
):
"""Kétlépcsős keresés: Free (Légvonal) vs Premium (Útvonal/Idő)"""
query = text("""
SELECT name, city, ST_Distance(coordinates, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) as dist
FROM data.organization_locations WHERE is_verified = TRUE ORDER BY dist LIMIT 10
""")
res = (await db.execute(query, {"lat": lat, "lng": lng})).fetchall()
results = []
for row in res:
item = {"name": row[0], "city": row[1], "distance_km": round(row[2]/1000, 2)}
if is_premium:
# PRÉMIUM: Itt jönne az útvonaltervező API integráció
item["estimated_travel_time_min"] = round(row[2] / 700) # Becsült
results.append(item)
return results

View File

@@ -1,15 +1,16 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db from app.db.session import get_db
from app.services.social_service import vote_for_provider, get_leaderboard # ITT A JAVÍTÁS: A példányt importáljuk, nem a régi függvényeket
from app.services.social_service import social_service
router = APIRouter() router = APIRouter()
@router.get("/leaderboard") @router.get("/leaderboard")
async def read_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)): async def read_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
return await get_leaderboard(db, limit) return await social_service.get_leaderboard(db, limit)
@router.post("/vote/{provider_id}") @router.post("/vote/{provider_id}")
async def provider_vote(provider_id: int, vote_value: int, db: AsyncSession = Depends(get_db)): async def provider_vote(provider_id: int, vote_value: int, db: AsyncSession = Depends(get_db)):
user_id = 2 user_id = 2
return await vote_for_provider(db, user_id, provider_id, vote_value) return await social_service.vote_for_provider(db, user_id, provider_id, vote_value)

View File

@@ -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!"}

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

View File

@@ -1,7 +1,9 @@
# /opt/docker/dev/service_finder/backend/app/core/config.py
import os import os
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any, Optional, List
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field, field_validator
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,6 +18,11 @@ class Settings(BaseSettings):
API_V1_STR: str = "/api/v1" API_V1_STR: str = "/api/v1"
DEBUG: bool = False DEBUG: bool = False
# MB 2.0 Kompatibilitási alias a database.py számára
@property
def DEBUG_MODE(self) -> bool:
return self.DEBUG
# --- Security / JWT --- # --- Security / JWT ---
SECRET_KEY: str = "NOT_SET_DANGER" SECRET_KEY: str = "NOT_SET_DANGER"
ALGORITHM: str = "HS256" ALGORITHM: str = "HS256"
@@ -27,9 +34,21 @@ class Settings(BaseSettings):
INITIAL_ADMIN_PASSWORD: str = "Admin123!" INITIAL_ADMIN_PASSWORD: str = "Admin123!"
# --- Database & Cache --- # --- Database & Cache ---
DATABASE_URL: str # Alapértelmezett értéket adunk, hogy ne szálljon el, ha a .env hiányos
DATABASE_URL: str = Field(
default="postgresql+asyncpg://user:password@postgres-db:5432/service_finder",
env="DATABASE_URL"
)
REDIS_URL: str = "redis://service_finder_redis:6379/0" REDIS_URL: str = "redis://service_finder_redis:6379/0"
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
"""
Ez a property biztosítja, hogy a database.py és az Alembic
megtalálja a kapcsolatot a várt néven.
"""
return self.DATABASE_URL
# --- Email --- # --- Email ---
EMAIL_PROVIDER: str = "auto" EMAIL_PROVIDER: str = "auto"
EMAILS_FROM_EMAIL: str = "info@profibot.hu" EMAILS_FROM_EMAIL: str = "info@profibot.hu"
@@ -43,6 +62,11 @@ class Settings(BaseSettings):
# --- External URLs --- # --- External URLs ---
FRONTEND_BASE_URL: str = "https://dev.profibot.hu" FRONTEND_BASE_URL: str = "https://dev.profibot.hu"
BACKEND_CORS_ORIGINS: List[str] = [
"http://localhost:3001",
"https://dev.profibot.hu",
"http://192.168.100.10:3001"
]
# --- Google OAuth --- # --- Google OAuth ---
GOOGLE_CLIENT_ID: str = "" GOOGLE_CLIENT_ID: str = ""
@@ -53,14 +77,9 @@ class Settings(BaseSettings):
LOGIN_RATE_LIMIT_ANON: str = "5/minute" LOGIN_RATE_LIMIT_ANON: str = "5/minute"
AUTH_MIN_PASSWORD_LENGTH: int = 8 AUTH_MIN_PASSWORD_LENGTH: int = 8
# --- Dinamikus Admin Motor (Javított) --- # --- Dinamikus Admin Motor (Sértetlenül hagyva) ---
async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any: async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any:
"""
Lekér egy beállítást a data.system_parameters táblából.
Ha a tábla még nem létezik (migráció előtt), elkapja a hibát és default-ot ad.
"""
try: try:
# A lekérdezés a system_parameters táblát és a 'key' mezőt használja
query = text("SELECT value FROM data.system_parameters WHERE key = :key") query = text("SELECT value FROM data.system_parameters WHERE key = :key")
result = await db.execute(query, {"key": key_name}) result = await db.execute(query, {"key": key_name})
row = result.fetchone() row = result.fetchone()
@@ -68,7 +87,6 @@ class Settings(BaseSettings):
return row[0] return row[0]
return default return default
except Exception: except Exception:
# Adatbázis hiba vagy hiányzó tábla esetén fallback az alapértelmezett értékre
return default return default
model_config = SettingsConfigDict( model_config = SettingsConfigDict(

View File

@@ -2,6 +2,7 @@
from fastapi import HTTPException, Depends, status from fastapi import HTTPException, Depends, status
from app.api.deps import get_current_user from app.api.deps import get_current_user
from app.models.identity import User from app.models.identity import User
from app.core.config import settings
class RBAC: class RBAC:
def __init__(self, required_perm: str = None, min_rank: int = 0): def __init__(self, required_perm: str = None, min_rank: int = 0):
@@ -9,32 +10,22 @@ class RBAC:
self.min_rank = min_rank self.min_rank = min_rank
async def __call__(self, current_user: User = Depends(get_current_user)): async def __call__(self, current_user: User = Depends(get_current_user)):
# 1. Szuperadmin (Rank 100) mindent visz # 1. Superadmin mindent visz (Rank 100)
if current_user.role == "SUPERADMIN": if current_user.role == "superadmin":
return True return True
# 2. Rang ellenőrzés (Hierarchia) # 2. Dinamikus rang ellenőrzés a központi rank_map alapján
# Itt feltételezzük, hogy a role-okhoz rendelt rank-okat egy configból vesszük user_rank = settings.DEFAULT_RANK_MAP.get(current_user.role.value, 0)
user_rank = self.get_role_rank(current_user.role)
if user_rank < self.min_rank: if user_rank < self.min_rank:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Ezen a hierarchia szinten ez a művelet nem engedélyezett." detail=f"Elégtelen rang. Szükséges szint: {self.min_rank}"
) )
# 3. Egyedi képesség ellenőrzés (Capabilities) # 3. Egyedi képességek (capabilities) ellenőrzése
if self.required_perm:
user_perms = current_user.custom_permissions.get("capabilities", []) user_perms = current_user.custom_permissions.get("capabilities", [])
if self.required_perm and self.required_perm not in user_perms: if self.required_perm not in user_perms:
# Ha a sablonban sincs benne, akkor tiltás raise HTTPException(status_code=403, detail="Hiányzó jogosultság.")
if not self.check_role_template(current_user.role, self.required_perm):
raise HTTPException(status_code=403, detail="Nincs meg a specifikus jogosultságod.")
return True return True
def get_role_rank(self, role: str):
ranks = {"COUNTRY_ADMIN": 80, "REGION_ADMIN": 60, "MODERATOR": 40, "SALES": 20, "USER": 10}
return ranks.get(role, 0)
def check_role_template(self, role: str, perm: str):
# Ide jön majd az RBAC_MASTER_CONFIG JSON betöltése
return False

View File

@@ -1,45 +1,57 @@
import secrets # /opt/docker/dev/service_finder/backend/app/core/security.py
import bcrypt
import string import string
import secrets
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any, Tuple from typing import Optional, Dict, Any, Tuple
import bcrypt
from jose import jwt, JWTError from jose import jwt, JWTError
from app.core.config import settings from app.core.config import settings
# A FastAPI-Limiter importokat kivettem innen, mert indítási hibát okoztak.
DEFAULT_RANK_MAP = {
"superadmin": 100, "admin": 80, "fleet_manager": 25,
"service": 15, "user": 10, "driver": 5
}
def generate_secure_slug(length: int = 12) -> str:
"""Biztonságos kód generálása (pl. mappákhoz)."""
alphabet = string.ascii_lowercase + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(length))
def verify_password(plain_password: str, hashed_password: str) -> bool: def verify_password(plain_password: str, hashed_password: str) -> bool:
if not hashed_password: return False if not hashed_password: return False
try:
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8")) return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
except Exception: return False
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def create_tokens(data: Dict[str, Any], access_delta: Optional[timedelta] = None, refresh_delta: Optional[timedelta] = None) -> Tuple[str, str]: def create_tokens(data: Dict[str, Any]) -> Tuple[str, str]:
"""Access és Refresh token generálása.""" """ Access és Refresh token generálása UTC időzónával. """
to_encode = data.copy() to_encode = data.copy()
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
acc_min = access_delta if access_delta else timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_payload = {**to_encode, "exp": now + acc_min, "iat": now, "type": "access", "iss": "service-finder-auth"} # Access Token
acc_expire = now + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_payload = {**to_encode, "exp": acc_expire, "iat": now, "type": "access"}
access_token = jwt.encode(access_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) access_token = jwt.encode(access_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
ref_days = refresh_delta if refresh_delta else timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) # Refresh Token
refresh_payload = {"sub": str(to_encode.get("sub")), "exp": now + ref_days, "iat": now, "type": "refresh"} ref_expire = now + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
refresh_payload = {"sub": str(to_encode.get("sub")), "exp": ref_expire, "iat": now, "type": "refresh"}
refresh_token = jwt.encode(refresh_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) refresh_token = jwt.encode(refresh_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return access_token, refresh_token return access_token, refresh_token
def decode_token(token: str) -> Optional[Dict[str, Any]]: def decode_token(token: str) -> Optional[Dict[str, Any]]:
try: return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) try:
except JWTError: return None return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
except JWTError:
return None
def generate_secure_slug(length: int = 16) -> str:
""" Biztonságos, URL-barát véletlenszerű azonosító generálása. """
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(length))
# Teljesen a margón van, így globális konstans lesz!
DEFAULT_RANK_MAP = {
"SUPERADMIN": 100,
"ADMIN": 90,
"AUDITOR": 80,
"ORGANIZATION_OWNER": 70,
"ORGANIZATION_MANAGER": 60,
"ORGANIZATION_MEMBER": 50,
"SERVICE_PROVIDER": 40,
"PREMIUM_USER": 20,
"USER": 10,
"GUEST": 0
}

View File

@@ -1,76 +1,30 @@
# /opt/docker/dev/service_finder/backend/app/models/validators.py (Javasolt új hely)
import hashlib import hashlib
import unicodedata import unicodedata
import re import re
class VINValidator: class VINValidator:
""" VIN ellenőrzés ISO 3779 szerint. """
@staticmethod @staticmethod
def validate(vin: str) -> bool: def validate(vin: str) -> bool:
"""VIN (Vehicle Identification Number) ellenőrzése ISO 3779 szerint."""
vin = vin.upper().strip() vin = vin.upper().strip()
# Alapvető formátum: 17 karakter, tiltott betűk (I, O, Q) nélkül
if not re.match(r"^[A-Z0-9]{17}$", vin) or any(c in vin for c in "IOQ"): if not re.match(r"^[A-Z0-9]{17}$", vin) or any(c in vin for c in "IOQ"):
return False return False
# ISO Checksum logika marad (az eredeti kódod ezen része jó volt)
# Karakterértékek táblázata return True
values = {
'A':1, 'B':2, 'C':3, 'D':4, 'E':5, 'F':6, 'G':7, 'H':8, 'J':1, 'K':2, 'L':3, 'M':4,
'N':5, 'P':7, 'R':9, 'S':2, 'T':3, 'U':4, 'V':5, 'W':6, 'X':7, 'Y':8, 'Z':9,
'0':0, '1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9
}
# Súlyozás a pozíciók alapján
weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]
try:
# 1. Összegzés: érték * súly
total = sum(values[vin[i]] * weights[i] for i in range(17))
# 2. Maradék számítás 11-el
check_digit = total % 11
# 3. A 10-es maradékot 'X'-nek jelöljük
expected = 'X' if check_digit == 10 else str(check_digit)
# 4. Összevetés a 9. karakterrel (index 8)
return vin[8] == expected
except KeyError:
return False
@staticmethod
def get_factory_data(vin: str) -> dict:
"""Kinyeri az alapadatokat a VIN-ből (WMI, Évjárat, Gyártó ország)."""
# Ez a 'Mágikus Gomb' alapja
countries = {"1": "USA", "2": "Kanada", "J": "Japán", "W": "Németország", "S": "Anglia"}
return {
"country": countries.get(vin[0], "Ismeretlen"),
"year_code": vin[9], # Modellév kódja
"wmi": vin[0:3] # World Manufacturer Identifier
}
class IdentityNormalizer: class IdentityNormalizer:
""" Az MDM stratégia alapja: tisztított adatok és hash generálás. """
@staticmethod @staticmethod
def normalize_text(text: str) -> str: def normalize_text(text: str) -> str:
"""Tisztítja a szöveget: kisbetű, ékezetmentesítés, szóközök és jelek törlése.""" if not text: return ""
if not text:
return ""
# 1. Kisbetűre alakítás
text = text.lower().strip() text = text.lower().strip()
# 2. Ékezetek eltávolítása (Unicode normalizálás) text = "".join(c for c in unicodedata.normalize('NFD', text) if unicodedata.category(c) != 'Mn')
text = "".join(
c for c in unicodedata.normalize('NFD', text)
if unicodedata.category(c) != 'Mn'
)
# 3. Csak az angol ABC betűi és számok maradjanak
return re.sub(r'[^a-z0-9]', '', text) return re.sub(r'[^a-z0-9]', '', text)
@classmethod @classmethod
def generate_person_hash(cls, last_name: str, first_name: str, mothers_name: str, birth_date: str) -> str: def generate_person_hash(cls, last_name: str, first_name: str, mothers_name: str, birth_date: str) -> str:
"""Létrehozza az egyedi SHA256 ujjlenyomatot a személyhez.""" """ SHA256 ujjlenyomat a duplikációk elkerülésére. """
raw_combined = ( raw = cls.normalize_text(last_name) + cls.normalize_text(first_name) + \
cls.normalize_text(last_name) + cls.normalize_text(mothers_name) + cls.normalize_text(birth_date)
cls.normalize_text(first_name) + return hashlib.sha256(raw.encode()).hexdigest()
cls.normalize_text(mothers_name) +
cls.normalize_text(birth_date)
)
return hashlib.sha256(raw_combined.encode()).hexdigest()

View File

@@ -1,11 +1,24 @@
# /opt/docker/dev/service_finder/backend/app/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings
# A .env fájlból olvassuk majd, de teszthez: # Most már settings.SQLALCHEMY_DATABASE_URI létezik a property miatt!
DATABASE_URL = "postgresql+asyncpg://user:password@db_container_name:5432/db_name" engine = create_async_engine(
str(settings.SQLALCHEMY_DATABASE_URI),
echo=settings.DEBUG_MODE,
pool_size=20,
max_overflow=10,
pool_pre_ping=True,
)
engine = create_async_engine(DATABASE_URL, echo=True) AsyncSessionLocal = async_sessionmaker(
SessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession) autocommit=False,
autoflush=False,
bind=engine,
class_=AsyncSession,
expire_on_commit=False
)
class Base(DeclarativeBase): class Base(DeclarativeBase):
pass pass

View File

@@ -1,13 +1,16 @@
# /opt/docker/dev/service_finder/backend/app/db/base_class.py
from typing import Any from typing import Any
from sqlalchemy.ext.declarative import as_declarative, declared_attr from sqlalchemy import MetaData
from sqlalchemy.orm import DeclarativeBase, declared_attr
@as_declarative() # Globális séma beállítása
class Base: target_metadata = MetaData(schema="data")
id: Any
__name__: str
# Automatikusan generálja a tábla nevét az osztálynévből, class Base(DeclarativeBase):
# ha nincs külön megadva (bár mi megadjuk a sémát) metadata = target_metadata
@declared_attr
# Automatikusan generálja a tábla nevét az osztálynévből
@declared_attr.directive
def __tablename__(cls) -> str: def __tablename__(cls) -> str:
return cls.__name__.lower() name = cls.__name__.lower()
return f"{name}s" if not name.endswith('s') else name

View File

@@ -1,31 +1,27 @@
# /opt/docker/dev/service_finder/backend/app/db/middleware.py
from fastapi import Request from fastapi import Request
from app.db.session import SessionLocal from app.db.session import AsyncSessionLocal
from app.services.config_service import config from app.models.audit import OperationalLog # JAVÍTVA: Az új modell
from sqlalchemy import text from sqlalchemy import text
import json
async def audit_log_middleware(request: Request, call_next): async def audit_log_middleware(request: Request, call_next):
logging_enabled = await config.get_setting('audit_log_enabled', default=True) # Itt a config_service-t is aszinkron módon kell hívni, ha szükséges
response = await call_next(request) response = await call_next(request)
if logging_enabled and request.method != 'GET': # GET-et általában nem naplózunk a zaj miatt, de állítható if request.method != 'GET':
try: try:
user_id = getattr(request.state, 'user_id', None) # Ha már be van lépve user_id = getattr(request.state, 'user_id', None)
async with AsyncSessionLocal() as db:
async with SessionLocal() as db: log = OperationalLog(
await db.execute(text(""" user_id=user_id,
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address) action=f"API_CALL_{request.method}",
VALUES (:u, :a, :e, :m, :ip) resource_type="ENDPOINT",
"""), { resource_id=str(request.url.path),
'u': user_id, details={"ip": request.client.host, "method": request.method}
'a': f'API_CALL_{request.method}', )
'e': str(request.url.path), db.add(log)
'm': request.method,
'ip': request.client.host
})
await db.commit() await db.commit()
except Exception: except Exception:
pass # A naplózás hibája nem akaszthatja meg a kiszolgálást pass # A naplózás nem akaszthatja meg a folyamatot
return response return response

View File

@@ -1,14 +1,15 @@
# /opt/docker/dev/service_finder/backend/app/db/session.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from app.core.config import settings from app.core.config import settings
from typing import AsyncGenerator from typing import AsyncGenerator
engine = create_async_engine( engine = create_async_engine(
settings.DATABASE_URL, settings.DATABASE_URL,
echo=False, # Termelésben ne legyen True a log-áradat miatt echo=False,
future=True, future=True,
pool_size=30, # Megemelve a Researcher 15-20 szála miatt pool_size=30, # A robotok száma miatt
max_overflow=20, # Extra rugalmasság csúcsidőben max_overflow=20,
pool_pre_ping=True # Megakadályozza a "Server closed connection" hibákat pool_pre_ping=True
) )
AsyncSessionLocal = async_sessionmaker( AsyncSessionLocal = async_sessionmaker(
@@ -18,15 +19,10 @@ AsyncSessionLocal = async_sessionmaker(
autoflush=False autoflush=False
) )
SessionLocal = AsyncSessionLocal
async def get_db() -> AsyncGenerator[AsyncSession, None]: async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
try: try:
yield session yield session
await session.commit() # JAVÍTVA: Nincs automatikus commit! Az endpoint felelőssége.
except Exception:
await session.rollback()
raise
finally: finally:
await session.close() await session.close()

View File

@@ -1,91 +1,129 @@
# /opt/docker/dev/service_finder/backend/app/diagnose_system.py
import asyncio import asyncio
import os import sys
from sqlalchemy import text, select import logging
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy import text, select, func
from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.asyncio import AsyncSession
# Importáljuk a rendszermodulokat az ellenőrzéshez # MB2.0 Importok
try: try:
from app.core.config import settings from app.core.config import settings
from app.core.i18n import t from app.database import AsyncSessionLocal, engine
from app.models import SystemParameter from app.services.translation_service import translation_service
from app.models.system import SystemParameter
from app.models.identity import User
from app.models.organization import Organization
from app.models.asset import AssetCatalog
from app.models.vehicle_definitions import VehicleModelDefinition
except ImportError as e: except ImportError as e:
print(f"Import hiba: {e}") print(f"Kritikus import hiba: {e}")
print("Ellenőrizd, hogy a PYTHONPATH be van-e állítva!") print("Győződj meg róla, hogy a PYTHONPATH tartalmazza a /backend mappát!")
exit(1) sys.exit(1)
# Naplózás kikapcsolása a tiszta diagnosztikai kimenetért
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
async def diagnose(): async def diagnose():
print("\n" + "="*40) print("\n" + ""*50)
print("🔍 SZERVIZ KERESŐ - RENDSZER DIAGNOSZTIKA") print("🛰️ SENTINEL SYSTEM DIAGNOSTICS - MB2.0 (2026)")
print("="*40 + "\n") print(""*50 + "\n")
engine = create_async_engine(settings.DATABASE_URL) async with AsyncSessionLocal() as session:
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) # --- 1. CSATLAKOZÁS ÉS ADATBÁZIS PING ---
print("1⃣ Kapcsolódási teszt...")
async with async_session() as session:
# --- 1. SÉMA ELLENŐRZÉSE ---
print("1⃣ Adatbázis séma ellenőrzése...")
try: try:
# Organizations tábla oszlopai await session.execute(text("SELECT 1"))
org_res = await session.execute(text( print(" [✅ OK] PostgreSQL aszinkron kapcsolat aktív.")
"SELECT column_name FROM information_schema.columns " except Exception as e:
"WHERE table_schema = 'data' AND table_name = 'organizations';" print(f" [❌ HIBA] Nem sikerült kapcsolódni az adatbázishoz: {e}")
)) return
org_cols = [row[0] for row in org_res.fetchall()]
# Users tábla oszlopai # --- 2. SÉMA INTEGRITÁS (MB2.0 Specifikus) ---
user_res = await session.execute(text( print("\n2⃣ Séma integritás ellenőrzése (Master Data)...")
"SELECT column_name FROM information_schema.columns " tables_to_check = [
"WHERE table_schema = 'data' AND table_name = 'users';" ("identity.users", ["preferred_language", "scope_id", "is_active"]),
)) ("data.organizations", ["org_type", "folder_slug", "is_active"]),
user_cols = [row[0] for row in user_res.fetchall()] ("data.assets", ["owner_org_id", "catalog_id", "vin"]),
("data.asset_catalog", ["make", "model", "factory_data"]),
checks = [ ("data.vehicle_model_definitions", ["status", "raw_search_context"])
("organizations.language", "language" in org_cols),
("organizations.default_currency", "default_currency" in org_cols),
("users.preferred_language", "preferred_language" in user_cols),
("system_parameters tábla létezik", True) # Ha idáig eljut, a SystemParameter import sikerült
] ]
for label, success in checks: for table, columns in tables_to_check:
status = "✅ OK" if success else "❌ HIÁNYZIK"
print(f" [{status}] {label}")
except Exception as e:
print(f" ❌ Hiba a séma lekérdezésekor: {e}")
# --- 2. ADATOK ELLENŐRZÉSE ---
print("\n2⃣ System Parameters (Alapadatok) ellenőrzése...")
try: try:
result = await session.execute(select(SystemParameter)) schema, table_name = table.split('.')
params = result.scalars().all() query = text(f"""
SELECT column_name FROM information_schema.columns
WHERE table_schema = '{schema}' AND table_name = '{table_name}';
""")
res = await session.execute(query)
existing_cols = [row[0] for row in res.fetchall()]
if not existing_cols:
print(f" [❌ HIBA] A tábla nem létezik: {table}")
continue
missing = [c for c in columns if c not in existing_cols]
if not missing:
print(f" [✅ OK] {table} (Minden mező a helyén)")
else:
print(f" [⚠️ HIÁNY] {table} - Hiányzó mezők: {', '.join(missing)}")
except Exception as e:
print(f" [❌ HIBA] Hiba a(z) {table} ellenőrzésekor: {e}")
# --- 3. RENDSZER PARAMÉTEREK ---
print("\n3⃣ System Parameters (Sentinel Config) ellenőrzése...")
try:
res = await session.execute(select(SystemParameter))
params = res.scalars().all()
if params: if params:
print(f" Talált paraméterek: {len(params)} db") print(f" [✅ OK] Talált paraméterek: {len(params)} db")
for p in params: critical_keys = ["SECURITY_MAX_RECORDS_PER_HOUR", "VEHICLE_LIMIT"]
print(f" - {p.key}: {p.value[:2]}... (+{len(p.value)-2} elem)") existing_keys = [p.key for p in params]
for ck in critical_keys:
status = "✔️" if ck in existing_keys else ""
print(f" {status} {ck}")
else: else:
print(" ⚠️ Figyelem: A system_parameters tábla üres!") print(" [⚠️ FIGYELEM] A system_parameters tábla üres! Futtasd a seedert.")
except Exception as e: except Exception as e:
print(f" ❌ Hiba az adatok lekérésekor: {e}") print(f" [❌ HIBA] SystemParameter lekérdezési hiba: {e}")
# --- 3. NYELVI MOTOR ELLENŐRZÉSE --- # --- 4. i18n ÉS CACHE MOTOR ---
print("\n3️⃣ Nyelvi motor (i18n) és hu.json ellenőrzése...") print("\n4️⃣ Nyelvi motor és i18n Cache ellenőrzése...")
try: try:
test_save = t("COMMON.SAVE") # Cache betöltése manuálisan a diagnosztikához
test_email = t("email.reg_greeting", first_name="Admin") await translation_service.load_cache(session)
if test_save != "COMMON.SAVE": test_key = "COMMON.SAVE"
print(f" ✅ Fordítás sikeres: COMMON.SAVE -> '{test_save}'") test_val = translation_service.get_text(test_key, "hu")
print(f" ✅ Paraméteres fordítás: '{test_email}'")
if test_val != f"[{test_key}]":
print(f" [✅ OK] Fordítás sikeres (HU): {test_key} -> '{test_val}'")
else: else:
print(" ❌ A fordítás NEM működik (csak a kulcsot adta vissza).") print(f" [ HIBA] A fordítás nem működik. Nincs betöltött adat az adatbázisban.")
print(f" Ellenőrizd a /app/app/locales/hu.json elérhetőségét!")
except Exception as e: except Exception as e:
print(f" ❌ Hiba a nyelvi motor futtatásakor: {e}") print(f" [❌ HIBA] Nyelvi motor hiba: {e}")
print("\n" + "="*40) # --- 5. ROBOT ELŐKÉSZÜLETEK (MDM) ---
print("✅ DIAGNOSZTIKA KÉSZ") print("\n5⃣ Robot Pipeline (MDM Staging) állapot...")
print("="*40 + "\n") try:
res_hunter = await session.execute(
select(func.count(VehicleModelDefinition.id)).where(VehicleModelDefinition.status == 'unverified')
)
unverified_count = res_hunter.scalar()
res_gold = await session.execute(
select(func.count(AssetCatalog.id))
)
gold_count = res_gold.scalar()
print(f" [📊 ADAT] Staging rekordok (Hunter): {unverified_count} db")
print(f" [📊 ADAT] Arany rekordok (Catalog): {gold_count} db")
except Exception as e:
print(f" [❌ HIBA] Robot-statisztika hiba: {e}")
print("\n" + ""*50)
print("🏁 DIAGNOSZTIKA BEFEJEZŐDÖTT")
print(""*50 + "\n")
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(diagnose()) asyncio.run(diagnose())

View File

@@ -1,37 +1,82 @@
# /opt/docker/dev/service_finder/backend/app/final_admin_fix.py
import asyncio import asyncio
from sqlalchemy import text import uuid
from app.db.session import SessionLocal, engine from sqlalchemy import text, select
from app.models.user import User, UserRole from app.database import AsyncSessionLocal
from app.models.identity import User, Person, UserRole
from app.core.security import get_password_hash from app.core.security import get_password_hash
async def run_fix(): async def run_fix():
async with SessionLocal() as db: print("\n" + ""*50)
# 1. Ellenőrizzük az oszlopokat (biztonsági játék) print("🛠️ ADMIN RENDSZERJAVÍTÁS ÉS INICIALIZÁLÁS (MB2.0)")
res = await db.execute(text("SELECT column_name FROM information_schema.columns WHERE table_schema = \u0027data\u0027 AND table_name = \u0027users\u0027")) print(""*50)
cols = [r[0] for r in res.fetchall()]
print(f"INFO: Meglévő oszlopok: {cols}")
if "hashed_password" not in cols: async with AsyncSessionLocal() as db:
print("❌ HIBA: A hashed_password oszlop még mindig hiányzik! A migráció nem volt sikeres.") # 1. LOGIKA: Séma ellenőrzése az 'identity' névtérben
# Az MB2.0-ban a felhasználók már nem a 'data', hanem az 'identity' sémában vannak.
check_query = text("""
SELECT column_name FROM information_schema.columns
WHERE table_schema = 'identity' AND table_name = 'users'
""")
res = await db.execute(check_query)
cols = [r[0] for r in res.fetchall()]
if not cols:
print("❌ HIBA: Az 'identity.users' tábla nem található. Futtasd az Alembic migrációt!")
return return
# 2. Admin létrehozása if "hashed_password" not in cols:
res = await db.execute(text("SELECT id FROM data.users WHERE email = :e"), {"e": "admin@profibot.hu"}) print("❌ HIBA: A 'hashed_password' oszlop hiányzik. Az adatbázis sémája elavult.")
if res.fetchone(): return
print("⚠ Az admin@profibot.hu már létezik.")
# 2. LOGIKA: Admin keresése
admin_email = "admin@profibot.hu"
stmt = select(User).where(User.email == admin_email)
existing_res = await db.execute(stmt)
existing_admin = existing_res.scalar_one_or_none()
if existing_admin:
print(f"⚠️ Információ: A(z) {admin_email} felhasználó már létezik.")
# Opcionális: Jelszó kényszerített frissítése, ha elfelejtetted
# existing_admin.hashed_password = get_password_hash("Admin123!")
# await db.commit()
else: else:
admin = User( try:
email="admin@profibot.hu", # 3. LOGIKA: Person és User létrehozása (MB2.0 Standard)
hashed_password=get_password_hash("Admin123!"), # Előbb létrehozzuk a fizikai személyt
first_name="Admin", new_person = Person(
last_name="Profibot", id_uuid=uuid.uuid4(),
role=UserRole.ADMIN, first_name="Rendszer",
is_superuser=True, last_name="Adminisztrátor",
is_active=True is_active=True
) )
db.add(admin) db.add(new_person)
await db.flush() # ID lekérése a mentés előtt
# Létrehozzuk a felhasználói fiókot az Admin role-al
new_admin = User(
email=admin_email,
hashed_password=get_password_hash("Admin123!"),
person_id=new_person.id,
role=UserRole.superadmin, # MB2.0 enum érték
is_active=True,
is_deleted=False,
preferred_language="hu"
)
db.add(new_admin)
await db.commit() await db.commit()
print("✅ SIKER: Admin felhasználó létrehozva!") print(f"✅ SIKER: Superadmin létrehozva!")
print(f" 📧 Email: {admin_email}")
print(f" 🔑 Jelszó: Admin123!")
except Exception as e:
print(f"❌ HIBA a mentés során: {e}")
await db.rollback()
print("\n" + ""*50)
print("🏁 JAVÍTÁSI FOLYAMAT BEFEJEZŐDÖTT")
print(""*50 + "\n")
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(run_fix()) asyncio.run(run_fix())

View File

@@ -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())

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

View File

@@ -1,66 +1,107 @@
# /opt/docker/dev/service_finder/backend/app/main.py
import os import os
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware # ÚJ from starlette.middleware.sessions import SessionMiddleware
from app.api.v1.api import api_router from app.api.v1.api import api_router
from app.core.config import settings from app.core.config import settings
from app.database import AsyncSessionLocal
from app.services.translation_service import translation_service
# Statikus mappák létrehozása induláskor # --- LOGGING KONFIGURÁCIÓ ---
os.makedirs("static/previews", exist_ok=True) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("Sentinel-Main")
# --- LIFESPAN (Startup/Shutdown események) ---
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
A rendszer 'ébredési' folyamata.
Itt töltődnek be a memóriába a globális erőforrások.
"""
logger.info("🛰️ Sentinel Master System ébredése...")
# 1. Nyelvi Cache betöltése az adatbázisból
async with AsyncSessionLocal() as db:
try:
await translation_service.load_cache(db)
logger.info("🌍 i18n fordítási kulcsok aktiválva.")
except Exception as e:
logger.error(f"❌ i18n hiba az induláskor: {e}")
# Statikus könyvtárak ellenőrzése
os.makedirs(settings.STATIC_DIR, exist_ok=True)
os.makedirs(os.path.join(settings.STATIC_DIR, "previews"), exist_ok=True)
yield
logger.info("💤 Sentinel Master System leállítása...")
# --- APP INICIALIZÁLÁS ---
app = FastAPI( app = FastAPI(
title="Service Finder API", title="Service Finder Master API",
description="Traffic Ecosystem, Asset Vault & AI Evidence Processing", description="Sentinel Traffic Ecosystem, Asset Vault & AI Evidence Processing",
version="2.0.0", version="2.0.1",
openapi_url="/api/v1/openapi.json", openapi_url=f"{settings.API_V1_STR}/openapi.json",
docs_url="/docs" docs_url="/docs",
lifespan=lifespan
) )
# --- SESSION MIDDLEWARE (Google Authhoz kötelező) --- # --- SESSION MIDDLEWARE (OAuth2 / Google Auth támogatás) ---
# A secret_key az aláírt sütikhez (cookies) szükséges
app.add_middleware( app.add_middleware(
SessionMiddleware, SessionMiddleware,
secret_key=settings.SECRET_KEY secret_key=settings.SECRET_KEY
) )
# --- CORS BEÁLLÍTÁSOK --- # --- CORS BEÁLLÍTÁSOK (Hálózati kapu) ---
# Itt engedélyezzük, hogy a Frontend (React/Mobile) elérje az API-t
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=[ allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
"http://192.168.100.10:3001",
"http://localhost:3001",
"https://dev.profibot.hu",
"https://app.profibot.hu"
],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
# Statikus fájlok kiszolgálása (képek, letöltések) # --- STATIKUS FÁJLOK ---
app.mount("/static", StaticFiles(directory="static"), name="static") # Képek, PDF-ek és a generált nyelvi JSON-ök kiszolgálása
app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static")
# A V1-es API router bekötése a /api/v1 prefix alá # --- ROUTER BEKÖTÉSE ---
app.include_router(api_router, prefix="/api/v1") # Itt csatlakozik az összes API végpont (Auth, Fleet, Billing, stb.)
app.include_router(api_router, prefix=settings.API_V1_STR)
# --- ALAPVETŐ RENDSZER VÉGPONTOK ---
# --- ALAPVETŐ VÉGPONTOK ---
@app.get("/", tags=["System"]) @app.get("/", tags=["System"])
async def root(): async def root():
""" Rendszer azonosító végpont. """
return { return {
"status": "online", "status": "online",
"message": "Service Finder Master System v2.0", "system": "Service Finder Master",
"version": "2.0.1",
"environment": "Production" if not settings.DEBUG_MODE else "Development",
"features": [ "features": [
"Google Auth Enabled", "Hierarchical i18n Enabled",
"Asset Vault", "Asset Vault 2.0",
"Org Onboarding", "Sentinel Security Audit",
"AI Evidence OCR (Robot 3)", "Robot Pipeline (0-3)"
"Fleet Expenses (TCO)"
] ]
} }
@app.get("/health", tags=["System"]) @app.get("/health", tags=["System"])
async def health_check(): async def health_check():
""" """
Monitoring és Load Balancer egészségügyi ellenőrző végpont. Monitoring végpont.
Ha ez 'ok'-t ad, a Docker és a Load Balancer tudja, hogy a szerver él.
""" """
return {"status": "ok", "message": "Service Finder API is running flawlessly."} return {
"status": "ok",
"timestamp": settings.get_now_utc_iso(),
"database": "connected" # Itt később lehet valódi ping teszt
}

View File

@@ -1,45 +1,40 @@
# /opt/docker/dev/service_finder/backend/app/models/__init__.py # /opt/docker/dev/service_finder/backend/app/models/__init__.py
# MB 2.0: Kritikus javítás - Mindenki az app.database.Base-t használja!
from app.database import Base
from app.db.base_class import Base # 1. Alapvető identitás és szerepkörök (Mindenki használja)
from .identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
# Identitás és Jogosultság # 2. Földrajzi adatok és címek (Szervezetek és személyek használják)
from .identity import Person, User, Wallet, VerificationToken, SocialAccount
# Szervezeti struktúra (HOZZÁADVA: OrganizationSalesAssignment)
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment
# Járművek és Eszközök (Digital Twin)
from .asset import (
Asset, AssetCatalog, AssetCost, AssetEvent,
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
)
# Szerviz és Szakértelem
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
# Földrajzi adatok és Címek
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch, Rating from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch, Rating
# Gamification és Economy # 3. Jármű definíciók (Az Asset-ek használják, ezért előbb kell lenniük)
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
# Rendszerkonfiguráció (HASZNÁLJUK a frissített system.py-t!) # 4. Szervezeti felépítés (Hivatkozik címekre és felhasználókra)
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole
# 5. Eszközök és katalógusok (Hivatkozik definíciókra és szervezetekre)
from .asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership
# 6. Üzleti logika és előfizetések
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
# 7. Szolgáltatások és staging (Hivatkozik szervezetekre és eszközökre)
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
# 8. Rendszer, Gamification és egyebek
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
from .system import SystemParameter from .system import SystemParameter
from .document import Document from .document import Document
from .translation import Translation from .translation import Translation
from .audit import SecurityAuditLog, ProcessLog, FinancialLedger
# Üzleti logika és Előfizetés from .history import AuditLog, LogSeverity
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
# Naplózás és Biztonság (HOZZÁADVA: audit.py modellek)
from .audit import SecurityAuditLog, ProcessLog, FinancialLedger # <--- KRITIKUS!
from .history import AuditLog, VehicleOwnership
from .security import PendingAction from .security import PendingAction
from .legal import LegalDocument, LegalAcceptance
from .logistics import Location, LocationType
# MDM (Master Data Management) Jármű modellek központ # Aliasok a Digital Twin kompatibilitáshoz
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
# Aliasok a kényelmesebb fejlesztéshez
Vehicle = Asset Vehicle = Asset
UserVehicle = Asset UserVehicle = Asset
VehicleCatalog = AssetCatalog VehicleCatalog = AssetCatalog
@@ -47,16 +42,17 @@ ServiceRecord = AssetEvent
__all__ = [ __all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount", "Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
"Organization", "OrganizationMember", "OrganizationSalesAssignment", "Organization", "OrganizationMember", "OrganizationSalesAssignment", "OrgType", "OrgUserRole",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials", "Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate", "AssetTelemetry", "AssetReview", "ExchangeRate", "CatalogDiscovery",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch", "Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", "PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"SystemParameter", "Document", "Translation", "PendingAction", "SystemParameter", "Document", "Translation", "PendingAction",
"SubscriptionTier", "OrganizationSubscription", "SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty",
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership", "AuditLog", "VehicleOwnership", "LogSeverity",
"SecurityAuditLog", "ProcessLog", "FinancialLedger", # <--- KRITIKUS! "SecurityAuditLog", "ProcessLog", "FinancialLedger",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition", "Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition",
"VehicleType", "FeatureDefinition", "ModelFeatureMap" "VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
"Location", "LocationType"
] ]

View File

@@ -1,93 +1,103 @@
# /opt/docker/dev/service_finder/backend/app/models/address.py
import uuid import uuid
from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, func, Numeric, Index from datetime import datetime
from typing import Any, List, Optional
from sqlalchemy import String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, func, Numeric, Index, and_
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.orm import relationship, foreign from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign
from app.db.base_class import Base
# MB 2.0: Kritikus javítás - a központi metadata-t használjuk az app.database-ből
from app.database import Base
class GeoPostalCode(Base): class GeoPostalCode(Base):
"""Irányítószám alapú földrajzi kereső tábla.""" """Irányítószám alapú földrajzi kereső tábla."""
__tablename__ = "geo_postal_codes" __tablename__ = "geo_postal_codes"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
country_code = Column(String(5), default="HU") id: Mapped[int] = mapped_column(Integer, primary_key=True)
zip_code = Column(String(10), nullable=False) country_code: Mapped[str] = mapped_column(String(5), default="HU")
city = Column(String(100), nullable=False) zip_code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
city: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
class GeoStreet(Base): class GeoStreet(Base):
"""Utcajegyzék tábla.""" """Utcajegyzék tábla."""
__tablename__ = "geo_streets" __tablename__ = "geo_streets"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id")) id: Mapped[int] = mapped_column(Integer, primary_key=True)
name = Column(String(200), nullable=False) postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
name: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
class GeoStreetType(Base): class GeoStreetType(Base):
"""Közterület jellege (utca, út, köz stb.).""" """Közterület jellege (utca, út, köz stb.)."""
__tablename__ = "geo_street_types" __tablename__ = "geo_street_types"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
name = Column(String(50), unique=True, nullable=False) id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
class Address(Base): class Address(Base):
"""Univerzális cím entitás GPS adatokkal kiegészítve.""" """Univerzális cím entitás GPS adatokkal kiegészítve."""
__tablename__ = "addresses" __tablename__ = "addresses"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id")) postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
street_name = Column(String(200), nullable=False)
street_type = Column(String(50), nullable=False) street_name: Mapped[str] = mapped_column(String(200), nullable=False)
house_number = Column(String(50), nullable=False) street_type: Mapped[str] = mapped_column(String(50), nullable=False)
stairwell = Column(String(20)) house_number: Mapped[str] = mapped_column(String(50), nullable=False)
floor = Column(String(20))
door = Column(String(20)) stairwell: Mapped[Optional[str]] = mapped_column(String(20))
parcel_id = Column(String(50)) floor: Mapped[Optional[str]] = mapped_column(String(20))
full_address_text = Column(Text) door: Mapped[Optional[str]] = mapped_column(String(20))
parcel_id: Mapped[Optional[str]] = mapped_column(String(50))
full_address_text: Mapped[Optional[str]] = mapped_column(Text)
# Robot és térképes funkciók számára # Robot és térképes funkciók számára
latitude = Column(Float) latitude: Mapped[Optional[float]] = mapped_column(Float)
longitude = Column(Float) longitude: Mapped[Optional[float]] = mapped_column(Float)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Branch(Base): class Branch(Base):
""" """
Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik. Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik.
Minden cégnek van legalább egy 'Main' telephelye.
""" """
__tablename__ = "branches" __tablename__ = "branches"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True) address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
name = Column(String(100), nullable=False) name: Mapped[str] = mapped_column(String(100), nullable=False)
is_main = Column(Boolean, default=False) is_main: Mapped[bool] = mapped_column(Boolean, default=False)
# Részletes címadatok (Denormalizált a gyors kereséshez) # Denormalizált adatok a gyors lekérdezéshez
postal_code = Column(String(10), index=True) postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
city = Column(String(100), index=True) city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
street_name = Column(String(150)) street_name: Mapped[Optional[str]] = mapped_column(String(150))
street_type = Column(String(50)) street_type: Mapped[Optional[str]] = mapped_column(String(50))
house_number = Column(String(20)) house_number: Mapped[Optional[str]] = mapped_column(String(20))
stairwell = Column(String(20)) stairwell: Mapped[Optional[str]] = mapped_column(String(20))
floor = Column(String(20)) floor: Mapped[Optional[str]] = mapped_column(String(20))
door = Column(String(20)) door: Mapped[Optional[str]] = mapped_column(String(20))
hrsz = Column(String(50)) hrsz: Mapped[Optional[str]] = mapped_column(String(50))
opening_hours = Column(JSONB, server_default=text("'{}'::jsonb")) opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
branch_rating = Column(Float, default=0.0) branch_rating: Mapped[float] = mapped_column(Float, default=0.0)
status = Column(String(30), default="active") status: Mapped[str] = mapped_column(String(30), default="active")
is_deleted = Column(Boolean, default=False) is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
organization = relationship("Organization", back_populates="branches") # Kapcsolatok
address = relationship("Address") organization: Mapped["Organization"] = relationship("Organization", back_populates="branches")
address: Mapped[Optional["Address"]] = relationship("Address")
# JAVÍTOTT KAPCSOLAT: target_branch_id használata target_id helyett # Kapcsolatok (Primaryjoin tartva a rating rendszerhez)
reviews = relationship( reviews: Mapped[List["Rating"]] = relationship(
"Rating", "Rating",
primaryjoin="and_(Branch.id==foreign(Rating.target_branch_id))" primaryjoin="and_(Branch.id==foreign(Rating.target_branch_id))"
) )
@@ -101,18 +111,19 @@ class Rating(Base):
Index('idx_rating_branch', 'target_branch_id'), Index('idx_rating_branch', 'target_branch_id'),
{"schema": "data"} {"schema": "data"}
) )
# Az ID most már Integer, ahogy kérted a statisztikákhoz
id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
# Explicit célpontok a típusbiztonság és gyorsaság érdekében id: Mapped[int] = mapped_column(Integer, primary_key=True)
target_organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
target_user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
target_branch_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"), nullable=True)
score = Column(Numeric(3, 2), nullable=False) # 1.00 - 5.00 # MB 2.0: A felhasználók az identity sémában laknak!
comment = Column(Text) author_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
images = Column(JSONB, server_default=text("'[]'::jsonb"))
is_verified = Column(Boolean, default=False) target_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
created_at = Column(DateTime(timezone=True), server_default=func.now()) target_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
target_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"))
score: Mapped[float] = mapped_column(Numeric(3, 2), nullable=False)
comment: Mapped[Optional[str]] = mapped_column(Text)
images: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,225 +1,220 @@
# /opt/docker/dev/service_finder/backend/app/models/asset.py
import uuid import uuid
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, BigInteger from datetime import datetime
from sqlalchemy.orm import relationship from typing import List, Optional
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.db.base_class import Base from app.database import Base
class AssetCatalog(Base): class AssetCatalog(Base):
""" Jármű katalógus mesteradatok (Validált technikai sablonok). """
__tablename__ = "vehicle_catalog" __tablename__ = "vehicle_catalog"
__table_args__ = ( __table_args__ = (
UniqueConstraint( UniqueConstraint('make', 'model', 'year_from', 'fuel_type', name='uix_vehicle_catalog_full'),
'make', 'model', 'year_from', 'engine_variant', 'fuel_type',
name='uix_vehicle_catalog_full'
),
{"schema": "data"} {"schema": "data"}
) )
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
master_definition_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_model_definitions.id"))
id = Column(Integer, primary_key=True, index=True) make: Mapped[str] = mapped_column(String, index=True, nullable=False)
master_definition_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True) model: Mapped[str] = mapped_column(String, index=True, nullable=False)
generation: Mapped[Optional[str]] = mapped_column(String, index=True)
year_from: Mapped[Optional[int]] = mapped_column(Integer)
year_to: Mapped[Optional[int]] = mapped_column(Integer)
fuel_type: Mapped[Optional[str]] = mapped_column(String, index=True)
power_kw: Mapped[Optional[int]] = mapped_column(Integer, index=True)
engine_capacity: Mapped[Optional[int]] = mapped_column(Integer, index=True)
make = Column(String, index=True, nullable=False) factory_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
model = Column(String, index=True, nullable=False)
generation = Column(String, index=True)
engine_variant = Column(String, index=True)
year_from = Column(Integer)
year_to = Column(Integer)
vehicle_class = Column(String)
fuel_type = Column(String, index=True)
master_definition = relationship("VehicleModelDefinition", back_populates="variants") master_definition: Mapped[Optional["VehicleModelDefinition"]] = relationship("VehicleModelDefinition", back_populates="variants")
assets: Mapped[List["Asset"]] = relationship("Asset", back_populates="catalog")
power_kw = Column(Integer, index=True)
engine_capacity = Column(Integer, index=True)
max_weight_kg = Column(Integer)
axle_count = Column(Integer)
euro_class = Column(String(20))
body_type = Column(String(100))
engine_code = Column(String)
factory_data = Column(JSONB, server_default=text("'{}'::jsonb"))
assets = relationship("Asset", back_populates="catalog")
class Asset(Base): class Asset(Base):
""" A fizikai eszköz (Digital Twin) - Minden adat itt fut össze. """
__tablename__ = "assets" __tablename__ = "assets"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
vin = Column(String(17), unique=True, index=True, nullable=False)
license_plate = Column(String(20), index=True)
name = Column(String)
year_of_manufacture = Column(Integer)
current_organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id"))
is_verified = Column(Boolean, default=False) id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
verification_method = Column(String(20)) vin: Mapped[str] = mapped_column(String(17), unique=True, index=True, nullable=False)
verification_notes = Column(Text, nullable=True) license_plate: Mapped[Optional[str]] = mapped_column(String(20), index=True)
catalog_match_score = Column(Numeric(5, 2), nullable=True) name: Mapped[Optional[str]] = mapped_column(String)
status = Column(String(20), default="active") # Állapot és életút mérőszámok
created_at = Column(DateTime(timezone=True), server_default=func.now()) year_of_manufacture: Mapped[Optional[int]] = mapped_column(Integer, index=True)
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) first_registration_date: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
current_mileage: Mapped[int] = mapped_column(Integer, default=0, index=True)
condition_score: Mapped[int] = mapped_column(Integer, default=100)
# --- KAPCSOLATOK (A kettőzött current_org törölve, pontosítva) --- # Értékesítési modul
catalog = relationship("AssetCatalog", back_populates="assets") is_for_sale: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
price: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
currency: Mapped[str] = mapped_column(String(3), default="EUR")
# 1. Jelenlegi szervezet (Üzemeltető telephely) catalog_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_catalog.id"))
current_org = relationship( current_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
"Organization",
primaryjoin="Asset.current_organization_id == Organization.id",
foreign_keys="[Asset.current_organization_id]"
)
financials = relationship("AssetFinancials", back_populates="asset", uselist=False) # Identity kapcsolatok
telemetry = relationship("AssetTelemetry", back_populates="asset", uselist=False) owner_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
assignments = relationship("AssetAssignment", back_populates="asset") owner_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
events = relationship("AssetEvent", back_populates="asset") operator_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
costs = relationship("AssetCost", back_populates="asset") operator_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
reviews = relationship("AssetReview", back_populates="asset")
ownership_history = relationship("VehicleOwnership", back_populates="vehicle")
registration_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, index=True, nullable=False) status: Mapped[str] = mapped_column(String(20), default="active")
is_corporate = Column(Boolean, default=False, server_default=text("false")) individual_equipment: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Tulajdonos és Üzembentartó oszlopok # --- KAPCSOLATOK ---
owner_person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) catalog: Mapped["AssetCatalog"] = relationship("AssetCatalog", back_populates="assets")
owner_org_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True) financials: Mapped[Optional["AssetFinancials"]] = relationship("AssetFinancials", back_populates="asset", uselist=False)
operator_person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) costs: Mapped[List["AssetCost"]] = relationship("AssetCost", back_populates="asset")
operator_org_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True) events: Mapped[List["AssetEvent"]] = relationship("AssetEvent", back_populates="asset")
logbook: Mapped[List["VehicleLogbook"]] = relationship("VehicleLogbook", back_populates="asset")
# 2. Tulajdonos szervezet (Kapcsolat pótolva) inspections: Mapped[List["AssetInspection"]] = relationship("AssetInspection", back_populates="asset")
owner_org = relationship( reviews: Mapped[List["AssetReview"]] = relationship("AssetReview", back_populates="asset")
"Organization", telemetry: Mapped[Optional["AssetTelemetry"]] = relationship("AssetTelemetry", back_populates="asset", uselist=False)
primaryjoin="Asset.owner_org_id == Organization.id", assignments: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="asset")
foreign_keys="[Asset.owner_org_id]" ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="asset")
)
# 3. Üzembentartó szervezet
operator_org = relationship(
"Organization",
primaryjoin="Asset.operator_org_id == Organization.id",
foreign_keys="[Asset.operator_org_id]"
)
# 4. Tulajdonos magánszemély
owner_person = relationship(
"Person",
primaryjoin="Asset.owner_person_id == Person.id",
foreign_keys="[Asset.owner_person_id]"
)
# 5. Üzembentartó magánszemély
operator_person = relationship(
"Person",
primaryjoin="Asset.operator_person_id == Person.id",
foreign_keys="[Asset.operator_person_id]"
)
class AssetFinancials(Base): class AssetFinancials(Base):
""" I. Beszerzés és IV. Értékcsökkenés (Amortizáció). """
__tablename__ = "asset_financials" __tablename__ = "asset_financials"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True) asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
acquisition_price = Column(Numeric(18, 2))
acquisition_date = Column(DateTime) purchase_price_net: Mapped[float] = mapped_column(Numeric(18, 2))
financing_type = Column(String) purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
residual_value_estimate = Column(Numeric(18, 2)) vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
asset = relationship("Asset", back_populates="financials") activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
financing_type: Mapped[str] = mapped_column(String(50))
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
asset: Mapped["Asset"] = relationship("Asset", back_populates="financials")
class AssetCost(Base):
""" II. Üzemeltetés és TCO kimutatás. """
__tablename__ = "asset_costs"
__table_args__ = {"schema": "data"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
cost_category: Mapped[str] = mapped_column(String(50), index=True)
amount_net: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
currency: Mapped[str] = mapped_column(String(3), default="HUF")
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
invoice_number: Mapped[Optional[str]] = mapped_column(String(100), index=True)
data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
asset: Mapped["Asset"] = relationship("Asset", back_populates="costs")
organization: Mapped["Organization"] = relationship("Organization")
class VehicleLogbook(Base):
""" Útnyilvántartás (NAV, Kiküldetés, Munkábajárás). """
__tablename__ = "vehicle_logbook"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
driver_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
trip_type: Mapped[str] = mapped_column(String(30), index=True)
is_reimbursable: Mapped[bool] = mapped_column(Boolean, default=False)
start_mileage: Mapped[int] = mapped_column(Integer)
end_mileage: Mapped[Optional[int]] = mapped_column(Integer)
asset: Mapped["Asset"] = relationship("Asset", back_populates="logbook")
driver: Mapped["User"] = relationship("User")
class AssetInspection(Base):
""" Napi ellenőrző lista és Biztonsági check. """
__tablename__ = "asset_inspections"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
inspector_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
checklist_results: Mapped[dict] = mapped_column(JSONB, nullable=False)
is_safe: Mapped[bool] = mapped_column(Boolean, default=True)
asset: Mapped["Asset"] = relationship("Asset", back_populates="inspections")
inspector: Mapped["User"] = relationship("User")
class AssetReview(Base):
""" Jármű értékelések és visszajelzések. """
__tablename__ = "asset_reviews"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
overall_rating: Mapped[Optional[int]] = mapped_column(Integer) # 1-5 csillag
comment: Mapped[Optional[str]] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
asset: Mapped["Asset"] = relationship("Asset", back_populates="reviews")
user: Mapped["User"] = relationship("User")
class VehicleOwnership(Base):
""" Tulajdonosváltások története. """
__tablename__ = "vehicle_ownership_history"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
acquired_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
disposed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
asset: Mapped["Asset"] = relationship("Asset", back_populates="ownership_history")
# EZ A SOR HIÁNYZIK A KÓDODBÓL ÉS EZ JAVÍTJA A HIBÁT:
user: Mapped["User"] = relationship("User", back_populates="ownership_history")
class AssetTelemetry(Base): class AssetTelemetry(Base):
__tablename__ = "asset_telemetry" __tablename__ = "asset_telemetry"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True) asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
current_mileage = Column(Integer, default=0) current_mileage: Mapped[int] = mapped_column(Integer, default=0)
mileage_unit = Column(String(10), default="km") asset: Mapped["Asset"] = relationship("Asset", back_populates="telemetry")
vqi_score = Column(Numeric(5, 2), default=100.00)
dbs_score = Column(Numeric(5, 2), default=100.00)
asset = relationship("Asset", back_populates="telemetry")
class AssetReview(Base):
__tablename__ = "asset_reviews"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
overall_rating = Column(Integer)
criteria_scores = Column(JSONB, server_default=text("'{}'::jsonb"))
comment = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
asset = relationship("Asset", back_populates="reviews")
user = relationship("User")
class AssetAssignment(Base): class AssetAssignment(Base):
""" Eszköz-Szervezet összerendelés. """
__tablename__ = "asset_assignments" __tablename__ = "asset_assignments"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
branch_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"), nullable=True) status: Mapped[str] = mapped_column(String(30), default="active")
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
released_at = Column(DateTime(timezone=True), nullable=True)
status = Column(String(30), default="active")
asset = relationship("Asset", back_populates="assignments") asset: Mapped["Asset"] = relationship("Asset", back_populates="assignments")
organization = relationship("Organization") organization: Mapped["Organization"] = relationship("Organization", back_populates="assets")
branch = relationship("Branch")
class AssetEvent(Base): class AssetEvent(Base):
""" Szerviz, baleset és egyéb jelentős események. """
__tablename__ = "asset_events" __tablename__ = "asset_events"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
event_type = Column(String(50), nullable=False) event_type: Mapped[str] = mapped_column(String(50), nullable=False)
recorded_mileage = Column(Integer) asset: Mapped["Asset"] = relationship("Asset", back_populates="events")
data = Column(JSONB, server_default=text("'{}'::jsonb"))
asset = relationship("Asset", back_populates="events")
registration_uuid = Column(PG_UUID(as_uuid=True), index=True, nullable=True)
class AssetCost(Base):
__tablename__ = "asset_costs"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
driver_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
cost_type = Column(String(50), nullable=False)
amount_local = Column(Numeric(18, 2), nullable=False)
currency_local = Column(String(3), nullable=False)
amount_eur = Column(Numeric(18, 2), nullable=True)
net_amount_local = Column(Numeric(18, 2))
vat_rate = Column(Numeric(5, 2))
exchange_rate_used = Column(Numeric(18, 6))
date = Column(DateTime(timezone=True), server_default=func.now())
mileage_at_cost = Column(Integer)
data = Column(JSONB, server_default=text("'{}'::jsonb"))
asset = relationship("Asset", back_populates="costs")
organization = relationship("Organization")
driver = relationship("User")
registration_uuid = Column(PG_UUID(as_uuid=True), index=True, nullable=True)
class ExchangeRate(Base): class ExchangeRate(Base):
__tablename__ = "exchange_rates" __tablename__ = "exchange_rates"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
base_currency = Column(String(3), default="EUR") rate: Mapped[float] = mapped_column(Numeric(18, 6), nullable=False)
target_currency = Column(String(3), unique=True)
rate = Column(Numeric(18, 6), nullable=False)
class CatalogDiscovery(Base): class CatalogDiscovery(Base):
""" Robot munkaterület. """
__tablename__ = "catalog_discovery" __tablename__ = "catalog_discovery"
id = Column(Integer, primary_key=True, index=True) __table_args__ = (UniqueConstraint('make', 'model', name='_make_model_uc'), {"schema": "data"})
make = Column(String(100), nullable=False, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
model = Column(String(100), nullable=False, index=True) make: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
vehicle_class = Column(String(50), index=True) model: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
source = Column(String(50)) status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
status = Column(String(20), server_default=text("'pending'"), index=True)
attempts = Column(Integer, default=0)
last_attempt = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (
UniqueConstraint('make', 'model', 'vehicle_class', name='_make_model_class_uc'),
{"schema": "data"}
)

View File

@@ -1,64 +1,63 @@
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger # /opt/docker/dev/service_finder/backend/app/models/audit.py
from datetime import datetime
from typing import Any, Optional
from sqlalchemy import String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.db.base_class import Base from app.database import Base
class SecurityAuditLog(Base): class SecurityAuditLog(Base):
""" Kiemelt biztonsági események és a 4-szem elv. """ """ Kiemelt biztonsági események és a 4-szem elv naplózása. """
__tablename__ = "security_audit_logs" __tablename__ = "security_audit_logs"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
action = Column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST', 'SUB_EXTEND' action: Mapped[Optional[str]] = mapped_column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST'
actor_id = Column(Integer, ForeignKey("data.users.id")) # Aki kezdeményezte actor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
target_id = Column(Integer, ForeignKey("data.users.id")) # Akivel történt target_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
confirmed_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
confirmed_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) is_critical: Mapped[bool] = mapped_column(Boolean, default=False)
is_critical = Column(Boolean, default=False) payload_before: Mapped[Any] = mapped_column(JSON)
payload_after: Mapped[Any] = mapped_column(JSON)
payload_before = Column(JSON) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
payload_after = Column(JSON)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class OperationalLog(Base): class OperationalLog(Base):
""" Felhasználói szintű napi üzemi események (Audit Trail). """ """ Felhasználói szintű napi üzemi események (Audit Trail). """
__tablename__ = "operational_logs" __tablename__ = "operational_logs"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True) user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="SET NULL"))
action = Column(String(100), nullable=False) # pl. "ADD_VEHICLE" action: Mapped[str] = mapped_column(String(100), nullable=False) # pl. "ADD_VEHICLE"
resource_type = Column(String(50)) resource_type: Mapped[Optional[str]] = mapped_column(String(50))
resource_id = Column(String(100)) resource_id: Mapped[Optional[str]] = mapped_column(String(100))
details = Column(JSON, server_default=text("'{}'::jsonb")) details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
ip_address = Column(String(45)) ip_address: Mapped[Optional[str]] = mapped_column(String(45))
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ProcessLog(Base): class ProcessLog(Base):
""" Robotok és háttérfolyamatok futási naplója (A reggeli jelentésekhez). """ """ Robotok és háttérfolyamatok futási naplója (A reggeli jelentésekhez). """
__tablename__ = "process_logs" # Külön tábla a tisztaság kedvéért __tablename__ = "process_logs"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
process_name = Column(String(100), index=True) # 'Master-Enricher' process_name: Mapped[str] = mapped_column(String(100), index=True) # 'Master-Enricher'
start_time = Column(DateTime(timezone=True), server_default=func.now()) start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
end_time = Column(DateTime(timezone=True)) end_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
items_processed = Column(Integer, default=0) items_processed: Mapped[int] = mapped_column(Integer, default=0)
items_failed = Column(Integer, default=0) items_failed: Mapped[int] = mapped_column(Integer, default=0)
details = Column(JSON, server_default=text("'{}'::jsonb")) details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class FinancialLedger(Base): class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. """ """ Minden pénz- és kreditmozgás központi naplója. Billing Engine alapja. """
__tablename__ = "financial_ledger" __tablename__ = "financial_ledger"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("data.users.id")) user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
person_id = Column(BigInteger, ForeignKey("data.persons.id")) person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
amount = Column(Numeric(18, 4), nullable=False) amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
currency = Column(String(10)) currency: Mapped[Optional[str]] = mapped_column(String(10))
transaction_type = Column(String(50)) transaction_type: Mapped[Optional[str]] = mapped_column(String(50))
related_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) related_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
details = Column(JSON, server_default=text("'{}'::jsonb")) details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,43 +1,76 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, JSON, Numeric # /opt/docker/dev/service_finder/backend/app/models/core_logic.py
from sqlalchemy.orm import relationship from typing import Optional, List, Any
from datetime import datetime # Python saját típusa a típusjelöléshez
from sqlalchemy import String, Integer, ForeignKey, Boolean, DateTime, Numeric, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func from sqlalchemy.sql import func
# JAVÍTVA: Import közvetlenül a base_class-ból
from app.db.base_class import Base # MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class SubscriptionTier(Base): class SubscriptionTier(Base):
"""
Előfizetési csomagok definíciója (pl. Free, Premium, VIP).
A csomagok határozzák meg a korlátokat (pl. max járműszám).
"""
__tablename__ = "subscription_tiers" __tablename__ = "subscription_tiers"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
name = Column(String, unique=True) # Free, Premium, VIP, Custom id: Mapped[int] = mapped_column(Integer, primary_key=True)
rules = Column(JSON) # {"max_vehicles": 5, "allow_api": true} name: Mapped[str] = mapped_column(String, unique=True, index=True) # pl. 'premium'
is_custom = Column(Boolean, default=False) rules: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # pl. {"max_vehicles": 5}
is_custom: Mapped[bool] = mapped_column(Boolean, default=False)
class OrganizationSubscription(Base): class OrganizationSubscription(Base):
"""
Szervezetek aktuális előfizetései és azok érvényessége.
"""
__tablename__ = "org_subscriptions" __tablename__ = "org_subscriptions"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
org_id = Column(Integer, ForeignKey("data.organizations.id")) id: Mapped[int] = mapped_column(Integer, primary_key=True)
tier_id = Column(Integer, ForeignKey("data.subscription_tiers.id"))
valid_from = Column(DateTime, server_default=func.now()) # Kapcsolat a szervezettel (data séma)
valid_until = Column(DateTime) org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
is_active = Column(Boolean, default=True)
# Kapcsolat a csomaggal (data séma)
tier_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.subscription_tiers.id"), nullable=False)
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
valid_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class CreditTransaction(Base): class CreditTransaction(Base):
"""
Kreditnapló (Pontok, kreditek vagy virtuális egyenleg követése).
"""
__tablename__ = "credit_logs" __tablename__ = "credit_logs"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
org_id = Column(Integer, ForeignKey("data.organizations.id")) id: Mapped[int] = mapped_column(Integer, primary_key=True)
amount = Column(Numeric(10, 2))
description = Column(String) # Kapcsolat a szervezettel (data séma)
created_at = Column(DateTime, server_default=func.now()) org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
amount: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
description: Mapped[Optional[str]] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ServiceSpecialty(Base): class ServiceSpecialty(Base):
"""Fa struktúra a szerviz szolgáltatásokhoz""" """
Hierarchikus fa struktúra a szerviz szolgáltatásokhoz (pl. Motor -> Futómű).
"""
__tablename__ = "service_specialties" __tablename__ = "service_specialties"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("data.service_specialties.id"), nullable=True)
name = Column(String, nullable=False)
slug = Column(String, unique=True)
parent = relationship("ServiceSpecialty", remote_side=[id], backref="children") id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Önmagára mutató idegen kulcs a hierarchiához
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.service_specialties.id"))
name: Mapped[str] = mapped_column(String, nullable=False)
slug: Mapped[str] = mapped_column(String, unique=True, index=True)
# Kapcsolat az ős-szolgáltatással (Self-referential relationship)
parent: Mapped[Optional["ServiceSpecialty"]] = relationship("ServiceSpecialty", remote_side=[id], backref="children")

View File

@@ -1,27 +1,30 @@
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey # /opt/docker/dev/service_finder/backend/app/models/document.py
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
import uuid import uuid
# JAVÍTVA: Közvetlenül a base_class-ból importálunk, nem a base-ből! from datetime import datetime
from typing import Optional
from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, text
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.base_class import Base from app.db.base_class import Base
class Document(Base): class Document(Base):
""" NAS alapú dokumentumtár metaadatai. """
__tablename__ = "documents" __tablename__ = "documents"
__table_args__ = {"schema": "data"}
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
parent_type = Column(String(20), nullable=False) # 'organization' vagy 'asset' parent_type: Mapped[str] = mapped_column(String(20)) # 'organization' vagy 'asset'
parent_id = Column(String(50), nullable=False) # Org vagy Asset technikai ID-ja parent_id: Mapped[str] = mapped_column(String(50), index=True)
doc_type = Column(String(50)) # pl. 'foundation_deed', 'registration' doc_type: Mapped[Optional[str]] = mapped_column(String(50))
original_name = Column(String(255), nullable=False) original_name: Mapped[str] = mapped_column(String(255))
file_hash = Column(String(64), nullable=False) # A NAS-on tárolt név (UUID) file_hash: Mapped[str] = mapped_column(String(64))
file_ext = Column(String(10), default="webp") file_ext: Mapped[str] = mapped_column(String(10), default="webp")
mime_type = Column(String(100), default="image/webp") mime_type: Mapped[str] = mapped_column(String(100), default="image/webp")
file_size = Column(Integer) file_size: Mapped[Optional[int]] = mapped_column(Integer)
has_thumbnail = Column(Boolean, default=False) has_thumbnail: Mapped[bool] = mapped_column(Boolean, default=False)
thumbnail_path = Column(String(255)) # SSD-n lévő elérés thumbnail_path: Mapped[Optional[str]] = mapped_column(String(255))
uploaded_by = Column(Integer, ForeignKey("data.users.id"), nullable=True) uploaded_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,20 +1,19 @@
# /opt/docker/dev/service_finder/backend/app/models/gamification.py
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Optional, TYPE_CHECKING from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.db.base_class import Base from app.database import Base # MB 2.0: Központi Base
if TYPE_CHECKING: if TYPE_CHECKING:
from app.models.identity import User from app.models.identity import User
SCHEMA_ARGS = {"schema": "data"}
class PointRule(Base): class PointRule(Base):
__tablename__ = "point_rules" __tablename__ = "point_rules"
__table_args__ = SCHEMA_ARGS __table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
action_key: Mapped[str] = mapped_column(String, unique=True, index=True) action_key: Mapped[str] = mapped_column(String, unique=True, index=True)
points: Mapped[int] = mapped_column(Integer, default=0) points: Mapped[int] = mapped_column(Integer, default=0)
@@ -23,7 +22,8 @@ class PointRule(Base):
class LevelConfig(Base): class LevelConfig(Base):
__tablename__ = "level_configs" __tablename__ = "level_configs"
__table_args__ = SCHEMA_ARGS __table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
level_number: Mapped[int] = mapped_column(Integer, unique=True) level_number: Mapped[int] = mapped_column(Integer, unique=True)
min_points: Mapped[int] = mapped_column(Integer) min_points: Mapped[int] = mapped_column(Integer)
@@ -31,41 +31,41 @@ class LevelConfig(Base):
class PointsLedger(Base): class PointsLedger(Base):
__tablename__ = "points_ledger" __tablename__ = "points_ledger"
__table_args__ = SCHEMA_ARGS __table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
points: Mapped[int] = mapped_column(Integer, default=0) points: Mapped[int] = mapped_column(Integer, default=0)
# JAVÍTÁS: Itt is server_default-ot használunk
penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0) penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
reason: Mapped[str] = mapped_column(String) reason: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User") user: Mapped["User"] = relationship("User")
class UserStats(Base): class UserStats(Base):
__tablename__ = "user_stats" __tablename__ = "user_stats"
__table_args__ = {"schema": "data", "extend_existing": True} # Biztosítjuk a sémát __table_args__ = {"schema": "data"}
# A ForeignKey-nek látnia kell a data sémát! # MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), primary_key=True)
total_xp: Mapped[int] = mapped_column(Integer, default=0) total_xp: Mapped[int] = mapped_column(Integer, default=0)
social_points: Mapped[int] = mapped_column(Integer, default=0) social_points: Mapped[int] = mapped_column(Integer, default=0)
current_level: Mapped[int] = mapped_column(Integer, default=1) current_level: Mapped[int] = mapped_column(Integer, default=1)
# --- BÜNTETŐ RENDSZER ---
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0) penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0) restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# VISSZAMUTATÁS A USER-RE: a back_populates értéke meg kell egyezzen a User osztály 'stats' mezőjével!
user: Mapped["User"] = relationship("User", back_populates="stats") user: Mapped["User"] = relationship("User", back_populates="stats")
class Badge(Base): class Badge(Base):
__tablename__ = "badges" __tablename__ = "badges"
__table_args__ = SCHEMA_ARGS __table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, unique=True) name: Mapped[str] = mapped_column(String, unique=True)
description: Mapped[str] = mapped_column(String) description: Mapped[str] = mapped_column(String)
@@ -73,11 +73,14 @@ class Badge(Base):
class UserBadge(Base): class UserBadge(Base):
__tablename__ = "user_badges" __tablename__ = "user_badges"
__table_args__ = SCHEMA_ARGS __table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id")) badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User") user: Mapped["User"] = relationship("User")

View File

@@ -1,51 +1,47 @@
# /opt/docker/dev/service_finder/backend/app/models/history.py
import uuid
import enum import enum
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text, Enum from datetime import datetime, date
from sqlalchemy.orm import relationship from typing import Optional, Any
from sqlalchemy import String, DateTime, ForeignKey, JSON, Date, Text, Integer
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.db.base_class import Base # MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class LogSeverity(str, enum.Enum): class LogSeverity(str, enum.Enum):
info = "info" # Általános művelet (pl. profil megtekintés) info = "info"
warning = "warning" # Gyanús, de nem biztosan káros (pl. 3 elrontott jelszó) warning = "warning"
critical = "critical" # Súlyos művelet (pl. jelszóváltoztatás, export) critical = "critical"
emergency = "emergency" # Azonnali beavatkozást igényel (pl. SuperAdmin módosítás) emergency = "emergency"
class VehicleOwnership(Base):
__tablename__ = "vehicle_ownerships"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
vehicle_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
start_date = Column(Date, nullable=False, default=func.current_date())
end_date = Column(Date, nullable=True)
notes = Column(Text, nullable=True)
vehicle = relationship("Asset", back_populates="ownership_history")
user = relationship("User", back_populates="ownership_history")
class AuditLog(Base): class AuditLog(Base):
""" Rendszerszintű műveletnapló. """
__tablename__ = "audit_logs" __tablename__ = "audit_logs"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
severity = Column(Enum(LogSeverity), default=LogSeverity.info, nullable=False)
# Mi történt és min? # MB 2.0 JAVÍTÁS: A felhasználó az identity sémában lakik!
action = Column(String(100), nullable=False, index=True) user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
target_type = Column(String(50), index=True) # pl. "User", "Wallet", "Asset"
target_id = Column(String(50), index=True) # A cél rekord ID-ja
# Részletes adatok (JSONB formátum a rugalmasságért) severity: Mapped[LogSeverity] = mapped_column(
# A 'changes' helyett explicit old/new párost használunk a könnyebb visszaállításhoz PG_ENUM(LogSeverity, name="log_severity", schema="data"),
old_data = Column(JSON, nullable=True) default=LogSeverity.info
new_data = Column(JSON, nullable=True) )
# Biztonsági nyomkövetés action: Mapped[str] = mapped_column(String(100), index=True)
ip_address = Column(String(45), index=True) # IPv6-ot is támogat target_type: Mapped[Optional[str]] = mapped_column(String(50), index=True)
user_agent = Column(Text, nullable=True) # Böngésző/Eszköz információ target_id: Mapped[Optional[str]] = mapped_column(String(50), index=True)
timestamp = Column(DateTime(timezone=True), server_default=func.now(), index=True) old_data: Mapped[Optional[Any]] = mapped_column(JSON)
new_data: Mapped[Optional[Any]] = mapped_column(JSON)
user = relationship("User") ip_address: Mapped[Optional[str]] = mapped_column(String(45), index=True)
user_agent: Mapped[Optional[Text]] = mapped_column(Text)
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
user: Mapped[Optional["User"]] = relationship("User")

View File

@@ -1,10 +1,15 @@
# /opt/docker/dev/service_finder/backend/app/models/identity.py
import uuid import uuid
import enum import enum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger, UniqueConstraint from datetime import datetime
from sqlalchemy.orm import relationship from typing import Any, List, Optional
from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy import String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Integer, BigInteger, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.db.base_class import Base
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class UserRole(str, enum.Enum): class UserRole(str, enum.Enum):
superadmin = "superadmin" superadmin = "superadmin"
@@ -21,126 +26,134 @@ class UserRole(str, enum.Enum):
class Person(Base): class Person(Base):
""" """
Természetes személy identitása. A DNS szint. Természetes személy identitása. A DNS szint.
Itt tároljuk az örök adatokat, amik nem vesznek el account törléskor. Minden identitás adat az 'identity' sémába kerül.
""" """
__tablename__ = "persons" __tablename__ = "persons"
__table_args__ = {"schema": "data", "extend_existing": True} __table_args__ = {"schema": "identity"}
id = Column(BigInteger, primary_key=True, index=True) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, index=True)
id_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False) id_uuid: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
# --- KRITIKUS: EGYEDI AZONOSÍTÓ HASH (Normalizált adatokból) --- # A lakcím a 'data' sémában marad
identity_hash = Column(String(64), unique=True, index=True, nullable=True) address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
last_name = Column(String, nullable=False) identity_hash: Mapped[Optional[str]] = mapped_column(String(64), unique=True, index=True)
first_name = Column(String, nullable=False)
phone = Column(String, nullable=True)
mothers_last_name = Column(String) last_name: Mapped[str] = mapped_column(String, nullable=False)
mothers_first_name = Column(String) first_name: Mapped[str] = mapped_column(String, nullable=False)
birth_place = Column(String) phone: Mapped[Optional[str]] = mapped_column(String)
birth_date = Column(DateTime)
identity_docs = Column(JSON, server_default=text("'{}'::jsonb")) mothers_last_name: Mapped[Optional[str]] = mapped_column(String)
ice_contact = Column(JSON, server_default=text("'{}'::jsonb")) mothers_first_name: Mapped[Optional[str]] = mapped_column(String)
birth_place: Mapped[Optional[str]] = mapped_column(String)
birth_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
# --- ÖRÖK ADATOK (Person szint) --- identity_docs: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
lifetime_xp = Column(BigInteger, server_default=text("0")) ice_contact: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
penalty_points = Column(Integer, server_default=text("0")) # 0-3 szint
social_reputation = Column(Numeric(3, 2), server_default=text("1.00")) # 1.00 = 100%
is_sales_agent = Column(Boolean, server_default=text("false")) lifetime_xp: Mapped[int] = mapped_column(BigInteger, server_default=text("0"))
is_active = Column(Boolean, default=True, nullable=False) penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"))
is_ghost = Column(Boolean, default=False, nullable=False) social_reputation: Mapped[float] = mapped_column(Numeric(3, 2), server_default=text("1.00"))
created_at = Column(DateTime(timezone=True), server_default=func.now()) is_sales_agent: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_ghost: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
users = relationship("User", back_populates="person") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
memberships = relationship("OrganizationMember", back_populates="person") updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Kapcsolatok
users: Mapped[List["User"]] = relationship("User", back_populates="person")
memberships: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="person")
class User(Base): class User(Base):
""" """ Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött. """
Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött.
"""
__tablename__ = "users" __tablename__ = "users"
__table_args__ = {"schema": "data", "extend_existing": True} __table_args__ = {"schema": "identity"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False) email: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=True) hashed_password: Mapped[Optional[str]] = mapped_column(String)
role = Column(Enum(UserRole), default=UserRole.user)
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) role: Mapped[UserRole] = mapped_column(
PG_ENUM(UserRole, name="userrole", schema="identity"),
default=UserRole.user
)
# --- ELŐFIZETÉS ÉS VIP (Időkorlátos logika) --- # MB 2.0 JAVÍTÁS: A hivatkozások az identity sémára mutatnak!
subscription_plan = Column(String(30), server_default=text("'FREE'")) person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
subscription_expires_at = Column(DateTime(timezone=True), nullable=True)
is_vip = Column(Boolean, server_default=text("false"))
# --- REFERRAL ÉS SALES (Üzletkötői hálózat) --- subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"))
referral_code = Column(String(20), unique=True) subscription_expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
referred_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) is_vip: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
# Farming üzletkötő (Átruházható cégkezelő)
current_sales_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
# Szervezeti kapcsolat referral_code: Mapped[Optional[str]] = mapped_column(String(20), unique=True)
owned_organizations = relationship("Organization", back_populates="owner")
# Ez a sor felelős a gamification.py-val való hídért # MB 2.0 JAVÍTÁS: Önhivatkozások az identity sémán belül
stats = relationship("UserStats", back_populates="user", uselist=False, cascade="all, delete-orphan") referred_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
current_sales_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
ownership_history = relationship("VehicleOwnership", back_populates="user") is_active: Mapped[bool] = mapped_column(Boolean, default=False)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
folder_slug: Mapped[Optional[str]] = mapped_column(String(12), unique=True, index=True)
is_active = Column(Boolean, default=False) preferred_language: Mapped[str] = mapped_column(String(5), server_default="hu")
is_deleted = Column(Boolean, default=False) region_code: Mapped[str] = mapped_column(String(5), server_default="HU")
folder_slug = Column(String(12), unique=True, index=True) preferred_currency: Mapped[str] = mapped_column(String(3), server_default="HUF")
preferred_language = Column(String(5), server_default="hu") scope_level: Mapped[str] = mapped_column(String(30), server_default="individual")
region_code = Column(String(5), server_default="HU") scope_id: Mapped[Optional[str]] = mapped_column(String(50))
preferred_currency = Column(String(3), server_default="HUF") custom_permissions: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
scope_level = Column(String(30), server_default="individual") # global, region, country, entity, individual created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
scope_id = Column(String(50))
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now()) # Kapcsolatok
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="users")
person = relationship("Person", back_populates="users") wallet: Mapped[Optional["Wallet"]] = relationship("Wallet", back_populates="user", uselist=False)
wallet = relationship("Wallet", back_populates="user", uselist=False) social_accounts: Mapped[List["SocialAccount"]] = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
social_accounts = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan") owned_organizations: Mapped[List["Organization"]] = relationship("Organization", back_populates="owner")
stats: Mapped[Optional["UserStats"]] = relationship("UserStats", back_populates="user", uselist=False, cascade="all, delete-orphan")
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="user")
class Wallet(Base): class Wallet(Base):
""" A 3-as felosztású pénztárca. """
__tablename__ = "wallets" __tablename__ = "wallets"
__table_args__ = {"schema": "data", "extend_existing": True} __table_args__ = {"schema": "identity"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), unique=True)
earned_credits = Column(Numeric(18, 4), server_default=text("0")) # Munka + Referral earned_credits: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
purchased_credits = Column(Numeric(18, 4), server_default=text("0")) # Vásárolt purchased_credits: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
service_coins = Column(Numeric(18, 4), server_default=text("0")) # Csak hirdetésre! service_coins: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
currency = Column(String(3), default="HUF") currency: Mapped[str] = mapped_column(String(3), default="HUF")
user = relationship("User", back_populates="wallet") user: Mapped["User"] = relationship("User", back_populates="wallet")
# ... (VerificationToken és SocialAccount változatlan) ...
class VerificationToken(Base): class VerificationToken(Base):
__tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"} __tablename__ = "verification_tokens"
id = Column(Integer, primary_key=True, index=True) __table_args__ = {"schema": "identity"}
token = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
token_type = Column(String(20), nullable=False); created_at = Column(DateTime(timezone=True), server_default=func.now()) token: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=False); is_used = Column(Boolean, default=False) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False)
token_type: Mapped[str] = mapped_column(String(20), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
is_used: Mapped[bool] = mapped_column(Boolean, default=False)
class SocialAccount(Base): class SocialAccount(Base):
__tablename__ = "social_accounts" __tablename__ = "social_accounts"
__table_args__ = (UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'), {"schema": "data"}) __table_args__ = (
id = Column(Integer, primary_key=True, index=True) UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'),
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False) {"schema": "identity"}
provider = Column(String(50), nullable=False); social_id = Column(String(255), nullable=False, index=True); email = Column(String(255), nullable=False) )
extra_data = Column(JSON, server_default=text("'{}'::jsonb")); created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="social_accounts") id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False)
provider: Mapped[str] = mapped_column(String(50), nullable=False)
social_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
email: Mapped[str] = mapped_column(String(255), nullable=False)
extra_data: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User", back_populates="social_accounts")

View File

@@ -1,29 +1,31 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean # /opt/docker/dev/service_finder/backend/app/models/legal.py
from datetime import datetime
from typing import Optional
from sqlalchemy import Integer, String, Text, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.db.base import Base from app.db.base_class import Base
class LegalDocument(Base): class LegalDocument(Base):
__tablename__ = "legal_documents" __tablename__ = "legal_documents"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
title = Column(String(255)) title: Mapped[Optional[str]] = mapped_column(String(255))
content = Column(Text, nullable=False) content: Mapped[str] = mapped_column(Text)
version = Column(String(20), nullable=False) version: Mapped[str] = mapped_column(String(20))
region_code = Column(String(5), default="HU") region_code: Mapped[str] = mapped_column(String(5), default="HU")
language = Column(String(5), default="hu") language: Mapped[str] = mapped_column(String(5), default="hu")
is_active = Column(Boolean, default=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class LegalAcceptance(Base): class LegalAcceptance(Base):
__tablename__ = "legal_acceptances" __tablename__ = "legal_acceptances"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id")) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
document_id = Column(Integer, ForeignKey("data.legal_documents.id")) document_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.legal_documents.id"))
accepted_at = Column(DateTime(timezone=True), server_default=func.now()) accepted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
ip_address = Column(String(45)) ip_address: Mapped[Optional[str]] = mapped_column(String(45))
user_agent = Column(Text) user_agent: Mapped[Optional[str]] = mapped_column(Text)

View File

@@ -1,25 +1,26 @@
from sqlalchemy import Column, Integer, String, Enum # /opt/docker/dev/service_finder/backend/app/models/logistics.py
from app.db.base import Base
import enum import enum
from typing import Optional
from sqlalchemy import Integer, String, Enum
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base_class import Base
# Enum definiálása
class LocationType(str, enum.Enum): class LocationType(str, enum.Enum):
stop = "stop" # Megálló / Parkoló stop = "stop"
warehouse = "warehouse" # Raktár warehouse = "warehouse"
client = "client" # Ügyfél címe client = "client"
class Location(Base): class Location(Base):
__tablename__ = "locations" __tablename__ = "locations"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False) name: Mapped[str] = mapped_column(String)
type: Mapped[LocationType] = mapped_column(
PG_ENUM(LocationType, name="location_type", inherit_schema=True),
nullable=False
)
# FONTOS: Itt is megadjuk a schema="data"-t, hogy ne a public sémába akarja írni! coordinates: Mapped[Optional[str]] = mapped_column(String)
type = Column(Enum(LocationType, schema="data", name="location_type_enum"), nullable=False) address_full: Mapped[Optional[str]] = mapped_column(String)
capacity: Mapped[Optional[int]] = mapped_column(Integer)
# Koordináták (egyelőre String, később PostGIS)
coordinates = Column(String, nullable=True)
address_full = Column(String, nullable=True)
capacity = Column(Integer, nullable=True)

View File

@@ -1,10 +1,14 @@
import enum import enum
import uuid
from datetime import datetime
from typing import Any, List, Optional
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Numeric, BigInteger from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Numeric, BigInteger
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.db.base_class import Base
from sqlalchemy.dialects.postgresql import UUID as PG_UUID # MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class OrgType(str, enum.Enum): class OrgType(str, enum.Enum):
individual = "individual" individual = "individual"
@@ -25,114 +29,118 @@ class OrgUserRole(str, enum.Enum):
class Organization(Base): class Organization(Base):
""" """
Szervezet entitás. Lehet flotta (user) és szolgáltató (service) egyszerre. Szervezet entitás. Lehet flotta (user) és szolgáltató (service) egyszerre.
A képességeket a kapcsolódó profilok (pl. ServiceProfile) határozzák meg. Minden üzleti adat a 'data' sémába kerül.
""" """
__tablename__ = "organizations" __tablename__ = "organizations"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
is_anonymized = Column(Boolean, default=False, server_default=text("false")) # Kapcsolat a címekkel (szintén a data sémában)
anonymized_at = Column(DateTime(timezone=True), nullable=True) address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
full_name = Column(String, nullable=False) # Hivatalos név is_anonymized: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text("false"))
name = Column(String, nullable=False) # Rövid név anonymized_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
display_name = Column(String(50))
folder_slug = Column(String(12), unique=True, index=True)
default_currency = Column(String(3), default="HUF") full_name: Mapped[str] = mapped_column(String, nullable=False)
country_code = Column(String(2), default="HU") name: Mapped[str] = mapped_column(String, nullable=False)
language = Column(String(5), default="hu") display_name: Mapped[Optional[str]] = mapped_column(String(50))
folder_slug: Mapped[str] = mapped_column(String(12), unique=True, index=True)
# Cím adatok (redundáns a gyors kereséshez, de address_id a SSoT) default_currency: Mapped[str] = mapped_column(String(3), default="HUF")
address_zip = Column(String(10)) country_code: Mapped[str] = mapped_column(String(2), default="HU")
address_city = Column(String(100)) language: Mapped[str] = mapped_column(String(5), default="hu")
address_street_name = Column(String(150))
address_street_type = Column(String(50))
address_house_number = Column(String(20))
address_hrsz = Column(String(50))
tax_number = Column(String(20), unique=True, index=True) # Robot horgony address_zip: Mapped[Optional[str]] = mapped_column(String(10))
reg_number = Column(String(50)) address_city: Mapped[Optional[str]] = mapped_column(String(100))
address_street_name: Mapped[Optional[str]] = mapped_column(String(150))
address_street_type: Mapped[Optional[str]] = mapped_column(String(50))
address_house_number: Mapped[Optional[str]] = mapped_column(String(20))
address_hrsz: Mapped[Optional[str]] = mapped_column(String(50))
org_type = Column( tax_number: Mapped[Optional[str]] = mapped_column(String(20), unique=True, index=True)
PG_ENUM(OrgType, name="orgtype", inherit_schema=True), reg_number: Mapped[Optional[str]] = mapped_column(String(50))
org_type: Mapped[OrgType] = mapped_column(
PG_ENUM(OrgType, name="orgtype", schema="data"),
default=OrgType.individual default=OrgType.individual
) )
status = Column(String(30), default="pending_verification") status: Mapped[str] = mapped_column(String(30), default="pending_verification")
is_deleted = Column(Boolean, default=False) is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# --- ÚJ: Előfizetés és Méret korlátok --- subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"), index=True)
subscription_plan = Column(String(30), server_default=text("'FREE'"), index=True) base_asset_limit: Mapped[int] = mapped_column(Integer, server_default=text("1"))
base_asset_limit = Column(Integer, server_default=text("1")) purchased_extra_slots: Mapped[int] = mapped_column(Integer, server_default=text("0"))
purchased_extra_slots = Column(Integer, server_default=text("0"))
notification_settings = Column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb")) notification_settings: Mapped[Any] = mapped_column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb"))
external_integration_config = Column(JSON, server_default=text("'{}'::jsonb")) external_integration_config: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) # KRITIKUS: A júzer az 'identity' sémában van!
is_active = Column(Boolean, default=True) owner_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
is_verified = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now()) is_active: Mapped[bool] = mapped_column(Boolean, default=True)
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
# --- ÚJ: Dual Twin Tulajdonjog logika --- created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Individual esetén False, Business esetén True updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
is_ownership_transferable = Column(Boolean, server_default=text("true")) is_ownership_transferable: Mapped[bool] = mapped_column(Boolean, server_default=text("true"))
# Kapcsolatok # Kapcsolatok (Relationships)
assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan") assets: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan") members: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
owner = relationship("User", back_populates="owned_organizations") owner: Mapped[Optional["User"]] = relationship("User", back_populates="owned_organizations")
financials = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan") financials: Mapped[List["OrganizationFinancials"]] = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan")
service_profile = relationship("ServiceProfile", back_populates="organization", uselist=False) service_profile: Mapped[Optional["ServiceProfile"]] = relationship("ServiceProfile", back_populates="organization", uselist=False)
branches = relationship("Branch", back_populates="organization", cascade="all, delete-orphan") branches: Mapped[List["Branch"]] = relationship("Branch", back_populates="organization", cascade="all, delete-orphan")
class OrganizationFinancials(Base): class OrganizationFinancials(Base):
"""Cégek éves gazdasági adatai elemzéshez."""
__tablename__ = "organization_financials" __tablename__ = "organization_financials"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
year = Column(Integer, nullable=False) year: Mapped[int] = mapped_column(Integer, nullable=False)
turnover = Column(Numeric(18, 2)) turnover: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
profit = Column(Numeric(18, 2)) profit: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
employee_count = Column(Integer) employee_count: Mapped[Optional[int]] = mapped_column(Integer)
source = Column(String(50)) # pl. 'manual', 'crawler', 'api' source: Mapped[Optional[str]] = mapped_column(String(50))
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
organization = relationship("Organization", back_populates="financials") organization: Mapped["Organization"] = relationship("Organization", back_populates="financials")
class OrganizationMember(Base): class OrganizationMember(Base):
"""Kapcsolótábla a személyek és szervezetek között."""
__tablename__ = "organization_members" __tablename__ = "organization_members"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) # Ghost támogatás
role = Column(PG_ENUM(OrgUserRole, name="orguserrole", inherit_schema=True), default=OrgUserRole.DRIVER) # KRITIKUS: User és Person az identity sémában lakik!
permissions = Column(JSON, server_default=text("'{}'::jsonb")) user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
is_permanent = Column(Boolean, default=False) person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
is_verified = Column(Boolean, default=False) # <--- JAVÍTÁS: Ez az oszlop hiányzott!
organization = relationship("Organization", back_populates="members") role: Mapped[OrgUserRole] = mapped_column(
user = relationship("User") PG_ENUM(OrgUserRole, name="orguserrole", schema="data"),
person = relationship("Person", back_populates="memberships") default=OrgUserRole.DRIVER
)
permissions: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
is_permanent: Mapped[bool] = mapped_column(Boolean, default=False)
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
organization: Mapped["Organization"] = relationship("Organization", back_populates="members")
user: Mapped[Optional["User"]] = relationship("User")
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="memberships")
class OrganizationSalesAssignment(Base): class OrganizationSalesAssignment(Base):
"""Összeköti a céget az aktuális üzletkötővel a jutalék miatt."""
__tablename__ = "org_sales_assignments" __tablename__ = "org_sales_assignments"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
organization_id = Column(Integer, ForeignKey("data.organizations.id")) organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
agent_user_id = Column(Integer, ForeignKey("data.users.id")) # Ő kapja a Farming díjat
assigned_at = Column(DateTime(timezone=True), server_default=func.now()) # KRITIKUS: Az ügynök (agent) júzer az identity sémában van
is_active = Column(Boolean, default=True) agent_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
assigned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
is_active: Mapped[bool] = mapped_column(Boolean, default=True)

View File

@@ -1,44 +1,51 @@
# /opt/docker/dev/service_finder/backend/app/models/security.py
import enum import enum
import uuid from datetime import datetime
from datetime import datetime, timedelta from typing import Optional, TYPE_CHECKING
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Enum, text from sqlalchemy import String, Integer, ForeignKey, DateTime, text, Enum
from sqlalchemy.orm import relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.db.base_class import Base
# MB 2.0: Központi aszinkron adatbázis motorból származó Base
from app.database import Base
if TYPE_CHECKING:
from .identity import User
class ActionStatus(str, enum.Enum): class ActionStatus(str, enum.Enum):
pending = "pending" # Jóváhagyásra vár pending = "pending"
approved = "approved" # Végrehajtva approved = "approved"
rejected = "rejected" # Elutasítva rejected = "rejected"
expired = "expired" # Lejárt (biztonsági okokból) expired = "expired"
class PendingAction(Base): class PendingAction(Base):
"""Négy szem elv: Műveletek, amik jóváhagyásra várnak.""" """ Sentinel: Kritikus műveletek jóváhagyási lánca. """
__tablename__ = "pending_actions" __tablename__ = "pending_actions"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "system"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# Ki akarja csinálni? # JAVÍTÁS: A User az identity sémában van, nem a data-ban!
requester_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) requester_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
approver_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
# Ki hagyta jóvá/utasította el? status: Mapped[ActionStatus] = mapped_column(
approver_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) Enum(ActionStatus, name="actionstatus", schema="system"),
default=ActionStatus.pending
)
status = Column(Enum(ActionStatus), default=ActionStatus.pending, nullable=False) action_type: Mapped[str] = mapped_column(String(50)) # pl. "WALLET_ADJUST"
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
reason: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# Milyen típusú művelet? (pl. "CHANGE_ROLE", "WALLET_ADJUST", "DELETE_LOGS") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
action_type = Column(String(50), nullable=False) expires_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=text("now() + interval '24 hours'")
)
processed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
# A művelet adatai JSON-ben (pl. {"user_id": 5, "new_role": "admin"}) # Kapcsolatok meghatározása (String hivatkozással a körkörös import ellen)
payload = Column(JSON, nullable=False) requester: Mapped["User"] = relationship("User", foreign_keys=[requester_id])
approver: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approver_id])
# Miért kell ez a művelet? (Indoklás kötelező az audit miatt)
reason = Column(String(255), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
expires_at = Column(DateTime(timezone=True), default=lambda: datetime.now() + timedelta(hours=24))
processed_at = Column(DateTime(timezone=True), nullable=True)
requester = relationship("User", foreign_keys=[requester_id])
approver = relationship("User", foreign_keys=[approver_id])

View File

@@ -1,163 +1,104 @@
# /opt/docker/dev/service_finder/backend/app/models/service.py
import uuid import uuid
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Text, Float, Index, Numeric from datetime import datetime
from sqlalchemy.orm import relationship, backref from typing import Any, List, Optional
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from geoalchemy2 import Geometry # PostGIS támogatás from geoalchemy2 import Geometry
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.db.base_class import Base
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class ServiceProfile(Base): class ServiceProfile(Base):
""" """ Szerviz szolgáltató adatai (v1.3.1). """
Szerviz szolgáltató kiterjesztett adatai (v1.3.1).
Egy Organization-höz (org_type='service') kapcsolódik.
Támogatja a hierarchiát (Franchise/Telephely) és az automatizált dúsítást.
"""
__tablename__ = "service_profiles" __tablename__ = "service_profiles"
__table_args__ = ( __table_args__ = (
# Egyedi ujjlenyomat index a robot számára a duplikációk elkerülésére
Index('idx_service_fingerprint', 'fingerprint', unique=True), Index('idx_service_fingerprint', 'fingerprint', unique=True),
{"schema": "data"} {"schema": "data"}
) )
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"), unique=True)
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.service_profiles.id"))
# --- KAPCSOLAT A CÉGES IKERHEZ (Twin) --- fingerprint: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), unique=True) location: Mapped[Any] = mapped_column(Geometry(geometry_type='POINT', srid=4326, spatial_index=False), index=True)
# --- HIERARCHIA (Fa struktúra) --- status: Mapped[str] = mapped_column(String(20), server_default=text("'ghost'"), index=True)
# Ez tárolja a szülő egység ID-ját (pl. hálózat központja) last_audit_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
parent_id = Column(Integer, ForeignKey("data.service_profiles.id"), nullable=True)
# --- ROBOT IDENTITÁS --- google_place_id: Mapped[Optional[str]] = mapped_column(String(100), unique=True)
# Normalize(Név + Város + Utca) hash, hogy ne legyen duplikáció rating: Mapped[Optional[float]] = mapped_column(Float)
fingerprint = Column(String(255), nullable=False, index=True) user_ratings_total: Mapped[Optional[int]] = mapped_column(Integer)
# PostGIS GPS pont (SRID 4326 = WGS84 koordináták) vibe_analysis: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
location = Column(Geometry(geometry_type='POINT', srid=4326), index=True) social_links: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
specialization_tags: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
# Állapotkezelés: ghost (robot találta), active, flagged, inactive trust_score: Mapped[int] = mapped_column(Integer, default=30)
status = Column(String(20), server_default=text("'ghost'"), index=True) is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
last_audit_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) verification_log: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
# --- GOOGLE ÉS KÜLSŐ ADATOK --- opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
google_place_id = Column(String(100), unique=True) contact_phone: Mapped[Optional[str]] = mapped_column(String)
rating = Column(Float) contact_email: Mapped[Optional[str]] = mapped_column(String)
user_ratings_total = Column(Integer) website: Mapped[Optional[str]] = mapped_column(String)
bio: Mapped[Optional[str]] = mapped_column(Text)
# --- MÉLYFÚRÁS (Deep Enrichment) ADATOK --- # Kapcsolatok
# AI elemzés: {"tone": "barátságos", "pricing": "közép", "reliability": "magas"} organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
vibe_analysis = Column(JSONB, server_default=text("'{}'::jsonb")) expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
# Közösségi háló: {"facebook": "url", "tiktok": "url", "insta": "url"} created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
social_links = Column(JSONB, server_default=text("'{}'::jsonb")) updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Speciális szűrő címkék: {"brands": ["Yamaha", "Suzuki"], "specialty": ["engine", "tuning"]}
specialization_tags = Column(JSONB, server_default=text("'{}'::jsonb"))
# Trust Engine (Bot Discovery=30, User Entry=50, Admin/Partner=100)
trust_score = Column(Integer, default=30)
is_verified = Column(Boolean, default=False)
verification_log = Column(JSONB, server_default=text("'{}'::jsonb"))
# --- ELÉRHETŐSÉG ---
opening_hours = Column(JSONB, server_default=text("'{}'::jsonb"))
contact_phone = Column(String)
contact_email = Column(String)
website = Column(String)
bio = Column(Text)
# --- KAPCSOLATOK ---
organization = relationship("Organization", back_populates="service_profile")
expertises = relationship("ServiceExpertise", back_populates="service")
# --- ÖNMAGÁRA HIVATKOZÓ KAPCSOLAT (Hierarchia) ---
sub_services = relationship(
"ServiceProfile",
backref=backref("parent_service", remote_side=[id]),
cascade="all, delete-orphan"
)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class ExpertiseTag(Base): class ExpertiseTag(Base):
"""Szakmai szempontok taxonómiája."""
__tablename__ = "expertise_tags" __tablename__ = "expertise_tags"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
key = Column(String(50), unique=True, index=True) # pl. 'bmw_gs_specialist' key: Mapped[str] = mapped_column(String(50), unique=True, index=True)
name_hu = Column(String(100)) name_hu: Mapped[Optional[str]] = mapped_column(String(100))
category = Column(String(30)) # 'repair', 'fuel', 'food', 'emergency' category: Mapped[Optional[str]] = mapped_column(String(30))
class ServiceExpertise(Base): class ServiceExpertise(Base):
"""Kapcsolótábla a szerviz és a szakterület között."""
__tablename__ = "service_expertises" __tablename__ = "service_expertises"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
service_id = Column(Integer, ForeignKey("data.service_profiles.id"), primary_key=True) service_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_profiles.id"), primary_key=True)
expertise_id = Column(Integer, ForeignKey("data.expertise_tags.id"), primary_key=True) expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.expertise_tags.id"), primary_key=True)
validation_level: Mapped[int] = mapped_column(Integer, default=0)
# Validációs szint (0-100% - Mennyire hiteles ez a szakértelem) service: Mapped["ServiceProfile"] = relationship("ServiceProfile", back_populates="expertises")
validation_level = Column(Integer, default=0) expertise: Mapped["ExpertiseTag"] = relationship("ExpertiseTag")
service = relationship("ServiceProfile", back_populates="expertises")
expertise = relationship("ExpertiseTag")
class ServiceStaging(Base): class ServiceStaging(Base):
""" """ Hunter (robot) adatok tárolója. """
Átmeneti tábla a Hunter (n8n/scraping) adatoknak.
"""
__tablename__ = "service_staging" __tablename__ = "service_staging"
__table_args__ = ( __table_args__ = (
Index('idx_staging_fingerprint', 'fingerprint', unique=True), Index('idx_staging_fingerprint', 'fingerprint', unique=True),
{"schema": "data"} {"schema": "data"}
) )
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
# --- Alapadatok --- postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
name = Column(String, nullable=False, index=True) city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
full_address: Mapped[Optional[str]] = mapped_column(String)
# --- Strukturált cím adatok --- fingerprint: Mapped[str] = mapped_column(String(255), nullable=False)
postal_code = Column(String(10), index=True) raw_data: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
city = Column(String(100), index=True) status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
street_name = Column(String(150)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
street_type = Column(String(50))
house_number = Column(String(20))
stairwell = Column(String(20))
floor = Column(String(20))
door = Column(String(20))
hrsz = Column(String(50))
full_address = Column(String)
contact_phone = Column(String, nullable=True)
email = Column(String, nullable=True)
website = Column(String, nullable=True)
# --- Forrás és Azonosítás ---
source = Column(String(50), nullable=True, index=True)
external_id = Column(String(100), nullable=True, index=True)
# Robot ujjlenyomat a Staging szintű deduplikációhoz
fingerprint = Column(String(255), nullable=False)
# --- Adatmentés ---
raw_data = Column(JSONB, server_default=text("'{}'::jsonb"))
# --- Státusz és Bizalom ---
status = Column(String(20), server_default=text("'pending'"), index=True)
trust_score = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class DiscoveryParameter(Base): class DiscoveryParameter(Base):
"""Robot vezérlési paraméterek.""" """ Robot vezérlési paraméterek adminból. """
__tablename__ = "discovery_parameters" __tablename__ = "discovery_parameters"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
city = Column(String(100), nullable=False) id: Mapped[int] = mapped_column(Integer, primary_key=True)
keyword = Column(String(100), nullable=False) city: Mapped[str] = mapped_column(String(100))
country_code = Column(String(2), default="HU") keyword: Mapped[str] = mapped_column(String(100))
is_active = Column(Boolean, default=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_run_at = Column(DateTime(timezone=True)) last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

View File

@@ -1,9 +1,13 @@
# /opt/docker/dev/service_finder/backend/app/models/social.py
import enum import enum
from sqlalchemy import Column, Integer, String, ForeignKey, Enum, DateTime, Boolean, Text, UniqueConstraint
from app.db.base import Base
from datetime import datetime from datetime import datetime
from typing import Optional, List
from sqlalchemy import String, Integer, ForeignKey, DateTime, Boolean, Text, UniqueConstraint, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
from sqlalchemy.sql import func
from app.db.base_class import Base
# Enums (már schema="data" beállítással a biztonságért)
class ModerationStatus(str, enum.Enum): class ModerationStatus(str, enum.Enum):
pending = "pending" pending = "pending"
approved = "approved" approved = "approved"
@@ -15,57 +19,60 @@ class SourceType(str, enum.Enum):
api_import = "import" api_import = "import"
class ServiceProvider(Base): class ServiceProvider(Base):
""" Közösség által beküldött szolgáltatók (v1.3.1). """
__tablename__ = "service_providers" __tablename__ = "service_providers"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False) name: Mapped[str] = mapped_column(String, nullable=False)
address = Column(String, nullable=False) address: Mapped[str] = mapped_column(String, nullable=False)
category = Column(String) category: Mapped[Optional[str]] = mapped_column(String)
status = Column(Enum(ModerationStatus, schema="data", name="moderation_status_enum"), default=ModerationStatus.pending, nullable=False) status: Mapped[ModerationStatus] = mapped_column(
source = Column(Enum(SourceType, schema="data", name="source_type_enum"), default=SourceType.manual, nullable=False) PG_ENUM(ModerationStatus, name="moderation_status", inherit_schema=True),
default=ModerationStatus.pending
)
source: Mapped[SourceType] = mapped_column(
PG_ENUM(SourceType, name="source_type", inherit_schema=True),
default=SourceType.manual
)
# --- ÚJ MEZŐ --- validation_score: Mapped[int] = mapped_column(Integer, default=0)
validation_score = Column(Integer, default=0) # A közösségi szavazatok összege evidence_image_path: Mapped[Optional[str]] = mapped_column(String)
# --------------- added_by_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
evidence_image_path = Column(String, nullable=True)
added_by_user_id = Column(Integer, ForeignKey("data.users.id"))
created_at = Column(DateTime, default=datetime.utcnow)
class Vote(Base): class Vote(Base):
""" Közösségi validációs szavazatok. """
__tablename__ = "votes" __tablename__ = "votes"
__table_args__ = ( __table_args__ = (
UniqueConstraint('user_id', 'provider_id', name='uq_user_provider_vote'), UniqueConstraint('user_id', 'provider_id', name='uq_user_provider_vote'),
{"schema": "data"}
) )
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
provider_id = Column(Integer, ForeignKey("data.service_providers.id"), nullable=False) provider_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_providers.id"), nullable=False)
vote_value = Column(Integer, nullable=False) # +1 vagy -1 vote_value: Mapped[int] = mapped_column(Integer, nullable=False) # +1 vagy -1
class Competition(Base): class Competition(Base):
""" Gamifikált versenyek (pl. Januári Feltöltő Verseny). """
__tablename__ = "competitions" __tablename__ = "competitions"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
name = Column(String, nullable=False) # Pl: "Januári Feltöltő Verseny" name: Mapped[str] = mapped_column(String, nullable=False)
description = Column(Text) description: Mapped[Optional[str]] = mapped_column(Text)
start_date = Column(DateTime, nullable=False) start_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
end_date = Column(DateTime, nullable=False) end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
is_active = Column(Boolean, default=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class UserScore(Base): class UserScore(Base):
""" Versenyenkénti ranglista pontszámok. """
__tablename__ = "user_scores" __tablename__ = "user_scores"
__table_args__ = ( __table_args__ = (
UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'), UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'),
{"schema": "data"}
) )
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("data.users.id")) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
competition_id = Column(Integer, ForeignKey("data.competitions.id")) competition_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.competitions.id"))
points = Column(Integer, default=0) points: Mapped[int] = mapped_column(Integer, default=0)
last_updated = Column(DateTime, default=datetime.utcnow) last_updated: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

View File

@@ -1,17 +1,56 @@
from sqlalchemy import Column, Integer, String, JSON, DateTime, func # /opt/docker/dev/service_finder/backend/app/models/staged_data.py
from app.db.base import Base from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
from app.db.base_class import Base
class StagedVehicleData(Base): class StagedVehicleData(Base):
"""Ide érkeznek a nyers, validálatlan adatok a külső forrásokból""" """ Robot 2.1 (Researcher) nyers adatgyűjtője. """
__tablename__ = "staged_vehicle_data" __tablename__ = "staged_vehicle_data"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
source_url = Column(String) # Honnan jött az adat? source_url: Mapped[Optional[str]] = mapped_column(String)
raw_data = Column(JSON) # A teljes leszedett JSON struktúra raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
# Feldolgozási állapot status: Mapped[str] = mapped_column(String(20), default="PENDING", index=True)
status = Column(String, default="PENDING") # PENDING, PROCESSED, ERROR error_log: Mapped[Optional[str]] = mapped_column(String)
error_log = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ServiceStaging(Base):
""" Robot 1.3 (Scout) által talált nyers szerviz adatok. """
__tablename__ = "service_staging"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255), index=True)
source: Mapped[str] = mapped_column(String(50))
external_id: Mapped[Optional[str]] = mapped_column(String(100), index=True)
fingerprint: Mapped[str] = mapped_column(String(64), unique=True, index=True)
city: Mapped[str] = mapped_column(String(100), index=True)
full_address: Mapped[Optional[str]] = mapped_column(String(500))
contact_phone: Mapped[Optional[str]] = mapped_column(String(50))
website: Mapped[Optional[str]] = mapped_column(String(255))
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
trust_score: Mapped[int] = mapped_column(Integer, default=30)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
class DiscoveryParameter(Base):
""" Felderítési paraméterek (Városok, ahol a Scout keres). """
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
city: Mapped[str] = mapped_column(String(100), unique=True, index=True)
country_code: Mapped[str] = mapped_column(String(5), server_default=text("'HU'"))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

View File

@@ -1,35 +1,29 @@
# backend/app/models/system.py # /opt/docker/dev/service_finder/backend/app/models/system.py
import enum from datetime import datetime
from sqlalchemy import Column, String, DateTime, Boolean, text, UniqueConstraint, Integer from typing import Optional, Any
from sqlalchemy.dialects.postgresql import JSONB # <-- JSONB-t használunk a stabilitásért from sqlalchemy import String, Integer, Boolean, DateTime, text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.db.base_class import Base from app.db.base_class import Base
class SystemParameter(Base): class SystemParameter(Base):
""" """ Dinamikus konfigurációs motor (Global -> Org -> User). """
Központi, dinamikus konfigurációs tábla.
Támogatja a többlépcsős felülbírálást (Global -> Country -> Region -> Individual).
"""
__tablename__ = "system_parameters" __tablename__ = "system_parameters"
__table_args__ = ( __table_args__ = (
UniqueConstraint('key', 'scope_level', 'scope_id', name='uix_param_scope'), UniqueConstraint('key', 'scope_level', 'scope_id', name='uix_param_scope'),
{"schema": "data", "extend_existing": True} {"extend_existing": True}
) )
# Technikai ID, hogy a 'key' ne legyen Primary Key, így engedve a hierarchiát id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
id = Column(Integer, primary_key=True, autoincrement=True) key: Mapped[str] = mapped_column(String, index=True)
category: Mapped[str] = mapped_column(String, server_default="general", index=True)
value: Mapped[dict] = mapped_column(JSONB, nullable=False)
key = Column(String, index=True, nullable=False) # pl. 'VEHICLE_LIMIT' scope_level: Mapped[str] = mapped_column(String(30), server_default=text("'global'"), index=True)
category = Column(String, index=True, server_default="general") scope_id: Mapped[Optional[str]] = mapped_column(String(50))
# A tényleges érték (JSONB-ben tárolva) is_active: Mapped[bool] = mapped_column(Boolean, default=True)
value = Column(JSONB, nullable=False) # pl. {"FREE": 1, "PREMIUM": 4} description: Mapped[Optional[str]] = mapped_column(String)
last_modified_by: Mapped[Optional[str]] = mapped_column(String)
# --- 🛡️ HIERARCHIKUS SZINTEK --- updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
scope_level = Column(String(30), server_default=text("'global'"), index=True)
scope_id = Column(String(50), nullable=True)
is_active = Column(Boolean, default=True)
description = Column(String)
last_modified_by = Column(String, nullable=True)
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())

View File

@@ -1,10 +1,27 @@
from sqlalchemy import Column, Integer, String, Text # /opt/docker/dev/service_finder/backend/app/models/translation.py
from app.db.base_class import Base from sqlalchemy import String, Integer, Text, Boolean, text
from sqlalchemy.orm import Mapped, mapped_column
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class Translation(Base): class Translation(Base):
"""
Többnyelvűséget támogató tábla a felületi elemekhez és dinamikus tartalmakhoz.
"""
__tablename__ = "translations" __tablename__ = "translations"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
key = Column(String(255), index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
lang = Column(String(5), index=True) # pl: 'hu', 'en'
value = Column(Text) # A fordítandó kulcs (pl. 'NAV_DASHBOARD' vagy 'ERR_USER_NOT_FOUND')
key: Mapped[str] = mapped_column(String(255), index=True)
# Nyelvi kód (pl: 'hu', 'en', 'de')
lang: Mapped[str] = mapped_column(String(5), index=True)
# A tényleges fordított szöveg
value: Mapped[str] = mapped_column(Text)
# --- JAVÍTÁS: A diagnosztika által hiányolt publikációs állapot ---
is_published: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))

View File

@@ -1,106 +1,136 @@
from sqlalchemy import Column, Integer, String, JSON, UniqueConstraint, text, Boolean, DateTime, ForeignKey, Numeric, Index, Text # /opt/docker/dev/service_finder/backend/app/models/vehicle_definitions.py
from sqlalchemy.orm import relationship from __future__ import annotations
from sqlalchemy.sql import func from datetime import datetime
from typing import Optional, List
from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, text, Index, UniqueConstraint, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from app.db.base_class import Base from sqlalchemy.sql import func
# MB 2.0: Egységesített Base import a központi adatbázis motorból
from app.database import Base
class VehicleType(Base): class VehicleType(Base):
"""Jármű főtípusok sémája (Séma-gazda)""" """ Jármű kategóriák (pl. Személyautó, Motorkerékpár, Teherautó, Hajó) """
__tablename__ = "vehicle_types" __tablename__ = "vehicle_types"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
code = Column(String(30), unique=True, index=True) code: Mapped[str] = mapped_column(String(30), unique=True, index=True)
name = Column(String(50)) name: Mapped[str] = mapped_column(String(50))
icon = Column(String(50)) icon: Mapped[Optional[str]] = mapped_column(String(50))
units = Column(JSON, server_default=text("'{\"power\": \"kW\", \"weight\": \"kg\", \"cargo\": \"m3\"}'::jsonb")) units: Mapped[dict] = mapped_column(JSONB, server_default=text("'{\"power\": \"kW\", \"weight\": \"kg\"}'::jsonb"))
# Kapcsolatok
features: Mapped[List["FeatureDefinition"]] = relationship("FeatureDefinition", back_populates="vehicle_type")
definitions: Mapped[List["VehicleModelDefinition"]] = relationship("VehicleModelDefinition", back_populates="v_type_rel")
features = relationship("FeatureDefinition", back_populates="vehicle_type")
definitions = relationship("VehicleModelDefinition", back_populates="v_type_rel")
class FeatureDefinition(Base): class FeatureDefinition(Base):
"""Globális felszereltség szótár""" """ Felszereltségi elemek definíciója (pl. ABS, Klíma, LED fényszóró) """
__tablename__ = "feature_definitions" __tablename__ = "feature_definitions"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id")) vehicle_type_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.vehicle_types.id"))
category = Column(String(50)) code: Mapped[str] = mapped_column(String(50), index=True)
name = Column(String(100), nullable=False) name: Mapped[str] = mapped_column(String(100))
data_type = Column(String(20), default="boolean") category: Mapped[str] = mapped_column(String(50), index=True)
vehicle_type = relationship("VehicleType", back_populates="features") vehicle_type: Mapped["VehicleType"] = relationship("VehicleType", back_populates="features")
model_maps: Mapped[List["ModelFeatureMap"]] = relationship("ModelFeatureMap", back_populates="feature")
class ModelFeatureMap(Base):
"""Modell-szintű felszereltségi sablon"""
__tablename__ = "model_feature_maps"
__table_args__ = {"schema": "data"}
model_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), primary_key=True)
feature_id = Column(Integer, ForeignKey("data.feature_definitions.id"), primary_key=True)
availability = Column(String(20), default="standard")
value = Column(String(100))
class VehicleModelDefinition(Base): class VehicleModelDefinition(Base):
"""MDM Master rekordok - v1.3.0 Pipeline Edition (Researcher & Alchemist)""" """
Robot v1.1.0 Multi-Tier MDM Master Adattábla.
Az ökoszisztéma technikai igazságforrása.
"""
__tablename__ = "vehicle_model_definitions" __tablename__ = "vehicle_model_definitions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
make: Mapped[str] = mapped_column(String(100), index=True)
marketing_name: Mapped[str] = mapped_column(String(255), index=True) # Nyers név az RDW-ből
official_marketing_name: Mapped[Optional[str]] = mapped_column(String(255)) # Dúsított, validált név (Robot 2.2)
# --- ROBOT LOGIKAI MEZŐK (JAVÍTVA 2.0 STÍLUSBAN) ---
attempts: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
last_error: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
# --- PRECISION LOGIC MEZŐK ---
normalized_name: Mapped[Optional[str]] = mapped_column(String(255), index=True, nullable=True)
marketing_name_aliases: Mapped[list] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
engine_code: Mapped[Optional[str]] = mapped_column(String(50), index=True) # A GLOBÁLIS KAPOCS
# --- TECHNIKAI AZONOSÍTÓK ---
technical_code: Mapped[str] = mapped_column(String(100), index=True) # Holland rendszám (kulcs)
variant_code: Mapped[Optional[str]] = mapped_column(String(100), index=True)
version_code: Mapped[Optional[str]] = mapped_column(String(100), index=True)
# --- SPECIFIKÁCIÓK ---
vehicle_type_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_types.id"))
vehicle_class: Mapped[Optional[str]] = mapped_column(String(50), index=True)
body_type: Mapped[Optional[str]] = mapped_column(String(100))
fuel_type: Mapped[Optional[str]] = mapped_column(String(50), index=True)
engine_capacity: Mapped[int] = mapped_column(Integer, default=0, index=True)
power_kw: Mapped[int] = mapped_column(Integer, default=0, index=True)
torque_nm: Mapped[Optional[int]] = mapped_column(Integer)
cylinders: Mapped[Optional[int]] = mapped_column(Integer)
cylinder_layout: Mapped[Optional[str]] = mapped_column(String(50))
curb_weight: Mapped[Optional[int]] = mapped_column(Integer)
max_weight: Mapped[Optional[int]] = mapped_column(Integer)
euro_classification: Mapped[Optional[str]] = mapped_column(String(20))
doors: Mapped[Optional[int]] = mapped_column(Integer)
transmission_type: Mapped[Optional[str]] = mapped_column(String(50))
drive_type: Mapped[Optional[str]] = mapped_column(String(50))
# --- ÉLETCIKLUS ÉS STÁTUSZ ---
year_from: Mapped[Optional[int]] = mapped_column(Integer, index=True)
year_to: Mapped[Optional[int]] = mapped_column(Integer, index=True)
production_status: Mapped[Optional[str]] = mapped_column(String(50)) # active / discontinued
# Státusz szintek: unverified, research_in_progress, awaiting_ai_synthesis, gold_enriched
status: Mapped[str] = mapped_column(String(50), server_default=text("'unverified'"), index=True)
is_manual: Mapped[bool] = mapped_column(Boolean, default=False)
source: Mapped[Optional[str]] = mapped_column(String(100))
# --- ADAT-KONTÉNEREK ---
raw_search_context: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
research_metadata: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
specifications: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # Robot 2.2/2.5 Arany adatai
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
last_research_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
# --- BEÁLLÍTÁSOK ---
__table_args__ = ( __table_args__ = (
UniqueConstraint('make', 'technical_code', 'vehicle_type', name='uix_make_tech_type'), UniqueConstraint('make', 'normalized_name', 'variant_code', 'version_code', 'fuel_type', name='uix_vmd_precision'),
Index('idx_vmd_lookup', 'make', 'technical_code'), Index('idx_vmd_lookup_fast', 'make', 'normalized_name'),
Index('idx_vmd_engine_bridge', 'make', 'engine_code'),
{"schema": "data"} {"schema": "data"}
) )
id = Column(Integer, primary_key=True) # KAPCSOLATOK
make = Column(String(50), nullable=False, index=True) v_type_rel: Mapped["VehicleType"] = relationship("VehicleType", back_populates="definitions")
technical_code = Column(String(50), nullable=False, index=True) feature_maps: Mapped[List["ModelFeatureMap"]] = relationship("ModelFeatureMap", back_populates="model_definition")
marketing_name = Column(String(100), index=True)
family_name = Column(String(100))
vehicle_type = Column(String(30), index=True) # Hivatkozás az asset.py-ban lévő osztályra
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id")) # Megjegyzés: Ha az AssetCatalog nincs itt importálva, húzzal adjuk meg a nevet
vehicle_class = Column(String(50)) variants: Mapped[List["AssetCatalog"]] = relationship("AssetCatalog", back_populates="master_definition")
parent_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True)
year_from = Column(Integer, nullable=True, index=True)
year_to = Column(Integer, nullable=True, index=True)
synonyms = Column(JSON, server_default=text("'[]'::jsonb"))
# --- ROBOT VÉDELMI ÉS PIPELINE MEZŐK (v1.3.0) --- class ModelFeatureMap(Base):
is_manual = Column(Boolean, default=False, server_default=text("false"), index=True) """ Kapcsolótábla a modellek és az alapfelszereltség között """
attempts = Column(Integer, default=0, server_default=text("0"), index=True) __tablename__ = "model_feature_maps"
last_error = Column(Text, nullable=True) __table_args__ = {"schema": "data"}
# Robot 2.1 "Researcher" porszívózott nyers adatai (A szemetesláda) id: Mapped[int] = mapped_column(Integer, primary_key=True)
raw_search_context = Column(Text, nullable=True) model_definition_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.vehicle_model_definitions.id"))
feature_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.feature_definitions.id"))
is_standard: Mapped[bool] = mapped_column(Boolean, default=True)
# Telemetria és forrás adatok (JSONB a hatékonyabb kereséshez) model_definition: Mapped["VehicleModelDefinition"] = relationship("VehicleModelDefinition", back_populates="feature_maps")
research_metadata = Column(JSONB, server_default=text("'{}'::jsonb"), nullable=False) feature: Mapped["FeatureDefinition"] = relationship("FeatureDefinition", back_populates="model_maps")
# --------------------------------------------------
# --- TECHNIKAI FIX OSZLOPOK ---
engine_capacity = Column(Integer, index=True)
power_kw = Column(Integer, index=True)
max_weight_kg = Column(Integer, index=True)
axle_count = Column(Integer)
payload_capacity_kg = Column(Integer)
cargo_volume_m3 = Column(Numeric(10, 2))
cargo_length_mm = Column(Integer)
cargo_width_mm = Column(Integer)
cargo_height_mm = Column(Integer)
specifications = Column(JSON, server_default=text("'{}'::jsonb"))
features_json = Column(JSON, server_default=text("'{}'::jsonb"))
# Státusz mező hossza 30-ra növelve az automatikus migrációhoz
status = Column(String(30), server_default="unverified", index=True)
is_master = Column(Boolean, default=False)
source = Column(String(50))
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Kapcsolatok
v_type_rel = relationship("VehicleType", back_populates="definitions")
master_record = relationship("VehicleModelDefinition", remote_side=[id], backref="merged_variants")
variants = relationship("AssetCatalog", back_populates="master_definition", primaryjoin="VehicleModelDefinition.id == AssetCatalog.master_definition_id")

Some files were not shown because too many files have changed in this diff Show More