d582f15681
- Implemented tests for import polling functionality, ensuring correct behavior for active and terminal import statuses. - Created tests for the login page, validating user interactions and error handling. - Added unit tests for money utility functions, covering parsing, formatting, and summation of monetary values. - Developed tests for query keys, ensuring proper scoping and invalidation roots. chore: setup testing environment and configuration - Added setup file for testing with Vitest and Testing Library. - Configured TypeScript and Vite for the frontend project. - Created Docker and Nginx configurations for local development and production environments. - Established a PostgreSQL initialization script for setting up application roles. docs: add README and environment configuration for infra - Included README for the infra directory, detailing local development setup and service descriptions. - Created an example environment configuration file for local development with placeholders for sensitive information.
182 lines
7.9 KiB
Python
182 lines
7.9 KiB
Python
import uuid
|
|
from datetime import date
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
Boolean,
|
|
CheckConstraint,
|
|
Date,
|
|
ForeignKey,
|
|
ForeignKeyConstraint,
|
|
Integer,
|
|
String,
|
|
UniqueConstraint,
|
|
Uuid,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.db.base import Base, PkMixin, TimestampMixin
|
|
|
|
ACCOUNT_KINDS = ("cash", "debit", "credit")
|
|
ACCOUNT_STATUSES = ("active", "archived")
|
|
OPERATION_TYPES = ("expense", "income", "transfer", "refund")
|
|
OPERATION_STATUSES = ("confirmed", "voided")
|
|
|
|
|
|
class Account(Base, PkMixin, TimestampMixin):
|
|
"""Financial account. Balance is always derived: opening + confirmed postings."""
|
|
|
|
__tablename__ = "accounts"
|
|
__table_args__ = (
|
|
CheckConstraint("kind IN ('cash', 'debit', 'credit')", name="kind_valid"),
|
|
CheckConstraint("status IN ('active', 'archived')", name="status_valid"),
|
|
CheckConstraint("currency = 'RUB'", name="currency_rub_only"),
|
|
UniqueConstraint("space_id", "id", name="uq_accounts_space_id"),
|
|
)
|
|
|
|
space_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
|
kind: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="RUB")
|
|
opening_amount_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0)
|
|
opened_on: Mapped[date] = mapped_column(Date, nullable=False)
|
|
bank_code: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
masked_identifier: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
fingerprint: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
|
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
|
|
__mapper_args__ = {"version_id_col": version}
|
|
|
|
|
|
class Operation(Base, PkMixin, TimestampMixin):
|
|
"""User financial fact. amount_kopecks is always positive; direction lives in postings."""
|
|
|
|
__tablename__ = "operations"
|
|
__table_args__ = (
|
|
CheckConstraint("type IN ('expense', 'income', 'transfer', 'refund')", name="type_valid"),
|
|
CheckConstraint("status IN ('confirmed', 'voided')", name="status_valid"),
|
|
CheckConstraint("amount_kopecks > 0", name="amount_positive"),
|
|
CheckConstraint(
|
|
"NOT (type = 'transfer' AND counter_account_id IS NULL)",
|
|
name="transfer_requires_counter",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "account_id"],
|
|
["accounts.space_id", "accounts.id"],
|
|
name="fk_operations_account_tenant",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "counter_account_id"],
|
|
["accounts.space_id", "accounts.id"],
|
|
name="fk_operations_counter_account_tenant",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "category_id"],
|
|
["categories.space_id", "categories.id"],
|
|
name="fk_operations_category_tenant",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "income_source_id"],
|
|
["income_sources.space_id", "income_sources.id"],
|
|
name="fk_operations_income_source_tenant",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "payee_id"],
|
|
["payees.space_id", "payees.id"],
|
|
name="fk_operations_payee_tenant",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "parent_operation_id"],
|
|
["operations.space_id", "operations.id"],
|
|
name="fk_operations_parent_tenant",
|
|
),
|
|
UniqueConstraint("space_id", "request_key", name="uq_operations_space_request_key"),
|
|
UniqueConstraint("space_id", "id", name="uq_operations_space_id"),
|
|
)
|
|
|
|
space_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
type: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
status: Mapped[str] = mapped_column(String(16), nullable=False, default="confirmed")
|
|
amount_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
account_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True)
|
|
counter_account_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True)
|
|
category_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True, index=True)
|
|
income_source_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True)
|
|
payee_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True)
|
|
parent_operation_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True)
|
|
occurred_on: Mapped[date] = mapped_column(Date, nullable=False, index=True)
|
|
posted_on: Mapped[date | None] = mapped_column(Date, nullable=True)
|
|
description: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
void_reason: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
request_key: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
|
created_by_user_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
Uuid, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
|
|
__mapper_args__ = {"version_id_col": version}
|
|
|
|
|
|
class Posting(Base, PkMixin, TimestampMixin):
|
|
"""Signed balance change of one account within one operation.
|
|
|
|
Sign convention (uniform for asset and credit accounts, from the account's
|
|
perspective): income and refunds post positive; expenses post negative;
|
|
transfers post negative on the source and positive on the destination.
|
|
Credit accounts normally carry a non-positive balance; debt = -balance.
|
|
"""
|
|
|
|
__tablename__ = "postings"
|
|
__table_args__ = (
|
|
CheckConstraint("amount_kopecks <> 0", name="amount_nonzero"),
|
|
ForeignKeyConstraint(
|
|
["space_id", "operation_id"],
|
|
["operations.space_id", "operations.id"],
|
|
ondelete="CASCADE",
|
|
name="fk_postings_operation_tenant",
|
|
),
|
|
ForeignKeyConstraint(
|
|
["space_id", "account_id"],
|
|
["accounts.space_id", "accounts.id"],
|
|
name="fk_postings_account_tenant",
|
|
),
|
|
UniqueConstraint("space_id", "id", name="uq_postings_space_id"),
|
|
)
|
|
|
|
space_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
operation_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True)
|
|
account_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False, index=True)
|
|
amount_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
|
|
|
|
class Category(Base, PkMixin, TimestampMixin):
|
|
"""Budget category. Kept in ledger module to avoid circular imports."""
|
|
|
|
__tablename__ = "categories"
|
|
__table_args__ = (
|
|
CheckConstraint("kind IN ('monthly', 'savings_envelope')", name="kind_valid"),
|
|
ForeignKeyConstraint(
|
|
["space_id", "linked_account_id"],
|
|
["accounts.space_id", "accounts.id"],
|
|
name="fk_categories_linked_account_tenant",
|
|
),
|
|
UniqueConstraint("space_id", "id", name="uq_categories_space_id"),
|
|
)
|
|
|
|
space_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid, ForeignKey("financial_spaces.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
kind: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
system_key: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
name_override: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
|
custom_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
|
linked_account_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True)
|
|
archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|