MyMidas/backend/app/db/models/tax.py
megaproxy afb5e99bb2 Add recurring transaction detection, subscriptions page, and UK tax reporting
- Recurring service: auto-detects direct debits/subscriptions from CSV imports
  using frequency analysis; manual toggle in transaction detail drawer
- Subscriptions page (/subscriptions): groups recurring payments with monthly
  cost equivalents, next-payment badges, and re-scan trigger
- UK Tax page (/tax): payslips/P60 entry, income tax + NI + CGT + dividend tax
  calculations, configurable rate tables per tax year (pre-seeded 2024/25 and
  2025/26), editable in-app so Budget changes need no rebuild
- Migration 0006: tax_rate_configs, tax_profiles, payslips, manual_cgt_disposals
  with RLS; seeds 2025/2026 rate configs for existing users
- Chart tooltip fix: all Recharts tooltips now use TOOLTIP_STYLE constant so
  they render correctly across all dark/light themes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 21:40:02 +00:00

74 lines
4.2 KiB
Python

import uuid
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, LargeBinary, Numeric, SmallInteger, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class TaxRateConfig(Base):
__tablename__ = "tax_rate_configs"
__table_args__ = (
UniqueConstraint("user_id", "tax_year", "rate_type", name="uq_tax_rate_configs_user_year_type"),
)
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)
tax_year: Mapped[int] = mapped_column(Integer, nullable=False)
rate_type: Mapped[str] = mapped_column(String(30), nullable=False) # income_tax|ni|cgt|dividend
config: Mapped[dict] = mapped_column(JSONB, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class TaxProfile(Base):
__tablename__ = "tax_profiles"
__table_args__ = (
UniqueConstraint("user_id", "tax_year", name="uq_tax_profiles_user_year"),
)
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)
tax_year: Mapped[int] = mapped_column(Integer, nullable=False)
employer_name_enc: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
tax_code: Mapped[str] = mapped_column(String(20), nullable=False, default="1257L")
is_cumulative: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
payslips: Mapped[list["Payslip"]] = relationship(back_populates="tax_profile", lazy="noload")
class Payslip(Base):
__tablename__ = "payslips"
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)
tax_profile_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tax_profiles.id", ondelete="CASCADE"), nullable=False, index=True)
period_month: Mapped[int | None] = mapped_column(SmallInteger, nullable=True)
period_year: Mapped[int] = mapped_column(SmallInteger, nullable=False)
gross_pay: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False)
income_tax_withheld: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False)
ni_withheld: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False)
net_pay: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False)
is_p60: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
notes_enc: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
tax_profile: Mapped["TaxProfile"] = relationship(back_populates="payslips", lazy="noload")
class ManualCGTDisposal(Base):
__tablename__ = "manual_cgt_disposals"
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)
tax_year: Mapped[int] = mapped_column(Integer, nullable=False)
disposal_date: Mapped[date] = mapped_column(Date, nullable=False)
asset_description_enc: Mapped[bytes] = mapped_column(LargeBinary, nullable=False)
proceeds: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False)
cost_basis: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False)
notes_enc: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)