204 lines
8.1 KiB
Python
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,
|
|
)
|