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:
parent
6348c55e37
commit
d717a6e214
12 changed files with 530 additions and 681 deletions
|
|
@ -1,114 +1,19 @@
|
|||
"""
|
||||
Tests for DCA transaction calculations using empirical data.
|
||||
Tests for DCA transaction calculations.
|
||||
|
||||
These tests verify commission and distribution calculations against
|
||||
real Lamassu transaction data to ensure the math is correct.
|
||||
Covers the pure-function helpers that survive the 2026-05-26 cleanup:
|
||||
- calculate_distribution (proportional split across LPs by balance)
|
||||
|
||||
The previous test surface for `calculate_commission` and
|
||||
`calculate_exchange_rate` was deleted alongside those functions — the
|
||||
Lamassu-era reverse-derivation is obsolete now that bitSpire stamps
|
||||
`principal_sats` and `fee_sats` directly on Payment.extra.
|
||||
|
||||
Two-stage commission split tests live in `test_two_stage_split.py`.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# Import from the parent package (following lnurlp pattern)
|
||||
from ..calculations import calculate_commission, calculate_distribution, calculate_exchange_rate
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COMMISSION CALCULATION TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestCommissionCalculation:
|
||||
"""Tests for commission calculation logic."""
|
||||
|
||||
# Empirical test cases: (crypto_atoms, commission%, discount%, expected_base, expected_commission)
|
||||
# Formula: base = round(crypto_atoms / (1 + effective_commission))
|
||||
# Where: effective_commission = commission_percentage * (100 - discount) / 100
|
||||
EMPIRICAL_COMMISSION_CASES = [
|
||||
# =============================================================
|
||||
# REAL LAMASSU TRANSACTIONS (extracted from production database)
|
||||
# =============================================================
|
||||
|
||||
# 8.75% commission, no discount - small transaction
|
||||
# 15600 / 1.0875 = 14344.827... → 14345
|
||||
(15600, 0.0875, 0.0, 14345, 1255),
|
||||
|
||||
# 8.75% commission, no discount - large transaction
|
||||
# 309200 / 1.0875 = 284322.298... → 284322
|
||||
(309200, 0.0875, 0.0, 284322, 24878),
|
||||
|
||||
# 5.5% commission, no discount
|
||||
# 309500 / 1.055 = 293364.928... → 293365
|
||||
(309500, 0.055, 0.0, 293365, 16135),
|
||||
|
||||
# 5.5% commission with 100% discount (no commission charged)
|
||||
# effective = 0.055 * (100-100)/100 = 0
|
||||
(292400, 0.055, 100.0, 292400, 0),
|
||||
|
||||
# 5.5% commission with 90% discount
|
||||
# effective = 0.055 * (100-90)/100 = 0.0055
|
||||
# 115000 / 1.0055 = 114370.96... → 114371
|
||||
(115000, 0.055, 90.0, 114371, 629),
|
||||
|
||||
# 5.5% commission, no discount - 1300 GTQ transaction
|
||||
# 205600 / 1.055 = 194881.516... → 194882
|
||||
# Note: This tx showed 0.01 GTQ rounding discrepancy in per-client fiat
|
||||
(205600, 0.055, 0.0, 194882, 10718),
|
||||
|
||||
# =============================================================
|
||||
# SYNTHETIC TEST CASES (edge cases)
|
||||
# =============================================================
|
||||
|
||||
# Zero commission - all goes to base
|
||||
(100000, 0.0, 0.0, 100000, 0),
|
||||
|
||||
# Small amount edge case (1 sat minimum)
|
||||
(100, 0.03, 0.0, 97, 3),
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"crypto_atoms,commission_pct,discount,expected_base,expected_commission",
|
||||
EMPIRICAL_COMMISSION_CASES,
|
||||
ids=[
|
||||
"lamassu_8.75pct_small",
|
||||
"lamassu_8.75pct_large",
|
||||
"lamassu_5.5pct_no_discount",
|
||||
"lamassu_5.5pct_100pct_discount",
|
||||
"lamassu_5.5pct_90pct_discount",
|
||||
"lamassu_5.5pct_1300gtq",
|
||||
"zero_commission",
|
||||
"small_amount_100sats",
|
||||
]
|
||||
)
|
||||
def test_commission_calculation(
|
||||
self,
|
||||
crypto_atoms: int,
|
||||
commission_pct: float,
|
||||
discount: float,
|
||||
expected_base: int,
|
||||
expected_commission: int
|
||||
):
|
||||
"""Test commission calculation against empirical data."""
|
||||
base, commission, _ = calculate_commission(crypto_atoms, commission_pct, discount)
|
||||
|
||||
assert base == expected_base, f"Base amount mismatch: got {base}, expected {expected_base}"
|
||||
assert commission == expected_commission, f"Commission mismatch: got {commission}, expected {expected_commission}"
|
||||
|
||||
# Invariant: base + commission must equal total
|
||||
assert base + commission == crypto_atoms, "Base + commission must equal total crypto_atoms"
|
||||
|
||||
def test_commission_invariant_always_sums_to_total(self):
|
||||
"""Commission + base must always equal the original amount."""
|
||||
test_values = [1, 100, 1000, 10000, 100000, 266800, 1000000]
|
||||
commission_rates = [0.0, 0.01, 0.03, 0.05, 0.10]
|
||||
discounts = [0.0, 10.0, 25.0, 50.0]
|
||||
|
||||
for crypto_atoms in test_values:
|
||||
for comm_rate in commission_rates:
|
||||
for discount in discounts:
|
||||
base, commission, _ = calculate_commission(crypto_atoms, comm_rate, discount)
|
||||
assert base + commission == crypto_atoms, \
|
||||
f"Invariant failed: {base} + {commission} != {crypto_atoms} " \
|
||||
f"(rate={comm_rate}, discount={discount})"
|
||||
from ..calculations import calculate_distribution
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -157,7 +62,6 @@ class TestDistributionCalculation:
|
|||
|
||||
def test_distribution_invariant_sums_to_total(self):
|
||||
"""Total distributed sats must always equal base amount."""
|
||||
# Test with various client configurations
|
||||
test_cases = [
|
||||
{"a": 100.0},
|
||||
{"a": 100.0, "b": 100.0},
|
||||
|
|
@ -215,156 +119,6 @@ class TestDistributionCalculation:
|
|||
|
||||
assert distributions == {}
|
||||
|
||||
def test_fiat_round_trip_invariant(self):
|
||||
"""
|
||||
Verify that distributed sats convert back to original fiat amount.
|
||||
|
||||
The sum of each client's fiat equivalent should equal the original
|
||||
fiat amount (within rounding tolerance).
|
||||
"""
|
||||
# Use real Lamassu transaction data
|
||||
test_cases = [
|
||||
# (crypto_atoms, fiat_amount, commission_pct, discount, client_balances)
|
||||
(309200, 2000.0, 0.0875, 0.0, {"a": 1000.0, "b": 1000.0}),
|
||||
(309500, 2000.0, 0.055, 0.0, {"a": 500.0, "b": 1000.0, "c": 500.0}),
|
||||
(292400, 2000.0, 0.055, 100.0, {"a": 800.0, "b": 1200.0}),
|
||||
(115000, 800.0, 0.055, 90.0, {"a": 400.0, "b": 400.0}),
|
||||
# Transaction that showed 0.01 GTQ rounding discrepancy with 4 clients
|
||||
(205600, 1300.0, 0.055, 0.0, {"a": 1.0, "b": 986.0, "c": 14.0, "d": 4.0}),
|
||||
]
|
||||
|
||||
for crypto_atoms, fiat_amount, comm_pct, discount, client_balances in test_cases:
|
||||
# Calculate commission and base amount
|
||||
base_sats, _, _ = calculate_commission(crypto_atoms, comm_pct, discount)
|
||||
|
||||
# Calculate exchange rate
|
||||
exchange_rate = calculate_exchange_rate(base_sats, fiat_amount)
|
||||
|
||||
# Distribute sats to clients
|
||||
distributions = calculate_distribution(base_sats, client_balances)
|
||||
|
||||
# Convert each client's sats back to fiat
|
||||
total_fiat_distributed = sum(
|
||||
sats / exchange_rate for sats in distributions.values()
|
||||
)
|
||||
|
||||
# Should equal original fiat amount (within small rounding tolerance)
|
||||
assert abs(total_fiat_distributed - fiat_amount) < 0.01, \
|
||||
f"Fiat round-trip failed: {total_fiat_distributed:.2f} != {fiat_amount:.2f} " \
|
||||
f"(crypto={crypto_atoms}, comm={comm_pct}, discount={discount})"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EMPIRICAL END-TO-END TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestEmpiricalTransactions:
|
||||
"""
|
||||
End-to-end tests using real Lamassu transaction data.
|
||||
|
||||
Add your empirical test cases here! Each case should include:
|
||||
- Transaction details (crypto_atoms, fiat, commission, discount)
|
||||
- Client balances at time of transaction
|
||||
- Expected distribution outcome
|
||||
"""
|
||||
|
||||
# TODO: Add your empirical data here
|
||||
# Example structure:
|
||||
EMPIRICAL_SCENARIOS = [
|
||||
{
|
||||
"name": "real_tx_266800sats_two_equal_clients",
|
||||
"transaction": {
|
||||
"crypto_atoms": 266800,
|
||||
"fiat_amount": 2000,
|
||||
"commission_percentage": 0.03,
|
||||
"discount": 0.0,
|
||||
},
|
||||
"client_balances": {
|
||||
"client_a": 1000.00, # 50% of total
|
||||
"client_b": 1000.00, # 50% of total
|
||||
},
|
||||
# 266800 / 1.03 = 259029
|
||||
"expected_base_sats": 259029,
|
||||
"expected_commission_sats": 7771,
|
||||
"expected_distributions": {
|
||||
# 259029 / 2 = 129514.5 → both get 129514 or 129515
|
||||
# With banker's rounding: 129514.5 → 129514 (even)
|
||||
# Remainder of 1 sat goes to first client by fractional sort
|
||||
"client_a": 129515,
|
||||
"client_b": 129514,
|
||||
},
|
||||
},
|
||||
# Add more scenarios from your real data!
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scenario",
|
||||
EMPIRICAL_SCENARIOS,
|
||||
ids=[s["name"] for s in EMPIRICAL_SCENARIOS]
|
||||
)
|
||||
def test_empirical_scenario(self, scenario):
|
||||
"""Test full transaction flow against empirical data."""
|
||||
tx = scenario["transaction"]
|
||||
|
||||
# Calculate commission
|
||||
base, commission, _ = calculate_commission(
|
||||
tx["crypto_atoms"],
|
||||
tx["commission_percentage"],
|
||||
tx["discount"]
|
||||
)
|
||||
|
||||
assert base == scenario["expected_base_sats"], \
|
||||
f"Base amount mismatch in {scenario['name']}"
|
||||
assert commission == scenario["expected_commission_sats"], \
|
||||
f"Commission mismatch in {scenario['name']}"
|
||||
|
||||
# Calculate distribution
|
||||
distributions = calculate_distribution(
|
||||
base,
|
||||
scenario["client_balances"]
|
||||
)
|
||||
|
||||
# Verify each client's allocation
|
||||
for client_id, expected_sats in scenario["expected_distributions"].items():
|
||||
actual_sats = distributions.get(client_id, 0)
|
||||
assert actual_sats == expected_sats, \
|
||||
f"Distribution mismatch for {client_id} in {scenario['name']}: " \
|
||||
f"got {actual_sats}, expected {expected_sats}"
|
||||
|
||||
# Verify total distribution equals base
|
||||
assert sum(distributions.values()) == base, \
|
||||
f"Total distribution doesn't match base in {scenario['name']}"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EDGE CASE TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Tests for edge cases and boundary conditions."""
|
||||
|
||||
def test_minimum_amount_1_sat(self):
|
||||
"""Test with minimum possible amount (1 sat)."""
|
||||
base, commission, _ = calculate_commission(1, 0.03, 0.0)
|
||||
# With 3% commission on 1 sat, base rounds to 1, commission to 0
|
||||
assert base + commission == 1
|
||||
|
||||
def test_large_transaction(self):
|
||||
"""Test with large transaction (100 BTC worth of sats)."""
|
||||
crypto_atoms = 10_000_000_000 # 100 BTC in sats
|
||||
base, commission, _ = calculate_commission(crypto_atoms, 0.03, 0.0)
|
||||
|
||||
assert base + commission == crypto_atoms
|
||||
assert commission > 0
|
||||
|
||||
def test_100_percent_discount(self):
|
||||
"""100% discount should result in zero commission."""
|
||||
base, commission, effective = calculate_commission(100000, 0.03, 100.0)
|
||||
|
||||
assert effective == 0.0
|
||||
assert commission == 0
|
||||
assert base == 100000
|
||||
|
||||
def test_many_clients_distribution(self):
|
||||
"""Test distribution with many clients."""
|
||||
# 10 clients with varying balances
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ def _machine(npub: str) -> Machine:
|
|||
location=None,
|
||||
fiat_code="EUR",
|
||||
is_active=True,
|
||||
fallback_commission_pct=0.05,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
Tests for the v2 two-stage commission split (super first, operator remainder).
|
||||
|
||||
The plan calls out a verification scenario explicitly:
|
||||
super_fee_pct=30%, operator split 50/30/20 on a 100-sat commission
|
||||
→ super_wallet gets 30, operator_self gets 35, employee 21, maint 14.
|
||||
super_fee_fraction=0.30 (i.e. 30%), operator splits [0.5, 0.3, 0.2] on a
|
||||
100-sat fee → super_wallet gets 30, operator legs get 35 / 21 / 14.
|
||||
|
||||
Also covers the edge cases: super_fee_pct=0 (no super), super_fee_pct=1.0
|
||||
(everything to super), single-leg operator ruleset, zero operator fee.
|
||||
Also covers the edge cases: super_fee_fraction=0.0 (no super takes the
|
||||
whole fee), super_fee_fraction=1.0 (super takes everything), single-leg
|
||||
operator ruleset, zero operator fee.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
|
@ -18,7 +19,7 @@ from ..calculations import (
|
|||
|
||||
|
||||
class TestSplitTwoStageCommission:
|
||||
"""Stage-1: super takes super_fee_pct of commission; operator gets rest."""
|
||||
"""Stage-1: super takes super_fee_fraction of the fee; operator gets rest."""
|
||||
|
||||
def test_plan_example_100sats_30pct(self):
|
||||
platform, operator = split_two_stage_commission(100, 0.30)
|
||||
|
|
@ -33,12 +34,12 @@ class TestSplitTwoStageCommission:
|
|||
assert operator == 5575 # 7965 - 2390
|
||||
assert platform + operator == 7965
|
||||
|
||||
def test_super_pct_zero_leaves_all_to_operator(self):
|
||||
def test_super_fraction_zero_leaves_all_to_operator(self):
|
||||
platform, operator = split_two_stage_commission(7965, 0.0)
|
||||
assert platform == 0
|
||||
assert operator == 7965
|
||||
|
||||
def test_super_pct_one_takes_everything(self):
|
||||
def test_super_fraction_one_takes_everything(self):
|
||||
platform, operator = split_two_stage_commission(7965, 1.0)
|
||||
assert platform == 7965
|
||||
assert operator == 0
|
||||
|
|
@ -54,13 +55,13 @@ class TestSplitTwoStageCommission:
|
|||
assert platform == 0
|
||||
assert operator == 0
|
||||
|
||||
@pytest.mark.parametrize("commission_sats", [1, 7, 100, 7965, 1_000_000])
|
||||
@pytest.mark.parametrize("super_pct", [0.0, 0.1, 0.30, 0.5, 0.777, 1.0])
|
||||
def test_invariant_sum_equals_commission(self, commission_sats, super_pct):
|
||||
platform, operator = split_two_stage_commission(commission_sats, super_pct)
|
||||
assert platform + operator == commission_sats
|
||||
assert 0 <= platform <= commission_sats
|
||||
assert 0 <= operator <= commission_sats
|
||||
@pytest.mark.parametrize("fee_sats", [1, 7, 100, 7965, 1_000_000])
|
||||
@pytest.mark.parametrize("super_fraction", [0.0, 0.1, 0.30, 0.5, 0.777, 1.0])
|
||||
def test_invariant_sum_equals_commission(self, fee_sats, super_fraction):
|
||||
platform, operator = split_two_stage_commission(fee_sats, super_fraction)
|
||||
assert platform + operator == fee_sats
|
||||
assert 0 <= platform <= fee_sats
|
||||
assert 0 <= operator <= fee_sats
|
||||
|
||||
|
||||
class TestAllocateOperatorSplitLegs:
|
||||
|
|
@ -102,7 +103,7 @@ class TestAllocateOperatorSplitLegs:
|
|||
assert amounts[2] == 100 - amounts[0] - amounts[1]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"operator_fee,pcts",
|
||||
"operator_fee,fractions",
|
||||
[
|
||||
(1, [0.5, 0.5]),
|
||||
(7, [0.5, 0.3, 0.2]),
|
||||
|
|
@ -111,8 +112,8 @@ class TestAllocateOperatorSplitLegs:
|
|||
(1_000_000, [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]),
|
||||
],
|
||||
)
|
||||
def test_invariant_sum_equals_operator_fee(self, operator_fee, pcts):
|
||||
amounts = allocate_operator_split_legs(operator_fee, pcts)
|
||||
def test_invariant_sum_equals_operator_fee(self, operator_fee, fractions):
|
||||
amounts = allocate_operator_split_legs(operator_fee, fractions)
|
||||
assert sum(amounts) == operator_fee
|
||||
assert all(a >= 0 for a in amounts)
|
||||
|
||||
|
|
@ -121,21 +122,21 @@ class TestEndToEndScenarios:
|
|||
"""The full two-stage split — super then operator legs — composed."""
|
||||
|
||||
def test_plan_example_full(self):
|
||||
# 100 sats commission, super=30%, operator splits 50/30/20.
|
||||
# 100 sats fee, super_fee_fraction=0.30, operator splits [0.5, 0.3, 0.2].
|
||||
platform, operator = split_two_stage_commission(100, 0.30)
|
||||
legs = allocate_operator_split_legs(operator, [0.5, 0.3, 0.2])
|
||||
assert platform == 30
|
||||
assert legs == [35, 21, 14]
|
||||
assert platform + sum(legs) == 100
|
||||
|
||||
def test_super_pct_zero_full_pipeline(self):
|
||||
def test_super_fraction_zero_full_pipeline(self):
|
||||
platform, operator = split_two_stage_commission(7965, 0.0)
|
||||
legs = allocate_operator_split_legs(operator, [1.0])
|
||||
assert platform == 0
|
||||
assert legs == [7965]
|
||||
assert platform + sum(legs) == 7965
|
||||
|
||||
def test_super_pct_one_full_pipeline(self):
|
||||
def test_super_fraction_one_full_pipeline(self):
|
||||
platform, operator = split_two_stage_commission(7965, 1.0)
|
||||
legs = allocate_operator_split_legs(operator, [0.5, 0.5])
|
||||
assert platform == 7965
|
||||
|
|
@ -147,27 +148,27 @@ class TestEndToEndScenarios:
|
|||
class TestPartialDispenseSplitRatio:
|
||||
"""The partial-dispense recompute (H6 fix) must preserve the ORIGINAL
|
||||
platform/operator ratio from the landed settlement — NOT re-derive
|
||||
from the current super_fee_pct.
|
||||
from the current super_fee_fraction.
|
||||
|
||||
These tests cover the math; the actual function lives in distribution.py
|
||||
and is exercised end-to-end via integration testing. Here we verify the
|
||||
invariant a future maintainer should never break.
|
||||
"""
|
||||
|
||||
def _recompute(self, original_commission, original_platform_fee, new_commission):
|
||||
def _recompute(self, original_fee, original_platform_fee, new_fee):
|
||||
"""Mirror of the ratio math in apply_partial_dispense_and_redistribute."""
|
||||
if original_commission > 0:
|
||||
ratio = original_platform_fee / original_commission
|
||||
if original_fee > 0:
|
||||
ratio = original_platform_fee / original_fee
|
||||
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
|
||||
new_platform = round(new_fee * ratio)
|
||||
new_platform = max(0, min(new_platform, new_fee))
|
||||
new_operator = new_fee - new_platform
|
||||
return new_platform, new_operator
|
||||
|
||||
def test_plan_scenario_30pct_lands_then_partial(self):
|
||||
# Landed at super_fee_pct=30%: 100-sat commission → 30 / 70.
|
||||
# Partial-dispense to 50% gross → new_commission = 50.
|
||||
# Landed at super_fee_fraction=0.30: 100-sat fee → 30 / 70.
|
||||
# Partial-dispense to 50% gross → new_fee = 50.
|
||||
# Original ratio (30/100 = 0.30) preserved.
|
||||
new_platform, new_operator = self._recompute(100, 30, 50)
|
||||
assert new_platform == 15
|
||||
|
|
@ -175,9 +176,9 @@ class TestPartialDispenseSplitRatio:
|
|||
assert new_platform + new_operator == 50
|
||||
|
||||
def test_super_changed_rate_doesnt_affect_existing_settlement(self):
|
||||
# Landed at super_fee_pct=30% (commission 7965, platform 2390).
|
||||
# Landed at super_fee_fraction=0.30 (fee 7965, platform 2390).
|
||||
# Super then raises rate to 50% globally. Operator partial-dispenses
|
||||
# to 50% gross → new_commission = 3982 (round(7965 * 0.5)).
|
||||
# to 50% gross → new_fee = 3982 (round(7965 * 0.5)).
|
||||
# Original ratio (2390/7965 ≈ 0.30) MUST still apply, not 50%.
|
||||
new_platform, new_operator = self._recompute(7965, 2390, 3982)
|
||||
# Expected with original ratio: round(3982 * 0.30006...) = 1195
|
||||
|
|
@ -187,17 +188,17 @@ class TestPartialDispenseSplitRatio:
|
|||
# Original platform share was ~30%; preserved within rounding.
|
||||
assert abs(new_platform / 3982 - 2390 / 7965) < 0.001
|
||||
|
||||
def test_zero_original_commission_yields_zero_platform(self):
|
||||
def test_zero_original_fee_yields_zero_platform(self):
|
||||
new_platform, new_operator = self._recompute(0, 0, 0)
|
||||
assert new_platform == 0
|
||||
assert new_operator == 0
|
||||
|
||||
def test_invariant_sum_equals_new_commission(self):
|
||||
def test_invariant_sum_equals_new_fee(self):
|
||||
# Random-ish parameter sweep over realistic values.
|
||||
cases = [
|
||||
(100, 30, 50),
|
||||
(100, 0, 50), # original platform_fee was 0 (super_pct=0)
|
||||
(100, 100, 50), # original platform_fee was 100 (super_pct=100)
|
||||
(100, 0, 50), # original platform_fee was 0 (super_fraction=0)
|
||||
(100, 100, 50), # original platform_fee was 100 (super_fraction=100)
|
||||
(7965, 2390, 3982),
|
||||
(7965, 7965, 3982),
|
||||
(1_000_000, 333_333, 250_000),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue