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: деньги пришли снаружи, баланс счёта их не видел, и для доходности это внешний поток, а не внутреннее движение.
59 lines
2.2 KiB
Python
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
|