Add lifetime income/expense totals to UserBalance
New get_user_lifetime_totals_bql() runs tag-filtered BQL queries
(Payable + expense-entry, Receivable + income-entry) to compute
per-user lifetime totals separately from the net balance. Plumbed
through /api/v1/balance and /api/v1/balance/{user_id}; existing
clients keep working (fields default to zero / empty dict).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
483e89163e
commit
deeec7e2c5
3 changed files with 82 additions and 0 deletions
|
|
@ -827,6 +827,66 @@ class FavaClient:
|
|||
"accounts": accounts
|
||||
}
|
||||
|
||||
async def get_user_lifetime_totals_bql(self, user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get lifetime totals of expenses submitted and income recorded by this user.
|
||||
|
||||
Sums original entries only (tag-filtered) — does not net against payments
|
||||
or other reconciliation activity, so totals match "amounts ever entered".
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
|
||||
Returns:
|
||||
{
|
||||
"total_expenses_sats": int,
|
||||
"total_expenses_fiat": {"EUR": Decimal("...")},
|
||||
"total_income_sats": int,
|
||||
"total_income_fiat": {"EUR": Decimal("...")},
|
||||
}
|
||||
"""
|
||||
from decimal import Decimal
|
||||
|
||||
user_id_prefix = user_id[:8]
|
||||
|
||||
async def _sum_for(account_pattern: str, tag: str):
|
||||
query = f"""
|
||||
SELECT currency, sum(number), sum(weight)
|
||||
WHERE account ~ '{account_pattern}:User-{user_id_prefix}'
|
||||
AND '{tag}' IN tags
|
||||
AND flag = '*'
|
||||
GROUP BY currency
|
||||
"""
|
||||
result = await self.query_bql(query)
|
||||
sats_total = 0
|
||||
fiat_total: Dict[str, Decimal] = {}
|
||||
for row in result["rows"]:
|
||||
currency, number_sum, weight_sum = row
|
||||
# Skip SATS-currency rows (payment/reconciliation legs)
|
||||
if currency == "SATS":
|
||||
continue
|
||||
if isinstance(weight_sum, dict) and "SATS" in weight_sum:
|
||||
sats_total += abs(int(Decimal(str(weight_sum["SATS"]))))
|
||||
fiat_amount = abs(Decimal(str(number_sum))) if number_sum else Decimal(0)
|
||||
if fiat_amount > 0:
|
||||
fiat_total[currency] = fiat_total.get(currency, Decimal(0)) + fiat_amount
|
||||
return sats_total, fiat_total
|
||||
|
||||
exp_sats, exp_fiat = await _sum_for("Liabilities:Payable", "expense-entry")
|
||||
inc_sats, inc_fiat = await _sum_for("Assets:Receivable", "income-entry")
|
||||
|
||||
logger.info(
|
||||
f"User {user_id[:8]} lifetime totals (BQL): "
|
||||
f"expenses={exp_sats} sats {dict(exp_fiat)}, income={inc_sats} sats {dict(inc_fiat)}"
|
||||
)
|
||||
|
||||
return {
|
||||
"total_expenses_sats": exp_sats,
|
||||
"total_expenses_fiat": exp_fiat,
|
||||
"total_income_sats": inc_sats,
|
||||
"total_income_fiat": inc_fiat,
|
||||
}
|
||||
|
||||
async def get_all_user_balances_bql(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get balances for all users using BQL with currency-grouped aggregation.
|
||||
|
|
|
|||
|
|
@ -90,6 +90,11 @@ class UserBalance(BaseModel):
|
|||
balance: int # positive = libra owes user, negative = user owes libra
|
||||
accounts: list[Account] = []
|
||||
fiat_balances: dict[str, Decimal] = {} # e.g. {"EUR": Decimal("250.0"), "USD": Decimal("100.0")}
|
||||
# Lifetime totals (original entries only; not net of reconciliation)
|
||||
total_expenses_sats: int = 0
|
||||
total_expenses_fiat: dict[str, Decimal] = {}
|
||||
total_income_sats: int = 0
|
||||
total_income_fiat: dict[str, Decimal] = {}
|
||||
|
||||
|
||||
class ExpenseEntry(BaseModel):
|
||||
|
|
|
|||
17
views_api.py
17
views_api.py
|
|
@ -1591,22 +1591,34 @@ async def api_get_my_balance(
|
|||
# Add all balances (positive and negative)
|
||||
total_fiat_balances[currency] += amount
|
||||
|
||||
# Super-user totals reflect their personal submissions (if any), not org-wide
|
||||
super_totals = await fava.get_user_lifetime_totals_bql(wallet.wallet.user)
|
||||
|
||||
# Return net position
|
||||
return UserBalance(
|
||||
user_id=wallet.wallet.user,
|
||||
balance=net_balance,
|
||||
accounts=[],
|
||||
fiat_balances=total_fiat_balances,
|
||||
total_expenses_sats=super_totals["total_expenses_sats"],
|
||||
total_expenses_fiat=super_totals["total_expenses_fiat"],
|
||||
total_income_sats=super_totals["total_income_sats"],
|
||||
total_income_fiat=super_totals["total_income_fiat"],
|
||||
)
|
||||
|
||||
# For regular users, show their individual balance from Fava
|
||||
balance_data = await fava.get_user_balance_bql(wallet.wallet.user)
|
||||
totals = await fava.get_user_lifetime_totals_bql(wallet.wallet.user)
|
||||
|
||||
return UserBalance(
|
||||
user_id=wallet.wallet.user,
|
||||
balance=balance_data["balance"],
|
||||
accounts=[], # Could populate from balance_data["accounts"] if needed
|
||||
fiat_balances=balance_data["fiat_balances"],
|
||||
total_expenses_sats=totals["total_expenses_sats"],
|
||||
total_expenses_fiat=totals["total_expenses_fiat"],
|
||||
total_income_sats=totals["total_income_sats"],
|
||||
total_income_fiat=totals["total_income_fiat"],
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1627,12 +1639,17 @@ async def api_get_user_balance(
|
|||
|
||||
fava = get_fava_client()
|
||||
balance_data = await fava.get_user_balance_bql(user_id)
|
||||
totals = await fava.get_user_lifetime_totals_bql(user_id)
|
||||
|
||||
return UserBalance(
|
||||
user_id=user_id,
|
||||
balance=balance_data["balance"],
|
||||
accounts=[],
|
||||
fiat_balances=balance_data["fiat_balances"],
|
||||
total_expenses_sats=totals["total_expenses_sats"],
|
||||
total_expenses_fiat=totals["total_expenses_fiat"],
|
||||
total_income_sats=totals["total_income_sats"],
|
||||
total_income_fiat=totals["total_income_fiat"],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue