mirror of
https://github.com/mblanke/Lottery-Tracker.git
synced 2026-03-01 06:00:21 -05:00
208 lines
8.7 KiB
Python
208 lines
8.7 KiB
Python
"""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"
|