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,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,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,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,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,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