Ctrl K
ComunidadGuías…

tests/test_cgt_2027.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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("<h2>Positions closed this year", 1)[1]
    row = re.search(r"<tr>\s*<td>2028-08-01</td>.*?</tr>", 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'<td>Indexation</td>\s*<td class="num">−<span class="m">60\.00</span>',
                     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)