refactor(v2): canonical sat-amount vocabulary + delete Lamassu-era reverse-derivation

Cross-codebase decision logged at memory `reference_sat_amount_vocabulary.md`
and at `~/dev/coordination/log.md` (2026-05-26). Canonical names with
explicit units across satmachineadmin, lamassu-next, atm-tui:

  - `wire_sats` — actual Lightning payment amount (direction-agnostic;
                  was `gross_sats`, only "gross" for cash-out)
  - `principal_sats` — market-rate sats before commission (unchanged)
  - `fee_sats` — commission (was `commission_sats` internally;
                 already the wire format)
  - `fee_fraction` — commission rate as unit fraction in [0, 1]
                     (was `*_pct` / `fee_percent`; eliminates the
                     latent 100x bug from `feePercent * 100` on the
                     lamassu-next side)

Invariants enforced in bitspire._assert_sat_invariants on every
parsed settlement — range (all sats >= 0, 0 <= fee_fraction <= 1) +
direction-specific sum:

  - cash-out: wire_sats == principal_sats + fee_sats
  - cash-in:  wire_sats == principal_sats - fee_sats
              AND fee_sats <= principal_sats

Breaches raise SettlementInvariantError; tasks._handle_payment
records the row as `status='rejected'` with the exception message
and skips distribution. Attribution failure path symmetric.

Schema changes (m001 + m006):

  - dca_settlements.gross_sats              -> wire_sats
  - dca_settlements.commission_sats         -> fee_sats
  - super_config.super_fee_pct              -> super_fee_fraction
  - dca_commission_splits.pct               -> fraction
  - dca_machines.fallback_commission_pct    DROPPED (obsolete)
  - dca_settlements.used_fallback_split     DROPPED (obsolete)

m006 idempotently renames + drops columns on existing installs;
m001 lays down the canonical schema for fresh installs.

Obsolete code removed (Lamassu-era reverse-derivation):

  - calculations.calculate_commission — back-derived principal+fee
    from gross-with-commission-baked-in. v2 stamps both directly.
  - calculations.calculate_exchange_rate — bitSpire stamps directly.
  - bitspire._parse_fallback — sole caller of calculate_commission.
  - Machine.fallback_commission_fraction — only read by _parse_fallback.
  - DcaSettlement.used_fallback_split — only written by _parse_fallback.

parse_settlement now raises SettlementMetadataError if Payment.extra
lacks the bitSpire stamp or required absolute sat fields. No silent
back-derivation; upstream-bug surfacing via dashboard rejection.

Frontend (JS + Quasar templates) updated for the column renames and
the removed fallback fields. Settlements table renders "Wire" + "Fee"
columns; the "(fallback split)" warning badge is gone.

Tests:
  - test_calculations.py: kept distribution tests; deleted
    calculate_commission + calculate_exchange_rate tests.
  - test_two_stage_split.py: renamed variables; rewrote docstring
    value literals (e.g. `super_fee_fraction=0.30` not `=30%`).
  - test_nostr_attribution.py: dropped fallback_commission_fraction
    from machine fixture.
  - 72/72 pass on regtest container.

Cross-codebase follow-ups tracked in coordination log:
  - lamassu-next: rename `fee_percent` -> `fee_fraction` on
    Payment.extra + state.db; drop the `* 100` at lightning.ts:780.
  - atm-tui: read `fee_fraction` column in db.zig.

Memory artefacts:
  - reference_sat_amount_vocabulary.md (canonical + invariants)
  - feedback_pct_to_fraction_renames_need_value_sweep.md (gotcha)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-26 20:08:30 +02:00
commit d717a6e214
12 changed files with 530 additions and 681 deletions

41
crud.py
View file

@ -78,10 +78,9 @@ async def create_machine(operator_user_id: str, data: CreateMachineData) -> Mach
"""
INSERT INTO satoshimachine.dca_machines
(id, operator_user_id, machine_npub, wallet_id, name, location,
fiat_code, is_active, fallback_commission_pct, created_at, updated_at)
fiat_code, is_active, created_at, updated_at)
VALUES (:id, :operator_user_id, :machine_npub, :wallet_id, :name,
:location, :fiat_code, :is_active, :fallback_commission_pct,
:created_at, :updated_at)
:location, :fiat_code, :is_active, :created_at, :updated_at)
""",
{
"id": machine_id,
@ -92,7 +91,6 @@ async def create_machine(operator_user_id: str, data: CreateMachineData) -> Mach
"location": data.location,
"fiat_code": data.fiat_code,
"is_active": True,
"fallback_commission_pct": data.fallback_commission_pct,
"created_at": now,
"updated_at": now,
},
@ -555,14 +553,14 @@ async def create_settlement_idempotent(
"""
INSERT INTO satoshimachine.dca_settlements
(id, machine_id, payment_hash, bitspire_event_id, bitspire_txid,
gross_sats, fiat_amount, fiat_code, exchange_rate, principal_sats,
commission_sats, platform_fee_sats, operator_fee_sats,
used_fallback_split, tx_type, bills_json, cassettes_json,
wire_sats, fiat_amount, fiat_code, exchange_rate, principal_sats,
fee_sats, platform_fee_sats, operator_fee_sats,
tx_type, bills_json, cassettes_json,
status, error_message, created_at)
VALUES (:id, :machine_id, :payment_hash, :bitspire_event_id,
:bitspire_txid, :gross_sats, :fiat_amount, :fiat_code,
:exchange_rate, :principal_sats, :commission_sats,
:platform_fee_sats, :operator_fee_sats, :used_fallback_split,
:bitspire_txid, :wire_sats, :fiat_amount, :fiat_code,
:exchange_rate, :principal_sats, :fee_sats,
:platform_fee_sats, :operator_fee_sats,
:tx_type, :bills_json, :cassettes_json, :status,
:error_message, :created_at)
""",
@ -572,15 +570,14 @@ async def create_settlement_idempotent(
"payment_hash": data.payment_hash,
"bitspire_event_id": data.bitspire_event_id,
"bitspire_txid": data.bitspire_txid,
"gross_sats": data.gross_sats,
"wire_sats": data.wire_sats,
"fiat_amount": data.fiat_amount,
"fiat_code": data.fiat_code,
"exchange_rate": data.exchange_rate,
"principal_sats": data.principal_sats,
"commission_sats": data.commission_sats,
"fee_sats": data.fee_sats,
"platform_fee_sats": data.platform_fee_sats,
"operator_fee_sats": data.operator_fee_sats,
"used_fallback_split": data.used_fallback_split,
"tx_type": data.tx_type,
"bills_json": data.bills_json,
"cassettes_json": data.cassettes_json,
@ -840,9 +837,9 @@ async def reset_settlement_for_retry(
async def apply_partial_dispense(
settlement_id: str,
*,
new_gross_sats: int,
new_wire_sats: int,
new_principal_sats: int,
new_commission_sats: int,
new_fee_sats: int,
new_platform_fee_sats: int,
new_operator_fee_sats: int,
new_fiat_amount: float,
@ -858,9 +855,9 @@ async def apply_partial_dispense(
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
SET gross_sats = :gross,
SET wire_sats = :gross,
principal_sats = :principal,
commission_sats = :commission,
fee_sats = :commission,
platform_fee_sats = :platform,
operator_fee_sats = :operator,
fiat_amount = :fiat,
@ -875,9 +872,9 @@ async def apply_partial_dispense(
""",
{
"id": settlement_id,
"gross": new_gross_sats,
"gross": new_wire_sats,
"principal": new_principal_sats,
"commission": new_commission_sats,
"commission": new_fee_sats,
"platform": new_platform_fee_sats,
"operator": new_operator_fee_sats,
"fiat": new_fiat_amount,
@ -1013,9 +1010,9 @@ async def replace_commission_splits(
await db.execute(
"""
INSERT INTO satoshimachine.dca_commission_splits
(id, machine_id, operator_user_id, target, label, pct,
(id, machine_id, operator_user_id, target, label, fraction,
sort_order, created_at)
VALUES (:id, :machine_id, :uid, :target, :label, :pct,
VALUES (:id, :machine_id, :uid, :target, :label, :fraction,
:sort_order, :created_at)
""",
{
@ -1024,7 +1021,7 @@ async def replace_commission_splits(
"uid": operator_user_id,
"target": leg.target,
"label": leg.label,
"pct": leg.pct,
"fraction": leg.fraction,
"sort_order": leg.sort_order,
"created_at": now,
},