78 lines
2.6 KiB
Python
78 lines
2.6 KiB
Python
|
|
"""Read the deployed payslip-ingest schema for income + RSU vest cadence.
|
|||
|
|
|
|||
|
|
Read-only: we never write to `payslip_ingest.*`. The DB role
|
|||
|
|
`pg-fire-planner` only needs SELECT on payslip_ingest.payslip and
|
|||
|
|
payslip_ingest.rsu_vest_events.
|
|||
|
|
|
|||
|
|
Outputs feed scenario calibration:
|
|||
|
|
- savings_per_year_gbp: median monthly net_pay × 12 less the £100k
|
|||
|
|
baseline spend (the planner allocates the surplus to portfolio).
|
|||
|
|
- annual_rsu_gross_gbp: median annual RSU vest value, used to validate
|
|||
|
|
the savings rate against expected gross compensation.
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from dataclasses import dataclass
|
|||
|
|
from datetime import date
|
|||
|
|
from decimal import Decimal
|
|||
|
|
|
|||
|
|
from sqlalchemy import text
|
|||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass(frozen=True)
|
|||
|
|
class IncomeSummary:
|
|||
|
|
median_monthly_net_pay_gbp: Decimal
|
|||
|
|
median_annual_rsu_gbp: Decimal
|
|||
|
|
earliest_date: date | None
|
|||
|
|
latest_date: date | None
|
|||
|
|
payslip_count: int
|
|||
|
|
rsu_count: int
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def read_income_summary(session: AsyncSession, months: int = 24) -> IncomeSummary:
|
|||
|
|
"""Aggregate the most-recent `months` of payslips + RSU vests."""
|
|||
|
|
payslip_rows = (await session.execute(
|
|||
|
|
text(
|
|||
|
|
"""
|
|||
|
|
SELECT pay_date, net_pay
|
|||
|
|
FROM payslip_ingest.payslip
|
|||
|
|
WHERE pay_date >= CURRENT_DATE - (:months || ' months')::interval
|
|||
|
|
ORDER BY pay_date DESC
|
|||
|
|
""", ),
|
|||
|
|
{"months": months},
|
|||
|
|
)).all()
|
|||
|
|
|
|||
|
|
rsu_rows = (await session.execute(
|
|||
|
|
text(
|
|||
|
|
"""
|
|||
|
|
SELECT vest_date, gross_value_gbp
|
|||
|
|
FROM payslip_ingest.rsu_vest_events
|
|||
|
|
WHERE vest_date >= CURRENT_DATE - (:months || ' months')::interval
|
|||
|
|
ORDER BY vest_date DESC
|
|||
|
|
""", ),
|
|||
|
|
{"months": months},
|
|||
|
|
)).all()
|
|||
|
|
|
|||
|
|
monthly_nets = sorted(Decimal(str(r[1] or 0)) for r in payslip_rows)
|
|||
|
|
median_monthly_net = (monthly_nets[len(monthly_nets) // 2] if monthly_nets else Decimal("0"))
|
|||
|
|
|
|||
|
|
rsu_total_gbp = sum((Decimal(str(r[1] or 0)) for r in rsu_rows), start=Decimal("0"))
|
|||
|
|
months_span = max(1, months)
|
|||
|
|
annual_rsu = rsu_total_gbp * 12 / months_span
|
|||
|
|
|
|||
|
|
pay_dates = [r[0] for r in payslip_rows]
|
|||
|
|
rsu_dates = [r[0] for r in rsu_rows]
|
|||
|
|
all_dates = pay_dates + rsu_dates
|
|||
|
|
earliest = min(all_dates) if all_dates else None
|
|||
|
|
latest = max(all_dates) if all_dates else None
|
|||
|
|
|
|||
|
|
return IncomeSummary(
|
|||
|
|
median_monthly_net_pay_gbp=median_monthly_net,
|
|||
|
|
median_annual_rsu_gbp=annual_rsu,
|
|||
|
|
earliest_date=earliest,
|
|||
|
|
latest_date=latest,
|
|||
|
|
payslip_count=len(payslip_rows),
|
|||
|
|
rsu_count=len(rsu_rows),
|
|||
|
|
)
|