fix(v2): partial-dispense preserves original split ratio (H6)
Closes H6 from #11. The partial-dispense recompute path was reading the CURRENT super_fee_pct via get_super_config() to re-derive the platform/ operator split. That breaks the "absolute fields are the source of truth" invariant the v2 schema was built around: if super raises (or lowers) the global rate between landing and partial-dispense, the operator's share would retroactively shift — without any notice to the operator and contrary to the original transaction's contract. Fix: re-derive new_platform from the *original* platform_fee_sats / commission_sats ratio stored on the settlement row, not from the current super_config. The contract was locked at landing; rate changes after the fact must not retroactively touch this transaction. Before: super_config = await get_super_config() super_fee_pct = float(super_config.super_fee_pct) if super_config else 0.0 new_platform, new_operator = split_two_stage_commission( new_commission, super_fee_pct ) After: ratio = (settlement.platform_fee_sats / settlement.commission_sats if settlement.commission_sats > 0 else 0.0) new_platform = round(new_commission * ratio) new_platform = max(0, min(new_platform, new_commission)) new_operator = new_commission - new_platform Note: split_two_stage_commission at LANDING time (in bitspire.py) still uses the current super_fee_pct — that's correct, the rate at landing is the locked rate. Only the *recompute* path was wrong. Tests: TestPartialDispenseSplitRatio.test_plan_scenario_30pct_lands_then_partial: 100-sat commission @ 30% → partial to 50% → 15/35 (preserves ratio). test_super_changed_rate_doesnt_affect_existing_settlement: Super raises rate to 50% after a 30% landing; partial-dispense to 50% must keep the ORIGINAL ~30% platform share, not the new 50%. test_zero_original_commission_yields_zero_platform: edge case. test_invariant_sum_equals_new_commission: parametrised sum invariant. Also dropped the now-unused split_two_stage_commission import from distribution.py (still used in bitspire.py at landing time and by the test suite, just not in this file anymore). 54 / 54 tests pass. Refs: aiolabs/satmachineadmin#11 — H6 ✅ Remaining in #11: fix bundle 3 (dead-code purge), M and N items. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ecef916dda
commit
00b8253dd3
2 changed files with 88 additions and 10 deletions
|
|
@ -34,7 +34,6 @@ from loguru import logger
|
|||
from .calculations import (
|
||||
allocate_operator_split_legs,
|
||||
calculate_distribution,
|
||||
split_two_stage_commission,
|
||||
)
|
||||
from .crud import (
|
||||
apply_partial_dispense,
|
||||
|
|
@ -268,9 +267,12 @@ async def apply_partial_dispense_and_redistribute(
|
|||
When a bitSpire dispense fails mid-transaction (e.g., dispenser jam after
|
||||
6 of 10 bills), the operator confirms the actual amount dispensed and we
|
||||
re-allocate the split against that partial gross. Sat amounts scale
|
||||
linearly, preserving the original commission ratio exactly; the two-stage
|
||||
super/operator split is recomputed using the CURRENT super_fee_pct
|
||||
(super may have changed the rate since the original landed).
|
||||
linearly, preserving the original commission ratio exactly. The two-stage
|
||||
super/operator split also scales by the *original* platform_fee_sats /
|
||||
commission_sats ratio rather than re-reading current super_fee_pct —
|
||||
this honors the "absolute fields are the source of truth" invariant
|
||||
even when super has changed the global rate since the settlement landed
|
||||
(closes #11 H6).
|
||||
|
||||
Hard guard: refuses if any dca_payments leg has already completed.
|
||||
Lightning payments can't be clawed back, so we won't try.
|
||||
|
|
@ -301,12 +303,19 @@ async def apply_partial_dispense_and_redistribute(
|
|||
new_net = new_gross - new_commission
|
||||
new_fiat = round(float(settlement.fiat_amount) * scale, 2)
|
||||
|
||||
# Re-stage-1 split using the CURRENT super_fee_pct.
|
||||
super_config = await get_super_config()
|
||||
super_fee_pct = float(super_config.super_fee_pct) if super_config else 0.0
|
||||
new_platform, new_operator = split_two_stage_commission(
|
||||
new_commission, super_fee_pct
|
||||
)
|
||||
# Re-derive the stage-1 split from the ORIGINAL ratio stored on this
|
||||
# settlement row — NOT the current super_fee_pct. The contract was
|
||||
# locked at landing; super raising or lowering the global rate after
|
||||
# the fact must not retroactively change this transaction's share.
|
||||
# Operator absorbs the rounding remainder so platform + operator
|
||||
# == new_commission exactly.
|
||||
if settlement.commission_sats > 0:
|
||||
ratio = settlement.platform_fee_sats / settlement.commission_sats
|
||||
else:
|
||||
ratio = 0.0
|
||||
new_platform = round(new_commission * ratio)
|
||||
new_platform = max(0, min(new_platform, new_commission))
|
||||
new_operator = new_commission - new_platform
|
||||
|
||||
memo = _build_partial_dispense_memo(
|
||||
settlement,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue