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:
parent
9414a18f82
commit
1feaba80ed
8 changed files with 97 additions and 106 deletions
|
|
@ -10,7 +10,7 @@
|
|||
# Leg order:
|
||||
# 1. super_fee — platform_fee_sats → super_fee_wallet_id (if set)
|
||||
# 2. operator_split — operator_fee_sats split per operator's rules
|
||||
# 3. dca — net_sats distributed proportionally to active LPs,
|
||||
# 3. dca — principal_sats distributed proportionally to active LPs,
|
||||
# each leg capped at the LP's remaining fiat balance
|
||||
# (preserves the v1 sync-mismatch fix from PR #2)
|
||||
#
|
||||
|
|
@ -105,8 +105,7 @@ async def _record_skipped_leg(
|
|||
)
|
||||
await update_payment_status(leg.id, "skipped", None, reason[:512])
|
||||
logger.info(
|
||||
f"distribution: skipped {leg_type} leg "
|
||||
f"({amount_sats} sats) — {reason}"
|
||||
f"distribution: skipped {leg_type} leg " f"({amount_sats} sats) — {reason}"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -134,7 +133,7 @@ def _build_partial_dispense_memo(
|
|||
data: PartialDispenseData,
|
||||
*,
|
||||
new_gross: int,
|
||||
new_net: int,
|
||||
new_principal: int,
|
||||
new_commission: int,
|
||||
new_platform: int,
|
||||
new_operator: int,
|
||||
|
|
@ -147,11 +146,13 @@ def _build_partial_dispense_memo(
|
|||
ts = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
return (
|
||||
f"[{ts}] partial dispense applied — {adjust}. "
|
||||
f"Original gross={settlement.gross_sats} net={settlement.net_sats} "
|
||||
f"Original gross={settlement.gross_sats} "
|
||||
f"principal={settlement.principal_sats} "
|
||||
f"commission={settlement.commission_sats} "
|
||||
f"(super_fee={settlement.platform_fee_sats} "
|
||||
f"operator_fee={settlement.operator_fee_sats}). "
|
||||
f"New gross={new_gross} net={new_net} commission={new_commission} "
|
||||
f"New gross={new_gross} principal={new_principal} "
|
||||
f"commission={new_commission} "
|
||||
f"(super_fee={new_platform} operator_fee={new_operator}). "
|
||||
f"Reason: {reason}"
|
||||
)
|
||||
|
|
@ -177,14 +178,10 @@ async def settle_lp_balance(
|
|||
raise ValueError(f"client {client.id} balance not available")
|
||||
remaining = float(summary.remaining_balance)
|
||||
if remaining <= 0:
|
||||
raise ValueError(
|
||||
f"client {client.id} has no remaining balance to settle"
|
||||
)
|
||||
raise ValueError(f"client {client.id} has no remaining balance to settle")
|
||||
|
||||
# Resolve fiat amount: explicit if given (capped at remaining), else full.
|
||||
requested = (
|
||||
float(data.amount_fiat) if data.amount_fiat is not None else remaining
|
||||
)
|
||||
requested = float(data.amount_fiat) if data.amount_fiat is not None else remaining
|
||||
amount_fiat = round(min(requested, remaining), 2)
|
||||
if amount_fiat <= 0:
|
||||
raise ValueError("computed settlement amount is zero")
|
||||
|
|
@ -300,7 +297,7 @@ async def apply_partial_dispense_and_redistribute(
|
|||
# Linear scale preserves the original commission ratio exactly.
|
||||
scale = new_gross / settlement.gross_sats
|
||||
new_commission = round(settlement.commission_sats * scale)
|
||||
new_net = new_gross - new_commission
|
||||
new_principal = new_gross - new_commission
|
||||
new_fiat = round(float(settlement.fiat_amount) * scale, 2)
|
||||
|
||||
# Re-derive the stage-1 split from the ORIGINAL ratio stored on this
|
||||
|
|
@ -321,7 +318,7 @@ async def apply_partial_dispense_and_redistribute(
|
|||
settlement,
|
||||
data,
|
||||
new_gross=new_gross,
|
||||
new_net=new_net,
|
||||
new_principal=new_principal,
|
||||
new_commission=new_commission,
|
||||
new_platform=new_platform,
|
||||
new_operator=new_operator,
|
||||
|
|
@ -331,7 +328,7 @@ async def apply_partial_dispense_and_redistribute(
|
|||
updated = await apply_partial_dispense(
|
||||
settlement_id,
|
||||
new_gross_sats=new_gross,
|
||||
new_net_sats=new_net,
|
||||
new_principal_sats=new_principal,
|
||||
new_commission_sats=new_commission,
|
||||
new_platform_fee_sats=new_platform,
|
||||
new_operator_fee_sats=new_operator,
|
||||
|
|
@ -374,9 +371,7 @@ async def process_settlement(settlement_id: str) -> None:
|
|||
f"distribution: settlement {settlement_id} references missing "
|
||||
f"machine {settlement.machine_id}"
|
||||
)
|
||||
await mark_settlement_status(
|
||||
settlement_id, "errored", "machine missing"
|
||||
)
|
||||
await mark_settlement_status(settlement_id, "errored", "machine missing")
|
||||
return
|
||||
super_config = await get_super_config()
|
||||
errors: List[str] = []
|
||||
|
|
@ -390,9 +385,7 @@ async def process_settlement(settlement_id: str) -> None:
|
|||
errors.append(f"unexpected: {exc}")
|
||||
|
||||
if errors:
|
||||
await mark_settlement_status(
|
||||
settlement_id, "errored", "; ".join(errors)[:512]
|
||||
)
|
||||
await mark_settlement_status(settlement_id, "errored", "; ".join(errors)[:512])
|
||||
else:
|
||||
await mark_settlement_status(settlement_id, "processed", None)
|
||||
|
||||
|
|
@ -415,7 +408,8 @@ async def _pay_super_fee(
|
|||
# the sats in the machine wallet and record a skipped audit row.
|
||||
# The super needs to configure their wallet before they can collect.
|
||||
await _record_skipped_leg(
|
||||
settlement, machine,
|
||||
settlement,
|
||||
machine,
|
||||
leg_type="super_fee",
|
||||
amount_sats=settlement.platform_fee_sats,
|
||||
reason="super_fee_wallet_id not configured by LNbits super",
|
||||
|
|
@ -445,12 +439,11 @@ async def _pay_operator_splits(
|
|||
) -> None:
|
||||
if settlement.operator_fee_sats <= 0:
|
||||
return
|
||||
splits = await get_effective_commission_splits(
|
||||
machine.operator_user_id, machine.id
|
||||
)
|
||||
splits = await get_effective_commission_splits(machine.operator_user_id, machine.id)
|
||||
if not splits:
|
||||
await _record_skipped_leg(
|
||||
settlement, machine,
|
||||
settlement,
|
||||
machine,
|
||||
leg_type="operator_split",
|
||||
amount_sats=settlement.operator_fee_sats,
|
||||
reason=(
|
||||
|
|
@ -492,17 +485,18 @@ async def _pay_dca_distributions(
|
|||
machine: Machine,
|
||||
errors: List[str],
|
||||
) -> None:
|
||||
if settlement.net_sats <= 0:
|
||||
if settlement.principal_sats <= 0:
|
||||
return
|
||||
if settlement.exchange_rate <= 0:
|
||||
# Fallback path with no exchange rate (bitSpire Payment.extra absent).
|
||||
# Without a rate we can't compute fiat balances → can't compute
|
||||
# proportional shares → leave net_sats in the machine wallet for
|
||||
# manual reconciliation. Audit row makes the strand visible.
|
||||
# proportional shares → leave principal_sats in the machine wallet
|
||||
# for manual reconciliation. Audit row makes the strand visible.
|
||||
await _record_skipped_leg(
|
||||
settlement, machine,
|
||||
settlement,
|
||||
machine,
|
||||
leg_type="dca",
|
||||
amount_sats=settlement.net_sats,
|
||||
amount_sats=settlement.principal_sats,
|
||||
reason=(
|
||||
"no exchange_rate on settlement (bitSpire fallback path; "
|
||||
"see aiolabs/lamassu-next#44)"
|
||||
|
|
@ -512,9 +506,10 @@ async def _pay_dca_distributions(
|
|||
clients = await get_flow_mode_clients_for_machine(machine.id)
|
||||
if not clients:
|
||||
await _record_skipped_leg(
|
||||
settlement, machine,
|
||||
settlement,
|
||||
machine,
|
||||
leg_type="dca",
|
||||
amount_sats=settlement.net_sats,
|
||||
amount_sats=settlement.principal_sats,
|
||||
reason="no active flow-mode LPs registered at this machine",
|
||||
)
|
||||
return
|
||||
|
|
@ -527,9 +522,10 @@ async def _pay_dca_distributions(
|
|||
client_balances[client.id] = summary.remaining_balance
|
||||
if not client_balances:
|
||||
await _record_skipped_leg(
|
||||
settlement, machine,
|
||||
settlement,
|
||||
machine,
|
||||
leg_type="dca",
|
||||
amount_sats=settlement.net_sats,
|
||||
amount_sats=settlement.principal_sats,
|
||||
reason=(
|
||||
"no LP has remaining-fiat-balance > 0 — all confirmed deposits "
|
||||
"already paid out"
|
||||
|
|
@ -539,7 +535,7 @@ async def _pay_dca_distributions(
|
|||
# Compute proportional sat allocations, then cap each at the client's
|
||||
# remaining-fiat-balance-in-sats (the v1 sync-mismatch safeguard).
|
||||
raw_allocations = calculate_distribution(
|
||||
base_amount_sats=settlement.net_sats,
|
||||
base_amount_sats=settlement.principal_sats,
|
||||
client_balances=client_balances,
|
||||
)
|
||||
capped_allocations: dict[str, int] = {}
|
||||
|
|
@ -565,9 +561,7 @@ async def _pay_one_dca_leg(
|
|||
if amount_sats <= 0:
|
||||
return
|
||||
amount_fiat = round(amount_sats / float(settlement.exchange_rate), 2)
|
||||
memo = (
|
||||
f"DCA: {amount_sats} sats • {amount_fiat:.2f} {settlement.fiat_code}"
|
||||
)
|
||||
memo = f"DCA: {amount_sats} sats • {amount_fiat:.2f} {settlement.fiat_code}"
|
||||
dca_leg = await _pay_internal(
|
||||
settlement=settlement,
|
||||
machine=machine,
|
||||
|
|
@ -654,9 +648,7 @@ async def _attempt_autoforward(
|
|||
"satmachine_destination": address,
|
||||
},
|
||||
)
|
||||
await update_payment_status(
|
||||
leg.id, "completed", paid.payment_hash, None
|
||||
)
|
||||
await update_payment_status(leg.id, "completed", paid.payment_hash, None)
|
||||
logger.info(
|
||||
f"distribution: autoforward {amount_sats} sats from client "
|
||||
f"{client.id} → {address} OK"
|
||||
|
|
@ -716,9 +708,7 @@ async def _pay_split_leg(
|
|||
"satmachine_destination": target,
|
||||
}
|
||||
try:
|
||||
ln_target = (
|
||||
LnAddress(target) if "@" in target else target
|
||||
)
|
||||
ln_target = LnAddress(target) if "@" in target else target
|
||||
bolt11 = await get_pr_from_lnurl(
|
||||
lnurl=ln_target,
|
||||
amount_msat=amount_sats * 1000,
|
||||
|
|
@ -740,9 +730,7 @@ async def _pay_split_leg(
|
|||
f"distribution: operator_split (LNURL/LN-addr) FAILED "
|
||||
f"target={target} settlement={settlement.id}: {exc}"
|
||||
)
|
||||
await update_payment_status(
|
||||
leg_row.id, "failed", None, str(exc)[:512]
|
||||
)
|
||||
await update_payment_status(leg_row.id, "failed", None, str(exc)[:512])
|
||||
errors.append(f"operator_split→{target}: {exc}")
|
||||
return leg_row
|
||||
|
||||
|
|
@ -750,6 +738,7 @@ async def _pay_split_leg(
|
|||
resolved_wallet_id = target
|
||||
try:
|
||||
from lnbits.core.crud.wallets import get_wallet_for_key
|
||||
|
||||
wallet = await get_wallet_for_key(target)
|
||||
if wallet is not None:
|
||||
resolved_wallet_id = wallet.id
|
||||
|
|
@ -828,9 +817,7 @@ async def _pay_internal(
|
|||
tag=tag,
|
||||
extra=extra,
|
||||
)
|
||||
await update_payment_status(
|
||||
leg_row.id, "completed", paid.payment_hash, None
|
||||
)
|
||||
await update_payment_status(leg_row.id, "completed", paid.payment_hash, None)
|
||||
return leg_row
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue