From 913e4705b1343499995a2c59993fa559ee7afee7 Mon Sep 17 00:00:00 2001 From: padreug Date: Mon, 15 Dec 2025 00:19:49 +0100 Subject: [PATCH] Fix amount parsing to handle both @ and @@ SATS notation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pending expense entries use per-unit price notation (@ SATS) while migrated entries use total price notation (@@ SATS). Formats handled: - "50.00 EUR @@ 50000 SATS" - total price (multiply = amount) - "50.00 EUR @ 1000.5 SATS" - per-unit price (multiply for total) - "50.00 EUR" with metadata - legacy format - "50000 SATS" - old SATS-first format 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- fava_client.py | 118 +++++++++++++++++++++++++++++++++++++------------ views_api.py | 77 +++++++++++++++++++++++--------- 2 files changed, 144 insertions(+), 51 deletions(-) diff --git a/fava_client.py b/fava_client.py index 601907b..5c66593 100644 --- a/fava_client.py +++ b/fava_client.py @@ -224,27 +224,61 @@ class FavaClient: continue import re - # 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) + + # Try total price notation: "50.00 EUR @@ 50000 SATS" + total_price_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})\s+@@\s+(-?\d+)\s+SATS$', amount_str) + # Try per-unit price notation: "50.00 EUR @ 1000.5 SATS" + unit_price_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})\s+@\s+([\d.]+)\s+SATS$', amount_str) + + if total_price_match: + fiat_amount = Decimal(total_price_match.group(1)) + fiat_currency = total_price_match.group(2) + sats_amount = int(total_price_match.group(3)) if fiat_currency not in fiat_balances: fiat_balances[fiat_currency] = Decimal(0) - fiat_balances[fiat_currency] += fiat_amount - # 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 + 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 + + elif unit_price_match: + fiat_amount = Decimal(unit_price_match.group(1)) + fiat_currency = unit_price_match.group(2) + sats_per_unit = Decimal(unit_price_match.group(3)) + sats_amount = int(fiat_amount * sats_per_unit) + + if fiat_currency not in fiat_balances: + fiat_balances[fiat_currency] = Decimal(0) + fiat_balances[fiat_currency] += fiat_amount + + 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 + + # Try simple fiat format: "50.00 EUR" (check metadata for sats) + elif re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str): + fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str) + if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'): + 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) + fiat_balances[fiat_currency] += fiat_amount + + # Also track SATS equivalent from metadata if available (legacy) + 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 @@ -347,24 +381,50 @@ class FavaClient: continue import re - # 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) + + # Try total price notation: "50.00 EUR @@ 50000 SATS" + total_price_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})\s+@@\s+(-?\d+)\s+SATS$', amount_str) + # Try per-unit price notation: "50.00 EUR @ 1000.5 SATS" + unit_price_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})\s+@\s+([\d.]+)\s+SATS$', amount_str) + + if total_price_match: + fiat_amount = Decimal(total_price_match.group(1)) + fiat_currency = total_price_match.group(2) + sats_amount = int(total_price_match.group(3)) if fiat_currency not in user_data[user_id]["fiat_balances"]: user_data[user_id]["fiat_balances"][fiat_currency] = Decimal(0) - user_data[user_id]["fiat_balances"][fiat_currency] += fiat_amount + user_data[user_id]["balance"] += sats_amount - # 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) - user_data[user_id]["balance"] += sats_amount + elif unit_price_match: + fiat_amount = Decimal(unit_price_match.group(1)) + fiat_currency = unit_price_match.group(2) + sats_per_unit = Decimal(unit_price_match.group(3)) + sats_amount = int(fiat_amount * sats_per_unit) + + if fiat_currency not in user_data[user_id]["fiat_balances"]: + user_data[user_id]["fiat_balances"][fiat_currency] = Decimal(0) + user_data[user_id]["fiat_balances"][fiat_currency] += fiat_amount + user_data[user_id]["balance"] += sats_amount + + # Try simple fiat format: "50.00 EUR" (check metadata for sats) + elif re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str): + fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str) + if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'): + fiat_amount = Decimal(fiat_match.group(1)) + fiat_currency = fiat_match.group(2) + + if fiat_currency not in user_data[user_id]["fiat_balances"]: + user_data[user_id]["fiat_balances"][fiat_currency] = Decimal(0) + user_data[user_id]["fiat_balances"][fiat_currency] += fiat_amount + + # Also track SATS equivalent from metadata if available (legacy) + 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) + user_data[user_id]["balance"] += sats_amount else: # Old format: SATS with cost/price notation diff --git a/views_api.py b/views_api.py index 814d83f..cc1b614 100644 --- a/views_api.py +++ b/views_api.py @@ -508,21 +508,38 @@ async def api_get_user_entries( if isinstance(first_posting, dict): amount_str = first_posting.get("amount", "") - # Parse amount string: can be EUR/USD directly (new format) or "SATS {EUR}" (old format) + # Parse amount string: price notation, simple fiat, or legacy SATS format if isinstance(amount_str, str) and amount_str: import re - # Try EUR/USD format first (new format: "37.22 EUR") - 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 fiat amount (new approach) - fiat_amount = abs(float(fiat_match.group(1))) - fiat_currency = fiat_match.group(2) - # Get SATS from metadata - posting_meta = first_posting.get("meta", {}) - sats_equiv = posting_meta.get("sats-equivalent") - if sats_equiv: - amount_sats = abs(int(sats_equiv)) + # Try total price notation: "50.00 EUR @@ 50000 SATS" + total_price_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})\s+@@\s+(\d+)\s+SATS$', amount_str) + # Try per-unit price notation: "50.00 EUR @ 1000.5 SATS" + unit_price_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})\s+@\s+([\d.]+)\s+SATS$', amount_str) + + if total_price_match: + fiat_amount = abs(float(total_price_match.group(1))) + fiat_currency = total_price_match.group(2) + amount_sats = abs(int(total_price_match.group(3))) + elif unit_price_match: + fiat_amount = abs(float(unit_price_match.group(1))) + fiat_currency = unit_price_match.group(2) + sats_per_unit = float(unit_price_match.group(3)) + amount_sats = abs(int(fiat_amount * sats_per_unit)) + + # Try simple fiat format: "50.00 EUR" (check metadata for sats) + elif re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str): + fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str) + if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'): + fiat_amount = abs(float(fiat_match.group(1))) + fiat_currency = fiat_match.group(2) + + # Get SATS from metadata (legacy) + posting_meta = first_posting.get("meta", {}) + sats_equiv = posting_meta.get("sats-equivalent") + if sats_equiv: + amount_sats = abs(int(sats_equiv)) + else: # Old format: "36791 SATS {33.33 EUR, 2025-11-09}" or "36791 SATS" sats_match = re.match(r'^(-?\d+)\s+SATS', amount_str) @@ -781,17 +798,33 @@ async def api_get_pending_entries( if isinstance(amount_str, str) and amount_str: import re - # Try EUR/USD format first (new architecture): "50.00 EUR" - fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str) - if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'): - fiat_amount = abs(float(fiat_match.group(1))) - fiat_currency = fiat_match.group(2) + # Try total price notation: "50.00 EUR @@ 50000 SATS" + total_price_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})\s+@@\s+(\d+)\s+SATS$', amount_str) + # Try per-unit price notation: "50.00 EUR @ 1000.5 SATS" + unit_price_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})\s+@\s+([\d.]+)\s+SATS$', amount_str) - # Extract sats equivalent from metadata - posting_meta = first_posting.get("meta", {}) - sats_equiv = posting_meta.get("sats-equivalent") - if sats_equiv: - amount_sats = abs(int(sats_equiv)) + if total_price_match: + fiat_amount = abs(float(total_price_match.group(1))) + fiat_currency = total_price_match.group(2) + amount_sats = abs(int(total_price_match.group(3))) + elif unit_price_match: + fiat_amount = abs(float(unit_price_match.group(1))) + fiat_currency = unit_price_match.group(2) + sats_per_unit = float(unit_price_match.group(3)) + amount_sats = abs(int(fiat_amount * sats_per_unit)) + + # Try simple fiat format: "50.00 EUR" (check metadata for sats) + elif re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str): + fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str) + if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'): + fiat_amount = abs(float(fiat_match.group(1))) + fiat_currency = fiat_match.group(2) + + # Extract sats equivalent from metadata (legacy) + posting_meta = first_posting.get("meta", {}) + sats_equiv = posting_meta.get("sats-equivalent") + if sats_equiv: + amount_sats = abs(int(sats_equiv)) else: # Legacy SATS format: "36791 SATS {33.33 EUR, 2025-11-09}" or "36791 SATS"