63d065011b
- Updated timestamps and AST hashes in manifest.json for various backend and frontend files to reflect recent changes. - Added new files related to MCC categories and optional hold reasons in the manifest. - Modified the README in the infra directory to correct the linting tool from 'mypy' to 'ty' for type checking in the backend project.
266 lines
12 KiB
Python
266 lines
12 KiB
Python
import uuid
|
|
from datetime import date, datetime
|
|
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, UTCDateTime, utcnow
|
|
|
|
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")
|
|
BANK_STATUSES = ("posted", "hold")
|
|
HOLD_RESOLUTIONS = ("matched_posted", "confirmed_as_posted", "discarded_as_cancelled")
|
|
HOLD_MANUAL_RESOLUTIONS = ("confirmed_as_posted", "discarded_as_cancelled")
|
|
|
|
|
|
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__ = (
|
|
CheckConstraint("bank_status IN ('posted', 'hold')", name="bank_status_valid"),
|
|
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)
|
|
bank_status: Mapped[str] = mapped_column(String(8), nullable=False, default="posted")
|
|
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
|
|
)
|
|
|
|
|
|
class HoldResolution(Base, PkMixin, TimestampMixin):
|
|
"""Append-only, tenant-scoped settlement of one HOLD source record.
|
|
|
|
The extracted HOLD row itself is never mutated. At most one resolution
|
|
exists per hold source record; automatic matches (`matched_posted`) are
|
|
created only when a later statement is confirmed, and manual actions
|
|
(`confirmed_as_posted`, `discarded_as_cancelled`) may carry a reason.
|
|
"""
|
|
|
|
__tablename__ = "hold_resolutions"
|
|
__table_args__ = (
|
|
CheckConstraint(
|
|
"resolution IN ('matched_posted', 'confirmed_as_posted', 'discarded_as_cancelled')",
|
|
name="resolution_valid",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "hold_source_record_id"],
|
|
["source_records.space_id", "source_records.id"],
|
|
ondelete="CASCADE",
|
|
name="fk_hold_resolutions_hold_tenant",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "resolving_source_record_id"],
|
|
["source_records.space_id", "source_records.id"],
|
|
name="fk_hold_resolutions_resolving_tenant",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "operation_id"],
|
|
["operations.space_id", "operations.id"],
|
|
name="fk_hold_resolutions_operation_tenant",
|
|
),
|
|
UniqueConstraint("hold_source_record_id", name="uq_hold_resolutions_hold_record"),
|
|
UniqueConstraint("space_id", "id", name="uq_hold_resolutions_space_id"),
|
|
)
|
|
|
|
space_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
hold_source_record_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True)
|
|
resolution: Mapped[str] = mapped_column(String(24), nullable=False)
|
|
resolving_source_record_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True)
|
|
operation_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True, index=True)
|
|
resolved_by_user_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
Uuid, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
reason: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
resolved_at: Mapped[datetime] = mapped_column(UTCDateTime(), nullable=False, default=utcnow)
|