"""Recording today's scheduled buy without retyping it. The trade form fills itself in from the DCA Schedule. The design note that shaped it: **a form that silently supplies a quantity is asking to be signed off without being read.** The number can be wrong — a stale close is the obvious way — so the form shows the arithmetic that produced it. `prefill()` returns None rather than a partial answer whenever any input is missing. A half-filled form looks like the app knows something it does not. """ from __future__ import annotations import datetime as dt import re from decimal import Decimal import pytest from freezegun import freeze_time from sqlalchemy import select import factories as fac from app import plans from test_routes import bind_to_only_portfolio, make_login HTML = {"accept": "text/html"} TODAY = "2026-08-08" def _planned(session_factory, *, amount="500", brokerage="9.50", price="142.50", currency="AUD"): with session_factory() as s: bind_to_only_portfolio(s) inst = fac.make_instrument(s, "ALPHA", currency=currency) if price is not None: fac.add_price(s, inst, "2026-08-07", price) from app.models import InvestmentPlan plan = InvestmentPlan(name="Regular buys", interval_days=28, amount=Decimal(amount), brokerage=Decimal(brokerage), start_date=dt.date(2026, 8, 1), active=True) s.add(plan) s.flush() plans.set_entries(s, plan, [inst.id]) s.commit() return inst.id @freeze_time(TODAY) def test_it_buys_whole_units_only(client, session_factory): """You cannot buy part of a share. $500 − $9.50 = $490.50, and at $142.50 that is 3 units — not 3.44.""" make_login(client, session_factory) _planned(session_factory) with session_factory() as s: bind_to_only_portfolio(s) got = plans.prefill(s) assert got is not None assert got.units == 3 @freeze_time(TODAY) def test_the_remainder_is_reported_rather_than_hidden(client, session_factory): """$490.50 − 3 × $142.50 = $63.00. The same idea as a DRP residual, and worded the same way — money that did not get invested.""" make_login(client, session_factory) _planned(session_factory) with session_factory() as s: bind_to_only_portfolio(s) got = plans.prefill(s) assert got.leftover == Decimal("63.00") @freeze_time(TODAY) def test_brokerage_comes_off_before_the_division(client, session_factory): """Buying $500 worth and paying brokerage on top spends more than $500, which is not what "about $500 each" means.""" make_login(client, session_factory) _planned(session_factory, amount="500", brokerage="0") with session_factory() as s: bind_to_only_portfolio(s) assert plans.prefill(s).units == 3 # 500 / 142.50 = 3.50 @freeze_time(TODAY) @pytest.mark.parametrize("kwargs, why", [ ({"price": None}, "no stored price to divide by"), ({"amount": "0"}, "no amount set on the plan"), ({"amount": "100", "brokerage": "0"}, "not enough for even one unit"), ({"price": "0"}, "a close of nothing to divide by"), ({"amount": "10", "brokerage": "10"}, "brokerage swallows the whole buy"), ]) def test_it_declines_rather_than_guessing(client, session_factory, kwargs, why): """None, not a partial fill. A form half-filled from missing data looks like the app knows something it does not.""" make_login(client, session_factory) _planned(session_factory, **kwargs) with session_factory() as s: bind_to_only_portfolio(s) assert plans.prefill(s) is None, f"should have declined: {why}" @freeze_time(TODAY) def test_exactly_enough_for_one_unit_buys_one(client, session_factory): make_login(client, session_factory) _planned(session_factory, amount="142.50", brokerage="0") with session_factory() as s: bind_to_only_portfolio(s) assert plans.prefill(s).units == 1 @freeze_time(TODAY) def test_the_price_is_this_instruments_latest_close(client, session_factory): """Not an older close of it, and not a later one of something else.""" make_login(client, session_factory) _planned(session_factory, brokerage="0", currency="USD") # 142.50 on 7 Aug with session_factory() as s: bind_to_only_portfolio(s) alpha = s.scalars(select(fac.Instrument).where(fac.Instrument.ticker == "ALPHA")).one() fac.add_price(s, alpha, "2026-08-03", "100.00") # older fac.add_price(s, fac.make_instrument(s, "OTHER"), "2026-08-08", "1.00") s.commit() with session_factory() as s: bind_to_only_portfolio(s) got = plans.prefill(s) assert (got.unit_price, got.price_date, got.units, got.currency) == ( Decimal("142.50"), dt.date(2026, 8, 7), 3, "USD") @freeze_time(TODAY) def test_with_no_plan_there_is_nothing_to_prefill(client, session_factory): make_login(client, session_factory) with session_factory() as s: bind_to_only_portfolio(s) assert plans.prefill(s) is None # --------------------------------------------------------------------------- # # On the form # --------------------------------------------------------------------------- # @freeze_time(TODAY) def test_the_form_shows_the_arithmetic_not_just_the_answer(client, session_factory): """The whole point. Somebody who can see how 3 was reached will notice a stale price; somebody shown only "3" will not.""" make_login(client, session_factory) _planned(session_factory) page = client.get("/trade/new", headers=HTML).text assert "From your DCA Schedule" in page assert "$500.00" in page # the plan's amount assert "$9.50" in page # brokerage, taken off first assert "$142.50" in page # the close it divided by assert "2026-08-07" in page # …and WHEN that close was assert "$427.50" in page # 3 x $142.50 — what this buy costs @freeze_time(TODAY) def test_the_fields_are_filled_in(client, session_factory): make_login(client, session_factory) _planned(session_factory) page = client.get("/trade/new", headers=HTML).text assert re.search(r'name="quantity"[^>]*value="3"', page) # Normalised: the stored close is Numeric(18,6) and would otherwise fill # the box with "142.500000". assert re.search(r'name="unit_price"[^>]*value="142\.5"', page) @freeze_time(TODAY) def test_the_date_is_today_not_the_due_date(client, session_factory): """Back-dating a trade quietly would be worse than not helping — the due date is when it was scheduled, not when you bought it.""" make_login(client, session_factory) _planned(session_factory) page = client.get("/trade/new", headers=HTML).text assert re.search(r'name="trade_date"[^>]*value="2026-08-08"', page) @freeze_time(TODAY) def test_nothing_is_prefilled_when_the_schedule_is_switched_off(client, session_factory): """An install that turned the feature off should not have its trade form quietly filled in from a schedule it does not have.""" from app.main import settings make_login(client, session_factory) _planned(session_factory) settings.features.dca_schedule = False page = client.get("/trade/new", headers=HTML).text assert "From your DCA Schedule" not in page