chore: Archive legacy docs and backup files, prepare for codebase cleanup v2.0

This commit is contained in:
2026-02-18 00:11:50 +00:00
parent b11b9bce87
commit 5757754aae
94 changed files with 405 additions and 0 deletions

View File

@@ -1,95 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
# Cím beállítása
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def build_db():
print(f"🔌 Kapcsolódás a rendszerhez...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("🏗️ 'DATA' séma előkészítése...")
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data;"))
# 1. FELHASZNÁLÓK TÁBLA (Céges/Magán logika)
print("👤 Users tábla létrehozása...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) DEFAULT 'PRIVATE', -- 'PRIVATE', 'COMPANY', 'ADMIN'
company_name VARCHAR(255), -- Ha cég
tax_number VARCHAR(50), -- Ha cég
is_active BOOLEAN DEFAULT FALSE, -- Email megerősítésig
created_at TIMESTAMP DEFAULT NOW()
);
"""))
# 2. JÁRMŰ TÖRZS (A Fizikai Vas - Tulajdonos nélkül!)
print("🚗 Vehicles tábla létrehozása (A Vas)...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.vehicles (
id SERIAL PRIMARY KEY,
model_id INTEGER REFERENCES ref.vehicle_models(id),
vin VARCHAR(50) UNIQUE NOT NULL, -- ALVÁZSZÁM (Az igazi kulcs)
current_plate VARCHAR(20) NOT NULL, -- A mostani rendszám
production_year INTEGER,
created_at TIMESTAMP DEFAULT NOW()
);
"""))
# 3. ÉLETÚT ÉS JOGOSULTSÁG (A Történelem)
print("📜 History tábla létrehozása (Kié mikor volt?)...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.vehicle_history (
id SERIAL PRIMARY KEY,
vehicle_id INTEGER REFERENCES data.vehicles(id),
user_id INTEGER REFERENCES data.users(id),
role VARCHAR(20) NOT NULL, -- 'OWNER' (Tulaj), 'DRIVER' (Sofőr), 'LEASE' (Lízingelő)
start_date DATE NOT NULL, -- Mikor vette át?
end_date DATE, -- Mikor adta le? (Ha NULL, akkor nála van!)
start_mileage INTEGER, -- Óraállás átvételkor
end_mileage INTEGER, -- Óraállás leadáskor
is_active BOOLEAN GENERATED ALWAYS AS (end_date IS NULL) STORED -- Segédmező a gyors kereséshez
);
"""))
# 4. KÖLTSÉGEK (Szeparált pénzügyek)
print("💸 Costs tábla létrehozása...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.costs (
id SERIAL PRIMARY KEY,
vehicle_id INTEGER REFERENCES data.vehicles(id),
user_id INTEGER REFERENCES data.users(id), -- Ki fizette? (Csak ő láthatja!)
cost_type VARCHAR(50) NOT NULL, -- FUEL, SERVICE, TAX, INSURANCE, LEASE...
amount DECIMAL(10, 2) NOT NULL,
date DATE NOT NULL,
mileage_at_cost INTEGER,
description TEXT,
document_url VARCHAR(255), -- Fotó linkje
created_at TIMESTAMP DEFAULT NOW()
);
"""))
print("✅ KÉSZ! A Professzionális Adatmodell felépült.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(build_db())

View File

@@ -1,51 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
# DB Config
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def check_data():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("\n🚗 A TE GARÁZSOD (User ID: 1):")
print("-" * 80)
# Ez a lekérdezés összeköti a Történelmet (History), a Vasat (Vehicle) és a Katalógust (Model)
query = text("""
SELECT
vh.role,
vh.start_date,
v.vin,
v.current_plate,
m.name as brand,
mo.model_name
FROM data.vehicle_history vh
JOIN data.vehicles v ON vh.vehicle_id = v.id
JOIN ref.vehicle_models mo ON v.model_id = mo.id
JOIN ref.vehicle_makes m ON mo.make_id = m.id
WHERE vh.user_id = 1;
""")
result = await conn.execute(query)
rows = result.fetchall()
if not rows:
print("📭 A garázs üres.")
else:
for r in rows:
print(f"🔹 {r.brand} {r.model_name} | 🆔 {r.vin} | 🔢 {r.current_plate}")
print(f" Jogviszony: {r.role} | Kezdete: {r.start_date}")
print("-" * 80)
await engine.dispose()
if __name__ == "__main__":
asyncio.run(check_data())

View File

@@ -1,35 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
# Adatbázis elérés
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def create_user():
print(f"🔌 Kapcsolódás...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("👤 1-es számú felhasználó beszúrása...")
# Kényszerítjük az ID=1-et, hogy passzoljon a main.py-hoz
await conn.execute(text("""
INSERT INTO data.users (id, email, password_hash, role, is_active)
VALUES (1, 'demo@user.com', 'dummy_hash', 'PRIVATE', TRUE)
ON CONFLICT (id) DO NOTHING;
"""))
# Frissítjük a számlálót, hogy a következő user ID 2 legyen (ne akadjon össze)
await conn.execute(text("SELECT setval('data.users_id_seq', (SELECT MAX(id) FROM data.users));"))
print("✅ KÉSZ! A Demo User (ID: 1) létezik.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(create_user())

View File

@@ -1,42 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def hire_driver():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("👤 Kovács János (User ID: 2) létrehozása...")
# Létrehozzuk a User-t
await conn.execute(text("""
INSERT INTO data.users (id, email, password_hash, role, country, default_currency, is_active)
VALUES (2, 'sofor@ceg.hu', 'hash123', 'PRIVATE', 'HU', 'HUF', TRUE)
ON CONFLICT (id) DO NOTHING;
"""))
# Frissítjük a sorrendet
await conn.execute(text("SELECT setval('data.users_id_seq', (SELECT MAX(id) FROM data.users));"))
print("🤝 Hozzárendelés a Te cégedhez (ID: 1)...")
# Betesszük a fleet_members táblába
await conn.execute(text("""
INSERT INTO data.fleet_members (user_id, owner_id, role)
VALUES (2, 1, 'DRIVER')
ON CONFLICT (user_id, owner_id) DO NOTHING;
"""))
print("✅ KÉSZ! Kovács János mostantól a csapatod tagja.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(hire_driver())

View File

@@ -1,17 +0,0 @@
services:
service_finder_api:
build: .
container_name: service_finder_api
env_file:
- .env
networks:
- existing_net
ports:
- "8000:8000"
restart: unless-stopped
networks:
existing_net:
external: true
name: docker-server_internal_net

View File

@@ -1,81 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
# Cím összeállítása (Ugyanaz a logika, mint a main.py-ban)
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def init_db():
print(f"🔌 Kapcsolódás ide: {DATABASE_URL}")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("🏗️ Séma és Táblák létrehozása...")
# 1. Séma létrehozása
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS ref;"))
# 2. Márka tábla
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ref.vehicle_makes (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE
);
"""))
# 3. Modell tábla
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ref.vehicle_models (
id SERIAL PRIMARY KEY,
make_id INTEGER REFERENCES ref.vehicle_makes(id),
model_name VARCHAR(100) NOT NULL,
category VARCHAR(50)
);
"""))
print("🧹 Meglévő adatok törlése (Tiszta lap)...")
await conn.execute(text("TRUNCATE TABLE ref.vehicle_models, ref.vehicle_makes RESTART IDENTITY CASCADE;"))
print("🚗 Adatok beszúrása...")
# Adatok
makes = ["BMW", "Audi", "Mercedes-Benz", "Toyota", "Honda", "Suzuki", "Volkswagen"]
models = [
("BMW", "3 Series", "Autó"), ("BMW", "5 Series", "Autó"), ("BMW", "X5", "Autó"), ("BMW", "R 1250 GS", "Motor"),
("Audi", "A3", "Autó"), ("Audi", "A4", "Autó"), ("Audi", "A6", "Autó"), ("Audi", "Q5", "Autó"),
("Mercedes-Benz", "C-Class", "Autó"), ("Mercedes-Benz", "E-Class", "Autó"), ("Mercedes-Benz", "S-Class", "Autó"), ("Mercedes-Benz", "G-Class", "Autó"),
("Toyota", "Corolla", "Autó"), ("Toyota", "Yaris", "Autó"), ("Toyota", "RAV4", "Autó"), ("Toyota", "Hilux", "Autó"),
("Honda", "Civic", "Autó"), ("Honda", "CR-V", "Autó"), ("Honda", "Africa Twin", "Motor"), ("Honda", "CB500X", "Motor"),
("Suzuki", "Swift", "Autó"), ("Suzuki", "Vitara", "Autó"), ("Suzuki", "S-Cross", "Autó"), ("Suzuki", "DL 650 V-Strom", "Motor"),
("Volkswagen", "Golf", "Autó"), ("Volkswagen", "Passat", "Autó"), ("Volkswagen", "Polo", "Autó"), ("Volkswagen", "Tiguan", "Autó")
]
# Márkák beszúrása
for make in makes:
await conn.execute(text(f"INSERT INTO ref.vehicle_makes (name) VALUES ('{make}') ON CONFLICT DO NOTHING;"))
# Modellek beszúrása (kicsit trükkös, mert kell a make_id)
for brand, model, cat in models:
# Megkeressük az ID-t
result = await conn.execute(text(f"SELECT id FROM ref.vehicle_makes WHERE name = '{brand}'"))
make_id = result.scalar()
# Beszúrjuk a modellt
await conn.execute(text(f"""
INSERT INTO ref.vehicle_models (make_id, model_name, category)
VALUES ({make_id}, '{model}', '{cat}')
"""))
print("✅ KÉSZ! Az adatbázis feltöltve.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(init_db())

View File

@@ -1,61 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
# Cím beállítása
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def inspect_schema():
print(f"🔎 Kapcsolódás az adatbázishoz...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
# SQL lekérdezés a rendszer katalógusból (information_schema)
# Ez megmondja milyen táblák és oszlopok léteznek
query = text("""
SELECT
table_schema,
table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema IN ('public', 'ref', 'data')
ORDER BY table_schema, table_name, ordinal_position;
""")
result = await conn.execute(query)
rows = result.fetchall()
if not rows:
print("⚠️ Nem találtam táblákat a 'ref' vagy 'data' sémákban!")
current_table = ""
for row in rows:
schema = row.table_schema
table = row.table_name
full_table_name = f"{schema}.{table}"
# Ha új táblához érünk, kiírjuk a nevét
if full_table_name != current_table:
print(f"\n📦 TÁBLA: {full_table_name.upper()}")
print("-" * 50)
print(f"{'OSZLOP NÉV':<20} | {'TÍPUS':<15} | {'KÖTELEZŐ?'}")
print("-" * 50)
current_table = full_table_name
# Oszlop adatok
req = "IGEN" if row.is_nullable == 'NO' else "nem"
print(f"{row.column_name:<20} | {row.data_type:<15} | {req}")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(inspect_schema())

View File

@@ -1,69 +0,0 @@
import asyncio
from sqlalchemy import inspect, text
from sqlalchemy.ext.asyncio import create_async_engine
# CSERÉLD KI a saját adataidra, ha nem ezeket használod!
DATABASE_URL = "postgresql+asyncpg://kincses:MiskociA74@postgres-db:5432/service_finder"
async def run_inspection():
# Aszinkron motor létrehozása
engine = create_async_engine(DATABASE_URL)
async with engine.connect() as conn:
def do_inspect(sync_conn):
inspector = inspect(sync_conn)
# 1. Sémák lekérése (amiket mi hoztunk létre + public)
target_schemas = ['public', 'data', 'ref']
all_schemas = inspector.get_schema_names()
schemas_to_check = [s for s in target_schemas if s in all_schemas]
print(f"{'='*60}")
print(f" JÁRMŰNYILVÁNTARTÓ RENDSZER - ADATBÁZIS AUDIT")
print(f"{'='*60}")
for schema in schemas_to_check:
print(f"\n--- SÉMA: {schema.upper()} ---")
tables = inspector.get_table_names(schema=schema)
if not tables:
print(" (Nincsenek táblák ebben a sémában)")
continue
for table_name in tables:
print(f"\n[Tábla: {schema}.{table_name}]")
# Oszlopok részletei
columns = inspector.get_columns(table_name, schema=schema)
for col in columns:
pk = " [PK]" if col['primary_key'] else ""
nullable = "NULL" if col['nullable'] else "NOT NULL"
print(f" L {col['name']:<15} | {str(col['type']):<12} | {nullable}{pk}")
# Idegen kulcsok (Kapcsolatok)
fks = inspector.get_foreign_keys(table_name, schema=schema)
for fk in fks:
constrained = fk['constrained_columns']
referred_schema = fk['referred_schema']
referred_table = fk['referred_table']
referred_cols = fk['referred_columns']
print(f" --> Kapcsolat: {constrained} -> {referred_schema}.{referred_table}({referred_cols})")
# Mivel az SQLAlchemy inspector alapvetően szinkron, run_sync-et használunk
await conn.run_sync(do_inspect)
# 2. Extra: Kiterjesztések ellenőrzése
print(f"\n{'='*60}")
print(" AKTÍV POSTGRESQL KITERJESZTÉSEK:")
result = await conn.execute(text("SELECT extname FROM pg_extension;"))
for row in result:
print(f" - {row[0]}")
print(f"{'='*60}")
await engine.dispose()
if __name__ == "__main__":
try:
asyncio.run(run_inspection())
except Exception as e:
print(f"HIBA TÖRTÉNT: {e}")

View File

@@ -1,81 +0,0 @@
from fastapi import FastAPI, HTTPException, Form, Depends
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from datetime import datetime, timedelta, date
from jose import jwt
import bcrypt, os, traceback
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = "SZUPER_TITKOS_KULCS_2026"
ALGORITHM = "HS256"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
DATABASE_URL = os.getenv("DATABASE_URL", "").replace("postgresql://", "postgresql+asyncpg://")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
app = FastAPI()
# --- AUTH ---
def get_password_hash(p): return bcrypt.hashpw(p.encode('utf-8')[:72], bcrypt.gensalt()).decode('utf-8')
def verify_password(p, h): return bcrypt.checkpw(p.encode('utf-8')[:72], h.encode('utf-8'))
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
p = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return int(p.get("sub"))
except: raise HTTPException(status_code=401)
@app.post("/api/auth/register")
async def register(email: str = Form(...), password: str = Form(...)):
try:
async with AsyncSessionLocal() as session:
async with session.begin():
await session.execute(text("INSERT INTO data.users (email, password_hash) VALUES (:e, :p)"),
{"e": email, "p": get_password_hash(password)})
return {"status": "success"}
except Exception as e:
return JSONResponse(status_code=400, content={"detail": "Email már létezik vagy adatbázis hiba."})
@app.post("/api/auth/login")
async def login(f: OAuth2PasswordRequestForm = Depends()):
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT id, password_hash FROM data.users WHERE email = :e"), {"e": f.username})
u = res.fetchone()
if not u or not verify_password(f.password, u.password_hash): raise HTTPException(status_code=401, detail="Hibás adatok")
t = jwt.encode({"sub": str(u.id), "exp": datetime.utcnow() + timedelta(days=1)}, SECRET_KEY, algorithm=ALGORITHM)
return {"access_token": t, "token_type": "bearer"}
# --- DATA ---
@app.get("/api/meta/vehicle-hierarchy")
async def get_hierarchy():
async with AsyncSessionLocal() as session:
q = text("SELECT vm.category, m.name as brand, vm.id as model_id, vm.model_name FROM ref.vehicle_models vm JOIN ref.vehicle_makes m ON vm.make_id = m.id ORDER BY vm.category, m.name")
res = await session.execute(q)
h = {}
for r in res:
if r.category not in h: h[r.category] = {}
if r.brand not in h[r.category]: h[r.category][r.brand] = []
h[r.category][r.brand].append({"id": r.model_id, "name": r.model_name})
return h
@app.get("/api/meta/cost-types")
async def get_cost_types():
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT code, name FROM ref.cost_types ORDER BY name"))
return {r.code: r.name for r in res.fetchall()}
@app.get("/api/my_vehicles")
async def my_vehicles(uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
q = text("SELECT v.id as vehicle_id, v.current_plate as plate, m.name as brand, mo.model_name as model, v.status FROM data.vehicle_history vh JOIN data.vehicles v ON vh.vehicle_id = v.id JOIN ref.vehicle_models mo ON v.model_id = mo.id JOIN ref.vehicle_makes m ON mo.make_id = m.id WHERE vh.user_id = :uid AND vh.end_date IS NULL")
res = await session.execute(q, {"uid": uid})
return [dict(r._mapping) for r in res.fetchall()]
@app.get("/")
async def index(): return FileResponse("/app/frontend/index.html")

View File

@@ -1,120 +0,0 @@
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, validator
from typing import Optional
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
import os
from datetime import date
from dotenv import load_dotenv
load_dotenv()
# --- 1. ADATBÁZIS KAPCSOLAT ---
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
fixed_url = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
engine = create_async_engine(fixed_url)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
app = FastAPI(title="Service Finder API")
# --- 2. ADATMODELLEK (VALIDÁCIÓVAL) ---
class VehicleRegister(BaseModel):
model_id: int
vin: str # Alvázszám (KÖTELEZŐ!)
plate: str # Rendszám
mileage: int # Km óra
purchase_date: date
role: str = "OWNER" # Alapértelmezett: Tulajdonos
# Adatőr: Automatikus nagybetűsítés és tisztítás
@validator('vin')
def clean_vin(cls, v):
if len(v) < 5: raise ValueError('Az alvázszám túl rövid!')
return v.upper().replace("-", "").replace(" ", "")
@validator('plate')
def clean_plate(cls, v):
return v.upper().replace("-", "").replace(" ", "")
@validator('mileage')
def positive_mileage(cls, v):
if v < 0: raise ValueError('A kilométer nem lehet negatív!')
return v
# --- 3. VÉGPONTOK ---
# Lista lekérése (Katalógus)
@app.get("/api/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
result = await session.execute(text("""
SELECT vm.id, m.name as brand, vm.model_name, vm.category
FROM ref.vehicle_models vm
JOIN ref.vehicle_makes m ON vm.make_id = m.id
ORDER BY m.name, vm.model_name
"""))
return [{"id": r.id, "brand": r.brand, "model": r.model_name, "category": r.category} for r in result.fetchall()]
# ÚJ: JÁRMŰ REGISZTRÁCIÓ (A Nagy Logika)
@app.post("/api/register")
async def register_vehicle(data: VehicleRegister):
async with AsyncSessionLocal() as session:
async with session.begin(): # Tranzakció indítása
try:
# 1. Megnézzük, létezik-e már ez a VAS (Alvázszám alapján)?
check_query = text("SELECT id FROM data.vehicles WHERE vin = :vin")
result = await session.execute(check_query, {"vin": data.vin})
existing_car_id = result.scalar()
vehicle_id = existing_car_id
# 2. HA NEM LÉTEZIK -> Létrehozzuk a VASAT
if not vehicle_id:
print(f"🆕 Új autó az adatbázisban: {data.vin}")
insert_car = text("""
INSERT INTO data.vehicles (model_id, vin, current_plate)
VALUES (:mid, :vin, :plt)
RETURNING id
""")
res = await session.execute(insert_car, {
"mid": data.model_id, "vin": data.vin, "plt": data.plate
})
vehicle_id = res.scalar()
else:
print(f"♻️ Létező autó átvétele: {data.vin} (ID: {vehicle_id})")
# Itt később lezárhatjuk az előző tulajdonos history-ját!
# 3. BEJEGYZÉS A TÖRTÉNELEMBE (HISTORY)
# Ez köti össze a User-t (most fixen ID=1) az Autóval
insert_history = text("""
INSERT INTO data.vehicle_history
(vehicle_id, user_id, role, start_date, start_mileage)
VALUES (:vid, 1, :role, :s_date, :s_mil)
""")
await session.execute(insert_history, {
"vid": vehicle_id,
"role": data.role,
"s_date": data.purchase_date,
"s_mil": data.mileage
})
return {"status": "success", "message": "Jármű sikeresen rögzítve a flottában!"}
except Exception as e:
print(f"❌ Hiba: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Frontend kiszolgálása
@app.get("/")
async def serve_frontend():
if os.path.exists("/app/frontend/index.html"):
return FileResponse("/app/frontend/index.html")
return {"error": "Frontend hiányzik"}

View File

@@ -1,49 +0,0 @@
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
import os
from dotenv import load_dotenv
load_dotenv()
app = FastAPI(title="Service Finder API")
# --- A DUPLA JAVÍTÁS ---
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
# Vészhelyzeti fallback (ha nem lenne .env)
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
# 1. Javítás: Driver csere (asyncpg)
fixed_url = raw_url.replace("postgresql://", "postgresql+asyncpg://")
# 2. Javítás: Adatbázis név csere (Ha véletlenül _db a vége, levágjuk)
fixed_url = fixed_url.replace("/service_finder_db", "/service_finder")
print(f"🔧 VÉGLEGES ADATBÁZIS CÍM: {fixed_url}")
# Kapcsolódás a javított címmel
engine = create_async_engine(fixed_url)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@app.get("/")
async def root():
return {"message": "Service Finder API működik! 🚀"}
@app.get("/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
try:
query = text("""
SELECT m.name as brand, vm.model_name, vm.category
FROM ref.vehicle_models vm
JOIN ref.vehicle_makes m ON vm.make_id = m.id
ORDER BY m.name, vm.model_name
""")
result = await session.execute(query)
vehicles = result.fetchall()
return [{"brand": r.brand, "model": r.model_name, "category": r.category} for r in vehicles]
except Exception as e:
return {"error": str(e)}

View File

@@ -1,52 +0,0 @@
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
import os
from dotenv import load_dotenv
# Betöltjük a környezeti változókat
load_dotenv()
app = FastAPI(title="Service Finder API")
# --- A FIX ---
# Kiolvassuk a címet
raw_url = os.getenv("DATABASE_URL")
# Ha nincs beállítva, adunk egy vészhelyzeti alapértelmezettet
if not raw_url:
print("⚠️ FIGYELEM: Nincs DATABASE_URL, alapértelmezett használata!")
# Itt a konténer belső nevét (postgres-db) használjuk
raw_url = "postgresql://admin:MiskociA74@postgres-db:5432/service_finder"
# A LÉNYEG: Ha hiányzik az '+asyncpg', hozzáadjuk!
if "asyncpg" not in raw_url:
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://")
print(f"🔧 URL javítva erre: {DATABASE_URL}")
else:
DATABASE_URL = raw_url
# Motor indítása a javított URL-lel
engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@app.get("/")
async def root():
return {"message": "Service Finder API működik! 🚀"}
@app.get("/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
try:
query = text("""
SELECT m.name as brand, vm.model_name, vm.category
FROM ref.vehicle_models vm
JOIN ref.vehicle_makes m ON vm.make_id = m.id
ORDER BY m.name, vm.model_name
""")
result = await session.execute(query)
vehicles = result.fetchall()
return [{"brand": r.brand, "model": r.model_name, "category": r.category} for r in vehicles]
except Exception as e:
return {"error": str(e)}

View File

@@ -1,71 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def seed_cost_types():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("🔨 Költség típusok tábla létrehozása...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ref.cost_types (
code VARCHAR(50) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
parent_code VARCHAR(50) REFERENCES ref.cost_types(code), -- Hierarchia!
is_active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0
);
"""))
print("📥 Adatok feltöltése...")
# Először a szülők (Főkategóriák)
parents = [
('FUEL', '⛽ Tankolás', 10),
('PURCHASE', '💰 Beszerzés / Pénzügy', 20),
('INSURANCE', '📄 Biztosítás', 30),
('TAX', '🏛️ Adó / Illeték', 40),
('SERVICE', '🔧 Szerviz', 50),
('OTHER', 'Egyéb', 99)
]
for p in parents:
await conn.execute(text("INSERT INTO ref.cost_types (code, name, sort_order) VALUES (:c, :n, :s) ON CONFLICT (code) DO NOTHING"), {"c": p[0], "n": p[1], "s": p[2]})
# Aztán a gyerekek (Alkategóriák)
children = [
# Beszerzés
('PURCHASE_PRICE', 'Vételár', 'PURCHASE'),
('FINANCE_LEASE', 'Lízing díj', 'PURCHASE'),
('FINANCE_LOAN', 'Hitel törlesztő', 'PURCHASE'),
('IMPORT_FEE', 'Honosítás / Regadó', 'PURCHASE'),
# Biztosítás
('INSURANCE_KGFB', 'Kötelező (KGFB)', 'INSURANCE'),
('INSURANCE_CASCO', 'Casco', 'INSURANCE'),
('INSURANCE_GAP', 'GAP', 'INSURANCE'),
('INSURANCE_ASSIST', 'Assistance', 'INSURANCE'),
# Adó
('TAX_WEIGHT', 'Gépjárműadó (Súlyadó)', 'TAX'),
('TAX_COMPANY', 'Cégautóadó', 'TAX'),
('TAX_TRANSFER', 'Vagyonszerzési illeték', 'TAX'),
('TAX_OTHER', 'Egyéb adó', 'TAX'),
# Szerviz
('SERVICE_MAINTENANCE', 'Kötelező karbantartás', 'SERVICE'),
('SERVICE_REPAIR', 'Javítás', 'SERVICE'),
('SERVICE_TIRE', 'Gumicsere', 'SERVICE'),
('SERVICE_MOT', 'Műszaki vizsga', 'SERVICE')
]
for c in children:
await conn.execute(text("INSERT INTO ref.cost_types (code, name, parent_code) VALUES (:c, :n, :p) ON CONFLICT (code) DO NOTHING"), {"c": c[0], "n": c[1], "p": c[2]})
print("✅ KÉSZ! A tudás most már az adatbázisban van, nem a HTML-ben.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(seed_cost_types())

View File

@@ -1,46 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def build_audit_system():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("📜 Audit Log tábla létrehozása...")
# Ez a tábla sosem töröl, csak ír! (Append-only)
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.audit_logs (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES data.users(id), -- KI csinálta?
event_type VARCHAR(50) NOT NULL, -- MIT? (pl. ISSUE_REPORT, MILEAGE_UPDATE)
target_id INTEGER, -- MELYIK autón? (Vehicle ID)
old_value TEXT, -- MI volt előtte? (A visszaállításhoz)
new_value TEXT, -- MI lett utána?
details TEXT, -- Egyéb megjegyzés (pl. hiba leírása)
ip_address VARCHAR(45), -- Honnan? (Biztonság)
created_at TIMESTAMP DEFAULT NOW()
);
"""))
print("🚦 Jármű Státusz mezők hozzáadása...")
# Bővítjük a járműveket, hogy tárolják a hibát
await conn.execute(text("""
ALTER TABLE data.vehicles
ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'OK', -- OK, WARNING, CRITICAL
ADD COLUMN IF NOT EXISTS current_issue TEXT; -- A hiba leírása
"""))
print("✅ KÉSZ! A mindent látó szem (Audit Log) aktív.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(build_audit_system())

View File

@@ -1,56 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def expand_categories():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("📥 Új Főkategóriák beszúrása...")
parents = [
('EQUIPMENT', '🛠️ Felszerelés / Extrák', 25), # Vételár után
('OPERATIONAL', '🅿️ Üzemeltetés / Út', 15), # Tankolás után
('FINE', '👮 Bírság / Büntetés', 90)
]
for p in parents:
await conn.execute(text("INSERT INTO ref.cost_types (code, name, sort_order) VALUES (:c, :n, :s) ON CONFLICT (code) DO NOTHING"), {"c": p[0], "n": p[1], "s": p[2]})
print("📥 Új Alkategóriák beszúrása...")
children = [
# Felszerelés (EQUIPMENT)
('EQUIP_SHELVING', 'Raktérburkolat / Polcrendszer', 'EQUIPMENT'),
('EQUIP_BRANDING', 'Matricázás / Fóliázás', 'EQUIPMENT'),
('EQUIP_TOWBAR', 'Vonóhorog / Tetőcsomagtartó', 'EQUIPMENT'),
('EQUIP_SAFETY', 'Kötelező tartozékok (Eü. csomag)', 'EQUIPMENT'),
('EQUIP_GPS', 'GPS / Nyomkövető hardver', 'EQUIPMENT'),
('EQUIP_WINTER_TIRE', 'Téli gumi szett (Beszerzés)', 'EQUIPMENT'),
# Üzemeltetés (OPERATIONAL)
('OP_PARKING', 'Parkolás', 'OPERATIONAL'),
('OP_TOLL', 'Útdíj / Matrica / Behajtás', 'OPERATIONAL'),
('OP_WASH', 'Mosás / Kozmetika', 'OPERATIONAL'),
('OP_ADBLUE', 'AdBlue', 'OPERATIONAL'),
('OP_EV_CHARGE', 'Elektromos töltés', 'OPERATIONAL'),
('OP_STORAGE', 'Gumi hotel / Tárolás', 'OPERATIONAL'),
# Bírság (FINE)
('FINE_SPEEDING', 'Gyorshajtás', 'FINE'),
('FINE_PARKING', 'Parkolási bírság', 'FINE'),
('FINE_ADMIN', 'Közigazgatási bírság', 'FINE')
]
for c in children:
await conn.execute(text("INSERT INTO ref.cost_types (code, name, parent_code) VALUES (:c, :n, :p) ON CONFLICT (code) DO NOTHING"), {"c": c[0], "n": c[1], "p": c[2]})
print("✅ KÉSZ! A költséglista most már teljeskörű.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(expand_categories())

View File

@@ -1,46 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def upgrade_db():
print(f"🔌 Kapcsolódás...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("🌍 Felhasználói tábla bővítése (Ország + Alapértelmezett pénznem)...")
# 1. Users tábla bővítése
await conn.execute(text("""
ALTER TABLE data.users
ADD COLUMN IF NOT EXISTS country VARCHAR(2) DEFAULT 'HU',
ADD COLUMN IF NOT EXISTS default_currency VARCHAR(3) DEFAULT 'HUF';
"""))
print("💶 Költség tábla bővítése (Tranzakciós pénznem)...")
# 2. Costs tábla bővítése
await conn.execute(text("""
ALTER TABLE data.costs
ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'HUF';
"""))
# Frissítjük a meglévő Demo Usert (ID=1)
print("👤 Demo User beállítása: Magyarország / HUF")
await conn.execute(text("""
UPDATE data.users
SET country = 'HU', default_currency = 'HUF'
WHERE id = 1;
"""))
print("✅ KÉSZ! Az adatbázis mostantól támogatja a több pénznemet.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(upgrade_db())

View File

@@ -1,26 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def add_doc_column():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("📄 Dokumentum oszlop hozzáadása a Costs táblához...")
await conn.execute(text("""
ALTER TABLE data.costs
ADD COLUMN IF NOT EXISTS document_url VARCHAR(255);
"""))
print("✅ KÉSZ! Mehetnek a fájlok.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(add_doc_column())

View File

@@ -1,58 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def upgrade_invites():
print(f"🔌 Kapcsolódás...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("📨 Invitations (Meghívók) tábla létrehozása...")
# Ez tárolja a függőben lévő meghívásokat
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.invitations (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
inviter_id INTEGER REFERENCES data.users(id), -- Ki hívta meg? (Cég)
role VARCHAR(20) NOT NULL, -- Milyen szerepre? (MANAGER, DRIVER)
access_level VARCHAR(20) DEFAULT 'FULL', -- A/B Sofőr szint
token VARCHAR(100) UNIQUE NOT NULL, -- A titkos link kódja
status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, ACCEPTED, EXPIRED
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP
);
"""))
print("🤝 Fleet Members (Többcéges tagság) tábla létrehozása...")
# Ez teszi lehetővé, hogy valaki több céghez is tartozzon
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.fleet_members (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES data.users(id), -- A Dolgozó
owner_id INTEGER REFERENCES data.users(id), -- A Cég / Tulajdonos
role VARCHAR(20) NOT NULL, -- FLEET_MANAGER, DRIVER
joined_at TIMESTAMP DEFAULT NOW(),
-- Egy ember egy cégnél csak egyszer szerepelhet
UNIQUE(user_id, owner_id)
);
"""))
# Takarítás: A régi 'parent_id' már nem kell, mert a fleet_members kiváltja
# De biztonságból egyelőre csak NULL-ra állítjuk, nem töröljük az oszlopot
# await conn.execute(text("UPDATE data.users SET parent_id = NULL;"))
print("✅ KÉSZ! A rendszer készen áll a biztonságos meghívókra.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(upgrade_invites())

View File

@@ -1,61 +0,0 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def upgrade_permissions():
print(f"🔌 Kapcsolódás...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("👑 USERS tábla bővítése (Rendszer és Szervezeti szintek)...")
# 1. SYS_ROLE: Rendszergazda / Moderátor / User
await conn.execute(text("""
ALTER TABLE data.users
ADD COLUMN IF NOT EXISTS sys_role VARCHAR(20) DEFAULT 'USER';
"""))
# 2. HIERARCHIA: Ki a főnököd? (Parent User ID)
# Ha NULL, akkor ő a Cégtulajdonos (SuperUser). Ha van ID, akkor Alkalmazott.
await conn.execute(text("""
ALTER TABLE data.users
ADD COLUMN IF NOT EXISTS parent_id INTEGER REFERENCES data.users(id);
"""))
# 3. ORG_ROLE: Cégen belüli szerep (Owner, Fleet Manager, Employee)
await conn.execute(text("""
ALTER TABLE data.users
ADD COLUMN IF NOT EXISTS org_role VARCHAR(20) DEFAULT 'OWNER';
"""))
print("🚦 VEHICLE_HISTORY tábla bővítése (Sofőr jogosultságok)...")
# 4. ACCESS_LEVEL: Mit láthat a sofőr? (COST_MANAGER vs LOG_ONLY)
await conn.execute(text("""
ALTER TABLE data.vehicle_history
ADD COLUMN IF NOT EXISTS access_level VARCHAR(20) DEFAULT 'FULL';
-- Lehetséges értékek: 'FULL', 'COST_MANAGER', 'LOG_ONLY'
"""))
# --- DEMO ADATOK FRISSÍTÉSE ---
print("👤 Demo User (ID:1) kinevezése Rendszergazdának és Cégtulajdonosnak...")
await conn.execute(text("""
UPDATE data.users
SET sys_role = 'SYS_ADMIN', org_role = 'OWNER', parent_id = NULL
WHERE id = 1;
"""))
print("✅ KÉSZ! A jogosultsági mátrix beépítve az adatbázisba.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(upgrade_permissions())

View File

@@ -1,18 +0,0 @@
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
def send_verification_email(to_email: str, token: str):
message = Mail(
from_email='noreply@servicefinder.pro', # Ezt majd igazítsd a SendGrid verified senderhez
to_emails=to_email,
subject='Service Finder - Regisztráció megerősítése',
html_content=f'<h3>Üdvözöljük a Service Finderben!</h3><p>A regisztráció befejezéséhez kattintson az alábbi linkre:</p><p><a href="https://servicefinder.pro/verify?token={token}">Megerősítem a regisztrációmat</a></p>'
)
try:
sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
response = sg.send(message)
return True
except Exception as e:
print(f"Email hiba: {e}")
return False

View File

@@ -1,24 +0,0 @@
# /opt/docker/dev/service_finder/backend/app/core/security.py
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any
import bcrypt
from jose import jwt
from app.core.config import settings
def verify_password(plain_password: str, hashed_password: str) -> bool:
if not hashed_password:
return False
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
def get_password_hash(password: str) -> str:
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)

View File

@@ -1,18 +0,0 @@
from sqlalchemy import Column, String, JSON, Boolean, DateTime, Integer, text
from sqlalchemy.sql import func
from app.db.base_class import Base
class SystemParameter(Base):
"""
Rendszerszintű dinamikus paraméterek tárolása.
Szinkronban az admin.py és config.py elvárásaival.
"""
__tablename__ = "system_parameters"
__table_args__ = {"schema": "data"}
# Az admin.py 'key' mezőt vár, nem 'key_name'-et!
key = Column(String(50), primary_key=True, index=True)
value = Column(JSON, server_default=text("'{}'::jsonb"), nullable=False)
description = Column(String(255), nullable=True)
is_active = Column(Boolean, default=True)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

View File

@@ -1,116 +0,0 @@
import os
import json
import logging
import asyncio
from typing import Dict, Any, Optional
from google import genai
from google.genai import types
from sqlalchemy import select
from app.db.session import SessionLocal
from app.models import SystemParameter
logger = logging.getLogger("AI-Service")
class AIService:
"""
AI Service v1.2.4 - Production Ready
- Robot 2 (Technical Enrichment) & Robot 3 (OCR)
- Fix: JSON response cleaning and array-to-dict transformation.
"""
api_key = os.getenv("GEMINI_API_KEY")
client = genai.Client(api_key=api_key) if api_key else None
PRIMARY_MODEL = "gemini-2.0-flash"
@classmethod
async def get_config_delay(cls) -> float:
"""Lekéri az adminisztrálható késleltetést az adatbázisból."""
try:
async with SessionLocal() as db:
stmt = select(SystemParameter).where(SystemParameter.key == "AI_REQUEST_DELAY")
res = await db.execute(stmt)
param = res.scalar_one_or_none()
return float(param.value) if param else 1.0
except Exception:
return 1.0
@classmethod
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str) -> Optional[Dict[str, Any]]:
"""Robot 2: Gépjármű technikai adatok dúsítása."""
if not cls.client:
return None
await asyncio.sleep(await cls.get_config_delay())
prompt = f"""
Jármű: {make} {raw_model} ({v_type}).
Adj technikai adatokat JSON formátumban.
FONTOS: A 'technical_code' mező NEM lehet üres. Ha nem tudod a gyári kódot, adj 'N/A' értéket!
Várt struktúra:
{{
"marketing_name": "tiszta marketing név",
"technical_code": "gyári kód vagy N/A",
"ccm": egész szám,
"kw": egész szám,
"maintenance": {{
"oil_type": "viszkozitás",
"oil_qty": tizedes tört literben,
"spark_plug": "gyertya típus",
"coolant": "hűtőfolyadék"
}}
}}
"""
config = types.GenerateContentConfig(
system_instruction="Profi gépjárműtechnikus vagy. Kizárólag tiszta JSON-t válaszolsz.",
response_mime_type="application/json",
temperature=0.1
)
try:
response = cls.client.models.generate_content(model=cls.PRIMARY_MODEL, contents=prompt, config=config)
res_json = json.loads(response.text)
if isinstance(res_json, list) and len(res_json) > 0:
res_json = res_json[0]
return res_json if isinstance(res_json, dict) else None
except Exception as e:
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
return None
@classmethod
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
"""Robot 3: Multimodális OCR elemzés (Képbeolvasás)."""
if not cls.client:
return None
await asyncio.sleep(await cls.get_config_delay())
prompts = {
"identity": "Személyes okmány adatok.",
"vehicle_reg": "Rendszám, alvázszám, technikai adatok.",
"invoice": "Számla adatok, összegek, dátumok.",
"odometer": "Csak a kilométeróra állása számként."
}
config = types.GenerateContentConfig(
system_instruction="Profi OCR dokumentum-elemző vagy. Csak tiszta JSON-t válaszolsz.",
response_mime_type="application/json"
)
try:
response = cls.client.models.generate_content(
model=cls.PRIMARY_MODEL,
contents=[
f"Elemezd ezt a képet ({doc_type}): {prompts.get(doc_type, '')}",
types.Part.from_bytes(data=image_data, mime_type="image/jpeg")
],
config=config
)
res_json = json.loads(response.text)
if isinstance(res_json, list) and len(res_json) > 0:
res_json = res_json[0]
return res_json if isinstance(res_json, dict) else None
except Exception as e:
logger.error(f"❌ AI OCR hiba ({doc_type}): {e}")
return None

View File

@@ -1,102 +0,0 @@
import asyncio
import httpx
import logging
import os # <--- EZ HIÁNYZOTT!
import datetime
from sqlalchemy import select
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-v1.2.4-Fixed")
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):
"""Hibatűrő RDW lekérdezés tisztított paraméterekkel."""
clean_model = str(model).strip().upper()
params = {"merk": make.upper(), "handelsbenaming": clean_model, "$limit": 1}
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
try:
resp = await client.get(cls.API_URL, params=params, timeout=15)
if resp.status_code == 200 and resp.json():
return resp.json()[0]
return None
except Exception:
return None
@classmethod
async def run(cls):
logger.info("🚀 Master Enricher INDUL (Atomi mentés üzemmód)...")
# 1. Csak az ID-kat kérjük le, hogy ne tartsuk nyitva a tranzakciót feleslegesen
async with SessionLocal() as main_db:
stmt = select(VehicleModelDefinition.id).where(
VehicleModelDefinition.status == "unverified"
).limit(50)
res = await main_db.execute(stmt)
ids = res.scalars().all()
if not ids:
logger.info("😴 Nincs dúsítandó adat.")
return
# 2. Egyesével dolgozzuk fel a rekordokat saját session-ben
for m_id in ids:
async with SessionLocal() as db:
try:
master = await db.get(VehicleModelDefinition, m_id)
if not master:
continue
logger.info(f"🧪 Feldolgozás: {master.make} {master.marketing_name} (ID: {m_id})")
data_found = False
# A: RDW fázis
rdw_data = await cls.fetch_rdw_tech_data(master.make, master.marketing_name)
if rdw_data:
master.engine_capacity = int(float(rdw_data.get("cilinderinhoud", 0))) or None
master.power_kw = int(float(rdw_data.get("netto_maximum_vermogen_kw", 0))) or None
data_found = True
# B: AI fázis (ha hiányzik adat vagy pontosítani kell)
if not data_found or master.engine_capacity is None:
ai_data = await AIService.get_clean_vehicle_data(
master.make, master.marketing_name, master.vehicle_type
)
if ai_data:
master.marketing_name = ai_data.get("marketing_name", master.marketing_name)
master.technical_code = ai_data.get("technical_code") or master.technical_code or "N/A"
master.engine_capacity = ai_data.get("ccm") or master.engine_capacity
master.power_kw = ai_data.get("kw") or master.power_kw
master.specifications = ai_data.get("maintenance", {})
data_found = True
# C: Mentés és véglegesítés
if data_found:
master.status = "ai_enriched"
master.updated_at = datetime.datetime.now()
await db.commit() # AZONNALI COMMIT A LEMEZRE
logger.info(f"✅ Sikeresen mentve: {master.marketing_name} (CCM: {master.engine_capacity})")
else:
logger.warning(f"⚠️ Nem találtam adatot az ID {m_id} esetében.")
except IntegrityError:
await db.rollback()
logger.warning(f"🚫 Duplikáció vagy Constraint hiba (ID: {m_id}). Kihagyva.")
except Exception as e:
await db.rollback()
logger.error(f"❌ Váratlan hiba az ID {m_id} esetében: {e}")
finally:
await db.close()
logger.info("🏁 50-es batch feldolgozva.")
if __name__ == "__main__":
asyncio.run(TechEnricher.run())