extractor: track rsu_vest + rsu_offset separately from cash pay

UK payslips for equity-comp employees report RSU vests as notional pay
for HMRC only. A paired same-magnitude deduction (Shares Retained /
Stock Tax Withholding / RSU Offset) nets it back out of cash. The UK
payslip's income_tax line shows tax on the grossed-up total, but the
actual RSU tax is handled by Schwab (US broker) via share sale. No
cash flows through UK payroll for RSU.

Previously the extractor folded RSU notional into gross_pay and
income_tax, which inflated the dashboard numbers — a payslip with
£25k RSU vest looked like 2x salary with 80% tax rate.

Changes:
- schema: add rsu_vest + rsu_offset fields (default 0).
- db + alembic 0002: add two new NUMERIC(12,2) columns with server
  default 0 (backward-compatible; existing rows get 0).
- validate_totals: include rsu_offset in deductions sum so the
  gross + rsu_vest inflation is properly netted out.
- extraction prompt: explicit rules for identifying RSU lines by the
  common Meta/Sage/Workday labels, and to NOT put them in
  other_deductions.

Dashboards in a follow-up commit: cash_gross = gross_pay - rsu_vest,
effective tax rate based on cash metrics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-04-18 23:37:25 +00:00
parent 86cac65572
commit 9105b6b79d
6 changed files with 72 additions and 3 deletions

View file

@ -30,6 +30,8 @@ EXTRACTION_PROMPT = (
' "pension_employee": number,\n'
' "pension_employer": number,\n'
' "student_loan": number,\n'
' "rsu_vest": number,\n'
' "rsu_offset": number,\n'
' "other_deductions": {"label": number, ...},\n'
' "net_pay": number\n'
"}\n"
@ -37,8 +39,19 @@ EXTRACTION_PROMPT = (
"Rules:\n"
"- Report numbers as the payslip shows them; do not compute sums.\n"
"- Unknown numeric fields → 0, not null.\n"
"- `rsu_vest`: any notional/reporting entry in the EARNINGS block labelled "
'"RSU Vest", "Restricted Stock Units", "Stock Value", "Notional Pay", '
'"Share Award", "Equity Vest", "GSU Vest". For Meta UK payslips this is '
"the grossed-up RSU value reported for HMRC only; Schwab handles actual "
"tax withholding via share sale.\n"
"- `rsu_offset`: the matching DEDUCTION that nets the RSU out of cash pay — "
'labels vary: "Shares Retained", "Stock Tax Withholding", "RSU Offset", '
'"Notional Pay Offset", "Shares Withheld". For Meta this is typically equal '
"in magnitude to rsu_vest so cash net is unaffected.\n"
"- If either rsu_vest or rsu_offset is present, BOTH should be populated; "
"do NOT put them in `other_deductions`.\n"
"- `other_deductions` covers cycle-to-work, share-save, benefits-in-kind, court orders, "
"anything not in the main fields.\n"
"anything not in the main fields (and NOT RSU — those have dedicated fields).\n"
"- All money in GBP unless the payslip is denominated otherwise.\n"
'- If a field\'s value is ambiguous, pick the value from the "this period" column, not YTD.')