fix(settlement): per-currency netting, balance guards, Decimal rates
Settlement correctness cluster from CODE-REVIEW-2026-06 (#2, #8, #13) plus libra-#38: - format_net_settlement_entry now enforces the same inline balance constraint as the fiat formatter (payment = receivable - payable + credit) and grows an optional credit leg. An unbalanced settlement raises instead of reaching the ledger. - on_invoice_paid settles only what the payment covers: a partial payment clears that much receivable; excess (or a payment with nothing owed) becomes user credit. Previously the full prior balance was cleared against a smaller payment, shipping unbalanced postings. Settlement links are attached only when the payment clears the full open balance, and only for same-currency entries. - get_unsettled_entries_bql returns each entry's real posting currency (was hardcoded "EUR") and exact Decimal amount strings (was float). /receivables/settle nets only entries denominated in the settlement currency. - fiat_rate/btc_rate metadata computed via Decimal (new fiat_rate_metadata helper) instead of float division — cost-basis records no longer carry float drift. - format_posting_at_average_cost omits the cost braces when cost_currency is unset ("SATS {}" is invalid Beancount). - Underpay error payload serializes amounts as exact Decimal strings. - validate_metadata catches decimal.InvalidOperation so bad fiat_amount input becomes ValidationError (libra-#38); flipped the tracking xfail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
44e10caac7
commit
cf1a0967bf
7 changed files with 276 additions and 68 deletions
|
|
@ -1817,7 +1817,7 @@ class FavaClient:
|
|||
# Query 1: Get all original expense/receivable entries for this user
|
||||
# These are entries with the expense-entry or receivable-entry tag
|
||||
original_query = f"""
|
||||
SELECT date, narration, account, number, weight, links,
|
||||
SELECT date, narration, account, number, currency, weight, links,
|
||||
any_meta('entry-id') as entry_id
|
||||
WHERE account ~ '{account_pattern}'
|
||||
AND '{entry_tag}' IN tags
|
||||
|
|
@ -1851,7 +1851,10 @@ class FavaClient:
|
|||
entries_by_link: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
for row in original_result["rows"]:
|
||||
date_val, narration, account, number, weight, links, entry_id = row
|
||||
(
|
||||
date_val, narration, account, number, currency,
|
||||
weight, links, entry_id,
|
||||
) = row
|
||||
|
||||
# Skip if no links
|
||||
if not links or not isinstance(links, list):
|
||||
|
|
@ -1875,9 +1878,11 @@ class FavaClient:
|
|||
if entry_link in entries_by_link:
|
||||
continue
|
||||
|
||||
# Parse amounts
|
||||
fiat_amount = abs(float(number)) if number else 0.0
|
||||
fiat_currency = "EUR" # Default, could be extracted from posting
|
||||
# Parse amounts. The posting's real currency matters: callers
|
||||
# net these totals per currency, and the old hardcoded "EUR"
|
||||
# let USD (or SATS-only) entries be summed as if they were EUR.
|
||||
fiat_amount = str(abs(Decimal(str(number)))) if number else "0"
|
||||
fiat_currency = currency or "EUR"
|
||||
|
||||
# Parse SATS from weight column
|
||||
sats_amount = 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue