62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
|
|
"""Add rsu_vest_events for Schwab vest ground-truth reconciliation.
|
||
|
|
|
||
|
|
Schwab emails a "Release Confirmation" on each RSU vest, listing the vest
|
||
|
|
date, shares released at FMV, shares sold to cover tax, and the USD
|
||
|
|
withholding amount. broker-sync will parse these emails and populate
|
||
|
|
this table; Panel 15 of the dashboard reconciles
|
||
|
|
(payslip.rsu_vest, payslip.rsu_income_tax) ↔
|
||
|
|
(rsu_vest_events.gross_value_gbp, rsu_vest_events.tax_withheld_gbp)
|
||
|
|
to validate the parser's RSU-split correctness.
|
||
|
|
|
||
|
|
Idempotent on `external_id` — re-running the IMAP sync doesn't create
|
||
|
|
duplicates. USD-denominated raw values are retained alongside the
|
||
|
|
GBP-converted values for audit.
|
||
|
|
"""
|
||
|
|
import sqlalchemy as sa
|
||
|
|
|
||
|
|
from alembic import op
|
||
|
|
|
||
|
|
revision = "0008"
|
||
|
|
down_revision = "0007"
|
||
|
|
branch_labels = None
|
||
|
|
depends_on = None
|
||
|
|
|
||
|
|
SCHEMA = "payslip_ingest"
|
||
|
|
|
||
|
|
|
||
|
|
def upgrade() -> None:
|
||
|
|
op.create_table(
|
||
|
|
"rsu_vest_events",
|
||
|
|
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
||
|
|
sa.Column("external_id", sa.String(), nullable=False, unique=True),
|
||
|
|
sa.Column("vest_date", sa.Date(), nullable=False),
|
||
|
|
sa.Column("ticker", sa.String(), nullable=False),
|
||
|
|
sa.Column("shares_vested", sa.Numeric(14, 4), nullable=False),
|
||
|
|
sa.Column("shares_sold_to_cover", sa.Numeric(14, 4), nullable=True),
|
||
|
|
sa.Column("fmv_at_vest_usd", sa.Numeric(12, 4), nullable=False),
|
||
|
|
sa.Column("tax_withheld_usd", sa.Numeric(12, 2), nullable=True),
|
||
|
|
sa.Column("fx_rate_gbp", sa.Numeric(10, 6), nullable=True),
|
||
|
|
sa.Column("gross_value_gbp", sa.Numeric(12, 2), nullable=True),
|
||
|
|
sa.Column("tax_withheld_gbp", sa.Numeric(12, 2), nullable=True),
|
||
|
|
sa.Column("source", sa.String(length=32), nullable=False),
|
||
|
|
sa.Column("raw_extraction", sa.JSON(), nullable=True),
|
||
|
|
sa.Column("created_at",
|
||
|
|
sa.TIMESTAMP(timezone=True),
|
||
|
|
server_default=sa.text("now()"),
|
||
|
|
nullable=False),
|
||
|
|
schema=SCHEMA,
|
||
|
|
)
|
||
|
|
op.create_index(
|
||
|
|
"ix_rsu_vest_events_vest_date",
|
||
|
|
"rsu_vest_events",
|
||
|
|
["vest_date"],
|
||
|
|
schema=SCHEMA,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def downgrade() -> None:
|
||
|
|
op.drop_index("ix_rsu_vest_events_vest_date",
|
||
|
|
table_name="rsu_vest_events",
|
||
|
|
schema=SCHEMA)
|
||
|
|
op.drop_table("rsu_vest_events", schema=SCHEMA)
|