init commit
This commit is contained in:
5
app/db/__init__.py
Normal file
5
app/db/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Database module - models, repositories, and session management"""
|
||||
|
||||
from app.db.database import SessionLocal, engine, Base
|
||||
|
||||
__all__ = ["SessionLocal", "engine", "Base"]
|
||||
36
app/db/database.py
Normal file
36
app/db/database.py
Normal 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()
|
||||
28
app/db/models/__init__.py
Normal file
28
app/db/models/__init__.py
Normal 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",
|
||||
]
|
||||
50
app/db/models/account.py
Normal file
50
app/db/models/account.py
Normal 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})>"
|
||||
50
app/db/models/budget.py
Normal file
50
app/db/models/budget.py
Normal 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})>"
|
||||
47
app/db/models/category.py
Normal file
47
app/db/models/category.py
Normal 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})>"
|
||||
98
app/db/models/family.py
Normal file
98
app/db/models/family.py
Normal 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})>"
|
||||
44
app/db/models/goal.py
Normal file
44
app/db/models/goal.py
Normal 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})>"
|
||||
57
app/db/models/transaction.py
Normal file
57
app/db/models/transaction.py
Normal 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})>"
|
||||
35
app/db/models/user.py
Normal file
35
app/db/models/user.py
Normal 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})>"
|
||||
21
app/db/repositories/__init__.py
Normal file
21
app/db/repositories/__init__.py
Normal 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",
|
||||
]
|
||||
54
app/db/repositories/account.py
Normal file
54
app/db/repositories/account.py
Normal 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)
|
||||
64
app/db/repositories/base.py
Normal file
64
app/db/repositories/base.py
Normal 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()
|
||||
54
app/db/repositories/budget.py
Normal file
54
app/db/repositories/budget.py
Normal 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
|
||||
50
app/db/repositories/category.py
Normal file
50
app/db/repositories/category.py
Normal 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()
|
||||
)
|
||||
69
app/db/repositories/family.py
Normal file
69
app/db/repositories/family.py
Normal 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()
|
||||
)
|
||||
50
app/db/repositories/goal.py
Normal file
50
app/db/repositories/goal.py
Normal 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
|
||||
94
app/db/repositories/transaction.py
Normal file
94
app/db/repositories/transaction.py
Normal 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)
|
||||
38
app/db/repositories/user.py
Normal file
38
app/db/repositories/user.py
Normal 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
|
||||
Reference in New Issue
Block a user