import uuid from datetime import date from typing import Any from sqlalchemy import ( JSON, BigInteger, Boolean, CheckConstraint, Date, ForeignKey, ForeignKeyConstraint, Integer, LargeBinary, String, UniqueConstraint, Uuid, ) from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base, PkMixin, TimestampMixin DOCUMENT_STATUSES = ( "uploaded", "queued", "processing", "ready", "review_required", "confirmed", "failed", "deleted", ) RUN_STATUSES = ("pending", "running", "succeeded", "failed") CANDIDATE_KINDS = ("duplicate", "transfer") CANDIDATE_CONFIDENCES = ("unique", "possible") CANDIDATE_RESOLUTIONS = ("pending", "is_duplicate", "is_new") class StatementDocument(Base, PkMixin, TimestampMixin): __tablename__ = "statement_documents" __table_args__ = ( ForeignKeyConstraint( ["space_id", "account_id"], ["accounts.space_id", "accounts.id"], name="fk_documents_account_tenant", ), UniqueConstraint("space_id", "content_sha256", name="uq_documents_space_sha"), UniqueConstraint("space_id", "id", name="uq_documents_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) account_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True, index=True) original_filename: Mapped[str] = mapped_column(String(300), nullable=False) size_bytes: Mapped[int] = mapped_column(BigInteger, nullable=False) content_sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True) object_key: Mapped[str] = mapped_column(String(64), nullable=False, unique=True) dek_wrapped: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True) dek_wrap_nonce: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True) content_nonce: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True) adapter_id: Mapped[str | None] = mapped_column(String(48), nullable=True) account_fingerprint: Mapped[str | None] = mapped_column(String(128), nullable=True) status: Mapped[str] = mapped_column(String(20), nullable=False, default="uploaded", index=True) error_code: Mapped[str | None] = mapped_column(String(64), nullable=True) reconciliation: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) needs_account_mapping: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) active_parse_run_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True) uploaded_by_user_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) class ParseRun(Base, PkMixin, TimestampMixin): __tablename__ = "parse_runs" __table_args__ = ( ForeignKeyConstraint( ["space_id", "document_id"], ["statement_documents.space_id", "statement_documents.id"], ondelete="CASCADE", name="fk_parse_runs_document_tenant", ), UniqueConstraint("space_id", "id", name="uq_parse_runs_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) document_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True) adapter_id: Mapped[str | None] = mapped_column(String(48), nullable=True) parser_version: Mapped[str] = mapped_column(String(16), nullable=False, default="1") status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending") error_code: Mapped[str | None] = mapped_column(String(64), nullable=True) draft: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) class SourceRecord(Base, PkMixin, TimestampMixin): """Immutable extracted bank fact with document origin coordinates.""" __tablename__ = "source_records" __table_args__ = ( ForeignKeyConstraint( ["space_id", "document_id"], ["statement_documents.space_id", "statement_documents.id"], name="fk_source_records_document_tenant", ), ForeignKeyConstraint( ["space_id", "parse_run_id"], ["parse_runs.space_id", "parse_runs.id"], ondelete="CASCADE", name="fk_source_records_run_tenant", ), ForeignKeyConstraint( ["space_id", "duplicate_operation_id"], ["operations.space_id", "operations.id"], name="fk_source_records_duplicate_tenant", ), UniqueConstraint("space_id", "id", name="uq_source_records_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) document_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True, index=True) parse_run_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True) external_id: Mapped[str | None] = mapped_column(String(120), nullable=True) occurred_on: Mapped[date | None] = mapped_column(Date, nullable=True) posted_on: Mapped[date | None] = mapped_column(Date, nullable=True) amount_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False) direction: Mapped[str] = mapped_column(String(8), nullable=False) description: Mapped[str | None] = mapped_column(String(500), nullable=True) mcc: Mapped[int | None] = mapped_column(Integer, nullable=True) bank_category: Mapped[str | None] = mapped_column(String(100), nullable=True) location: Mapped[str | None] = mapped_column(String(200), nullable=True) row_ref: Mapped[str] = mapped_column(String(120), nullable=False) field_warnings: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) critical: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) record_fp: Mapped[str] = mapped_column(String(64), nullable=False, index=True) review_decision: Mapped[str] = mapped_column(String(16), nullable=False, default="create") duplicate_operation_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True) class SourceRecordLink(Base, PkMixin, TimestampMixin): __tablename__ = "source_record_links" __table_args__ = ( ForeignKeyConstraint( ["space_id", "source_record_id"], ["source_records.space_id", "source_records.id"], ondelete="CASCADE", name="fk_links_source_record_tenant", ), ForeignKeyConstraint( ["space_id", "operation_id"], ["operations.space_id", "operations.id"], name="fk_links_operation_tenant", ), UniqueConstraint("source_record_id", name="uq_links_source_record"), UniqueConstraint("space_id", "id", name="uq_links_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) source_record_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) operation_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True) kind: Mapped[str] = mapped_column(String(16), nullable=False) class DedupCandidate(Base, PkMixin, TimestampMixin): __tablename__ = "dedup_candidates" __table_args__ = ( CheckConstraint("candidate_kind IN ('duplicate', 'transfer')", name="kind_valid"), CheckConstraint( "resolution IN ('pending', 'is_duplicate', 'is_new', 'accepted_transfer')", name="resolution_valid", ), ForeignKeyConstraint( ["space_id", "source_record_id"], ["source_records.space_id", "source_records.id"], ondelete="CASCADE", name="fk_candidates_source_tenant", ), ForeignKeyConstraint( ["space_id", "existing_operation_id"], ["operations.space_id", "operations.id"], name="fk_candidates_operation_tenant", ), ForeignKeyConstraint( ["space_id", "existing_source_record_id"], ["source_records.space_id", "source_records.id"], ondelete="CASCADE", name="fk_candidates_existing_source_tenant", ), UniqueConstraint("space_id", "id", name="uq_candidates_space_id"), ) space_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True ) source_record_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True) candidate_kind: Mapped[str] = mapped_column(String(16), nullable=False, default="duplicate") existing_operation_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True, index=True) existing_source_record_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, nullable=True, index=True ) confidence: Mapped[str] = mapped_column(String(16), nullable=False, default="possible") resolution: Mapped[str] = mapped_column(String(16), nullable=False, default="pending") replacement_snapshot: Mapped[dict | None] = mapped_column(JSON, nullable=True) decided_by_user_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("users.id", ondelete="SET NULL"), nullable=True )