tests/test_minimum_tax.py
Rama main · réplica de lectura del repositorio original.
"""The 30% minimum tax on capital gains from 1 July 2027 (decisions.md #151).
ITAA 1997 Div 119, as inserted by Act No. 49 of 2026: an individual pays extra
tax of 30% of the year's minimum tax capital gain, its gains from 1 July 2027
left after losses, less the basic income tax that gain adds, rounded down to a
whole dollar, when that is more than nil (s 119-10). Basic income tax leaves out
the Medicare levy. Nobody who gets an income support payment in the year pays
it (s 119-15). With one income tax rate (#147), the extra is (30% − the rate)
of the gain, worked out for the year, so a sale adds the difference it makes.
Every figure below is worked out by hand.
"""
from __future__ import annotations
import datetime as dt
import re
from decimal import Decimal
import pytest
from freezegun import freeze_time
from test_api import bearer, issue_key
from test_cgt_2027 import _cpi, _fy_page
import factories as fac
from app import fyreport, simulate
from app.models import CpiQuarter, Portfolio
from test_routes import bind_to_only_portfolio, make_login, session_csrf
D = dt.date
HTML = {"accept": "text/html"}
RATES = "/api/v1/portfolio/tax-rates"
@pytest.fixture
def alpha(pf):
"""100 bought in 2025 for 1,010, worth 1,500 at the close of 30 June 2027,
and 2,000 now, in August 2028, before the September quarter's CPI is out.
Sold whole: 490 to 30 June 2027, discounted, and 1,990 − 1,500 × 1.030 =
445 after it, which is the minimum tax capital gain. (As test_cgt_2027's
alpha, copied rather than imported: testing.md.)"""
_cpi(pf)
pf.query(CpiQuarter).filter_by(quarter=D(2028, 7, 1)).delete()
inst = fac.make_instrument(pf, "ALPHA", asset_class="share")
fac.add_trade(pf, inst, "2025-01-06", "buy", 100, "10.00", brokerage="10")
fac.add_prices(pf, inst, [("2027-06-30", "15.00"), ("2028-08-14", "20.00")])
return inst
def _sell(pf, inst, **kwargs):
return simulate.sale(pf, {inst.id: Decimal(100)}, **kwargs)
@freeze_time("2028-08-15")
def test_below_30_percent_the_gains_from_1_july_2027_are_topped_up(pf, alpha):
"""At 15%: income tax on the 690 taxable is 103.50 and the levy 13.80; the
445 is topped up by 15% of it, 66.75, rounded down to 66."""
sale = _sell(pf, alpha, income_tax_rate=Decimal(15), medicare_levy=Decimal(2))
assert (sale.taxable, sale.income_tax, sale.levy, sale.minimum_tax) == (
Decimal("690.00"), Decimal("103.50"), Decimal("13.80"), Decimal("66"))
assert sale.tax == Decimal("183.30")
assert sale.take_home == Decimal("1806.70") # 2,000 − 10 brokerage − 183.30
@freeze_time("2028-08-15")
def test_with_no_income_tax_at_all_it_is_the_whole_30_percent(pf, alpha):
"""Under the tax-free threshold: 30% of 445 is 133.50, so 133."""
sale = _sell(pf, alpha, income_tax_rate=Decimal(0))
assert (sale.income_tax, sale.minimum_tax, sale.tax) == (0, Decimal("133"), Decimal("133"))
@freeze_time("2028-08-15")
@pytest.mark.parametrize("rate", [Decimal(30), Decimal(45)])
def test_at_30_percent_or_more_there_is_nothing_to_top_up(pf, alpha, rate):
assert _sell(pf, alpha, income_tax_rate=rate).minimum_tax == 0
@freeze_time("2028-08-15")
def test_it_is_the_years_figure_that_is_rounded_down(pf, alpha):
"""BETAX made 22.20 in July, held under a year so not indexed: the year's
top-up was 15% of it, 3.33, so 3. With the sale it is 15% of 467.20, 70.08,
so 70: the sale adds 67, where its own 66.75 rounded down would be 66."""
beta = fac.make_instrument(pf, "BETAX", asset_class="share")
fac.add_trade(pf, beta, "2028-01-10", "buy", 10, "100.00", brokerage="0")
fac.add_trade(pf, beta, "2028-07-20", "sell", 10, "102.22", brokerage="0")
# BETAX's is the last brokerage now, so ALPHA's 10 is typed.
sale = _sell(pf, alpha, income_tax_rate=Decimal(15), brokerage={alpha.id: Decimal(10)})
assert sale.minimum_tax == Decimal("67")
@freeze_time("2028-08-15")
def test_nobody_on_income_support_pays_it(pf, alpha):
pf.query(Portfolio).one().income_support = True
sale = _sell(pf, alpha, income_tax_rate=Decimal(15))
assert (sale.minimum_tax, sale.tax) == (0, Decimal("103.50"))
@freeze_time("2028-08-15")
def test_without_an_income_tax_rate_there_is_no_figure(pf, alpha):
assert _sell(pf, alpha).minimum_tax is None
@freeze_time("2027-06-15")
def test_before_1_july_2027_there_is_none(pf):
"""A sale today is under the old rules: there are no gains from 1 July
2027 for the tax to be worked on."""
gamma = fac.make_instrument(pf, "GAMMA", asset_class="share")
fac.add_trade(pf, gamma, "2025-01-06", "buy", 100, "10.00", brokerage="10")
fac.add_prices(pf, gamma, [("2027-06-14", "20.00")])
sale = simulate.sale(pf, {gamma.id: Decimal(100)}, income_tax_rate=Decimal(15))
assert (sale.minimum_tax, fyreport.fy_cgt(pf, 2027).minimum_tax_gain) == (0, 0)
def _owner(client, session_factory, **saved) -> dict:
make_login(client, session_factory)
with session_factory() as s:
portfolio = bind_to_only_portfolio(s)
for column, value in saved.items():
setattr(portfolio, column, value)
s.commit()
return {"portfolio": portfolio.id, "user": portfolio.members[0].user_id}
@freeze_time("2028-08-15")
def test_the_simulator_shows_the_top_up_with_the_other_tax(client, session_factory):
_owner(client, session_factory, income_tax_rate=Decimal(15), medicare_levy=Decimal(2))
with session_factory() as s:
bind_to_only_portfolio(s)
_cpi(s)
s.query(CpiQuarter).filter_by(quarter=D(2028, 7, 1)).delete()
inst = fac.make_instrument(s, "ALPHA", asset_class="share")
fac.add_trade(s, inst, "2025-01-06", "buy", 100, "10.00", brokerage="10")
fac.add_prices(s, inst, [("2027-06-30", "15.00"), ("2028-08-14", "20.00")])
s.commit()
alpha_id = inst.id
page = client.get("/simulate", params={f"units_{alpha_id}": "100"}, headers=HTML).text
row = re.search(r"<tr><td>Minimum tax top-up.*?</tr>", page, re.S)
assert row and re.search(r'−<span class="m">66\.00</span>', row.group(0))
assert "About the minimum tax" in row.group(0)
assert "183.30" in page # the Tax tile counts it
@freeze_time("2029-01-15")
def test_the_fy_page_gives_the_minimum_tax_capital_gain(client, session_factory):
"""test_cgt_2027's year: 430 from 1 July 2027 and no losses."""
page = _fy_page(client, session_factory)
assert re.search(r'<td>Minimum tax capital gain.*?</td><td class="num">'
r'<span class="m">430\.00</span>', page, re.S)
def test_the_owner_says_so_in_portfolio_settings(client, session_factory):
_owner(client, session_factory)
def save(**ticked):
data = {"_csrf": session_csrf(session_factory), "name": "Main",
"reporting_currency": "AUD", "jurisdiction": "AU", **ticked}
return client.post("/portfolio/settings", data=data, headers=HTML,
follow_redirects=False)
def stored():
with session_factory() as s:
return s.query(Portfolio).one().income_support
assert save(income_support="1").status_code == 303
box = re.search(r'<input[^>]*name="income_support"[^>]*>',
client.get("/members", headers=HTML).text).group(0)
assert (stored(), "checked" in box) == (True, True)
save()
assert stored() is False
def test_the_api_reads_and_sets_it_beside_the_rates(client, session_factory):
ids = _owner(client, session_factory, income_tax_rate=Decimal(15))
key = bearer(issue_key(session_factory, ids["portfolio"], created_by=ids["user"],
scopes="read,write"))
before = client.get("/api/v1/portfolio", headers=key).json()["portfolio"]
resp = client.put(RATES, headers=key, json={"income_support": True})
refused = client.put(RATES, headers=key, json={"income_support": None})
assert before["income_support"] is False
assert resp.json() == {"income_tax_rate": 15.0, "medicare_levy": None,
"income_support": True}
assert refused.status_code == 422