Files
service-finder/backend/app/api/v1/endpoints/dictionaries.py
2026-06-24 11:29:45 +00:00

270 lines
9.0 KiB
Python

# /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()