fire-planner/fire_planner/ingest/payslip.py
2026-05-07 17:06:19 +00:00

77 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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