import uuid from datetime import date, datetime from typing import Any from sqlalchemy import ( JSON, BigInteger, CheckConstraint, Date, ForeignKey, ForeignKeyConstraint, Integer, String, UniqueConstraint, Uuid, ) from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base, PkMixin, TimestampMixin, UTCDateTime PERIOD_STATUSES = ("open", "closed") FUNDING_KINDS = ("fund", "unfund") class BudgetPeriod(Base, PkMixin, TimestampMixin): """Calendar month budget period in the space timezone (month granularity).""" __tablename__ = "budget_periods" __table_args__ = ( CheckConstraint("status IN ('open', 'closed')", name="status_valid"), CheckConstraint("month BETWEEN 1 AND 12", name="month_valid"), UniqueConstraint("space_id", "year", "month", name="uq_budget_periods_space_ym"), UniqueConstraint("space_id", "id", name="uq_budget_periods_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) year: Mapped[int] = mapped_column(nullable=False) month: Mapped[int] = mapped_column(nullable=False) status: Mapped[str] = mapped_column(String(8), nullable=False, default="open") version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) __mapper_args__ = {"version_id_col": version} class PlanAssignment(Base, PkMixin, TimestampMixin): __tablename__ = "plan_assignments" __table_args__ = ( CheckConstraint("planned_kopecks >= 0", name="planned_nonnegative"), ForeignKeyConstraint( ["space_id", "period_id"], ["budget_periods.space_id", "budget_periods.id"], ondelete="CASCADE", name="fk_plan_assignments_period_tenant", ), ForeignKeyConstraint( ["space_id", "category_id"], ["categories.space_id", "categories.id"], name="fk_plan_assignments_category_tenant", ), UniqueConstraint("period_id", "category_id", name="uq_plan_assignments_period_category"), UniqueConstraint("space_id", "id", name="uq_plan_assignments_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) period_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True) category_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) planned_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) __mapper_args__ = {"version_id_col": version} class ExpectedIncome(Base, PkMixin, TimestampMixin): __tablename__ = "expected_incomes" __table_args__ = ( CheckConstraint("amount_kopecks > 0", name="amount_positive"), ForeignKeyConstraint( ["space_id", "period_id"], ["budget_periods.space_id", "budget_periods.id"], ondelete="CASCADE", name="fk_expected_incomes_period_tenant", ), UniqueConstraint("space_id", "id", name="uq_expected_incomes_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) period_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True) expected_on: Mapped[date] = mapped_column(Date, nullable=False) amount_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False) source_name: Mapped[str | None] = mapped_column(String(200), nullable=True) note: Mapped[str | None] = mapped_column(String(500), nullable=True) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) __mapper_args__ = {"version_id_col": version} class ExpectedIncomeMatch(Base, PkMixin, TimestampMixin): """Links one actual income operation to one expected income (1:1 per operation).""" __tablename__ = "expected_income_matches" __table_args__ = ( CheckConstraint("amount_kopecks > 0", name="amount_positive"), ForeignKeyConstraint( ["space_id", "expected_income_id"], ["expected_incomes.space_id", "expected_incomes.id"], ondelete="CASCADE", name="fk_income_matches_expected_tenant", ), ForeignKeyConstraint( ["space_id", "operation_id"], ["operations.space_id", "operations.id"], name="fk_income_matches_operation_tenant", ), UniqueConstraint("operation_id", name="uq_income_matches_operation"), UniqueConstraint("space_id", "id", name="uq_income_matches_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) expected_income_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True) operation_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) amount_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False) class FundingMovement(Base, PkMixin, TimestampMixin): """Manual money movement between the general pool and a category/reserve.""" __tablename__ = "funding_movements" __table_args__ = ( CheckConstraint("kind IN ('fund', 'unfund')", name="kind_valid"), CheckConstraint("amount_kopecks > 0", name="amount_positive"), ForeignKeyConstraint( ["space_id", "period_id"], ["budget_periods.space_id", "budget_periods.id"], ondelete="CASCADE", name="fk_funding_movements_period_tenant", ), ForeignKeyConstraint( ["space_id", "category_id"], ["categories.space_id", "categories.id"], name="fk_funding_movements_category_tenant", ), UniqueConstraint("space_id", "id", name="uq_funding_movements_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) period_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True) category_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) kind: Mapped[str] = mapped_column(String(8), nullable=False) amount_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False) note: Mapped[str | None] = mapped_column(String(500), nullable=True) created_by_user_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) class PeriodClosure(Base, PkMixin, TimestampMixin): """Immutable-at-rest snapshot written when a period closes.""" __tablename__ = "period_closures" __table_args__ = ( ForeignKeyConstraint( ["space_id", "period_id"], ["budget_periods.space_id", "budget_periods.id"], ondelete="CASCADE", name="fk_period_closures_period_tenant", ), UniqueConstraint("space_id", "id", name="uq_period_closures_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) period_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) snapshot: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False) closed_at: Mapped[datetime] = mapped_column(UTCDateTime(), nullable=False) closed_by_user_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("users.id", ondelete="SET NULL"), nullable=True )