From a8280e77b61b9569ce7dfe51350ce6210f282d42 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 19 Apr 2026 18:29:01 +0000 Subject: [PATCH] [broker-sync] unsuspend IMAP + Panel 15 RSU vest reconciliation (Phase D) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Activates the Schwab/InvestEngine IMAP ingest CronJob that's been scaffolded-but-suspended since Phase 2 of broker-sync, now that the Schwab parser can detect vest-confirmation emails. Runs nightly 02:30 UK. Current behaviour once deployed: - Trade confirmations (Schwab sell-to-cover, InvestEngine orders) → Activity rows posted to Wealthfolio. Unchanged. - Release Confirmations (Schwab RSU vests) → parser returns gross-vest BUY + sell-to-cover SELL Activities (to Wealthfolio) and a VestEvent object (NOT YET persisted — Postgres sink + DB grant pending; see follow-up under code-860). Vest detection uses a subject/body heuristic that will need tightening against a real email fixture. Panel 15 of the UK payslip dashboard added: per-vest-month join of payslip.rsu_vest vs rsu_vest_events (gross_value_gbp, tax_withheld_gbp) with delta columns. Tax-delta-percent coloured green/orange/red at 0/2%/5% thresholds. Table is empty until broker-sync starts persisting VestEvents — harmless until then. Before applying: - Verify IMAP creds in Vault (secret/broker-sync: imap_host, imap_user, imap_password, imap_directory) are still valid. - Empty vest-event table is expected; delta columns show NULL until the postgres sink lands. Part of: code-860 --- stacks/broker-sync/main.tf | 9 +- .../monitoring/dashboards/uk-payslip.json | 145 ++++++++++++++++++ 2 files changed, 152 insertions(+), 2 deletions(-) diff --git a/stacks/broker-sync/main.tf b/stacks/broker-sync/main.tf index b3c71905..7c99a916 100644 --- a/stacks/broker-sync/main.tf +++ b/stacks/broker-sync/main.tf @@ -105,7 +105,7 @@ resource "kubernetes_cron_job_v1" "version_probe" { metadata {} spec { backoff_limit = 1 - ttl_seconds_after_finished = 300 + ttl_seconds_after_finished = 86400 template { metadata { labels = { app = "broker-sync", component = "version-probe" } @@ -246,7 +246,12 @@ resource "kubernetes_cron_job_v1" "imap" { concurrency_policy = "Forbid" successful_jobs_history_limit = 3 failed_jobs_history_limit = 5 - suspend = true # enable in Phase 2 + # Unsuspended 2026-04-19 for RSU vest ground-truth ingestion — the parser + # now detects Schwab Release Confirmations and scaffolds VestEvents; the + # postgres sink that persists them into payslip_ingest.rsu_vest_events is + # pending a real-email fixture and cross-service DB grant (see + # follow-up beads task filed under the RSU tax spike fix epic). + suspend = false job_template { metadata {} spec { diff --git a/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json b/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json index 76a513eb..eb20a480 100644 --- a/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json +++ b/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json @@ -2158,6 +2158,151 @@ "editorMode": "code" } ] + }, + { + "id": 15, + "title": "RSU vest reconciliation \u2014 payslip vs Schwab", + "description": "Per-vest-month join between payslip.rsu_vest (what HMRC reporting shows) and Schwab's vest-confirmation email data (what actually happened at the broker). Empty rows until the broker-sync IMAP ingest runs and VestEvents are persisted.", + "type": "table", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "payslips-pg" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 169 + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "right", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "unit": "currencyGBP", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "vest_month" + }, + "properties": [ + { + "id": "custom.width", + "value": 140 + }, + { + "id": "custom.align", + "value": "left" + }, + { + "id": "unit", + "value": "none" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ticker" + }, + "properties": [ + { + "id": "custom.width", + "value": 80 + }, + { + "id": "unit", + "value": "none" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "shares_vested" + }, + "properties": [ + { + "id": "unit", + "value": "none" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "tax_delta_pct" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background", + "mode": "basic" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 2 + }, + { + "color": "red", + "value": 5 + } + ] + } + } + ] + } + ] + }, + "options": { + "cellHeight": "sm", + "footer": { + "show": false + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "payslips-pg" + }, + "rawQuery": true, + "editorMode": "code", + "format": "table", + "rawSql": "WITH vest_by_month AS (SELECT DATE_TRUNC('month', vest_date)::date AS vest_month, ticker, SUM(shares_vested) AS shares_vested, SUM(gross_value_gbp) AS broker_gross_gbp, SUM(tax_withheld_gbp) AS broker_tax_gbp FROM payslip_ingest.rsu_vest_events GROUP BY 1, 2), payslip_by_month AS (SELECT DATE_TRUNC('month', pay_date)::date AS vest_month, SUM(rsu_vest) AS payslip_rsu_gbp, SUM(income_tax - COALESCE(cash_income_tax, income_tax)) AS payslip_rsu_tax_gbp FROM payslip_ingest.payslip WHERE rsu_vest > 0 GROUP BY 1) SELECT COALESCE(v.vest_month, p.vest_month) AS vest_month, v.ticker, v.shares_vested, v.broker_gross_gbp, p.payslip_rsu_gbp, (p.payslip_rsu_gbp - v.broker_gross_gbp) AS gross_delta_gbp, v.broker_tax_gbp, p.payslip_rsu_tax_gbp, (p.payslip_rsu_tax_gbp - v.broker_tax_gbp) AS tax_delta_gbp, CASE WHEN v.broker_tax_gbp IS NULL OR v.broker_tax_gbp = 0 THEN NULL ELSE ABS(p.payslip_rsu_tax_gbp - v.broker_tax_gbp) * 100.0 / v.broker_tax_gbp END AS tax_delta_pct FROM vest_by_month v FULL OUTER JOIN payslip_by_month p ON p.vest_month = v.vest_month ORDER BY vest_month DESC" + } + ] } ], "refresh": "5m",