feat: stabilize KYC, international assets and multi-currency schema

- Split mother's name in KYC (last/first)
- Added mileage_unit and fuel_type to Assets
- Expanded AssetCost for international VAT and original currency
- Fixed SQLAlchemy IndexError in asset catalog lookup
- Added exchange_rate and ratings tables to models
This commit is contained in:
2026-02-08 23:41:07 +00:00
parent 451900ae1a
commit 24d35fe0c1
34 changed files with 709 additions and 347 deletions

View File

@@ -1,104 +1,136 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.db.session import get_db
from app.schemas.asset import AssetCreate, AssetResponse
from app.models.vehicle import Asset, VehicleCatalog
from app.models.organization import Organization
from app.core.validators import VINValidator
from app.core.config import settings
from sqlalchemy import select, func, and_
import os
import logging
from app.db.session import get_db
from app.api.deps import get_current_user
from app.schemas.asset import AssetCreate, AssetResponse
from app.models.asset import Asset, AssetCatalog, AssetAssignment, AssetEvent
from app.models.identity import User
from app.models.organization import Organization, OrganizationMember, OrgType
from app.core.config import settings
# VIN Validator - Standard 17 karakter, tiltott karakterek (I, O, Q) szűrése
class VINValidator:
@staticmethod
def validate(vin: str) -> bool:
vin = vin.upper()
if len(vin) != 17:
return False
if any(c in vin for c in "IOQ"):
return False
return True
router = APIRouter()
logger = logging.getLogger(__name__)
@router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
async def create_asset(
asset_in: AssetCreate,
db: AsyncSession = Depends(get_db)
# Később ide jön: current_user: User = Depends(get_current_active_user)
target_org_id: int = None,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Új jármű (Asset) rögzítése a flottába.
- Validálja a VIN-t (MOD 11 checksum).
- Ellenőrzi a Katalógus elemet.
- Létrehozza a NAS mappát a dokumentumoknak.
"""
# 1. VIN Validáció (Szigorú Checksum)
# A GEM protokoll szerint kötelező a validátor használata
is_valid_vin = VINValidator.validate(asset_in.vin)
if not is_valid_vin:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Érvénytelen alvázszám (VIN)! A Checksum ellenőrzés sikertelen."
)
# 1. VIN Validáció
if not VINValidator.validate(asset_in.vin):
raise HTTPException(status_code=400, detail="Érvénytelen alvázszám (VIN) formátum!")
# 2. Katalógus elem ellenőrzése
stmt_catalog = select(VehicleCatalog).where(VehicleCatalog.id == asset_in.catalog_id)
result_catalog = await db.execute(stmt_catalog)
catalog_item = result_catalog.scalar_one_or_none()
# 2. Célflotta ellenőrzése
if not target_org_id:
stmt_org = select(Organization).join(OrganizationMember).where(
and_(
OrganizationMember.user_id == current_user.id,
Organization.org_type == OrgType.individual
)
)
org = (await db.execute(stmt_org)).scalar_one_or_none()
if not org:
raise HTTPException(status_code=404, detail="Privát flotta nem található. KYC szükséges.")
final_org_id = org.id
else:
# Céges jogosultság ellenőrzése
stmt_mem = select(OrganizationMember).where(
and_(
OrganizationMember.organization_id == target_org_id,
OrganizationMember.user_id == current_user.id
)
)
member = (await db.execute(stmt_mem)).scalar_one_or_none()
if not member or (member.role != "owner" and not (member.permissions or {}).get("can_add_asset")):
raise HTTPException(status_code=403, detail="Nincs jogod ehhez a flottához!")
final_org_id = target_org_id
# 3. Katalógus ellenőrzése
stmt_cat = select(AssetCatalog).where(
and_(
AssetCatalog.make.ilike(asset_in.make), # Simán ilike, nem kell func() köré
AssetCatalog.model.ilike(asset_in.model)
)
)
catalog_item = (await db.execute(stmt_cat)).scalar_one_or_none()
if not catalog_item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A kiválasztott járműtípus nem található a katalógusban."
catalog_item = AssetCatalog(
make=asset_in.make,
model=asset_in.model,
vehicle_class=asset_in.vehicle_class,
fuel_type=asset_in.fuel_type
)
db.add(catalog_item)
await db.flush()
# 3. Szervezet ellenőrzése (Létezik-e, és van-e joga - jogultságkezelés később)
stmt_org = select(Organization).where(Organization.id == asset_in.organization_id)
result_org = await db.execute(stmt_org)
org_item = result_org.scalar_one_or_none()
if not org_item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A megadott flotta/szervezet nem található."
# 4. Asset létrehozása vagy betöltése (Shadow Identity)
stmt_exist = select(Asset).where(Asset.vin == asset_in.vin.upper())
new_asset = (await db.execute(stmt_exist)).scalar_one_or_none()
if not new_asset:
new_asset = Asset(
vin=asset_in.vin.upper(),
license_plate=asset_in.license_plate,
name=asset_in.name or f"{asset_in.make} {asset_in.model}",
year_of_manufacture=asset_in.year_of_manufacture,
fuel_type=asset_in.fuel_type, # JAVÍTVA: Most már átadjuk
vehicle_class=asset_in.vehicle_class, # JAVÍTVA: Most már átadjuk
mileage_unit=asset_in.reading_unit, # JAVÍTVA: Most már átadjuk
catalog_id=catalog_item.id,
quality_index=1.00,
system_mileage=0
)
db.add(new_asset)
await db.flush()
# 4. Asset Duplikáció ellenőrzése (Ugyanaz a VIN nem szerepelhet 2x aktívként)
# Megj: A UAI mező tárolja a VIN-t (Unique Asset Identifier)
stmt_exist = select(Asset).where(
Asset.uai == asset_in.vin.upper(),
Asset.status != "deleted"
# 5. Assignment
new_assignment = AssetAssignment(
asset_id=new_asset.id,
organization_id=final_org_id,
status="active"
)
result_exist = await db.execute(stmt_exist)
if result_exist.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Ez a jármű (VIN) már szerepel a rendszerben!"
)
db.add(new_assignment)
# 5. Mentés az adatbázisba
new_asset = Asset(
uai=asset_in.vin.upper(), # A VIN a fő azonosító
catalog_id=asset_in.catalog_id,
organization_id=asset_in.organization_id,
asset_type=catalog_item.category, # 'car', 'van', 'motorcycle', etc.
name=asset_in.name or f"{catalog_item.brand} {catalog_item.model}",
current_plate_number=asset_in.license_plate.upper(),
status="active",
privacy_level="private" # Alapértelmezett
)
db.add(new_asset)
await db.commit()
await db.refresh(new_asset)
# 6. Kezdő KM esemény
if asset_in.current_reading:
db.add(AssetEvent(
asset_id=new_asset.id,
event_type="initial_reading",
recorded_mileage=asset_in.current_reading,
description="Kezdeti óraállás rögzítése",
data={"source": "user_registration"}
))
# 6. NAS Tároló Létrehozása
# Útvonal: /mnt/nas/app_data/assets/{uuid}/
# A settings.NAS_STORAGE_PATH-ot használjuk (GEM protokoll)
try:
# Ha a settings-ben nincs definiálva, fallback a hardcoded path-ra (biztonsági háló)
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data/assets")
asset_path = os.path.join(base_path, str(new_asset.id))
await db.commit()
await db.refresh(new_asset)
os.makedirs(asset_path, exist_ok=True)
logger.info(f"NAS mappa létrehozva: {asset_path}")
# 7. NAS mappa struktúra
nas_base = getattr(settings, "NAS_STORAGE_PATH", "/opt/docker/dev/service_finder/nas/assets")
asset_path = os.path.join(nas_base, str(new_asset.id))
os.makedirs(os.path.join(asset_path, "docs"), exist_ok=True)
os.makedirs(os.path.join(asset_path, "photos"), exist_ok=True)
except OSError as e:
logger.error(f"CRITICAL: Nem sikerült létrehozni a NAS mappát: {e}")
# Nem dobunk hibát a usernek, mert az adatbázisba bekerült, de riasztunk logban
return new_asset
return new_asset
except Exception as e:
await db.rollback()
logger.error(f"Asset Creation Error: {str(e)}")
raise HTTPException(status_code=500, detail="Hiba a mentés során.")

View File

@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, or_
from app.db.session import get_db
from app.models.vehicle import VehicleCatalog
from app.models import AssetCatalog
from typing import List
router = APIRouter()