egyedi jármű szerkesztés előtti mentés

This commit is contained in:
Roo
2026-06-12 07:56:15 +00:00
parent 0a3fd8de74
commit ef8df9608c
29 changed files with 3863 additions and 396 deletions

View File

@@ -4,6 +4,7 @@ import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, text, or_
from sqlalchemy.orm import selectinload
@@ -17,11 +18,81 @@ from app.services.asset_service import AssetService
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
from app.schemas.asset import AssetResponse, AssetCreate, AssetUpdate
class ArchiveVehicleRequest(BaseModel):
"""Payload a jármű archiválásához (Soft Delete)."""
final_mileage: int = Field(..., ge=0, description="Utolsó km óra állás")
archive_reason: str = Field(..., min_length=1, max_length=100, description="Eltávolítás oka")
router = APIRouter()
logger = logging.getLogger(__name__)
@router.get("/vehicles/quota-status")
async def get_vehicle_quota_status(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Preemptive quota check endpoint for the frontend.
Returns whether the user can add a new vehicle BEFORE they start the wizard.
GET /api/v1/assets/vehicles/quota-status
Returns:
{ "can_add": bool, "current_count": int, "limit": int }
"""
try:
# Determine organization ID based on user's active scope (garage isolation)
org_id = None
if current_user.scope_id is not None:
try:
org_id = int(current_user.scope_id)
except (ValueError, TypeError):
pass
# Get the user's vehicle limit
allowed_limit = await AssetService.get_user_vehicle_limit(
db, current_user.id, org_id
)
# SAFETY: Ensure limit is at least 1
allowed_limit = max(allowed_limit or 1, 1)
# Count only active vehicles (draft vehicles don't count toward the limit)
from sqlalchemy import func
if org_id is not None:
count_stmt = select(func.count(Asset.id)).where(
Asset.current_organization_id == org_id,
Asset.owner_person_id == current_user.person_id,
Asset.status == "active"
)
else:
count_stmt = select(func.count(Asset.id)).where(
Asset.current_organization_id.is_(None),
Asset.owner_person_id == current_user.person_id,
Asset.status == "active"
)
current_count = (await db.execute(count_stmt)).scalar() or 0
can_add = current_count < allowed_limit
logger.info(
f"Quota check for user {current_user.id} (org={org_id}): "
f"current_count={current_count}, allowed_limit={allowed_limit}, can_add={can_add}"
)
return {
"can_add": can_add,
"current_count": current_count,
"limit": allowed_limit
}
except Exception as e:
logger.error(f"Quota status check error for user {current_user.id}: {e}")
# Fail open — if we can't check quota, allow the user to proceed
return {"can_add": True, "current_count": 0, "limit": 9999}
@router.get("/vehicles/check")
async def check_vehicle_exists(
license_plate: str,
@@ -286,14 +357,42 @@ async def create_or_claim_vehicle(
f"Setting data_status='draft' for transfer scenario."
)
# Determine organization ID based on user's active scope (garage isolation)
org_id = None
if current_user.scope_id is not None:
# ── 🛡️ KETTŐS VÉDELEM: Szervezet hozzárendelés ──
# 1. PRIORITÁS: A frontend által küldött organization_id (ha van)
# 2. FALLBACK: A felhasználó aktív scope-ja (scope_id)
# 3. VASÁJTÓ: Ha egyik sincs, lekérdezzük a fleet.organization_members táblából
org_id = payload.organization_id
if org_id is None and current_user.scope_id is not None:
try:
org_id = int(current_user.scope_id)
except (ValueError, TypeError):
pass
# VASÁJTÓ FALLBACK: Ha még mindig nincs org_id, keressük meg a felhasználó
# első elérhető szervezetét a fleet.organization_members táblában
if org_id is None:
from app.models.marketplace.organization import OrganizationMember
from sqlalchemy import select
member_stmt = (
select(OrganizationMember.organization_id)
.where(OrganizationMember.user_id == current_user.id)
.limit(1)
)
member_result = await db.execute(member_stmt)
member_row = member_result.first()
if member_row:
org_id = member_row[0]
logger.info(
f"Iron Door fallback: assigned org_id={org_id} from "
f"organization_members for user {current_user.id}"
)
else:
logger.warning(
f"Iron Door fallback: user {current_user.id} has no "
f"organization memberships — asset will be created without org"
)
asset = await AssetService.create_or_claim_vehicle(
db=db,
user_id=current_user.id,
@@ -591,4 +690,46 @@ async def create_maintenance_record(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error while creating maintenance record"
)
@router.post("/vehicles/{vehicle_id}/archive", response_model=AssetResponse)
async def archive_vehicle(
vehicle_id: uuid.UUID,
payload: ArchiveVehicleRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Jármű biztonságos kivezetése (Strict Soft Delete).
POST /api/v1/assets/vehicles/{vehicle_id}/archive
A végpont:
1. Frissíti a jármű current_mileage értékét a megadott final_mileage-ra
2. Átállítja a státuszt 'archived'-ra
3. Nullázza a tulajdonosi mezőket (owner_person_id, owner_org_id)
4. Elmenti az archive_info metaadatokat az individual_equipment JSONB-be
5. Naplózza a biztonsági auditba
Payload:
- final_mileage: int (utolsó km óra állás)
- archive_reason: string (Eladás, Gazdasági totálkár / Bontás, Lízing/Bérlet lejárta, Téves rögzítés, Egyéb)
"""
try:
asset = await AssetService.archive_vehicle(
db=db,
asset_id=vehicle_id,
user_id=current_user.id,
final_mileage=payload.final_mileage,
archive_reason=payload.archive_reason,
)
return asset
except HTTPException:
raise
except Exception as e:
logger.error(f"Vehicle archive error for {vehicle_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Belső szerverhiba a jármű archiválásakor"
)