átlagos kiegészítséek jó sok

This commit is contained in:
Roo
2026-03-22 11:02:05 +00:00
parent f53e0b53df
commit 5d44339f21
249 changed files with 20922 additions and 2253 deletions

186
backend/app/admin_ui.py Normal file
View File

@@ -0,0 +1,186 @@
#!/usr/bin/env python3
import asyncio
import json
import urllib.parse
import streamlit as st
from sqlalchemy import text
from app.database import AsyncSessionLocal
# Streamlit oldal alapbeállításai
st.set_page_config(
page_title="Service Finder - HITL Adattisztító",
page_icon="🔧",
layout="wide"
)
# --- ADATBÁZIS MŰVELETEK (Hardened Stateless Logic) ---
async def get_review_vehicle():
"""Lekérdez egy javításra váró járművet izolált sessionben."""
async with AsyncSessionLocal() as session:
try:
query = text("""
SELECT id, make, marketing_name, year_from, fuel_type,
raw_api_data, raw_search_context,
trim_level, body_type, power_kw, engine_capacity,
specifications, last_error
FROM vehicle.vehicle_model_definitions
WHERE status = 'manual_review_needed'
ORDER BY priority_score DESC
LIMIT 1
""")
result = await session.execute(query)
row = result.fetchone()
if not row:
return None
vehicle = dict(row._mapping)
# URL bányászat a JSON adatokból
source_url = None
if vehicle.get('raw_api_data'):
api_data = vehicle['raw_api_data']
if isinstance(api_data, str):
try: api_data = json.loads(api_data)
except: api_data = {}
source_url = api_data.get('url') or api_data.get('source_url') or api_data.get('link')
vehicle['extracted_url'] = source_url
return vehicle
except Exception as e:
st.error(f"❌ Lekérdezési hiba: {e}")
return None
finally:
# Garantáljuk a session lezárását
await session.close()
async def update_vehicle_data(vehicle_id, updates, new_status):
"""Elmenti az adatokat és azonnal felszabadítja a hálózati erőforrásokat."""
session = AsyncSessionLocal()
try:
# Dinamikus SQL összeállítása
set_items = [f"{k} = :{k}" for k in updates.keys()]
set_clause = ", ".join(set_items)
sql = text(f"""
UPDATE vehicle.vehicle_model_definitions
SET status = :status, {set_clause}, updated_at = NOW()
WHERE id = :id
""")
params = {"status": new_status, "id": vehicle_id, **updates}
await session.execute(sql, params)
await session.commit()
return True
except Exception as e:
await session.rollback()
st.error(f"❌ Mentési hiba az adatbázisban: {e}")
return False
finally:
# KRITIKUS JAVÍTÁS: Explicit lezárás, hogy ne maradjon nyitott transport
await session.close()
# Itt kényszerítjük a kapcsolat-kezelőt a háttérben futó motor elengedésére
bind = session.bind
if bind:
await bind.dispose()
# --- UI LOGIKA ---
async def main_async():
st.title("🔧 HITL Adattisztító - Autó Adat Javítás")
# Adat betöltése a memóriába (ha üres)
if "current_vehicle" not in st.session_state or st.session_state.current_vehicle is None:
with st.spinner("Adatbázis szinkronizálása..."):
st.session_state.current_vehicle = await get_review_vehicle()
v = st.session_state.current_vehicle
if not v:
st.success("🎉 Minden jármű ellenőrizve!")
if st.button("🔄 Új lekérdezés"):
st.session_state.current_vehicle = None
st.rerun()
return
# Felület felépítése
st.header(f"🚗 {v['year_from'] or '????'} {v['make']} {v['marketing_name']}")
st.caption(f"DB ID: {v['id']} | Üzemanyag: {v['fuel_type'] or 'n/a'}")
# 3 oszlopos nézet
col_raw, col_source, col_edit = st.columns([1, 1, 1.2])
with col_raw:
st.subheader("📄 Robot Naplók")
if v['raw_api_data']:
with st.expander("Nyers JSON (API)", expanded=True):
st.json(v['raw_api_data'])
with st.expander("Keresési Környezet", expanded=False):
st.text_area("Talált szövegek", v['raw_search_context'] or "Nincs adat", height=400)
with col_source:
st.subheader("🔗 Eredeti Források")
if v['extracted_url']:
st.success("📍 Közvetlen adatlap linkje:")
st.markdown(f"### [FORRÁS MEGNYITÁSA ↗️]({v['extracted_url']})")
else:
st.warning("⚠️ Nincs közvetlen link.")
st.markdown("---")
st.write("**Segédeszközök:**")
search_q = urllib.parse.quote(f"{v['make']} {v['marketing_name']} {v['year_from'] or ''} specifications")
st.markdown(f"- [🔍 Google Keresés](https://www.google.com/search?q={search_q})")
us_query = urllib.parse.quote(f"{v['make']} {v['marketing_name']}")
us_url = f"https://www.google.com/search?q=site:ultimatespecs.com+{us_query}"
if v['specifications']:
with st.expander("Már meglévő specifikációk", expanded=True):
st.json(v['specifications'])
with col_edit:
st.subheader("✏️ Adatbevitel")
with st.form("hitl_form_v2", clear_on_submit=False):
trim = st.text_input("Trim / Felszereltség", value=v['trim_level'] or "")
body_opts = ["", "SEDAN", "HATCHBACK", "SUV", "ESTATE", "COUPE", "CONVERTIBLE", "VAN", "PICKUP", "MPV"]
curr_body = v['body_type'] if v['body_type'] in body_opts else ""
body = st.selectbox("Karosszéria", body_opts, index=body_opts.index(curr_body))
pwr = st.number_input("Teljesítmény (kW)", value=int(v['power_kw'] or 0))
cap = st.number_input("Hengerűrtartalom (cm³)", value=int(v['engine_capacity'] or 0))
st.markdown("---")
comment = st.text_area("Megjegyzés (második zsák adatai)", placeholder="További kiegészítő adatok...")
st.write("")
b1, b2, b3 = st.columns(3)
save_btn = b1.form_submit_button("💾 MENTÉS", type="primary")
skip_btn = b2.form_submit_button("⏭️ KIHAGYÁS")
reject_btn = b3.form_submit_button("🗑️ KUKA")
# Mentési logika
if save_btn:
updates = {
"trim_level": trim,
"body_type": body,
"power_kw": pwr,
"engine_capacity": cap,
"last_error": f"Manual fix OK. {comment}".strip()
}
with st.spinner("Véglegesítés..."):
if await update_vehicle_data(v['id'], updates, "published"):
st.session_state.current_vehicle = None
st.rerun()
if skip_btn:
st.session_state.current_vehicle = None
st.rerun()
if reject_btn:
if await update_vehicle_data(v['id'], {"last_error": "Manual rejection"}, "rejected"):
st.session_state.current_vehicle = None
st.rerun()
if __name__ == "__main__":
asyncio.run(main_async())

View File

@@ -138,4 +138,25 @@ def check_min_rank(role_key: str):
detail=f"Alacsony jogosultsági szint. (Elvárt: {required_rank})"
)
return True
return rank_checker
return rank_checker
async def get_current_admin(
current_user: User = Depends(get_current_user)
) -> User:
"""
Csak admin/moderátor/superadmin szerepkörrel rendelkező felhasználók számára.
"""
# A UserRole Enum értékeit használjuk
allowed_roles = {
UserRole.superadmin,
UserRole.admin,
UserRole.region_admin,
UserRole.country_admin,
UserRole.moderator,
}
if current_user.role not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Nincs megfelelő jogosultságod (Admin/Moderátor)!"
)
return current_user

View File

@@ -3,7 +3,8 @@ from fastapi import APIRouter
from app.api.v1.endpoints import (
auth, catalog, assets, organizations, documents,
services, admin, expenses, evidence, social, security,
billing, finance_admin, analytics, vehicles
billing, finance_admin, analytics, vehicles, system_parameters,
gamification
)
api_router = APIRouter()
@@ -22,4 +23,6 @@ api_router.include_router(social.router, prefix="/social", tags=["Social & Leade
api_router.include_router(security.router, prefix="/security", tags=["Dual Control (Security)"])
api_router.include_router(finance_admin.router, prefix="/finance/issuers", tags=["finance-admin"])
api_router.include_router(analytics.router, prefix="/analytics", tags=["Analytics"])
api_router.include_router(vehicles.router, prefix="/vehicles", tags=["Vehicles"])
api_router.include_router(vehicles.router, prefix="/vehicles", tags=["Vehicles"])
api_router.include_router(system_parameters.router, prefix="/system/parameters", tags=["System Parameters"])
api_router.include_router(gamification.router, prefix="/gamification", tags=["Gamification"])

View File

@@ -1,5 +1,5 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Body
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, delete
from typing import List, Any, Dict, Optional
@@ -10,9 +10,9 @@ from app.models.identity import User, UserRole # JAVÍTVA: Központi import
from app.models.system import SystemParameter, ParameterScope
from app.services.system_service import system_service
# JAVÍTVA: Security audit modellek
from app.models.audit import SecurityAuditLog, OperationalLog
from app.models 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 import PendingAction, ActionStatus
from app.services.security_service import security_service
from app.services.translation_service import TranslationService
@@ -235,4 +235,127 @@ async def set_odometer_manual_override(
"message": f"Manuális átlag {action}: {request.daily_avg} km/nap",
"vehicle_id": vehicle_id,
"manual_override_avg": odometer_state.manual_override_avg
}
@router.get("/ping", tags=["Admin Test"])
async def admin_ping(
current_user: User = Depends(deps.get_current_admin)
):
"""
Egyszerű ping végpont admin jogosultság ellenőrzéséhez.
"""
return {
"message": "Admin felület aktív",
"role": current_user.role.value if hasattr(current_user.role, "value") else current_user.role
}
@router.post("/users/{user_id}/ban", tags=["Admin Security"])
async def ban_user(
user_id: int,
reason: str = Body(..., embed=True),
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
Felhasználó tiltása (Ban Hammer).
- Megkeresi a usert (identity.users táblában).
- Ha nincs -> 404
- Ha a user.role == superadmin -> 403 (Saját magát/másik admint ne tiltson le).
- Állítja be a tiltást (is_active = False).
- Audit logba rögzíti a reason-t.
"""
from sqlalchemy import select
# 1. Keresd meg a usert
stmt = select(User).where(User.id == user_id)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User not found with ID: {user_id}"
)
# 2. Ellenőrizd, hogy nem superadmin-e
if user.role == UserRole.superadmin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot ban a superadmin user"
)
# 3. Tiltás beállítása
user.is_active = False
# Opcionálisan: banned_until mező kitöltése, ha létezik a modellben
# user.banned_until = datetime.now() + timedelta(days=30)
# 4. Audit log létrehozása
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="ban_user",
target_user_id=user_id,
details=f"User banned. Reason: {reason}",
is_critical=True,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"User {user_id} banned successfully.",
"reason": reason
}
@router.post("/marketplace/services/{staging_id}/approve", tags=["Marketplace Moderation"])
async def approve_staged_service(
staging_id: int,
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
Szerviz jóváhagyása a Piactéren (Kék Pipa).
- Megkeresi a marketplace.service_staging rekordot.
- Ha nincs -> 404
- Állítja a validation_level-t 100-ra, a status-t 'approved'-ra.
"""
from sqlalchemy import select
from app.models.staged_data import ServiceStaging
stmt = select(ServiceStaging).where(ServiceStaging.id == staging_id)
result = await db.execute(stmt)
staging = result.scalar_one_or_none()
if not staging:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Service staging record not found with ID: {staging_id}"
)
# Jóváhagyás
staging.validation_level = 100
staging.status = "approved"
# Audit log
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="approve_service",
target_staging_id=staging_id,
details=f"Service staging approved: {staging.service_name}",
is_critical=False,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"Service staging {staging_id} approved.",
"service_name": staging.service_name
}

View File

@@ -12,7 +12,7 @@ from app.api import deps
from app.schemas.analytics import TCOSummaryResponse, TCOErrorResponse
from app.services.analytics_service import TCOAnalytics
from app.models import Vehicle
from app.models.organization import OrganizationMember
from app.models.marketplace.organization import OrganizationMember
logger = logging.getLogger(__name__)

View File

@@ -1,5 +1,6 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py
import uuid
import logging
from typing import Any, Dict, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
@@ -8,11 +9,12 @@ 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
from app.models import Asset, AssetCost
from app.models.identity import User
from app.services.cost_service import cost_service
from app.services.asset_service import AssetService
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
from app.schemas.asset import AssetResponse
from app.schemas.asset import AssetResponse, AssetCreate
router = APIRouter()
@@ -51,4 +53,39 @@ async def list_asset_costs(
.limit(limit)
)
res = await db.execute(stmt)
return res.scalars().all()
return res.scalars().all()
@router.post("/vehicles", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
async def create_or_claim_vehicle(
payload: AssetCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Új jármű hozzáadása vagy meglévő jármű igénylése a flottához.
A végpont a következőket végzi:
- Ellenőrzi a felhasználó járműlimitjét
- Ha a VIN már létezik, tulajdonjog-átvitelt kezdeményez
- Ha új, létrehozza a járművet és a kapcsolódó digitális ikreket
- XP jutalom adása a felhasználónak
"""
try:
asset = await AssetService.create_or_claim_vehicle(
db=db,
user_id=current_user.id,
org_id=payload.organization_id,
vin=payload.vin,
license_plate=payload.license_plate,
catalog_id=payload.catalog_id
)
return asset
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Vehicle creation error: {e}")
raise HTTPException(status_code=500, detail="Belső szerverhiba a jármű létrehozásakor")

View File

@@ -1,4 +1,4 @@
# backend/app/api/v1/endpoints/auth.py
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/auth.py
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
@@ -10,9 +10,23 @@ from app.core.config import settings
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
from app.api.deps import get_current_user
from app.models.identity import User # JAVÍTVA: Új központi modell
from pydantic import BaseModel, Field
router = APIRouter()
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
"""
Regisztráció (Lite fázis) - új felhasználó létrehozása.
"""
user = await AuthService.register_lite(db, user_in)
return {
"status": "success",
"message": "Regisztráció sikeres. Aktivációs e-mail elküldve.",
"user_id": user.id,
"email": user.email
}
@router.post("/login", response_model=Token)
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
user = await AuthService.authenticate(db, form_data.username, form_data.password)
@@ -34,6 +48,19 @@ async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordReq
access, refresh = create_tokens(data=token_data)
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
class VerifyEmailRequest(BaseModel):
token: str = Field(..., description="Email verification token (UUID)")
@router.post("/verify-email")
async def verify_email(request: VerifyEmailRequest, db: AsyncSession = Depends(get_db)):
"""
Email megerősítés token alapján.
"""
success = await AuthService.verify_email(db, request.token)
if not success:
raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.")
return {"status": "success", "message": "Email sikeresen megerősítve."}
@router.post("/complete-kyc")
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)

View File

@@ -1,4 +1,4 @@
# backend/app/api/v1/endpoints/billing.py
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/billing.py
from fastapi import APIRouter, Depends, HTTPException, status, Request, Header
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
@@ -7,8 +7,8 @@ import logging
from app.api.deps import get_db, get_current_user
from app.models.identity import User, Wallet, UserRole
from app.models.audit import FinancialLedger, WalletType
from app.models.payment import PaymentIntent, PaymentIntentStatus
from app.models import FinancialLedger, WalletType
from app.models.marketplace.payment import PaymentIntent, PaymentIntentStatus
from app.services.config_service import config
from app.services.payment_router import PaymentRouter
from app.services.stripe_adapter import stripe_adapter

View File

@@ -84,4 +84,147 @@ async def get_document_status(
):
"""Lekérdezhető, hogy a robot végzett-e már a feldolgozással."""
# (Itt egy egyszerű lekérdezés a Document táblából a státuszra)
pass
pass
# RBAC helper function
def _check_premium_or_admin(user: User) -> bool:
"""Check if user has premium subscription or admin role."""
premium_plans = ['PREMIUM', 'PREMIUM_PLUS', 'VIP', 'VIP_PLUS']
if user.role == 'admin':
return True
if hasattr(user, 'subscription_plan') and user.subscription_plan in premium_plans:
return True
return False
@router.post("/scan-instant")
async def scan_instant(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Szinkron végpont (Villámszkenner) - forgalmi/ID dokumentumokhoz.
Azonnali OCR feldolgozás és válasz.
RBAC: Csak prémium előfizetés vagy admin.
"""
# RBAC ellenőrzés
if not _check_premium_or_admin(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Prémium előfizetés szükséges a funkcióhoz"
)
try:
# 1. Fájl feltöltése MinIO-ba (StorageService segítségével)
# Jelenleg mock: feltételezzük, hogy a StorageService.upload_file létezik
from app.services.storage_service import StorageService
file_url = await StorageService.upload_file(file, prefix="instant_scan")
# 2. Mock OCR hívás (valós implementációban AiOcrService-t hívnánk)
mock_ocr_result = {
"plate": "TEST-123",
"vin": "TRX12345",
"make": "Toyota",
"model": "Corolla",
"year": 2022,
"fuel_type": "petrol",
"engine_capacity": 1600
}
# 3. Dokumentum rekord létrehozása system.documents táblában
from app.models import Document
from datetime import datetime, timezone
import uuid
doc = Document(
id=uuid.uuid4(),
user_id=current_user.id,
original_name=file.filename,
file_path=file_url,
file_size=file.size,
mime_type=file.content_type,
status='processed',
ocr_data=mock_ocr_result,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(doc)
await db.commit()
await db.refresh(doc)
# 4. Válasz
return {
"document_id": str(doc.id),
"status": "processed",
"ocr_result": mock_ocr_result,
"file_url": file_url,
"message": "Dokumentum sikeresen feldolgozva"
}
except Exception as e:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Hiba a dokumentum feldolgozása során: {str(e)}"
)
@router.post("/upload-async")
async def upload_async(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Aszinkron végpont (Költség/Számla nyelő) - háttérben futó OCR-nek.
Azonnali 202 Accepted válasz, pending_ocr státusszal.
RBAC: Csak prémium előfizetés vagy admin.
"""
# RBAC ellenőrzés
if not _check_premium_or_admin(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Prémium előfizetés szükséges a funkcióhoz"
)
try:
# 1. Fájl feltöltése MinIO-ba
from app.services.storage_service import StorageService
file_url = await StorageService.upload_file(file, prefix="async_upload")
# 2. Dokumentum rekord létrehozása pending_ocr státusszal
from app.models import Document
from datetime import datetime, timezone
import uuid
doc = Document(
id=uuid.uuid4(),
user_id=current_user.id,
original_name=file.filename,
file_path=file_url,
file_size=file.size,
mime_type=file.content_type,
status='pending_ocr', # Fontos: a háttérrobot ezt fogja felvenni
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(doc)
await db.commit()
await db.refresh(doc)
# 3. 202 Accepted válasz
return {
"document_id": str(doc.id),
"status": "pending_ocr",
"message": "A dokumentum feltöltve, háttérben történő elemzése megkezdődött.",
"file_url": file_url
}
except Exception as e:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Hiba a dokumentum feltöltése során: {str(e)}"
)

View File

@@ -1,10 +1,10 @@
# backend/app/api/v1/endpoints/evidence.py
# /opt/docker/dev/service_finder/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 select, func, text
from app.api.deps import get_db, get_current_user
from app.models.identity import User
from app.models.asset import Asset # JAVÍTVA: Asset modell
from app.models import Asset # JAVÍTVA: Asset modell
router = APIRouter()

View File

@@ -1,9 +1,9 @@
# backend/app/api/v1/endpoints/expenses.py
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/expenses.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.api.deps import get_db, get_current_user
from app.models.asset import Asset, AssetCost # JAVÍTVA
from app.models import Asset, AssetCost # JAVÍTVA
from pydantic import BaseModel
from datetime import date
@@ -18,15 +18,23 @@ class ExpenseCreate(BaseModel):
@router.post("/add")
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():
result = await db.execute(stmt)
asset = result.scalar_one_or_none()
if not asset:
raise HTTPException(status_code=404, detail="Jármű nem található.")
# Determine organization_id from asset
organization_id = asset.current_organization_id or asset.owner_org_id
if not organization_id:
raise HTTPException(status_code=400, detail="Az eszközhez nincs társított szervezet.")
new_cost = AssetCost(
asset_id=expense.asset_id,
cost_type=expense.category,
amount_local=expense.amount,
cost_category=expense.category,
amount_net=expense.amount,
currency="HUF",
date=expense.date,
currency_local="HUF"
organization_id=organization_id
)
db.add(new_cost)
await db.commit()

View File

@@ -11,7 +11,7 @@ from typing import List
from app.api import deps
from app.models.identity import User, UserRole
from app.models.finance import Issuer
from app.models.marketplace.finance import Issuer
from app.schemas.finance import IssuerResponse, IssuerUpdate
router = APIRouter()

View File

@@ -1,40 +1,475 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/gamification.py
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Body, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from typing import List
from sqlalchemy import select, desc, func, and_
from typing import List, Optional
from datetime import datetime, timedelta
from app.db.session import get_db
from app.api.deps import get_current_user
from app.models.identity import User
from app.models.gamification import UserStats, PointsLedger
from app.services.config_service import config
from app.models import UserStats, PointsLedger, LevelConfig, UserContribution, Badge, UserBadge, Season
from app.models.system import SystemParameter, ParameterScope
from app.models.marketplace.service import ServiceStaging
from app.schemas.gamification import SeasonResponse, UserStatResponse, LeaderboardEntry
router = APIRouter()
# -- SEGÉDFÜGGVÉNY A RENDSZERBEÁLLÍTÁSOKHOZ --
async def get_system_param(db: AsyncSession, key: str, default_value):
stmt = select(SystemParameter).where(SystemParameter.key == key)
res = (await db.execute(stmt)).scalar_one_or_none()
return res.value if res else default_value
@router.get("/my-stats")
async def get_my_stats(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
"""A bejelentkezett felhasználó aktuális XP-je, szintje és büntetőpontjai."""
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
stats = (await db.execute(stmt)).scalar_one_or_none()
if not stats:
return {"total_xp": 0, "current_level": 1, "penalty_points": 0, "services_submitted": 0}
return stats
@router.get("/leaderboard")
async def get_leaderboard(
limit: int = 10,
season_id: Optional[int] = None,
db: AsyncSession = Depends(get_db)
):
"""Vezetőlista - globális vagy szezonális"""
if season_id:
# Szezonális vezetőlista
stmt = (
select(
User.email,
func.sum(UserContribution.points_awarded).label("total_points"),
func.sum(UserContribution.xp_awarded).label("total_xp")
)
.join(UserContribution, User.id == UserContribution.user_id)
.where(UserContribution.season_id == season_id)
.group_by(User.id)
.order_by(desc("total_points"))
.limit(limit)
)
else:
# Globális vezetőlista
stmt = (
select(User.email, UserStats.total_xp, UserStats.current_level)
.join(UserStats, User.id == UserStats.user_id)
.order_by(desc(UserStats.total_xp))
.limit(limit)
)
result = await db.execute(stmt)
if season_id:
return [
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "points": r[1], "xp": r[2]}
for r in result.all()
]
else:
return [
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "xp": r[1], "level": r[2]}
for r in result.all()
]
@router.get("/seasons")
async def get_seasons(
active_only: bool = True,
db: AsyncSession = Depends(get_db)
):
"""Szezonok listázása"""
stmt = select(Season)
if active_only:
stmt = stmt.where(Season.is_active == True)
result = await db.execute(stmt)
seasons = result.scalars().all()
return [
{
"id": s.id,
"name": s.name,
"start_date": s.start_date,
"end_date": s.end_date,
"is_active": s.is_active
}
for s in seasons
]
@router.get("/my-contributions")
async def get_my_contributions(
season_id: Optional[int] = None,
limit: int = 50,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Felhasználó hozzájárulásainak listázása"""
stmt = select(UserContribution).where(UserContribution.user_id == current_user.id)
if season_id:
stmt = stmt.where(UserContribution.season_id == season_id)
stmt = stmt.order_by(desc(UserContribution.created_at)).limit(limit)
result = await db.execute(stmt)
contributions = result.scalars().all()
return [
{
"id": c.id,
"contribution_type": c.contribution_type,
"entity_type": c.entity_type,
"entity_id": c.entity_id,
"points_awarded": c.points_awarded,
"xp_awarded": c.xp_awarded,
"status": c.status,
"created_at": c.created_at
}
for c in contributions
]
@router.get("/season-standings/{season_id}")
async def get_season_standings(
season_id: int,
limit: int = 20,
db: AsyncSession = Depends(get_db)
):
"""Szezon állása - top hozzájárulók"""
# Aktuális szezon ellenőrzése
season_stmt = select(Season).where(Season.id == season_id)
season = (await db.execute(season_stmt)).scalar_one_or_none()
if not season:
raise HTTPException(status_code=404, detail="Season not found")
# Top hozzájárulók lekérdezése
stmt = (
select(
User.email,
func.sum(UserContribution.points_awarded).label("total_points"),
func.sum(UserContribution.xp_awarded).label("total_xp"),
func.count(UserContribution.id).label("contribution_count")
)
.join(UserContribution, User.id == UserContribution.user_id)
.where(
and_(
UserContribution.season_id == season_id,
UserContribution.status == "approved"
)
)
.group_by(User.id)
.order_by(desc("total_points"))
.limit(limit)
)
result = await db.execute(stmt)
standings = result.all()
# Szezonális jutalmak konfigurációja
season_config = await get_system_param(
db, "seasonal_competition_config",
{
"top_contributors_count": 10,
"rewards": {
"first_place": {"credits": 1000, "badge": "season_champion"},
"second_place": {"credits": 500, "badge": "season_runner_up"},
"third_place": {"credits": 250, "badge": "season_bronze"},
"top_10": {"credits": 100, "badge": "season_elite"}
}
}
)
return {
"season": {
"id": season.id,
"name": season.name,
"start_date": season.start_date,
"end_date": season.end_date
},
"standings": [
{
"rank": idx + 1,
"user": f"{r[0][:2]}***@{r[0].split('@')[1]}",
"points": r[1],
"xp": r[2],
"contributions": r[3],
"reward": get_season_reward(idx + 1, season_config)
}
for idx, r in enumerate(standings)
],
"config": season_config
}
def get_season_reward(rank: int, config: dict) -> dict:
"""Szezonális jutalom meghatározása a rang alapján"""
rewards = config.get("rewards", {})
if rank == 1:
return rewards.get("first_place", {})
elif rank == 2:
return rewards.get("second_place", {})
elif rank == 3:
return rewards.get("third_place", {})
elif rank <= config.get("top_contributors_count", 10):
return rewards.get("top_10", {})
else:
return {}
@router.get("/self-defense-status")
async def get_self_defense_status(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Önvédelmi rendszer státusz lekérdezése"""
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
stats = (await db.execute(stmt)).scalar_one_or_none()
if not stats:
return {"total_xp": 0, "current_level": 1, "penalty_points": 0}
return {
"penalty_level": 0,
"restrictions": [],
"recovery_progress": 0,
"can_submit_services": True
}
return stats
# Önvédelmi büntetések konfigurációja
penalty_config = await get_system_param(
db, "self_defense_penalties",
{
"level_minus_1": {"restrictions": ["no_service_submissions"], "duration_days": 7},
"level_minus_2": {"restrictions": ["no_service_submissions", "no_reviews"], "duration_days": 30},
"level_minus_3": {"restrictions": ["no_service_submissions", "no_reviews", "no_messaging"], "duration_days": 365}
}
)
# Büntetési szint meghatározása (egyszerűsített logika)
penalty_level = 0
if stats.penalty_points >= 1000:
penalty_level = -3
elif stats.penalty_points >= 500:
penalty_level = -2
elif stats.penalty_points >= 100:
penalty_level = -1
restrictions = []
if penalty_level < 0:
level_key = f"level_minus_{abs(penalty_level)}"
restrictions = penalty_config.get(level_key, {}).get("restrictions", [])
return {
"penalty_level": penalty_level,
"penalty_points": stats.penalty_points,
"restrictions": restrictions,
"recovery_progress": min(stats.total_xp / 10000 * 100, 100) if penalty_level < 0 else 100,
"can_submit_services": "no_service_submissions" not in restrictions
}
@router.get("/leaderboard")
async def get_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
"""A 10 legtöbb XP-vel rendelkező felhasználó listája."""
# --- AZ ÚJ, DINAMIKUS BEKÜLDŐ VÉGPONT (Gamification 2.0 kompatibilis) ---
@router.post("/submit-service")
async def submit_new_service(
name: str = Body(...),
city: str = Body(...),
address: str = Body(...),
contact_phone: Optional[str] = Body(None),
website: Optional[str] = Body(None),
description: Optional[str] = Body(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
# 1. Önvédelmi státusz ellenőrzése
defense_status = await get_self_defense_status(db, current_user)
if not defense_status["can_submit_services"]:
raise HTTPException(
status_code=403,
detail="Önvédelmi korlátozás miatt nem küldhetsz be új szerviz adatokat."
)
# 2. Beállítások lekérése az Admin által vezérelt táblából
submission_rewards = await get_system_param(
db, "service_submission_rewards",
{"points": 50, "xp": 100, "social_credits": 10}
)
contribution_config = await get_system_param(
db, "contribution_types_config",
{
"service_submission": {"points": 50, "xp": 100, "weight": 1.0}
}
)
# 3. Aktuális szezon lekérdezése
season_stmt = select(Season).where(
and_(
Season.is_active == True,
Season.start_date <= datetime.utcnow().date(),
Season.end_date >= datetime.utcnow().date()
)
).limit(1)
season_result = await db.execute(season_stmt)
current_season = season_result.scalar_one_or_none()
# 4. Felhasználó statisztikák
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
stats = (await db.execute(stmt)).scalar_one_or_none()
user_lvl = stats.current_level if stats else 1
# 5. Trust score számítás a szint alapján
trust_weight = min(20 + (user_lvl * 6), 90)
# 6. Nyers adat beküldése a Robotoknak (Staging)
import hashlib
f_print = hashlib.md5(f"{name.lower()}{city.lower()}{address.lower()}".encode()).hexdigest()
new_staging = ServiceStaging(
name=name,
city=city,
address_line1=address,
contact_phone=contact_phone,
website=website,
description=description,
fingerprint=f_print,
status="pending",
trust_score=trust_weight,
submitted_by=current_user.id,
raw_data={
"submitted_by_user": current_user.id,
"user_level": user_lvl,
"submitted_at": datetime.utcnow().isoformat()
}
)
db.add(new_staging)
await db.flush() # Get the ID
# 7. UserContribution létrehozása
contribution = UserContribution(
user_id=current_user.id,
season_id=current_season.id if current_season else None,
contribution_type="service_submission",
entity_type="service_staging",
entity_id=new_staging.id,
points_awarded=submission_rewards.get("points", 50),
xp_awarded=submission_rewards.get("xp", 100),
status="pending", # Robot 5 jóváhagyására vár
metadata={
"service_name": name,
"city": city,
"staging_id": new_staging.id
},
created_at=datetime.utcnow()
)
db.add(contribution)
# 8. PointsLedger bejegyzés
ledger = PointsLedger(
user_id=current_user.id,
points=submission_rewards.get("points", 50),
xp=submission_rewards.get("xp", 100),
source_type="service_submission",
source_id=new_staging.id,
description=f"Szerviz beküldés: {name}",
created_at=datetime.utcnow()
)
db.add(ledger)
# 9. UserStats frissítése
if stats:
stats.total_points += submission_rewards.get("points", 50)
stats.total_xp += submission_rewards.get("xp", 100)
stats.services_submitted += 1
stats.updated_at = datetime.utcnow()
else:
# Ha nincs még UserStats, létrehozzuk
stats = UserStats(
user_id=current_user.id,
total_points=submission_rewards.get("points", 50),
total_xp=submission_rewards.get("xp", 100),
services_submitted=1,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
db.add(stats)
try:
await db.commit()
return {
"status": "success",
"message": "Szerviz beküldve a rendszerbe elemzésre!",
"xp_earned": submission_rewards.get("xp", 100),
"points_earned": submission_rewards.get("points", 50),
"staging_id": new_staging.id,
"season_id": current_season.id if current_season else None
}
except Exception as e:
await db.rollback()
raise HTTPException(status_code=400, detail=f"Hiba a beküldés során: {str(e)}")
# --- Gamification 2.0 API végpontok (Frontend/Mobil) ---
@router.get("/me", response_model=UserStatResponse)
async def get_my_gamification_stats(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Visszaadja a bejelentkezett felhasználó aktuális statisztikáit.
Ha nincs rekord, alapértelmezett értékekkel tér vissza.
"""
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
stats = (await db.execute(stmt)).scalar_one_or_none()
if not stats:
# Alapértelmezett statisztika
return UserStatResponse(
user_id=current_user.id,
total_xp=0,
current_level=1,
restriction_level=0,
penalty_quota_remaining=0,
banned_until=None
)
return UserStatResponse.from_orm(stats)
@router.get("/seasons/active", response_model=SeasonResponse)
async def get_active_season(
db: AsyncSession = Depends(get_db)
):
"""
Visszaadja az éppen aktív szezont.
"""
stmt = select(Season).where(Season.is_active == True)
season = (await db.execute(stmt)).scalar_one_or_none()
if not season:
raise HTTPException(status_code=404, detail="No active season found")
return SeasonResponse.from_orm(season)
@router.get("/leaderboard", response_model=List[LeaderboardEntry])
async def get_leaderboard_top10(
limit: int = Query(10, ge=1, le=100),
db: AsyncSession = Depends(get_db)
):
"""
Visszaadja a top felhasználókat total_xp alapján csökkenő sorrendben.
"""
stmt = (
select(User.email, UserStats.total_xp, UserStats.current_level)
.join(UserStats, User.id == UserStats.user_id)
select(UserStats, User.email)
.join(User, UserStats.user_id == User.id)
.order_by(desc(UserStats.total_xp))
.limit(limit)
)
result = await db.execute(stmt)
# Az email-eket maszkoljuk a GDPR miatt (pl. k***s@p***.hu)
return [
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "xp": r[1], "level": r[2]}
for r in result.all()
]
rows = result.all()
leaderboard = []
for stats, email in rows:
leaderboard.append(
LeaderboardEntry(
user_id=stats.user_id,
username=email, # email használata username helyett
total_xp=stats.total_xp,
current_level=stats.current_level
)
)
return leaderboard

View File

@@ -5,6 +5,7 @@ import uuid
import hashlib
import logging
from typing import List
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
@@ -12,7 +13,7 @@ from sqlalchemy import select
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
from app.models.marketplace.organization import Organization, OrgType, OrganizationMember
from app.models.identity import User # JAVÍTVA: Központi Identity modell
from app.core.config import settings
@@ -65,12 +66,19 @@ async def onboard_organization(
address_street_type=org_in.address_street_type,
address_house_number=org_in.address_house_number,
address_hrsz=org_in.address_hrsz,
address_stairwell=org_in.address_stairwell,
address_floor=org_in.address_floor,
address_door=org_in.address_door,
country_code=org_in.country_code,
org_type=OrgType.business,
status="pending_verification"
status="pending_verification",
# --- EXPLICIT IDŐBÉLYEGEK A DB HIBA ELKERÜLÉSÉRE ---
first_registered_at=datetime.now(timezone.utc),
current_lifecycle_started_at=datetime.now(timezone.utc),
created_at=datetime.now(timezone.utc),
subscription_plan="FREE",
base_asset_limit=1,
purchased_extra_slots=0,
notification_settings={},
external_integration_config={},
is_ownership_transferable=True
)
db.add(new_org)

View File

@@ -1,10 +1,10 @@
# backend/app/api/v1/endpoints/search.py
# /opt/docker/dev/service_finder/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.models.organization import Organization # JAVÍTVA
from app.models.marketplace.organization import Organization # JAVÍTVA
router = APIRouter()

View File

@@ -99,7 +99,7 @@ async def approve_action(
await security_service.approve_action(db, approver_id=current_user.id, action_id=action_id)
# Frissített művelet lekérdezése
from sqlalchemy import select
from app.models.security import PendingAction
from app.models import PendingAction
stmt = select(PendingAction).where(PendingAction.id == action_id)
action = (await db.execute(stmt)).scalar_one()
return PendingActionResponse.from_orm(action)
@@ -135,7 +135,7 @@ async def reject_action(
)
# Frissített művelet lekérdezése
from sqlalchemy import select
from app.models.security import PendingAction
from app.models import PendingAction
stmt = select(PendingAction).where(PendingAction.id == action_id)
action = (await db.execute(stmt)).scalar_one()
return PendingActionResponse.from_orm(action)
@@ -158,7 +158,7 @@ async def get_action(
Csak a művelet létrehozója vagy admin/superadmin érheti el.
"""
from sqlalchemy import select
from app.models.security import PendingAction
from app.models import PendingAction
stmt = select(PendingAction).where(PendingAction.id == action_id)
action = (await db.execute(stmt)).scalar_one_or_none()
if not action:

View File

@@ -4,7 +4,9 @@ from sqlalchemy import select, and_, text
from typing import List, Optional
from app.db.session import get_db
from app.services.gamification_service import GamificationService
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise
from app.services.config_service import ConfigService
from app.services.security_auditor import SecurityAuditorService
from app.models.marketplace.service import ServiceProfile, ExpertiseTag, ServiceExpertise
from app.services.marketplace_service import (
create_verified_review,
get_service_reviews,
@@ -19,24 +21,92 @@ router = APIRouter()
# --- 🎯 SZERVIZ VADÁSZAT (Service Hunt) ---
@router.post("/hunt")
async def register_service_hunt(
name: str = Form(...),
lat: float = Form(...),
lng: float = Form(...),
name: str = Form(...),
lat: float = Form(...),
lng: float = Form(...),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
""" Új szerviz-jelölt rögzítése a staging táblába jutalompontért. """
# Új szerviz-jelölt rögzítése
await db.execute(text("""
INSERT INTO marketplace.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})
INSERT INTO marketplace.service_staging (name, fingerprint, status, city, submitted_by, raw_data)
VALUES (:n, :f, 'pending', 'Unknown', :user_id, jsonb_build_object('lat', CAST(:lat AS double precision), 'lng', CAST(:lng AS double precision)))
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng, "user_id": current_user.id})
# MB 2.0 Gamification: 50 pont a felfedezésért
# TODO: A 1-es ID helyett a bejelentkezett felhasználót kell használni (current_user.id)
await GamificationService.award_points(db, 1, 50, f"Service Hunt: {name}")
# MB 2.0 Gamification: Dinamikus pontszám a felfedezésért
reward_points = await ConfigService.get_int(db, "GAMIFICATION_HUNT_REWARD", 50)
await GamificationService.award_points(db, current_user.id, reward_points, f"Service Hunt: {name}")
await db.commit()
return {"status": "success", "message": "Discovery registered and points awarded."}
# --- ✅ SZERVIZ VALIDÁLÁS (Service Validation) ---
@router.post("/hunt/{staging_id}/validate")
async def validate_staged_service(
staging_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""
Validálja egy másik felhasználó által beküldött szerviz-jelöltet.
Növeli a validation_level-t 10-zel (max 80), adományoz 10 XP-t,
és növeli a places_validated számlálót a felhasználó statisztikáiban.
"""
# Anti-Cheat: Rapid Fire ellenőrzés
await SecurityAuditorService.check_rapid_fire_validation(db, current_user.id)
# 1. Keresd meg a staging rekordot
result = await db.execute(
text("SELECT id, submitted_by, validation_level FROM marketplace.service_staging WHERE id = :id"),
{"id": staging_id}
)
staging = result.fetchone()
if not staging:
raise HTTPException(status_code=404, detail="Staging record not found")
# 2. Ha a saját beküldését validálná, hiba
if staging.submitted_by == current_user.id:
raise HTTPException(status_code=400, detail="Cannot validate your own submission")
# 3. Növeld a validation_level-t 10-zel (max 80)
new_level = staging.validation_level + 10
if new_level > 80:
new_level = 80
# 4. UPDATE a validation_level és a status (ha elérte a 80-at, akkor "verified"?)
# Jelenleg csak a validation_level frissítése
await db.execute(
text("""
UPDATE marketplace.service_staging
SET validation_level = :new_level
WHERE id = :id
"""),
{"new_level": new_level, "id": staging_id}
)
# 5. Adományozz dinamikus XP-t a current_user-nek a GamificationService-en keresztül
validation_reward = await ConfigService.get_int(db, "GAMIFICATION_VALIDATE_REWARD", 10)
await GamificationService.award_points(db, current_user.id, validation_reward, f"Service Validation: staging #{staging_id}")
# 6. Növeld a current_user places_validated értékét a UserStats-ban
await db.execute(
text("""
UPDATE gamification.user_stats
SET places_validated = places_validated + 1
WHERE user_id = :user_id
"""),
{"user_id": current_user.id}
)
await db.commit()
return {
"status": "success",
"message": "Validation successful",
"validation_level": new_level,
"places_validated_incremented": True
}
# --- 🔍 SZERVIZ KERESŐ (Service Search) ---
@router.get("/search")
async def search_services(

View File

@@ -0,0 +1,132 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/system_parameters.py
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from typing import List, Optional
from app.api.deps import get_db, get_current_user
from app.schemas.system import (
SystemParameterResponse,
SystemParameterUpdate,
SystemParameterCreate,
)
from app.models.system import SystemParameter, ParameterScope
from app.models.identity import UserRole
router = APIRouter()
@router.get("/", response_model=List[SystemParameterResponse])
async def list_system_parameters(
db: AsyncSession = Depends(get_db),
scope_level: Optional[ParameterScope] = Query(None, description="Scope szint (global, country, region, user)"),
scope_id: Optional[str] = Query(None, description="Scope azonosító (pl. 'HU', 'budapest', user_id)"),
is_active: Optional[bool] = Query(True, description="Csak aktív paraméterek"),
):
"""
Listázza az összes aktív (vagy opcionálisan inaktív) rendszerparamétert.
Szűrhető scope_level és scope_id alapján.
"""
query = select(SystemParameter)
if scope_level is not None:
query = query.where(SystemParameter.scope_level == scope_level)
if scope_id is not None:
query = query.where(SystemParameter.scope_id == scope_id)
if is_active is not None:
query = query.where(SystemParameter.is_active == is_active)
result = await db.execute(query)
parameters = result.scalars().all()
return parameters
@router.get("/{key}", response_model=SystemParameterResponse)
async def get_system_parameter(
key: str,
db: AsyncSession = Depends(get_db),
scope_level: ParameterScope = Query("global", description="Scope szint (alapértelmezett: global)"),
scope_id: Optional[str] = Query(None, description="Scope azonosító"),
):
"""
Visszaad egy konkrét paramétert a key és scope_level (és opcionálisan scope_id) alapján.
"""
query = select(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level,
)
if scope_id is not None:
query = query.where(SystemParameter.scope_id == scope_id)
else:
query = query.where(SystemParameter.scope_id.is_(None))
result = await db.execute(query)
parameter = result.scalar_one_or_none()
if not parameter:
raise HTTPException(
status_code=404,
detail=f"System parameter not found with key='{key}', scope_level='{scope_level}', scope_id='{scope_id}'"
)
return parameter
@router.put("/{key}", response_model=SystemParameterResponse)
async def update_system_parameter(
key: str,
param_in: SystemParameterUpdate,
db: AsyncSession = Depends(get_db),
current_user=Depends(get_current_user),
scope_level: ParameterScope = Query("global", description="Scope szint (alapértelmezett: global)"),
scope_id: Optional[str] = Query(None, description="Scope azonosító"),
):
"""
Módosítja egy létező paraméter value (JSONB) vagy is_active mezőjét (Admin funkció).
Csak superadmin vagy admin jogosultságú felhasználók használhatják.
"""
# Jogosultság ellenőrzése
if current_user.role not in (UserRole.superadmin, UserRole.admin):
raise HTTPException(
status_code=403,
detail="Insufficient permissions. Only superadmin or admin can update system parameters."
)
# Paraméter keresése
query = select(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level,
)
if scope_id is not None:
query = query.where(SystemParameter.scope_id == scope_id)
else:
query = query.where(SystemParameter.scope_id.is_(None))
result = await db.execute(query)
parameter = result.scalar_one_or_none()
if not parameter:
raise HTTPException(
status_code=404,
detail=f"System parameter not found with key='{key}', scope_level='{scope_level}', scope_id='{scope_id}'"
)
# Frissítés
update_data = {}
if param_in.description is not None:
update_data["description"] = param_in.description
if param_in.value is not None:
update_data["value"] = param_in.value
if param_in.is_active is not None:
update_data["is_active"] = param_in.is_active
if update_data:
stmt = (
update(SystemParameter)
.where(SystemParameter.id == parameter.id)
.values(**update_data)
)
await db.execute(stmt)
await db.commit()
await db.refresh(parameter)
return parameter

View File

@@ -11,7 +11,7 @@ from sqlalchemy.orm import selectinload
from app.db.session import get_db
from app.api.deps import get_current_user
from app.models.vehicle import VehicleUserRating
from app.models.vehicle_definitions import VehicleModelDefinition
from app.models import VehicleModelDefinition
from app.models.identity import User
from app.schemas.vehicle import VehicleRatingCreate, VehicleRatingResponse

View File

@@ -59,10 +59,16 @@ class Settings(BaseSettings):
)
REDIS_URL: str = "redis://service_finder_redis:6379/0"
# --- MinIO S3 Storage ---
MINIO_ENDPOINT: str = "sf_minio:9000"
MINIO_ACCESS_KEY: str = "kincses"
MINIO_SECRET_KEY: str = "MiskociA74"
MINIO_SECURE: bool = False
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
"""
Ez a property biztosítja, hogy a database.py és az Alembic
"""
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

View File

@@ -21,9 +21,9 @@ from apscheduler.jobstores.memory import MemoryJobStore
from app.database import AsyncSessionLocal
from app.services.billing_engine import SmartDeduction
from app.models.payment import WithdrawalRequest, WithdrawalRequestStatus
from app.models.marketplace.payment import WithdrawalRequest, WithdrawalRequestStatus
from app.models.identity import User
from app.models.audit import ProcessLog, WalletType, FinancialLedger
from app.models import ProcessLog, WalletType, FinancialLedger
from sqlalchemy import select, update, and_
from sqlalchemy.orm import selectinload
@@ -152,12 +152,16 @@ async def daily_financial_maintenance() -> None:
stats["errors"].append(f"Soft downgrade error: {str(e)}")
logger.error(f"Soft downgrade error: {e}", exc_info=True)
# D. Naplózás ProcessLog-ba
# D. Naplózás ProcessLog-ba (JAVÍTOTT RÉSZ)
process_log = ProcessLog(
process_name="Daily-Financial-Maintenance",
status="COMPLETED" if not stats["errors"] else "PARTIAL",
details=stats,
executed_at=datetime.utcnow()
items_processed=stats["vouchers_expired"] + stats["withdrawals_rejected"] + stats["users_downgraded"],
items_failed=len(stats["errors"]),
end_time=datetime.utcnow(),
details={
"status": "COMPLETED" if not stats["errors"] else "PARTIAL",
**stats
}
)
db.add(process_log)
await db.commit()
@@ -166,12 +170,17 @@ async def daily_financial_maintenance() -> None:
except Exception as e:
logger.error(f"Daily financial maintenance failed: {e}", exc_info=True)
# Hiba esetén is naplózzuk
# Hiba esetén is naplózzuk a modellnek megfelelő mezőkkel
process_log = ProcessLog(
process_name="Daily-Financial-Maintenance",
status="FAILED",
details={"error": str(e), **stats},
executed_at=datetime.utcnow()
items_processed=0,
items_failed=1,
end_time=datetime.utcnow(),
details={
"status": "FAILED",
"error": str(e),
**stats
}
)
db.add(process_log)
await db.commit()

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/validators.py (Javasolt új hely)
# /opt/docker/dev/service_finder/backend/app/core/validators.py
import hashlib
import unicodedata
import re

View File

@@ -6,30 +6,30 @@ from app.models.address import Address, GeoPostalCode, GeoStreet, GeoStreetType,
from app.models.identity import Person, User, Wallet, VerificationToken, SocialAccount # noqa
from app.models.organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment # noqa
from app.models.marketplace.organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment # noqa
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter # noqa
from app.models.marketplace.service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter # noqa
from app.models.vehicle_definitions import VehicleType, VehicleModelDefinition, FeatureDefinition # noqa
from app.models import VehicleType, VehicleModelDefinition, FeatureDefinition # noqa
from app.models.audit import SecurityAuditLog, OperationalLog, FinancialLedger # noqa <--- KRITIKUS!
from app.models import SecurityAuditLog, OperationalLog, FinancialLedger # noqa <--- KRITIKUS!
from app.models.asset import ( # noqa
from app.models import ( # noqa
Asset, AssetCatalog, AssetCost, AssetEvent,
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
)
from app.models.gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger # noqa
from app.models import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger # noqa
from app.models.system import SystemParameter # noqa (system.py használata)
from app.models.history import AuditLog, VehicleOwnership # noqa
from app.models import AuditLog, VehicleOwnership # noqa
from app.models.document import Document # noqa
from app.models import Document # noqa
from app.models.translation import Translation # noqa
from app.models import Translation # noqa
from app.models.core_logic import ( # noqa
SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
)
from app.models.security import PendingAction # noqa
from app.models import PendingAction # noqa

View File

@@ -1,7 +1,7 @@
# /opt/docker/dev/service_finder/backend/app/db/middleware.py
from fastapi import Request
from app.db.session import AsyncSessionLocal
from app.models.audit import OperationalLog # JAVÍTVA: Az új modell
from app.models import OperationalLog # JAVÍTVA: Az új modell
from sqlalchemy import text
async def audit_log_middleware(request: Request, call_next):

View File

@@ -9,7 +9,8 @@ engine = create_async_engine(
future=True,
pool_size=30, # A robotok száma miatt
max_overflow=20,
pool_pre_ping=True
pool_pre_ping=True,
pool_reset_on_return='rollback'
)
AsyncSessionLocal = async_sessionmaker(
@@ -21,8 +22,20 @@ AsyncSessionLocal = async_sessionmaker(
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
# Start with a clean transaction state by rolling back any failed transaction
try:
await session.rollback()
except Exception:
# If rollback fails, it's probably because there's no transaction
# This is fine, just continue
pass
try:
yield session
# JAVÍTVA: Nincs automatikus commit! Az endpoint felelőssége.
except Exception:
# If any exception occurs, rollback the transaction
await session.rollback()
raise
finally:
# Ensure session is closed
await session.close()

View File

@@ -3,46 +3,53 @@
from app.database import Base
# 1. Alapvető identitás és szerepkörök
from .identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
from .identity.identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
# 2. Földrajzi adatok és címek
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Rating
from .identity.address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Rating
# 3. Jármű definíciók
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
from .vehicle.vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
from .reference_data import ReferenceLookup
from .vehicle import CostCategory, VehicleCost
from .vehicle.vehicle import CostCategory, VehicleCost, GbCatalogDiscovery
from .vehicle.external_reference import ExternalReferenceLibrary
from .vehicle.external_reference_queue import ExternalReferenceQueue
# 4. Szervezeti felépítés
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole, Branch
from .marketplace.organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole, Branch
# 5. Eszközök és katalógusok
from .asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership
from .vehicle.asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetAssignment, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership
# 6. Üzleti logika és előfizetések
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
from .payment import PaymentIntent, PaymentIntentStatus
from .finance import Issuer, IssuerType
from .marketplace.payment import PaymentIntent, PaymentIntentStatus
from .marketplace.finance import Issuer, IssuerType
# 7. Szolgáltatások és staging
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
# JAVÍTVA: ServiceStaging és társai a staged_data-ból jönnek!
from .marketplace.service import ServiceProfile, ExpertiseTag, ServiceExpertise
from .marketplace.staged_data import ServiceStaging, DiscoveryParameter, StagedVehicleData
from .marketplace.service_request import ServiceRequest
# 8. Közösségi és értékelési modellek (Social 3)
from .social import ServiceProvider, Vote, Competition, UserScore, ServiceReview, ModerationStatus, SourceType
from .identity.social import ServiceProvider, Vote, Competition, UserScore, ServiceReview, ModerationStatus, SourceType
# 9. Rendszer, Gamification és egyebek
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
from .gamification.gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger, UserContribution, Season
# --- 2.2 ÚJDONSÁG: InternalNotification hozzáadása ---
from .system import SystemParameter, InternalNotification
from .system.system import SystemParameter, ParameterScope, InternalNotification, SystemServiceStaging
from .system.document import Document
from .system.translation import Translation
# Direct import from audit module
from .system.audit import SecurityAuditLog, OperationalLog, ProcessLog, FinancialLedger, WalletType, LedgerStatus, LedgerEntryType
from .vehicle.history import AuditLog, LogSeverity
from .identity.security import PendingAction, ActionStatus
from .system.legal import LegalDocument, LegalAcceptance
from .marketplace.logistics import Location, LocationType
from .document import Document
from .translation import Translation
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
# Aliasok a Digital Twin kompatibilitáshoz
Vehicle = Asset
@@ -53,25 +60,26 @@ ServiceRecord = AssetEvent
__all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
"Organization", "OrganizationMember", "OrganizationSalesAssignment", "OrgType", "OrgUserRole",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetAssignment", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate", "CatalogDiscovery",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", "UserContribution",
# --- 2.2 ÚJDONSÁG KIEGÉSZÍTÉS ---
"SystemParameter", "InternalNotification",
"SystemParameter", "ParameterScope", "InternalNotification",
# Social models (Social 3)
"ServiceProvider", "Vote", "Competition", "UserScore", "ServiceReview", "ModerationStatus", "SourceType",
"Document", "Translation", "PendingAction",
"Document", "Translation", "PendingAction", "ActionStatus",
"SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty",
"PaymentIntent", "PaymentIntentStatus",
"AuditLog", "VehicleOwnership", "LogSeverity",
"SecurityAuditLog", "ProcessLog", "FinancialLedger",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter",
"SecurityAuditLog", "OperationalLog", "ProcessLog",
"FinancialLedger", "WalletType", "LedgerStatus", "LedgerEntryType",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter", "ServiceRequest",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition", "ReferenceLookup",
"VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
"Location", "LocationType", "Issuer", "IssuerType", "CostCategory", "VehicleCost"
]
from app.models.payment import PaymentIntent, WithdrawalRequest
"Location", "LocationType", "Issuer", "IssuerType", "CostCategory", "VehicleCost", "ExternalReferenceLibrary", "ExternalReferenceQueue",
"GbCatalogDiscovery", "Season", "StagedVehicleData"
]

135
backend/app/models/audit.py Executable file → Normal file
View File

@@ -1,115 +1,24 @@
# /opt/docker/dev/service_finder/backend/app/models/audit.py
import enum
import uuid
from datetime import datetime
from typing import Any, Optional
from sqlalchemy import String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from app.database import Base
# Backward compatibility stub for audit module
# After restructuring, audit models moved to system.audit
# This file re-exports everything to maintain compatibility
class SecurityAuditLog(Base):
""" Kiemelt biztonsági események és a 4-szem elv naplózása. """
__tablename__ = "security_audit_logs"
__table_args__ = {"schema": "audit"}
from .system.audit import (
SecurityAuditLog,
OperationalLog,
ProcessLog,
LedgerEntryType,
WalletType,
LedgerStatus,
FinancialLedger,
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
action: Mapped[Optional[str]] = mapped_column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST'
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)
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": "audit"}
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"
__table_args__ = {"schema": "audit"}
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 LedgerEntryType(str, enum.Enum):
DEBIT = "DEBIT"
CREDIT = "CREDIT"
class WalletType(str, enum.Enum):
EARNED = "EARNED"
PURCHASED = "PURCHASED"
SERVICE_COINS = "SERVICE_COINS"
VOUCHER = "VOUCHER"
class LedgerStatus(str, enum.Enum):
PENDING = "PENDING"
SUCCESS = "SUCCESS"
FAILED = "FAILED"
REFUNDED = "REFUNDED"
REFUND = "REFUND"
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. Billing Engine alapja. """
__tablename__ = "financial_ledger"
__table_args__ = {"schema": "audit"}
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())
# Új mezők doubleentry és okos levonáshoz
entry_type: Mapped[LedgerEntryType] = mapped_column(
PG_ENUM(LedgerEntryType, name="ledger_entry_type", schema="audit"),
nullable=False
)
balance_after: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
wallet_type: Mapped[Optional[WalletType]] = mapped_column(
PG_ENUM(WalletType, name="wallet_type", schema="audit")
)
# Economy 1: számlázási mezők
issuer_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("finance.issuers.id"), nullable=True)
invoice_status: Mapped[Optional[str]] = mapped_column(String(50), default="PENDING")
tax_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
gross_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
net_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
transaction_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), default=uuid.uuid4, nullable=False, index=True
)
status: Mapped[LedgerStatus] = mapped_column(
PG_ENUM(LedgerStatus, name="ledger_status", schema="audit"),
default=LedgerStatus.SUCCESS,
nullable=False
)
# Re-export everything
__all__ = [
"SecurityAuditLog",
"OperationalLog",
"ProcessLog",
"LedgerEntryType",
"WalletType",
"LedgerStatus",
"FinancialLedger",
]

View File

@@ -1,86 +0,0 @@
# /opt/docker/dev/service_finder/backend/app/models/gamification.py
import uuid
from datetime import datetime
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.database import Base # MB 2.0: Központi Base
if TYPE_CHECKING:
from app.models.identity import User
class PointRule(Base):
__tablename__ = "point_rules"
__table_args__ = {"schema": "system"}
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)
description: Mapped[Optional[str]] = mapped_column(String)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class LevelConfig(Base):
__tablename__ = "level_configs"
__table_args__ = {"schema": "system"}
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)
rank_name: Mapped[str] = mapped_column(String)
class PointsLedger(Base):
__tablename__ = "points_ledger"
__table_args__ = {"schema": "system"}
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"))
points: Mapped[int] = mapped_column(Integer, default=0)
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(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")
class UserStats(Base):
__tablename__ = "user_stats"
__table_args__ = {"schema": "system"}
# 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)
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(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": "system"}
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)
icon_url: Mapped[Optional[str]] = mapped_column(String)
class UserBadge(Base):
__tablename__ = "user_badges"
__table_args__ = {"schema": "system"}
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("system.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")

View File

@@ -0,0 +1,22 @@
# gamification package exports
from .gamification import (
PointRule,
LevelConfig,
PointsLedger,
UserStats,
Badge,
UserBadge,
UserContribution,
Season,
)
__all__ = [
"PointRule",
"LevelConfig",
"PointsLedger",
"UserStats",
"Badge",
"UserBadge",
"UserContribution",
"Season",
]

View File

@@ -0,0 +1,144 @@
# /opt/docker/dev/service_finder/backend/app/models/gamification/gamification.py
import uuid
from datetime import datetime, date
from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text, Date
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from app.database import Base # MB 2.0: Központi Base
if TYPE_CHECKING:
from app.models.identity import User
class PointRule(Base):
__tablename__ = "point_rules"
__table_args__ = {"schema": "gamification", "extend_existing": True}
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)
description: Mapped[Optional[str]] = mapped_column(String)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class LevelConfig(Base):
__tablename__ = "level_configs"
__table_args__ = {"schema": "gamification", "extend_existing": True}
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)
rank_name: Mapped[str] = mapped_column(String)
class PointsLedger(Base):
__tablename__ = "points_ledger"
__table_args__ = {"schema": "gamification", "extend_existing": True}
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"))
points: Mapped[int] = mapped_column(Integer, default=0)
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(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")
class UserStats(Base):
__tablename__ = "user_stats"
__table_args__ = {"schema": "gamification", "extend_existing": 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)
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)
penalty_quota_remaining: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
places_discovered: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
places_validated: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
banned_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
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": "gamification", "extend_existing": True}
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)
icon_url: Mapped[Optional[str]] = mapped_column(String)
class UserBadge(Base):
__tablename__ = "user_badges"
__table_args__ = {"schema": "gamification", "extend_existing": True}
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("gamification.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")
class UserContribution(Base):
"""
Felhasználói hozzájárulások nyilvántartása (szerviz beküldés, validálás, jelentés).
Ez a tábla tárolja, hogy melyik felhasználó milyen tevékenységet végzett és milyen jutalmat kapott.
"""
__tablename__ = "user_contributions"
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False, index=True)
season_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("gamification.seasons.id"), nullable=True, index=True)
# --- HIÁNYZÓ MEZŐK PÓTOLVA A SPAM VÉDELEMHEZ ---
service_fingerprint: Mapped[Optional[str]] = mapped_column(String(255), index=True)
cooldown_end: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
action_type: Mapped[int] = mapped_column(Integer, nullable=False)
earned_xp: Mapped[int] = mapped_column(Integer, nullable=False)
contribution_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True) # 'service_submission', 'service_validation', 'report_abuse'
entity_type: Mapped[Optional[str]] = mapped_column(String(50), index=True) # 'service', 'review', 'comment'
entity_id: Mapped[Optional[int]] = mapped_column(Integer, index=True) # ID of the contributed entity
points_awarded: Mapped[int] = mapped_column(Integer, default=0)
xp_awarded: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(20), default="pending", index=True) # 'pending', 'approved', 'rejected'
reviewed_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
# --- JAVÍTOTT FOGLALT SZÓ ---
provided_fields: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Relationships
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
reviewer: Mapped[Optional["User"]] = relationship("User", foreign_keys=[reviewed_by])
season: Mapped[Optional["Season"]] = relationship("Season")
class Season(Base):
""" Szezonális versenyek tárolása. """
__tablename__ = "seasons"
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String(100), nullable=False)
start_date: Mapped[date] = mapped_column(Date, nullable=False)
end_date: Mapped[date] = mapped_column(Date, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -0,0 +1,55 @@
# identity package exports
from .identity import (
Person,
User,
Wallet,
VerificationToken,
SocialAccount,
ActiveVoucher,
UserTrustProfile,
UserRole,
)
from .address import (
Address,
GeoPostalCode,
GeoStreet,
GeoStreetType,
Rating,
)
from .security import PendingAction, ActionStatus
from .social import (
ServiceProvider,
Vote,
Competition,
UserScore,
ServiceReview,
ModerationStatus,
SourceType,
)
__all__ = [
"Person",
"User",
"Wallet",
"VerificationToken",
"SocialAccount",
"ActiveVoucher",
"UserTrustProfile",
"UserRole",
"Address",
"GeoPostalCode",
"GeoStreet",
"GeoStreetType",
"Rating",
"PendingAction",
"ActionStatus",
"ServiceProvider",
"Vote",
"Competition",
"UserScore",
"ServiceReview",
"ModerationStatus",
"SourceType",
]

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/address.py
# /opt/docker/dev/service_finder/backend/app/models/identity/address.py
import uuid
from datetime import datetime
from typing import Any, List, Optional

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/identity/identity.py
from __future__ import annotations
import uuid
import enum
@@ -56,25 +57,51 @@ class Person(Base):
birth_place: Mapped[Optional[str]] = mapped_column(String)
birth_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
identity_docs: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
ice_contact: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
identity_docs: Mapped[Any] = mapped_column(JSON, nullable=False, default=lambda: {}, server_default=text("'{}'::jsonb"))
ice_contact: Mapped[Any] = mapped_column(JSON, nullable=False, default=lambda: {}, server_default=text("'{}'::jsonb"))
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"))
lifetime_xp: Mapped[int] = mapped_column(BigInteger, default=-1, nullable=False)
penalty_points: Mapped[int] = mapped_column(Integer, default=-1, nullable=False)
social_reputation: Mapped[float] = mapped_column(Numeric(3, 2), default=0.0, nullable=False)
is_sales_agent: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
is_sales_agent: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_ghost: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=func.now(), nullable=False)
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")
owned_business_entities: Mapped[List["Organization"]] = relationship("Organization", back_populates="legal_owner")
# JAVÍTÁS 1: Explicit 'foreign_keys' megadás az AmbiguousForeignKeysError ellen
users: Mapped[List["User"]] = relationship(
"User",
foreign_keys="[User.person_id]",
back_populates="person",
cascade="all, delete-orphan"
)
# JAVÍTÁS 2: 'post_update' és 'use_alter' a körbe-függőség (circular cycle) feloldásához
active_user_account: Mapped[Optional["User"]] = relationship(
"User",
foreign_keys="[Person.user_id]",
post_update=True
)
user_id: Mapped[Optional[int]] = mapped_column(
Integer,
ForeignKey("identity.users.id", use_alter=True, name="fk_person_active_user"),
nullable=True
)
memberships: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="person")
# Kapcsolat a tulajdonolt szervezetekhez (Organization táblában legal_owner_id)
owned_business_entities: Mapped[List["Organization"]] = relationship(
"Organization",
foreign_keys="[Organization.legal_owner_id]",
back_populates="legal_owner"
)
class User(Base):
""" Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött. """
__tablename__ = "users"
@@ -97,6 +124,7 @@ class User(Base):
referral_code: Mapped[Optional[str]] = mapped_column(String(20), unique=True)
# JAVÍTÁS 3: Az ajánló és értékesítő mezőknek is kell a tiszta kapcsolat nevesítés
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"))
@@ -115,10 +143,32 @@ class User(Base):
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# --- KAPCSOLATOK ---
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="users")
# JAVÍTÁS 4: Itt is explicit megadjuk, hogy melyik kulcs köti az emberhez
person: Mapped[Optional["Person"]] = relationship(
"Person",
foreign_keys=[person_id],
back_populates="users"
)
# JAVÍTÁS 5: Ajánlói (Referrer) önhivatkozó kapcsolat feloldása
referrer: Mapped[Optional["User"]] = relationship(
"User",
remote_side=[id],
foreign_keys=[referred_by_id]
)
# JAVÍTÁS 6: Értékesítő (Sales Agent) önhivatkozó kapcsolat feloldása
sales_agent: Mapped[Optional["User"]] = relationship(
"User",
remote_side=[id],
foreign_keys=[current_sales_agent_id]
)
wallet: Mapped[Optional["Wallet"]] = relationship("Wallet", back_populates="user", uselist=False)
payment_intents_as_payer = relationship("PaymentIntent", foreign_keys="[PaymentIntent.payer_id]", back_populates="payer")
payment_intents_as_beneficiary = relationship("PaymentIntent", foreign_keys="[PaymentIntent.beneficiary_id]", back_populates="beneficiary")
# JAVÍTÁS: Ez a sor KELL az OCR robot és a Trust Engine működéséhez
trust_profile: Mapped[Optional["UserTrustProfile"]] = relationship("UserTrustProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
social_accounts: Mapped[List["SocialAccount"]] = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
@@ -126,6 +176,9 @@ class User(Base):
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")
# MB 2.1: Vehicle ratings kapcsolat (hiányzott a listából, visszatéve)
vehicle_ratings: Mapped[List["VehicleUserRating"]] = relationship("VehicleUserRating", back_populates="user", cascade="all, delete-orphan")
# Pénzügyi és egyéb kapcsolatok
withdrawal_requests: Mapped[List["WithdrawalRequest"]] = relationship("WithdrawalRequest", foreign_keys="[WithdrawalRequest.user_id]", back_populates="user", cascade="all, delete-orphan")
service_reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="user", cascade="all, delete-orphan")

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/security.py
# /opt/docker/dev/service_finder/backend/app/models/identity/security.py
import enum
from datetime import datetime
from typing import Optional, TYPE_CHECKING

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/social.py
# /opt/docker/dev/service_finder/backend/app/models/identity/social.py
import enum
import uuid
from datetime import datetime
@@ -59,7 +59,7 @@ class Vote(Base):
class Competition(Base):
""" Gamifikált versenyek (pl. Januári Feltöltő Verseny). """
__tablename__ = "competitions"
__table_args__ = {"schema": "system"}
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String, nullable=False)
@@ -73,12 +73,12 @@ class UserScore(Base):
__tablename__ = "user_scores"
__table_args__ = (
UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'),
{"schema": "system"}
{"schema": "gamification"}
)
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("system.competitions.id"))
competition_id: Mapped[int] = mapped_column(Integer, ForeignKey("gamification.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,234 +0,0 @@
# /opt/docker/dev/service_finder/backend/app/models/identity.py
from __future__ import annotations
import uuid
import enum
from datetime import datetime
from typing import Any, List, Optional, TYPE_CHECKING
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
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
if TYPE_CHECKING:
from .organization import Organization, OrganizationMember
from .asset import VehicleOwnership
from .gamification import UserStats
class UserRole(str, enum.Enum):
superadmin = "superadmin"
admin = "admin"
region_admin = "region_admin"
country_admin = "country_admin"
moderator = "moderator"
sales_agent = "sales_agent"
user = "user"
service_owner = "service_owner"
fleet_manager = "fleet_manager"
driver = "driver"
class Person(Base):
"""
Természetes személy identitása. A DNS szint.
Minden identitás adat az 'identity' sémába kerül.
"""
__tablename__ = "persons"
__table_args__ = {"schema": "identity"}
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)
# A lakcím a 'data' sémában marad
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.addresses.id"))
# Kritikus azonosító: Név + Anyja neve + Szül.idő hash-elve.
# Ezzel ismerjük fel a személyt akkor is, ha új User accountot hoz létre.
identity_hash: Mapped[Optional[str]] = mapped_column(String(64), unique=True, index=True)
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)
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)
identity_docs: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
ice_contact: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
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"))
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)
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")
# MB 2.0 KIEGÉSZÍTÉS: A személy által birtokolt üzleti entitások (Cégek/Szolgáltatók)
# Ez a lista megmarad akkor is, ha az Organization deaktiválódik.
owned_business_entities: Mapped[List["Organization"]] = relationship("Organization", back_populates="legal_owner")
class User(Base):
""" Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött. """
__tablename__ = "users"
__table_args__ = {"schema": "identity"}
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)
role: Mapped[UserRole] = mapped_column(
PG_ENUM(UserRole, name="userrole", schema="identity"),
default=UserRole.user
)
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
trust_profile: Mapped[Optional["UserTrustProfile"]] = relationship("UserTrustProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
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)
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"))
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)
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")
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"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# 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")
# PaymentIntent kapcsolatok
payment_intents_as_payer: Mapped[List["PaymentIntent"]] = relationship(
"PaymentIntent",
foreign_keys="[PaymentIntent.payer_id]",
back_populates="payer"
)
withdrawal_requests: Mapped[List["WithdrawalRequest"]] = relationship("WithdrawalRequest", foreign_keys="[WithdrawalRequest.user_id]", back_populates="user", cascade="all, delete-orphan")
payment_intents_as_beneficiary: Mapped[List["PaymentIntent"]] = relationship(
"PaymentIntent",
foreign_keys="[PaymentIntent.beneficiary_id]",
back_populates="beneficiary"
)
# Service reviews
service_reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="user", cascade="all, delete-orphan")
@property
def tier_name(self) -> str:
"""Kompatibilitási mező a keresőhöz: a 'FREE' -> 'free' konverzióhoz"""
return (self.subscription_plan or "free").lower()
class Wallet(Base):
__tablename__ = "wallets"
__table_args__ = {"schema": "identity"}
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: 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: Mapped[str] = mapped_column(String(3), default="HUF")
user: Mapped["User"] = relationship("User", back_populates="wallet")
active_vouchers: Mapped[List["ActiveVoucher"]] = relationship("ActiveVoucher", back_populates="wallet", cascade="all, delete-orphan")
class VerificationToken(Base):
__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": "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")
class ActiveVoucher(Base):
"""Aktív, le nem járt voucher-ek tárolása FIFO elv szerint."""
__tablename__ = "active_vouchers"
__table_args__ = {"schema": "identity"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
wallet_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.wallets.id", ondelete="CASCADE"), nullable=False)
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
original_amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Kapcsolatok
wallet: Mapped["Wallet"] = relationship("Wallet", back_populates="active_vouchers")
class UserTrustProfile(Base):
"""
Gondos Gazda Index (Trust Score) tárolása felhasználónként.
A pontszámot a trust_engine számolja dinamikusan a SystemParameter-ek alapján.
"""
__tablename__ = "user_trust_profiles"
__table_args__ = {"schema": "identity"}
user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("identity.users.id", ondelete="CASCADE"),
primary_key=True,
index=True
)
trust_score: Mapped[int] = mapped_column(Integer, default=0, nullable=False) # 0-100 pont
maintenance_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False) # 0.0-1.0
quality_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False) # 0.0-1.0
preventive_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False) # 0.0-1.0
last_calculated: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False
)
# Kapcsolatok
user: Mapped["User"] = relationship("User", back_populates="trust_profile", uselist=False)

View File

@@ -0,0 +1,53 @@
# marketplace package exports
from .organization import (
Organization,
OrganizationMember,
OrganizationFinancials,
OrganizationSalesAssignment,
OrgType,
OrgUserRole,
Branch,
)
from .payment import PaymentIntent, PaymentIntentStatus
from .finance import Issuer, IssuerType
from .service import (
ServiceProfile,
ExpertiseTag,
ServiceExpertise,
)
from .logistics import Location, LocationType
# THOUGHT PROCESS: A StagedVehicleData nevet StagedVehicleData-ra javítottuk,
# és ide csoportosítottuk a staged_data.py-ban lévő többi osztályt is.
from .staged_data import (
StagedVehicleData,
ServiceStaging,
DiscoveryParameter
)
from .service_request import ServiceRequest
__all__ = [
"Organization",
"OrganizationMember",
"OrganizationFinancials",
"OrganizationSalesAssignment",
"OrgType",
"OrgUserRole",
"Branch",
"PaymentIntent",
"PaymentIntentStatus",
"Issuer",
"IssuerType",
"ServiceProfile",
"ExpertiseTag",
"ServiceExpertise",
"ServiceStaging",
"DiscoveryParameter",
"Location",
"LocationType",
"StagedVehicleData",
"ServiceRequest",
]

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/finance.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/finance.py
"""
Finance modellek: Issuer (Kibocsátó) és FinancialLedger (Pénzügyi főkönyv) bővítése.
"""

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/logistics.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/logistics.py
import enum
from typing import Optional
from sqlalchemy import Integer, String, Enum

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/organization.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/organization.py
import enum
import uuid
from datetime import datetime
@@ -8,6 +8,7 @@ from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, J
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID, JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign
from sqlalchemy.sql import func
from geoalchemy2 import Geometry
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
@@ -202,6 +203,12 @@ class Branch(Base):
door: Mapped[Optional[str]] = mapped_column(String(20))
hrsz: Mapped[Optional[str]] = mapped_column(String(50))
# PostGIS location field for geographic queries
location: Mapped[Optional[Any]] = mapped_column(
Geometry(geometry_type='POINT', srid=4326),
nullable=True
)
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
branch_rating: Mapped[float] = mapped_column(Float, default=0.0)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/payment.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/payment.py
"""
Payment Intent modell a Stripe integrációhoz és belső fizetésekhez.
Kettős Lakat (Double Lock) biztonságot valósít meg.
@@ -14,7 +14,7 @@ from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from sqlalchemy.sql import func
from app.database import Base
from app.models.audit import WalletType
from app.models.system.audit import WalletType
class PaymentIntentStatus(str, enum.Enum):

View File

@@ -1,16 +1,23 @@
# /opt/docker/dev/service_finder/backend/app/models/service.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service.py
import enum
import uuid
from datetime import datetime
from typing import Any, List, Optional
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric, BigInteger
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB, ENUM as SQLEnum
from geoalchemy2 import Geometry
from sqlalchemy.sql import func
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class ServiceStatus(str, enum.Enum):
ghost = "ghost" # Nyers, robot által talált, nem validált
active = "active" # Publikus, aktív szerviz
flagged = "flagged" # Gyanús, kézi ellenőrzést igényel
suspended = "suspended" # Felfüggesztett, tiltott szerviz
class ServiceProfile(Base):
""" Szerviz szolgáltató adatai (v1.3.1). """
__tablename__ = "service_profiles"
@@ -26,7 +33,12 @@ class ServiceProfile(Base):
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)
status: Mapped[str] = mapped_column(String(20), server_default=text("'ghost'"), index=True)
status: Mapped[ServiceStatus] = mapped_column(
SQLEnum(ServiceStatus, name="service_status", schema="marketplace"),
server_default=ServiceStatus.ghost.value,
nullable=False,
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)
@@ -73,55 +85,29 @@ class ExpertiseTag(Base):
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Egyedi azonosító kulcs (pl. 'ENGINE_REBUILD')
key: Mapped[str] = mapped_column(String(50), unique=True, index=True)
# Megjelenítendő nevek
name_hu: Mapped[Optional[str]] = mapped_column(String(100))
name_en: Mapped[Optional[str]] = mapped_column(String(100))
# Főcsoport (pl. 'MECHANICS', 'ELECTRICAL', 'EMERGENCY')
category: Mapped[Optional[str]] = mapped_column(String(30), index=True)
# --- 🎮 GAMIFICATION ÉS DISCOVERY ---
# Hivatalos címke (True) vagy júzer/robot által javasolt (False)
is_official: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
# Ha júzer javasolta, itt tároljuk, ki volt az (XP jóváíráshoz)
suggested_by_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
# ÁLLÍTHATÓ PONTÉRTÉK: Az adatbázisból jön, így bármikor módosítható.
# Ritka szakmáknál magasabb, gyakoriaknál alacsonyabb érték állítható be.
discovery_points: Mapped[int] = mapped_column(Integer, default=10, server_default=text("10"))
# Robot kulcsszavak (JSONB): ["fék", "betét", "tárcsa", "fékfolyadék"]
# A Scout robot ez alapján azonosítja be a szervizt a weboldala alapján.
search_keywords: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
# Népszerűségi mutató (hányszor lett felhasználva a rendszerben)
usage_count: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
# UI ikon azonosító (pl. 'wrench', 'tire-flat', 'car-electric')
icon: Mapped[Optional[str]] = mapped_column(String(50))
# Leírás a szakmáról (Adminisztratív célokra)
description: Mapped[Optional[str]] = mapped_column(Text)
# Időbélyegek
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 ---
services: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="tag")
# Visszamutatás a beküldőre (ha van)
suggested_by: Mapped[Optional["Person"]] = relationship("Person")
class ServiceExpertise(Base):
"""
KAPCSOLÓTÁBLA: Ez köti össze a szervizt a szakmáival.
Itt tároljuk, hogy az adott szerviznél mennyire validált egy szakma.
"""
__tablename__ = "service_expertises"
__table_args__ = {"schema": "marketplace"}
@@ -129,13 +115,9 @@ class ServiceExpertise(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"))
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.expertise_tags.id", ondelete="CASCADE"))
# Mennyire biztos ez a tudás? (0: robot találta, 1: júzer mondta, 2: igazolt szakma)
confidence_level: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
# Kapcsolatok visszafelé
service = relationship("ServiceProfile", back_populates="expertises")
tag = relationship("ExpertiseTag", back_populates="services")
@@ -154,6 +136,14 @@ class ServiceStaging(Base):
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"))
# Audit fix: contact_email hossza rögzítve a DB szinkronhoz
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
contact_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
website: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
external_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
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())

View File

@@ -0,0 +1,175 @@
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service.py
import uuid
from datetime import datetime
from typing import Any, List, Optional
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric, BigInteger
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from geoalchemy2 import Geometry
from sqlalchemy.sql import func
# 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ó adatai (v1.3.1). """
__tablename__ = "service_profiles"
__table_args__ = (
Index('idx_service_fingerprint', 'fingerprint', unique=True),
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), unique=True)
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id"))
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)
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)
# Aggregated verified review ratings (Social 3)
rating_verified_count: Mapped[Optional[int]] = mapped_column(Integer, server_default=text("0"))
rating_price_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_quality_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_time_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_communication_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_overall: Mapped[Optional[float]] = mapped_column(Float)
last_review_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=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"))
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"))
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)
# Kapcsolatok
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="service")
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 címkék mesterlistája (MB 2.0).
Ez a tábla vezérli a robotok keresését és a Gamification pontozást is.
"""
__tablename__ = "expertise_tags"
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Egyedi azonosító kulcs (pl. 'ENGINE_REBUILD')
key: Mapped[str] = mapped_column(String(50), unique=True, index=True)
# Megjelenítendő nevek
name_hu: Mapped[Optional[str]] = mapped_column(String(100))
name_en: Mapped[Optional[str]] = mapped_column(String(100))
# Főcsoport (pl. 'MECHANICS', 'ELECTRICAL', 'EMERGENCY')
category: Mapped[Optional[str]] = mapped_column(String(30), index=True)
# --- 🎮 GAMIFICATION ÉS DISCOVERY ---
# Hivatalos címke (True) vagy júzer/robot által javasolt (False)
is_official: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
# Ha júzer javasolta, itt tároljuk, ki volt az (XP jóváíráshoz)
suggested_by_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
# ÁLLÍTHATÓ PONTÉRTÉK: Az adatbázisból jön, így bármikor módosítható.
# Ritka szakmáknál magasabb, gyakoriaknál alacsonyabb érték állítható be.
discovery_points: Mapped[int] = mapped_column(Integer, default=10, server_default=text("10"))
# Robot kulcsszavak (JSONB): ["fék", "betét", "tárcsa", "fékfolyadék"]
# A Scout robot ez alapján azonosítja be a szervizt a weboldala alapján.
search_keywords: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
# Népszerűségi mutató (hányszor lett felhasználva a rendszerben)
usage_count: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
# UI ikon azonosító (pl. 'wrench', 'tire-flat', 'car-electric')
icon: Mapped[Optional[str]] = mapped_column(String(50))
# Leírás a szakmáról (Adminisztratív célokra)
description: Mapped[Optional[str]] = mapped_column(Text)
# Időbélyegek
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 ---
services: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="tag")
# Visszamutatás a beküldőre (ha van)
suggested_by: Mapped[Optional["Person"]] = relationship("Person")
class ServiceExpertise(Base):
"""
KAPCSOLÓTÁBLA: Ez köti össze a szervizt a szakmáival.
Itt tároljuk, hogy az adott szerviznél mennyire validált egy szakma.
"""
__tablename__ = "service_expertises"
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"))
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.expertise_tags.id", ondelete="CASCADE"))
# Mennyire biztos ez a tudás? (0: robot találta, 1: júzer mondta, 2: igazolt szakma)
confidence_level: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
# Kapcsolatok visszafelé
service = relationship("ServiceProfile", back_populates="expertises")
tag = relationship("ExpertiseTag", back_populates="services")
class ServiceStaging(Base):
""" Hunter (robot) adatok tárolója. """
__tablename__ = "service_staging"
__table_args__ = (
Index('idx_staging_fingerprint', 'fingerprint', unique=True),
{"schema": "marketplace"}
)
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"))
# Additional contact and identification fields
contact_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
website: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
external_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
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 adminból. """
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "marketplace"}
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

@@ -0,0 +1,95 @@
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service_request.py
"""
ServiceRequest - Piactér központi tranzakciós modellje.
Epic 7: Marketplace ServiceRequest dedikált modell.
"""
from typing import Optional
from datetime import datetime
from sqlalchemy import String, ForeignKey, Text, DateTime, Numeric, Integer, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from app.database import Base
class ServiceRequest(Base):
"""
Szervizigény (ServiceRequest) tábla.
Egy felhasználó által létrehozott szervizigényt reprezentál, amely lehetővé teszi
a szervizszolgáltatók számára árajánlatok készítését és a tranzakciók lebonyolítását.
"""
__tablename__ = "service_requests"
__table_args__ = (
Index('idx_service_request_status', 'status'),
Index('idx_service_request_user_id', 'user_id'),
Index('idx_service_request_asset_id', 'asset_id'),
Index('idx_service_request_branch_id', 'branch_id'),
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# Idegen kulcsok (Kapcsolódási pontok)
user_id: Mapped[int] = mapped_column(
ForeignKey("identity.users.id", ondelete="CASCADE"),
nullable=False,
index=True,
comment="A szervizigényt létrehozó felhasználó"
)
asset_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("vehicle.assets.id", ondelete="SET NULL"),
nullable=True,
comment="Érintett jármű (opcionális)"
)
branch_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("fleet.branches.id", ondelete="SET NULL"),
nullable=True,
comment="Célzott szerviz (ha van)"
)
# Üzleti logika mezők
status: Mapped[str] = mapped_column(
String(50),
server_default="pending",
index=True,
comment="pending, quoted, accepted, scheduled, completed, cancelled"
)
description: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
comment="A szervizigény részletes leírása"
)
price_estimate: Mapped[Optional[float]] = mapped_column(
Numeric(10, 2),
nullable=True,
comment="Becsült ár (opcionális)"
)
requested_date: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
nullable=True,
comment="Kért szerviz dátum"
)
# Audit
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
comment="Létrehozás időbélyege"
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
comment="Utolsó módosítás időbélyege"
)
# Relationships (opcionális, de ajánlott a lazy loading miatt)
user = relationship("User", back_populates="service_requests", lazy="selectin")
asset = relationship("Asset", back_populates="service_requests", lazy="selectin")
branch = relationship("Branch", back_populates="service_requests", lazy="selectin")
def __repr__(self) -> str:
return f"<ServiceRequest(id={self.id}, status='{self.status}', user_id={self.user_id})>"

View File

@@ -0,0 +1,94 @@
from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float, Text, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
from app.database import Base # MB 2.0 Standard: Központi bázis használata
class StagedVehicleData(Base):
""" Robot 2.1 (Researcher) nyers adatgyűjtője. """
__tablename__ = "staged_vehicle_data"
__table_args__ = {"schema": "system", "extend_existing": True}
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"))
status: Mapped[str] = mapped_column(String(20), default="PENDING", index=True)
error_log: Mapped[Optional[str]] = mapped_column(String)
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 és a Robot 5 (Auditor) naplója.
A séma és a mezők szinkronban az adatbázis audittal.
"""
__tablename__ = "service_staging"
__table_args__ = {"schema": "marketplace", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255), index=True)
# 1. ⚠️ EXTRA OSZLOP: source
source: Mapped[Optional[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)
# Elérhetőségek
city: Mapped[str] = mapped_column(String(100), index=True)
postal_code: Mapped[Optional[str]] = mapped_column(String(10))
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))
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# 2. ⚠️ EXTRA OSZLOP: description
description: Mapped[Optional[str]] = mapped_column(Text)
# 3. ⚠️ EXTRA OSZLOP: submitted_by
submitted_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
# 4. ⚠️ EXTRA OSZLOP: trust_score
trust_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
validation_level: Mapped[int] = mapped_column(Integer, default=40, server_default=text("40"))
# --- Robot 5 (Auditor) technikai mezők ---
# 5. ⚠️ EXTRA OSZLOP: rejection_reason
rejection_reason: Mapped[Optional[str]] = mapped_column(String(500))
# 6. ⚠️ EXTRA OSZLOP: published_at
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
# 7. ⚠️ EXTRA OSZLOP: service_profile_id
service_profile_id: Mapped[Optional[int]] = mapped_column(Integer)
# 8. ⚠️ EXTRA OSZLOP: organization_id
organization_id: Mapped[Optional[int]] = mapped_column(Integer)
# 9. ⚠️ EXTRA OSZLOP: audit_trail
audit_trail: Mapped[Optional[dict]] = mapped_column(JSONB)
# Időbélyegek
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# 10. ⚠️ EXTRA OSZLOP: updated_at
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": "marketplace", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
city: Mapped[str] = mapped_column(String(100), unique=True, index=True)
country_code: Mapped[Optional[str]] = mapped_column(String(2), nullable=True, default="HU")
keyword: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/staged_data.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/staged_data.py
from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float
@@ -22,25 +22,42 @@ class StagedVehicleData(Base):
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. """
""" Robot 1.3 (Scout) által talált nyers szerviz adatok és a Robot 5 (Auditor) naplója. """
__tablename__ = "service_staging"
__table_args__ = {"schema": "system"}
__table_args__ = {"schema": "marketplace"}
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))
source: Mapped[Optional[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)
# Elérhetőségek
city: Mapped[str] = mapped_column(String(100), index=True)
postal_code: Mapped[Optional[str]] = mapped_column(String(10))
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))
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# Beküldés és Bizalom
description: Mapped[Optional[str]] = mapped_column(Text)
submitted_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
trust_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
# Nyers adatok és Státusz
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)
# --- Robot 5 (Auditor) technikai mezők ---
# Ezek kellenek a munka naplózásához
rejection_reason: Mapped[Optional[str]] = mapped_column(String(500))
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
service_profile_id: Mapped[Optional[int]] = mapped_column(Integer)
organization_id: Mapped[Optional[int]] = mapped_column(Integer)
audit_trail: Mapped[Optional[dict]] = mapped_column(JSONB)
# Időbélyegek
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())

View File

@@ -1,4 +1,4 @@
# /app/app/models/reference_data.py
# /opt/docker/dev/service_finder/backend/app/models/reference_data.py
from sqlalchemy import Column, Integer, String, DateTime, func
from sqlalchemy.dialects.postgresql import JSONB
from app.database import Base

View File

@@ -0,0 +1,12 @@
# system package barrel
from .system import SystemParameter, ParameterScope, InternalNotification, SystemServiceStaging
from .audit import SecurityAuditLog, OperationalLog, ProcessLog, FinancialLedger, WalletType, LedgerStatus, LedgerEntryType
from .document import Document
from .translation import Translation
from .legal import LegalDocument, LegalAcceptance
__all__ = [
"SystemParameter", "InternalNotification", "SystemServiceStaging",
"SecurityAuditLog", "ProcessLog", "FinancialLedger", "WalletType", "LedgerStatus", "LedgerEntryType",
"Document", "Translation", "LegalDocument", "LegalAcceptance"
]

View File

@@ -0,0 +1,115 @@
# /opt/docker/dev/service_finder/backend/app/models/system/audit.py
import enum
import uuid
from datetime import datetime
from typing import Any, Optional
from sqlalchemy import String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from app.database import Base
class SecurityAuditLog(Base):
""" Kiemelt biztonsági események és a 4-szem elv naplózása. """
__tablename__ = "security_audit_logs"
__table_args__ = {"schema": "audit"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
action: Mapped[Optional[str]] = mapped_column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST'
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)
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": "audit"}
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"
__table_args__ = {"schema": "audit"}
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 LedgerEntryType(str, enum.Enum):
DEBIT = "DEBIT"
CREDIT = "CREDIT"
class WalletType(str, enum.Enum):
EARNED = "EARNED"
PURCHASED = "PURCHASED"
SERVICE_COINS = "SERVICE_COINS"
VOUCHER = "VOUCHER"
class LedgerStatus(str, enum.Enum):
PENDING = "PENDING"
SUCCESS = "SUCCESS"
FAILED = "FAILED"
REFUNDED = "REFUNDED"
REFUND = "REFUND"
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. Billing Engine alapja. """
__tablename__ = "financial_ledger"
__table_args__ = {"schema": "audit"}
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())
# Új mezők doubleentry és okos levonáshoz
entry_type: Mapped[LedgerEntryType] = mapped_column(
PG_ENUM(LedgerEntryType, name="ledger_entry_type", schema="audit"),
nullable=False
)
balance_after: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
wallet_type: Mapped[Optional[WalletType]] = mapped_column(
PG_ENUM(WalletType, name="wallet_type", schema="audit")
)
# Economy 1: számlázási mezők
issuer_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("finance.issuers.id"), nullable=True)
invoice_status: Mapped[Optional[str]] = mapped_column(String(50), default="PENDING")
tax_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
gross_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
net_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
transaction_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), default=uuid.uuid4, nullable=False, index=True
)
status: Mapped[LedgerStatus] = mapped_column(
PG_ENUM(LedgerStatus, name="ledger_status", schema="audit"),
default=LedgerStatus.SUCCESS,
nullable=False
)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/document.py
# /opt/docker/dev/service_finder/backend/app/models/system/document.py
import uuid
from datetime import datetime
from typing import Optional
@@ -6,7 +6,7 @@ from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, Text
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.base_class import Base
from app.database import Base # MB 2.0: Egységesített Base a szinkronitáshoz
class Document(Base):
""" NAS alapú dokumentumtár metaadatai. """
@@ -35,18 +35,6 @@ class Document(Base):
# =========================================================================
# Probléma: Az `ocr_robot.py` (Robot 3) módosítani próbálta a dokumentumok
# állapotát és menteni akarta az AI eredményeket, de a mezők hiányoztak.
#
# Megoldás: Hozzáadtuk a szükséges mezőket a munkafolyamat (Workflow)
# támogatásához.
#
# 1. `status`: A robot a 'pending_ocr' státuszra szűr. Indexeljük,
# mert a WHERE feltételben szerepel, így az adatbázis sokkal gyorsabb lesz.
#
# 2. `ocr_data`: A kinyert adatokat tárolja. Text típust használunk String
# helyett, mert az AI válasza (pl. JSON formátumú adat) hosszú lehet.
#
# 3. `error_log`: Ha az AI hibázik, vagy üres választ ad, itt rögzítjük
# a hiba okát a könnyebb debuggolás érdekében.
# =========================================================================
status: Mapped[str] = mapped_column(String(50), default="uploaded", index=True)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/legal.py
# /opt/docker/dev/service_finder/backend/app/models/system/legal.py
from datetime import datetime
from typing import Optional
from sqlalchemy import Integer, String, Text, DateTime, ForeignKey, Boolean

View File

@@ -1,9 +1,9 @@
# /opt/docker/dev/service_finder/backend/app/models/system.py
# /opt/docker/dev/service_finder/backend/app/models/system/system.py
import uuid
from datetime import datetime
from datetime import datetime, date
from enum import Enum
from typing import Optional
from sqlalchemy import String, Integer, Boolean, DateTime, text, UniqueConstraint, ForeignKey, Text, Enum as SQLEnum
from sqlalchemy import String, Integer, Boolean, DateTime, text, UniqueConstraint, ForeignKey, Text, Enum as SQLEnum, Date
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.sql import func
@@ -28,7 +28,7 @@ class SystemParameter(Base):
category: Mapped[str] = mapped_column(String, server_default="general", index=True)
value: Mapped[dict] = mapped_column(JSONB, nullable=False)
scope_level: Mapped[ParameterScope] = mapped_column(SQLEnum(ParameterScope, name="parameter_scope"), server_default=ParameterScope.GLOBAL.value, index=True)
scope_level: Mapped[ParameterScope] = mapped_column(SQLEnum(ParameterScope, name="parameter_scope", schema="system"), server_default=ParameterScope.GLOBAL.value, index=True)
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
@@ -49,12 +49,43 @@ class InternalNotification(Base):
title: Mapped[str] = mapped_column(String(255), nullable=False)
message: Mapped[str] = mapped_column(Text, nullable=False)
category: Mapped[str] = mapped_column(String(50), server_default="info") # insurance, mot, service, legal
priority: Mapped[str] = mapped_column(String(20), server_default="medium") # low, medium, high, critical
category: Mapped[str] = mapped_column(String(50), server_default="info")
priority: Mapped[str] = mapped_column(String(20), server_default="medium")
read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
is_read: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
class SystemServiceStaging(Base):
""" Robot 1.3 (Scout) által talált nyers szerviz adatok. """
__tablename__ = "service_staging"
__table_args__ = {"schema": "system"}
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)
# Metaadatok a gyors eléréshez (melyik autó, melyik VIN)
data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
postal_code: Mapped[Optional[str]] = mapped_column(String(20), 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))
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
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())
# JAVÍTÁS: Ezeket az oszlopokat vissza kell tenni, mert az audit szerint
# az adatbázisban léteznek a system.service_staging táblában.
read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/translation.py
# /opt/docker/dev/service_finder/backend/app/models/system/translation.py
from sqlalchemy import String, Integer, Text, Boolean, text
from sqlalchemy.orm import Mapped, mapped_column

View File

@@ -0,0 +1,63 @@
# vehicle package exports
from .vehicle_definitions import (
VehicleModelDefinition,
VehicleType,
FeatureDefinition,
ModelFeatureMap,
)
from .vehicle import (
CostCategory,
VehicleCost,
VehicleOdometerState,
VehicleUserRating,
GbCatalogDiscovery,
)
from .external_reference import ExternalReferenceLibrary
from .external_reference_queue import ExternalReferenceQueue
from .asset import (
Asset,
AssetCatalog,
AssetCost,
AssetEvent,
AssetFinancials,
AssetTelemetry,
AssetReview,
ExchangeRate,
CatalogDiscovery,
VehicleOwnership,
)
from .history import AuditLog, LogSeverity
# --- ÚJ MOTOROS SPECIFIKÁCIÓ MODELL BEEMELÉSE ---
from .motorcycle_specs import MotorcycleSpecs
__all__ = [
"VehicleModelDefinition",
"VehicleType",
"FeatureDefinition",
"ModelFeatureMap",
"CostCategory",
"VehicleCost",
"VehicleOdometerState",
"VehicleUserRating",
"GbCatalogDiscovery",
"ExternalReferenceLibrary",
"ExternalReferenceQueue",
"Asset",
"AssetCatalog",
"AssetCost",
"AssetEvent",
"AssetFinancials",
"AssetTelemetry",
"AssetReview",
"ExchangeRate",
"CatalogDiscovery",
"VehicleOwnership",
"AuditLog",
"LogSeverity",
# --- EXPORT LISTA KIEGÉSZÍTÉSE ---
"MotorcycleSpecs",
]

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/asset.py
# /opt/docker/dev/service_finder/backend/app/models/vehicle/asset.py
from __future__ import annotations
import uuid
from datetime import datetime
@@ -80,6 +80,12 @@ class Asset(Base):
assignments: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="asset")
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="asset")
# --- COMPUTED PROPERTIES (for Pydantic schema compatibility) ---
@property
def is_verified(self) -> bool:
"""Always False for now, as verification is not yet implemented."""
return False
class AssetFinancials(Base):
""" I. Beszerzés és IV. Értékcsökkenés (Amortizáció). """
__tablename__ = "asset_financials"

View File

@@ -0,0 +1,36 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle/external_reference.py
from sqlalchemy import Column, Integer, String, JSON, DateTime, UniqueConstraint, ForeignKey
from sqlalchemy.sql import func
from app.database import Base
class ExternalReferenceLibrary(Base):
__tablename__ = "external_reference_library"
__table_args__ = (
UniqueConstraint('source_url', name='_source_url_uc'),
{"schema": "vehicle"}
)
id = Column(Integer, primary_key=True, index=True)
source_name = Column(String(50), default="auto-data.net") # Később jöhet más forrás is (motorokhoz/kamionokhoz)
make = Column(String(100), index=True)
model = Column(String(100), index=True)
generation = Column(String(255))
modification = Column(String(255))
year_from = Column(Integer)
year_to = Column(Integer, nullable=True)
power_kw = Column(Integer, index=True)
engine_cc = Column(Integer, index=True)
category = Column(String(20), default='car', index=True) # ÚJ
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Minden egyéb technikai adat (olaj, gumi, fogyasztás stb.) ide megy
specifications = Column(JSON, default={})
source_url = Column(String(500), unique=True)
last_scraped_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
pipeline_status = Column(String(30), default='pending_enrich', index=True)
matched_vmd_id = Column(Integer, ForeignKey('vehicle.vehicle_model_definitions.id'), nullable=True, index=True)
# Biztosítjuk, hogy ne legyen duplikáció azonos linkről

View File

@@ -0,0 +1,33 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle/external_reference_queue.py
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, text
from sqlalchemy.sql import func
from app.database import Base
class ExternalReferenceQueue(Base):
__tablename__ = "auto_data_crawler_queue"
__table_args__ = {"schema": "vehicle"}
id = Column(Integer, primary_key=True, index=True)
url = Column(String(500), unique=True, nullable=False)
# Szintek: 'brand', 'model', 'generation', 'engine'
level = Column(String(20), nullable=False, index=True)
# Kategóriák
category = Column(String(20), default='car', index=True)
# Szülő azonosító (pl. a modell tudja, melyik márkához tartozik)
parent_id = Column(Integer, nullable=True)
# Megjelenítési név (pl. "Audi", "A3 Sportback")
name = Column(String(255))
# Állapot: 'pending', 'processing', 'completed', 'error'
status = Column(String(20), default='pending', index=True)
# Hibakezeléshez
error_msg = Column(String(1000), nullable=True)
retry_count = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/history.py
# /opt/docker/dev/service_finder/backend/app/models/vehicle/history.py
import uuid
import enum
from datetime import datetime, date

View File

@@ -0,0 +1,35 @@
from sqlalchemy import Column, Integer, Text, ForeignKey, DateTime
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
from app.database import Base
class MotorcycleSpecs(Base):
"""
Gondolatmenet: Ez a modell reprezentálja a motorok végleges technikai adatait.
A JSONB mező lehetővé teszi, hogy az AutoEvolution-ról lekerülő összes változatos
adatot (hengerűrtartalom, nyomaték, hűtés, stb.) sémakötöttség nélkül tároljuk.
"""
__tablename__ = "motorcycle_specs"
__table_args__ = {"schema": "vehicle"}
id = Column(Integer, primary_key=True, index=True)
# Kapcsolat a crawler várólistájával
crawler_id = Column(
Integer,
ForeignKey("vehicle.auto_data_crawler_queue.id", ondelete="CASCADE"),
unique=True,
nullable=False
)
full_name = Column(Text, nullable=False)
url = Column(Text)
# A lényeg: ide kerül minden technikai adat kulcs-érték párban
raw_data = Column(JSONB, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
def __repr__(self):
return f"<MotorcycleSpecs(name='{self.full_name}', crawler_id={self.crawler_id})>"

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle.py
# /opt/docker/dev/service_finder/backend/app/models/vehicle/vehicle.py
"""
TCO (Total Cost of Ownership) alapmodelljei a 'vehicle' sémában.
- CostCategory: Standardizált költségkategóriák hierarchiája
@@ -189,4 +189,15 @@ class VehicleUserRating(Base):
def average_score(self) -> float:
"""Számított átlagpontszám a 4 dimenzióból."""
scores = [self.driving_experience, self.reliability, self.comfort, self.consumption_satisfaction]
return sum(scores) / 4.0
return sum(scores) / 4.0
class GbCatalogDiscovery(Base):
__tablename__ = "gb_catalog_discovery"
__table_args__ = {"schema": "vehicle"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
vrm: Mapped[str] = mapped_column(String(20), unique=True)
make: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
model: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
status: Mapped[str] = mapped_column(String(20), default='pending')
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,15 +1,19 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle_definitions.py
# /opt/docker/dev/service_finder/backend/app/models/vehicle/vehicle_definitions.py
from __future__ import annotations
from datetime import datetime
from typing import Optional, List
from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, text, JSON, Index, UniqueConstraint, Text, ARRAY, func, Numeric
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
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
# Típus ellenőrzés a körkörös importok elkerülésére
if TYPE_CHECKING:
from .asset import AssetCatalog
from .vehicle import VehicleCost, VehicleOdometerState, VehicleUserRating
class VehicleType(Base):
""" Jármű kategóriák (pl. Személyautó, Motorkerékpár, Teherautó, Hajó) """
__tablename__ = "vehicle_types"
@@ -42,109 +46,100 @@ class FeatureDefinition(Base):
class VehicleModelDefinition(Base):
market: Mapped[str] = mapped_column(String(20), server_default=text("'GLOBAL'"), index=True)
"""
Robot v1.1.0 Multi-Tier MDM Master Adattábla.
Az ökoszisztéma technikai igazságforrása.
"""
__tablename__ = "vehicle_model_definitions"
__table_args__ = {"schema": "vehicle"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
market: Mapped[str] = mapped_column(String(20), server_default=text("'EU'"), index=True) # GLOBÁLIS helyett EU az alap
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)
marketing_name: Mapped[str] = mapped_column(String(255), index=True)
official_marketing_name: Mapped[Optional[str]] = mapped_column(String(255))
# --- ROBOT LOGIKAI MEZŐK (JAVÍTVA 2.0 STÍLUSBAN) ---
# --- ROBOT LOGIKAI MEZŐK ---
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())
priority_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
# --- 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
normalized_name: Mapped[str] = mapped_column(String(255), index=True) # EZT KÖTELEZŐVÉ TETTÜK
marketing_name_aliases: Mapped[dict] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
engine_code: Mapped[Optional[str]] = mapped_column(String(50), index=True)
# --- 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)
technical_code: Mapped[str] = mapped_column(String(100), index=True, server_default=text("'UNKNOWN'"))
variant_code: Mapped[str] = mapped_column(String(100), index=True, server_default=text("'UNKNOWN'"))
version_code: Mapped[str] = mapped_column(String(100), index=True, server_default=text("'UNKNOWN'"))
# --- ÚJ PRÉMIUM MŰSZAKI MEZŐK ---
type_approval_number: Mapped[Optional[str]] = mapped_column(String(100), index=True) # e1*2001/...
seats: Mapped[Optional[int]] = mapped_column(Integer)
width: Mapped[Optional[int]] = mapped_column(Integer) # cm
wheelbase: Mapped[Optional[int]] = mapped_column(Integer) # cm
list_price: Mapped[Optional[int]] = mapped_column(Integer) # EUR (catalogusprijs)
max_speed: Mapped[Optional[int]] = mapped_column(Integer) # km/h
# Vontatási adatok
towing_weight_unbraked: Mapped[Optional[int]] = mapped_column(Integer)
towing_weight_braked: Mapped[Optional[int]] = mapped_column(Integer)
# Környezetvédelmi adatok
fuel_consumption_combined: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True)
co2_emissions_combined: Mapped[Optional[int]] = mapped_column(Integer)
# --- MŰSZAKI MEZŐK ---
type_approval_number: Mapped[Optional[str]] = mapped_column(String(100), index=True)
seats: Mapped[int] = mapped_column(Integer, server_default=text("0"))
width: Mapped[int] = mapped_column(Integer, server_default=text("0"))
wheelbase: Mapped[int] = mapped_column(Integer, server_default=text("0"))
list_price: Mapped[int] = mapped_column(Integer, server_default=text("0"))
max_speed: Mapped[int] = mapped_column(Integer, server_default=text("0"))
towing_weight_unbraked: Mapped[int] = mapped_column(Integer, server_default=text("0"))
towing_weight_braked: Mapped[int] = mapped_column(Integer, server_default=text("0"))
fuel_consumption_combined: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), server_default=text("0.0"))
co2_emissions_combined: Mapped[int] = mapped_column(Integer, server_default=text("0"))
# --- SPECIFIKÁCIÓK ---
vehicle_type_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("vehicle.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)
fuel_type: Mapped[str] = mapped_column(String(50), index=True, server_default=text("'Unknown'"))
trim_level: Mapped[str] = mapped_column(String(100), server_default=text("''"))
engine_capacity: Mapped[int] = mapped_column(Integer, server_default=text("0"), index=True)
power_kw: Mapped[int] = mapped_column(Integer, server_default=text("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)
curb_weight: Mapped[int] = mapped_column(Integer, server_default=text("0"))
max_weight: Mapped[int] = mapped_column(Integer, server_default=text("0"))
euro_classification: Mapped[Optional[str]] = mapped_column(String(20))
doors: Mapped[Optional[int]] = mapped_column(Integer)
doors: Mapped[int] = mapped_column(Integer, server_default=text("0"))
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)
# --- ÉLETCIKLUS ---
year_from: Mapped[int] = mapped_column(Integer, index=True, server_default=text("0")) # EZT IS BELETETTÜK A KULCSBA
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
production_status: Mapped[Optional[str]] = mapped_column(String(50))
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))
is_manual: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
source: Mapped[str] = mapped_column(String(100), server_default=text("'ROBOT'"))
# --- ADAT-KONTÉNEREK ---
raw_search_context: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
# --- ADATOK ---
raw_search_context: Mapped[str] = mapped_column(Text, server_default=text("''")) # JSONB helyett TEXT a keresési adatoknak!
raw_api_data: 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
specifications: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
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__ = (
# A LEGONTOSABB SOR: Ez határozza meg, mi számít duplikációnak!
UniqueConstraint('make', 'normalized_name', 'variant_code', 'version_code', 'fuel_type', 'market', 'year_from', name='uix_vmd_precision_v2'),
Index('idx_vmd_lookup_fast', 'make', 'normalized_name'),
Index('idx_vmd_engine_bridge', 'make', 'engine_code'),
{"schema": "vehicle"}
)
# KAPCSOLATOK
# --- KAPCSOLATOK (Relationships) ---
v_type_rel: Mapped["VehicleType"] = relationship("VehicleType", back_populates="definitions")
feature_maps: Mapped[List["ModelFeatureMap"]] = relationship("ModelFeatureMap", back_populates="model_definition")
# Hivatkozás az asset.py-ban lévő osztályra
# Megjegyzés: Ha az AssetCatalog nincs itt importálva, húzzal adjuk meg a neve
variants: Mapped[List["AssetCatalog"]] = relationship("AssetCatalog", back_populates="master_definition")
# TCO költségnapló kapcsolata
costs: Mapped[List["VehicleCost"]] = relationship("VehicleCost", back_populates="vehicle")
# Kilométeróra állapot kapcsolata
odometer_state: Mapped["VehicleOdometerState"] = relationship("VehicleOdometerState", back_populates="vehicle")
# JAVÍTÁS: Ez a sor hiányzott az API indításához!
ratings: Mapped[List["VehicleUserRating"]] = relationship("VehicleUserRating", back_populates="vehicle", cascade="all, delete-orphan")
costs: Mapped[List["VehicleCost"]] = relationship("VehicleCost", back_populates="vehicle", cascade="all, delete-orphan")
odometer_state: Mapped[Optional["VehicleOdometerState"]] = relationship("VehicleOdometerState", back_populates="vehicle", uselist=False, cascade="all, delete-orphan")
class ModelFeatureMap(Base):

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
# /opt/docker/dev/service_finder/backend/app/schemas/admin.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, delete
@@ -8,8 +8,8 @@ from datetime import datetime, timedelta
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.models import SecurityAuditLog, OperationalLog
from app.models 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

View File

@@ -2,7 +2,7 @@
from pydantic import BaseModel, ConfigDict
from datetime import datetime
from typing import Optional, Any, Dict
from app.models.security import ActionStatus
from app.models import ActionStatus
class PendingActionResponse(BaseModel):
id: int

View File

@@ -53,4 +53,12 @@ class AssetResponse(BaseModel):
created_at: datetime
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
class AssetCreate(BaseModel):
""" Jármű létrehozásához szükséges adatok. """
vin: str = Field(..., min_length=17, max_length=17, description="VIN szám (17 karakter)")
license_plate: str = Field(..., min_length=2, max_length=20, description="Rendszám")
catalog_id: Optional[int] = Field(None, description="Opcionális katalógus ID (ha ismert a modell)")
organization_id: int = Field(..., description="Szervezet ID, amelyhez a jármű tartozik")

View File

@@ -18,6 +18,9 @@ class UserLiteRegister(BaseModel):
password: str = Field(..., min_length=8, description="Minimum 8 karakter hosszú jelszó")
first_name: str
last_name: str
region_code: Optional[str] = "HU"
lang: Optional[str] = "hu"
timezone: Optional[str] = "Europe/Budapest"
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,25 @@
-- ==========================================
-- MOTOROS TECHNIKAI ADATOK NYILVÁNTARTÁSA
-- ==========================================
-- 1. Séma biztosítása
CREATE SCHEMA IF NOT EXISTS vehicle;
-- 2. A kinyert specifikációk táblája
-- Ez a tábla tárolja az R4 által parszolt adatokat JSONB formátumban.
CREATE TABLE IF NOT EXISTS vehicle.motorcycle_specs (
id SERIAL PRIMARY KEY,
crawler_id INTEGER UNIQUE REFERENCES vehicle.auto_data_crawler_queue(id) ON DELETE CASCADE,
full_name TEXT NOT NULL,
raw_data JSONB NOT NULL, -- Rugalmas tárolás minden technikai paraméternek
url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 3. Teljesítmény-indexek
-- Segít, ha később a JSON-on belül akarunk keresni (pl. lóerő alapján)
CREATE INDEX IF NOT EXISTS idx_motorcycle_specs_raw_data ON vehicle.motorcycle_specs USING GIN (raw_data);
CREATE INDEX IF NOT EXISTS idx_motorcycle_specs_full_name ON vehicle.motorcycle_specs(full_name);
COMMENT ON TABLE vehicle.motorcycle_specs IS 'Az R4-es robot által kinyert végleges motoros műszaki adatok.';

View File

@@ -0,0 +1,36 @@
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime, date
class SeasonResponse(BaseModel):
id: int
name: str
start_date: date
end_date: date
is_active: bool
class Config:
from_attributes = True
class UserStatResponse(BaseModel):
user_id: int
total_xp: int
current_level: int
restriction_level: int
penalty_quota_remaining: int
banned_until: Optional[datetime]
class Config:
from_attributes = True
class LeaderboardEntry(BaseModel):
user_id: int
username: str # email or person name
total_xp: int
current_level: int
class Config:
from_attributes = True

View File

@@ -6,7 +6,7 @@ from datetime import datetime
from typing import Optional, Dict, Any
from pydantic import BaseModel, Field
from app.models.security import ActionStatus
from app.models import ActionStatus
# --- Request schemas ---

View File

@@ -2,7 +2,7 @@ import uuid # HOZZÁADVA
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from datetime import datetime
from app.models.social import ModerationStatus, SourceType
from app.models import ModerationStatus, SourceType
# --- Alap Sémák (Szolgáltatók) ---

View File

@@ -0,0 +1,30 @@
# /opt/docker/dev/service_finder/backend/app/schemas/system.py
from pydantic import BaseModel, ConfigDict
from typing import Dict, Any, Optional
from datetime import datetime
class SystemParameterBase(BaseModel):
description: Optional[str] = None
value: Dict[str, Any] # JSONB mező
scope_level: str = 'global'
scope_id: Optional[str] = None
is_active: bool = True
class SystemParameterCreate(SystemParameterBase):
key: str
class SystemParameterUpdate(BaseModel):
description: Optional[str] = None
value: Optional[Dict[str, Any]] = None
is_active: Optional[bool] = None
class SystemParameterResponse(SystemParameterBase):
id: int
key: str
updated_at: datetime
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,22 @@
import sys
from sqlalchemy.orm import configure_mappers
# Az összes modell importálása
from app.models.identity import *
from app.models.vehicle import *
from app.models.marketplace import *
# from app.models.fleet import * # Nincs fleet modul
from app.models.gamification import *
from app.models.system import *
def check_all_mappers():
try:
configure_mappers()
print("\n✅ [SUCCESS] Minden SQLAlchemy Mapper és Relationship 100%-ig hibátlanül felépült!")
sys.exit(0)
except Exception as e:
print(f"\n❌ [ERROR] Mapper inicializálási hiba:\n{e}")
sys.exit(1)
if __name__ == "__main__":
check_all_mappers()

View File

@@ -0,0 +1,439 @@
#!/usr/bin/env python3
"""
Robot Health & Integrity Audit Script - Recursive Deep Integrity Audit
Ez a szkript automatikusan diagnosztizálja az összes robotunk (Scout, Enricher, Validator, Auditor)
üzembiztonságát rekurzív felfedezéssel. A következő ellenőrzéseket végzi el:
1. Auto-Discovery: Rekurzívan bejárja a `backend/app/workers/` teljes könyvtárszerkezetét
2. Identification: Minden `.py` fájlt, ami nem `__init__.py` és nem segédfájl, kezel robotként/worker-ként
3. Deep Import Test: Megpróbálja importálni mindet, különös figyelemmel a kritikus modulokra
4. Model Sync 2.0: Ellenőrzi, hogy az összes robot a helyes modelleket használja-e
5. Interface Standardizálás: Ellenőrzi a `run()` metódus jelenlétét
6. Kategorizált jelentés: Service, Vehicle General, Vehicle Special, System & OCR kategóriák
"""
import sys
import importlib
import inspect
import asyncio
from pathlib import Path
from typing import List, Dict, Any, Tuple
import logging
import re
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s')
logger = logging.getLogger("Robot-Integrity-Audit")
# Root directory for workers (relative to backend/app)
WORKERS_ROOT = Path(__file__).parent.parent / "workers"
# Exclusion patterns for non-robot files
EXCLUDE_PATTERNS = [
"__init__.py",
"__pycache__",
".pyc",
"test_",
"mapping_",
"config",
"dictionary",
"rules",
"report",
"monitor_",
"py_to_database",
"README",
# Files with dots in name (not valid Python module names)
r".*\..*\.py", # Matches files like "something.1.0.py"
]
# Categorization patterns
CATEGORY_PATTERNS = {
"Service Robots": [
r"service_robot_\d+",
r"service/.*\.py$",
],
"Vehicle General": [
r"vehicle_robot_[0-4]_.*",
r"R[0-4]_.*\.py$",
r"vehicle_robot_1_[245]_.*", # NHTSA, Heavy EU, GB
r"vehicle_robot_2_.*", # RDW, AutoData
],
"Vehicle Special": [
r"bike_.*\.py$",
r"vehicle_ultimate_.*\.py$",
r"ultimatespecs/.*\.py$",
],
"System & OCR": [
r"system_.*\.py$",
r"subscription_.*\.py$",
r"ocr/.*\.py$",
],
}
def discover_robot_files() -> List[Tuple[str, Path, str]]:
"""
Recursively discover all robot files in the workers directory.
Returns list of (module_name, file_path, category) tuples.
"""
robot_files = []
for py_file in WORKERS_ROOT.rglob("*.py"):
# Skip excluded files
file_name = py_file.name
# Check for simple pattern matches
skip = False
for pattern in EXCLUDE_PATTERNS:
if pattern.startswith('r.') and len(pattern) > 2:
# Regex pattern (simplified)
if re.match(pattern[2:], file_name):
skip = True
break
elif pattern in file_name:
skip = True
break
# Also skip files with multiple dots in name (not valid Python modules)
if file_name.count('.') > 1: # e.g., "something.1.0.py"
skip = True
if skip:
continue
# Skip directories
if not py_file.is_file():
continue
# Calculate module name (relative to backend/app)
try:
rel_path = py_file.relative_to(Path(__file__).parent.parent)
# Convert path parts to module names, handling dots in filenames
module_parts = []
for part in rel_path.parts:
if part.endswith('.py'):
part = part[:-3] # Remove .py
# Replace dots with underscores in filename (e.g., "1.0" -> "1_0")
part = part.replace('.', '_')
module_parts.append(part)
# Add 'app' prefix since we're in backend/app directory
module_name = "app." + ".".join(module_parts)
# Determine category
category = "Uncategorized"
for cat_name, patterns in CATEGORY_PATTERNS.items():
for pattern in patterns:
if re.search(pattern, str(rel_path), re.IGNORECASE):
category = cat_name
break
if category != "Uncategorized":
break
robot_files.append((module_name, py_file, category))
except ValueError as e:
logger.warning(f"Could not determine module for {py_file}: {e}")
# Sort by category and module name
robot_files.sort(key=lambda x: (x[2], x[0]))
return robot_files
async def test_import(module_name: str) -> Tuple[bool, str]:
"""Try to import a robot module and return (success, error_message)."""
try:
module = importlib.import_module(module_name)
logger.info(f"{module_name} import successful")
return True, ""
except ImportError as e:
error_msg = f"ImportError: {e}"
logger.error(f"{module_name} import failed: {e}")
return False, error_msg
except SyntaxError as e:
error_msg = f"SyntaxError at line {e.lineno}: {e.msg}"
logger.error(f"{module_name} syntax error: {e}")
return False, error_msg
except Exception as e:
error_msg = f"Exception: {type(e).__name__}: {e}"
logger.error(f"{module_name} import failed: {e}")
return False, error_msg
async def check_model_sync(module_name: str) -> List[str]:
"""Check if a robot uses correct model references."""
errors = []
try:
module = importlib.import_module(module_name)
# Get all classes in the module
classes = [cls for name, cls in inspect.getmembers(module, inspect.isclass)
if not name.startswith('_')]
for cls in classes:
# Check class source code for model references
try:
source = inspect.getsource(cls)
# Look for common model name issues
old_patterns = [
r"VehicleModelDefinitions", # Plural mistake
r"vehicle_model_definitions", # Old table name
r"ExternalReferenceQueues", # Plural mistake
]
for pattern in old_patterns:
if re.search(pattern, source):
errors.append(f"⚠️ {module_name}.{cls.__name__} uses old pattern: {pattern}")
except (OSError, TypeError):
pass # Can't get source for built-in or C extensions
except Exception as e:
# If we can't import, this will be caught in import test
pass
return errors
async def test_robot_interface(module_name: str) -> Tuple[bool, List[str]]:
"""Test if a robot has a proper interface (run method, etc.)."""
interface_issues = []
try:
module = importlib.import_module(module_name)
# Find the main robot class (usually ends with the module name or contains 'Robot')
classes = [cls for name, cls in inspect.getmembers(module, inspect.isclass)
if not name.startswith('_')]
if not classes:
interface_issues.append("No classes found")
return False, interface_issues
main_class = None
for cls in classes:
cls_name = cls.__name__
# Heuristic: class name contains 'Robot' or matches file name pattern
if 'Robot' in cls_name or cls_name.lower().replace('_', '') in module_name.lower().replace('_', ''):
main_class = cls
break
if main_class is None:
main_class = classes[0] # Fallback to first class
# Check for run/execute/process method (can be classmethod or instance method)
has_run_method = hasattr(main_class, 'run')
has_execute_method = hasattr(main_class, 'execute')
has_process_method = hasattr(main_class, 'process')
if not (has_run_method or has_execute_method or has_process_method):
interface_issues.append(f"No run/execute/process method in {main_class.__name__}")
else:
# Log which method is found
if has_run_method:
run_method = getattr(main_class, 'run')
# Check if it's a classmethod or instance method
if inspect.ismethod(run_method) and run_method.__self__ is main_class:
logger.debug(f"{module_name}.{main_class.__name__}.run is classmethod")
elif inspect.iscoroutinefunction(run_method):
logger.debug(f"{module_name}.{main_class.__name__}.run is async")
else:
logger.debug(f" {module_name}.{main_class.__name__}.run is sync")
# Try to instantiate only if the class appears to be instantiable (not abstract)
# Check if class has __init__ that doesn't require special arguments
try:
# First check if class can be instantiated with no arguments
sig = inspect.signature(main_class.__init__)
params = list(sig.parameters.keys())
# If only 'self' parameter, it's instantiable
if len(params) == 1: # only self
instance = main_class()
interface_issues.append(f"Instantiation successful")
else:
interface_issues.append(f"Instantiation requires arguments, skipping")
except (TypeError, AttributeError):
# __init__ may not be standard, try anyway
try:
instance = main_class()
interface_issues.append(f"Instantiation successful")
except Exception as e:
interface_issues.append(f"Instantiation failed (expected): {e}")
# If we found at least one of the required methods, consider interface OK
interface_ok = has_run_method or has_execute_method or has_process_method
return interface_ok, interface_issues
except Exception as e:
interface_issues.append(f"Interface test error: {e}")
return False, interface_issues
async def check_syntax_errors(file_path: Path) -> List[str]:
"""Check for syntax errors by attempting to compile the file."""
errors = []
try:
with open(file_path, 'r', encoding='utf-8') as f:
source = f.read()
compile(source, str(file_path), 'exec')
except SyntaxError as e:
errors.append(f"Syntax error at line {e.lineno}: {e.msg}")
except Exception as e:
errors.append(f"Compilation error: {e}")
return errors
async def generate_categorized_report(results: Dict) -> str:
"""Generate a categorized audit report."""
report_lines = []
report_lines.append("# 🤖 Robot Integrity Audit Report")
report_lines.append(f"Generated: {importlib.import_module('datetime').datetime.now().isoformat()}")
report_lines.append(f"Total robots discovered: {results['total_robots']}")
report_lines.append("")
for category in ["Service Robots", "Vehicle General", "Vehicle Special", "System & OCR", "Uncategorized"]:
cat_robots = [r for r in results['robots'] if r['category'] == category]
if not cat_robots:
continue
report_lines.append(f"## {category}")
report_lines.append(f"**Count:** {len(cat_robots)}")
# Statistics
import_success = sum(1 for r in cat_robots if r['import_success'])
syntax_success = sum(1 for r in cat_robots if not r['syntax_errors'])
interface_ok = sum(1 for r in cat_robots if r['interface_ok'])
report_lines.append(f"- Import successful: {import_success}/{len(cat_robots)}")
report_lines.append(f"- Syntax clean: {syntax_success}/{len(cat_robots)}")
report_lines.append(f"- Interface OK: {interface_ok}/{len(cat_robots)}")
# List problematic robots
problematic = [r for r in cat_robots if not r['import_success'] or r['syntax_errors'] or not r['interface_ok']]
if problematic:
report_lines.append("\n**Problematic robots:**")
for robot in problematic:
issues = []
if not robot['import_success']:
issues.append("Import failed")
if robot['syntax_errors']:
issues.append(f"Syntax errors ({len(robot['syntax_errors'])})")
if not robot['interface_ok']:
issues.append("Interface issues")
report_lines.append(f"- `{robot['module']}`: {', '.join(issues)}")
report_lines.append("")
# Summary
report_lines.append("## 📊 Summary")
report_lines.append(f"- **Total robots:** {results['total_robots']}")
report_lines.append(f"- **Import successful:** {results['import_success']}/{results['total_robots']}")
report_lines.append(f"- **Syntax clean:** {results['syntax_clean']}/{results['total_robots']}")
report_lines.append(f"- **Interface OK:** {results['interface_ok']}/{results['total_robots']}")
# Critical issues
critical = [r for r in results['robots'] if not r['import_success']]
if critical:
report_lines.append("\n## 🚨 Critical Issues (Import Failed)")
for robot in critical:
report_lines.append(f"- `{robot['module']}`: {robot['import_error']}")
return "\n".join(report_lines)
async def main():
"""Main audit function with recursive discovery."""
logger.info("🤖 Starting Recursive Deep Integrity Audit")
logger.info("=" * 60)
# Discover all robot files
logger.info("\n🔍 STEP 1: Discovering robot files...")
robot_files = discover_robot_files()
if not robot_files:
logger.error("❌ No robot files found!")
return False
logger.info(f"📁 Found {len(robot_files)} robot files")
results = {
'robots': [],
'total_robots': len(robot_files),
'import_success': 0,
'syntax_clean': 0,
'interface_ok': 0,
}
# Process each robot
logger.info("\n📦 STEP 2: Import and syntax tests...")
logger.info("-" * 40)
for i, (module_name, file_path, category) in enumerate(robot_files, 1):
logger.info(f"\n[{i}/{len(robot_files)}] Testing: {module_name} ({category})")
# Check syntax first
syntax_errors = await check_syntax_errors(file_path)
# Test import
import_success, import_error = await test_import(module_name)
# Test interface
interface_ok, interface_issues = await test_robot_interface(module_name)
# Check model sync
model_errors = await check_model_sync(module_name)
robot_result = {
'module': module_name,
'file': str(file_path),
'category': category,
'import_success': import_success,
'import_error': import_error,
'syntax_errors': syntax_errors,
'interface_ok': interface_ok,
'interface_issues': interface_issues,
'model_errors': model_errors,
}
results['robots'].append(robot_result)
if import_success:
results['import_success'] += 1
if not syntax_errors:
results['syntax_clean'] += 1
if interface_ok:
results['interface_ok'] += 1
# Log summary for this robot
status_symbol = "" if import_success and not syntax_errors else ""
logger.info(f"{status_symbol} {module_name}: Import={import_success}, Syntax={len(syntax_errors)} errors, Interface={interface_ok}")
# Generate report
logger.info("\n📊 STEP 3: Generating categorized report...")
report = await generate_categorized_report(results)
# Print summary to console
logger.info("\n" + "=" * 60)
logger.info("📊 AUDIT SUMMARY")
logger.info("=" * 60)
logger.info(f"Total robots discovered: {results['total_robots']}")
logger.info(f"Import successful: {results['import_success']}/{results['total_robots']}")
logger.info(f"Syntax clean: {results['syntax_clean']}/{results['total_robots']}")
logger.info(f"Interface OK: {results['interface_ok']}/{results['total_robots']}")
# Save report to file
report_path = Path(__file__).parent.parent.parent / "audit_report_robots.md"
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report)
logger.info(f"\n📄 Full report saved to: {report_path}")
# Determine overall status
critical_count = sum(1 for r in results['robots'] if not r['import_success'])
if critical_count > 0:
logger.error(f"🚨 ROBOT INTEGRITY CHECK FAILED - {critical_count} critical issues found!")
return False
elif results['import_success'] < results['total_robots']:
logger.warning("⚠️ ROBOT INTEGRITY CHECK PASSED with warnings")
return True
else:
logger.info("✅ ROBOT INTEGRITY CHECK PASSED - All systems operational!")
return True
if __name__ == "__main__":
success = asyncio.run(main())
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
Check tables in system and gamification schemas.
"""
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
async def check():
from app.core.config import settings
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
async with engine.begin() as conn:
# List tables
result = await conn.execute(text("""
SELECT table_schema, table_name,
(SELECT count(*) FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name) as column_count
FROM information_schema.tables t
WHERE table_name IN ('competitions', 'user_scores')
ORDER BY table_schema;
"""))
rows = result.fetchall()
print("Tables found:")
for row in rows:
print(f" {row.table_schema}.{row.table_name} ({row.column_count} columns)")
# Count rows
count_result = await conn.execute(text(f'SELECT COUNT(*) FROM "{row.table_schema}"."{row.table_name}"'))
count = count_result.scalar()
print(f" Rows: {count}")
# Check foreign keys
result = await conn.execute(text("""
SELECT conname, conrelid::regclass as source_table, confrelid::regclass as target_table
FROM pg_constraint
WHERE contype = 'f'
AND (conrelid::regclass::text LIKE '%competitions%' OR conrelid::regclass::text LIKE '%user_scores%'
OR confrelid::regclass::text LIKE '%competitions%' OR confrelid::regclass::text LIKE '%user_scores%');
"""))
fks = result.fetchall()
print("\nForeign keys involving these tables:")
for fk in fks:
print(f" {fk.conname}: {fk.source_table} -> {fk.target_table}")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(check())

View File

@@ -0,0 +1,48 @@
import asyncio
import json
from app.database import AsyncSessionLocal
from sqlalchemy import text
async def repair_cars():
async with AsyncSessionLocal() as db:
# Javított lekérdezés: make, model és year oszlopokat használunk name helyett
query = text("""
SELECT id, make, model, year, url
FROM vehicle.catalog_discovery
WHERE status = 'incomplete' OR status = 'pending'
ORDER BY id ASC
LIMIT 5
""")
try:
res = await db.execute(query)
cars = res.fetchall()
if not cars:
print("✨ Nincs több javítandó autó a listában!")
return
for car_id, make, model, year, url in cars:
full_name = f"{year} {make} {model}"
print(f"\n🚗 JÁRMŰ: {full_name}")
print(f"🔗 LINK: {url}")
print("-" * 30)
# Itt írhatod be a hiányzó adatokat
val = input("Írd be a műszaki adatokat (pl. '150 HP, 1998cc') vagy 'skip': ")
if val.lower() != 'skip':
# A JSONB mezőt frissítjük a kézi javítással
data_update = {"manual_fix": val}
await db.execute(text("""
UPDATE vehicle.catalog_discovery
SET raw_data = raw_data || :data, status = 'ready_for_catalog'
WHERE id = :id
"""), {"data": json.dumps(data_update), "id": car_id})
await db.commit()
print(f"{full_name} mentve és kész a katalógusba tolásra!")
except Exception as e:
print(f"❌ Hiba történt: {e}")
if __name__ == "__main__":
asyncio.run(repair_cars())

View File

@@ -0,0 +1,292 @@
-- Database cleanup script for Service Finder identity tables
-- WARNING: This will delete ALL users and persons, reset sequences, and create fresh admin users.
-- Only run this in development environments with explicit approval from the Owner.
-- 1. Disable foreign key checks temporarily (PostgreSQL doesn't support, but we can use TRUNCATE CASCADE)
-- Instead we'll use TRUNCATE with CASCADE which automatically handles dependent tables.
BEGIN;
-- 2. Truncate identity tables and restart identity sequences
TRUNCATE TABLE identity.users, identity.persons, identity.wallets, identity.user_trust_profiles
RESTART IDENTITY CASCADE;
-- Note: The CASCADE option will also truncate any tables that have foreign keys referencing these tables.
-- This includes: identity.social_accounts, identity.organization_members, etc.
-- If you want to preserve other tables (e.g., system.addresses), you may need to adjust.
-- 3. Insert the superadmin person
INSERT INTO identity.persons (
first_name,
last_name,
identity_hash,
phone,
is_active,
is_sales_agent,
lifetime_xp,
penalty_points,
social_reputation,
identity_docs,
ice_contact,
created_at
) VALUES (
'Super',
'Admin',
'superadmin_hash_' || gen_random_uuid(),
'+36123456789',
true,
false,
0,
0,
5.0,
'{}'::jsonb,
'{}'::jsonb,
NOW()
) RETURNING id;
-- 4. Insert the superadmin user (using the returned person_id)
INSERT INTO identity.users (
email,
hashed_password,
role,
person_id,
is_active,
is_deleted,
subscription_plan,
is_vip,
subscription_expires_at,
referral_code,
referred_by_id,
current_sales_agent_id,
folder_slug,
preferred_language,
region_code,
preferred_currency,
scope_level,
scope_id,
custom_permissions,
created_at
) VALUES (
'superadmin@profibot.hu',
-- Password hash for 'Admin123!' (generated with bcrypt, cost 12)
'$2b$12$6YQ.Zj.8Vq8Z8Z8Z8Z8Z8O',
'superadmin',
(SELECT id FROM identity.persons WHERE identity_hash LIKE 'superadmin_hash_%'),
true,
false,
'ENTERPRISE',
false,
NULL,
NULL,
NULL,
NULL,
NULL,
'hu',
'HU',
'HUF',
'system',
NULL,
'{}'::jsonb,
NOW()
) RETURNING id;
-- 5. Create wallet for superadmin
INSERT INTO identity.wallets (
user_id,
earned_credits,
purchased_credits,
service_coins,
currency
) VALUES (
(SELECT id FROM identity.users WHERE email = 'superadmin@profibot.hu'),
1000000.0,
500000.0,
10000.0,
'HUF'
);
-- 6. Insert an admin person
INSERT INTO identity.persons (
first_name,
last_name,
identity_hash,
phone,
is_active,
is_sales_agent,
lifetime_xp,
penalty_points,
social_reputation,
identity_docs,
ice_contact,
created_at
) VALUES (
'Admin',
'User',
'adminuser_hash_' || gen_random_uuid(),
'+36123456780',
true,
false,
0,
0,
4.5,
'{}'::jsonb,
'{}'::jsonb,
NOW()
) RETURNING id;
-- 7. Insert the admin user
INSERT INTO identity.users (
email,
hashed_password,
role,
person_id,
is_active,
is_deleted,
subscription_plan,
is_vip,
subscription_expires_at,
referral_code,
referred_by_id,
current_sales_agent_id,
folder_slug,
preferred_language,
region_code,
preferred_currency,
scope_level,
scope_id,
custom_permissions,
created_at
) VALUES (
'admin@profibot.hu',
-- Password hash for 'Admin123!' (same as above)
'$2b$12$6YQ.Zj.8Vq8Z8Z8Z8Z8Z8O',
'admin',
(SELECT id FROM identity.persons WHERE identity_hash LIKE 'adminuser_hash_%'),
true,
false,
'PRO',
false,
NULL,
NULL,
NULL,
NULL,
NULL,
'hu',
'HU',
'HUF',
'system',
NULL,
'{}'::jsonb,
NOW()
) RETURNING id;
-- 8. Create wallet for admin
INSERT INTO identity.wallets (
user_id,
earned_credits,
purchased_credits,
service_coins,
currency
) VALUES (
(SELECT id FROM identity.users WHERE email = 'admin@profibot.hu'),
500000.0,
200000.0,
5000.0,
'HUF'
);
-- 9. Optionally, insert a test user for development
INSERT INTO identity.persons (
first_name,
last_name,
identity_hash,
phone,
is_active,
is_sales_agent,
lifetime_xp,
penalty_points,
social_reputation,
identity_docs,
ice_contact,
created_at
) VALUES (
'Test',
'User',
'testuser_hash_' || gen_random_uuid(),
'+36123456781',
true,
false,
0,
0,
3.0,
'{}'::jsonb,
'{}'::jsonb,
NOW()
);
INSERT INTO identity.users (
email,
hashed_password,
role,
person_id,
is_active,
is_deleted,
subscription_plan,
is_vip,
subscription_expires_at,
referral_code,
referred_by_id,
current_sales_agent_id,
folder_slug,
preferred_language,
region_code,
preferred_currency,
scope_level,
scope_id,
custom_permissions,
created_at
) VALUES (
'test@profibot.hu',
'$2b$12$6YQ.Zj.8Vq8Z8Z8Z8Z8Z8O',
'user',
(SELECT id FROM identity.persons WHERE identity_hash LIKE 'testuser_hash_%'),
true,
false,
'FREE',
false,
NULL,
NULL,
NULL,
NULL,
NULL,
'hu',
'HU',
'HUF',
'individual',
NULL,
'{}'::jsonb,
NOW()
);
INSERT INTO identity.wallets (
user_id,
earned_credits,
purchased_credits,
service_coins,
currency
) VALUES (
(SELECT id FROM identity.users WHERE email = 'test@profibot.hu'),
1000.0,
0.0,
100.0,
'HUF'
);
COMMIT;
-- 10. Verify the cleanup
SELECT 'Cleanup completed. New users:' AS message;
SELECT u.id, u.email, u.role, p.first_name, p.last_name
FROM identity.users u
JOIN identity.persons p ON u.person_id = p.id
ORDER BY u.id;

View File

@@ -0,0 +1,38 @@
# /opt/docker/dev/service_finder/backend/app/scripts/fix_imports_diag.py
import os
import re
# Az alapkönyvtár, ahol a kódjaid vannak
BASE_DIR = "/app/app"
def check_imports():
print("🔍 Importálási hibák keresése...")
broken_count = 0
for root, dirs, files in os.walk(BASE_DIR):
for file in files:
if file.endswith(".py"):
file_path = os.path.join(root, file)
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
for i, line in enumerate(lines):
# Keresünk minden 'from app.models...' kezdetű sort
match = re.search(r'from app\.models\.(\w+)', line)
if match:
model_name = match.group(1)
# Ellenőrizzük, hogy létezik-e ilyen fájl vagy mappa a models alatt
# Figyelem: itt az új szerkezetet (marketplace, system, identity) kellene látnia
target_path = os.path.join(BASE_DIR, "models", model_name)
target_file = target_path + ".py"
if not os.path.exists(target_path) and not os.path.exists(target_file):
print(f"❌ HIBA: {file_path} (sor: {i+1})")
print(f" -> Importált: {match.group(0)}")
print(f" -> Nem található itt: {target_file} vagy {target_path}")
broken_count += 1
print(f"\n✅ Vizsgálat kész. Összesen {broken_count} törött importot találtam.")
if __name__ == "__main__":
check_imports()

View File

@@ -2,8 +2,8 @@
import asyncio
from sqlalchemy import select, update
from app.db.session import SessionLocal
from app.models.asset import AssetCatalog
from app.models.vehicle_definitions import VehicleModelDefinition, VehicleType
from app.models import AssetCatalog
from app.models import VehicleModelDefinition, VehicleType
async def link_catalog_to_mdm():
""" Összefűzi a technikai katalógust a központi Master Definíciókkal. """

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
# docker exec -it sf_api python -m app.scripts.monitor_crawler
import asyncio
import os
from sqlalchemy import text
from app.database import AsyncSessionLocal
from datetime import datetime
async def monitor():
print(f"\n🛰️ AUTO-DATA CRAWLER MONITOR | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 60)
async with AsyncSessionLocal() as db:
# 1. Összesített statisztika szintenként
stats_query = text("""
SELECT level, status, COUNT(*)
FROM vehicle.auto_data_crawler_queue
GROUP BY level, status
ORDER BY level, status;
""")
# 2. Utolsó 5 hiba
error_query = text("""
SELECT name, level, error_msg, updated_at
FROM vehicle.auto_data_crawler_queue
WHERE status = 'error'
ORDER BY updated_at DESC LIMIT 5;
""")
res = await db.execute(stats_query)
rows = res.fetchall()
if not rows:
print("📭 A várólista üres.")
else:
print(f"{'SZINT':<15} | {'STÁTUSZ':<12} | {'DARABSZÁM':<10}")
print("-" * 45)
for r in rows:
icon = "" if r[1] == 'pending' else "⚙️" if r[1] == 'processing' else "" if r[1] == 'completed' else ""
print(f"{r[0].upper():<15} | {icon} {r[1]:<10} | {r[2]:<10}")
errors = await db.execute(error_query)
error_rows = errors.fetchall()
if error_rows:
print("\n🚨 LEGUTÓBBI HIBÁK:")
print("-" * 60)
for e in error_rows:
print(f"📍 {e[0]} ({e[1]}): {e[2][:70]}... [{e[3].strftime('%H:%M:%S')}]")
if __name__ == "__main__":
asyncio.run(monitor())

View File

@@ -2,7 +2,7 @@
import asyncio
from sqlalchemy import select
from app.db.session import SessionLocal
from app.models.audit import ProcessLog
from app.models import ProcessLog
from datetime import datetime, timedelta, timezone
async def generate_morning_report():

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""
Move tables from system schema to gamification schema.
"""
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
async def move_tables():
# Use the same DATABASE_URL as sync_engine
from app.core.config import settings
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
async with engine.begin() as conn:
# Check if tables exist in system schema
result = await conn.execute(text("""
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_name IN ('competitions', 'user_scores')
ORDER BY table_schema;
"""))
rows = result.fetchall()
print("Current tables:")
for row in rows:
print(f" {row.table_schema}.{row.table_name}")
# Move competitions
print("\nMoving system.competitions to gamification.competitions...")
try:
await conn.execute(text('ALTER TABLE system.competitions SET SCHEMA gamification;'))
print(" OK")
except Exception as e:
print(f" Error: {e}")
# Move user_scores
print("Moving system.user_scores to gamification.user_scores...")
try:
await conn.execute(text('ALTER TABLE system.user_scores SET SCHEMA gamification;'))
print(" OK")
except Exception as e:
print(f" Error: {e}")
# Verify
result = await conn.execute(text("""
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_name IN ('competitions', 'user_scores')
ORDER BY table_schema;
"""))
rows = result.fetchall()
print("\nAfter moving:")
for row in rows:
print(f" {row.table_schema}.{row.table_name}")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(move_tables())

View File

@@ -7,6 +7,9 @@ echo "=================================================="
# Ensure we are in the correct directory (should be /app inside container)
cd /app
# Override EMAIL_PROVIDER to smtp for development
export EMAIL_PROVIDER=smtp
# Run the unified database synchronizer with --apply flag
echo "📦 Running unified_db_sync.py --apply..."
python -m app.scripts.unified_db_sync --apply

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""
Rename tables in system schema to deprecated to avoid extra detection.
"""
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
async def rename():
from app.core.config import settings
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
async with engine.begin() as conn:
# Check if tables exist
result = await conn.execute(text("""
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema = 'system' AND table_name IN ('competitions', 'user_scores');
"""))
rows = result.fetchall()
print("Tables to rename:")
for row in rows:
print(f" {row.table_schema}.{row.table_name}")
# Rename competitions
try:
await conn.execute(text('ALTER TABLE system.competitions RENAME TO competitions_deprecated;'))
print("Renamed system.competitions -> system.competitions_deprecated")
except Exception as e:
print(f"Error renaming competitions: {e}")
# Rename user_scores
try:
await conn.execute(text('ALTER TABLE system.user_scores RENAME TO user_scores_deprecated;'))
print("Renamed system.user_scores -> system.user_scores_deprecated")
except Exception as e:
print(f"Error renaming user_scores: {e}")
# Verify
result = await conn.execute(text("""
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema = 'system' AND table_name LIKE '%deprecated';
"""))
rows = result.fetchall()
print("\nAfter rename:")
for row in rows:
print(f" {row.table_schema}.{row.table_name}")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(rename())

View File

@@ -131,6 +131,80 @@ async def seed_params():
"description": "Szintek, büntetések és jutalmak mátrixa",
"scope_level": "global"
},
# --- 6.1 GAMIFICATION 2.0 (Seasonal Competitions & Self-Defense) ---
{
"key": "service_trust_threshold",
"value": 70,
"category": "gamification",
"description": "Minimum trust score a szerviz publikálásához (0-100)",
"scope_level": "global"
},
{
"key": "service_submission_rewards",
"value": {
"points": 50,
"xp": 100,
"social_credits": 10
},
"category": "gamification",
"description": "Jutalmak sikeres szerviz beküldésért",
"scope_level": "global"
},
{
"key": "seasonal_competition_config",
"value": {
"season_duration_days": 90,
"top_contributors_count": 10,
"rewards": {
"first_place": {"credits": 1000, "badge": "season_champion"},
"second_place": {"credits": 500, "badge": "season_runner_up"},
"third_place": {"credits": 250, "badge": "season_bronze"},
"top_10": {"credits": 100, "badge": "season_elite"}
}
},
"category": "gamification",
"description": "Szezonális verseny beállítások",
"scope_level": "global"
},
{
"key": "self_defense_penalties",
"value": {
"level_minus_1": {
"name": "Figyelmeztetés",
"restrictions": ["no_service_submissions", "reduced_search_priority"],
"duration_days": 7,
"recovery_xp": 500
},
"level_minus_2": {
"name": "Felfüggesztés",
"restrictions": ["no_service_submissions", "no_reviews", "no_messaging", "reduced_search_priority"],
"duration_days": 30,
"recovery_xp": 2000
},
"level_minus_3": {
"name": "Kitiltás",
"restrictions": ["no_service_submissions", "no_reviews", "no_messaging", "no_search", "account_frozen"],
"duration_days": 365,
"recovery_xp": 10000
}
},
"category": "gamification",
"description": "Önvédelmi rendszer büntetési szintek",
"scope_level": "global"
},
{
"key": "contribution_types_config",
"value": {
"service_submission": {"points": 50, "xp": 100, "weight": 1.0},
"verified_review": {"points": 30, "xp": 50, "weight": 0.8},
"expertise_tagging": {"points": 20, "xp": 30, "weight": 0.6},
"data_validation": {"points": 15, "xp": 25, "weight": 0.5},
"community_moderation": {"points": 40, "xp": 75, "weight": 0.9}
},
"category": "gamification",
"description": "Hozzájárulási típusok és pontozási súlyok",
"scope_level": "global"
},
# --- 7. ÉRTESÍTÉSEK ÉS KARBANTARTÁS ---
{
@@ -248,209 +322,4 @@ async def seed_params():
# --- 11. KÜLSŐ API-K (DVLA, UK) ---
{
"key": "dvla_api_enabled",
"value": True,
"category": "api_keys",
"description": "Engedélyezze-e a brit DVLA lekérdezéseket?",
"scope_level": "global"
},
{
"key": "dvla_api_url",
"value": "https://driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles",
"category": "api_keys",
"description": "Hivatalos DVLA Vehicle Enquiry API végpont",
"scope_level": "global"
},
{
"key": "dvla_api_key",
"value": "IDE_JÖN_A_VALÓDI_KULCS",
"category": "api_keys",
"description": "Bizalmas DVLA API kulcs (X-API-KEY)",
"scope_level": "global"
},
# --- 12. AI & ROBOTOK (Ollama integráció) ---
{
"key": "ai_model_text",
"value": "qwen2.5-coder:32b",
"category": "ai",
"description": "Fő technikai elemző modell (Ollama)",
"scope_level": "global"
},
{
"key": "ai_model_vision",
"value": "llava:7b",
"category": "ai",
"description": "Látó modell az OCR folyamatokhoz",
"scope_level": "global"
},
{
"key": "ai_temperature",
"value": 0.1,
"category": "ai",
"description": "AI válasz kreativitása (0.1 = precíz, 0.9 = kreatív)",
"scope_level": "global"
},
{
"key": "ai_prompt_ocr_invoice",
"value": "FELADAT: Olvasd ki a számla adatait. JSON válasz: {amount, currency, date, vendor, vat}.",
"category": "ai",
"description": "Robot 1 - Számla OCR prompt",
"scope_level": "global"
},
# --- 13. SOCIAL & VERIFIED REVIEWS (Epic 4.1 - #66) ---
{
"key": "REVIEW_WINDOW_DAYS",
"value": 30,
"category": "social",
"description": "Értékelési időablak napokban a tranzakció után",
"scope_level": "global"
},
{
"key": "TRUST_SCORE_INFLUENCE_FACTOR",
"value": 1.0,
"category": "social",
"description": "Trustscore súlyozási tényező a szerviz értékeléseknél",
"scope_level": "global"
},
{
"key": "REVIEW_RATING_WEIGHTS",
"value": {
"price": 0.25,
"quality": 0.35,
"time": 0.20,
"communication": 0.20
},
"category": "social",
"description": "Értékelési dimenziók súlyai az összpontszám számításához",
"scope_level": "global"
},
{
"key": "ai_prompt_gold_data",
"value": "Készíts technikai adatlapot a(z) {make} {model} típushoz a megadott adatok alapján: {context}. Csak hiteles JSON-t adj!",
"category": "ai",
"description": "Robot 3 - Technikai dúsító prompt",
"scope_level": "global"
}
] # <-- ITT HIÁNYZOTT A ZÁRÓJEL!
# ----------------------------------------------------------------------
# HIERARCHIKUS KERESÉSI MÁTRIXOK (A SearchService 2.4-hez)
# Ezek az értékek felülbírálják az alapértelmezéseket a megfelelő "scope" esetén.
# ----------------------------------------------------------------------
# 1. GLOBÁLIS ALAP (Free usereknek)
params.append({
"key": "RANKING_RULES",
"scope_level": "global",
"scope_id": None,
"value": {
"ad_weight": 8000,
"partner_weight": 1000,
"trust_weight": 5,
"dist_penalty": 40,
"can_use_prefs": False,
"search_radius_km": 25
},
"category": "search",
"description": "Alapértelmezett (Free) rangsorolási szabályok"
})
# 2. PREMIUM CSOMAG SZINTŰ BEÁLLÍTÁS (Közepes szint)
params.append({
"key": "RANKING_RULES",
"scope_level": "package",
"scope_id": "premium",
"value": {
"pref_weight": 10000,
"partner_weight": 2000,
"trust_weight": 50,
"ad_weight": 500,
"dist_penalty": 20,
"can_use_prefs": True,
"search_radius_km": 50
},
"category": "search",
"description": "Prémium csomag rangsorolási szabályai"
})
# 3. VIP CSOMAG SZINTŰ BEÁLLÍTÁS
params.append({
"key": "RANKING_RULES",
"scope_level": "package",
"scope_id": "vip",
"value": {
"pref_weight": 20000, # A kedvenc mindent visz
"partner_weight": 5000,
"trust_weight": 100, # A minőség számít
"ad_weight": 0, # VIP-nek nem tolunk hirdetést az élre
"dist_penalty": 5, # Alig büntetjük a távolságot
"can_use_prefs": True,
"search_radius_km": 150
},
"category": "search",
"description": "VIP csomag rangsorolási szabályai"
})
# 4. EGYÉNI CÉGES FELÜLBÍRÁLÁS (Pl. ProfiBot Flotta Co.)
params.append({
"key": "RANKING_RULES",
"scope_level": "user",
"scope_id": "99",
"value": {
"pref_weight": 50000, # Nekik csak a saját szerződött partnereik kellenek
"can_use_prefs": True,
"search_radius_km": 500 # Az egész országot látják
},
"category": "search",
"description": "Egyedi flotta-ügyfél keresési szabályai"
})
logger.info("🚀 Rendszerparaméterek szinkronizálása a 2.0-ás modell szerint...")
added_count = 0
updated_count = 0
for p in params:
# GONDOLATMENET A JAVÍTÁSHOZ:
# Muszáj a scope_level-t és scope_id-t is vizsgálni, különben az SQLAlchemy
# összeomlik (MultipleResultsFound), mert ugyanaz a 'key' (pl. RANKING_RULES)
# több sorban is szerepel a hierarchia miatt!
s_level = p.get("scope_level", "global")
s_id = p.get("scope_id", None)
stmt = select(SystemParameter).where(
SystemParameter.key == p["key"],
SystemParameter.scope_level == s_level,
SystemParameter.scope_id == s_id
)
res = await db.execute(stmt)
existing = res.scalar_one_or_none()
if not existing:
# Új rekord létrehozása
new_param = SystemParameter(
key=p["key"],
value=p["value"],
category=p["category"],
description=p["description"],
scope_level=s_level,
scope_id=s_id,
last_modified_by=None
)
db.add(new_param)
added_count += 1
# Azonnali commit, hogy a következő körben már lássa a DB!
await db.commit()
else:
# Csak frissítés, ha szükséges
existing.description = p["description"]
existing.category = p["category"]
updated_count += 1
await db.commit()
logger.info(f"✅ Kész! Új: {added_count}, Frissített meta: {updated_count}")
if __name__ == "__main__":
asyncio.run(seed_params())
"key": "dvla_api_en

View File

@@ -2,7 +2,7 @@
import asyncio
from sqlalchemy import select
from app.db.session import SessionLocal
from app.models.vehicle_definitions import VehicleType, FeatureDefinition
from app.models import VehicleType, FeatureDefinition
async def seed_system_data():
""" Alapvető típusok és extrák (Features) feltöltése. """

View File

@@ -0,0 +1,353 @@
#!/usr/bin/env python3
"""
Smart Admin Audit Script
This script performs a targeted audit of the Service Finder admin system:
1. Finds business hardcoded values (excluding trivial 0, 1, True, False)
2. Identifies which API modules lack /admin prefixed endpoints
3. Generates a comprehensive gap analysis report in Markdown format
"""
import ast
import os
import re
import datetime
from pathlib import Path
from typing import List, Dict, Set, Tuple, Any
import sys
# Project root (relative to script location)
# In container: /app/app/scripts/smart_admin_audit.py -> parent.parent.parent = /app
PROJECT_ROOT = Path("/app")
BACKEND_DIR = PROJECT_ROOT # /app is the backend root in container
ENDPOINTS_DIR = BACKEND_DIR / "app" / "api" / "v1" / "endpoints"
SERVICES_DIR = BACKEND_DIR / "app" / "services"
MODELS_DIR = BACKEND_DIR / "app" / "models"
OUTPUT_FILE = PROJECT_ROOT / "admin_gap_analysis.md"
# Patterns for business hardcoded values (exclude trivial values)
BUSINESS_PATTERNS = [
r"award_points\s*=\s*(\d+)",
r"validation_level\s*=\s*(\d+)",
r"max_vehicles\s*=\s*(\d+)",
r"max_users\s*=\s*(\d+)",
r"credit_limit\s*=\s*(\d+)",
r"daily_limit\s*=\s*(\d+)",
r"monthly_limit\s*=\s*(\d+)",
r"threshold\s*=\s*(\d+)",
r"quota\s*=\s*(\d+)",
r"priority\s*=\s*(\d+)",
r"timeout\s*=\s*(\d+)",
r"retry_count\s*=\s*(\d+)",
r"batch_size\s*=\s*(\d+)",
r"page_size\s*=\s*(\d+)",
r"cache_ttl\s*=\s*(\d+)",
r"expiry_days\s*=\s*(\d+)",
r"cooldown\s*=\s*(\d+)",
r"penalty\s*=\s*(\d+)",
r"reward\s*=\s*(\d+)",
r"discount\s*=\s*(\d+)",
r"commission\s*=\s*(\d+)",
r"fee\s*=\s*(\d+)",
r"vat_rate\s*=\s*(\d+)",
r"service_fee\s*=\s*(\d+)",
r"subscription_fee\s*=\s*(\d+)",
]
# Trivial values to exclude
TRIVIAL_VALUES = {"0", "1", "True", "False", "None", "''", '""', "[]", "{}"}
def find_hardcoded_values() -> List[Dict[str, Any]]:
"""
Scan Python files for business-relevant hardcoded values.
Returns list of findings with file, line, value, and context.
"""
findings = []
# Walk through backend directory
for root, dirs, files in os.walk(BACKEND_DIR):
# Skip virtual environments and test directories
if any(exclude in root for exclude in ["__pycache__", ".venv", "tests", "migrations"]):
continue
for file in files:
if file.endswith(".py"):
filepath = Path(root) / file
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# Parse AST to find assignments
tree = ast.parse(content, filename=str(filepath))
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
var_name = target.id
# Check if assignment value is a constant
if isinstance(node.value, ast.Constant):
value = node.value.value
value_str = str(value)
# Skip trivial values
if value_str in TRIVIAL_VALUES:
continue
# Check if variable name matches business patterns
for pattern in BUSINESS_PATTERNS:
if re.match(pattern.replace(r"\s*=\s*(\d+)", ""), var_name):
findings.append({
"file": str(filepath.relative_to(PROJECT_ROOT)),
"line": node.lineno,
"variable": var_name,
"value": value_str,
"context": ast.get_source_segment(content, node)
})
break
# Also check numeric values > 1 or strings that look like config
if isinstance(value, (int, float)) and value > 1:
findings.append({
"file": str(filepath.relative_to(PROJECT_ROOT)),
"line": node.lineno,
"variable": var_name,
"value": value_str,
"context": ast.get_source_segment(content, node)
})
elif isinstance(value, str) and len(value) > 10 and " " not in value:
# Could be API keys, URLs, etc
findings.append({
"file": str(filepath.relative_to(PROJECT_ROOT)),
"line": node.lineno,
"variable": var_name,
"value": f'"{value_str[:50]}..."',
"context": ast.get_source_segment(content, node)
})
except (SyntaxError, UnicodeDecodeError):
continue
return findings
def analyze_admin_endpoints() -> Dict[str, Dict[str, Any]]:
"""
Analyze which API modules have /admin prefixed endpoints.
Returns dict with module analysis.
"""
modules = {}
if not ENDPOINTS_DIR.exists():
print(f"Warning: Endpoints directory not found: {ENDPOINTS_DIR}")
return modules
for endpoint_file in ENDPOINTS_DIR.glob("*.py"):
module_name = endpoint_file.stem
with open(endpoint_file, "r", encoding="utf-8") as f:
content = f.read()
# Check for router definition
router_match = re.search(r"router\s*=\s*APIRouter\(.*?prefix\s*=\s*[\"']/admin[\"']", content, re.DOTALL)
has_admin_prefix = bool(router_match)
# Check for admin endpoints (routes with /admin in path)
admin_routes = re.findall(r'@router\.\w+\([\"\'][^\"\']*?/admin[^\"\']*?[\"\']', content)
# Check for admin-specific functions
admin_functions = re.findall(r"def\s+\w+.*admin.*:", content, re.IGNORECASE)
modules[module_name] = {
"has_admin_prefix": has_admin_prefix,
"admin_routes_count": len(admin_routes),
"admin_functions": len(admin_functions),
"file_size": len(content),
"has_admin_file": (endpoint_file.stem == "admin")
}
return modules
def identify_missing_admin_modules(modules: Dict[str, Dict[str, Any]]) -> List[str]:
"""
Identify which core modules lack admin endpoints.
"""
core_modules = [
"users", "vehicles", "services", "assets", "organizations",
"billing", "gamification", "analytics", "security", "documents",
"evidence", "expenses", "finance_admin", "notifications", "reports",
"catalog", "providers", "search", "social", "system_parameters"
]
missing = []
for module in core_modules:
if module not in modules:
missing.append(module)
continue
mod_info = modules[module]
if not mod_info["has_admin_prefix"] and mod_info["admin_routes_count"] == 0:
missing.append(module)
return missing
def generate_markdown_report(hardcoded_findings: List[Dict[str, Any]],
modules: Dict[str, Dict[str, Any]],
missing_admin_modules: List[str]) -> str:
"""
Generate comprehensive Markdown report.
"""
report = []
report.append("# Admin System Gap Analysis Report")
report.append(f"*Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
report.append("")
# Executive Summary
report.append("## 📊 Executive Summary")
report.append("")
report.append(f"- **Total hardcoded business values found:** {len(hardcoded_findings)}")
report.append(f"- **API modules analyzed:** {len(modules)}")
report.append(f"- **Modules missing admin endpoints:** {len(missing_admin_modules)}")
report.append("")
# Hardcoded Values Section
report.append("## 🔍 Hardcoded Business Values")
report.append("")
report.append("These values should be moved to `system_parameters` table for dynamic configuration.")
report.append("")
if hardcoded_findings:
report.append("| File | Line | Variable | Value | Context |")
report.append("|------|------|----------|-------|---------|")
for finding in hardcoded_findings[:50]: # Limit to 50 for readability
file_link = finding["file"]
line = finding["line"]
variable = finding["variable"]
value = finding["value"]
context = finding["context"].replace("|", "\\|").replace("\n", " ").strip()[:100]
report.append(f"| `{file_link}` | {line} | `{variable}` | `{value}` | `{context}` |")
if len(hardcoded_findings) > 50:
report.append(f"\n*... and {len(hardcoded_findings) - 50} more findings*")
else:
report.append("*No significant hardcoded business values found.*")
report.append("")
# Admin Endpoints Analysis
report.append("## 🏗️ Admin Endpoints Analysis")
report.append("")
report.append("### Modules with Admin Prefix")
report.append("")
admin_modules = [m for m, info in modules.items() if info["has_admin_prefix"]]
if admin_modules:
report.append(", ".join(f"`{m}`" for m in admin_modules))
else:
report.append("*No modules have `/admin` prefix*")
report.append("")
report.append("### Modules with Admin Routes (but no prefix)")
report.append("")
mixed_modules = [m for m, info in modules.items() if not info["has_admin_prefix"] and info["admin_routes_count"] > 0]
if mixed_modules:
for module in mixed_modules:
info = modules[module]
report.append(f"- `{module}`: {info['admin_routes_count']} admin routes")
else:
report.append("*No mixed admin routes found*")
report.append("")
# Missing Admin Modules
report.append("## ⚠️ Critical Gaps: Missing Admin Endpoints")
report.append("")
report.append("These core business modules lack dedicated admin endpoints:")
report.append("")
if missing_admin_modules:
for module in missing_admin_modules:
report.append(f"- **{module}** - No `/admin` prefix and no admin routes")
report.append("")
report.append("### Recommended Actions:")
report.append("1. Create `/admin` prefixed routers for each missing module")
report.append("2. Implement CRUD endpoints for administrative operations")
report.append("3. Add audit logging and permission checks")
else:
report.append("*All core modules have admin endpoints!*")
report.append("")
# Recommendations
report.append("## 🚀 Recommendations")
report.append("")
report.append("### Phase 1: Hardcode Elimination")
report.append("1. Create `system_parameters` migration if not exists")
report.append("2. Move identified hardcoded values to database")
report.append("3. Implement `ConfigService` for dynamic value retrieval")
report.append("")
report.append("### Phase 2: Admin Endpoint Expansion")
report.append("1. Prioritize modules with highest business impact:")
report.append(" - `users` (user management)")
report.append(" - `billing` (financial oversight)")
report.append(" - `security` (access control)")
report.append("2. Follow consistent pattern: `/admin/{module}/...`")
report.append("3. Implement RBAC with `admin` and `superadmin` roles")
report.append("")
report.append("### Phase 3: Monitoring & Audit")
report.append("1. Add admin action logging to `SecurityAuditLog`")
report.append("2. Implement admin dashboard with real-time metrics")
report.append("3. Create automated health checks for admin endpoints")
report.append("")
# Technical Details
report.append("## 🔧 Technical Details")
report.append("")
report.append("### Scan Parameters")
report.append(f"- Project root: `{PROJECT_ROOT}`")
report.append(f"- Files scanned: Python files in `{BACKEND_DIR}`")
report.append(f"- Business patterns: {len(BUSINESS_PATTERNS)}")
report.append(f"- Trivial values excluded: {', '.join(TRIVIAL_VALUES)}")
report.append("")
return "\n".join(report)
def main():
"""Main execution function."""
print("🔍 Starting Smart Admin Audit...")
# 1. Find hardcoded values
print("Step 1: Scanning for hardcoded business values...")
hardcoded_findings = find_hardcoded_values()
print(f" Found {len(hardcoded_findings)} potential hardcoded values")
# 2. Analyze admin endpoints
print("Step 2: Analyzing admin endpoints...")
modules = analyze_admin_endpoints()
print(f" Analyzed {len(modules)} API modules")
# 3. Identify missing admin modules
missing_admin_modules = identify_missing_admin_modules(modules)
print(f" Found {len(missing_admin_modules)} modules missing admin endpoints")
# 4. Generate report
print("Step 3: Generating Markdown report...")
import datetime
report = generate_markdown_report(hardcoded_findings, modules, missing_admin_modules)
# Write to file
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
f.write(report)
print(f"✅ Report generated: {OUTPUT_FILE}")
print(f" - Hardcoded values: {len(hardcoded_findings)}")
print(f" - Modules analyzed: {len(modules)}")
print(f" - Missing admin: {len(missing_admin_modules)}")
# Print summary to console
if missing_admin_modules:
print("\n⚠️ CRITICAL GAPS:")
for module in missing_admin_modules[:5]:
print(f" - {module} lacks admin endpoints")
if len(missing_admin_modules) > 5:
print(f" ... and {len(missing_admin_modules) - 5} more")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,169 +1,153 @@
# /opt/docker/dev/service_finder/backend/app/scripts/sync_engine.py
#!/usr/bin/env python3
"""
Universal Schema Synchronizer
Dynamically imports all SQLAlchemy models from app.models, compares them with the live database,
and creates missing tables/columns without dropping anything.
Safety First:
- NEVER drops tables or columns.
- Prints planned SQL before execution.
- Requires confirmation for destructive operations (none in this script).
"""
# docker exec -it sf_api python -m app.scripts.sync_engine
import asyncio
import importlib
import os
import sys
from pathlib import Path
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import inspect, text
from sqlalchemy.schema import CreateTable, AddConstraint
from sqlalchemy.sql.ddl import CreateColumn
from sqlalchemy.schema import CreateTable
# Add backend to path
# Path beállítása
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from app.database import Base
from app.core.config import settings
def dynamic_import_models():
"""
Dynamically import all .py files in app.models directory to ensure Base.metadata is populated.
"""
"""Modellek betöltése a Metadata feltöltéséhez."""
models_dir = Path(__file__).parent.parent / "models"
imported = []
for py_file in models_dir.glob("*.py"):
if py_file.name == "__init__.py":
continue
module_name = f"app.models.{py_file.stem}"
# Rekurzív bejárás az alkönyvtárakkal együtt
for py_file in models_dir.rglob("*.py"):
if py_file.name == "__init__.py": continue
# Számítsuk ki a modulnevet a models könyvtárhoz képest
relative_path = py_file.relative_to(models_dir)
# Konvertáljuk path-t modulná: pl. identity/identity.py -> identity.identity
module_stem = str(relative_path).replace('/', '.').replace('\\', '.')[:-3] # eltávolítjuk a .py-t
module_name = f"app.models.{module_stem}"
try:
module = importlib.import_module(module_name)
imported.append(module_name)
print(f"✅ Imported {module_name}")
importlib.import_module(module_name)
except Exception as e:
print(f"⚠️ Could not import {module_name}: {e}")
# Also ensure the __init__ is loaded (it imports many models manually)
import app.models
print(f"📦 Total tables in Base.metadata: {len(Base.metadata.tables)}")
return imported
# Csak debug célra
print(f"Failed to import {module_name}: {e}")
pass
async def compare_and_repair():
"""
Compare SQLAlchemy metadata with live database and create missing tables/columns.
"""
print("🔗 Connecting to database...")
async def perform_detailed_audit():
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def get_diff_and_repair(connection):
# Audit számlálók
stats = {"ok": 0, "fixed": 0, "extra": 0, "missing": 0}
def audit_logic(connection):
inspector = inspect(connection)
metadata = Base.metadata
db_schemas = inspector.get_schema_names()
model_schemas = sorted({t.schema for t in metadata.sorted_tables if t.schema})
print("\n" + "="*80)
print(f"{'🔍 RÉSZLETES SCHEMA AUDIT JELENTÉS':^80}")
print("="*80)
# --- A IRÁNY: KÓD -> ADATBÁZIS (Minden ellenőrzése) ---
print(f"\n[A IRÁNY: Kód (SQLAlchemy) -> Adatbázis (PostgreSQL)]")
print("-" * 50)
# Get all schemas from models
expected_schemas = sorted({t.schema for t in Base.metadata.sorted_tables if t.schema})
print(f"📋 Expected schemas: {expected_schemas}")
# Ensure enum types exist in marketplace schema
if 'marketplace' in expected_schemas:
print("\n🔧 Ensuring enum types in marketplace schema...")
# moderation_status enum
connection.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'moderation_status' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'marketplace')) THEN
CREATE TYPE marketplace.moderation_status AS ENUM ('pending', 'approved', 'rejected');
END IF;
END $$;
"""))
# source_type enum
connection.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'source_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'marketplace')) THEN
CREATE TYPE marketplace.source_type AS ENUM ('manual', 'ocr', 'import');
END IF;
END $$;
"""))
print("✅ Enum types ensured.")
for schema in expected_schemas:
print(f"\n--- 🔍 Checking schema '{schema}' ---")
# Check if schema exists
db_schemas = inspector.get_schema_names()
for schema in model_schemas:
# 1. Séma ellenőrzése
if schema not in db_schemas:
print(f"Schema '{schema}' missing. Creating...")
print(f"HIÁNYZIK: Séma [{schema}] -> Létrehozás...")
connection.execute(text(f'CREATE SCHEMA IF NOT EXISTS "{schema}"'))
print(f"✅ Schema '{schema}' created.")
# Get tables in this schema from models
model_tables = [t for t in Base.metadata.sorted_tables if t.schema == schema]
stats["fixed"] += 1
else:
print(f"✅ RENDBEN: Séma [{schema}] létezik.")
stats["ok"] += 1
db_tables = inspector.get_table_names(schema=schema)
model_tables = [t for t in metadata.sorted_tables if t.schema == schema]
for table in model_tables:
full_name = f"{schema}.{table.name}"
# 2. Tábla ellenőrzése
if table.name not in db_tables:
print(f"❌ Missing table: {schema}.{table.name}")
# Generate CREATE TABLE statement
create_stmt = CreateTable(table)
# Print SQL for debugging
sql_str = str(create_stmt.compile(bind=engine))
print(f" SQL: {sql_str}")
connection.execute(create_stmt)
print(f"✅ Table {schema}.{table.name} created.")
print(f" ❌ HIÁNYZIK: Tábla [{full_name}] -> Létrehozás...")
connection.execute(CreateTable(table))
stats["fixed"] += 1
continue
else:
# Check columns
db_columns = {c['name']: c for c in inspector.get_columns(table.name, schema=schema)}
model_columns = table.columns
missing_cols = []
for col in model_columns:
if col.name not in db_columns:
missing_cols.append(col)
if missing_cols:
print(f"⚠️ Table {schema}.{table.name} missing columns: {[c.name for c in missing_cols]}")
for col in missing_cols:
# Generate ADD COLUMN statement
col_type = col.type.compile(dialect=engine.dialect)
sql = f'ALTER TABLE "{schema}"."{table.name}" ADD COLUMN "{col.name}" {col_type}'
if col.nullable is False:
sql += " NOT NULL"
if col.default is not None:
# Handle default values (simplistic)
sql += f" DEFAULT {col.default.arg}"
print(f" SQL: {sql}")
connection.execute(text(sql))
print(f"✅ Column {col.name} added.")
print(f" ✅ RENDBEN: Tábla [{full_name}] létezik.")
stats["ok"] += 1
# 3. Oszlopok ellenőrzése
db_cols = {c['name']: c for c in inspector.get_columns(table.name, schema=schema)}
for col in table.columns:
col_path = f"{full_name}.{col.name}"
if col.name not in db_cols:
print(f" ❌ HIÁNYZIK: Oszlop [{col_path}] -> Hozzáadás...")
col_type = col.type.compile(dialect=connection.dialect)
default_sql = ""
if col.server_default is not None:
arg = col.server_default.arg
val = arg.text if hasattr(arg, 'text') else str(arg)
default_sql = f" DEFAULT {val}"
null_sql = " NOT NULL" if not col.nullable else ""
connection.execute(text(f'ALTER TABLE "{schema}"."{table.name}" ADD COLUMN "{col.name}" {col_type}{default_sql}{null_sql}'))
stats["fixed"] += 1
else:
print(f"✅ Table {schema}.{table.name} is uptodate.")
print(f" ✅ RENDBEN: Oszlop [{col_path}]")
stats["ok"] += 1
# --- B IRÁNY: ADATBÁZIS -> KÓD (Árnyék adatok keresése) ---
print(f"\n[B IRÁNY: Adatbázis -> Kód (Extra elemek keresése)]")
print("-" * 50)
print("\n--- ✅ Schema synchronization complete. ---")
for schema in model_schemas:
if schema not in db_schemas: continue
db_tables = inspector.get_table_names(schema=schema)
model_table_names = {t.name for t in metadata.sorted_tables if t.schema == schema}
for db_table in db_tables:
# Ignore deprecated tables (ending with _deprecated)
if db_table.endswith("_deprecated"):
continue
full_db_name = f"{schema}.{db_table}"
if db_table not in model_table_names:
print(f" ⚠️ EXTRA TÁBLA: [{full_db_name}] (Nincs a kódban!)")
stats["extra"] += 1
else:
# Extra oszlopok a táblán belül
db_cols = inspector.get_columns(db_table, schema=schema)
model_col_names = {c.name for c in metadata.tables[full_db_name].columns}
for db_col in db_cols:
col_name = db_col['name']
if col_name not in model_col_names:
print(f" ⚠️ EXTRA OSZLOP: [{full_db_name}.{col_name}]")
stats["extra"] += 1
# --- ÖSSZESÍTŐ ---
print("\n" + "="*80)
print(f"{'📊 AUDIT ÖSSZESÍTŐ':^80}")
print("="*80)
print(f" ✅ Megfelelt (OK): {stats['ok']:>4} elem")
print(f" ❌ Javítva/Pótolva (Fixed): {stats['fixed']:>4} elem")
print(f" ⚠️ Extra (Shadow Data): {stats['extra']:>4} elem")
print("-" * 80)
if stats["fixed"] == 0 and stats["extra"] == 0:
print(f"{'✨ A RENDSZER TÖKÉLETESEN SZINKRONBAN VAN!':^80}")
else:
print(f"{' A rendszer üzemkész, de nézd át az extra (Shadow) elemeket!':^80}")
print("="*80 + "\n")
async with engine.begin() as conn:
await conn.run_sync(get_diff_and_repair)
await conn.run_sync(audit_logic)
await engine.dispose()
async def main():
print("🚀 Universal Schema Synchronizer")
print("=" * 50)
# Step 1: Dynamic import
print("\n📥 Step 1: Dynamically importing all models...")
dynamic_import_models()
# Step 2: Compare and repair
print("\n🔧 Step 2: Comparing with database and repairing...")
await compare_and_repair()
# Step 3: Final verification
print("\n📊 Step 3: Final verification...")
# Run compare_schema.py logic to confirm everything is green
from app.tests_internal.diagnostics.compare_schema import compare
await compare()
print("\n✨ Synchronization finished successfully!")
await perform_detailed_audit()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,170 @@
# /opt/docker/dev/service_finder/backend/app/scripts/sync_engine.py
#!/usr/bin/env python3
"""
Universal Schema Synchronizer
Dynamically imports all SQLAlchemy models from app.models, compares them with the live database,
and creates missing tables/columns without dropping anything.
Safety First:
- NEVER drops tables or columns.
- Prints planned SQL before execution.
- Requires confirmation for destructive operations (none in this script).
"""
import asyncio
import importlib
import os
import sys
from pathlib import Path
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import inspect, text
from sqlalchemy.schema import CreateTable, AddConstraint
from sqlalchemy.sql.ddl import CreateColumn
# Add backend to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from app.database import Base
from app.core.config import settings
def dynamic_import_models():
"""
Dynamically import all .py files in app.models directory to ensure Base.metadata is populated.
"""
models_dir = Path(__file__).parent.parent / "models"
imported = []
for py_file in models_dir.glob("*.py"):
if py_file.name == "__init__.py":
continue
module_name = f"app.models.{py_file.stem}"
try:
module = importlib.import_module(module_name)
imported.append(module_name)
print(f"✅ Imported {module_name}")
except Exception as e:
print(f"⚠️ Could not import {module_name}: {e}")
# Also ensure the __init__ is loaded (it imports many models manually)
import app.models
print(f"📦 Total tables in Base.metadata: {len(Base.metadata.tables)}")
return imported
async def compare_and_repair():
"""
Compare SQLAlchemy metadata with live database and create missing tables/columns.
"""
print("🔗 Connecting to database...")
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def get_diff_and_repair(connection):
inspector = inspect(connection)
# Get all schemas from models
expected_schemas = sorted({t.schema for t in Base.metadata.sorted_tables if t.schema})
print(f"📋 Expected schemas: {expected_schemas}")
# Ensure enum types exist in marketplace schema
if 'marketplace' in expected_schemas:
print("\n🔧 Ensuring enum types in marketplace schema...")
# moderation_status enum
connection.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'moderation_status' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'marketplace')) THEN
CREATE TYPE marketplace.moderation_status AS ENUM ('pending', 'approved', 'rejected');
END IF;
END $$;
"""))
# source_type enum
connection.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'source_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'marketplace')) THEN
CREATE TYPE marketplace.source_type AS ENUM ('manual', 'ocr', 'import');
END IF;
END $$;
"""))
print("✅ Enum types ensured.")
for schema in expected_schemas:
print(f"\n--- 🔍 Checking schema '{schema}' ---")
# Check if schema exists
db_schemas = inspector.get_schema_names()
if schema not in db_schemas:
print(f"❌ Schema '{schema}' missing. Creating...")
connection.execute(text(f'CREATE SCHEMA IF NOT EXISTS "{schema}"'))
print(f"✅ Schema '{schema}' created.")
# Get tables in this schema from models
model_tables = [t for t in Base.metadata.sorted_tables if t.schema == schema]
db_tables = inspector.get_table_names(schema=schema)
for table in model_tables:
if table.name not in db_tables:
print(f"❌ Missing table: {schema}.{table.name}")
# Generate CREATE TABLE statement
create_stmt = CreateTable(table)
# Print SQL for debugging
sql_str = str(create_stmt.compile(bind=engine))
print(f" SQL: {sql_str}")
connection.execute(create_stmt)
print(f"✅ Table {schema}.{table.name} created.")
else:
# Check columns
db_columns = {c['name']: c for c in inspector.get_columns(table.name, schema=schema)}
model_columns = table.columns
missing_cols = []
for col in model_columns:
if col.name not in db_columns:
missing_cols.append(col)
if missing_cols:
print(f"⚠️ Table {schema}.{table.name} missing columns: {[c.name for c in missing_cols]}")
for col in missing_cols:
# Generate ADD COLUMN statement
col_type = col.type.compile(dialect=engine.dialect)
sql = f'ALTER TABLE "{schema}"."{table.name}" ADD COLUMN "{col.name}" {col_type}'
if col.nullable is False:
sql += " NOT NULL"
if col.default is not None:
# Handle default values (simplistic)
sql += f" DEFAULT {col.default.arg}"
print(f" SQL: {sql}")
connection.execute(text(sql))
print(f"✅ Column {col.name} added.")
else:
print(f"✅ Table {schema}.{table.name} is uptodate.")
print("\n--- ✅ Schema synchronization complete. ---")
async with engine.begin() as conn:
await conn.run_sync(get_diff_and_repair)
await engine.dispose()
async def main():
print("🚀 Universal Schema Synchronizer")
print("=" * 50)
# Step 1: Dynamic import
print("\n📥 Step 1: Dynamically importing all models...")
dynamic_import_models()
# Step 2: Compare and repair
print("\n🔧 Step 2: Comparing with database and repairing...")
await compare_and_repair()
# Step 3: Final verification
print("\n📊 Step 3: Final verification...")
# Run compare_schema.py logic to confirm everything is green
from app.tests_internal.diagnostics.compare_schema import compare
await compare()
print("\n✨ Synchronization finished successfully!")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,67 @@
# /opt/docker/dev/service_finder/backend/app/scripts/sync_python_models_generator.py
#
import asyncio
from sqlalchemy import inspect
from sqlalchemy.ext.asyncio import create_async_engine
from app.core.config import settings
import sqlalchemy.types as types
# PostgreSQL specifikus típusok importálása
from sqlalchemy.dialects.postgresql import JSONB, UUID, ENUM
# Típus leképezés javítva
TYPE_MAP = {
types.INTEGER: "Integer",
types.VARCHAR: "String",
types.TEXT: "String",
types.BOOLEAN: "Boolean",
types.DATETIME: "DateTime",
types.TIMESTAMP: "DateTime",
types.NUMERIC: "Numeric",
types.JSON: "JSON",
JSONB: "JSONB",
UUID: "UUID"
}
async def generate_perfect_models():
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
def analyze(connection):
inspector = inspect(connection)
# Csak azokat a sémákat nézzük, ahol extra adatot találtunk
schemas = ['gamification', 'identity', 'marketplace', 'system', 'vehicle']
print("\n" + "="*80)
print(f"{'🛠️ PONTOS PYTHON MODELL KÓDOK A HIÁNYZÓ ELEMEKHEZ':^80}")
print("="*80)
for schema in schemas:
tables = inspector.get_table_names(schema=schema)
for table_name in tables:
# Osztálynév generálás (pl. user_contributions -> UserContribution)
class_name = "".join(x.capitalize() for x in table_name.split("_"))
if class_name.endswith("s"): class_name = class_name[:-1]
print(f"\n# --- [{schema}.{table_name}] ---")
for col in inspector.get_columns(table_name, schema=schema):
# Típus meghatározása intelligensebben
col_raw_type = col['type']
col_type = "String"
for k, v in TYPE_MAP.items():
if isinstance(col_raw_type, k):
col_type = v
break
params = []
if col.get('primary_key'): params.append("primary_key=True")
if not col.get('nullable'): params.append("nullable=False")
param_str = ", ".join(params)
print(f"{col['name']} = Column({col_type}{', ' + param_str if param_str else ''})")
async with engine.begin() as conn:
await conn.run_sync(analyze)
await engine.dispose()
if __name__ == "__main__":
asyncio.run(generate_perfect_models())

View File

@@ -2,14 +2,79 @@
import json
import httpx
import base64
import logging
from typing import Dict, Any, Optional
from app.schemas.evidence import RegistrationDocumentExtracted
logger = logging.getLogger(__name__)
class AiOcrService:
OLLAMA_URL = "http://service_finder_ollama:11434/api/generate"
OLLAMA_URL = "http://sf_ollama:11434/api/generate"
MODEL_NAME = "llama3.2-vision"
DEFAULT_TIMEOUT = 90.0
@classmethod
async def analyze_image(cls, image_bytes: bytes, prompt: str) -> Dict[str, Any]:
"""
Általános képfeldolgozás Ollama Vision modellel.
Args:
image_bytes: A kép bájtjai
prompt: A prompt szöveg, amit a modelnek küldünk
Returns:
Dict a válasz adataival (a 'response' mezőből parse-olt JSON)
Raises:
httpx.RequestError: Ha a hálózati kérés sikertelen
json.JSONDecodeError: Ha a válasz nem érvényes JSON
ValueError: Ha más hiba történik
"""
base64_image = base64.b64encode(image_bytes).decode('utf-8')
payload = {
"model": cls.MODEL_NAME,
"prompt": prompt,
"images": [base64_image],
"stream": False,
"format": "json"
}
async with httpx.AsyncClient(timeout=cls.DEFAULT_TIMEOUT) as client:
try:
logger.info(f"Ollama API hívás: {cls.OLLAMA_URL}, model: {cls.MODEL_NAME}")
response = await client.post(cls.OLLAMA_URL, json=payload)
response.raise_for_status()
result = response.json()
ai_response_text = result.get("response", "{}")
# Próbáljuk JSON-ként értelmezni a választ
try:
parsed = json.loads(ai_response_text)
except json.JSONDecodeError:
# Ha nem JSON, visszaadjuk szövegként
parsed = {"raw_response": ai_response_text}
logger.info(f"Ollama válasz sikeresen feldolgozva")
return parsed
except httpx.TimeoutException:
logger.error("Ollama API timeout")
raise ValueError("Ollama API időtúllépés")
except httpx.HTTPStatusError as e:
logger.error(f"Ollama HTTP hiba: {e.response.status_code} - {e.response.text}")
raise ValueError(f"Ollama HTTP hiba: {e.response.status_code}")
except Exception as e:
logger.error(f"Ollama API hiba: {e}")
raise ValueError(f"AI hiba a képfeldolgozás során: {str(e)}")
@classmethod
async def extract_registration_data(cls, clean_image_bytes: bytes) -> RegistrationDocumentExtracted:
"""
Speciális metódus magyar forgalmi engedély adatainak kinyerésére.
A régi kompatibilitás miatt megtartva.
"""
base64_image = base64.b64encode(clean_image_bytes).decode('utf-8')
prompt = """
@@ -49,7 +114,7 @@ class AiOcrService:
"format": "json"
}
async with httpx.AsyncClient(timeout=90.0) as client:
async with httpx.AsyncClient(timeout=cls.DEFAULT_TIMEOUT) as client:
try:
response = await client.post(cls.OLLAMA_URL, json=payload)
response.raise_for_status()
@@ -60,5 +125,5 @@ class AiOcrService:
return RegistrationDocumentExtracted(**data_dict)
except Exception as e:
print(f"Robot 3 AI Hiba: {e}")
logger.error(f"Robot 3 AI Hiba: {e}")
raise ValueError(f"AI hiba az adatkivonás során: {str(e)}")

View File

@@ -11,8 +11,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.vehicle import VehicleCost, CostCategory
from app.models.vehicle_definitions import VehicleModelDefinition
from app.models.organization import Organization
from app.models import VehicleModelDefinition
from app.models.marketplace.organization import Organization
from app.services.system_service import SystemService
logger = logging.getLogger(__name__)

View File

@@ -8,8 +8,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from sqlalchemy.orm import selectinload
from app.models.asset import Asset, AssetAssignment, AssetTelemetry, AssetFinancials
from app.models import Asset, AssetAssignment, AssetTelemetry, AssetFinancials
from app.models.identity import User
from app.models.vehicle.history import LogSeverity
from app.services.config_service import config
from app.services.gamification_service import GamificationService
from app.services.security_service import security_service
@@ -79,7 +80,8 @@ class AssetService:
catalog_id=catalog_id,
current_organization_id=org_id,
status="active",
is_verified=False
individual_equipment={},
created_at=datetime.utcnow()
)
db.add(new_asset)
await db.flush()
@@ -87,7 +89,12 @@ class AssetService:
# Digitális Iker Alapmodulok
db.add(AssetAssignment(asset_id=new_asset.id, organization_id=org_id, status="active"))
db.add(AssetTelemetry(asset_id=new_asset.id))
db.add(AssetFinancials(asset_id=new_asset.id))
db.add(AssetFinancials(
asset_id=new_asset.id,
purchase_price_net=0.0,
purchase_price_gross=0.0,
financing_type="unknown"
))
# Gamification
reward = await config.get_setting(db, "xp_reward_asset_register", default=250)
@@ -112,7 +119,7 @@ class AssetService:
# Logoljuk a kísérletet a biztonsági szolgálatnál (Sentinel)
await security_service.log_event(
db, user_id=user_id, action="VEHICLE_CLAIM_INITIATED",
severity="warning", target_type="Asset", target_id=str(asset.id),
severity=LogSeverity.warning, target_type="Asset", target_id=str(asset.id),
new_data={"vin": asset.vin, "new_org": org_id}
)

View File

@@ -9,8 +9,8 @@ from fastapi.encoders import jsonable_encoder
from fastapi import HTTPException, status
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
from app.models.gamification import UserStats
from app.models.organization import Organization, OrganizationMember, OrgType, Branch
from app.models import UserStats
from app.models.marketplace import Organization, OrganizationMember, OrgType, Branch
from app.schemas.auth import UserLiteRegister, UserKYCComplete
from app.core.security import get_password_hash, verify_password, generate_secure_slug
from app.services.email_manager import email_manager
@@ -41,7 +41,15 @@ class AuthService:
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
is_active=False
is_active=False,
identity_docs={}, # EXPLICIT BEÁLLÍTÁS A DB HIBA ELKERÜLÉSÉRE
ice_contact={}, # EXPLICIT BEÁLLÍTÁS A DB HIBA ELKERÜLÉSÉRE
lifetime_xp=0, # default -1, de explicit 0
penalty_points=0, # default -1, de explicit 0
social_reputation=1.0, # default 0.0, de explicit 1.0
is_sales_agent=False, # default True, de explicit False
is_ghost=False,
created_at=datetime.now(timezone.utc)
)
db.add(new_person)
await db.flush()
@@ -58,7 +66,13 @@ class AuthService:
is_deleted=False,
region_code=user_in.region_code,
preferred_language=user_in.lang,
timezone=user_in.timezone
subscription_plan='FREE',
# --- EXPLICIT DEFAULT ÉRTÉKEK A DB HIBA ELKERÜLÉSÉRE ---
is_vip=True, # Changed to True to force inclusion in INSERT
preferred_currency="HUF",
scope_level="individual",
custom_permissions={},
created_at=datetime.now(timezone.utc)
)
db.add(new_user)
await db.flush()
@@ -84,7 +98,7 @@ class AuthService:
# Sentinel Audit Log
await security_service.log_event(
db, user_id=new_user.id, action="USER_REGISTER_LITE",
severity="info", target_type="User", target_id=str(new_user.id),
severity="INFO", target_type="User", target_id=str(new_user.id),
new_data={"email": user_in.email}
)
@@ -136,7 +150,17 @@ class AuthService:
owner_id=user.id,
is_active=True,
status="verified",
country_code=user.region_code
country_code=user.region_code,
# --- EXPLICIT IDŐBÉLYEGEK A DB HIBA ELKERÜLÉSÉRE ---
first_registered_at=datetime.now(timezone.utc),
current_lifecycle_started_at=datetime.now(timezone.utc),
created_at=datetime.now(timezone.utc),
subscription_plan="FREE",
base_asset_limit=1,
purchased_extra_slots=0,
notification_settings={},
external_integration_config={},
is_ownership_transferable=True
)
db.add(new_org)
await db.flush()
@@ -251,7 +275,7 @@ class AuthService:
await security_service.log_event(
db, user_id=actor_id, action="USER_SOFT_DELETE",
severity="warning", target_type="User", target_id=str(user_id),
severity="WARNING", target_type="User", target_id=str(user_id),
new_data={"reason": reason}
)
await db.commit()

View File

@@ -13,7 +13,7 @@ from fastapi import HTTPException, status
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
from app.models.gamification import UserStats
from app.models.organization import Organization, OrganizationMember, OrgType
from app.models import Organization, OrganizationMember, OrgType
from app.schemas.auth import UserLiteRegister, UserKYCComplete
from app.core.security import get_password_hash, verify_password, generate_secure_slug
from app.services.email_manager import email_manager

View File

@@ -27,7 +27,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import selectinload
from app.models.identity import User, Wallet, ActiveVoucher, UserRole
from app.models.audit import FinancialLedger, LedgerEntryType, WalletType
from app.models import FinancialLedger, LedgerEntryType, WalletType
from app.core.config import settings
from app.services.config_service import config

View File

@@ -2,6 +2,7 @@
from typing import Any, Optional, Dict
import logging
import os
import json
from decimal import Decimal
from datetime import datetime, timezone
@@ -10,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
# Modellek importálása a központi helyről
from app.models import ExchangeRate, AssetCost, AssetTelemetry
from app.models.system.system import SystemParameter, ParameterScope
from app.db.session import AsyncSessionLocal
logger = logging.getLogger(__name__)
@@ -59,25 +61,211 @@ class CostService:
raise e
class ConfigService:
"""
MB 2.0 Alapvető konfigurációs szerviz.
Kezeli az AI szolgáltatások (Ollama) dinamikus beállításait és promptjait.
"""
async def get_setting(self, db: AsyncSession, key: str, default: Any = None) -> Any:
Egyszerű konfigurációs szolgáltatás a SystemParameter tábla lekérdezéséhez.
Támogatja a különböző típusú értékek lekérését alapértelmezett értékkel.
"""
@staticmethod
async def get(db: AsyncSession, key: str, default: Any = None, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> Any:
"""
Lekéri a kért beállítást.
1. Megnézi a környezeti változókat (NAGYBETŰVEL).
2. Ha nincs ilyen ENV, visszaadja a kódba égetett 'default' értéket.
"""
env_val = os.getenv(key.upper())
if env_val is not None:
# Automatikus típuskonverzió a default paraméter típusa alapján
if isinstance(default, int): return int(env_val)
if isinstance(default, float): return float(env_val)
if isinstance(default, bool): return str(env_val).lower() in ('true', '1', 'yes')
return env_val
Általános lekérdezés a SystemParameter táblából.
Args:
db: AsyncSession
key: A konfigurációs kulcs
default: Alapértelmezett érték, ha a kulcs nem található
scope_level: A paraméter scope-ja (global, country, region, user)
scope_id: A scope azonosítója (pl. országkód, user_id)
Returns:
A talált érték (a megfelelő típusban) vagy a default.
"""
from sqlalchemy import select, and_, cast, String
try:
# Convert scope_level to lowercase string for comparison
# PostgreSQL enum expects lowercase values, but Python Enum may be uppercase
scope_str = scope_level.value.lower() if hasattr(scope_level, 'value') else str(scope_level).lower()
# Build query with cast to avoid strict enum type mismatch
query = select(SystemParameter).where(
and_(
SystemParameter.key == key,
cast(SystemParameter.scope_level, String) == scope_str,
SystemParameter.is_active == True
)
)
if scope_id is None:
query = query.where(SystemParameter.scope_id.is_(None))
else:
query = query.where(SystemParameter.scope_id == scope_id)
result = await db.execute(query)
param = result.scalar_one_or_none()
if param is None:
# Opcionálisan beilleszthetjük a default értéket a táblába
# await ConfigService._insert_default(db, key, default, scope_level, scope_id)
return default
# A value oszlop JSONB, lehet dict, list, string, number, bool
db_value = param.value
# Típuskonverzió a default típusa alapján
if default is None:
return db_value
if isinstance(default, int):
if isinstance(db_value, (int, float, str)):
try:
return int(db_value)
except (ValueError, TypeError):
return default
return default
elif isinstance(default, float):
if isinstance(db_value, (int, float, str)):
try:
return float(db_value)
except (ValueError, TypeError):
return default
return default
elif isinstance(default, bool):
if isinstance(db_value, bool):
return db_value
elif isinstance(db_value, str):
return db_value.lower() in ('true', '1', 'yes', 'on')
elif isinstance(db_value, int):
return db_value != 0
return default
elif isinstance(default, str):
if isinstance(db_value, str):
return db_value
elif isinstance(db_value, (dict, list)):
return json.dumps(db_value)
else:
return str(db_value)
elif isinstance(default, dict) and isinstance(db_value, dict):
return db_value
elif isinstance(default, list) and isinstance(db_value, list):
return db_value
else:
# Egyébként visszaadjuk a db_value-t
return db_value
except Exception as e:
logger.warning(f"ConfigService.get error for key '{key}': {e}")
return default
async def get_setting(self, db: AsyncSession, key: str, default: Any = None, region_code: Optional[str] = None, org_id: Optional[int] = None, **kwargs) -> Any:
"""
Általános beállítás lekérése a régi kód kompatibilitásához.
Args:
db: AsyncSession
key: A konfigurációs kulcs
default: Alapértelmezett érték
region_code: Országkód (pl. "HU") - COUNTRY scope
org_id: Szervezet azonosító - ORGANIZATION scope
**kwargs: További paraméterek (pl. user_id)
Returns:
A talált érték vagy default.
"""
from app.models.system.system import ParameterScope
# Scope meghatározása
if org_id is not None:
scope_level = ParameterScope.ORGANIZATION
scope_id = str(org_id)
elif region_code is not None:
scope_level = ParameterScope.COUNTRY
scope_id = region_code
else:
scope_level = ParameterScope.GLOBAL
scope_id = None
# További scope-ok (pl. user) a kwargs-ból
if 'user_id' in kwargs:
scope_level = ParameterScope.USER
scope_id = str(kwargs['user_id'])
return await ConfigService.get(db, key, default, scope_level, scope_id)
@staticmethod
async def get_int(db: AsyncSession, key: str, default: int, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> int:
"""Egész szám lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, int):
return value
try:
return int(value)
except (ValueError, TypeError):
return default
@staticmethod
async def get_str(db: AsyncSession, key: str, default: str, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> str:
"""Szöveg lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, str):
return value
return str(value)
@staticmethod
async def get_bool(db: AsyncSession, key: str, default: bool, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> bool:
"""Logikai érték lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in ('true', '1', 'yes', 'on')
if isinstance(value, int):
return value != 0
return default
@staticmethod
async def get_float(db: AsyncSession, key: str, default: float, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> float:
"""Lebegőpontos szám lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, float):
return value
try:
return float(value)
except (ValueError, TypeError):
return default
@staticmethod
async def get_json(db: AsyncSession, key: str, default: dict, scope_level: ParameterScope = ParameterScope.GLOBAL, scope_id: Optional[str] = None) -> dict:
"""JSON objektum lekérése."""
value = await ConfigService.get(db, key, default, scope_level, scope_id)
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
return json.loads(value)
except json.JSONDecodeError:
return default
return default
@staticmethod
async def _insert_default(db: AsyncSession, key: str, default: Any, scope_level: ParameterScope, scope_id: Optional[str] = None) -> None:
"""Opcionális: beszúrja a default értéket a táblába, hogy látható legyen az Admin UI-ban."""
try:
from app.models.system.system import SystemParameter
param = SystemParameter(
key=key,
category="auto_inserted",
value=default if isinstance(default, (dict, list)) else {"value": default},
scope_level=scope_level,
scope_id=scope_id,
is_active=True,
description=f"Auto-inserted default value for {key}"
)
db.add(param)
await db.commit()
except Exception as e:
logger.debug(f"Could not insert default for {key}: {e}")
await db.rollback()
# A példány, amit a többi modul (pl. az auth_service, ai_service) importálni próbál
config = ConfigService()

View File

@@ -5,7 +5,7 @@ from decimal import Decimal
from typing import Any, Dict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, func
from app.models.asset import AssetCost, AssetTelemetry, ExchangeRate
from app.models import AssetCost, AssetTelemetry, ExchangeRate
from app.services.gamification_service import GamificationService
from app.services.config_service import config
from app.schemas.asset_cost import AssetCostCreate

View File

@@ -7,7 +7,7 @@ from typing import Optional, Dict, Any
from sqlalchemy import select, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.vehicle_definitions import VehicleModelDefinition
from app.models import VehicleModelDefinition
from app.workers.vehicle.mapping_rules import SOURCE_MAPPINGS, unify_data
logger = logging.getLogger(__name__)

View File

@@ -9,7 +9,7 @@ from fastapi import UploadFile, BackgroundTasks, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from app.models.document import Document
from app.models import Document
from app.models.identity import User
from app.services.config_service import config # 2.0 Dinamikus beállítások
from app.workers.ocr.robot_1_ocr_processor import OCRRobot # Robot 1 hívása
@@ -122,7 +122,7 @@ class DocumentService:
if doc_type in auto_ocr_types:
# Robot 1 (OCR) sorba állítása háttérfolyamatként
background_tasks.add_task(OCRRobot.process_document, db, new_doc.id)
new_doc.status = "processing"
new_doc.status = "pending_ocr"
logger.info(f"🤖 Robot 1 (OCR) riasztva: {new_doc.id}")
await db.commit()

View File

@@ -81,6 +81,37 @@ class EmailManager:
smtp_cfg = await config.get_setting(db, "smtp_config", default={
"host": "localhost", "port": 587, "user": "", "pass": "", "tls": True
})
logger.info(f"SMTP config retrieved: {smtp_cfg}")
# Ha a default értéket kaptuk, próbáljuk a környezeti változókból felépíteni a konfigurációt
import os
env_host = os.getenv("SMTP_HOST")
env_port = os.getenv("SMTP_PORT")
env_user = os.getenv("SMTP_USER")
env_pass = os.getenv("SMTP_PASSWORD")
env_tls = os.getenv("SMTP_TLS", "False").lower() in ("true", "1", "yes")
env_ssl = os.getenv("SMTP_SSL", "False").lower() in ("true", "1", "yes")
logger.info(f"Env SMTP: host={env_host}, port={env_port}, tls={env_tls}, ssl={env_ssl}")
# Felülírjuk a konfigurációt a környezeti változókkal, ha vannak
if env_host:
smtp_cfg["host"] = env_host
if env_port:
try:
smtp_cfg["port"] = int(env_port)
except:
pass
if env_user:
smtp_cfg["user"] = env_user
if env_pass:
smtp_cfg["pass"] = env_pass
# TLS/SSL kezelése: ha SSL igaz, akkor TLS legyen False (mert külön SMTP_SSL kapcsolat kell)
# Egyszerűsítés: tls = not ssl (de a Mailpit esetén TLS=False, SSL=False)
smtp_cfg["tls"] = env_tls
# SSL esetén a port változhat, de a kódunk nem támogatja az SMTP_SSL-t, csak TLS-t.
# A Mailpit nem igényel TLS-t, így maradjon False.
if env_ssl:
smtp_cfg["tls"] = False
# Megjegyzés: SSL kapcsolathoz smtplib.SMTP_SSL kellene, de most nem implementáljuk.
logger.info(f"Final SMTP config: {smtp_cfg}")
return await EmailManager._send_via_smtp(smtp_cfg, from_email, from_name, recipient, subject, html)
finally:
@@ -119,8 +150,12 @@ class EmailManager:
with smtplib.SMTP(cfg["host"], cfg["port"], timeout=15) as server:
if cfg.get("tls", True):
server.starttls()
if cfg.get("user") and cfg.get("pass"):
server.login(cfg["user"], cfg["pass"])
# Mailpit nem támogatja az SMTP AUTH-ot, és ha üres string a user/pass, akkor se próbáljuk meg
user = cfg.get("user", "")
passwd = cfg.get("pass", "")
# Ha a user/pass nem üres és nem csak idézőjelek, akkor login
if user and passwd and user.strip() not in ('', '""') and passwd.strip() not in ('', '""'):
server.login(user, passwd)
server.send_message(msg)
logger.info(f"SMTP siker -> {recipient}")

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