Fix amount parsing to handle both @ and @@ SATS notation

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 <noreply@anthropic.com>
This commit is contained in:
padreug 2025-12-15 00:19:49 +01:00
parent 7173e051fe
commit 913e4705b1
2 changed files with 144 additions and 51 deletions

View file

@ -224,19 +224,53 @@ class FavaClient:
continue continue
import re import re
# Try to extract EUR/USD amount first (new format)
# 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
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) fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str)
if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'): 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_amount = Decimal(fiat_match.group(1))
fiat_currency = fiat_match.group(2) fiat_currency = fiat_match.group(2)
if fiat_currency not in fiat_balances: if fiat_currency not in fiat_balances:
fiat_balances[fiat_currency] = Decimal(0) fiat_balances[fiat_currency] = Decimal(0)
fiat_balances[fiat_currency] += fiat_amount fiat_balances[fiat_currency] += fiat_amount
# Also track SATS equivalent from metadata if available # Also track SATS equivalent from metadata if available (legacy)
posting_meta = posting.get("meta", {}) posting_meta = posting.get("meta", {})
sats_equiv = posting_meta.get("sats-equivalent") sats_equiv = posting_meta.get("sats-equivalent")
if sats_equiv: if sats_equiv:
@ -347,19 +381,45 @@ class FavaClient:
continue continue
import re import re
# Try to extract EUR/USD amount first (new format)
# 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
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) fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str)
if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'): 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_amount = Decimal(fiat_match.group(1))
fiat_currency = fiat_match.group(2) fiat_currency = fiat_match.group(2)
if fiat_currency not in user_data[user_id]["fiat_balances"]: 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] = Decimal(0)
user_data[user_id]["fiat_balances"][fiat_currency] += fiat_amount user_data[user_id]["fiat_balances"][fiat_currency] += fiat_amount
# Also track SATS equivalent from metadata if available # Also track SATS equivalent from metadata if available (legacy)
posting_meta = posting.get("meta", {}) posting_meta = posting.get("meta", {})
sats_equiv = posting_meta.get("sats-equivalent") sats_equiv = posting_meta.get("sats-equivalent")
if sats_equiv: if sats_equiv:

View file

@ -508,21 +508,38 @@ async def api_get_user_entries(
if isinstance(first_posting, dict): if isinstance(first_posting, dict):
amount_str = first_posting.get("amount", "") 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: if isinstance(amount_str, str) and amount_str:
import re import re
# Try EUR/USD format first (new format: "37.22 EUR")
# 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) fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str)
if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'): 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_amount = abs(float(fiat_match.group(1)))
fiat_currency = fiat_match.group(2) fiat_currency = fiat_match.group(2)
# Get SATS from metadata # Get SATS from metadata (legacy)
posting_meta = first_posting.get("meta", {}) posting_meta = first_posting.get("meta", {})
sats_equiv = posting_meta.get("sats-equivalent") sats_equiv = posting_meta.get("sats-equivalent")
if sats_equiv: if sats_equiv:
amount_sats = abs(int(sats_equiv)) amount_sats = abs(int(sats_equiv))
else: else:
# Old format: "36791 SATS {33.33 EUR, 2025-11-09}" or "36791 SATS" # Old format: "36791 SATS {33.33 EUR, 2025-11-09}" or "36791 SATS"
sats_match = re.match(r'^(-?\d+)\s+SATS', amount_str) sats_match = re.match(r'^(-?\d+)\s+SATS', amount_str)
@ -781,13 +798,29 @@ async def api_get_pending_entries(
if isinstance(amount_str, str) and amount_str: if isinstance(amount_str, str) and amount_str:
import re import re
# Try EUR/USD format first (new architecture): "50.00 EUR" # 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) fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str)
if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'): if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'):
fiat_amount = abs(float(fiat_match.group(1))) fiat_amount = abs(float(fiat_match.group(1)))
fiat_currency = fiat_match.group(2) fiat_currency = fiat_match.group(2)
# Extract sats equivalent from metadata # Extract sats equivalent from metadata (legacy)
posting_meta = first_posting.get("meta", {}) posting_meta = first_posting.get("meta", {})
sats_equiv = posting_meta.get("sats-equivalent") sats_equiv = posting_meta.get("sats-equivalent")
if sats_equiv: if sats_equiv: