MyMidas/backend/alembic/versions/0007_add_pension_tables.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

84 lines
4.1 KiB
Python

"""add pension tables
Revision ID: 0007
Revises: 0006
Create Date: 2026-04-27
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "0007"
down_revision = "0006"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ------------------------------------------------------------------
# pension_metadata — one row per pension account
# ------------------------------------------------------------------
op.create_table(
"pension_metadata",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
sa.Column("account_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False),
sa.Column("pension_type", sa.String(20), nullable=False),
sa.Column("provider_name", sa.LargeBinary, nullable=True),
sa.Column("scheme_name", sa.LargeBinary, nullable=True),
sa.Column("member_reference", sa.LargeBinary, nullable=True),
sa.Column("dob", sa.Date, nullable=True),
sa.Column("target_retirement_age", sa.Integer, nullable=True),
sa.Column("assumed_growth_rate", sa.Numeric(5, 4), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_unique_constraint("uq_pension_metadata_account_id", "pension_metadata", ["account_id"])
op.create_index("ix_pension_metadata_user_id", "pension_metadata", ["user_id"])
op.create_index("ix_pension_metadata_account_id", "pension_metadata", ["account_id"])
# ------------------------------------------------------------------
# pension_contributions — contribution history per pension
# ------------------------------------------------------------------
op.create_table(
"pension_contributions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
sa.Column("pension_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("pension_metadata.id", ondelete="CASCADE"), nullable=False),
sa.Column("contribution_date", sa.Date, nullable=False),
sa.Column("tax_year", sa.Integer, nullable=False),
sa.Column("member_amount", sa.Numeric(14, 2), nullable=False),
sa.Column("employer_amount", sa.Numeric(14, 2), nullable=False, server_default="0"),
sa.Column("relief_type", sa.String(20), nullable=False),
sa.Column("gross_amount", sa.Numeric(14, 2), nullable=False),
sa.Column("relief_amount", sa.Numeric(14, 2), nullable=False, server_default="0"),
sa.Column("notes", sa.LargeBinary, nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_pension_contributions_user_id", "pension_contributions", ["user_id"])
op.create_index("ix_pension_contributions_pension_id", "pension_contributions", ["pension_id"])
op.create_index("ix_pension_contributions_date", "pension_contributions", ["contribution_date"])
op.create_index("ix_pension_contributions_tax_year", "pension_contributions", ["tax_year"])
# ------------------------------------------------------------------
# RLS
# ------------------------------------------------------------------
for table in ["pension_metadata", "pension_contributions"]:
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY")
op.execute(f"""
CREATE POLICY {table}_user_isolation ON {table}
USING (user_id = current_app_user_id())
""")
def downgrade() -> None:
for table in ["pension_metadata", "pension_contributions"]:
op.execute(f"DROP POLICY IF EXISTS {table}_user_isolation ON {table}")
op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY")
op.drop_table("pension_contributions")
op.drop_table("pension_metadata")