Add imap-ingest CLI + ImapProvider: route emails to IE/Schwab parsers

Wires the IE + Schwab email parsers into an actual runnable sync. Walks
the IMAP mailbox, routes each message by sender domain:
  - *@investengine.com → invest_engine.parse_invest_engine_email
  - *@schwab.com       → schwab.parse_schwab_email
then pushes the resulting Activities through the shared pipeline.

broker-sync imap-ingest — new CLI command taking IMAP_HOST/USER/PASSWORD/
DIRECTORY (mirrors the old wealthfolio-sync image's env shape so the
Terraform CronJob's existing env wiring works unchanged).

Verified: poetry run pytest -q → 109 passed + 1 skipped; mypy strict
clean (37 files); ruff + yapf clean.
This commit is contained in:
Viktor Barzin 2026-04-17 22:12:05 +00:00
parent f089b8b93a
commit 6efd03570a
6 changed files with 290 additions and 35 deletions

View file

@ -48,7 +48,10 @@ def _login_ok(req: httpx.Request) -> httpx.Response:
assert body == {"password": "hunter2"}
return httpx.Response(
200,
json={"authenticated": True, "expiresIn": 604800},
json={
"authenticated": True,
"expiresIn": 604800
},
headers={"set-cookie": "wf_token=abc123; Path=/api; HttpOnly"},
)
@ -219,21 +222,25 @@ async def test_import_dry_run_then_real(tmp_path: Path) -> None:
calls.append(req.url.path)
if req.url.path == "/api/v1/activities/import/check":
# /import/check hydrates and returns a list of ActivityImport.
return httpx.Response(200, json=[
{
"symbol": "VUAG",
"isValid": True,
"errors": None,
"assetId": "enriched-asset-uuid",
"exchangeMic": "XLON",
},
])
return httpx.Response(200,
json=[
{
"symbol": "VUAG",
"isValid": True,
"errors": None,
"assetId": "enriched-asset-uuid",
"exchangeMic": "XLON",
},
])
if req.url.path == "/api/v1/activities/import":
return httpx.Response(
200,
json={
"activities": [
{"id": "wf-1", "external_id": "t212:1"},
{
"id": "wf-1",
"external_id": "t212:1"
},
],
},
)

View file

@ -86,18 +86,22 @@ async def test_pipeline_skips_dedup_then_imports_new(tmp_path: Path) -> None:
body = json.loads(req.content)
# Echo each activity back marked valid (mimic Wealthfolio's
# hydrate step).
return httpx.Response(200, json=[
{**a, "isValid": True, "errors": None} for a in body["activities"]
])
return httpx.Response(200,
json=[{
**a, "isValid": True,
"errors": None
} for a in body["activities"]])
if req.url.path == "/api/v1/activities/import":
body = req.content.decode()
posted_batches.append(body)
return httpx.Response(
200,
json={"activities": [
{"id": f"wf-{i}", "external_id": ext}
for i, ext in enumerate(["a", "b", "c"])
]},
json={
"activities": [{
"id": f"wf-{i}",
"external_id": ext
} for i, ext in enumerate(["a", "b", "c"])]
},
)
return httpx.Response(500)