refactor(v2): rename net_sats → principal_sats for semantic clarity

`net` is financial-accounting ambiguous (net of what?). In the
bitSpire/DCA context this column is specifically the principal the
operator distributes to LPs (gross − commission), not a generic net
amount. Renaming locally before any bitSpire firmware locks the
wire-level name; lamassu-next#44 should adopt the same name.

Scope:
- migrations.py: m003 ALTER TABLE … RENAME COLUMN, idempotent probe
  pattern matching m002. Also updates the m001 canonical schema so
  fresh installs land on the new column directly.
- models.py: `CreateDcaSettlementData.principal_sats` /
  `DcaSettlement.principal_sats`. Field-doc comment updated.
- bitspire.py: both happy path and fallback path return
  `principal_sats=…`. Reads `extra.get("principal_sats")` from the
  bitSpire payload (lamassu-next#44 should follow this rename).
- crud.py: INSERT column list + `apply_partial_dispense(
  new_principal_sats=…)` keyword.
- distribution.py: every `settlement.net_sats` → `settlement.
  principal_sats`; partial-dispense memo + helper signatures updated;
  the leg-order docblock at the top reads "principal_sats".
- tasks.py: landed-settlement log line.
- static/js/index.js: settlements-table column `principal_sats` with
  label "Principal (→ LPs)".
- templates/satmachineadmin/index.html: q-td key + binding.

All 86 unit tests still pass. No backwards-compat shim — v2-bitspire
isn't released; the rename is a clean break.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-15 23:21:32 +02:00
commit 1feaba80ed
8 changed files with 97 additions and 106 deletions

View file

@ -59,16 +59,14 @@ async def m001_satmachine_v2_initial(db):
await db.execute(f"DROP TABLE IF EXISTS satoshimachine.{table}")
# 2. super_config — singleton (id='default') with platform-fee config.
await db.execute(
f"""
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.super_config (
id TEXT PRIMARY KEY,
super_fee_pct DECIMAL(10,4) NOT NULL DEFAULT 0.0000,
super_fee_wallet_id TEXT,
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
""")
existing = await db.fetchone(
"SELECT id FROM satoshimachine.super_config WHERE id = 'default'"
)
@ -80,8 +78,7 @@ async def m001_satmachine_v2_initial(db):
# 3. dca_machines — one row per bitSpire ATM, owned by exactly one
# operator. wallet_id UNIQUE prevents the IDOR funds-theft vector.
await db.execute(
f"""
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_machines (
id TEXT PRIMARY KEY,
operator_user_id TEXT NOT NULL,
@ -95,8 +92,7 @@ async def m001_satmachine_v2_initial(db):
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
""")
await db.execute(
"CREATE INDEX IF NOT EXISTS dca_machines_operator_idx "
"ON dca_machines (operator_user_id)"
@ -109,8 +105,7 @@ async def m001_satmachine_v2_initial(db):
# 4. dca_clients — LP registrations scoped per (machine, user). An LP
# can hold positions at many machines (and many operators) on the
# same LNbits instance.
await db.execute(
f"""
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_clients (
id TEXT PRIMARY KEY,
machine_id TEXT NOT NULL,
@ -125,21 +120,18 @@ async def m001_satmachine_v2_initial(db):
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
""")
await db.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS dca_clients_machine_user_uq "
"ON dca_clients (machine_id, user_id)"
)
await db.execute(
"CREATE INDEX IF NOT EXISTS dca_clients_user_idx "
"ON dca_clients (user_id)"
"CREATE INDEX IF NOT EXISTS dca_clients_user_idx " "ON dca_clients (user_id)"
)
# 5. dca_deposits — fiat the operator (or super) records against an LP
# at a machine. creator_user_id preserves audit trail.
await db.execute(
f"""
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_deposits (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
@ -152,8 +144,7 @@ async def m001_satmachine_v2_initial(db):
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
confirmed_at TIMESTAMP
);
"""
)
""")
await db.execute(
"CREATE INDEX IF NOT EXISTS dca_deposits_client_idx "
"ON dca_deposits (client_id, created_at DESC)"
@ -170,8 +161,7 @@ async def m001_satmachine_v2_initial(db):
# ships, these two columns are the audit-grade record of who
# forgave what per transaction. Do not collapse them into a single
# commission_pct. See plan section "Customer discounts" and #10.
await db.execute(
f"""
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_settlements (
id TEXT PRIMARY KEY,
machine_id TEXT NOT NULL,
@ -182,7 +172,7 @@ async def m001_satmachine_v2_initial(db):
fiat_amount DECIMAL(10,2) NOT NULL,
fiat_code TEXT NOT NULL DEFAULT 'GTQ',
exchange_rate REAL NOT NULL,
net_sats BIGINT NOT NULL,
principal_sats BIGINT NOT NULL,
commission_sats BIGINT NOT NULL,
platform_fee_sats BIGINT NOT NULL,
operator_fee_sats BIGINT NOT NULL,
@ -197,8 +187,7 @@ async def m001_satmachine_v2_initial(db):
notes TEXT,
processing_claim TEXT
);
"""
)
""")
await db.execute(
"CREATE INDEX IF NOT EXISTS dca_settlements_machine_idx "
"ON dca_settlements (machine_id, created_at DESC)"
@ -216,8 +205,7 @@ async def m001_satmachine_v2_initial(db):
# - Lightning address (user@domain)
# - LNURL string (bech32 LNURL...)
# Resolution lives in distribution._pay_one_split_leg.
await db.execute(
f"""
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_commission_splits (
id TEXT PRIMARY KEY,
machine_id TEXT,
@ -228,8 +216,7 @@ async def m001_satmachine_v2_initial(db):
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
""")
await db.execute(
"CREATE INDEX IF NOT EXISTS dca_commission_splits_lookup_idx "
"ON dca_commission_splits (operator_user_id, machine_id)"
@ -239,8 +226,7 @@ async def m001_satmachine_v2_initial(db):
# discriminator: dca | super_fee | operator_split | settlement |
# autoforward | refund. status enum: pending | completed | failed |
# voided | skipped | refunded.
await db.execute(
f"""
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_payments (
id TEXT PRIMARY KEY,
settlement_id TEXT,
@ -259,8 +245,7 @@ async def m001_satmachine_v2_initial(db):
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
""")
await db.execute(
"CREATE INDEX IF NOT EXISTS dca_payments_client_idx "
"ON dca_payments (client_id, created_at DESC)"
@ -280,8 +265,7 @@ async def m001_satmachine_v2_initial(db):
# only cash_in/cash_out/cash_level/fiat/model — post-#43 fields
# (name, location, geo, fees, limits, denominations, version) are
# nullable until that upstream issue lands. Ingest opportunistically.
await db.execute(
"""
await db.execute("""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_telemetry (
machine_id TEXT PRIMARY KEY,
beacon_cash_in BOOLEAN,
@ -300,8 +284,7 @@ async def m001_satmachine_v2_initial(db):
telemetry_json TEXT,
telemetry_received_at TIMESTAMP
);
"""
)
""")
async def m002_rename_commission_split_wallet_id_to_target(db):
@ -332,3 +315,24 @@ async def m002_rename_commission_split_wallet_id_to_target(db):
"ALTER TABLE satoshimachine.dca_commission_splits "
"RENAME COLUMN wallet_id TO target"
)
async def m003_rename_settlements_net_sats_to_principal_sats(db):
"""Rename `dca_settlements.net_sats` → `principal_sats` for clarity.
"Net" in financial accounting is overloaded (net of what?). In the
bitSpire/DCA context this column is specifically the principal the
operator distributes to LPs (gross commission), not a generic
"net" amount. Renaming locally before any bitSpire firmware locks
the wire-level name; lamassu-next#44 should adopt the same name.
Idempotent: probes for the old `net_sats` column. If present, rename.
"""
try:
await db.fetchone("SELECT net_sats FROM satoshimachine.dca_settlements LIMIT 1")
except Exception:
return
await db.execute(
"ALTER TABLE satoshimachine.dca_settlements "
"RENAME COLUMN net_sats TO principal_sats"
)