# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/dictionaries.py """ Szótárak és katalógusok végpontjai. - GET /dictionaries/cost-categories: Költségkategóriák lekérése visibility szerint szűrve (P0 3D Capability Matrix: filtered by resolved org capabilities) - CRUD /dictionaries/insurance-providers: Biztosítók katalógusa """ import logging from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from app.db.session import get_db from app.api import deps from app.models.fleet_finance import CostCategory, InsuranceProvider from app.schemas.fleet_finance import ( InsuranceProviderResponse, InsuranceProviderCreate, InsuranceProviderUpdate, ) from app.services.capability_service import resolve_org_capabilities router = APIRouter() logger = logging.getLogger(__name__) @router.get("/cost-categories") async def list_cost_categories( visibility: Optional[str] = Query(None, description="Szűrés láthatóságra: b2c, b2b, both, internal"), db: AsyncSession = Depends(get_db), current_user = Depends(deps.get_current_user) ): """ Költségkategóriák lekérése hierarchikus struktúrában. Opcionálisan szűrhető visibility alapján (pl. ha a frontend csak a 'b2c' és 'both' kategóriákat akarja). P0 3D Capability Matrix: - Resolves the active organization's capability matrix. - If the user is SUPERADMIN (Fast-Pass), all categories are returned. - Otherwise, categories with a required_feature are only included if the resolved capabilities have that feature set to True. """ # ── Determine active organization ── active_org_id: Optional[int] = None if hasattr(current_user, "active_organization_id") and current_user.active_organization_id: active_org_id = current_user.active_organization_id # ── Resolve capabilities ── resolved_caps = await resolve_org_capabilities(current_user, active_org_id, db) stmt = select(CostCategory).order_by(CostCategory.name) if visibility: # Ha 'b2c' van megadva, akkor a 'b2c' ÉS 'both' láthatóságú kategóriákat adjuk vissza if visibility == "b2c": stmt = stmt.where(CostCategory.visibility.in_(["b2c", "both"])) elif visibility == "b2b": stmt = stmt.where(CostCategory.visibility.in_(["b2b", "both"])) else: stmt = stmt.where(CostCategory.visibility == visibility) result = await db.execute(stmt) categories = result.scalars().all() # ── P0 3D Capability Matrix: filter by required_feature ── is_superadmin = resolved_caps.get("_is_superadmin", False) if not is_superadmin: categories = [ cat for cat in categories if not cat.required_feature or resolved_caps.get(cat.required_feature) is True ] # Hierarchikus struktúra építése category_map = {} root_categories = [] for cat in categories: cat_dict = { "id": cat.id, "code": cat.code, "name": cat.name, "description": cat.description, "parent_id": cat.parent_id, "visibility": cat.visibility, "accounting_code": cat.accounting_code, "is_system": cat.is_system, "min_tier": cat.min_tier, "required_feature": cat.required_feature, "children": [], } category_map[cat.id] = cat_dict for cat in categories: cat_dict = category_map[cat.id] if cat.parent_id and cat.parent_id in category_map: category_map[cat.parent_id]["children"].append(cat_dict) else: root_categories.append(cat_dict) return root_categories # ============================================================================= # InsuranceProvider CRUD # ============================================================================= @router.get( "/insurance-providers", response_model=List[InsuranceProviderResponse], ) async def list_insurance_providers( active_only: bool = Query(True, description="Csak az aktív biztosítók listázása"), country_code: Optional[str] = Query(None, max_length=2, description="ISO 3166-1 alpha-2 országkód szerinti szűrés (pl. 'HU'). Visszaadja az adott ország és a globális (NULL) szolgáltatókat."), db: AsyncSession = Depends(get_db), current_user = Depends(deps.get_current_user), ): """ Biztosítók listázása. Alapértelmezés szerint csak az aktív biztosítókat adja vissza (frontend dropdown számára). Opcionálisan szűrhető country_code alapján: ha meg van adva, visszaadja az adott ország szolgáltatóit ÉS a globális (country_code IS NULL) szolgáltatókat is. """ stmt = select(InsuranceProvider).order_by(InsuranceProvider.name) if active_only: stmt = stmt.where(InsuranceProvider.is_active == True) if country_code: stmt = stmt.where( (InsuranceProvider.country_code == country_code) | (InsuranceProvider.country_code.is_(None)) ) result = await db.execute(stmt) providers = result.scalars().all() return providers @router.get( "/insurance-providers/{provider_id}", response_model=InsuranceProviderResponse, ) async def get_insurance_provider( provider_id: int, db: AsyncSession = Depends(get_db), current_user = Depends(deps.get_current_user), ): """ Egy biztosító adatainak lekérése ID alapján. """ stmt = select(InsuranceProvider).where(InsuranceProvider.id == provider_id) result = await db.execute(stmt) provider = result.scalar_one_or_none() if not provider: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Biztosító nem található", ) return provider @router.post( "/insurance-providers", response_model=InsuranceProviderResponse, status_code=status.HTTP_201_CREATED, ) async def create_insurance_provider( payload: InsuranceProviderCreate, db: AsyncSession = Depends(get_db), current_user = Depends(deps.get_current_user), ): """ Új biztosító hozzáadása a katalógushoz. """ # Check for duplicate name existing_stmt = select(InsuranceProvider).where( InsuranceProvider.name == payload.name ) existing_result = await db.execute(existing_stmt) if existing_result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Már létezik biztosító ezzel a névvel: '{payload.name}'", ) provider = InsuranceProvider( name=payload.name, claim_phone=payload.claim_phone, claim_url=payload.claim_url, services_offered=payload.services_offered or {}, is_active=payload.is_active, ) db.add(provider) await db.commit() await db.refresh(provider) return provider @router.put( "/insurance-providers/{provider_id}", response_model=InsuranceProviderResponse, ) async def update_insurance_provider( provider_id: int, payload: InsuranceProviderUpdate, db: AsyncSession = Depends(get_db), current_user = Depends(deps.get_current_user), ): """ Biztosító adatainak módosítása. """ stmt = select(InsuranceProvider).where(InsuranceProvider.id == provider_id) result = await db.execute(stmt) provider = result.scalar_one_or_none() if not provider: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Biztosító nem található", ) # Check for duplicate name if name is being changed if payload.name is not None and payload.name != provider.name: dup_stmt = select(InsuranceProvider).where( InsuranceProvider.name == payload.name, InsuranceProvider.id != provider_id, ) dup_result = await db.execute(dup_stmt) if dup_result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Már létezik biztosító ezzel a névvel: '{payload.name}'", ) update_data = payload.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(provider, field, value) await db.commit() await db.refresh(provider) return provider @router.delete( "/insurance-providers/{provider_id}", status_code=status.HTTP_204_NO_CONTENT, ) async def delete_insurance_provider( provider_id: int, db: AsyncSession = Depends(get_db), current_user = Depends(deps.get_current_user), ): """ Biztosító törlése a katalógusból. """ stmt = select(InsuranceProvider).where(InsuranceProvider.id == provider_id) result = await db.execute(stmt) provider = result.scalar_one_or_none() if not provider: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Biztosító nem található", ) await db.delete(provider) await db.commit()