feat: implement pivot-currency model, rbac smart tokens & fix circular imports
This commit is contained in:
@@ -1,136 +1,129 @@
|
||||
import uuid
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, and_
|
||||
import os
|
||||
import logging
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
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.asset import Asset, AssetCost, AssetTelemetry
|
||||
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
|
||||
from app.services.cost_service import cost_service
|
||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_asset(
|
||||
asset_in: AssetCreate,
|
||||
target_org_id: int = None,
|
||||
# --- 1. MODUL: IDENTITÁS (Alapadatok) ---
|
||||
@router.get("/{asset_id}", response_model=Dict[str, Any])
|
||||
async def get_asset_identity(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
# 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. 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()
|
||||
"""Csak a jármű alapadatai és katalógus információi."""
|
||||
stmt = select(Asset).where(Asset.id == asset_id).options(selectinload(Asset.catalog))
|
||||
asset = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not catalog_item:
|
||||
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()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található")
|
||||
|
||||
return {
|
||||
"id": asset.id,
|
||||
"vin": asset.vin,
|
||||
"license_plate": asset.license_plate,
|
||||
"name": asset.name,
|
||||
"catalog": {
|
||||
"make": asset.catalog.make,
|
||||
"model": asset.catalog.model,
|
||||
"type": asset.catalog.vehicle_class,
|
||||
"factory_data": getattr(asset.catalog, 'factory_data', {})
|
||||
}
|
||||
}
|
||||
|
||||
# 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()
|
||||
|
||||
# 5. Assignment
|
||||
new_assignment = AssetAssignment(
|
||||
asset_id=new_asset.id,
|
||||
organization_id=final_org_id,
|
||||
status="active"
|
||||
)
|
||||
db.add(new_assignment)
|
||||
|
||||
# 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"}
|
||||
))
|
||||
# --- 2. MODUL: PÉNZÜGY (Költségek) ---
|
||||
@router.get("/{asset_id}/costs", response_model=Dict[str, Any])
|
||||
async def get_asset_costs(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Pénzügyi modul: Helyi és EUR alapú összesítő, tételes lista."""
|
||||
stmt = select(AssetCost).where(AssetCost.asset_id == asset_id)
|
||||
costs = (await db.execute(stmt)).scalars().all()
|
||||
|
||||
summary_local = {}
|
||||
summary_eur = {}
|
||||
history = []
|
||||
|
||||
for c in costs:
|
||||
cat = c.cost_type or "OTHER"
|
||||
amt_local = float(c.amount_local)
|
||||
amt_eur = float(c.amount_eur) if c.amount_eur else 0.0
|
||||
|
||||
summary_local[cat] = summary_local.get(cat, 0) + amt_local
|
||||
summary_eur[cat] = summary_eur.get(cat, 0) + amt_eur
|
||||
|
||||
history.append({
|
||||
"id": c.id,
|
||||
"category": cat,
|
||||
"amount_local": amt_local,
|
||||
"currency_local": c.currency_local,
|
||||
"amount_eur": amt_eur,
|
||||
"exchange_rate": float(c.exchange_rate_used) if c.exchange_rate_used else 1.0,
|
||||
"date": c.date
|
||||
})
|
||||
|
||||
return {
|
||||
"total_gross_local": sum(summary_local.values()),
|
||||
"total_gross_eur": sum(summary_eur.values()),
|
||||
"summary_local": summary_local,
|
||||
"summary_eur": summary_eur,
|
||||
"history": history
|
||||
}
|
||||
|
||||
@router.post("/{asset_id}/costs", response_model=AssetCostResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_asset_cost(
|
||||
asset_id: uuid.UUID,
|
||||
cost_in: AssetCostCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Új költség rögzítése.
|
||||
Automatikus: EUR konverzió, Telemetria frissítés, XP jóváírás.
|
||||
"""
|
||||
# Validáció: az asset_id-nak egyeznie kell a path-szal
|
||||
if cost_in.asset_id != asset_id:
|
||||
raise HTTPException(status_code=400, detail="Asset ID mismatch")
|
||||
|
||||
try:
|
||||
await db.commit()
|
||||
await db.refresh(new_asset)
|
||||
|
||||
# 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)
|
||||
|
||||
return new_asset
|
||||
new_cost = await cost_service.record_cost(
|
||||
db=db,
|
||||
cost_in=cost_in,
|
||||
user_id=current_user.id
|
||||
)
|
||||
return new_cost
|
||||
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.")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# --- 3. MODUL: TELEMETRIA (Állapot) ---
|
||||
@router.get("/{asset_id}/telemetry", response_model=Dict[str, Any])
|
||||
async def get_asset_telemetry(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Műszaki állapot: KM óra, VQI (Quality) és DBS (Driving) pontszámok."""
|
||||
stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == asset_id)
|
||||
tel = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not tel:
|
||||
return {"current_mileage": 0, "vqi_score": 100.0, "dbs_score": 100.0}
|
||||
|
||||
return {
|
||||
"current_mileage": tel.current_mileage,
|
||||
"vqi_score": float(tel.vqi_score),
|
||||
"dbs_score": float(tel.dbs_score),
|
||||
"last_update": tel.updated_at if hasattr(tel, 'updated_at') else None
|
||||
}
|
||||
Reference in New Issue
Block a user