feat: add unit tests for DCA calculations with empirical Lamassu data
- Extract pure calculation functions to calculations.py (no lnbits deps) - transaction_processor.py now imports from calculations.py (DRY) - Add 22 tests covering commission, distribution, and fiat round-trip - Include real Lamassu transaction data (8.75%, 5.5% commission rates) - Test edge cases: discounts (90%, 100%), zero commission, small amounts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8d94dcc2b7
commit
397fd4b002
3 changed files with 560 additions and 84 deletions
|
|
@ -26,6 +26,7 @@ from lnbits.core.crud.wallets import get_wallet
|
|||
from lnbits.core.services import update_wallet_balance
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .calculations import calculate_commission, calculate_distribution, calculate_exchange_rate
|
||||
from .crud import (
|
||||
get_flow_mode_clients,
|
||||
get_payments_by_lamassu_transaction,
|
||||
|
|
@ -715,21 +716,13 @@ class LamassuTransactionProcessor:
|
|||
# Could use current time as fallback, but this indicates a data issue
|
||||
# transaction_time = datetime.now(timezone.utc)
|
||||
|
||||
# Calculate effective commission percentage after discount (following the reference logic)
|
||||
if commission_percentage > 0:
|
||||
effective_commission = commission_percentage * (100 - discount) / 100
|
||||
# Since crypto_atoms already includes commission, we need to extract the base amount
|
||||
# Formula: crypto_atoms = base_amount * (1 + effective_commission)
|
||||
# Therefore: base_amount = crypto_atoms / (1 + effective_commission)
|
||||
base_crypto_atoms = round(crypto_atoms / (1 + effective_commission))
|
||||
commission_amount_sats = crypto_atoms - base_crypto_atoms
|
||||
else:
|
||||
effective_commission = 0.0
|
||||
base_crypto_atoms = crypto_atoms
|
||||
commission_amount_sats = 0
|
||||
|
||||
# Calculate commission split using the extracted pure function
|
||||
base_crypto_atoms, commission_amount_sats, effective_commission = calculate_commission(
|
||||
crypto_atoms, commission_percentage, discount
|
||||
)
|
||||
|
||||
# Calculate exchange rate based on base amounts
|
||||
exchange_rate = base_crypto_atoms / fiat_amount if fiat_amount > 0 else 0 # sats per fiat unit
|
||||
exchange_rate = calculate_exchange_rate(base_crypto_atoms, fiat_amount)
|
||||
|
||||
logger.info(f"Transaction - Total crypto: {crypto_atoms} sats")
|
||||
logger.info(f"Commission: {commission_percentage*100:.1f}% - {discount:.1f}% discount = {effective_commission*100:.1f}% effective ({commission_amount_sats} sats)")
|
||||
|
|
@ -758,67 +751,29 @@ class LamassuTransactionProcessor:
|
|||
if total_confirmed_deposits == 0:
|
||||
logger.info("No clients with remaining DCA balance - skipping distribution")
|
||||
return {}
|
||||
|
||||
# Calculate proportional distribution with remainder allocation
|
||||
|
||||
# Calculate sat allocations using the extracted pure function
|
||||
sat_allocations = calculate_distribution(base_crypto_atoms, client_balances)
|
||||
|
||||
if not sat_allocations:
|
||||
logger.info("No allocations calculated - skipping distribution")
|
||||
return {}
|
||||
|
||||
# Build final distributions dict with additional tracking fields
|
||||
distributions = {}
|
||||
distributed_sats = 0
|
||||
client_calculations = []
|
||||
|
||||
# First pass: calculate base amounts and track remainders
|
||||
for client_id, client_balance in client_balances.items():
|
||||
# Calculate this client's proportion of the total DCA pool
|
||||
proportion = client_balance / total_confirmed_deposits
|
||||
|
||||
# Calculate exact share (with decimals)
|
||||
exact_share = base_crypto_atoms * proportion
|
||||
|
||||
# Use banker's rounding for base allocation
|
||||
client_sats_amount = round(exact_share)
|
||||
|
||||
client_calculations.append({
|
||||
'client_id': client_id,
|
||||
'proportion': proportion,
|
||||
'exact_share': exact_share,
|
||||
'allocated_sats': client_sats_amount,
|
||||
'client_balance': client_balance
|
||||
})
|
||||
|
||||
distributed_sats += client_sats_amount
|
||||
|
||||
# Handle any remainder due to rounding (should be small)
|
||||
remainder = base_crypto_atoms - distributed_sats
|
||||
|
||||
if remainder != 0:
|
||||
logger.info(f"Distributing remainder: {remainder} sats among {len(client_calculations)} clients")
|
||||
|
||||
# Sort clients by largest fractional remainder to distribute fairly
|
||||
client_calculations.sort(
|
||||
key=lambda x: x['exact_share'] - x['allocated_sats'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
# Distribute remainder one sat at a time to clients with largest fractional parts
|
||||
for i in range(abs(remainder)):
|
||||
if remainder > 0:
|
||||
client_calculations[i % len(client_calculations)]['allocated_sats'] += 1
|
||||
else:
|
||||
client_calculations[i % len(client_calculations)]['allocated_sats'] -= 1
|
||||
|
||||
# Second pass: create distributions with final amounts
|
||||
for calc in client_calculations:
|
||||
client_id = calc['client_id']
|
||||
client_sats_amount = calc['allocated_sats']
|
||||
proportion = calc['proportion']
|
||||
|
||||
for client_id, client_sats_amount in sat_allocations.items():
|
||||
# Calculate proportion for logging
|
||||
proportion = client_balances[client_id] / total_confirmed_deposits
|
||||
|
||||
# Calculate equivalent fiat value in GTQ for tracking purposes
|
||||
client_fiat_amount = round(client_sats_amount / exchange_rate, 2) if exchange_rate > 0 else 0.0
|
||||
|
||||
|
||||
distributions[client_id] = {
|
||||
"fiat_amount": client_fiat_amount,
|
||||
"sats_amount": client_sats_amount,
|
||||
"exchange_rate": exchange_rate
|
||||
}
|
||||
|
||||
|
||||
logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount:.2f} GTQ, {proportion:.2%} share)")
|
||||
|
||||
# Verification: ensure total distribution equals base amount
|
||||
|
|
@ -1062,18 +1017,13 @@ class LamassuTransactionProcessor:
|
|||
elif transaction_time.tzinfo != timezone.utc:
|
||||
transaction_time = transaction_time.astimezone(timezone.utc)
|
||||
|
||||
# Calculate commission metrics
|
||||
if commission_percentage > 0:
|
||||
effective_commission = commission_percentage * (100 - discount) / 100
|
||||
base_crypto_atoms = round(crypto_atoms / (1 + effective_commission))
|
||||
commission_amount_sats = crypto_atoms - base_crypto_atoms
|
||||
else:
|
||||
effective_commission = 0.0
|
||||
base_crypto_atoms = crypto_atoms
|
||||
commission_amount_sats = 0
|
||||
|
||||
# Calculate commission metrics using the extracted pure function
|
||||
base_crypto_atoms, commission_amount_sats, effective_commission = calculate_commission(
|
||||
crypto_atoms, commission_percentage, discount
|
||||
)
|
||||
|
||||
# Calculate exchange rate
|
||||
exchange_rate = base_crypto_atoms / fiat_amount if fiat_amount > 0 else 0
|
||||
exchange_rate = calculate_exchange_rate(base_crypto_atoms, fiat_amount)
|
||||
|
||||
# Create transaction data with GTQ amounts
|
||||
transaction_data = CreateLamassuTransactionData(
|
||||
|
|
@ -1201,12 +1151,10 @@ class LamassuTransactionProcessor:
|
|||
commission_percentage = transaction.get("commission_percentage", 0.0)
|
||||
discount = transaction.get("discount", 0.0)
|
||||
|
||||
if commission_percentage and commission_percentage > 0:
|
||||
effective_commission = commission_percentage * (100 - discount) / 100
|
||||
base_crypto_atoms = round(crypto_atoms / (1 + effective_commission))
|
||||
commission_amount_sats = crypto_atoms - base_crypto_atoms
|
||||
else:
|
||||
commission_amount_sats = 0
|
||||
# Calculate commission amount using the extracted pure function
|
||||
_, commission_amount_sats, _ = calculate_commission(
|
||||
crypto_atoms, commission_percentage, discount
|
||||
)
|
||||
|
||||
# Distribute to clients
|
||||
await self.distribute_to_clients(transaction, distributions)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue