MyMidas/backend/app/db/models/pension.py
megaproxy 1a2c8efd01 Add pensions module and integrate with tax report
Adds a full pensions feature: SIPP/workplace DC/LISA account metadata,
contribution recording with relief-at-source/net-pay/salary-sacrifice
gross calculations, state pension tracker, annual allowance monitor,
and LISA summary. Pension contributions feed into the tax report
(RAS gross totals, allowance used). Includes two Alembic migrations,
backend service/schema/API, and full frontend pensions page with
cards for allowance, state pension, LISA, and retirement projection.

Also fixes CSRF cookie secure flag (must be false for HTTP deployments)
and extends tax schemas/service to expose pension data in the report.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:59:01 +00:00

59 lines
3.9 KiB
Python

import uuid
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, LargeBinary, Numeric, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class PensionMetadata(Base):
__tablename__ = "pension_metadata"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
account_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
pension_type: Mapped[str] = mapped_column(String(20), nullable=False) # workplace_dc|workplace_db|sipp|lisa
provider_name_enc: Mapped[bytes | None] = mapped_column("provider_name", LargeBinary, nullable=True)
scheme_name_enc: Mapped[bytes | None] = mapped_column("scheme_name", LargeBinary, nullable=True)
member_reference_enc: Mapped[bytes | None] = mapped_column("member_reference", LargeBinary, nullable=True)
dob: Mapped[date | None] = mapped_column(Date, nullable=True)
target_retirement_age: Mapped[int | None] = mapped_column(Integer, nullable=True)
assumed_growth_rate: Mapped[Decimal | None] = mapped_column(Numeric(5, 4), nullable=True) # e.g. 0.0500
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
account: Mapped["Account"] = relationship(lazy="noload") # type: ignore[name-defined]
contributions: Mapped[list["PensionContribution"]] = relationship(back_populates="pension", lazy="noload", cascade="all, delete-orphan")
class PensionContribution(Base):
__tablename__ = "pension_contributions"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
pension_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pension_metadata.id", ondelete="CASCADE"), nullable=False, index=True)
contribution_date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
tax_year: Mapped[int] = mapped_column(Integer, nullable=False, index=True) # year ending 5 Apr, e.g. 2026
member_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False)
employer_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=0)
relief_type: Mapped[str] = mapped_column(String(20), nullable=False) # relief_at_source|net_pay|salary_sacrifice|none
gross_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False)
relief_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=0)
notes_enc: Mapped[bytes | None] = mapped_column("notes", LargeBinary, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
pension: Mapped["PensionMetadata"] = relationship(back_populates="contributions", lazy="noload")
class StatePensionRecord(Base):
__tablename__ = "state_pension_records"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
qualifying_years: Mapped[int] = mapped_column(Integer, nullable=False)
checked_date: Mapped[date | None] = mapped_column(Date, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)