"""Capital gains from 1 July 2027 (decisions.md #150). Treasury Laws Amendment (Tax Reform No. 1) Act 2026, No. 49 of 2026: - An asset held through 30 June 2027 is treated as sold just before 1 July 2027 for its market value, and bought again on 1 July 2027 for the same (ITAA 1997 s 112-155). The gain to then is deferred to the year of the real sale, and is discounted by 50% if the asset was held at least 12 months by that sale (s 112-160). - From 1 July 2027 a cost base is indexed by the CPI instead (s 110-36(1A)), if the asset was acquired at least 12 months before the sale, counted from the real acquisition (s 114-10(9)). Indexing cannot make or grow a loss: a loss is worked from the cost base as it was. - The year's losses reduce the deferred gains first and the others after; losses carried in do the same; the discount comes last (s 102-5(1)). Every figure below is worked out by hand. The CPI numbers are made up. """ from __future__ import annotations import datetime as dt import re from decimal import Decimal import pytest from freezegun import freeze_time import factories as fac from app import exports, fyreport, queries, simulate from app.models import CpiQuarter D = dt.date CPI = {D(2027, 7, 1): "100.00", D(2027, 10, 1): "101.00", D(2028, 1, 1): "102.00", D(2028, 4, 1): "103.00", D(2028, 7, 1): "104.00"} def _cpi(pf, quarters=None): for quarter, number in (quarters or CPI).items(): pf.add(CpiQuarter(quarter=quarter, index_number=Decimal(number), reference_period="Sep 2025 = 100.0")) pf.flush() def _sale(pf, inst) -> fyreport.Disposal: [sale] = fyreport.instrument_disposals(inst, queries.FxBook(pf)) return sale @pytest.fixture def alpha(pf): """100 bought in 2025 for 1,010, worth 1,500 at the close of 30 June 2027.""" _cpi(pf) 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")]) return inst @freeze_time("2029-01-15") def test_a_parcel_held_through_30_june_2027_is_split_at_its_value_then(pf, alpha): """Sold for 1,990 net in August 2028. Deferred: 1,500 − 1,010 = 490, discountable. After: 1,500 indexed by 104/100 is 1,560, so 430.""" fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") [use] = _sale(pf, alpha).parcels cgt = fyreport.fy_cgt(pf, 2029) assert (use.deferred_proceeds, use.deferred_cost, use.deferred) == ( Decimal("1500.00"), Decimal("1010.00"), Decimal("490.00")) assert (use.cost_base, use.factor, use.indexed_cost, use.proceeds) == ( Decimal("1500.00"), Decimal("1.040"), Decimal("1560.00"), Decimal("1990.00")) assert (use.indexed_gain, use.gain, use.discountable) == ( Decimal("430.00"), Decimal("920.00"), True) # What was paid and what indexing took off: 1,990 − 1,010 − 60 is the 920. assert (use.cost, use.indexation, use.discount) == ( Decimal("1010.00"), Decimal("60.00"), Decimal("245.00")) assert (cgt.gains_discountable, cgt.gains_from_july_2027, cgt.discount, cgt.net_capital_gain) == ( Decimal("490.00"), Decimal("430.00"), Decimal("245.00"), Decimal("675.00")) @freeze_time("2029-01-15") def test_a_parcel_bought_from_1_july_2027_is_indexed_from_its_own_quarter(pf): _cpi(pf) beta = fac.make_instrument(pf, "BETAX", asset_class="share") fac.add_trade(pf, beta, "2027-08-02", "buy", 100, "10.00", brokerage="10") fac.add_trade(pf, beta, "2028-09-01", "sell", 100, "12.00", brokerage="10") [use] = _sale(pf, beta).parcels assert (use.deferred, use.factor, use.indexed_cost, use.gain) == ( None, Decimal("1.040"), Decimal("1050.40"), Decimal("139.60")) assert fyreport.fy_cgt(pf, 2029).net_capital_gain == Decimal("139.60") @freeze_time("2029-01-15") def test_held_under_a_year_there_is_no_discount_and_no_indexing(pf): """Bought 3 May 2027, sold 1 November 2027: the year is counted from the real purchase, not the deemed one on 1 July (s 114-10(9)).""" _cpi(pf) gamma = fac.make_instrument(pf, "GAMMA", asset_class="share") fac.add_trade(pf, gamma, "2027-05-03", "buy", 100, "10.00", brokerage="10") fac.add_prices(pf, gamma, [("2027-06-30", "15.00")]) fac.add_trade(pf, gamma, "2027-11-01", "sell", 100, "16.00", brokerage="10") [use] = _sale(pf, gamma).parcels cgt = fyreport.fy_cgt(pf, 2028) assert (use.deferred, use.discountable, use.factor, use.indexed_gain) == ( Decimal("490.00"), False, None, Decimal("90.00")) assert (cgt.discount, cgt.net_capital_gain) == (0, Decimal("580.00")) # Not indexed, but a gain from 1 July 2027 all the same: the minimum tax's. assert (cgt.gains_other, cgt.gains_from_july_2027, cgt.minimum_tax_gain) == ( Decimal("490.00"), Decimal("90.00"), Decimal("90.00")) @freeze_time("2029-01-15") def test_the_year_is_counted_from_the_real_purchase(pf): """Bought 1 March 2027 and sold 3 April 2028: over a year since it was bought, under one since 1 July 2027. So the gain to then is discounted, and the cost base after is indexed, 103/100: 1,545, and 445 after.""" _cpi(pf) delta = fac.make_instrument(pf, "DELTA", asset_class="share") fac.add_trade(pf, delta, "2027-03-01", "buy", 100, "10.00", brokerage="10") fac.add_prices(pf, delta, [("2027-06-30", "15.00")]) fac.add_trade(pf, delta, "2028-04-03", "sell", 100, "20.00", brokerage="10") [use] = _sale(pf, delta).parcels assert (use.discountable, use.factor, use.indexed_gain, use.discount) == ( True, Decimal("1.030"), Decimal("445.00"), Decimal("245.00")) @freeze_time("2029-01-15") def test_an_increase_after_1_july_2027_is_indexed_from_its_own_quarter(pf, alpha): """An AMIT increase of 104 in February 2028 is indexed by 104/102, 1.020, to 106.08, beside the 1,500 by 1.040 to 1,560: 1,666.08, so 323.92 after. What it cost is the 1,010 paid and the 104.""" fac.add_action(pf, alpha, "2028-02-01", "cost_base", amount="104.00") fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") [use] = _sale(pf, alpha).parcels assert (use.cost_base, use.indexed_cost, use.indexed_gain) == ( Decimal("1604.00"), Decimal("1666.08"), Decimal("323.92")) assert (use.cost, use.indexation) == (Decimal("1114.00"), Decimal("62.08")) @freeze_time("2029-01-15") @pytest.mark.parametrize("price, gain, indexation", [ ("10.30", Decimal("0"), Decimal("10.00")), ("9.10", Decimal("-110.00"), Decimal("0")), ], ids=["between-the-two-cost-bases", "below-the-cost-base"]) def test_indexing_cannot_make_a_loss(pf, price, gain, indexation): """1,010 indexed is 1,050.40. Sold for 1,020: no gain, and no loss either, so only 10 of the 40.40 is used. Sold for 900: a loss of 110, from the cost base as it was, and none of it.""" _cpi(pf) beta = fac.make_instrument(pf, "BETAX", asset_class="share") fac.add_trade(pf, beta, "2027-08-02", "buy", 100, "10.00", brokerage="10") fac.add_trade(pf, beta, "2028-09-01", "sell", 100, price, brokerage="10") [use] = _sale(pf, beta).parcels assert (use.gain, use.indexation) == (gain, indexation) @freeze_time("2029-01-15") def test_the_years_losses_reduce_the_deferred_gains_first(pf, alpha): """s 102-5(1) step 1: a 100 loss comes off the 490 deferred gain, not the 430 indexed one, so 390 is halved: 195 + 430 = 625, where taking it off the indexed gain would have given 575.""" fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") beta = fac.make_instrument(pf, "BETAX", asset_class="share") fac.add_trade(pf, beta, "2028-07-03", "buy", 10, "20.00") fac.add_trade(pf, beta, "2028-09-01", "sell", 10, "10.00") cgt = fyreport.fy_cgt(pf, 2029) assert (cgt.losses, cgt.losses_applied_discountable, cgt.losses_applied_from_july_2027) == ( Decimal("100.00"), Decimal("100.00"), 0) assert cgt.net_capital_gain == Decimal("625.00") @freeze_time("2029-01-15") def test_a_loss_carried_in_follows_the_same_order(pf, alpha): """A 100 loss in FY2028 carries to FY2029 and comes off the deferred gain.""" gamma = fac.make_instrument(pf, "GAMMA", asset_class="share") fac.add_trade(pf, gamma, "2027-08-02", "buy", 10, "20.00") fac.add_trade(pf, gamma, "2027-09-01", "sell", 10, "10.00") fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") earlier, cgt = fyreport.fy_cgt(pf, 2028), fyreport.fy_cgt(pf, 2029) assert (earlier.net_capital_loss, earlier.losses_carried_out) == ( Decimal("100.00"), Decimal("100.00")) assert (cgt.losses_carried_applied, cgt.net_capital_gain) == ( Decimal("100.00"), Decimal("625.00")) @freeze_time("2029-01-15") def test_a_fall_to_30_june_2027_is_a_loss_in_the_year_of_the_sale(pf, alpha): """Worth 800 at the close of 30 June 2027 against 1,010: a deferred loss of 210. After: 800 indexed is 832, so 358, less that loss: 148.""" fac.add_prices(pf, alpha, [("2027-06-29", "8.00")]) pf.query(fac.Price).filter_by(instrument_id=alpha.id, date=D(2027, 6, 30)).delete() fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "12.00", brokerage="10") [use] = _sale(pf, alpha).parcels cgt = fyreport.fy_cgt(pf, 2029) assert (use.deferred, use.indexed_gain) == (Decimal("-210.00"), Decimal("358.00")) assert (cgt.losses, cgt.losses_applied_from_july_2027, cgt.net_capital_gain) == ( Decimal("210.00"), Decimal("210.00"), Decimal("148.00")) @freeze_time("2029-01-15") def test_a_foreign_parcel_is_valued_at_the_rate_of_30_june_2027(pf): """Cost 1,005 × 1.60 = 1,608; worth 120 × 10 × 1.50 = 1,800 then; sold for (1,300 − 5) × 1.40 = 1,813. Deferred 192; 1,800 indexed is 1,872, more than 1,813 and less than 1,800 is not, so nothing after.""" _cpi(pf) nova = fac.make_instrument(pf, "NOVA", exchange="NASDAQ", currency="USD") fac.add_trade(pf, nova, "2025-01-06", "buy", 10, "100.00", brokerage="5", fx_rate="1.60") fac.add_prices(pf, nova, [("2027-06-30", "120.00")]) # 30 June's rate, not the day before's or the day after's. for day, rate in (("2027-06-29", "1.55"), ("2027-06-30", "1.50"), ("2027-07-01", "1.45")): fac.add_fx(pf, "USDAUD", day, rate) fac.add_trade(pf, nova, "2028-08-01", "sell", 10, "130.00", brokerage="5", fx_rate="1.40") [use] = _sale(pf, nova).parcels assert (use.deferred_proceeds, use.deferred, use.indexed_gain, use.gain) == ( Decimal("1800.00"), Decimal("192.00"), 0, Decimal("192.00")) @freeze_time("2029-01-15") def test_a_quarter_not_yet_published_leaves_the_sale_out_and_says_why(pf, alpha): pf.query(CpiQuarter).filter_by(quarter=D(2028, 7, 1)).delete() fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") sale = _sale(pf, alpha) cgt = fyreport.fy_cgt(pf, 2029) assert (sale.known, sale.parcels[0].awaiting_cpi) == (False, D(2028, 7, 1)) assert (cgt.awaiting_cpi, cgt.net_capital_gain) == (["ALPHA"], 0) @freeze_time("2029-01-15") def test_without_a_close_in_the_last_week_of_june_2027_the_value_is_unknown(pf, alpha): """The market value just before 1 July 2027 is not a close from months earlier: a suspended or unpriced holding is named instead.""" pf.query(fac.Price).filter_by(instrument_id=alpha.id).delete() fac.add_prices(pf, alpha, [("2027-03-31", "14.00")]) fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") cgt = fyreport.fy_cgt(pf, 2029) assert (cgt.unvalued, cgt.net_capital_gain) == (["ALPHA"], 0) @freeze_time("2029-01-15") def test_a_return_of_capital_after_1_july_2027_comes_off_the_value_unindexed(pf, alpha): """100 back in December 2027: the tax cost base from 1 July 2027 is 1,400, and only the 1,500 is indexed: 1,560 − 100 = 1,460, so 530 after. What the holding cost, on the dashboard, is 910.""" fac.add_action(pf, alpha, "2027-12-01", "capital_return", amount="100.00") with freeze_time("2028-03-01"): [held] = queries.all_holdings(pf) fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") pf.expire_all() [use] = _sale(pf, alpha).parcels assert held.cost_aud == Decimal("910") assert (use.cost_base, use.indexed_cost, use.indexed_gain, use.deferred) == ( Decimal("1400.00"), Decimal("1460.00"), Decimal("530.00"), Decimal("490.00")) # What it cost is the holding's 910: 1,990 − 910 − 60 is the 1,020. assert (use.cost, use.indexation, use.gain) == ( Decimal("910.00"), Decimal("60.00"), Decimal("1020.00")) @freeze_time("2028-01-15") def test_the_dashboard_still_shows_what_was_paid(pf, alpha): """The value on 30 June 2027 is a tax figure, not a cost.""" [holding] = queries.all_holdings(pf) assert holding.cost_aud == Decimal("1010") @freeze_time("2029-01-15") def test_the_realised_gains_export_shows_the_working(pf, alpha): fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") headers, rows = exports.realised_cgt(pf, fy=2029) row = dict(zip(headers, rows[0])) assert (row["Value at 30 Jun 2027 (AUD)"], row["Gain to 30 Jun 2027 (AUD)"], row["Indexation factor"], row["Indexed cost base (AUD)"]) == ( Decimal("1500.00"), Decimal("490.00"), Decimal("1.040"), Decimal("1560.00")) assert (row["Cost base (AUD)"], row["Indexation (AUD)"], row["Gross gain (AUD)"], row["CGT discount (AUD)"], row["Gain after discount (AUD)"]) == ( Decimal("1010.00"), Decimal("60.00"), Decimal("920.00"), Decimal("245.00"), Decimal("675.00")) @freeze_time("2029-01-15") def test_the_unrealised_gains_export_works_a_sale_today_as_the_simulator_does(pf, alpha): """Worth 2,000 now, against 1,010 paid: 990. As a sale today: 490 to 30 June 2027, halved, and 2,000 − 1,560 = 440 after it, the CPI of the September 2028 quarter standing in for this one's.""" fac.add_prices(pf, alpha, [("2029-01-14", "20.00")]) headers, [row] = exports.unrealised_cgt(pf) row = dict(zip(headers, row)) assert (row["Cost base (AUD)"], row["Market value (AUD)"], row["Unrealised gain (AUD)"]) == ( Decimal("1010.00"), Decimal("2000.00"), Decimal("990.00")) assert (row["Indexation (AUD)"], row["Gain after discount (AUD)"]) == ( Decimal("60.00"), Decimal("685.00")) @freeze_time("2029-01-15") def test_a_closed_position_is_discounted_only_to_30_june_2027(pf, alpha): """Its return is still what came back over what was paid: 1,990 − 1,010.""" fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") headers, [row] = exports.closed_positions(pf) row = dict(zip(headers, row)) assert (row["Outlay incl. brokerage (AUD)"], row["Gross capital gain (AUD)"], row["CGT discount (AUD)"], row["Total return incl. dividends (AUD)"]) == ( Decimal("1010.00"), Decimal("920.00"), Decimal("245.00"), Decimal("980.00")) @freeze_time("2028-08-15") def test_a_simulated_sale_estimates_with_the_latest_cpi_and_says_so(pf, alpha): """The September 2028 quarter is not out by August: the June quarter's 103 stands in, so 1,500 × 1.030 = 1,545 and 1,990 − 1,545 = 445.""" fac.add_prices(pf, alpha, [("2028-08-14", "20.00")]) pf.query(CpiQuarter).filter_by(quarter=D(2028, 7, 1)).delete() sale = simulate.sale(pf, {alpha.id: Decimal(100)}, income_tax_rate=Decimal(30)) [line] = sale.selling assert sale.cpi_estimate == D(2028, 4, 1) assert (line.gain, sale.discount, sale.taxable) == ( Decimal("935.00"), Decimal("245.00"), Decimal("690.00")) # Its table adds up: 2,000 − 10 − 1,010 − 45 is the 935. assert (sale.value, sale.brokerage, sale.cost_base, sale.indexation) == ( Decimal("2000.00"), Decimal("10.00"), Decimal("1010.00"), Decimal("45.00")) @freeze_time("2028-12-15") def test_a_simulated_sale_counts_this_years_gains_from_1_july_2027(pf, alpha): """ALPHA's sale in August made 490 to 30 June 2027 and 430 after it.""" fac.add_trade(pf, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") beta = fac.make_instrument(pf, "BETAX", asset_class="share") fac.add_trade(pf, beta, "2028-09-01", "buy", 10, "10.00") fac.add_prices(pf, beta, [("2028-12-14", "11.00")]) sale = simulate.sale(pf, {beta.id: Decimal(10)}, income_tax_rate=Decimal(30)) assert sale.year_so_far == Decimal("920.00") def _signed_in(client, session_factory, *, cpi=True, close="15.00", sold=True): """Signed in, with the `alpha` fixture's ALPHA, sold in August 2028 unless not `sold`; returns the user's and the portfolio's ids.""" from test_routes import bind_to_only_portfolio, make_login make_login(client, session_factory) with session_factory() as s: portfolio = bind_to_only_portfolio(s) if cpi: _cpi(s) alpha = fac.make_instrument(s, "ALPHA", asset_class="share") fac.add_trade(s, alpha, "2025-01-06", "buy", 100, "10.00", brokerage="10") fac.add_prices(s, alpha, [("2027-06-30", close)] if close else []) if sold: fac.add_trade(s, alpha, "2028-08-01", "sell", 100, "20.00", brokerage="10") portfolio.income_tax_rate = Decimal(30) s.commit() return portfolio.members[0].user_id, portfolio.id, alpha.id def _fy_page(client, session_factory, *, cpi=True, fy=2029) -> str: _signed_in(client, session_factory, cpi=cpi) return client.get("/", params={"fy": fy}, headers={"accept": "text/html"}).text @freeze_time("2029-01-15") def test_the_fy_page_shows_the_2027_working(client, session_factory): """The value on 30 June 2027, the indexed cost base and the factor, and the gains to 30 June 2027 apart from the indexed ones.""" page = _fy_page(client, session_factory) sales = page.split("

Positions closed this year", 1)[1] row = re.search(r"\s*2028-08-01.*?", sales, re.S).group(0) # Units, cost, value then, indexation, proceeds, gain: 1,990 − 1,010 − 60. assert re.findall(r">([\d,]+(?:\.\d\d)?)<", row) == [ "100", "1,010.00", "1,500.00", "60.00", "1,990.00", "920.00"] for shown in ("Value 30 Jun 2027", "Indexation (", "2025-01-06 ✓ ×1.040", "Gains to 30 June 2027 on parcels held", "Gains from 1 July 2027", "675.00"): assert shown in sales, shown @freeze_time("2029-01-15") def test_the_fy_page_names_a_sale_waiting_on_the_cpi(client, session_factory): page = _fy_page(client, session_factory, cpi=False) assert "Totals exclude ALPHA — no CPI yet for the quarter of the sale." in page @freeze_time("2027-06-15") def test_a_year_before_1_july_2027_shows_none_of_it(client, session_factory): from test_routes import bind_to_only_portfolio, make_login make_login(client, session_factory) with session_factory() as s: bind_to_only_portfolio(s) alpha = fac.make_instrument(s, "ALPHA", asset_class="share") fac.add_trade(s, alpha, "2025-01-06", "buy", 100, "10.00") fac.add_trade(s, alpha, "2026-09-01", "sell", 100, "20.00") s.commit() page = client.get("/", params={"fy": 2027}, headers={"accept": "text/html"}).text assert "Value 30 Jun 2027" not in page and "Gains from 1 July 2027" not in page def _simulator(client, session_factory, **kwargs) -> str: """The simulator, selling all of an unsold ALPHA, at 20.00 yesterday.""" _user, _portfolio, alpha_id = _signed_in(client, session_factory, sold=False, **kwargs) yesterday = dt.date.today() - dt.timedelta(days=1) with session_factory() as s: fac.add_prices(s, s.get(fac.Instrument, alpha_id), [(yesterday, "20.00")]) s.commit() return client.get("/simulate", params={f"units_{alpha_id}": "100"}, headers={"accept": "text/html"}).text @freeze_time("2028-11-15") def test_the_simulator_names_the_quarter_whose_cpi_stood_in(client, session_factory): page = _simulator(client, session_factory) assert "Indexation uses the CPI for the September 2028 quarter: this quarter" in page # 2,000 less 10 brokerage, 1,010 paid and 60 of indexation: 920. assert re.search(r'Indexation\s*−60\.00', page) @freeze_time("2028-08-15") def test_the_simulator_names_a_holding_it_cannot_value_at_30_june_2027(client, session_factory): page = _simulator(client, session_factory, close=None) assert "Totals exclude ALPHA — no close in the week before 1 July 2027" in page @freeze_time("2028-08-15") def test_the_simulator_names_a_holding_with_no_cpi_to_index_it_by(client, session_factory): """No CPI at all, with the feed blocked: there is nothing to stand in.""" page = _simulator(client, session_factory, cpi=False) assert "Totals exclude ALPHA — no CPI yet to index it by." in page assert "Indexation uses" not in page def _fy_api(client, session_factory, **kwargs) -> dict: from test_api import bearer, issue_key user_id, portfolio_id, _alpha = _signed_in(client, session_factory, **kwargs) key = issue_key(session_factory, portfolio_id, created_by=user_id) return client.get("/api/v1/fy/2029", headers=bearer(key)).json()["cgt"] @freeze_time("2029-01-15") def test_the_api_gives_the_gains_from_1_july_2027_and_the_minimum_tax_gain(client, session_factory): cgt = _fy_api(client, session_factory) assert (cgt["gains_discountable"], cgt["gains_from_july_2027"], cgt["minimum_tax_gain"], cgt["net_capital_gain"]) == (490, 430, 430, 675) assert (cgt["awaiting_cpi"], cgt["unvalued"]) == ([], []) @freeze_time("2029-01-15") @pytest.mark.parametrize("missing, named", [({"cpi": False}, "awaiting_cpi"), ({"close": None}, "unvalued")]) def test_the_api_names_a_sale_its_totals_leave_out(client, session_factory, missing, named): cgt = _fy_api(client, session_factory, **missing) assert (cgt[named], cgt["net_capital_gain"]) == (["ALPHA"], 0)