77 lines
2.6 KiB
Python
77 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),
|
||
)
|