init commit

This commit is contained in:
2025-12-10 22:09:31 +09:00
commit b79adf1c69
361 changed files with 47414 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
"""Database module - models, repositories, and session management"""
from app.db.database import SessionLocal, engine, Base
__all__ = ["SessionLocal", "engine", "Base"]

View File

@@ -0,0 +1,5 @@
"""Database module - models, repositories, and session management"""
from app.db.database import SessionLocal, engine, Base
__all__ = ["SessionLocal", "engine", "Base"]

View File

@@ -0,0 +1,36 @@
"""Database connection and session management"""
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core.config import get_settings
settings = get_settings()
# Create database engine
engine = create_engine(
settings.database_url,
echo=settings.database_echo,
pool_pre_ping=True, # Verify connections before using them
pool_recycle=3600, # Recycle connections every hour
)
# Create session factory
SessionLocal = sessionmaker(
bind=engine,
autocommit=False,
autoflush=False,
expire_on_commit=False,
)
# Create declarative base for models
Base = declarative_base()
def get_db():
"""Dependency for FastAPI to get database session"""
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,36 @@
"""Database connection and session management"""
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core.config import get_settings
settings = get_settings()
# Create database engine
engine = create_engine(
settings.database_url,
echo=settings.database_echo,
pool_pre_ping=True, # Verify connections before using them
pool_recycle=3600, # Recycle connections every hour
)
# Create session factory
SessionLocal = sessionmaker(
bind=engine,
autocommit=False,
autoflush=False,
expire_on_commit=False,
)
# Create declarative base for models
Base = declarative_base()
def get_db():
"""Dependency for FastAPI to get database session"""
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,21 @@
"""Database models"""
from app.db.models.user import User
from app.db.models.family import Family, FamilyMember, FamilyInvite
from app.db.models.account import Account
from app.db.models.category import Category
from app.db.models.transaction import Transaction
from app.db.models.budget import Budget
from app.db.models.goal import Goal
__all__ = [
"User",
"Family",
"FamilyMember",
"FamilyInvite",
"Account",
"Category",
"Transaction",
"Budget",
"Goal",
]

View File

@@ -0,0 +1,21 @@
"""Database models"""
from app.db.models.user import User
from app.db.models.family import Family, FamilyMember, FamilyInvite
from app.db.models.account import Account
from app.db.models.category import Category
from app.db.models.transaction import Transaction
from app.db.models.budget import Budget
from app.db.models.goal import Goal
__all__ = [
"User",
"Family",
"FamilyMember",
"FamilyInvite",
"Account",
"Category",
"Transaction",
"Budget",
"Goal",
]

View File

@@ -0,0 +1,28 @@
"""Database models"""
from app.db.models.user import User
from app.db.models.family import Family, FamilyMember, FamilyInvite, FamilyRole
from app.db.models.account import Account, AccountType
from app.db.models.category import Category, CategoryType
from app.db.models.transaction import Transaction, TransactionType
from app.db.models.budget import Budget, BudgetPeriod
from app.db.models.goal import Goal
__all__ = [
# Models
"User",
"Family",
"FamilyMember",
"FamilyInvite",
"Account",
"Category",
"Transaction",
"Budget",
"Goal",
# Enums
"FamilyRole",
"AccountType",
"CategoryType",
"TransactionType",
"BudgetPeriod",
]

View File

@@ -0,0 +1,28 @@
"""Database models"""
from app.db.models.user import User
from app.db.models.family import Family, FamilyMember, FamilyInvite, FamilyRole
from app.db.models.account import Account, AccountType
from app.db.models.category import Category, CategoryType
from app.db.models.transaction import Transaction, TransactionType
from app.db.models.budget import Budget, BudgetPeriod
from app.db.models.goal import Goal
__all__ = [
# Models
"User",
"Family",
"FamilyMember",
"FamilyInvite",
"Account",
"Category",
"Transaction",
"Budget",
"Goal",
# Enums
"FamilyRole",
"AccountType",
"CategoryType",
"TransactionType",
"BudgetPeriod",
]

View File

@@ -0,0 +1,50 @@
"""Account (wallet) model"""
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class AccountType(str, PyEnum):
"""Types of accounts"""
CARD = "card"
CASH = "cash"
DEPOSIT = "deposit"
GOAL = "goal"
OTHER = "other"
class Account(Base):
"""Account model - represents a user's wallet or account"""
__tablename__ = "accounts"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
name = Column(String(255), nullable=False)
account_type = Column(Enum(AccountType), default=AccountType.CARD)
description = Column(String(500), nullable=True)
# Balance
balance = Column(Float, default=0.0)
initial_balance = Column(Float, default=0.0)
# Status
is_active = Column(Boolean, default=True)
is_archived = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
family = relationship("Family", back_populates="accounts")
owner = relationship("User", back_populates="accounts")
transactions = relationship("Transaction", back_populates="account")
def __repr__(self) -> str:
return f"<Account(id={self.id}, name={self.name}, balance={self.balance})>"

View File

@@ -0,0 +1,50 @@
"""Account (wallet) model"""
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class AccountType(str, PyEnum):
"""Types of accounts"""
CARD = "card"
CASH = "cash"
DEPOSIT = "deposit"
GOAL = "goal"
OTHER = "other"
class Account(Base):
"""Account model - represents a user's wallet or account"""
__tablename__ = "accounts"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
name = Column(String(255), nullable=False)
account_type = Column(Enum(AccountType), default=AccountType.CARD)
description = Column(String(500), nullable=True)
# Balance
balance = Column(Float, default=0.0)
initial_balance = Column(Float, default=0.0)
# Status
is_active = Column(Boolean, default=True)
is_archived = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
family = relationship("Family", back_populates="accounts")
owner = relationship("User", back_populates="accounts")
transactions = relationship("Transaction", back_populates="account")
def __repr__(self) -> str:
return f"<Account(id={self.id}, name={self.name}, balance={self.balance})>"

View File

@@ -0,0 +1,50 @@
"""Budget model for budget tracking"""
from sqlalchemy import Column, Integer, Float, String, DateTime, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class BudgetPeriod(str, PyEnum):
"""Budget periods"""
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
YEARLY = "yearly"
class Budget(Base):
"""Budget model - spending limits"""
__tablename__ = "budgets"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
# Budget details
name = Column(String(255), nullable=False)
limit_amount = Column(Float, nullable=False)
spent_amount = Column(Float, default=0.0)
period = Column(Enum(BudgetPeriod), default=BudgetPeriod.MONTHLY)
# Alert threshold (percentage)
alert_threshold = Column(Float, default=80.0)
# Status
is_active = Column(Boolean, default=True)
# Timestamps
start_date = Column(DateTime, nullable=False)
end_date = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
family = relationship("Family", back_populates="budgets")
category = relationship("Category", back_populates="budgets")
def __repr__(self) -> str:
return f"<Budget(id={self.id}, name={self.name}, limit={self.limit_amount})>"

View File

@@ -0,0 +1,50 @@
"""Budget model for budget tracking"""
from sqlalchemy import Column, Integer, Float, String, DateTime, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class BudgetPeriod(str, PyEnum):
"""Budget periods"""
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
YEARLY = "yearly"
class Budget(Base):
"""Budget model - spending limits"""
__tablename__ = "budgets"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
# Budget details
name = Column(String(255), nullable=False)
limit_amount = Column(Float, nullable=False)
spent_amount = Column(Float, default=0.0)
period = Column(Enum(BudgetPeriod), default=BudgetPeriod.MONTHLY)
# Alert threshold (percentage)
alert_threshold = Column(Float, default=80.0)
# Status
is_active = Column(Boolean, default=True)
# Timestamps
start_date = Column(DateTime, nullable=False)
end_date = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
family = relationship("Family", back_populates="budgets")
category = relationship("Category", back_populates="budgets")
def __repr__(self) -> str:
return f"<Budget(id={self.id}, name={self.name}, limit={self.limit_amount})>"

View File

@@ -0,0 +1,47 @@
"""Category model for income/expense categories"""
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class CategoryType(str, PyEnum):
"""Types of categories"""
EXPENSE = "expense"
INCOME = "income"
class Category(Base):
"""Category model - income/expense categories"""
__tablename__ = "categories"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
name = Column(String(255), nullable=False)
category_type = Column(Enum(CategoryType), nullable=False)
emoji = Column(String(10), nullable=True)
color = Column(String(7), nullable=True) # Hex color
description = Column(String(500), nullable=True)
# Status
is_active = Column(Boolean, default=True)
is_default = Column(Boolean, default=False)
# Order for UI
order = Column(Integer, default=0)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
family = relationship("Family", back_populates="categories")
transactions = relationship("Transaction", back_populates="category")
budgets = relationship("Budget", back_populates="category")
def __repr__(self) -> str:
return f"<Category(id={self.id}, name={self.name}, type={self.category_type})>"

View File

@@ -0,0 +1,47 @@
"""Category model for income/expense categories"""
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class CategoryType(str, PyEnum):
"""Types of categories"""
EXPENSE = "expense"
INCOME = "income"
class Category(Base):
"""Category model - income/expense categories"""
__tablename__ = "categories"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
name = Column(String(255), nullable=False)
category_type = Column(Enum(CategoryType), nullable=False)
emoji = Column(String(10), nullable=True)
color = Column(String(7), nullable=True) # Hex color
description = Column(String(500), nullable=True)
# Status
is_active = Column(Boolean, default=True)
is_default = Column(Boolean, default=False)
# Order for UI
order = Column(Integer, default=0)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
family = relationship("Family", back_populates="categories")
transactions = relationship("Transaction", back_populates="category")
budgets = relationship("Budget", back_populates="category")
def __repr__(self) -> str:
return f"<Category(id={self.id}, name={self.name}, type={self.category_type})>"

View File

@@ -0,0 +1,98 @@
"""Family and Family-related models"""
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class FamilyRole(str, PyEnum):
"""Roles in family"""
OWNER = "owner"
MEMBER = "member"
RESTRICTED = "restricted"
class Family(Base):
"""Family model - represents a family group"""
__tablename__ = "families"
id = Column(Integer, primary_key=True)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
name = Column(String(255), nullable=False)
description = Column(String(500), nullable=True)
currency = Column(String(3), default="RUB") # ISO 4217 code
invite_code = Column(String(20), unique=True, nullable=False, index=True)
# Settings
notification_level = Column(String(50), default="all") # all, important, none
accounting_period = Column(String(20), default="month") # week, month, year
# Status
is_active = Column(Boolean, default=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
members = relationship("FamilyMember", back_populates="family", cascade="all, delete-orphan")
invites = relationship("FamilyInvite", back_populates="family", cascade="all, delete-orphan")
accounts = relationship("Account", back_populates="family", cascade="all, delete-orphan")
categories = relationship("Category", back_populates="family", cascade="all, delete-orphan")
budgets = relationship("Budget", back_populates="family", cascade="all, delete-orphan")
goals = relationship("Goal", back_populates="family", cascade="all, delete-orphan")
def __repr__(self) -> str:
return f"<Family(id={self.id}, name={self.name}, currency={self.currency})>"
class FamilyMember(Base):
"""Family member model - user membership in family"""
__tablename__ = "family_members"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
role = Column(Enum(FamilyRole), default=FamilyRole.MEMBER)
# Permissions
can_edit_budget = Column(Boolean, default=True)
can_manage_members = Column(Boolean, default=False)
# Timestamps
joined_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Relationships
family = relationship("Family", back_populates="members")
user = relationship("User", back_populates="family_members")
def __repr__(self) -> str:
return f"<FamilyMember(family_id={self.family_id}, user_id={self.user_id}, role={self.role})>"
class FamilyInvite(Base):
"""Family invite model - pending invitations"""
__tablename__ = "family_invites"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
invite_code = Column(String(20), unique=True, nullable=False, index=True)
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Invite validity
is_active = Column(Boolean, default=True)
expires_at = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Relationships
family = relationship("Family", back_populates="invites")
def __repr__(self) -> str:
return f"<FamilyInvite(id={self.id}, family_id={self.family_id})>"

View File

@@ -0,0 +1,98 @@
"""Family and Family-related models"""
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class FamilyRole(str, PyEnum):
"""Roles in family"""
OWNER = "owner"
MEMBER = "member"
RESTRICTED = "restricted"
class Family(Base):
"""Family model - represents a family group"""
__tablename__ = "families"
id = Column(Integer, primary_key=True)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
name = Column(String(255), nullable=False)
description = Column(String(500), nullable=True)
currency = Column(String(3), default="RUB") # ISO 4217 code
invite_code = Column(String(20), unique=True, nullable=False, index=True)
# Settings
notification_level = Column(String(50), default="all") # all, important, none
accounting_period = Column(String(20), default="month") # week, month, year
# Status
is_active = Column(Boolean, default=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
members = relationship("FamilyMember", back_populates="family", cascade="all, delete-orphan")
invites = relationship("FamilyInvite", back_populates="family", cascade="all, delete-orphan")
accounts = relationship("Account", back_populates="family", cascade="all, delete-orphan")
categories = relationship("Category", back_populates="family", cascade="all, delete-orphan")
budgets = relationship("Budget", back_populates="family", cascade="all, delete-orphan")
goals = relationship("Goal", back_populates="family", cascade="all, delete-orphan")
def __repr__(self) -> str:
return f"<Family(id={self.id}, name={self.name}, currency={self.currency})>"
class FamilyMember(Base):
"""Family member model - user membership in family"""
__tablename__ = "family_members"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
role = Column(Enum(FamilyRole), default=FamilyRole.MEMBER)
# Permissions
can_edit_budget = Column(Boolean, default=True)
can_manage_members = Column(Boolean, default=False)
# Timestamps
joined_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Relationships
family = relationship("Family", back_populates="members")
user = relationship("User", back_populates="family_members")
def __repr__(self) -> str:
return f"<FamilyMember(family_id={self.family_id}, user_id={self.user_id}, role={self.role})>"
class FamilyInvite(Base):
"""Family invite model - pending invitations"""
__tablename__ = "family_invites"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
invite_code = Column(String(20), unique=True, nullable=False, index=True)
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Invite validity
is_active = Column(Boolean, default=True)
expires_at = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Relationships
family = relationship("Family", back_populates="invites")
def __repr__(self) -> str:
return f"<FamilyInvite(id={self.id}, family_id={self.family_id})>"

View File

@@ -0,0 +1,44 @@
"""Savings goal model"""
from sqlalchemy import Column, Integer, Float, String, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.database import Base
class Goal(Base):
"""Goal model - savings goals with progress tracking"""
__tablename__ = "goals"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True)
# Goal details
name = Column(String(255), nullable=False)
description = Column(String(500), nullable=True)
target_amount = Column(Float, nullable=False)
current_amount = Column(Float, default=0.0)
# Priority
priority = Column(Integer, default=0)
# Status
is_active = Column(Boolean, default=True)
is_completed = Column(Boolean, default=False)
# Deadlines
target_date = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
completed_at = Column(DateTime, nullable=True)
# Relationships
family = relationship("Family", back_populates="goals")
account = relationship("Account")
def __repr__(self) -> str:
return f"<Goal(id={self.id}, name={self.name}, target={self.target_amount})>"

View File

@@ -0,0 +1,44 @@
"""Savings goal model"""
from sqlalchemy import Column, Integer, Float, String, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.database import Base
class Goal(Base):
"""Goal model - savings goals with progress tracking"""
__tablename__ = "goals"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True)
# Goal details
name = Column(String(255), nullable=False)
description = Column(String(500), nullable=True)
target_amount = Column(Float, nullable=False)
current_amount = Column(Float, default=0.0)
# Priority
priority = Column(Integer, default=0)
# Status
is_active = Column(Boolean, default=True)
is_completed = Column(Boolean, default=False)
# Deadlines
target_date = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
completed_at = Column(DateTime, nullable=True)
# Relationships
family = relationship("Family", back_populates="goals")
account = relationship("Account")
def __repr__(self) -> str:
return f"<Goal(id={self.id}, name={self.name}, target={self.target_amount})>"

View File

@@ -0,0 +1,57 @@
"""Transaction model for income/expense records"""
from sqlalchemy import Column, Integer, Float, String, DateTime, Boolean, ForeignKey, Text, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class TransactionType(str, PyEnum):
"""Types of transactions"""
EXPENSE = "expense"
INCOME = "income"
TRANSFER = "transfer"
class Transaction(Base):
"""Transaction model - represents income/expense transaction"""
__tablename__ = "transactions"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
account_id = Column(Integer, ForeignKey("accounts.id"), nullable=False, index=True)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
# Transaction details
amount = Column(Float, nullable=False)
transaction_type = Column(Enum(TransactionType), nullable=False)
description = Column(String(500), nullable=True)
notes = Column(Text, nullable=True)
tags = Column(String(500), nullable=True) # Comma-separated tags
# Receipt
receipt_photo_url = Column(String(500), nullable=True)
# Recurring transaction
is_recurring = Column(Boolean, default=False)
recurrence_pattern = Column(String(50), nullable=True) # daily, weekly, monthly, etc.
# Status
is_confirmed = Column(Boolean, default=True)
# Timestamps
transaction_date = Column(DateTime, nullable=False, index=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
family = relationship("Family")
user = relationship("User", back_populates="transactions")
account = relationship("Account", back_populates="transactions")
category = relationship("Category", back_populates="transactions")
def __repr__(self) -> str:
return f"<Transaction(id={self.id}, amount={self.amount}, type={self.transaction_type})>"

View File

@@ -0,0 +1,57 @@
"""Transaction model for income/expense records"""
from sqlalchemy import Column, Integer, Float, String, DateTime, Boolean, ForeignKey, Text, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.db.database import Base
class TransactionType(str, PyEnum):
"""Types of transactions"""
EXPENSE = "expense"
INCOME = "income"
TRANSFER = "transfer"
class Transaction(Base):
"""Transaction model - represents income/expense transaction"""
__tablename__ = "transactions"
id = Column(Integer, primary_key=True)
family_id = Column(Integer, ForeignKey("families.id"), nullable=False, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
account_id = Column(Integer, ForeignKey("accounts.id"), nullable=False, index=True)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
# Transaction details
amount = Column(Float, nullable=False)
transaction_type = Column(Enum(TransactionType), nullable=False)
description = Column(String(500), nullable=True)
notes = Column(Text, nullable=True)
tags = Column(String(500), nullable=True) # Comma-separated tags
# Receipt
receipt_photo_url = Column(String(500), nullable=True)
# Recurring transaction
is_recurring = Column(Boolean, default=False)
recurrence_pattern = Column(String(50), nullable=True) # daily, weekly, monthly, etc.
# Status
is_confirmed = Column(Boolean, default=True)
# Timestamps
transaction_date = Column(DateTime, nullable=False, index=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
family = relationship("Family")
user = relationship("User", back_populates="transactions")
account = relationship("Account", back_populates="transactions")
category = relationship("Category", back_populates="transactions")
def __repr__(self) -> str:
return f"<Transaction(id={self.id}, amount={self.amount}, type={self.transaction_type})>"

View File

@@ -0,0 +1,35 @@
"""User model"""
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.database import Base
class User(Base):
"""User model - represents a Telegram user"""
__tablename__ = "users"
id = Column(Integer, primary_key=True)
telegram_id = Column(Integer, unique=True, nullable=False, index=True)
username = Column(String(255), nullable=True)
first_name = Column(String(255), nullable=True)
last_name = Column(String(255), nullable=True)
phone = Column(String(20), nullable=True)
# Account status
is_active = Column(Boolean, default=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
last_activity = Column(DateTime, nullable=True)
# Relationships
family_members = relationship("FamilyMember", back_populates="user")
accounts = relationship("Account", back_populates="owner")
transactions = relationship("Transaction", back_populates="user")
def __repr__(self) -> str:
return f"<User(id={self.id}, telegram_id={self.telegram_id}, username={self.username})>"

View File

@@ -0,0 +1,35 @@
"""User model"""
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.database import Base
class User(Base):
"""User model - represents a Telegram user"""
__tablename__ = "users"
id = Column(Integer, primary_key=True)
telegram_id = Column(Integer, unique=True, nullable=False, index=True)
username = Column(String(255), nullable=True)
first_name = Column(String(255), nullable=True)
last_name = Column(String(255), nullable=True)
phone = Column(String(20), nullable=True)
# Account status
is_active = Column(Boolean, default=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
last_activity = Column(DateTime, nullable=True)
# Relationships
family_members = relationship("FamilyMember", back_populates="user")
accounts = relationship("Account", back_populates="owner")
transactions = relationship("Transaction", back_populates="user")
def __repr__(self) -> str:
return f"<User(id={self.id}, telegram_id={self.telegram_id}, username={self.username})>"

View File

@@ -0,0 +1,21 @@
"""Repository layer for database access"""
from app.db.repositories.base import BaseRepository
from app.db.repositories.user import UserRepository
from app.db.repositories.family import FamilyRepository
from app.db.repositories.account import AccountRepository
from app.db.repositories.category import CategoryRepository
from app.db.repositories.transaction import TransactionRepository
from app.db.repositories.budget import BudgetRepository
from app.db.repositories.goal import GoalRepository
__all__ = [
"BaseRepository",
"UserRepository",
"FamilyRepository",
"AccountRepository",
"CategoryRepository",
"TransactionRepository",
"BudgetRepository",
"GoalRepository",
]

View File

@@ -0,0 +1,21 @@
"""Repository layer for database access"""
from app.db.repositories.base import BaseRepository
from app.db.repositories.user import UserRepository
from app.db.repositories.family import FamilyRepository
from app.db.repositories.account import AccountRepository
from app.db.repositories.category import CategoryRepository
from app.db.repositories.transaction import TransactionRepository
from app.db.repositories.budget import BudgetRepository
from app.db.repositories.goal import GoalRepository
__all__ = [
"BaseRepository",
"UserRepository",
"FamilyRepository",
"AccountRepository",
"CategoryRepository",
"TransactionRepository",
"BudgetRepository",
"GoalRepository",
]

View File

@@ -0,0 +1,54 @@
"""Account repository"""
from typing import Optional, List
from sqlalchemy.orm import Session
from app.db.models import Account
from app.db.repositories.base import BaseRepository
class AccountRepository(BaseRepository[Account]):
"""Account data access operations"""
def __init__(self, session: Session):
super().__init__(session, Account)
def get_family_accounts(self, family_id: int) -> List[Account]:
"""Get all accounts for a family"""
return (
self.session.query(Account)
.filter(Account.family_id == family_id, Account.is_active == True)
.all()
)
def get_user_accounts(self, user_id: int) -> List[Account]:
"""Get all accounts owned by user"""
return (
self.session.query(Account)
.filter(Account.owner_id == user_id, Account.is_active == True)
.all()
)
def get_account_if_accessible(self, account_id: int, family_id: int) -> Optional[Account]:
"""Get account only if it belongs to family"""
return (
self.session.query(Account)
.filter(
Account.id == account_id,
Account.family_id == family_id,
Account.is_active == True
)
.first()
)
def update_balance(self, account_id: int, amount: float) -> Optional[Account]:
"""Update account balance by delta"""
account = self.get_by_id(account_id)
if account:
account.balance += amount
self.session.commit()
self.session.refresh(account)
return account
def archive_account(self, account_id: int) -> Optional[Account]:
"""Archive account"""
return self.update(account_id, is_archived=True)

View File

@@ -0,0 +1,54 @@
"""Account repository"""
from typing import Optional, List
from sqlalchemy.orm import Session
from app.db.models import Account
from app.db.repositories.base import BaseRepository
class AccountRepository(BaseRepository[Account]):
"""Account data access operations"""
def __init__(self, session: Session):
super().__init__(session, Account)
def get_family_accounts(self, family_id: int) -> List[Account]:
"""Get all accounts for a family"""
return (
self.session.query(Account)
.filter(Account.family_id == family_id, Account.is_active == True)
.all()
)
def get_user_accounts(self, user_id: int) -> List[Account]:
"""Get all accounts owned by user"""
return (
self.session.query(Account)
.filter(Account.owner_id == user_id, Account.is_active == True)
.all()
)
def get_account_if_accessible(self, account_id: int, family_id: int) -> Optional[Account]:
"""Get account only if it belongs to family"""
return (
self.session.query(Account)
.filter(
Account.id == account_id,
Account.family_id == family_id,
Account.is_active == True
)
.first()
)
def update_balance(self, account_id: int, amount: float) -> Optional[Account]:
"""Update account balance by delta"""
account = self.get_by_id(account_id)
if account:
account.balance += amount
self.session.commit()
self.session.refresh(account)
return account
def archive_account(self, account_id: int) -> Optional[Account]:
"""Archive account"""
return self.update(account_id, is_archived=True)

View File

@@ -0,0 +1,64 @@
"""Base repository with generic CRUD operations"""
from typing import TypeVar, Generic, Type, List, Optional, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.db.database import Base as SQLAlchemyBase
T = TypeVar("T", bound=SQLAlchemyBase)
class BaseRepository(Generic[T]):
"""Generic repository for CRUD operations"""
def __init__(self, session: Session, model: Type[T]):
self.session = session
self.model = model
def create(self, **kwargs) -> T:
"""Create and return new instance"""
instance = self.model(**kwargs)
self.session.add(instance)
self.session.commit()
self.session.refresh(instance)
return instance
def get_by_id(self, id: Any) -> Optional[T]:
"""Get instance by primary key"""
return self.session.query(self.model).filter(self.model.id == id).first()
def get_all(self, skip: int = 0, limit: int = 100) -> List[T]:
"""Get all instances with pagination"""
return (
self.session.query(self.model)
.offset(skip)
.limit(limit)
.all()
)
def update(self, id: Any, **kwargs) -> Optional[T]:
"""Update instance by id"""
instance = self.get_by_id(id)
if instance:
for key, value in kwargs.items():
setattr(instance, key, value)
self.session.commit()
self.session.refresh(instance)
return instance
def delete(self, id: Any) -> bool:
"""Delete instance by id"""
instance = self.get_by_id(id)
if instance:
self.session.delete(instance)
self.session.commit()
return True
return False
def exists(self, **kwargs) -> bool:
"""Check if instance exists with given filters"""
return self.session.query(self.model).filter_by(**kwargs).first() is not None
def count(self, **kwargs) -> int:
"""Count instances with given filters"""
return self.session.query(self.model).filter_by(**kwargs).count()

View File

@@ -0,0 +1,64 @@
"""Base repository with generic CRUD operations"""
from typing import TypeVar, Generic, Type, List, Optional, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.db.database import Base as SQLAlchemyBase
T = TypeVar("T", bound=SQLAlchemyBase)
class BaseRepository(Generic[T]):
"""Generic repository for CRUD operations"""
def __init__(self, session: Session, model: Type[T]):
self.session = session
self.model = model
def create(self, **kwargs) -> T:
"""Create and return new instance"""
instance = self.model(**kwargs)
self.session.add(instance)
self.session.commit()
self.session.refresh(instance)
return instance
def get_by_id(self, id: Any) -> Optional[T]:
"""Get instance by primary key"""
return self.session.query(self.model).filter(self.model.id == id).first()
def get_all(self, skip: int = 0, limit: int = 100) -> List[T]:
"""Get all instances with pagination"""
return (
self.session.query(self.model)
.offset(skip)
.limit(limit)
.all()
)
def update(self, id: Any, **kwargs) -> Optional[T]:
"""Update instance by id"""
instance = self.get_by_id(id)
if instance:
for key, value in kwargs.items():
setattr(instance, key, value)
self.session.commit()
self.session.refresh(instance)
return instance
def delete(self, id: Any) -> bool:
"""Delete instance by id"""
instance = self.get_by_id(id)
if instance:
self.session.delete(instance)
self.session.commit()
return True
return False
def exists(self, **kwargs) -> bool:
"""Check if instance exists with given filters"""
return self.session.query(self.model).filter_by(**kwargs).first() is not None
def count(self, **kwargs) -> int:
"""Count instances with given filters"""
return self.session.query(self.model).filter_by(**kwargs).count()

View File

@@ -0,0 +1,54 @@
"""Budget repository"""
from typing import List, Optional
from sqlalchemy.orm import Session
from app.db.models import Budget
from app.db.repositories.base import BaseRepository
class BudgetRepository(BaseRepository[Budget]):
"""Budget data access operations"""
def __init__(self, session: Session):
super().__init__(session, Budget)
def get_family_budgets(self, family_id: int) -> List[Budget]:
"""Get all active budgets for family"""
return (
self.session.query(Budget)
.filter(Budget.family_id == family_id, Budget.is_active == True)
.all()
)
def get_category_budget(self, family_id: int, category_id: int) -> Optional[Budget]:
"""Get budget for specific category"""
return (
self.session.query(Budget)
.filter(
Budget.family_id == family_id,
Budget.category_id == category_id,
Budget.is_active == True
)
.first()
)
def get_general_budget(self, family_id: int) -> Optional[Budget]:
"""Get general budget (no category)"""
return (
self.session.query(Budget)
.filter(
Budget.family_id == family_id,
Budget.category_id == None,
Budget.is_active == True
)
.first()
)
def update_spent_amount(self, budget_id: int, amount: float) -> Optional[Budget]:
"""Update spent amount for budget"""
budget = self.get_by_id(budget_id)
if budget:
budget.spent_amount += amount
self.session.commit()
self.session.refresh(budget)
return budget

View File

@@ -0,0 +1,54 @@
"""Budget repository"""
from typing import List, Optional
from sqlalchemy.orm import Session
from app.db.models import Budget
from app.db.repositories.base import BaseRepository
class BudgetRepository(BaseRepository[Budget]):
"""Budget data access operations"""
def __init__(self, session: Session):
super().__init__(session, Budget)
def get_family_budgets(self, family_id: int) -> List[Budget]:
"""Get all active budgets for family"""
return (
self.session.query(Budget)
.filter(Budget.family_id == family_id, Budget.is_active == True)
.all()
)
def get_category_budget(self, family_id: int, category_id: int) -> Optional[Budget]:
"""Get budget for specific category"""
return (
self.session.query(Budget)
.filter(
Budget.family_id == family_id,
Budget.category_id == category_id,
Budget.is_active == True
)
.first()
)
def get_general_budget(self, family_id: int) -> Optional[Budget]:
"""Get general budget (no category)"""
return (
self.session.query(Budget)
.filter(
Budget.family_id == family_id,
Budget.category_id == None,
Budget.is_active == True
)
.first()
)
def update_spent_amount(self, budget_id: int, amount: float) -> Optional[Budget]:
"""Update spent amount for budget"""
budget = self.get_by_id(budget_id)
if budget:
budget.spent_amount += amount
self.session.commit()
self.session.refresh(budget)
return budget

View File

@@ -0,0 +1,50 @@
"""Category repository"""
from typing import List, Optional
from sqlalchemy.orm import Session
from app.db.models import Category, CategoryType
from app.db.repositories.base import BaseRepository
class CategoryRepository(BaseRepository[Category]):
"""Category data access operations"""
def __init__(self, session: Session):
super().__init__(session, Category)
def get_family_categories(
self, family_id: int, category_type: Optional[CategoryType] = None
) -> List[Category]:
"""Get categories for family, optionally filtered by type"""
query = self.session.query(Category).filter(
Category.family_id == family_id,
Category.is_active == True
)
if category_type:
query = query.filter(Category.category_type == category_type)
return query.order_by(Category.order).all()
def get_by_name(self, family_id: int, name: str) -> Optional[Category]:
"""Get category by name"""
return (
self.session.query(Category)
.filter(
Category.family_id == family_id,
Category.name == name,
Category.is_active == True
)
.first()
)
def get_default_categories(self, family_id: int, category_type: CategoryType) -> List[Category]:
"""Get default categories of type"""
return (
self.session.query(Category)
.filter(
Category.family_id == family_id,
Category.category_type == category_type,
Category.is_default == True,
Category.is_active == True
)
.all()
)

View File

@@ -0,0 +1,50 @@
"""Category repository"""
from typing import List, Optional
from sqlalchemy.orm import Session
from app.db.models import Category, CategoryType
from app.db.repositories.base import BaseRepository
class CategoryRepository(BaseRepository[Category]):
"""Category data access operations"""
def __init__(self, session: Session):
super().__init__(session, Category)
def get_family_categories(
self, family_id: int, category_type: Optional[CategoryType] = None
) -> List[Category]:
"""Get categories for family, optionally filtered by type"""
query = self.session.query(Category).filter(
Category.family_id == family_id,
Category.is_active == True
)
if category_type:
query = query.filter(Category.category_type == category_type)
return query.order_by(Category.order).all()
def get_by_name(self, family_id: int, name: str) -> Optional[Category]:
"""Get category by name"""
return (
self.session.query(Category)
.filter(
Category.family_id == family_id,
Category.name == name,
Category.is_active == True
)
.first()
)
def get_default_categories(self, family_id: int, category_type: CategoryType) -> List[Category]:
"""Get default categories of type"""
return (
self.session.query(Category)
.filter(
Category.family_id == family_id,
Category.category_type == category_type,
Category.is_default == True,
Category.is_active == True
)
.all()
)

View File

@@ -0,0 +1,69 @@
"""Family repository"""
from typing import Optional, List
from sqlalchemy.orm import Session
from app.db.models import Family, FamilyMember, FamilyInvite
from app.db.repositories.base import BaseRepository
class FamilyRepository(BaseRepository[Family]):
"""Family data access operations"""
def __init__(self, session: Session):
super().__init__(session, Family)
def get_by_invite_code(self, invite_code: str) -> Optional[Family]:
"""Get family by invite code"""
return self.session.query(Family).filter(Family.invite_code == invite_code).first()
def get_user_families(self, user_id: int) -> List[Family]:
"""Get all families for a user"""
return (
self.session.query(Family)
.join(FamilyMember)
.filter(FamilyMember.user_id == user_id)
.all()
)
def is_member(self, family_id: int, user_id: int) -> bool:
"""Check if user is member of family"""
return (
self.session.query(FamilyMember)
.filter(
FamilyMember.family_id == family_id,
FamilyMember.user_id == user_id
)
.first() is not None
)
def add_member(self, family_id: int, user_id: int, role: str = "member") -> FamilyMember:
"""Add user to family"""
member = FamilyMember(family_id=family_id, user_id=user_id, role=role)
self.session.add(member)
self.session.commit()
self.session.refresh(member)
return member
def remove_member(self, family_id: int, user_id: int) -> bool:
"""Remove user from family"""
member = (
self.session.query(FamilyMember)
.filter(
FamilyMember.family_id == family_id,
FamilyMember.user_id == user_id
)
.first()
)
if member:
self.session.delete(member)
self.session.commit()
return True
return False
def get_invite(self, invite_code: str) -> Optional[FamilyInvite]:
"""Get invite by code"""
return (
self.session.query(FamilyInvite)
.filter(FamilyInvite.invite_code == invite_code)
.first()
)

View File

@@ -0,0 +1,69 @@
"""Family repository"""
from typing import Optional, List
from sqlalchemy.orm import Session
from app.db.models import Family, FamilyMember, FamilyInvite
from app.db.repositories.base import BaseRepository
class FamilyRepository(BaseRepository[Family]):
"""Family data access operations"""
def __init__(self, session: Session):
super().__init__(session, Family)
def get_by_invite_code(self, invite_code: str) -> Optional[Family]:
"""Get family by invite code"""
return self.session.query(Family).filter(Family.invite_code == invite_code).first()
def get_user_families(self, user_id: int) -> List[Family]:
"""Get all families for a user"""
return (
self.session.query(Family)
.join(FamilyMember)
.filter(FamilyMember.user_id == user_id)
.all()
)
def is_member(self, family_id: int, user_id: int) -> bool:
"""Check if user is member of family"""
return (
self.session.query(FamilyMember)
.filter(
FamilyMember.family_id == family_id,
FamilyMember.user_id == user_id
)
.first() is not None
)
def add_member(self, family_id: int, user_id: int, role: str = "member") -> FamilyMember:
"""Add user to family"""
member = FamilyMember(family_id=family_id, user_id=user_id, role=role)
self.session.add(member)
self.session.commit()
self.session.refresh(member)
return member
def remove_member(self, family_id: int, user_id: int) -> bool:
"""Remove user from family"""
member = (
self.session.query(FamilyMember)
.filter(
FamilyMember.family_id == family_id,
FamilyMember.user_id == user_id
)
.first()
)
if member:
self.session.delete(member)
self.session.commit()
return True
return False
def get_invite(self, invite_code: str) -> Optional[FamilyInvite]:
"""Get invite by code"""
return (
self.session.query(FamilyInvite)
.filter(FamilyInvite.invite_code == invite_code)
.first()
)

View File

@@ -0,0 +1,50 @@
"""Goal repository"""
from typing import List, Optional
from sqlalchemy.orm import Session
from app.db.models import Goal
from app.db.repositories.base import BaseRepository
class GoalRepository(BaseRepository[Goal]):
"""Goal data access operations"""
def __init__(self, session: Session):
super().__init__(session, Goal)
def get_family_goals(self, family_id: int) -> List[Goal]:
"""Get all active goals for family"""
return (
self.session.query(Goal)
.filter(Goal.family_id == family_id, Goal.is_active == True)
.order_by(Goal.priority.desc())
.all()
)
def get_goals_progress(self, family_id: int) -> List[dict]:
"""Get goals with progress info"""
goals = self.get_family_goals(family_id)
return [
{
"id": goal.id,
"name": goal.name,
"target": goal.target_amount,
"current": goal.current_amount,
"progress_percent": (goal.current_amount / goal.target_amount * 100) if goal.target_amount > 0 else 0,
"is_completed": goal.is_completed
}
for goal in goals
]
def update_progress(self, goal_id: int, amount: float) -> Optional[Goal]:
"""Update goal progress"""
goal = self.get_by_id(goal_id)
if goal:
goal.current_amount += amount
if goal.current_amount >= goal.target_amount:
goal.is_completed = True
from datetime import datetime
goal.completed_at = datetime.utcnow()
self.session.commit()
self.session.refresh(goal)
return goal

View File

@@ -0,0 +1,50 @@
"""Goal repository"""
from typing import List, Optional
from sqlalchemy.orm import Session
from app.db.models import Goal
from app.db.repositories.base import BaseRepository
class GoalRepository(BaseRepository[Goal]):
"""Goal data access operations"""
def __init__(self, session: Session):
super().__init__(session, Goal)
def get_family_goals(self, family_id: int) -> List[Goal]:
"""Get all active goals for family"""
return (
self.session.query(Goal)
.filter(Goal.family_id == family_id, Goal.is_active == True)
.order_by(Goal.priority.desc())
.all()
)
def get_goals_progress(self, family_id: int) -> List[dict]:
"""Get goals with progress info"""
goals = self.get_family_goals(family_id)
return [
{
"id": goal.id,
"name": goal.name,
"target": goal.target_amount,
"current": goal.current_amount,
"progress_percent": (goal.current_amount / goal.target_amount * 100) if goal.target_amount > 0 else 0,
"is_completed": goal.is_completed
}
for goal in goals
]
def update_progress(self, goal_id: int, amount: float) -> Optional[Goal]:
"""Update goal progress"""
goal = self.get_by_id(goal_id)
if goal:
goal.current_amount += amount
if goal.current_amount >= goal.target_amount:
goal.is_completed = True
from datetime import datetime
goal.completed_at = datetime.utcnow()
self.session.commit()
self.session.refresh(goal)
return goal

View File

@@ -0,0 +1,94 @@
"""Transaction repository"""
from typing import List, Optional
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import and_
from app.db.models import Transaction, TransactionType
from app.db.repositories.base import BaseRepository
class TransactionRepository(BaseRepository[Transaction]):
"""Transaction data access operations"""
def __init__(self, session: Session):
super().__init__(session, Transaction)
def get_family_transactions(self, family_id: int, skip: int = 0, limit: int = 50) -> List[Transaction]:
"""Get transactions for family"""
return (
self.session.query(Transaction)
.filter(Transaction.family_id == family_id)
.order_by(Transaction.transaction_date.desc())
.offset(skip)
.limit(limit)
.all()
)
def get_transactions_by_period(
self, family_id: int, start_date: datetime, end_date: datetime
) -> List[Transaction]:
"""Get transactions within date range"""
return (
self.session.query(Transaction)
.filter(
and_(
Transaction.family_id == family_id,
Transaction.transaction_date >= start_date,
Transaction.transaction_date <= end_date
)
)
.order_by(Transaction.transaction_date.desc())
.all()
)
def get_transactions_by_category(
self, family_id: int, category_id: int, start_date: datetime, end_date: datetime
) -> List[Transaction]:
"""Get transactions by category in date range"""
return (
self.session.query(Transaction)
.filter(
and_(
Transaction.family_id == family_id,
Transaction.category_id == category_id,
Transaction.transaction_date >= start_date,
Transaction.transaction_date <= end_date
)
)
.all()
)
def get_user_transactions(self, user_id: int, days: int = 30) -> List[Transaction]:
"""Get user's recent transactions"""
start_date = datetime.utcnow() - timedelta(days=days)
return (
self.session.query(Transaction)
.filter(
and_(
Transaction.user_id == user_id,
Transaction.transaction_date >= start_date
)
)
.order_by(Transaction.transaction_date.desc())
.all()
)
def sum_by_category(
self, family_id: int, category_id: int, start_date: datetime, end_date: datetime
) -> float:
"""Calculate sum of transactions by category"""
result = (
self.session.query(Transaction)
.filter(
and_(
Transaction.family_id == family_id,
Transaction.category_id == category_id,
Transaction.transaction_date >= start_date,
Transaction.transaction_date <= end_date,
Transaction.transaction_type == TransactionType.EXPENSE
)
)
.all()
)
return sum(t.amount for t in result)

View File

@@ -0,0 +1,94 @@
"""Transaction repository"""
from typing import List, Optional
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import and_
from app.db.models import Transaction, TransactionType
from app.db.repositories.base import BaseRepository
class TransactionRepository(BaseRepository[Transaction]):
"""Transaction data access operations"""
def __init__(self, session: Session):
super().__init__(session, Transaction)
def get_family_transactions(self, family_id: int, skip: int = 0, limit: int = 50) -> List[Transaction]:
"""Get transactions for family"""
return (
self.session.query(Transaction)
.filter(Transaction.family_id == family_id)
.order_by(Transaction.transaction_date.desc())
.offset(skip)
.limit(limit)
.all()
)
def get_transactions_by_period(
self, family_id: int, start_date: datetime, end_date: datetime
) -> List[Transaction]:
"""Get transactions within date range"""
return (
self.session.query(Transaction)
.filter(
and_(
Transaction.family_id == family_id,
Transaction.transaction_date >= start_date,
Transaction.transaction_date <= end_date
)
)
.order_by(Transaction.transaction_date.desc())
.all()
)
def get_transactions_by_category(
self, family_id: int, category_id: int, start_date: datetime, end_date: datetime
) -> List[Transaction]:
"""Get transactions by category in date range"""
return (
self.session.query(Transaction)
.filter(
and_(
Transaction.family_id == family_id,
Transaction.category_id == category_id,
Transaction.transaction_date >= start_date,
Transaction.transaction_date <= end_date
)
)
.all()
)
def get_user_transactions(self, user_id: int, days: int = 30) -> List[Transaction]:
"""Get user's recent transactions"""
start_date = datetime.utcnow() - timedelta(days=days)
return (
self.session.query(Transaction)
.filter(
and_(
Transaction.user_id == user_id,
Transaction.transaction_date >= start_date
)
)
.order_by(Transaction.transaction_date.desc())
.all()
)
def sum_by_category(
self, family_id: int, category_id: int, start_date: datetime, end_date: datetime
) -> float:
"""Calculate sum of transactions by category"""
result = (
self.session.query(Transaction)
.filter(
and_(
Transaction.family_id == family_id,
Transaction.category_id == category_id,
Transaction.transaction_date >= start_date,
Transaction.transaction_date <= end_date,
Transaction.transaction_type == TransactionType.EXPENSE
)
)
.all()
)
return sum(t.amount for t in result)

View File

@@ -0,0 +1,38 @@
"""User repository"""
from typing import Optional
from sqlalchemy.orm import Session
from app.db.models import User
from app.db.repositories.base import BaseRepository
class UserRepository(BaseRepository[User]):
"""User data access operations"""
def __init__(self, session: Session):
super().__init__(session, User)
def get_by_telegram_id(self, telegram_id: int) -> Optional[User]:
"""Get user by Telegram ID"""
return self.session.query(User).filter(User.telegram_id == telegram_id).first()
def get_by_username(self, username: str) -> Optional[User]:
"""Get user by username"""
return self.session.query(User).filter(User.username == username).first()
def get_or_create(self, telegram_id: int, **kwargs) -> User:
"""Get user or create if doesn't exist"""
user = self.get_by_telegram_id(telegram_id)
if not user:
user = self.create(telegram_id=telegram_id, **kwargs)
return user
def update_activity(self, telegram_id: int) -> Optional[User]:
"""Update user's last activity timestamp"""
from datetime import datetime
user = self.get_by_telegram_id(telegram_id)
if user:
user.last_activity = datetime.utcnow()
self.session.commit()
self.session.refresh(user)
return user

View File

@@ -0,0 +1,38 @@
"""User repository"""
from typing import Optional
from sqlalchemy.orm import Session
from app.db.models import User
from app.db.repositories.base import BaseRepository
class UserRepository(BaseRepository[User]):
"""User data access operations"""
def __init__(self, session: Session):
super().__init__(session, User)
def get_by_telegram_id(self, telegram_id: int) -> Optional[User]:
"""Get user by Telegram ID"""
return self.session.query(User).filter(User.telegram_id == telegram_id).first()
def get_by_username(self, username: str) -> Optional[User]:
"""Get user by username"""
return self.session.query(User).filter(User.username == username).first()
def get_or_create(self, telegram_id: int, **kwargs) -> User:
"""Get user or create if doesn't exist"""
user = self.get_by_telegram_id(telegram_id)
if not user:
user = self.create(telegram_id=telegram_id, **kwargs)
return user
def update_activity(self, telegram_id: int) -> Optional[User]:
"""Update user's last activity timestamp"""
from datetime import datetime
user = self.get_by_telegram_id(telegram_id)
if user:
user.last_activity = datetime.utcnow()
self.session.commit()
self.session.refresh(user)
return user