diff --git a/fava_client.py b/fava_client.py index ab8c57a..0fb28b5 100644 --- a/fava_client.py +++ b/fava_client.py @@ -296,37 +296,36 @@ class FavaClient: ] Note: - Excludes pending transactions (flag='!') from balance calculation. + Excludes pending transactions (flag='!') and voided (tag #voided) from balance calculation. Only cleared/completed transactions (flag='*') are included. """ - query = """ - SELECT account, sum(position) - WHERE account ~ 'Payable:User-|Receivable:User-' AND flag != '!' - GROUP BY account - """ + # Get all journal entries and calculate balances from postings + all_entries = await self.get_journal_entries() - try: - async with httpx.AsyncClient(timeout=self.timeout) as client: - response = await client.get( - f"{self.base_url}/query", - params={"query_string": query} - ) - response.raise_for_status() - data = response.json() + # Group by user_id + user_data = {} - # Group by user_id - user_data = {} + for entry in all_entries: + # Skip non-transactions, pending (!), and voided + if entry.get("t") != "Transaction": + continue + if entry.get("flag") == "!": + continue + if "voided" in entry.get("tags", []): + continue - for row in data['data']['rows']: - account_name = row[0] - positions = row[1] + # Process postings + for posting in entry.get("postings", []): + account_name = posting.get("account", "") + + # Only process user accounts (Payable or Receivable) + if ":User-" not in account_name: + continue + if "Payable" not in account_name and "Receivable" not in account_name: + continue # Extract user_id from account name - # e.g., "Liabilities:Payable:User-abc123" → "abc123..." - if ":User-" in account_name: - user_id = account_name.split(":User-")[1] - else: - continue + user_id = account_name.split(":User-")[1] if user_id not in user_data: user_data[user_id] = { @@ -336,58 +335,39 @@ class FavaClient: "accounts": [] } - account_info = {"account": account_name, "sats": 0, "positions": positions} + # Parse amount string: "36791 SATS {33.33 EUR, 2025-11-09}" + amount_str = posting.get("amount", "") + if not isinstance(amount_str, str) or not amount_str: + continue - # Process positions - if isinstance(positions, dict) and "SATS" in positions: - sats_positions = positions["SATS"] + import re + # Extract SATS amount + sats_match = re.match(r'^(-?\d+)\s+SATS', amount_str) + if sats_match: + sats_amount = int(sats_match.group(1)) - if isinstance(sats_positions, dict): - for cost_str, amount in sats_positions.items(): - amount_int = int(amount) + # Negate Beancount balance for user perspective + # Payable (liability): negative in Beancount = castle owes user (positive for user) + # Receivable (asset): positive in Beancount = user owes castle (negative for user) + adjusted_amount = -sats_amount + user_data[user_id]["balance"] += adjusted_amount - # Negate Beancount balance for user perspective - adjusted_amount = -amount_int - user_data[user_id]["balance"] += adjusted_amount - account_info["sats"] += adjusted_amount + # Extract fiat from cost syntax: {33.33 EUR, ...} + cost_match = re.search(r'\{([\d.]+)\s+([A-Z]+)', amount_str) + if cost_match: + fiat_amount_raw = Decimal(cost_match.group(1)) + fiat_currency = cost_match.group(2) - # Extract fiat - if cost_str and cost_str != "SATS": - cost_clean = cost_str.strip('{}') - parts = cost_clean.split() - if len(parts) == 2: - try: - fiat_amount = Decimal(parts[0]) - fiat_currency = parts[1] + if fiat_currency not in user_data[user_id]["fiat_balances"]: + user_data[user_id]["fiat_balances"][fiat_currency] = Decimal(0) - if fiat_currency not in user_data[user_id]["fiat_balances"]: - user_data[user_id]["fiat_balances"][fiat_currency] = Decimal(0) + # Apply same sign logic as sats + if "-" in amount_str: + fiat_amount_raw = -fiat_amount_raw + adjusted_fiat = -fiat_amount_raw + user_data[user_id]["fiat_balances"][fiat_currency] += adjusted_fiat - # Apply sign from amount to fiat - if amount_int < 0: - fiat_amount = -fiat_amount - adjusted_fiat = -fiat_amount - user_data[user_id]["fiat_balances"][fiat_currency] += adjusted_fiat - except (ValueError, IndexError): - pass - - elif isinstance(sats_positions, (int, float)): - amount_int = int(sats_positions) - # Negate Beancount balance for user perspective - adjusted_amount = -amount_int - user_data[user_id]["balance"] += adjusted_amount - account_info["sats"] += adjusted_amount - - user_data[user_id]["accounts"].append(account_info) - - return list(user_data.values()) - - except httpx.HTTPStatusError as e: - logger.error(f"Fava query error: {e.response.status_code} - {e.response.text}") - raise - except httpx.RequestError as e: - logger.error(f"Fava connection error: {e}") - raise + return list(user_data.values()) async def check_fava_health(self) -> bool: """