init commit
This commit is contained in:
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