From 3777839a5c7602a66add023fd4ff81227c3a5363 Mon Sep 17 00:00:00 2001
From: patrick-motsch
Date: Sun, 15 Feb 2026 10:44:26 +0100
Subject: [PATCH] feat(billing): scope parameter on /view/statistics endpoint
- New query parameter scope: personal/mandate/all
- personal: filters to only current user's transactions (ignores admin role)
- mandate: filters by mandateId parameter
- all: existing RBAC-filtered behavior (default)
Co-authored-by: Cursor
---
modules/routes/routeBilling.py | 41 ++++++++++++++++++++++------------
1 file changed, 27 insertions(+), 14 deletions(-)
diff --git a/modules/routes/routeBilling.py b/modules/routes/routeBilling.py
index f34acbd5..a3800753 100644
--- a/modules/routes/routeBilling.py
+++ b/modules/routes/routeBilling.py
@@ -989,16 +989,17 @@ def getUserViewStatistics(
period: str = Query(default="month", description="Period: 'day' or 'month'"),
year: int = Query(default=None, description="Year"),
month: Optional[int] = Query(None, description="Month (1-12, required for period='day')"),
+ scope: str = Query(default="all", description="Scope: 'personal' (own costs only), 'mandate' (filter by mandateId), 'all' (RBAC-filtered)"),
+ mandateId: Optional[str] = Query(None, description="Mandate ID filter (used with scope='mandate')"),
ctx: RequestContext = Depends(getRequestContext)
) -> ViewStatisticsResponse:
"""
Get aggregated usage statistics across all user's mandates.
- RBAC filtering:
- - SysAdmin: statistics across all mandates
- - Mandate-Admin: statistics for mandates they administrate
- - Feature-Instance-Admin: statistics for their feature instances
- - Regular user: only their own usage statistics
+ Scope:
+ - personal: only the current user's own transactions (ignores admin role)
+ - mandate: transactions for a specific mandate (requires mandateId parameter)
+ - all: RBAC-filtered (SysAdmin sees everything, admin sees mandate, user sees own)
- period='month': returns monthly time series for the given year
- period='day': returns daily time series for the given month/year
@@ -1015,22 +1016,34 @@ def getUserViewStatistics(
billingInterface = getBillingInterface(ctx.user, ctx.mandateId)
# Evaluate RBAC scope
- scope = _getBillingDataScope(ctx.user)
+ rbacScope = _getBillingDataScope(ctx.user)
# Determine mandate IDs for data loading
- if scope.isGlobalAdmin:
- mandateIds = None
+ if rbacScope.isGlobalAdmin:
+ loadMandateIds = None
else:
- mandateIds = scope.adminMandateIds + scope.memberMandateIds
- if not mandateIds:
+ loadMandateIds = rbacScope.adminMandateIds + rbacScope.memberMandateIds
+ if not loadMandateIds:
logger.warning("No mandate IDs found for user")
return ViewStatisticsResponse()
- # Get all transactions
- allTransactions = billingInterface.getUserTransactionsForMandates(mandateIds, limit=10000)
+ # Scope=mandate: restrict to specific mandate
+ if scope == "mandate" and mandateId:
+ loadMandateIds = [mandateId]
- # Apply RBAC filter
- allTransactions = _filterTransactionsByScope(allTransactions, scope)
+ # Get all transactions
+ allTransactions = billingInterface.getUserTransactionsForMandates(loadMandateIds, limit=10000)
+
+ # Apply RBAC filter (respects admin/user roles)
+ allTransactions = _filterTransactionsByScope(allTransactions, rbacScope)
+
+ # Scope=personal: further filter to only own transactions
+ if scope == "personal":
+ userId = str(ctx.user.id)
+ allTransactions = [
+ t for t in allTransactions
+ if (t.get("createdByUserId") or t.get("userId")) == userId
+ ]
logger.info(f"View statistics: {len(allTransactions)} RBAC-filtered transactions for period={period}, year={year}, month={month}")