# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/finance_admin.py """ Finance Admin API endpoints for managing Issuers and FinancialLedger with strict RBAC protection. Protected by DB-driven RequirePermission("finance:view") dependency. """ import logging from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, text from typing import List, Optional from app.api import deps from app.models.identity import User from app.models.system.audit import FinancialLedger from app.schemas.finance import IssuerResponse, IssuerUpdate, FinancialLedgerListResponse, FinancialLedgerResponse logger = logging.getLogger(__name__) router = APIRouter() # ────────────────────────────────────────────────────────────────────────────── # Issuer Endpoints (existing) # ────────────────────────────────────────────────────────────────────────────── @router.get("/", response_model=List[IssuerResponse], tags=["finance-admin"]) async def list_issuers( db: AsyncSession = Depends(deps.get_db), current_user: User = Depends(deps.get_current_active_user), _ = Depends(deps.RequirePermission("finance:view")), ): """ List all Issuers (billing entities). Protected by RequirePermission("finance:view"). """ from app.models.marketplace.finance import Issuer result = await db.execute(select(Issuer).order_by(Issuer.id)) issuers = result.scalars().all() return issuers @router.patch("/{issuer_id}", response_model=IssuerResponse, tags=["finance-admin"]) async def update_issuer( issuer_id: int, issuer_update: IssuerUpdate, db: AsyncSession = Depends(deps.get_db), current_user: User = Depends(deps.get_current_active_user), _ = Depends(deps.RequirePermission("finance:edit")), ): """ Update an Issuer's details (activate/deactivate, revenue limit, API config). Protected by RequirePermission("finance:edit"). """ from app.models.marketplace.finance import Issuer result = await db.execute(select(Issuer).where(Issuer.id == issuer_id)) issuer = result.scalar_one_or_none() if not issuer: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Issuer with ID {issuer_id} not found." ) # Update fields if provided update_data = issuer_update.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(issuer, field, value) await db.commit() await db.refresh(issuer) return issuer # ────────────────────────────────────────────────────────────────────────────── # FinancialLedger Admin Endpoints (NEW) # ────────────────────────────────────────────────────────────────────────────── @router.get("/ledger", response_model=FinancialLedgerListResponse, tags=["finance-admin"]) async def list_financial_ledger( page: int = Query(1, ge=1, description="Oldalszám (1-indexelt)"), page_size: int = Query(50, ge=1, le=200, description="Rekordok száma oldalanként"), user_id: Optional[int] = Query(None, description="Szűrés felhasználó ID alapján"), transaction_type: Optional[str] = Query(None, description="Tranzakció típus szűrés"), entry_type: Optional[str] = Query(None, description="Könyvelési irány (DEBIT / CREDIT)"), wallet_type: Optional[str] = Query(None, description="Tárcatípus szűrés"), date_from: Optional[str] = Query(None, description="Dátum szűrés kezdete (ISO, pl. 2026-01-01)"), date_to: Optional[str] = Query(None, description="Dátum szűrés vége (ISO, pl. 2026-12-31)"), sort_by: str = Query("created_at", description="Rendezési mező (created_at, amount, id)"), sort_order: str = Query("desc", description="Rendezési irány (asc / desc)"), db: AsyncSession = Depends(deps.get_db), current_user: User = Depends(deps.get_current_active_user), _ = Depends(deps.RequirePermission("finance:view")), ): """ List FinancialLedger entries with pagination, filtering, and user details. Uses raw SQL with explicit joins to avoid SQLAlchemy relationship issues. Protected by RequirePermission("finance:view"). Returns paginated results with joined user email and person name. """ # ── Build WHERE clauses dynamically ── where_clauses = [] params = {} if user_id is not None: where_clauses.append("fl.user_id = :user_id") params["user_id"] = user_id if transaction_type is not None: where_clauses.append("fl.transaction_type = :transaction_type") params["transaction_type"] = transaction_type if entry_type is not None: where_clauses.append("fl.entry_type = :entry_type") params["entry_type"] = entry_type if wallet_type is not None: where_clauses.append("fl.wallet_type = :wallet_type") params["wallet_type"] = wallet_type if date_from is not None: where_clauses.append("fl.created_at >= :date_from::timestamp") params["date_from"] = date_from if date_to is not None: where_clauses.append("fl.created_at <= :date_to::timestamp") params["date_to"] = date_to where_sql = " AND ".join(where_clauses) if where_clauses else "TRUE" # Validate sort_by to prevent SQL injection allowed_sort_fields = {"created_at", "amount", "id"} if sort_by not in allowed_sort_fields: sort_by = "created_at" sort_direction = "ASC" if sort_order == "asc" else "DESC" # ── Count query ── count_sql = f""" SELECT COUNT(*) FROM audit.financial_ledger fl WHERE {where_sql} """ count_result = await db.execute(text(count_sql), params) total = count_result.scalar() or 0 # ── Data query with joins ── offset = (page - 1) * page_size data_sql = f""" SELECT fl.id, fl.user_id, fl.person_id, fl.amount, fl.currency, fl.transaction_type, fl.entry_type::text, fl.wallet_type::text, fl.status::text, fl.details, fl.created_at, u.email AS user_email, p.first_name AS person_first_name, p.last_name AS person_last_name FROM audit.financial_ledger fl LEFT JOIN identity.users u ON u.id = fl.user_id LEFT JOIN identity.persons p ON p.id = u.person_id WHERE {where_sql} ORDER BY fl.{sort_by} {sort_direction} LIMIT :limit OFFSET :offset """ params["limit"] = page_size params["offset"] = offset data_result = await db.execute(text(data_sql), params) rows = data_result.all() # ── Build response ── items = [] for row in rows: user_name = None if row.person_first_name or row.person_last_name: parts = [p for p in [row.person_last_name, row.person_first_name] if p] user_name = " ".join(parts) items.append(FinancialLedgerResponse( id=row.id, user_id=row.user_id, person_id=row.person_id, amount=float(row.amount) if row.amount else 0.0, currency=row.currency, transaction_type=row.transaction_type, entry_type=row.entry_type, wallet_type=row.wallet_type, status=row.status, details=row.details, created_at=row.created_at, user_email=row.user_email, user_name=user_name, )) return FinancialLedgerListResponse( total=total, page=page, page_size=page_size, items=items, )