Version 1.1

This commit is contained in:
2026-02-18 08:24:54 -05:00
parent 4318c8f642
commit fdba869a8d
33 changed files with 2142 additions and 1942 deletions

View File

@@ -0,0 +1,207 @@
"""Unit tests for lottery_calculator.py — pure calculation logic."""
from __future__ import annotations
import pytest
from lottery_calculator import (
calculate_annuity,
calculate_break_even,
calculate_canadian_lottery,
calculate_group_split,
calculate_us_lottery,
)
# ---------------------------------------------------------------------------
# calculate_us_lottery
# ---------------------------------------------------------------------------
class TestCalculateUSLottery:
"""Tests for the US lottery calculation."""
def test_basic_calculation(self):
result = calculate_us_lottery(100_000_000)
assert result["country"] == "US"
assert result["originalJackpot"] == 100_000_000
def test_cash_sum_is_lump_sum_rate(self):
result = calculate_us_lottery(100_000_000, lump_sum_rate=0.52)
assert result["cashSum"] == pytest.approx(52_000_000)
def test_federal_tax_applied(self):
result = calculate_us_lottery(100_000_000, lump_sum_rate=0.52, federal_tax_rate=0.37)
assert result["federalTax"] == pytest.approx(52_000_000 * 0.37)
def test_state_tax_applied(self):
result = calculate_us_lottery(100_000_000, state_tax_rate=0.10)
assert result["stateTaxRate"] == 0.10
def test_net_amount_usd(self):
result = calculate_us_lottery(
100_000_000, lump_sum_rate=0.50, federal_tax_rate=0.30, state_tax_rate=0.05
)
expected = 50_000_000 * (1 - 0.30 - 0.05)
assert result["netAmountUsd"] == pytest.approx(expected)
def test_cad_conversion(self):
result = calculate_us_lottery(100_000_000, usd_cad_rate=1.40)
assert result["netAmountCad"] == pytest.approx(result["netAmountUsd"] * 1.40)
def test_investment_split(self):
result = calculate_us_lottery(100_000_000, invest_percentage=0.80)
total = result["investmentPrincipal"] + result["funMoney"]
assert total == pytest.approx(result["netAmountCad"])
assert result["funMoney"] == pytest.approx(result["netAmountCad"] * 0.20)
def test_cycles_count(self):
result = calculate_us_lottery(100_000_000, cycles=5)
assert len(result["cycles"]) == 5
def test_principal_grows(self):
result = calculate_us_lottery(100_000_000, cycles=4)
for i in range(1, len(result["cycles"])):
assert result["cycles"][i]["principalStart"] >= result["cycles"][i - 1]["principalStart"]
def test_final_principal_positive(self):
result = calculate_us_lottery(500_000_000)
assert result["finalPrincipal"] > 0
def test_daily_income_positive(self):
result = calculate_us_lottery(500_000_000)
assert result["netDailyIncome"] > 0
def test_zero_state_tax(self):
"""Florida / Texas have 0% state tax."""
result = calculate_us_lottery(100_000_000, state_tax_rate=0.0)
assert result["stateTax"] == 0.0
assert result["netAmountUsd"] > calculate_us_lottery(100_000_000, state_tax_rate=0.10)["netAmountUsd"]
# ---------------------------------------------------------------------------
# calculate_canadian_lottery
# ---------------------------------------------------------------------------
class TestCalculateCanadianLottery:
"""Tests for Canadian lottery calculation (tax-free winnings)."""
def test_basic_calculation(self):
result = calculate_canadian_lottery(50_000_000)
assert result["country"] == "Canada"
assert result["originalJackpot"] == 50_000_000
def test_tax_free(self):
result = calculate_canadian_lottery(50_000_000)
assert result["netAmountCad"] == 50_000_000
def test_investment_split(self):
result = calculate_canadian_lottery(50_000_000, invest_percentage=0.90)
assert result["investmentPrincipal"] == pytest.approx(45_000_000)
assert result["funMoney"] == pytest.approx(5_000_000)
def test_cycles_count(self):
result = calculate_canadian_lottery(50_000_000, cycles=3)
assert len(result["cycles"]) == 3
def test_daily_income_positive(self):
result = calculate_canadian_lottery(50_000_000)
assert result["netDailyIncome"] > 0
# ---------------------------------------------------------------------------
# calculate_break_even
# ---------------------------------------------------------------------------
class TestCalculateBreakEven:
"""Tests for the break-even expected-value calculator."""
def test_us_break_even(self):
result = calculate_break_even(odds=292_201_338, ticket_cost=2.0, country="us")
assert result["breakEvenJackpot"] > 0
assert result["breakEvenJackpot"] > 292_201_338 # must be >> odds because of tax
def test_canadian_break_even(self):
result = calculate_break_even(odds=13_983_816, ticket_cost=3.0, country="canadian")
# Canadian take-home fraction = 1.0, so break-even = 3 * 13_983_816
expected = 3.0 * 13_983_816
assert result["breakEvenJackpot"] == pytest.approx(expected)
def test_ev_equals_ticket_cost(self):
result = calculate_break_even(odds=100, ticket_cost=5.0, country="canadian")
# EV at break even = probability * jackpot * 1.0 = 5.0
assert result["expectedValueAtBreakEven"] == pytest.approx(5.0, rel=1e-6)
def test_higher_tax_needs_bigger_jackpot(self):
r1 = calculate_break_even(odds=100, ticket_cost=2.0, country="us", state_tax_rate=0.0)
r2 = calculate_break_even(odds=100, ticket_cost=2.0, country="us", state_tax_rate=0.10)
assert r2["breakEvenJackpot"] > r1["breakEvenJackpot"]
# ---------------------------------------------------------------------------
# calculate_annuity
# ---------------------------------------------------------------------------
class TestCalculateAnnuity:
"""Tests for the annuity payout schedule."""
def test_us_annuity_schedule_length(self):
result = calculate_annuity(500_000_000, country="us", years=30)
assert len(result["schedule"]) == 30
def test_canadian_no_tax(self):
result = calculate_annuity(100_000_000, country="canadian", years=10, annual_increase=0.0)
for entry in result["schedule"]:
assert entry["tax"] == 0.0
assert entry["afterTax"] == entry["preTax"]
def test_total_pretax_approximates_jackpot(self):
result = calculate_annuity(100_000_000, country="canadian", years=30, annual_increase=0.05)
assert result["totalPreTax"] == pytest.approx(100_000_000, rel=1e-6)
def test_payments_increase_annually(self):
result = calculate_annuity(100_000_000, years=5, annual_increase=0.05)
for i in range(1, len(result["schedule"])):
assert result["schedule"][i]["preTax"] > result["schedule"][i - 1]["preTax"]
def test_zero_increase(self):
result = calculate_annuity(100_000_000, years=5, annual_increase=0.0)
payments = [e["preTax"] for e in result["schedule"]]
assert all(p == pytest.approx(payments[0]) for p in payments)
# ---------------------------------------------------------------------------
# calculate_group_split
# ---------------------------------------------------------------------------
class TestCalculateGroupSplit:
"""Tests for the group play calculator."""
def test_equal_split(self):
result = calculate_group_split(100_000_000, members=4, country="canadian")
assert len(result["memberResults"]) == 4
for m in result["memberResults"]:
assert m["jackpotShare"] == pytest.approx(25_000_000)
def test_custom_shares(self):
shares = [0.5, 0.3, 0.2]
result = calculate_group_split(100_000_000, members=3, shares=shares, country="canadian")
assert result["memberResults"][0]["jackpotShare"] == pytest.approx(50_000_000)
assert result["memberResults"][1]["jackpotShare"] == pytest.approx(30_000_000)
assert result["memberResults"][2]["jackpotShare"] == pytest.approx(20_000_000)
def test_shares_normalize(self):
"""Shares that don't sum to 1.0 should be normalised."""
shares = [2, 2, 1] # sums to 5
result = calculate_group_split(100_000_000, members=3, shares=shares, country="canadian")
assert result["memberResults"][0]["share"] == pytest.approx(0.4)
assert result["memberResults"][2]["share"] == pytest.approx(0.2)
def test_mismatched_shares_falls_back_to_equal(self):
result = calculate_group_split(100_000_000, members=3, shares=[0.5, 0.5], country="canadian")
for m in result["memberResults"]:
assert m["share"] == pytest.approx(1 / 3)
def test_each_member_gets_calculation(self):
result = calculate_group_split(100_000_000, members=2, country="us")
for m in result["memberResults"]:
assert "calculation" in m
assert m["calculation"]["country"] == "US"