feat: add SQLAlchemy database models and tests
Add the data layer for the spending analysis app including models for household members, accounts, categories, transactions, categorization rules, and CSV import mappings. All models use SQLAlchemy 2.0 mapped columns with proper foreign key relationships. Includes db.py with Base class, engine/session factories, and 6 passing tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from src.models.household import HouseholdMember
|
||||
from src.models.account import Account
|
||||
from src.models.category import Category
|
||||
from src.models.transaction import Transaction
|
||||
from src.models.rule import CategorizationRule
|
||||
from src.models.csv_mapping import CsvMapping
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.db import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.household import HouseholdMember
|
||||
|
||||
|
||||
class Account(Base):
|
||||
__tablename__ = "accounts"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100))
|
||||
institution: Mapped[str] = mapped_column(String(100))
|
||||
account_type: Mapped[str] = mapped_column(String(20))
|
||||
owner_id: Mapped[int | None] = mapped_column(ForeignKey("household_members.id"))
|
||||
is_shared: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
owner: Mapped[HouseholdMember | None] = relationship(back_populates="accounts")
|
||||
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from src.db import Base
|
||||
|
||||
|
||||
class Category(Base):
|
||||
__tablename__ = "categories"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100), unique=True)
|
||||
default_tag: Mapped[str | None] = mapped_column(String(20))
|
||||
icon: Mapped[str | None] = mapped_column(String(50))
|
||||
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.db import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.account import Account
|
||||
|
||||
|
||||
class CsvMapping(Base):
|
||||
__tablename__ = "csv_mappings"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100))
|
||||
fingerprint: Mapped[str] = mapped_column(String(500))
|
||||
column_map: Mapped[str] = mapped_column(Text)
|
||||
amount_logic: Mapped[str] = mapped_column(String(50))
|
||||
account_id: Mapped[int] = mapped_column(ForeignKey("accounts.id"))
|
||||
|
||||
account: Mapped[Account] = relationship()
|
||||
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship as sa_relationship
|
||||
|
||||
from src.db import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.account import Account
|
||||
|
||||
|
||||
class HouseholdMember(Base):
|
||||
__tablename__ = "household_members"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100))
|
||||
relationship: Mapped[str] = mapped_column(String(50))
|
||||
|
||||
accounts: Mapped[list[Account]] = sa_relationship(back_populates="owner")
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.db import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.category import Category
|
||||
from src.models.household import HouseholdMember
|
||||
|
||||
|
||||
class CategorizationRule(Base):
|
||||
__tablename__ = "categorization_rules"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
pattern: Mapped[str] = mapped_column(String(300))
|
||||
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id"))
|
||||
tag_override: Mapped[str | None] = mapped_column(String(20))
|
||||
attributed_to_id: Mapped[int | None] = mapped_column(ForeignKey("household_members.id"))
|
||||
priority: Mapped[int] = mapped_column(default=0)
|
||||
|
||||
category: Mapped[Category] = relationship()
|
||||
attributed_to: Mapped[HouseholdMember | None] = relationship()
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Date, ForeignKey, Numeric, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.db import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.account import Account
|
||||
from src.models.category import Category
|
||||
from src.models.household import HouseholdMember
|
||||
|
||||
|
||||
class Transaction(Base):
|
||||
__tablename__ = "transactions"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
date: Mapped[datetime.date] = mapped_column(Date)
|
||||
amount: Mapped[float] = mapped_column(Numeric(10, 2))
|
||||
description: Mapped[str] = mapped_column(String(300))
|
||||
raw_description: Mapped[str | None] = mapped_column(Text)
|
||||
account_id: Mapped[int] = mapped_column(ForeignKey("accounts.id"))
|
||||
category_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"))
|
||||
attributed_to_id: Mapped[int | None] = mapped_column(ForeignKey("household_members.id"))
|
||||
tag: Mapped[str | None] = mapped_column(String(20))
|
||||
source_category: Mapped[str | None] = mapped_column(String(100))
|
||||
is_transfer: Mapped[bool] = mapped_column(default=False)
|
||||
transfer_pair_id: Mapped[int | None] = mapped_column(ForeignKey("transactions.id"))
|
||||
|
||||
account: Mapped[Account] = relationship()
|
||||
category: Mapped[Category | None] = relationship()
|
||||
attributed_to: Mapped[HouseholdMember | None] = relationship()
|
||||
Reference in New Issue
Block a user