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: деньги пришли снаружи, баланс счёта их не видел, и для доходности это внешний поток, а не внутреннее движение.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user