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>
This commit is contained in:
parent
b30e8e577b
commit
1a2c8efd01
30 changed files with 3537 additions and 8 deletions
59
backend/app/db/models/pension.py
Normal file
59
backend/app/db/models/pension.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue