fix(v2): read fiat_amount directly from Payment.extra (bill-validator truth)

Pairs with aiolabs/lamassu-next@8318489 which now stamps the customer-
transacted fiat amount as a top-level field on Payment.extra, sourced
directly from bitSpire's bill validator / dispenser ledger.

Previously `_parse_extra` computed `fiat_amount = gross_sats /
exchange_rate` (which is wrong — that's the fiat-equivalent of the
gross including commission, not the customer's transaction value)
or `principal_sats / exchange_rate` (close but assumes commission
lives entirely in BTC and accumulates rounding from floor() in the
bitSpire-side principalSats calc). Both are derivations from
adjacent quantities; the bill validator already knows the answer.

Now: read `extra.get("fiat_amount")` verbatim. Source of truth ends
up on the settlement row exactly as the machine recorded it.

Surfaced during the 2026-05-16 cash-out E2E test: 20 EUR customer
transaction was rendering as 21.55 EUR in the Fiat column — that
21.55 was the fiat-equivalent of the gross sats including commission,
not the cash that physically came out of the machine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-16 16:39:47 +02:00
commit da25d2e1f8

View file

@ -159,7 +159,13 @@ def _parse_extra(
# Without exchange rate we can't compute fiat. Use 1.0 as a stand-in
# and let the operator correct via manual reconciliation.
exchange_rate = 1.0
fiat_amount = round(gross_sats / exchange_rate, 2) if exchange_rate > 0 else 0.0
# `fiat_amount` is sourced directly from bitSpire's bill validator /
# dispenser ledger (lamassu-next@8318489). It's the cash that
# physically entered (cash-in) or exited (cash-out) the machine —
# canonical, not derived. We never recompute it from sats × rate
# downstream: the relationship is't load-bearing (commission lives
# in BTC today, but the cash side has its own ground truth).
fiat_amount = _coerce_float(extra.get("fiat_amount")) or 0.0
fiat_code = _coerce_str(extra.get("currency")) or machine.fiat_code
return CreateDcaSettlementData(
machine_id=machine.id,