"""Schwab workplace-RSU email parser. Schwab sends HTML transaction-confirmation emails with the core fields in five `` elements: 1. Trade date (human format — e.g. "Jan 23, 2025") 2. Direction word ("Sold" for SELL; anything else is BUY) 3. Quantity (share count, float) 4. Ticker 5. Price ("$123.45" — currency-sign-prefixed) One email → one Activity. On any parse failure we return an empty list (same as the original finance/ behaviour — an unparseable email shouldn't crash the whole IMAP batch). Ported from finance/position/provider/schwab/message_parser.py (39 lines). Dropped: per-row timestamp id suffix (we use ISO date + ticker + qty which is stable across re-pulls), currency-from-sign hackery (US Schwab is USD- only in practice — if that ever changes we'll add FX on parse). """ from __future__ import annotations from decimal import Decimal, InvalidOperation from bs4 import BeautifulSoup from dateutil import parser as dateparser from broker_sync.models import AccountType, Activity, ActivityType _ACCOUNT_ID = "schwab-workplace" _DEFAULT_CURRENCY = "USD" def parse_schwab_email(raw_html: str) -> list[Activity]: """Return a single-item list of Activity on success, empty on failure.""" try: soup = BeautifulSoup(raw_html, "html.parser") cells = [ td.get_text(strip=True) for td in soup.find_all("td", { "class": "dark-background-body", "align": "right" }) ] if len(cells) < 5: return [] date_txt, direction_txt, qty_txt, ticker, price_txt = cells[:5] trade_date = dateparser.parse(date_txt) direction = (ActivityType.SELL if direction_txt.strip().lower() == "sold" else ActivityType.BUY) quantity = Decimal(qty_txt.replace(",", "").strip()) # Price like "$123.45" — strip the currency sign and parse the numeric tail. # Handle "£", "€", "USD", etc. by taking the last numeric span. price_clean = price_txt for sign in ("$", "£", "€", "USD", "GBP", "EUR"): price_clean = price_clean.replace(sign, "") unit_price = Decimal(price_clean.replace(",", "").strip()) external_id = (f"schwab:{trade_date.date().isoformat()}:{ticker}:" f"{direction.value}:{quantity}") return [ Activity( external_id=external_id, account_id=_ACCOUNT_ID, account_type=AccountType.GIA, date=trade_date, activity_type=direction, symbol=ticker.strip(), quantity=quantity, unit_price=unit_price, currency=_DEFAULT_CURRENCY, notes=f"schwab-email:{direction_txt}", ) ] except (ValueError, InvalidOperation, IndexError, AttributeError): return []