Initial extraction from monorepo
This commit is contained in:
commit
f7ef7ca4ab
56 changed files with 6163 additions and 0 deletions
77
fire_planner/ingest/payslip.py
Normal file
77
fire_planner/ingest/payslip.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
"""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),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue