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

View File

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

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.get("/provider/inbox")
def provider_inbox(request: Request, provider_id: str):
cur = request.state.db.cursor()
cur.execute("""
SELECT * FROM app.v_provider_inbox
WHERE provider_listing_id = %s
ORDER BY created_at DESC
""", (provider_id,))
rows = cur.fetchall()
return rows
async def provider_inbox(provider_id: str, db: AsyncSession = Depends(get_db)):
""" Aszinkron szerviz-postaláda lekérdezés. """
query = text("""
SELECT * FROM data.service_profiles
WHERE id = :p_id
""")
result = await db.execute(query, {"p_id": provider_id})
return [dict(row._mapping) for row in result.fetchall()]

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

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

View File

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

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

View File

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

View File

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

View File

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

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.orm import DeclarativeBase
from app.core.config import settings
# A .env fájlból olvassuk majd, de teszthez:
DATABASE_URL = "postgresql+asyncpg://user:password@db_container_name:5432/db_name"
# Most már settings.SQLALCHEMY_DATABASE_URI létezik a property miatt!
engine = create_async_engine(
str(settings.SQLALCHEMY_DATABASE_URI),
echo=settings.DEBUG_MODE,
pool_size=20,
max_overflow=10,
pool_pre_ping=True,
)
engine = create_async_engine(DATABASE_URL, echo=True)
SessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
AsyncSessionLocal = async_sessionmaker(
autocommit=False,
autoflush=False,
bind=engine,
class_=AsyncSession,
expire_on_commit=False
)
class Base(DeclarativeBase):
pass

View File

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

View File

@@ -1,31 +1,27 @@
# /opt/docker/dev/service_finder/backend/app/db/middleware.py
from fastapi import Request
from app.db.session import SessionLocal
from app.services.config_service import config
from app.db.session import AsyncSessionLocal
from app.models.audit import OperationalLog # JAVÍTVA: Az új modell
from sqlalchemy import text
import json
async def audit_log_middleware(request: Request, call_next):
logging_enabled = await config.get_setting('audit_log_enabled', default=True)
# Itt a config_service-t is aszinkron módon kell hívni, ha szükséges
response = await call_next(request)
if logging_enabled and request.method != 'GET': # GET-et általában nem naplózunk a zaj miatt, de állítható
if request.method != 'GET':
try:
user_id = getattr(request.state, 'user_id', None) # Ha már be van lépve
async with SessionLocal() as db:
await db.execute(text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address)
VALUES (:u, :a, :e, :m, :ip)
"""), {
'u': user_id,
'a': f'API_CALL_{request.method}',
'e': str(request.url.path),
'm': request.method,
'ip': request.client.host
})
user_id = getattr(request.state, 'user_id', None)
async with AsyncSessionLocal() as db:
log = OperationalLog(
user_id=user_id,
action=f"API_CALL_{request.method}",
resource_type="ENDPOINT",
resource_id=str(request.url.path),
details={"ip": request.client.host, "method": request.method}
)
db.add(log)
await db.commit()
except Exception:
pass # A naplózás hibája nem akaszthatja meg a kiszolgálást
pass # A naplózás nem akaszthatja meg a folyamatot
return response
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 app.core.config import settings
from typing import AsyncGenerator
engine = create_async_engine(
settings.DATABASE_URL,
echo=False, # Termelésben ne legyen True a log-áradat miatt
echo=False,
future=True,
pool_size=30, # Megemelve a Researcher 15-20 szála miatt
max_overflow=20, # Extra rugalmasság csúcsidőben
pool_pre_ping=True # Megakadályozza a "Server closed connection" hibákat
pool_size=30, # A robotok száma miatt
max_overflow=20,
pool_pre_ping=True
)
AsyncSessionLocal = async_sessionmaker(
@@ -18,15 +19,10 @@ AsyncSessionLocal = async_sessionmaker(
autoflush=False
)
SessionLocal = AsyncSessionLocal
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
# JAVÍTVA: Nincs automatikus commit! Az endpoint felelőssége.
finally:
await session.close()

View File

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

View File

@@ -1,37 +1,82 @@
# /opt/docker/dev/service_finder/backend/app/final_admin_fix.py
import asyncio
from sqlalchemy import text
from app.db.session import SessionLocal, engine
from app.models.user import User, UserRole
import uuid
from sqlalchemy import text, select
from app.database import AsyncSessionLocal
from app.models.identity import User, Person, UserRole
from app.core.security import get_password_hash
async def run_fix():
async with SessionLocal() as db:
# 1. Ellenőrizzük az oszlopokat (biztonsági játék)
res = await db.execute(text("SELECT column_name FROM information_schema.columns WHERE table_schema = \u0027data\u0027 AND table_name = \u0027users\u0027"))
print("\n" + ""*50)
print("🛠️ ADMIN RENDSZERJAVÍTÁS ÉS INICIALIZÁLÁS (MB2.0)")
print(""*50)
async with AsyncSessionLocal() as db:
# 1. LOGIKA: Séma ellenőrzése az 'identity' névtérben
# Az MB2.0-ban a felhasználók már nem a 'data', hanem az 'identity' sémában vannak.
check_query = text("""
SELECT column_name FROM information_schema.columns
WHERE table_schema = 'identity' AND table_name = 'users'
""")
res = await db.execute(check_query)
cols = [r[0] for r in res.fetchall()]
print(f"INFO: Meglévő oszlopok: {cols}")
if "hashed_password" not in cols:
print("❌ HIBA: A hashed_password oszlop még mindig hiányzik! A migráció nem volt sikeres.")
if not cols:
print("❌ HIBA: Az 'identity.users' tábla nem található. Futtasd az Alembic migrációt!")
return
# 2. Admin létrehozása
res = await db.execute(text("SELECT id FROM data.users WHERE email = :e"), {"e": "admin@profibot.hu"})
if res.fetchone():
print("⚠ Az admin@profibot.hu már létezik.")
if "hashed_password" not in cols:
print("❌ HIBA: A 'hashed_password' oszlop hiányzik. Az adatbázis sémája elavult.")
return
# 2. LOGIKA: Admin keresése
admin_email = "admin@profibot.hu"
stmt = select(User).where(User.email == admin_email)
existing_res = await db.execute(stmt)
existing_admin = existing_res.scalar_one_or_none()
if existing_admin:
print(f"⚠️ Információ: A(z) {admin_email} felhasználó már létezik.")
# Opcionális: Jelszó kényszerített frissítése, ha elfelejtetted
# existing_admin.hashed_password = get_password_hash("Admin123!")
# await db.commit()
else:
admin = User(
email="admin@profibot.hu",
hashed_password=get_password_hash("Admin123!"),
first_name="Admin",
last_name="Profibot",
role=UserRole.ADMIN,
is_superuser=True,
is_active=True
)
db.add(admin)
await db.commit()
print("✅ SIKER: Admin felhasználó létrehozva!")
try:
# 3. LOGIKA: Person és User létrehozása (MB2.0 Standard)
# Előbb létrehozzuk a fizikai személyt
new_person = Person(
id_uuid=uuid.uuid4(),
first_name="Rendszer",
last_name="Adminisztrátor",
is_active=True
)
db.add(new_person)
await db.flush() # ID lekérése a mentés előtt
# Létrehozzuk a felhasználói fiókot az Admin role-al
new_admin = User(
email=admin_email,
hashed_password=get_password_hash("Admin123!"),
person_id=new_person.id,
role=UserRole.superadmin, # MB2.0 enum érték
is_active=True,
is_deleted=False,
preferred_language="hu"
)
db.add(new_admin)
await db.commit()
print(f"✅ SIKER: Superadmin létrehozva!")
print(f" 📧 Email: {admin_email}")
print(f" 🔑 Jelszó: Admin123!")
except Exception as e:
print(f"❌ HIBA a mentés során: {e}")
await db.rollback()
print("\n" + ""*50)
print("🏁 JAVÍTÁSI FOLYAMAT BEFEJEZŐDÖTT")
print(""*50 + "\n")
if __name__ == "__main__":
asyncio.run(run_fix())
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 logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware # ÚJ
from starlette.middleware.sessions import SessionMiddleware
from app.api.v1.api import api_router
from app.core.config import settings
from app.database import AsyncSessionLocal
from app.services.translation_service import translation_service
# Statikus mappák létrehozása induláskor
os.makedirs("static/previews", exist_ok=True)
# --- LOGGING KONFIGURÁCIÓ ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("Sentinel-Main")
# --- LIFESPAN (Startup/Shutdown események) ---
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
A rendszer 'ébredési' folyamata.
Itt töltődnek be a memóriába a globális erőforrások.
"""
logger.info("🛰️ Sentinel Master System ébredése...")
# 1. Nyelvi Cache betöltése az adatbázisból
async with AsyncSessionLocal() as db:
try:
await translation_service.load_cache(db)
logger.info("🌍 i18n fordítási kulcsok aktiválva.")
except Exception as e:
logger.error(f"❌ i18n hiba az induláskor: {e}")
# Statikus könyvtárak ellenőrzése
os.makedirs(settings.STATIC_DIR, exist_ok=True)
os.makedirs(os.path.join(settings.STATIC_DIR, "previews"), exist_ok=True)
yield
logger.info("💤 Sentinel Master System leállítása...")
# --- APP INICIALIZÁLÁS ---
app = FastAPI(
title="Service Finder API",
description="Traffic Ecosystem, Asset Vault & AI Evidence Processing",
version="2.0.0",
openapi_url="/api/v1/openapi.json",
docs_url="/docs"
title="Service Finder Master API",
description="Sentinel Traffic Ecosystem, Asset Vault & AI Evidence Processing",
version="2.0.1",
openapi_url=f"{settings.API_V1_STR}/openapi.json",
docs_url="/docs",
lifespan=lifespan
)
# --- SESSION MIDDLEWARE (Google Authhoz kötelező) ---
# --- SESSION MIDDLEWARE (OAuth2 / Google Auth támogatás) ---
# A secret_key az aláírt sütikhez (cookies) szükséges
app.add_middleware(
SessionMiddleware,
secret_key=settings.SECRET_KEY
)
# --- CORS BEÁLLÍTÁSOK ---
# --- CORS BEÁLLÍTÁSOK (Hálózati kapu) ---
# Itt engedélyezzük, hogy a Frontend (React/Mobile) elérje az API-t
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://192.168.100.10:3001",
"http://localhost:3001",
"https://dev.profibot.hu",
"https://app.profibot.hu"
],
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Statikus fájlok kiszolgálása (képek, letöltések)
app.mount("/static", StaticFiles(directory="static"), name="static")
# --- STATIKUS FÁJLOK ---
# Képek, PDF-ek és a generált nyelvi JSON-ök kiszolgálása
app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static")
# A V1-es API router bekötése a /api/v1 prefix alá
app.include_router(api_router, prefix="/api/v1")
# --- ROUTER BEKÖTÉSE ---
# Itt csatlakozik az összes API végpont (Auth, Fleet, Billing, stb.)
app.include_router(api_router, prefix=settings.API_V1_STR)
# --- ALAPVETŐ RENDSZER VÉGPONTOK ---
# --- ALAPVETŐ VÉGPONTOK ---
@app.get("/", tags=["System"])
async def root():
""" Rendszer azonosító végpont. """
return {
"status": "online",
"message": "Service Finder Master System v2.0",
"system": "Service Finder Master",
"version": "2.0.1",
"environment": "Production" if not settings.DEBUG_MODE else "Development",
"features": [
"Google Auth Enabled",
"Asset Vault",
"Org Onboarding",
"AI Evidence OCR (Robot 3)",
"Fleet Expenses (TCO)"
"Hierarchical i18n Enabled",
"Asset Vault 2.0",
"Sentinel Security Audit",
"Robot Pipeline (0-3)"
]
}
@app.get("/health", tags=["System"])
async def health_check():
"""
Monitoring végpont.
Ha ez 'ok'-t ad, a Docker és a Load Balancer tudja, hogy a szerver él.
"""
Monitoring és Load Balancer egészségügyi ellenőrző végpont.
"""
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
# MB 2.0: Kritikus javítás - Mindenki az app.database.Base-t használja!
from app.database import Base
from app.db.base_class import Base
# 1. Alapvető identitás és szerepkörök (Mindenki használja)
from .identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
# Identitás és Jogosultság
from .identity import Person, User, Wallet, VerificationToken, SocialAccount
# Szervezeti struktúra (HOZZÁADVA: OrganizationSalesAssignment)
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment
# Járművek és Eszközök (Digital Twin)
from .asset import (
Asset, AssetCatalog, AssetCost, AssetEvent,
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
)
# Szerviz és Szakértelem
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
# Földrajzi adatok és Címek
# 2. Földrajzi adatok és címek (Szervezetek és személyek használják)
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch, Rating
# Gamification és Economy
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
# 3. Jármű definíciók (Az Asset-ek használják, ezért előbb kell lenniük)
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
# Rendszerkonfiguráció (HASZNÁLJUK a frissített system.py-t!)
# 4. Szervezeti felépítés (Hivatkozik címekre és felhasználókra)
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole
# 5. Eszközök és katalógusok (Hivatkozik definíciókra és szervezetekre)
from .asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership
# 6. Üzleti logika és előfizetések
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
# 7. Szolgáltatások és staging (Hivatkozik szervezetekre és eszközökre)
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
# 8. Rendszer, Gamification és egyebek
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
from .system import SystemParameter
from .document import Document
from .translation import Translation
# Üzleti logika és Előfizetés
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
# Naplózás és Biztonság (HOZZÁADVA: audit.py modellek)
from .audit import SecurityAuditLog, ProcessLog, FinancialLedger # <--- KRITIKUS!
from .history import AuditLog, VehicleOwnership
from .audit import SecurityAuditLog, ProcessLog, FinancialLedger
from .history import AuditLog, LogSeverity
from .security import PendingAction
from .legal import LegalDocument, LegalAcceptance
from .logistics import Location, LocationType
# MDM (Master Data Management) Jármű modellek központ
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
# Aliasok a kényelmesebb fejlesztéshez
# Aliasok a Digital Twin kompatibilitáshoz
Vehicle = Asset
UserVehicle = Asset
VehicleCatalog = AssetCatalog
@@ -47,16 +42,17 @@ ServiceRecord = AssetEvent
__all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
"Organization", "OrganizationMember", "OrganizationSalesAssignment",
"Organization", "OrganizationMember", "OrganizationSalesAssignment", "OrgType", "OrgUserRole",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate",
"AssetTelemetry", "AssetReview", "ExchangeRate", "CatalogDiscovery",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"SystemParameter", "Document", "Translation", "PendingAction",
"SubscriptionTier", "OrganizationSubscription",
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
"SecurityAuditLog", "ProcessLog", "FinancialLedger", # <--- KRITIKUS!
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging",
"SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty",
"AuditLog", "VehicleOwnership", "LogSeverity",
"SecurityAuditLog", "ProcessLog", "FinancialLedger",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition",
"VehicleType", "FeatureDefinition", "ModelFeatureMap"
"VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
"Location", "LocationType"
]

View File

@@ -1,93 +1,103 @@
# /opt/docker/dev/service_finder/backend/app/models/address.py
import uuid
from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, func, Numeric, Index
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.orm import relationship, foreign
from app.db.base_class import Base
from datetime import datetime
from typing import Any, List, Optional
from sqlalchemy import String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, func, Numeric, Index, and_
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign
# MB 2.0: Kritikus javítás - a központi metadata-t használjuk az app.database-ből
from app.database import Base
class GeoPostalCode(Base):
"""Irányítószám alapú földrajzi kereső tábla."""
__tablename__ = "geo_postal_codes"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
country_code = Column(String(5), default="HU")
zip_code = Column(String(10), nullable=False)
city = Column(String(100), nullable=False)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
country_code: Mapped[str] = mapped_column(String(5), default="HU")
zip_code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
city: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
class GeoStreet(Base):
"""Utcajegyzék tábla."""
__tablename__ = "geo_streets"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id"))
name = Column(String(200), nullable=False)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
name: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
class GeoStreetType(Base):
"""Közterület jellege (utca, út, köz stb.)."""
__tablename__ = "geo_street_types"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
name = Column(String(50), unique=True, nullable=False)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
class Address(Base):
"""Univerzális cím entitás GPS adatokkal kiegészítve."""
__tablename__ = "addresses"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id"))
street_name = Column(String(200), nullable=False)
street_type = Column(String(50), nullable=False)
house_number = Column(String(50), nullable=False)
stairwell = Column(String(20))
floor = Column(String(20))
door = Column(String(20))
parcel_id = Column(String(50))
full_address_text = Column(Text)
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
street_name: Mapped[str] = mapped_column(String(200), nullable=False)
street_type: Mapped[str] = mapped_column(String(50), nullable=False)
house_number: Mapped[str] = mapped_column(String(50), nullable=False)
stairwell: Mapped[Optional[str]] = mapped_column(String(20))
floor: Mapped[Optional[str]] = mapped_column(String(20))
door: Mapped[Optional[str]] = mapped_column(String(20))
parcel_id: Mapped[Optional[str]] = mapped_column(String(50))
full_address_text: Mapped[Optional[str]] = mapped_column(Text)
# Robot és térképes funkciók számára
latitude = Column(Float)
longitude = Column(Float)
latitude: Mapped[Optional[float]] = mapped_column(Float)
longitude: Mapped[Optional[float]] = mapped_column(Float)
created_at = Column(DateTime(timezone=True), server_default=func.now())
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Branch(Base):
"""
Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik.
Minden cégnek van legalább egy 'Main' telephelye.
"""
__tablename__ = "branches"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
name = Column(String(100), nullable=False)
is_main = Column(Boolean, default=False)
name: Mapped[str] = mapped_column(String(100), nullable=False)
is_main: Mapped[bool] = mapped_column(Boolean, default=False)
# Részletes címadatok (Denormalizált a gyors kereséshez)
postal_code = Column(String(10), index=True)
city = Column(String(100), index=True)
street_name = Column(String(150))
street_type = Column(String(50))
house_number = Column(String(20))
stairwell = Column(String(20))
floor = Column(String(20))
door = Column(String(20))
hrsz = Column(String(50))
# Denormalizált adatok a gyors lekérdezéshez
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
street_name: Mapped[Optional[str]] = mapped_column(String(150))
street_type: Mapped[Optional[str]] = mapped_column(String(50))
house_number: Mapped[Optional[str]] = mapped_column(String(20))
stairwell: Mapped[Optional[str]] = mapped_column(String(20))
floor: Mapped[Optional[str]] = mapped_column(String(20))
door: Mapped[Optional[str]] = mapped_column(String(20))
hrsz: Mapped[Optional[str]] = mapped_column(String(50))
opening_hours = Column(JSONB, server_default=text("'{}'::jsonb"))
branch_rating = Column(Float, default=0.0)
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
branch_rating: Mapped[float] = mapped_column(Float, default=0.0)
status = Column(String(30), default="active")
is_deleted = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
status: Mapped[str] = mapped_column(String(30), default="active")
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
organization = relationship("Organization", back_populates="branches")
address = relationship("Address")
# Kapcsolatok
organization: Mapped["Organization"] = relationship("Organization", back_populates="branches")
address: Mapped[Optional["Address"]] = relationship("Address")
# JAVÍTOTT KAPCSOLAT: target_branch_id használata target_id helyett
reviews = relationship(
# Kapcsolatok (Primaryjoin tartva a rating rendszerhez)
reviews: Mapped[List["Rating"]] = relationship(
"Rating",
primaryjoin="and_(Branch.id==foreign(Rating.target_branch_id))"
)
@@ -101,18 +111,19 @@ class Rating(Base):
Index('idx_rating_branch', 'target_branch_id'),
{"schema": "data"}
)
# Az ID most már Integer, ahogy kérted a statisztikákhoz
id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Explicit célpontok a típusbiztonság és gyorsaság érdekében
target_organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
target_user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
target_branch_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"), nullable=True)
# MB 2.0: A felhasználók az identity sémában laknak!
author_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
score = Column(Numeric(3, 2), nullable=False) # 1.00 - 5.00
comment = Column(Text)
images = Column(JSONB, server_default=text("'[]'::jsonb"))
target_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
target_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
target_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"))
is_verified = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
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
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, BigInteger
from sqlalchemy.orm import relationship
from datetime import datetime
from typing import List, Optional
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.sql import func
from app.db.base_class import Base
from app.database import Base
class AssetCatalog(Base):
""" Jármű katalógus mesteradatok (Validált technikai sablonok). """
__tablename__ = "vehicle_catalog"
__table_args__ = (
UniqueConstraint(
'make', 'model', 'year_from', 'engine_variant', 'fuel_type',
name='uix_vehicle_catalog_full'
),
UniqueConstraint('make', 'model', 'year_from', 'fuel_type', name='uix_vehicle_catalog_full'),
{"schema": "data"}
)
id = Column(Integer, primary_key=True, index=True)
master_definition_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True)
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"))
make = Column(String, index=True, nullable=False)
model = Column(String, index=True, nullable=False)
generation = Column(String, index=True)
engine_variant = Column(String, index=True)
year_from = Column(Integer)
year_to = Column(Integer)
vehicle_class = Column(String)
fuel_type = Column(String, index=True)
master_definition = relationship("VehicleModelDefinition", back_populates="variants")
make: Mapped[str] = mapped_column(String, index=True, nullable=False)
model: Mapped[str] = mapped_column(String, index=True, nullable=False)
generation: Mapped[Optional[str]] = mapped_column(String, index=True)
year_from: Mapped[Optional[int]] = mapped_column(Integer)
year_to: Mapped[Optional[int]] = mapped_column(Integer)
fuel_type: Mapped[Optional[str]] = mapped_column(String, index=True)
power_kw: Mapped[Optional[int]] = mapped_column(Integer, index=True)
engine_capacity: Mapped[Optional[int]] = mapped_column(Integer, index=True)
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"))
factory_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
assets = relationship("Asset", back_populates="catalog")
master_definition: Mapped[Optional["VehicleModelDefinition"]] = relationship("VehicleModelDefinition", back_populates="variants")
assets: Mapped[List["Asset"]] = relationship("Asset", back_populates="catalog")
class Asset(Base):
""" A fizikai eszköz (Digital Twin) - Minden adat itt fut össze. """
__tablename__ = "assets"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
vin = Column(String(17), unique=True, index=True, nullable=False)
license_plate = Column(String(20), index=True)
name = Column(String)
year_of_manufacture = Column(Integer)
current_organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id"))
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
vin: Mapped[str] = mapped_column(String(17), unique=True, index=True, nullable=False)
license_plate: Mapped[Optional[str]] = mapped_column(String(20), index=True)
name: Mapped[Optional[str]] = mapped_column(String)
is_verified = Column(Boolean, default=False)
verification_method = Column(String(20))
verification_notes = Column(Text, nullable=True)
catalog_match_score = Column(Numeric(5, 2), nullable=True)
# Állapot és életút mérőszámok
year_of_manufacture: Mapped[Optional[int]] = mapped_column(Integer, index=True)
first_registration_date: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
current_mileage: Mapped[int] = mapped_column(Integer, default=0, index=True)
condition_score: Mapped[int] = mapped_column(Integer, default=100)
status = Column(String(20), default="active")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# --- KAPCSOLATOK (A kettőzött current_org törölve, pontosítva) ---
catalog = relationship("AssetCatalog", back_populates="assets")
# Értékesítési modul
is_for_sale: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
price: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
currency: Mapped[str] = mapped_column(String(3), default="EUR")
# 1. Jelenlegi szervezet (Üzemeltető telephely)
current_org = relationship(
"Organization",
primaryjoin="Asset.current_organization_id == Organization.id",
foreign_keys="[Asset.current_organization_id]"
)
catalog_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_catalog.id"))
current_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
financials = relationship("AssetFinancials", back_populates="asset", uselist=False)
telemetry = relationship("AssetTelemetry", back_populates="asset", uselist=False)
assignments = relationship("AssetAssignment", back_populates="asset")
events = relationship("AssetEvent", back_populates="asset")
costs = relationship("AssetCost", back_populates="asset")
reviews = relationship("AssetReview", back_populates="asset")
ownership_history = relationship("VehicleOwnership", back_populates="vehicle")
# Identity kapcsolatok
owner_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
owner_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
operator_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
operator_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
registration_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, index=True, nullable=False)
is_corporate = Column(Boolean, default=False, server_default=text("false"))
status: Mapped[str] = mapped_column(String(20), default="active")
individual_equipment: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Tulajdonos és Üzembentartó oszlopok
owner_person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
owner_org_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
operator_person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
operator_org_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
# 2. Tulajdonos szervezet (Kapcsolat pótolva)
owner_org = relationship(
"Organization",
primaryjoin="Asset.owner_org_id == Organization.id",
foreign_keys="[Asset.owner_org_id]"
)
# 3. Üzembentartó szervezet
operator_org = relationship(
"Organization",
primaryjoin="Asset.operator_org_id == Organization.id",
foreign_keys="[Asset.operator_org_id]"
)
# 4. Tulajdonos magánszemély
owner_person = relationship(
"Person",
primaryjoin="Asset.owner_person_id == Person.id",
foreign_keys="[Asset.owner_person_id]"
)
# 5. Üzembentartó magánszemély
operator_person = relationship(
"Person",
primaryjoin="Asset.operator_person_id == Person.id",
foreign_keys="[Asset.operator_person_id]"
)
# --- KAPCSOLATOK ---
catalog: Mapped["AssetCatalog"] = relationship("AssetCatalog", back_populates="assets")
financials: Mapped[Optional["AssetFinancials"]] = relationship("AssetFinancials", back_populates="asset", uselist=False)
costs: Mapped[List["AssetCost"]] = relationship("AssetCost", back_populates="asset")
events: Mapped[List["AssetEvent"]] = relationship("AssetEvent", back_populates="asset")
logbook: Mapped[List["VehicleLogbook"]] = relationship("VehicleLogbook", back_populates="asset")
inspections: Mapped[List["AssetInspection"]] = relationship("AssetInspection", back_populates="asset")
reviews: Mapped[List["AssetReview"]] = relationship("AssetReview", back_populates="asset")
telemetry: Mapped[Optional["AssetTelemetry"]] = relationship("AssetTelemetry", back_populates="asset", uselist=False)
assignments: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="asset")
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="asset")
class AssetFinancials(Base):
""" I. Beszerzés és IV. Értékcsökkenés (Amortizáció). """
__tablename__ = "asset_financials"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
acquisition_price = Column(Numeric(18, 2))
acquisition_date = Column(DateTime)
financing_type = Column(String)
residual_value_estimate = Column(Numeric(18, 2))
asset = relationship("Asset", back_populates="financials")
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
purchase_price_net: Mapped[float] = mapped_column(Numeric(18, 2))
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
financing_type: Mapped[str] = mapped_column(String(50))
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
asset: Mapped["Asset"] = relationship("Asset", back_populates="financials")
class AssetCost(Base):
""" II. Üzemeltetés és TCO kimutatás. """
__tablename__ = "asset_costs"
__table_args__ = {"schema": "data"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
cost_category: Mapped[str] = mapped_column(String(50), index=True)
amount_net: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
currency: Mapped[str] = mapped_column(String(3), default="HUF")
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
invoice_number: Mapped[Optional[str]] = mapped_column(String(100), index=True)
data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
asset: Mapped["Asset"] = relationship("Asset", back_populates="costs")
organization: Mapped["Organization"] = relationship("Organization")
class VehicleLogbook(Base):
""" Útnyilvántartás (NAV, Kiküldetés, Munkábajárás). """
__tablename__ = "vehicle_logbook"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
driver_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
trip_type: Mapped[str] = mapped_column(String(30), index=True)
is_reimbursable: Mapped[bool] = mapped_column(Boolean, default=False)
start_mileage: Mapped[int] = mapped_column(Integer)
end_mileage: Mapped[Optional[int]] = mapped_column(Integer)
asset: Mapped["Asset"] = relationship("Asset", back_populates="logbook")
driver: Mapped["User"] = relationship("User")
class AssetInspection(Base):
""" Napi ellenőrző lista és Biztonsági check. """
__tablename__ = "asset_inspections"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
inspector_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
checklist_results: Mapped[dict] = mapped_column(JSONB, nullable=False)
is_safe: Mapped[bool] = mapped_column(Boolean, default=True)
asset: Mapped["Asset"] = relationship("Asset", back_populates="inspections")
inspector: Mapped["User"] = relationship("User")
class AssetReview(Base):
""" Jármű értékelések és visszajelzések. """
__tablename__ = "asset_reviews"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
overall_rating: Mapped[Optional[int]] = mapped_column(Integer) # 1-5 csillag
comment: Mapped[Optional[str]] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
asset: Mapped["Asset"] = relationship("Asset", back_populates="reviews")
user: Mapped["User"] = relationship("User")
class VehicleOwnership(Base):
""" Tulajdonosváltások története. """
__tablename__ = "vehicle_ownership_history"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
acquired_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
disposed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
asset: Mapped["Asset"] = relationship("Asset", back_populates="ownership_history")
# EZ A SOR HIÁNYZIK A KÓDODBÓL ÉS EZ JAVÍTJA A HIBÁT:
user: Mapped["User"] = relationship("User", back_populates="ownership_history")
class AssetTelemetry(Base):
__tablename__ = "asset_telemetry"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
current_mileage = Column(Integer, default=0)
mileage_unit = Column(String(10), default="km")
vqi_score = Column(Numeric(5, 2), default=100.00)
dbs_score = Column(Numeric(5, 2), default=100.00)
asset = relationship("Asset", back_populates="telemetry")
class AssetReview(Base):
__tablename__ = "asset_reviews"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
overall_rating = Column(Integer)
criteria_scores = Column(JSONB, server_default=text("'{}'::jsonb"))
comment = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
asset = relationship("Asset", back_populates="reviews")
user = relationship("User")
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
current_mileage: Mapped[int] = mapped_column(Integer, default=0)
asset: Mapped["Asset"] = relationship("Asset", back_populates="telemetry")
class AssetAssignment(Base):
""" Eszköz-Szervezet összerendelés. """
__tablename__ = "asset_assignments"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
branch_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"), nullable=True)
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
released_at = Column(DateTime(timezone=True), nullable=True)
status = Column(String(30), default="active")
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
status: Mapped[str] = mapped_column(String(30), default="active")
asset = relationship("Asset", back_populates="assignments")
organization = relationship("Organization")
branch = relationship("Branch")
asset: Mapped["Asset"] = relationship("Asset", back_populates="assignments")
organization: Mapped["Organization"] = relationship("Organization", back_populates="assets")
class AssetEvent(Base):
""" Szerviz, baleset és egyéb jelentős események. """
__tablename__ = "asset_events"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
event_type = Column(String(50), nullable=False)
recorded_mileage = Column(Integer)
data = Column(JSONB, server_default=text("'{}'::jsonb"))
asset = relationship("Asset", back_populates="events")
registration_uuid = Column(PG_UUID(as_uuid=True), index=True, nullable=True)
class AssetCost(Base):
__tablename__ = "asset_costs"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
driver_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
cost_type = Column(String(50), nullable=False)
amount_local = Column(Numeric(18, 2), nullable=False)
currency_local = Column(String(3), nullable=False)
amount_eur = Column(Numeric(18, 2), nullable=True)
net_amount_local = Column(Numeric(18, 2))
vat_rate = Column(Numeric(5, 2))
exchange_rate_used = Column(Numeric(18, 6))
date = Column(DateTime(timezone=True), server_default=func.now())
mileage_at_cost = Column(Integer)
data = Column(JSONB, server_default=text("'{}'::jsonb"))
asset = relationship("Asset", back_populates="costs")
organization = relationship("Organization")
driver = relationship("User")
registration_uuid = Column(PG_UUID(as_uuid=True), index=True, nullable=True)
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
event_type: Mapped[str] = mapped_column(String(50), nullable=False)
asset: Mapped["Asset"] = relationship("Asset", back_populates="events")
class ExchangeRate(Base):
__tablename__ = "exchange_rates"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
base_currency = Column(String(3), default="EUR")
target_currency = Column(String(3), unique=True)
rate = Column(Numeric(18, 6), nullable=False)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
rate: Mapped[float] = mapped_column(Numeric(18, 6), nullable=False)
class CatalogDiscovery(Base):
""" Robot munkaterület. """
__tablename__ = "catalog_discovery"
id = Column(Integer, primary_key=True, index=True)
make = Column(String(100), nullable=False, index=True)
model = Column(String(100), nullable=False, index=True)
vehicle_class = Column(String(50), index=True)
source = Column(String(50))
status = Column(String(20), server_default=text("'pending'"), index=True)
attempts = Column(Integer, default=0)
last_attempt = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (
UniqueConstraint('make', 'model', 'vehicle_class', name='_make_model_class_uc'),
{"schema": "data"}
)
__table_args__ = (UniqueConstraint('make', 'model', name='_make_model_uc'), {"schema": "data"})
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
make: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
model: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)

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

View File

@@ -1,43 +1,76 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, JSON, Numeric
from sqlalchemy.orm import relationship
# /opt/docker/dev/service_finder/backend/app/models/core_logic.py
from typing import Optional, List, Any
from datetime import datetime # Python saját típusa a típusjelöléshez
from sqlalchemy import String, Integer, ForeignKey, Boolean, DateTime, Numeric, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
# JAVÍTVA: Import közvetlenül a base_class-ból
from app.db.base_class import Base
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class SubscriptionTier(Base):
"""
Előfizetési csomagok definíciója (pl. Free, Premium, VIP).
A csomagok határozzák meg a korlátokat (pl. max járműszám).
"""
__tablename__ = "subscription_tiers"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
name = Column(String, unique=True) # Free, Premium, VIP, Custom
rules = Column(JSON) # {"max_vehicles": 5, "allow_api": true}
is_custom = Column(Boolean, default=False)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String, unique=True, index=True) # pl. 'premium'
rules: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # pl. {"max_vehicles": 5}
is_custom: Mapped[bool] = mapped_column(Boolean, default=False)
class OrganizationSubscription(Base):
"""
Szervezetek aktuális előfizetései és azok érvényessége.
"""
__tablename__ = "org_subscriptions"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
org_id = Column(Integer, ForeignKey("data.organizations.id"))
tier_id = Column(Integer, ForeignKey("data.subscription_tiers.id"))
valid_from = Column(DateTime, server_default=func.now())
valid_until = Column(DateTime)
is_active = Column(Boolean, default=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Kapcsolat a szervezettel (data séma)
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
# Kapcsolat a csomaggal (data séma)
tier_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.subscription_tiers.id"), nullable=False)
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
valid_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class CreditTransaction(Base):
"""
Kreditnapló (Pontok, kreditek vagy virtuális egyenleg követése).
"""
__tablename__ = "credit_logs"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
org_id = Column(Integer, ForeignKey("data.organizations.id"))
amount = Column(Numeric(10, 2))
description = Column(String)
created_at = Column(DateTime, server_default=func.now())
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Kapcsolat a szervezettel (data séma)
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
amount: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
description: Mapped[Optional[str]] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ServiceSpecialty(Base):
"""Fa struktúra a szerviz szolgáltatásokhoz"""
"""
Hierarchikus fa struktúra a szerviz szolgáltatásokhoz (pl. Motor -> Futómű).
"""
__tablename__ = "service_specialties"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("data.service_specialties.id"), nullable=True)
name = Column(String, nullable=False)
slug = Column(String, unique=True)
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)
parent = relationship("ServiceSpecialty", remote_side=[id], backref="children")
# 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
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
# /opt/docker/dev/service_finder/backend/app/models/document.py
import uuid
# JAVÍTVA: Közvetlenül a base_class-ból importálunk, nem a base-ből!
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, text
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.base_class import Base
class Document(Base):
""" NAS alapú dokumentumtár metaadatai. """
__tablename__ = "documents"
__table_args__ = {"schema": "data"}
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
parent_type = Column(String(20), nullable=False) # 'organization' vagy 'asset'
parent_id = Column(String(50), nullable=False) # Org vagy Asset technikai ID-ja
doc_type = Column(String(50)) # pl. 'foundation_deed', 'registration'
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
parent_type: Mapped[str] = mapped_column(String(20)) # 'organization' vagy 'asset'
parent_id: Mapped[str] = mapped_column(String(50), index=True)
doc_type: Mapped[Optional[str]] = mapped_column(String(50))
original_name = Column(String(255), nullable=False)
file_hash = Column(String(64), nullable=False) # A NAS-on tárolt név (UUID)
file_ext = Column(String(10), default="webp")
mime_type = Column(String(100), default="image/webp")
file_size = Column(Integer)
original_name: Mapped[str] = mapped_column(String(255))
file_hash: Mapped[str] = mapped_column(String(64))
file_ext: Mapped[str] = mapped_column(String(10), default="webp")
mime_type: Mapped[str] = mapped_column(String(100), default="image/webp")
file_size: Mapped[Optional[int]] = mapped_column(Integer)
has_thumbnail = Column(Boolean, default=False)
thumbnail_path = Column(String(255)) # SSD-n lévő elérés
has_thumbnail: Mapped[bool] = mapped_column(Boolean, default=False)
thumbnail_path: Mapped[Optional[str]] = mapped_column(String(255))
uploaded_by = Column(Integer, ForeignKey("data.users.id"), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
uploaded_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,20 +1,19 @@
# /opt/docker/dev/service_finder/backend/app/models/gamification.py
import uuid
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.db.base_class import Base
from app.database import Base # MB 2.0: Központi Base
if TYPE_CHECKING:
from app.models.identity import User
SCHEMA_ARGS = {"schema": "data"}
class PointRule(Base):
__tablename__ = "point_rules"
__table_args__ = SCHEMA_ARGS
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
action_key: Mapped[str] = mapped_column(String, unique=True, index=True)
points: Mapped[int] = mapped_column(Integer, default=0)
@@ -23,7 +22,8 @@ class PointRule(Base):
class LevelConfig(Base):
__tablename__ = "level_configs"
__table_args__ = SCHEMA_ARGS
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
level_number: Mapped[int] = mapped_column(Integer, unique=True)
min_points: Mapped[int] = mapped_column(Integer)
@@ -31,41 +31,41 @@ class LevelConfig(Base):
class PointsLedger(Base):
__tablename__ = "points_ledger"
__table_args__ = SCHEMA_ARGS
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
points: Mapped[int] = mapped_column(Integer, default=0)
# JAVÍTÁS: Itt is server_default-ot használunk
penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
reason: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")
class UserStats(Base):
__tablename__ = "user_stats"
__table_args__ = {"schema": "data", "extend_existing": True} # Biztosítjuk a sémát
__table_args__ = {"schema": "data"}
# A ForeignKey-nek látnia kell a data sémát!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), primary_key=True)
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), primary_key=True)
total_xp: Mapped[int] = mapped_column(Integer, default=0)
social_points: Mapped[int] = mapped_column(Integer, default=0)
current_level: Mapped[int] = mapped_column(Integer, default=1)
# --- BÜNTETŐ RENDSZER ---
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now())
# VISSZAMUTATÁS A USER-RE: a back_populates értéke meg kell egyezzen a User osztály 'stats' mezőjével!
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
user: Mapped["User"] = relationship("User", back_populates="stats")
class Badge(Base):
__tablename__ = "badges"
__table_args__ = SCHEMA_ARGS
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, unique=True)
description: Mapped[str] = mapped_column(String)
@@ -73,11 +73,14 @@ class Badge(Base):
class UserBadge(Base):
__tablename__ = "user_badges"
__table_args__ = SCHEMA_ARGS
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
__table_args__ = {"schema": "data"}
user: Mapped["User"] = relationship("User")
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
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
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text, Enum
from sqlalchemy.orm import relationship
from datetime import datetime, date
from typing import Optional, Any
from sqlalchemy import String, DateTime, ForeignKey, JSON, Date, Text, Integer
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.db.base_class import Base
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class LogSeverity(str, enum.Enum):
info = "info" # Általános művelet (pl. profil megtekintés)
warning = "warning" # Gyanús, de nem biztosan káros (pl. 3 elrontott jelszó)
critical = "critical" # Súlyos művelet (pl. jelszóváltoztatás, export)
emergency = "emergency" # Azonnali beavatkozást igényel (pl. SuperAdmin módosítás)
info = "info"
warning = "warning"
critical = "critical"
emergency = "emergency"
class VehicleOwnership(Base):
__tablename__ = "vehicle_ownerships"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
vehicle_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
start_date = Column(Date, nullable=False, default=func.current_date())
end_date = Column(Date, nullable=True)
notes = Column(Text, nullable=True)
vehicle = relationship("Asset", back_populates="ownership_history")
user = relationship("User", back_populates="ownership_history")
class AuditLog(Base):
""" Rendszerszintű műveletnapló. """
__tablename__ = "audit_logs"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
severity = Column(Enum(LogSeverity), default=LogSeverity.info, nullable=False)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# Mi történt és min?
action = Column(String(100), nullable=False, index=True)
target_type = Column(String(50), index=True) # pl. "User", "Wallet", "Asset"
target_id = Column(String(50), index=True) # A cél rekord ID-ja
# MB 2.0 JAVÍTÁS: A felhasználó az identity sémában lakik!
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
# Részletes adatok (JSONB formátum a rugalmasságért)
# A 'changes' helyett explicit old/new párost használunk a könnyebb visszaállításhoz
old_data = Column(JSON, nullable=True)
new_data = Column(JSON, nullable=True)
severity: Mapped[LogSeverity] = mapped_column(
PG_ENUM(LogSeverity, name="log_severity", schema="data"),
default=LogSeverity.info
)
# Biztonsági nyomkövetés
ip_address = Column(String(45), index=True) # IPv6-ot is támogat
user_agent = Column(Text, nullable=True) # Böngésző/Eszköz információ
action: Mapped[str] = mapped_column(String(100), index=True)
target_type: Mapped[Optional[str]] = mapped_column(String(50), index=True)
target_id: Mapped[Optional[str]] = mapped_column(String(50), index=True)
timestamp = Column(DateTime(timezone=True), server_default=func.now(), index=True)
old_data: Mapped[Optional[Any]] = mapped_column(JSON)
new_data: Mapped[Optional[Any]] = mapped_column(JSON)
user = relationship("User")
ip_address: Mapped[Optional[str]] = mapped_column(String(45), index=True)
user_agent: Mapped[Optional[Text]] = mapped_column(Text)
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
user: Mapped[Optional["User"]] = relationship("User")

View File

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

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 app.db.base import Base
from app.db.base_class import Base
class LegalDocument(Base):
__tablename__ = "legal_documents"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255))
content = Column(Text, nullable=False)
version = Column(String(20), nullable=False)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
title: Mapped[Optional[str]] = mapped_column(String(255))
content: Mapped[str] = mapped_column(Text)
version: Mapped[str] = mapped_column(String(20))
region_code = Column(String(5), default="HU")
language = Column(String(5), default="hu")
region_code: Mapped[str] = mapped_column(String(5), default="HU")
language: Mapped[str] = mapped_column(String(5), default="hu")
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class LegalAcceptance(Base):
__tablename__ = "legal_acceptances"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id"))
document_id = Column(Integer, ForeignKey("data.legal_documents.id"))
accepted_at = Column(DateTime(timezone=True), server_default=func.now())
ip_address = Column(String(45))
user_agent = Column(Text)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
document_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.legal_documents.id"))
accepted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
user_agent: Mapped[Optional[str]] = mapped_column(Text)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
# DEPRECATED: Minden funkció átkerült az app.models.identity modulba.
# Ez a fájl csak a kompatibilitás miatt maradt meg, de táblát nem definiál.
from .identity import User, UserRole
# Kapcsolatok
# memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan")
# vehicles = relationship("VehicleOwnership", back_populates="user", cascade="all, delete-orphan")

View File

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

View File

@@ -1,109 +0,0 @@
from sqlalchemy import Column, Integer, String, JSON, UniqueConstraint, text, Boolean, DateTime, ForeignKey, Numeric, Index, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import JSONB # PostgreSQL specifikus JSONB a hatékony kereséshez
from app.db.base_class import Base
class VehicleType(Base):
"""Jármű főtípusok sémája (Séma-gazda)"""
__tablename__ = "vehicle_types"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
code = Column(String(30), unique=True, index=True) # car, motorcycle, truck, bus, boat, etc.
name = Column(String(50)) # Megjelenítendő név
icon = Column(String(50))
units = Column(JSON, server_default=text("'{\"power\": \"kW\", \"weight\": \"kg\", \"cargo\": \"m3\"}'::jsonb"))
features = relationship("FeatureDefinition", back_populates="vehicle_type")
definitions = relationship("VehicleModelDefinition", back_populates="v_type_rel")
class FeatureDefinition(Base):
"""Globális felszereltség szótár"""
__tablename__ = "feature_definitions"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
category = Column(String(50)) # Műszaki, Beltér, Kültér, Multimédia
name = Column(String(100), nullable=False)
data_type = Column(String(20), default="boolean")
vehicle_type = relationship("VehicleType", back_populates="features")
class ModelFeatureMap(Base):
"""Modell-szintű felszereltségi sablon (Alap vs Extra)"""
__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") # standard, optional, accessory
value = Column(String(100))
class VehicleModelDefinition(Base):
"""MDM Master rekordok - v1.3.0 Pipeline Edition (Researcher & Alchemist)"""
__tablename__ = "vehicle_model_definitions"
__table_args__ = (
UniqueConstraint('make', 'technical_code', 'vehicle_type', name='uix_make_tech_type'),
Index('idx_vmd_lookup', 'make', 'technical_code'),
{"schema": "data"}
)
id = Column(Integer, primary_key=True)
make = Column(String(50), nullable=False, index=True)
technical_code = Column(String(50), nullable=False, index=True)
marketing_name = Column(String(100), index=True)
family_name = Column(String(100))
vehicle_type = Column(String(30), index=True)
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
vehicle_class = Column(String(50))
parent_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True)
year_from = Column(Integer, nullable=True, index=True)
year_to = Column(Integer, nullable=True, index=True)
synonyms = Column(JSON, server_default=text("'[]'::jsonb"))
# --- ROBOT VÉDELMI ÉS PIPELINE MEZŐK (v1.3.0) ---
is_manual = Column(Boolean, default=False, server_default=text("false"), index=True)
attempts = Column(Integer, default=0, server_default=text("0"), index=True)
last_error = Column(Text, nullable=True)
# Robot 2.1 "Researcher" porszívózott nyers adatai (A szemetesláda)
raw_search_context = Column(Text, nullable=True)
# Telemetria és forrás adatok (melyik API/URL hozta az adatot)
research_metadata = Column(JSONB, server_default=text("'{}'::jsonb"), nullable=False)
# --------------------------------------------------
# --- TECHNIKAI FIX OSZLOPOK ---
engine_capacity = Column(Integer, index=True)
power_kw = Column(Integer, index=True)
max_weight_kg = Column(Integer, index=True)
axle_count = Column(Integer)
payload_capacity_kg = Column(Integer)
cargo_volume_m3 = Column(Numeric(10, 2))
cargo_length_mm = Column(Integer)
cargo_width_mm = Column(Integer)
cargo_height_mm = Column(Integer)
specifications = Column(JSON, server_default=text("'{}'::jsonb"))
features_json = Column(JSON, server_default=text("'{}'::jsonb"))
# Státusz mező hossza növelve a pipeline flagekhez
status = Column(String(30), server_default="unverified", index=True)
is_master = Column(Boolean, default=False)
source = Column(String(50)) # 'ROBOT-v1.3.0-Pipeline'
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")
# AssetCatalog kapcsolat
# Megjegyzés: Ellenőrizd, hogy az AssetCatalog modell be van-e importálva a Base-be!
variants = relationship("AssetCatalog", back_populates="master_definition", primaryjoin="VehicleModelDefinition.id == AssetCatalog.master_definition_id")

View File

@@ -1,19 +0,0 @@
from sqlalchemy import Column, Integer, ForeignKey, DateTime, Boolean
from sqlalchemy.sql import func
from app.db.base import Base
class VehicleOwnership(Base):
__tablename__ = "vehicle_ownerships"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
vehicle_id = Column(Integer, ForeignKey("data.vehicles.id"))
org_id = Column(Integer, ForeignKey("data.organizations.id"))
# Érvényességi időablak
start_date = Column(DateTime(timezone=True), server_default=func.now())
end_date = Column(DateTime(timezone=True), nullable=True) # Ha eladja, ide kerül a dátum
is_active = Column(Boolean, default=True)
# Csak ezen az ablakon belüli szervizeket láthatja az aktuális tulajdonos

View File

@@ -1,21 +0,0 @@
import enum
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum
from sqlalchemy.sql import func
from app.db.base import Base
class TokenType(str, enum.Enum):
email_verify = "email_verify"
password_reset = "password_reset"
class VerificationToken(Base):
__tablename__ = "verification_tokens"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
token_hash = Column(String(64), unique=True, index=True, nullable=False)
token_type = Column(Enum(TokenType, name="tokentype", schema="data"), nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)

View File

@@ -1,40 +1,123 @@
from pydantic import BaseModel, ConfigDict
from typing import Optional, Any
from datetime import datetime
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, delete
from typing import List, Any, Dict, Optional
from datetime import datetime, timedelta
# --- Pontszabályok (Point Rules) ---
class PointRuleBase(BaseModel):
rule_key: str
points: int
region_code: str = "GLOBAL"
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
is_active: bool = True
from app.api import deps
from app.models.identity import User, UserRole
from app.models.system import SystemParameter
from app.models.audit import SecurityAuditLog, OperationalLog
from app.models.security import PendingAction, ActionStatus
from app.services.security_service import security_service
from app.services.translation_service import TranslationService
from app.schemas.admin import PointRuleResponse, LevelConfigResponse, ConfigUpdate
from app.schemas.admin_security import PendingActionResponse, SecurityStatusResponse
class PointRuleCreate(PointRuleBase):
pass
router = APIRouter()
class PointRuleResponse(PointRuleBase):
id: int
model_config = ConfigDict(from_attributes=True)
# --- 🛡️ ADMIN JOGOSULTSÁG ELLENŐRZŐ ---
async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)):
""" Csak Admin vagy Superadmin léphet be a Sentinel központba. """
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Sentinel jogosultság szükséges a művelethez!"
)
return current_user
# --- Regionális Beállítások (MOT, Tax, stb.) ---
class RegionalSettingBase(BaseModel):
region_code: str
setting_key: str
value: Any # JSON adat (pl. {"months": 24})
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
# --- 🛰️ 1. SENTINEL: RENDSZERÁLLAPOT ÉS MONITORING ---
class RegionalSettingCreate(RegionalSettingBase):
pass
@router.get("/health-monitor", response_model=Dict[str, Any], tags=["Sentinel Monitoring"])
async def get_system_health(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
""" Részletes rendszerstatisztikák (Felhasználók, Eszközök, Biztonság). """
stats = {}
# Felhasználói eloszlás (Nyers SQL a sebességért)
user_res = 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_res}
# Eszköz és Szervezet számlálók
stats["total_assets"] = (await db.execute(text("SELECT count(*) FROM data.assets"))).scalar()
stats["total_organizations"] = (await db.execute(text("SELECT count(*) FROM data.organizations"))).scalar()
# --- Szintlépési Konfiguráció ---
class LevelConfigBase(BaseModel):
level_number: int
min_points: int
name_translation_key: str
region_code: str = "GLOBAL"
# Biztonsági riasztások (Kritikus logok az elmúlt 24 órában)
day_ago = datetime.now() - timedelta(days=1)
crit_logs = await db.execute(
select(func.count(SecurityAuditLog.id))
.where(SecurityAuditLog.is_critical == True, SecurityAuditLog.created_at >= day_ago)
)
stats["critical_alerts_24h"] = crit_logs.scalar() or 0
class LevelConfigUpdate(LevelConfigBase):
pass
return stats
# --- ⚖️ 2. SENTINEL: NÉGY SZEM ELV (Approval System) ---
@router.get("/pending-actions", response_model=List[PendingActionResponse], tags=["Sentinel Security"])
async def list_pending_actions(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
""" Jóváhagyásra váró kritikus műveletek listázása. """
stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
result = await db.execute(stmt)
return result.scalars().all()
@router.post("/approve/{action_id}", tags=["Sentinel Security"])
async def approve_action(
action_id: int,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
""" Művelet véglegesítése egy második admin által. """
try:
await security_service.approve_action(db, admin.id, action_id)
return {"status": "success", "message": "Művelet végrehajtva."}
except Exception as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# --- ⚙️ 3. DINAMIKUS KONFIGURÁCIÓ (System Parameters) ---
@router.get("/parameters", tags=["Dynamic Configuration"])
async def list_all_parameters(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
""" Globális és lokális paraméterek (Limitek, XP szorzók) lekérése. """
result = await db.execute(select(SystemParameter))
return result.scalars().all()
@router.post("/parameters", tags=["Dynamic Configuration"])
async def set_parameter(
config: ConfigUpdate,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
""" Paraméter beállítása vagy frissítése hierarchikus scope-al. """
query = text("""
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
VALUES (:key, :val, :sl, :sid, :cat, :user)
ON CONFLICT (key, scope_level, scope_id)
DO UPDATE SET
value = EXCLUDED.value,
category = EXCLUDED.category,
last_modified_by = EXCLUDED.last_modified_by,
updated_at = now()
""")
await db.execute(query, {
"key": config.key, "val": config.value, "sl": config.scope_level,
"sid": config.scope_id, "cat": config.category, "user": admin.email
})
await db.commit()
return {"status": "success", "message": f"'{config.key}' frissítve."}
@router.post("/translations/sync", tags=["System Utilities"])
async def sync_translations(db: AsyncSession = Depends(deps.get_db), admin: User = Depends(check_admin_access)):
""" DB fordítások exportálása JSON fájlokba a frontendnek. """
await TranslationService.export_to_json(db)
return {"message": "Nyelvi fájlok frissítve."}

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel
# /opt/docker/dev/service_finder/backend/app/schemas/admin_security.py
from pydantic import BaseModel, ConfigDict
from datetime import datetime
from typing import Optional, Any, Dict, List
from typing import Optional, Any, Dict
from app.models.security import ActionStatus
class PendingActionResponse(BaseModel):
@@ -13,14 +14,10 @@ class PendingActionResponse(BaseModel):
created_at: datetime
expires_at: datetime
class Config:
from_attributes = True
class ActionApproveRequest(BaseModel):
# Itt akár extra jelszót vagy MFA tokent is kérhetnénk a jövőben
comment: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class SecurityStatusResponse(BaseModel):
total_pending: int
critical_logs_last_24h: int
emergency_locks_active: int
emergency_locks_active: int

View File

@@ -1,73 +1,56 @@
# /opt/docker/dev/service_finder/backend/app/schemas/asset.py
from pydantic import BaseModel, ConfigDict, Field
from typing import Optional, Dict, Any, List
from uuid import UUID
from datetime import datetime
# --- KATALÓGUS SÉMÁK (Gyári adatok) ---
class AssetCatalogBase(BaseModel):
"""Alap katalógus adatok, amik a technikai dúsításból származnak."""
class AssetCatalogResponse(BaseModel):
""" A technikai katalógus (Master Data) teljes adattartalma. """
id: int
make: str
model: str
generation: Optional[str] = None
engine_variant: Optional[str] = None
year_from: Optional[int] = None
year_to: Optional[int] = None
vehicle_class: Optional[str] = None
fuel_type: Optional[str] = None
engine_code: Optional[str] = None
# --- ÚJ TECHNIKAI MEZŐK (Robot v1.0.8 Smart Hunter adatai) ---
# Technikai paraméterek az automatizáláshoz
power_kw: Optional[int] = None
engine_capacity: Optional[int] = None
max_weight_kg: Optional[int] = None
axle_count: Optional[int] = None
euro_class: Optional[str] = None
body_type: Optional[str] = None
class AssetCatalogResponse(AssetCatalogBase):
"""Katalógus válasz séma azonosítóval és extra gyári adatokkal."""
id: int
factory_data: Optional[Dict[str, Any]] = None
engine_code: Optional[str] = None
factory_data: Dict[str, Any] = Field(default_factory=dict)
# Pydantic v2 konfiguráció az ORM (SQLAlchemy) támogatáshoz
model_config = ConfigDict(from_attributes=True)
# --- JÁRMŰ SÉMÁK (Asset) ---
class AssetBase(BaseModel):
"""Jármű példány alapadatai (egyedi azonosítók)."""
class AssetResponse(BaseModel):
""" A konkrét járműpéldány (Asset) teljes válaszmodellje. """
id: UUID
vin: str = Field(..., min_length=17, max_length=17)
license_plate: str
license_plate: Optional[str] = None
name: Optional[str] = None
year_of_manufacture: Optional[int] = None
class AssetCreate(AssetBase):
"""Séma új jármű felvételéhez."""
make: str
model: str
vehicle_class: Optional[str] = "car"
fuel_type: Optional[str] = None
current_reading: Optional[int] = 0
class AssetResponse(AssetBase):
"""
Teljes jármű válasz séma.
Ez a séma tartalmazza a 'catalog' objektumot, amiben a dúsított műszaki adatok vannak.
"""
id: UUID
catalog_id: int
catalog: AssetCatalogResponse # Ez a pont kapcsolja össze a dúsított technikai adatokat
# Státusz és ellenőrzés
status: str
is_verified: bool
verification_method: Optional[str] = None
catalog_match_score: Optional[float] = None
# Kapcsolt adatok
catalog_id: Optional[int] = None
catalog: Optional[AssetCatalogResponse] = None # Itt jön a dúsítás!
owner_organization_id: Optional[int] = None
operator_person_id: Optional[int] = None
created_at: datetime
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
# --- DIGITÁLIS IKER (Full Profile) ---
class AssetFullProfile(BaseModel):
"""
Komplex jelentésekhez használt séma.
Összefogja az identitást, telemetriát, pénzügyeket és szerviztörténetet.
"""
identity: Dict[str, Any]
telemetry: Dict[str, Any]
financial_summary: Dict[str, Any]
service_history: List[Dict[str, Any]]
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,35 +1,35 @@
from pydantic import BaseModel, Field
# /opt/docker/dev/service_finder/backend/app/schemas/asset_cost.py
from pydantic import BaseModel, ConfigDict, Field
from typing import Optional, Dict, Any
from datetime import datetime
from decimal import Decimal
from uuid import UUID
class AssetCostBase(BaseModel):
"""Alap költség adatok (Frontendről érkező bevitel)."""
cost_type: str = Field(..., description="fuel, service, fine, insurance, toll, etc.")
amount_local: Decimal = Field(..., description="A fizetett bruttó összeg helyi devizában")
currency_local: str = Field("HUF", min_length=3, max_length=3)
date: datetime = Field(default_factory=datetime.now)
mileage_at_cost: Optional[int] = Field(None, description="Kilométeróra állása a költség rögzítésekor")
description: Optional[str] = None
cost_type: str # fuel, service, tax, insurance
amount_local: Decimal
currency_local: str = "HUF"
net_amount_local: Optional[Decimal] = None
vat_rate: Optional[Decimal] = Field(27.0, description="ÁFA kulcs (pl. 27.0)")
data: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Extra adatok (pl. helyszín, számlaszám)")
vat_rate: Optional[Decimal] = Field(default=27.0)
date: datetime = Field(default_factory=datetime.now)
mileage_at_cost: Optional[int] = None
description: Optional[str] = None
data: Dict[str, Any] = Field(default_factory=dict) # nyugta adatai, GPS koordináták
class AssetCostCreate(AssetCostBase):
"""Költség rögzítésekor használt séma."""
asset_id: UUID
organization_id: int
class AssetCostResponse(AssetCostBase):
"""Visszatérő adat modell a frontend felé."""
id: UUID
asset_id: UUID
organization_id: int
driver_id: Optional[int]
amount_eur: Decimal
exchange_rate_used: Decimal
created_at: Optional[datetime] = None
class Config:
from_attributes = True
driver_id: Optional[int] = None
# Pénzügyi dúsítás (Backend számolja)
amount_eur: Optional[Decimal] = None
exchange_rate_used: Optional[Decimal] = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,72 +1,54 @@
from pydantic import BaseModel, EmailStr, Field
from typing import Optional, Dict, Any
from datetime import date
# --- STEP 1: LITE REGISTRATION ---
class UserLiteRegister(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
first_name: str
last_name: str
region_code: str = "HU"
lang: str = Field("hu", description="Választott nyelv kódja")
timezone: str = Field("Europe/Budapest", description="Felhasználó időzónája")
class UserLogin(BaseModel):
email: EmailStr
password: str
# --- STEP 2: KYC & ONBOARDING ---
class ICEContact(BaseModel):
name: str
phone: str
relationship: Optional[str] = None
# /opt/docker/dev/service_finder/backend/app/schemas/auth.py
from pydantic import BaseModel, EmailStr, Field, ConfigDict
from typing import Optional, Dict, List
from datetime import date, datetime
class DocumentDetail(BaseModel):
number: str
expiry_date: date
class ICEContact(BaseModel):
name: str
phone: str
relationship: str
class UserLiteRegister(BaseModel):
""" Step 1: Gyors regisztráció (Alap azonosítás). """
email: EmailStr
password: str = Field(..., min_length=8, description="Minimum 8 karakter hosszú jelszó")
first_name: str
last_name: str
model_config = ConfigDict(from_attributes=True)
class UserKYCComplete(BaseModel):
phone_number: str
""" Step 2: Teljes körű személyazonosítás és címadatok. """
phone_number: str = Field(..., pattern=r"^\+?[0-9]{7,15}$")
birth_place: str
birth_date: date
mothers_last_name: str
mothers_first_name: str
# Bontott címmezők (B pont szerint)
# Atomizált címadatok a pontos GPS-hez és Robot-munkához
address_zip: str
address_city: str
address_street_name: str
address_street_type: str
address_street_type: str # utca, út, tér...
address_house_number: str
address_stairwell: Optional[str] = None # Lépcsőház
address_floor: Optional[str] = None # Emelet
address_door: Optional[str] = None # Ajtó
address_hrsz: Optional[str] = None # Helyrajzi szám
address_stairwell: Optional[str] = None
address_floor: Optional[str] = None
address_door: Optional[str] = None
address_hrsz: Optional[str] = None # Külterület/Helyrajzi szám
identity_docs: Dict[str, DocumentDetail]
# Okmányok és Vészhelyzet
identity_docs: Dict[str, DocumentDetail] # pl: {"ID_CARD": {...}, "LICENSE": {...}}
ice_contact: ICEContact
preferred_currency: Optional[str] = Field("HUF", max_length=3)
# --- COMMON & SECURITY ---
class PasswordResetRequest(BaseModel):
email: EmailStr
class PasswordResetConfirm(BaseModel):
email: EmailStr
token: str
password: str = Field(..., min_length=8)
password_confirm: str = Field(..., min_length=8)
preferred_language: str = "hu"
preferred_currency: str = "HUF"
class Token(BaseModel):
access_token: str
token_type: str
is_active: bool
class TokenPayload(BaseModel):
"""JWT Token payload struktúrája validációhoz."""
sub: Optional[str] = None
role: Optional[str] = None
rank: Optional[int] = 0
scope_level: Optional[str] = None
scope_id: Optional[str] = None
region: Optional[str] = None
refresh_token: Optional[str] = None
token_type: str = "bearer"
is_active: bool

View File

@@ -1,56 +1,20 @@
# /opt/docker/dev/service_finder/backend/app/schemas/fleet.py
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from datetime import date, datetime
from app.models.expense import ExpenseCategory
from datetime import date
from uuid import UUID
# --- Vehicle Schemas ---
class VehicleBase(BaseModel):
license_plate: str
make: str
model: str
year: int
fuel_type: Optional[str] = None
vin: Optional[str] = None
initial_odometer: int = 0
mot_expiry_date: Optional[date] = None
insurance_expiry_date: Optional[date] = None
class VehicleCreate(VehicleBase):
pass
class VehicleResponse(VehicleBase):
id: int
current_odometer: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)
# --- Event / Expense Schemas ---
class EventBase(BaseModel):
event_type: ExpenseCategory
class EventCreate(BaseModel):
asset_id: UUID
event_type: str # 'SERVICE', 'FUEL', 'MOT'
date: date
odometer_value: int
cost_amount: int
cost_amount: float
description: Optional[str] = None
is_diy: bool = False
# Ad-Hoc Provider mező: Ha stringet kapunk, a service megkeresi vagy létrehozza
provider_name: Optional[str] = None
provider_id: Optional[int] = None # Ha már ismert ID-t küldünk
class EventCreate(EventBase):
pass
class EventResponse(EventBase):
id: int
vehicle_id: int
odometer_anomaly: bool
service_provider_id: Optional[int]
image_paths: Optional[List[str]] = []
model_config = ConfigDict(from_attributes=True)
provider_id: Optional[int] = None
class TCOStats(BaseModel):
vehicle_id: int
total_cost: int
breakdown: dict[str, int] # Kategóriánkénti bontás
cost_per_km: Optional[float] = 0.0
asset_id: UUID
total_cost_huf: float
cost_per_km: float
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,4 +1,4 @@
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List
class ContactCreate(BaseModel):
@@ -8,30 +8,31 @@ class ContactCreate(BaseModel):
contact_type: str = "primary"
class CorpOnboardIn(BaseModel):
# Névkezelés
full_name: str = Field(..., description="Teljes hivatalos név")
name: str = Field(..., description="Rövidített cégnév (pl. ProfiBot Kft.)")
display_name: str = Field(..., description="Alkalmazáson belüli rövidítés (pl. ProfiBot)")
""" Teljes onboarding adatcsomag atomizált címekkel. """
full_name: str = Field(..., description="Hivatalos cégnév")
name: str = Field(..., description="Rövid név")
display_name: str
tax_number: str
country_code: str = "HU"
language: str = Field("hu", description="A szervezet alapértelmezett nyelve")
default_currency: str = Field("HUF", description="A szervezet alapértelmezett pénzneme")
reg_number: Optional[str] = None
country_code: str = "HU"
language: str = "hu"
default_currency: str = "HUF"
# Atomizált Címkezelés
# --- ATOMIZÁLT CÍM (Modell szinkron) ---
address_zip: str
address_city: str
address_street_name: Optional[str] = None
address_street_type: Optional[str] = None # utca, út, tér, dűlő
address_house_number: Optional[str] = None
address_hrsz: Optional[str] = None # Helyrajzi szám (ha nincs utca/házszám)
address_street_name: str
address_street_type: str
address_house_number: str
address_stairwell: Optional[str] = None
address_floor: Optional[str] = None
address_door: Optional[str] = None
address_hrsz: Optional[str] = None
contacts: List[ContactCreate] = []
class CorpOnboardResponse(BaseModel):
organization_id: int
status: str
status: str
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,45 +1,38 @@
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List
class ServiceCreateInternal(BaseModel):
name: str = Field(..., description="A szolgáltató neve")
# --- HIERARCHIA ---
# Ha a robot felismeri, hogy egy lánc része, itt tároljuk a szülő ID-t
parent_id: Optional[int] = Field(None, description="Szülő egység ID-ja (pl. Franchise központ)")
# --- CÍM ADATOK ---
postal_code: Optional[str] = None
city: str
street_name: Optional[str] = None
street_type: Optional[str] = "utca"
house_number: Optional[str] = None
stairwell: Optional[str] = None
floor: Optional[str] = None
door: Optional[str] = None
hrsz: Optional[str] = None
full_address: Optional[str] = Field(None, description="Eredeti, nyers cím szövege")
# --- ELÉRHETŐSÉG ---
contact_phone: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
# --- SOCIAL & AI ---
# A Deep Dive fázishoz előkészítve
social_links: Optional[Dict[str, str]] = Field(default_factory=dict)
vibe_analysis: Optional[Dict[str, Any]] = Field(default_factory=dict)
# --- IDENTITÁS ÉS FORRÁS ---
source: str # 'google', 'osm', 'manual', 'fb_scraper'
external_id: Optional[str] = None
# Ez a robot "horgonya" a duplikációk ellen
fingerprint: str = Field(..., description="Egyedi ujjlenyomat: Hash(Name+City+Street)")
trust_score: int = Field(30, ge=0, le=100)
raw_data: Optional[Dict[str, Any]] = {}
class ContactCreate(BaseModel):
full_name: str
email: str
phone: Optional[str] = None
contact_type: str = "primary"
class Config:
from_attributes = True
class CorpOnboardIn(BaseModel):
""" Teljes onboarding adatcsomag atomizált címekkel. """
full_name: str = Field(..., description="Hivatalos cégnév")
name: str = Field(..., description="Rövid név")
display_name: str
tax_number: str
reg_number: Optional[str] = None
country_code: str = "HU"
language: str = "hu"
default_currency: str = "HUF"
# --- ATOMIZÁLT CÍM (Modell szinkron) ---
address_zip: str
address_city: str
address_street_name: str
address_street_type: str
address_house_number: str
address_stairwell: Optional[str] = None
address_floor: Optional[str] = None
address_door: Optional[str] = None
address_hrsz: Optional[str] = None
contacts: List[ContactCreate] = []
class CorpOnboardResponse(BaseModel):
organization_id: int
status: str
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,12 +1,9 @@
from pydantic import BaseModel, Field
from typing import Optional, Dict
# /opt/docker/dev/service_finder/backend/app/schemas/service_hunt.py
class ServiceHuntRequest(BaseModel):
name: str = Field(..., example="Kovács Autóvillamosság")
name: str
category_id: int
address: str
latitude: float # A szerviz koordinátája
latitude: float
longitude: float
user_latitude: float # A felhasználó aktuális helyzete (GPS-ből)
user_longitude: float
name_translations: Optional[Dict[str, str]] = None
user_latitude: float
user_longitude: float

View File

@@ -1,9 +1,10 @@
# /opt/docker/dev/service_finder/backend/app/schemas/social.py
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from datetime import datetime
from app.models.social import ModerationStatus, SourceType
# --- Alap Sémák ---
# --- Alap Sémák (Szolgáltatók) ---
class ServiceProviderBase(BaseModel):
name: str
@@ -19,42 +20,39 @@ class ServiceProviderCreate(BaseModel):
class ServiceProviderResponse(ServiceProviderBase):
id: int
status: ModerationStatus
validation_score: int # Látni kell a pontszámot
validation_score: int
evidence_image_path: Optional[str] = None
added_by_user_id: Optional[int] = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
# --- Voting & Gamification Sémák ---
# --- Gamifikáció és Szavazás (Voting & Gamification) ---
class VoteCreate(BaseModel):
vote_value: int # Csak a +1 vagy -1 kell, a user_id jön a tokenből, a provider_id az URL-ből
vote_value: int
class LeaderboardEntry(BaseModel):
username: str
points: int
rank: int
model_config = ConfigDict(from_attributes=True)
# --- GAMIFIKÁCIÓS SÉMÁK (Amiket a log keresett) ---
class BadgeSchema(BaseModel):
id: int
name: str
description: str
image_url: Optional[str] = None
class Config:
from_attributes = True
icon_url: Optional[str] = None # JAVÍTVA: icon_url a modell szerint
model_config = ConfigDict(from_attributes=True) # Pydantic V2 kompatibilis
class UserStatSchema(BaseModel):
user_id: int
total_points: int
total_xp: int # JAVÍTVA: total_xp a modell szerint
current_level: int
rank_title: str
penalty_points: int # JAVÍTVA: új mező
rank_title: Optional[str] = None
badges: List[BadgeSchema] = []
class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,52 +1,25 @@
# /opt/docker/dev/service_finder/backend/app/schemas/user.py
from pydantic import BaseModel, EmailStr, field_validator, ConfigDict
from typing import Optional
from datetime import date
# Alap adatok, amik mindenhol kellenek
class UserBase(BaseModel):
email: EmailStr
first_name: Optional[str] = None
last_name: Optional[str] = None
is_active: Optional[bool] = True
is_superuser: bool = False
is_active: bool = True
region_code: str = "HU"
# --- REGISZTRÁCIÓ ---
class UserRegister(UserBase):
password: str
birthday: Optional[date] = None
is_company: bool = False
company_name: Optional[str] = None
tax_number: Optional[str] = None
@field_validator('email')
@classmethod
def block_temporary_emails(cls, v: str) -> str:
blacklist = ['mailinator.com', '10minutemail.com', 'temp-mail.org', 'guerrillamail.com']
domain = v.split('@')[-1].lower()
if domain in blacklist:
raise ValueError('Ideiglenes email szolgáltató nem engedélyezett!')
return v
@field_validator('tax_number')
@classmethod
def validate_tax_id(cls, v: Optional[str], info) -> Optional[str]:
if info.data.get('is_company') and (not v or len(v) < 8):
raise ValueError('Cég esetén az adószám első 8 karaktere kötelező!')
return v
# --- VÁLASZ (Ezt hiányolta a rendszer!) ---
class UserResponse(UserBase):
id: int
is_company: bool
company_name: Optional[str] = None
# Pydantic V2 konfiguráció az ORM (SQLAlchemy) támogatáshoz
person_id: Optional[int] = None
role: str
subscription_plan: str
scope_level: str
scope_id: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
# Frissítéshez használt séma
class UserUpdate(BaseModel):
password: Optional[str] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
email: Optional[EmailStr] = None
preferred_language: Optional[str] = None

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