Context ------- New connector suite that syncs UK brokerage activity (Trading212, InvestEngine, Schwab email-parsed, CSV drop-folder) into Wealthfolio. Lives outside finance/ intentionally — finance/ is untouched per the plan at ~/.claude/plans/let-s-work-on-linking-temporal-valiant.md. This change ----------- - Poetry project with httpx, typer, bs4, dev tools (pytest, mypy strict, ruff, yapf). - Canonical Activity + Account models with the 6 UK tax wrappers (ISA/SIPP/GIA/LISA/JISA/WORKPLACE_PENSION) and the 12 Wealthfolio activity types from docs/activities/activity-types.md on the upstream. - Validation invariants: BUY/SELL need qty+price, DIVIDEND/DEPOSIT/etc need amount — raises early so providers can't silently emit broken rows. - to_wealthfolio_csv_row() shape matches Wealthfolio's CSV import; primary sink path per the plan. Test plan --------- ## Automated - poetry run pytest -q → 7 passed in 0.03s - poetry run mypy broker_sync tests → Success: no issues found in 4 source files - poetry run ruff check . → All checks passed! - poetry run yapf --diff --recursive broker_sync tests → no diff ## Manual Verification Not applicable — pure data model, no runtime behaviour. Closes: code-thw.1
126 lines
3.5 KiB
Python
126 lines
3.5 KiB
Python
from datetime import UTC, datetime
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
|
|
from broker_sync.models import (
|
|
Account,
|
|
AccountType,
|
|
Activity,
|
|
ActivityType,
|
|
FxRateSource,
|
|
)
|
|
|
|
|
|
def test_account_type_covers_uk_wrappers() -> None:
|
|
assert {
|
|
AccountType.ISA,
|
|
AccountType.SIPP,
|
|
AccountType.GIA,
|
|
AccountType.LISA,
|
|
AccountType.JISA,
|
|
AccountType.WORKPLACE_PENSION,
|
|
} <= set(AccountType)
|
|
|
|
|
|
def test_activity_type_covers_wealthfolio_types() -> None:
|
|
# Mirrors Wealthfolio docs/activities/activity-types.md
|
|
for name in (
|
|
"BUY",
|
|
"SELL",
|
|
"DIVIDEND",
|
|
"INTEREST",
|
|
"DEPOSIT",
|
|
"WITHDRAWAL",
|
|
"TRANSFER_IN",
|
|
"TRANSFER_OUT",
|
|
"CONVERSION_IN",
|
|
"CONVERSION_OUT",
|
|
"FEE",
|
|
"TAX",
|
|
):
|
|
assert hasattr(ActivityType, name), f"missing {name}"
|
|
|
|
|
|
def test_activity_minimal_construct() -> None:
|
|
a = Activity(
|
|
external_id="t212:order:1",
|
|
account_id="t212-isa",
|
|
account_type=AccountType.ISA,
|
|
date=datetime(2026, 1, 15, tzinfo=UTC),
|
|
activity_type=ActivityType.BUY,
|
|
symbol="VUAG",
|
|
quantity=Decimal("10"),
|
|
unit_price=Decimal("85.5"),
|
|
currency="GBP",
|
|
)
|
|
assert a.external_id == "t212:order:1"
|
|
assert a.fee == Decimal("0")
|
|
assert a.fx_rate_source is None
|
|
|
|
|
|
def test_buy_requires_qty_and_price() -> None:
|
|
with pytest.raises(ValueError, match="BUY.*quantity"):
|
|
Activity(
|
|
external_id="x",
|
|
account_id="a",
|
|
account_type=AccountType.GIA,
|
|
date=datetime(2026, 1, 1, tzinfo=UTC),
|
|
activity_type=ActivityType.BUY,
|
|
symbol="VUAG",
|
|
currency="GBP",
|
|
)
|
|
|
|
|
|
def test_dividend_requires_amount() -> None:
|
|
with pytest.raises(ValueError, match="DIVIDEND.*amount"):
|
|
Activity(
|
|
external_id="x",
|
|
account_id="a",
|
|
account_type=AccountType.GIA,
|
|
date=datetime(2026, 1, 1, tzinfo=UTC),
|
|
activity_type=ActivityType.DIVIDEND,
|
|
currency="GBP",
|
|
)
|
|
|
|
|
|
def test_wealthfolio_csv_row_shape() -> None:
|
|
a = Activity(
|
|
external_id="ie:2026-04:0001",
|
|
account_id="ie-isa",
|
|
account_type=AccountType.ISA,
|
|
date=datetime(2026, 4, 1, 10, 30, tzinfo=UTC),
|
|
activity_type=ActivityType.BUY,
|
|
symbol="VWRP",
|
|
quantity=Decimal("2.5"),
|
|
unit_price=Decimal("110.00"),
|
|
currency="GBP",
|
|
fee=Decimal("0.50"),
|
|
amount_gbp=Decimal("275.50"),
|
|
fx_rate_gbp=Decimal("1.0"),
|
|
fx_rate_source=FxRateSource.ECB_LIVE,
|
|
notes="sync:invest-engine:ie:2026-04:0001",
|
|
)
|
|
row = a.to_wealthfolio_csv_row()
|
|
# Wealthfolio activity import columns (from docs):
|
|
# date, activity_type, symbol, quantity, unit_price, currency, fee, amount, notes, ...
|
|
assert row["date"] == "2026-04-01T10:30:00+00:00"
|
|
assert row["activity_type"] == "BUY"
|
|
assert row["symbol"] == "VWRP"
|
|
assert row["quantity"] == "2.5"
|
|
assert row["unit_price"] == "110.00"
|
|
assert row["currency"] == "GBP"
|
|
assert row["fee"] == "0.50"
|
|
assert row["notes"] == "sync:invest-engine:ie:2026-04:0001"
|
|
|
|
|
|
def test_account_construct() -> None:
|
|
acc = Account(
|
|
id="t212-isa",
|
|
name="Trading212 ISA",
|
|
account_type=AccountType.ISA,
|
|
currency="GBP",
|
|
provider="trading212",
|
|
)
|
|
assert acc.id == "t212-isa"
|
|
assert acc.account_type is AccountType.ISA
|