Files
service-finder/backend/app/api/v1/endpoints/finance_admin.py
2026-07-27 08:39:18 +00:00

204 lines
8.1 KiB
Python

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