CRUD портфелей (/portfolios), ручные события (POST /events, DELETE только для manual), PATCH /instruments/{id}, GET /analytics/overview — карточка на каждый скоуп одним запросом. Холдинги отдают logo_url и logo_color. openapi.json обновлён.
88 lines
3.5 KiB
Python
88 lines
3.5 KiB
Python
"""One card per scope for the home screen."""
|
|
|
|
from datetime import timedelta
|
|
from decimal import Decimal
|
|
|
|
from factories import make_account, make_event, make_instrument, make_price, refresh
|
|
from fintracker.analytics import today_local
|
|
from fintracker.models import AccountKind, AccountRole, EventKind
|
|
|
|
URL = "/api/v1/analytics/overview"
|
|
|
|
|
|
async def _broker(name: str) -> int:
|
|
return await make_account(
|
|
name=name, kind=AccountKind.broker, role=AccountRole.investment, balance=None
|
|
)
|
|
|
|
|
|
async def _invest(account: int, *, today_price: str) -> None:
|
|
"""10 000 ₽ in, 100 shares at 100; the price is flat until today, when it is `today_price`."""
|
|
t = today_local()
|
|
bought = t - timedelta(days=5)
|
|
sber = await make_instrument(ticker=f"T{account}")
|
|
await make_event(bought, account_id=account, kind=EventKind.deposit, amount=10000)
|
|
await make_event(
|
|
bought, account_id=account, kind=EventKind.buy, instrument_id=sber,
|
|
quantity=100, price=100, amount=-10000,
|
|
) # fmt: skip
|
|
d = bought
|
|
while d < t:
|
|
await make_price(d, instrument_id=sber, close="100")
|
|
d += timedelta(days=1)
|
|
await make_price(t, instrument_id=sber, close=today_price)
|
|
|
|
|
|
async def test_cards_carry_value_result_and_the_change_over_the_last_day(client, auth_headers):
|
|
account = await _broker("Брокер")
|
|
await _invest(account, today_price="105")
|
|
await refresh()
|
|
|
|
r = await client.get(URL, headers=auth_headers)
|
|
assert r.status_code == 200, r.text
|
|
cards = {c["scope"]: c for c in r.json()}
|
|
card = cards[f"account:{account}"]
|
|
assert card["name"] == "Брокер"
|
|
assert card["kind"] == "account"
|
|
assert Decimal(card["total_rub"]) == 10500
|
|
assert Decimal(card["invested_rub"]) == 10000
|
|
assert Decimal(card["pnl_rub"]) == 500
|
|
assert Decimal(card["pnl_pct"]) == Decimal("0.05")
|
|
assert Decimal(card["day_change_rub"]) == 500
|
|
assert Decimal(card["day_change_pct"]) == Decimal("500") / Decimal("10000")
|
|
assert Decimal(card["income_year_rub"]) == 0
|
|
|
|
|
|
async def test_all_comes_first_then_portfolios_then_accounts_by_value(client, auth_headers):
|
|
small, big = await _broker("Малый"), await _broker("Большой")
|
|
await _invest(small, today_price="100")
|
|
await _invest(big, today_price="130")
|
|
created = await client.post(
|
|
"/api/v1/portfolios", json={"name": "Мой", "account_ids": [small]}, headers=auth_headers
|
|
)
|
|
assert created.status_code == 201
|
|
await refresh()
|
|
|
|
cards = (await client.get(URL, headers=auth_headers)).json()
|
|
assert [c["kind"] for c in cards] == ["all", "portfolio", "account", "account"]
|
|
assert [c["name"] for c in cards[2:]] == ["Большой", "Малый"]
|
|
assert Decimal(cards[0]["total_rub"]) == Decimal(cards[2]["total_rub"]) + Decimal(
|
|
cards[3]["total_rub"]
|
|
)
|
|
|
|
|
|
async def test_an_account_switched_off_has_no_card(client, auth_headers):
|
|
kept, off = await _broker("Оставить"), await _broker("Отключить")
|
|
await _invest(kept, today_price="100")
|
|
await _invest(off, today_price="100")
|
|
await client.patch(f"/api/v1/accounts/{off}", json={"disabled": True}, headers=auth_headers)
|
|
await refresh()
|
|
|
|
names = [c["name"] for c in (await client.get(URL, headers=auth_headers)).json()]
|
|
assert "Оставить" in names
|
|
assert "Отключить" not in names
|
|
|
|
|
|
async def test_nothing_built_yet_is_an_empty_list(client, auth_headers):
|
|
assert (await client.get(URL, headers=auth_headers)).json() == []
|