- 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>
74 lines
4.2 KiB
Python
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)
|