DRRK/tests/unit/test_manual_operations.py

220 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Ручные операции (D21, D22): проверки по типам и запись на временном SQLite."""
from __future__ import annotations
from datetime import date
from decimal import Decimal
from pathlib import Path
import pytest
from sqlalchemy import select
from app.db.base import run_db
from app.db.models import Currency, Operation, Security
from app.imports.quik import parse_quik_xlsx
from app.services import quik_import
from app.services.manual_operations import (
ManualOperationError,
ManualOperationInput,
add_manual_operation,
list_manual_operations,
load_form_options,
parse_decimal,
validate,
)
USER = "analyst@rshb.ru"
ISIN = "RU000A10EVH2"
def _input(**changes) -> ManualOperationInput: # noqa: ANN003
values = {
"operation_code": "CPN",
"portfolio_code": "SALES",
"isin": ISIN,
"trade_date": date(2026, 4, 15),
"amount": Decimal("125000.50"),
"quantity": None,
"comment": "",
}
values.update(changes)
return ManualOperationInput(**values)
@pytest.fixture
def security(session_local) -> None:
async def _add() -> None:
async with session_local() as session:
rub = (
await session.execute(select(Currency.id).where(Currency.code == "RUB"))
).scalar_one()
session.add(
Security(
isin=ISIN,
secid=ISIN,
name="ЕврХол3P06",
currency_id=rub,
created_by=USER,
updated_by=USER,
)
)
await session.commit()
run_db(_add())
# --- разбор и проверки --------------------------------------------------------
@pytest.mark.parametrize(
("text", "expected"),
[
("125 000,50", Decimal("125000.50")),
("-3", Decimal("-3")),
("0.1", Decimal("0.1")),
("", None),
(" ", None),
],
)
def test_parse_decimal(text: str, expected: Decimal | None) -> None:
assert parse_decimal(text, "Сумма") == expected
@pytest.mark.parametrize("text", ["abc", "1,2,3", "nan", "inf"])
def test_parse_decimal_rejects_garbage(text: str) -> None:
with pytest.raises(ManualOperationError, match="«Сумма»: не число"):
parse_decimal(text, "Сумма")
@pytest.mark.parametrize(
("changes", "error"),
[
({"amount": None}, "Укажите сумму."),
({"amount": Decimal("0")}, "Сумма должна быть больше нуля."),
({"quantity": Decimal("-1")}, "Количество должно быть больше нуля."),
(
{"operation_code": "NOM", "amount": Decimal("-5")},
"Сумма должна быть больше нуля.",
),
(
{"operation_code": "UWFEE"},
"Комиссия за андеррайтинг вводится только в портфеле UW.",
),
(
{
"operation_code": "UWFEE",
"portfolio_code": "UW",
"quantity": Decimal("10"),
},
"У комиссии за андеррайтинг нет количества.",
),
(
{"operation_code": "ADJ", "amount": Decimal("-100")},
"У корректировки обязателен комментарий: что и почему корректируется.",
),
(
{"operation_code": "ADJ", "amount": None, "comment": "сверка"},
"Укажите сумму и/или количество (со знаком).",
),
({"operation_code": "BUY"}, "Тип операции BUY вручную не вводится."),
],
)
def test_validate_rejects(changes: dict, error: str) -> None:
assert error in validate(_input(**changes))
@pytest.mark.parametrize(
"changes",
[
{},
{"quantity": Decimal("1000")},
{"operation_code": "NOM", "amount": Decimal("250000")},
{"operation_code": "UWFEE", "portfolio_code": "UW"},
{"operation_code": "ADJ", "amount": Decimal("-100"), "comment": "сверка"},
{
"operation_code": "ADJ",
"amount": None,
"quantity": Decimal("-5"),
"comment": "сверка остатка",
},
],
)
def test_validate_accepts(changes: dict) -> None:
assert validate(_input(**changes)) == []
# --- БД --------------------------------------------------------------------
def test_add_and_list_manual_operations(session_local, security: None) -> None:
coupon_id = run_db(add_manual_operation(_input(comment=" купон №3 "), USER))
run_db(
add_manual_operation(
_input(
operation_code="UWFEE",
portfolio_code="UW",
amount=Decimal("1500000"),
currency_code="CNY",
),
USER,
)
)
rows = run_db(list_manual_operations())
assert [(r.operation, r.portfolio, r.currency) for r in rows] == [
("Комиссия за андеррайтинг", "UW", "CNY"),
("Купон", "SALES", "RUB"), # по умолчанию — валюта бумаги
]
assert rows[1].amount == Decimal("125000.50")
assert rows[1].comment == "купон №3"
assert rows[1].created_by == USER
async def _stored() -> Operation:
async with session_local() as session:
return await session.get(Operation, coupon_id)
stored = run_db(_stored())
assert stored.import_batch_id is None and stored.source_row is None
assert stored.trade_date == date(2026, 4, 15)
def test_add_refuses_invalid_input(session_local, security: None) -> None:
with pytest.raises(ManualOperationError, match="только в портфеле UW"):
run_db(add_manual_operation(_input(operation_code="UWFEE"), USER))
with pytest.raises(ManualOperationError, match="нет в справочнике"):
run_db(add_manual_operation(_input(isin="RU000A000000"), USER))
with pytest.raises(ManualOperationError, match="Валюты XXX нет"):
run_db(add_manual_operation(_input(currency_code="XXX"), USER))
assert run_db(list_manual_operations()) == []
def test_quik_operations_are_not_listed_as_manual(
session_local, security: None, fixtures_dir: Path
) -> None:
parsed = parse_quik_xlsx(
(fixtures_dir / "quik_sample_300326_anon.xlsx").read_bytes()
)
run_db(
quik_import.save_import(
parsed,
file_name="сделки.xlsx",
portfolio_code="SALES",
decisions={},
expected_rows=[1, 2, 3, 4, 5, 6],
user=USER,
)
)
run_db(add_manual_operation(_input(), USER))
assert [r.operation for r in run_db(list_manual_operations())] == ["Купон"]
def test_form_options(session_local, security: None) -> None:
options = run_db(load_form_options())
assert options.portfolios == {"UW": "Андеррайтинг", "SALES": "Продажи"}
assert [(s.isin, s.currency) for s in options.securities] == [(ISIN, "RUB")]
assert options.currencies == ["CNY", "RUB", "USD"]