From da25d2e1f8d25c5e64b4583165a9f36a76d5b5b6 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 16 May 2026 16:39:47 +0200 Subject: [PATCH] fix(v2): read fiat_amount directly from Payment.extra (bill-validator truth) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- bitspire.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bitspire.py b/bitspire.py index fa5620b..ac0a4b3 100644 --- a/bitspire.py +++ b/bitspire.py @@ -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,