Files
Dmitry 1adb1c16df feat(tinvest): источник T-Invest — операции, снапшоты и справочник
SDK t_tech.invest импортируется ровно в client.py: остальной код видит обычные
Decimal и dataclass'ы. Операции читаются по курсору GetOperationsByCursor,
снапшоты GetPortfolio/GetPositions складываются в position_snapshot и
cash_snapshot — только для сверки, аналитика их не читает.

mapper.py — явный словарь OperationType -> EventKind на все значения enum SDK,
покрытый тестом: новый тип должен ломать тест, а не молча уезжать в other.

Покупка с привязанной карты помечается meta.card_funded: деньги пришли снаружи,
баланс счёта их не видел, и для доходности это внешний поток, а не внутреннее
движение.
2026-09-18 13:44:31 +03:00

59 lines
2.2 KiB
Python

"""The mapping table must cover the SDK's enum exactly — no silent `other`."""
import pytest
from t_tech.invest import OperationType
from fintracker.models.ledger import EventKind
from fintracker.sources.tinvest.mapper import (
OPERATION_KINDS,
UnknownOperationType,
kind_for,
)
def test_all_operation_types_mapped():
"""A new SDK value fails here instead of quietly becoming `other` in the ledger."""
sdk = {m.name for m in OperationType}
assert sdk - set(OPERATION_KINDS) == set(), "не замаплены новые типы операций"
def test_no_stale_entries():
"""And a value dropped by the SDK does not linger in the table."""
sdk = {m.name for m in OperationType}
assert set(OPERATION_KINDS) - sdk == set(), "в таблице остались типы, которых нет в SDK"
def test_unknown_type_raises():
with pytest.raises(UnknownOperationType):
kind_for("OPERATION_TYPE_FROM_THE_FUTURE")
@pytest.mark.parametrize(
("op_type", "expected"),
[
("OPERATION_TYPE_BUY", EventKind.buy),
("OPERATION_TYPE_BUY_MARGIN", EventKind.buy),
("OPERATION_TYPE_SELL", EventKind.sell),
("OPERATION_TYPE_INPUT", EventKind.deposit),
("OPERATION_TYPE_OUTPUT", EventKind.withdrawal),
("OPERATION_TYPE_INPUT_SECURITIES", EventKind.transfer_in),
("OPERATION_TYPE_COUPON", EventKind.coupon),
("OPERATION_TYPE_DIVIDEND", EventKind.dividend),
("OPERATION_TYPE_BOND_REPAYMENT", EventKind.amortization),
("OPERATION_TYPE_BOND_REPAYMENT_FULL", EventKind.repayment),
("OPERATION_TYPE_BROKER_FEE", EventKind.commission),
("OPERATION_TYPE_TRACK_MFEE", EventKind.commission),
("OPERATION_TYPE_DIVIDEND_TAX", EventKind.tax),
],
)
def test_representative_mappings(op_type, expected):
assert kind_for(op_type) == expected
def test_tax_correction_follows_the_sign():
"""A positive correction is money returned, so it is a refund, not a tax."""
from decimal import Decimal
assert kind_for("OPERATION_TYPE_TAX_CORRECTION", Decimal("-10")) == EventKind.tax
assert kind_for("OPERATION_TYPE_TAX_CORRECTION", Decimal("10")) == EventKind.tax_refund