diff --git a/fava_client.py b/fava_client.py index 572cd95..277120e 100644 --- a/fava_client.py +++ b/fava_client.py @@ -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. diff --git a/models.py b/models.py index 25323dd..22d4503 100644 --- a/models.py +++ b/models.py @@ -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): diff --git a/views_api.py b/views_api.py index dc0dc69..3e139d6 100644 --- a/views_api.py +++ b/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"], )