[broker-sync] unsuspend IMAP + Panel 15 RSU vest reconciliation (Phase D)

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
This commit is contained in:
Viktor Barzin 2026-04-19 18:29:01 +00:00
parent 1c0e1bcdde
commit a8280e77b6
2 changed files with 152 additions and 2 deletions

View file

@ -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 {

View file

@ -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",