Refactors expense tracking to use fiat amounts
Updates the expense tracking system to store payables and receivables in fiat currency within Beancount. This ensures accurate debt representation and simplifies balance calculations. Changes include: - Converting `format_expense_entry` and `format_receivable_entry` to use fiat amounts. - Introducing `format_net_settlement_entry` for net settlement payments. - Modifying `format_payment_entry` to use cost syntax for fiat tracking. - Adjusting Fava client to correctly process new amount formats and metadata. - Adding average cost basis posting format The use of fiat amounts and cost basis aims to provide better accuracy and compatibility with existing Beancount workflows.
This commit is contained in:
parent
8396331d5a
commit
ca2ce1dfcc
3 changed files with 372 additions and 123 deletions
|
|
@ -193,9 +193,12 @@ class FavaClient:
|
|||
# Get all journal entries for this user
|
||||
all_entries = await self.get_journal_entries()
|
||||
|
||||
logger.info(f"Processing {len(all_entries)} journal entries for user {user_id[:8]}")
|
||||
|
||||
total_sats = 0
|
||||
fiat_balances = {}
|
||||
accounts_dict = {} # Track balances per account
|
||||
processed_count = 0
|
||||
|
||||
for entry in all_entries:
|
||||
# Skip non-transactions, pending (!), and voided
|
||||
|
|
@ -206,6 +209,11 @@ class FavaClient:
|
|||
if "voided" in entry.get("tags", []):
|
||||
continue
|
||||
|
||||
# Check if this entry has any User-375ec158 postings
|
||||
has_user_posting = any(f":User-{user_id[:8]}" in p.get("account", "") for p in entry.get("postings", []))
|
||||
if has_user_posting:
|
||||
logger.info(f"Entry '{entry.get('narration', 'N/A')}' has {len(entry.get('postings', []))} postings")
|
||||
|
||||
# Process postings for this user
|
||||
for posting in entry.get("postings", []):
|
||||
account_name = posting.get("account", "")
|
||||
|
|
@ -216,46 +224,78 @@ class FavaClient:
|
|||
if "Payable" not in account_name and "Receivable" not in account_name:
|
||||
continue
|
||||
|
||||
# Parse amount string: "36791 SATS {33.33 EUR}"
|
||||
# Parse amount string: can be EUR, USD, or SATS
|
||||
amount_str = posting.get("amount", "")
|
||||
if not isinstance(amount_str, str) or not amount_str:
|
||||
continue
|
||||
|
||||
logger.info(f"Processing posting in {account_name}: amount_str='{amount_str}'")
|
||||
processed_count += 1
|
||||
|
||||
import re
|
||||
# Extract SATS amount (with sign)
|
||||
sats_match = re.match(r'^(-?\d+)\s+SATS', amount_str)
|
||||
if sats_match:
|
||||
sats_amount = int(sats_match.group(1))
|
||||
total_sats += sats_amount
|
||||
|
||||
# Track per account
|
||||
if account_name not in accounts_dict:
|
||||
accounts_dict[account_name] = {"account": account_name, "sats": 0}
|
||||
accounts_dict[account_name]["sats"] += sats_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_unsigned = Decimal(cost_match.group(1))
|
||||
fiat_currency = cost_match.group(2)
|
||||
# Try to extract EUR/USD amount first (new format)
|
||||
fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str)
|
||||
if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'):
|
||||
# Direct EUR/USD amount (new approach)
|
||||
fiat_amount = Decimal(fiat_match.group(1))
|
||||
fiat_currency = fiat_match.group(2)
|
||||
|
||||
if fiat_currency not in fiat_balances:
|
||||
fiat_balances[fiat_currency] = Decimal(0)
|
||||
|
||||
# Apply the same sign as the SATS amount
|
||||
if sats_match:
|
||||
sats_amount_for_sign = int(sats_match.group(1))
|
||||
if sats_amount_for_sign < 0:
|
||||
fiat_amount_unsigned = -fiat_amount_unsigned
|
||||
fiat_balances[fiat_currency] += fiat_amount
|
||||
logger.info(f"Found fiat in {account_name}: {fiat_amount} {fiat_currency} (direct), running total: {fiat_balances[fiat_currency]}")
|
||||
|
||||
fiat_balances[fiat_currency] += fiat_amount_unsigned
|
||||
logger.info(f"Found fiat in {account_name}: {fiat_amount_unsigned} {fiat_currency}, running total: {fiat_balances[fiat_currency]}")
|
||||
# Also track SATS equivalent from metadata if available
|
||||
posting_meta = posting.get("meta", {})
|
||||
sats_equiv = posting_meta.get("sats-equivalent")
|
||||
if sats_equiv:
|
||||
sats_amount = int(sats_equiv) if fiat_amount > 0 else -int(sats_equiv)
|
||||
total_sats += sats_amount
|
||||
if account_name not in accounts_dict:
|
||||
accounts_dict[account_name] = {"account": account_name, "sats": 0}
|
||||
accounts_dict[account_name]["sats"] += sats_amount
|
||||
|
||||
else:
|
||||
# Old format: SATS with cost/price notation - extract SATS amount
|
||||
sats_match = re.match(r'^(-?\d+)\s+SATS', amount_str)
|
||||
if sats_match:
|
||||
sats_amount = int(sats_match.group(1))
|
||||
total_sats += sats_amount
|
||||
|
||||
# Track per account
|
||||
if account_name not in accounts_dict:
|
||||
accounts_dict[account_name] = {"account": account_name, "sats": 0}
|
||||
accounts_dict[account_name]["sats"] += sats_amount
|
||||
|
||||
# Try to extract fiat from metadata or cost syntax (backward compatibility)
|
||||
posting_meta = posting.get("meta", {})
|
||||
fiat_amount_total_str = posting_meta.get("fiat-amount-total")
|
||||
fiat_currency_meta = posting_meta.get("fiat-currency")
|
||||
|
||||
if fiat_amount_total_str and fiat_currency_meta:
|
||||
# Use exact total from metadata
|
||||
fiat_total = Decimal(fiat_amount_total_str)
|
||||
fiat_currency = fiat_currency_meta
|
||||
|
||||
if fiat_currency not in fiat_balances:
|
||||
fiat_balances[fiat_currency] = Decimal(0)
|
||||
|
||||
# Apply the same sign as the SATS amount
|
||||
if sats_match:
|
||||
sats_amount_for_sign = int(sats_match.group(1))
|
||||
if sats_amount_for_sign < 0:
|
||||
fiat_total = -fiat_total
|
||||
|
||||
fiat_balances[fiat_currency] += fiat_total
|
||||
logger.info(f"Found fiat in {account_name}: {fiat_total} {fiat_currency} (from SATS metadata), running total: {fiat_balances[fiat_currency]}")
|
||||
|
||||
result = {
|
||||
"balance": total_sats,
|
||||
"fiat_balances": fiat_balances,
|
||||
"accounts": list(accounts_dict.values())
|
||||
}
|
||||
logger.info(f"Processed {processed_count} postings for user {user_id[:8]}")
|
||||
logger.info(f"Returning balance for user {user_id[:8]}: sats={total_sats}, fiat_balances={fiat_balances}")
|
||||
return result
|
||||
|
||||
|
|
@ -502,7 +542,14 @@ class FavaClient:
|
|||
response = await client.get(f"{self.base_url}/journal")
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
return result.get("data", [])
|
||||
entries = result.get("data", [])
|
||||
logger.info(f"Fava /journal returned {len(entries)} entries")
|
||||
|
||||
# Log transactions with "Lightning payment" in narration
|
||||
lightning_entries = [e for e in entries if "Lightning payment" in e.get("narration", "")]
|
||||
logger.info(f"Found {len(lightning_entries)} Lightning payment entries in journal")
|
||||
|
||||
return entries
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Fava journal error: {e.response.status_code} - {e.response.text}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue