feat(v2): record fee_mismatch_sats per settlement, Phase 1 (#38 4/5)

Phase-1 observability per coord-log §2026-06-01T07:00Z (option A
locked: always record, no enforce_fee_match gate):

  fee_mismatch_sats = bitspire_fee_sats - (platform_fee_sats + operator_fee_sats)

Positive = bitspire over-reported; negative = under-reported; zero =
exact match. Recorded unconditionally on every settlement; WARN-
logged via loguru only when |delta| > tolerance, where
tolerance = max(1, int(principal_sats * 0.001)) — 1-sat floor with
0.1% relative ceiling.

bitspire.py:parse_settlement:
- Computes the delta after split_principal_based returns.
- WARN log line carries bitspire_fee_sats / expected / delta /
  tolerance / principal / both fractions / tx_type / machine-npub
  prefix for triage queries.
- Always stamps fee_mismatch_sats onto CreateDcaSettlementData.
- Comment explains the pre-Layer-3 expectation: large deltas are
  expected while the ATM hardcodes 7.77% cash-out (aiolabs/lamassu-
  next#57); the data here will quiet once Layer 3 ships.

crud.py:create_settlement_idempotent: extends the INSERT to persist
the new column.

Tests:
- tests/conftest.py: `loguru_capture` fixture — loguru routes to a
  pre-bound stderr sink that pytest's caplog (stdlib only) misses and
  capsys can't see; the fixture adds a list-sink for the test's
  duration. Reusable for future log-behavior tests.
- tests/test_fee_mismatch_recording.py: 8 cases covering exact-match
  zero delta, bitspire over- and under-reporting, the pre-Layer-3
  large-delta scenario, within-tolerance silence, over-tolerance
  warning, diagnostic-fields presence in the WARN line, and the
  1-sat floor on tiny-principal settlements.

164/164 tests green. Phase 2 (reject on out-of-tolerance) lands as
a follow-up once observability data justifies the tighter posture.

Refs: aiolabs/satmachineadmin#38 (Layer 1), coord-log §2026-06-01T07:00Z
(lnbits advisory + option A lock).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-01 14:34:25 +02:00
commit d9e8a04b8b
4 changed files with 236 additions and 2 deletions

View file

@ -601,13 +601,13 @@ async def create_settlement_idempotent(
INSERT INTO satoshimachine.dca_settlements
(id, machine_id, payment_hash, bitspire_event_id, bitspire_txid,
wire_sats, fiat_amount, fiat_code, exchange_rate, principal_sats,
fee_sats, platform_fee_sats, operator_fee_sats,
fee_sats, platform_fee_sats, operator_fee_sats, fee_mismatch_sats,
tx_type, bills_json, cassettes_json,
status, error_message, created_at)
VALUES (:id, :machine_id, :payment_hash, :bitspire_event_id,
:bitspire_txid, :wire_sats, :fiat_amount, :fiat_code,
:exchange_rate, :principal_sats, :fee_sats,
:platform_fee_sats, :operator_fee_sats,
:platform_fee_sats, :operator_fee_sats, :fee_mismatch_sats,
:tx_type, :bills_json, :cassettes_json, :status,
:error_message, :created_at)
""",
@ -625,6 +625,7 @@ async def create_settlement_idempotent(
"fee_sats": data.fee_sats,
"platform_fee_sats": data.platform_fee_sats,
"operator_fee_sats": data.operator_fee_sats,
"fee_mismatch_sats": data.fee_mismatch_sats,
"tx_type": data.tx_type,
"bills_json": data.bills_json,
"cassettes_json": data.cassettes_json,