MyMidas/backend/app/schemas/tax.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

227 lines
5.9 KiB
Python

import uuid
from datetime import date as DateType, datetime
from decimal import Decimal
from typing import Any
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# Tax rate config
# ---------------------------------------------------------------------------
class TaxRateConfigUpdate(BaseModel):
"""PUT /tax/rate-configs/{tax_year} — pass only the rate types you want to upsert."""
income_tax: dict[str, Any] | None = None
ni: dict[str, Any] | None = None
cgt: dict[str, Any] | None = None
dividend: dict[str, Any] | None = None
class TaxRateConfigResponse(BaseModel):
tax_year: int
rates: dict[str, Any]
updated_at: str
# ---------------------------------------------------------------------------
# Tax profile
# ---------------------------------------------------------------------------
class TaxProfileCreate(BaseModel):
tax_code: str = Field(default="1257L", min_length=1, max_length=20)
employer_name: str | None = Field(default=None, max_length=200)
is_cumulative: bool = True
class TaxProfileResponse(BaseModel):
id: uuid.UUID
tax_year: int
tax_code: str
employer_name: str | None
is_cumulative: bool
created_at: str
updated_at: str
# ---------------------------------------------------------------------------
# Payslips
# ---------------------------------------------------------------------------
class PayslipCreate(BaseModel):
period_month: int | None = Field(default=None, ge=1, le=12)
period_year: int = Field(..., ge=2000, le=2100)
gross_pay: Decimal = Field(..., ge=0)
income_tax_withheld: Decimal = Field(..., ge=0)
ni_withheld: Decimal = Field(..., ge=0)
net_pay: Decimal = Field(..., ge=0)
notes: str | None = None
class PayslipUpdate(BaseModel):
period_month: int | None = Field(default=None, ge=1, le=12)
period_year: int | None = Field(default=None, ge=2000, le=2100)
gross_pay: Decimal | None = Field(default=None, ge=0)
income_tax_withheld: Decimal | None = Field(default=None, ge=0)
ni_withheld: Decimal | None = Field(default=None, ge=0)
net_pay: Decimal | None = Field(default=None, ge=0)
notes: str | None = None
class PayslipResponse(BaseModel):
id: uuid.UUID
tax_profile_id: uuid.UUID
period_month: int | None
period_year: int
gross_pay: str
income_tax_withheld: str
ni_withheld: str
net_pay: str
is_p60: bool
notes: str | None
created_at: str
class P60Entry(BaseModel):
gross_pay: Decimal = Field(..., ge=0)
income_tax_withheld: Decimal = Field(..., ge=0)
ni_withheld: Decimal = Field(..., ge=0)
net_pay: Decimal = Field(..., ge=0)
# ---------------------------------------------------------------------------
# Manual CGT disposals
# ---------------------------------------------------------------------------
class ManualDisposalCreate(BaseModel):
disposal_date: DateType
asset_description: str = Field(..., min_length=1, max_length=500)
proceeds: Decimal = Field(..., ge=0)
cost_basis: Decimal = Field(..., ge=0)
notes: str | None = None
class ManualDisposalUpdate(BaseModel):
disposal_date: DateType | None = None
asset_description: str | None = Field(default=None, min_length=1, max_length=500)
proceeds: Decimal | None = Field(default=None, ge=0)
cost_basis: Decimal | None = Field(default=None, ge=0)
notes: str | None = None
class ManualDisposalResponse(BaseModel):
id: uuid.UUID
tax_year: int
disposal_date: str
asset_description: str
proceeds: str
cost_basis: str
gain_loss: str
notes: str | None
created_at: str
# ---------------------------------------------------------------------------
# Tax report (nested)
# ---------------------------------------------------------------------------
class BandBreakdownItem(BaseModel):
rate: float
taxable: float
tax: float
from_: int | None = Field(default=None, alias="from")
to: int | None = None
model_config = {"populate_by_name": True}
class IncomeTaxSummary(BaseModel):
personal_allowance: str
taxable_income: str
liability: str
band_breakdown: list[dict[str, Any]]
withheld: str
owed: str
class NISummary(BaseModel):
liability: str
band_breakdown: list[dict[str, Any]]
withheld: str
owed: str
class InvestmentDisposalItem(BaseModel):
date: str
asset: str
symbol: str
quantity: str
proceeds: str
cost_basis: str
fees: str
gain_loss: str
class CGTSummary(BaseModel):
gross_gain: str
exempt: str
taxable_gain: str
liability: str
band_breakdown: list[dict[str, Any]]
investment_disposals: list[dict[str, Any]]
manual_disposals: list[dict[str, Any]]
total_gain: str
class DividendTransactionItem(BaseModel):
date: str
asset: str
symbol: str
amount: str
class DividendSummary(BaseModel):
gross_dividends: str
allowance: str
taxable_dividends: str
liability: str
band_breakdown: list[dict[str, Any]]
dividend_transactions: list[dict[str, Any]]
class TaxReportSummary(BaseModel):
total_liability: str
total_withheld: str
net_owed: str
overpaid: bool
class IncomeSummary(BaseModel):
gross_income: str
income_tax_withheld: str
ni_withheld: str
payslips: list[dict[str, Any]]
class PensionTaxSummary(BaseModel):
net_pay_total: str
salary_sacrifice_total: str
ras_gross_total: str
higher_rate_claimable: str
additional_rate_claimable: str
annual_allowance_used: str
annual_allowance_remaining: str
standard_allowance: str
class TaxReportResponse(BaseModel):
tax_year: int
tax_year_display: str
profile: dict[str, Any] | None
income: IncomeSummary
income_tax: IncomeTaxSummary
ni: NISummary
cgt: CGTSummary
dividends: DividendSummary
pensions: PensionTaxSummary | None
summary: TaxReportSummary