from __future__ import annotations from datetime import datetime from decimal import Decimal from pathlib import Path from broker_sync.models import AccountType, ActivityType from broker_sync.providers.parsers.invest_engine import parse_invest_engine_email _FIXTURES = Path(__file__).parent.parent.parent / "fixtures" / "invest_engine" def _load(name: str) -> bytes: return (_FIXTURES / name).read_bytes() # -- RFC 2822 body (v2-style, single BUY) -- def test_rfc2822_single_buy_parses_to_one_activity() -> None: activities = parse_invest_engine_email(_load("rfc2822_v2_single_buy.eml")) assert len(activities) == 1 a = activities[0] assert a.activity_type is ActivityType.BUY assert a.symbol == "VUAG" assert a.quantity == Decimal("59.539562") assert a.unit_price == Decimal("60.46") assert a.currency == "GBP" assert a.date == datetime(2023, 1, 17) assert a.account_id == "invest-engine-primary" assert a.account_type is AccountType.ISA def test_rfc2822_external_id_is_deterministic() -> None: a1 = parse_invest_engine_email(_load("rfc2822_v2_single_buy.eml"))[0] a2 = parse_invest_engine_email(_load("rfc2822_v2_single_buy.eml"))[0] assert a1.external_id == a2.external_id assert a1.external_id.startswith("invest-engine:") def test_rfc2822_notes_record_parse_strategy() -> None: a = parse_invest_engine_email(_load("rfc2822_v2_single_buy.eml"))[0] assert a.notes is not None assert "rfc2822" in a.notes # -- HTML table body (multipart/alternative, two orders) -- def test_html_body_parses_both_orders() -> None: activities = parse_invest_engine_email(_load("html_two_orders.eml")) assert len(activities) == 2 a, b = activities assert a.symbol == "VUAG" assert a.quantity == Decimal("10.5") assert a.unit_price == Decimal("62.10") assert a.date == datetime(2026, 4, 1) assert a.account_id == "invest-engine-primary" assert a.account_type is AccountType.ISA assert a.activity_type is ActivityType.BUY assert b.symbol == "SWDA" assert b.quantity == Decimal("2.25") assert b.unit_price == Decimal("85.40") assert b.date == datetime(2026, 4, 1) def test_html_notes_record_html_strategy() -> None: a = parse_invest_engine_email(_load("html_two_orders.eml"))[0] assert a.notes is not None assert "html" in a.notes # -- CSV attachment body -- def test_csv_attachment_parses_all_rows() -> None: activities = parse_invest_engine_email(_load("csv_attachment.eml")) assert len(activities) == 3 by_symbol = {a.symbol: a for a in activities} assert by_symbol["VUAG"].quantity == Decimal("12.5") assert by_symbol["VUAG"].unit_price == Decimal("63.21") assert by_symbol["VUAG"].date == datetime(2025, 4, 2) assert by_symbol["SWDA"].quantity == Decimal("4.75") assert by_symbol["VUSA"].date == datetime(2025, 4, 4) for a in activities: assert a.activity_type is ActivityType.BUY assert a.currency == "GBP" assert a.account_id == "invest-engine-primary" assert a.account_type is AccountType.ISA assert a.notes is not None assert "csv" in a.notes # -- graceful failure modes -- def test_unparseable_email_returns_empty_list() -> None: assert parse_invest_engine_email(_load("unparseable.eml")) == [] def test_html_partial_match_returns_only_parseable_orders() -> None: activities = parse_invest_engine_email(_load("html_partial_match.eml")) assert len(activities) == 1 a = activities[0] assert a.symbol == "VUAG" assert a.quantity == Decimal("3.0") assert a.unit_price == Decimal("61.25") assert a.date == datetime(2026, 4, 15)