feat: income
This commit is contained in:
		@@ -1,170 +0,0 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
import math
 | 
			
		||||
from fastapi import HTTPException
 | 
			
		||||
from fastapi import status
 | 
			
		||||
from sqlalchemy import delete, select, func, update, insert
 | 
			
		||||
 | 
			
		||||
from models import User
 | 
			
		||||
from models.expense import Expense, ExpenseTag
 | 
			
		||||
from schemas.base import PaginationSchema
 | 
			
		||||
from schemas.expense import *
 | 
			
		||||
from services.base import BaseService
 | 
			
		||||
from utils.dependecies import is_valid_pagination
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExpensesService(BaseService):
 | 
			
		||||
    async def get_all(self, pagination: PaginationSchema) -> GetAllExpensesResponse:
 | 
			
		||||
        if not is_valid_pagination(pagination):
 | 
			
		||||
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid pagination')
 | 
			
		||||
        page = max(0, pagination.page - 1)
 | 
			
		||||
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(Expense)
 | 
			
		||||
            .order_by(Expense.spent_date.desc())
 | 
			
		||||
            .offset(page * pagination.items_per_page)
 | 
			
		||||
            .limit(pagination.items_per_page)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        total_records = await self.session.scalar(select(func.count()).select_from(Expense))
 | 
			
		||||
        if not total_records:
 | 
			
		||||
            return GetAllExpensesResponse(
 | 
			
		||||
                expenses=[],
 | 
			
		||||
                pagination_info=PaginationInfoSchema(
 | 
			
		||||
                    total_pages=0,
 | 
			
		||||
                    total_items=0
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        total_items = total_records
 | 
			
		||||
        total_pages = math.ceil(total_records / pagination.items_per_page)
 | 
			
		||||
 | 
			
		||||
        expenses = await self.session.execute(stmt)
 | 
			
		||||
        expenses = expenses.scalars().all()
 | 
			
		||||
        response = GetAllExpensesResponse(
 | 
			
		||||
            expenses=expenses,
 | 
			
		||||
            pagination_info=PaginationInfoSchema(
 | 
			
		||||
                total_items=total_items,
 | 
			
		||||
                total_pages=total_pages
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
    async def get_by_id(self, expense_id: int) -> Optional[Expense]:
 | 
			
		||||
        expense = await self.session.get(Expense, expense_id)
 | 
			
		||||
        return expense
 | 
			
		||||
 | 
			
		||||
    async def add_tags(self, expense: Expense, tag_names: list[str]):
 | 
			
		||||
        tags = []
 | 
			
		||||
        for tag_name in tag_names:
 | 
			
		||||
            existing_tag = await self.get_tag_by_name(tag_name)
 | 
			
		||||
            if existing_tag:
 | 
			
		||||
                tags.append(existing_tag)
 | 
			
		||||
            else:
 | 
			
		||||
                tag = ExpenseTag(name=tag_name)
 | 
			
		||||
                self.session.add(tag)
 | 
			
		||||
                tags.append(tag)
 | 
			
		||||
 | 
			
		||||
        expense.tags = tags
 | 
			
		||||
 | 
			
		||||
    async def update_expense(self, user: User, request: UpdateExpenseRequest) -> UpdateExpenseResponse:
 | 
			
		||||
        expense = await self.get_by_id(request.expense.id or -1)
 | 
			
		||||
 | 
			
		||||
        if not expense:
 | 
			
		||||
            new_expense = Expense(
 | 
			
		||||
                created_at=datetime.datetime.now(),
 | 
			
		||||
                name=request.expense.name,
 | 
			
		||||
                comment=request.expense.comment,
 | 
			
		||||
                amount=request.expense.amount,
 | 
			
		||||
                spent_date=request.expense.spent_date,
 | 
			
		||||
                created_by_user_id=user.id,
 | 
			
		||||
            )
 | 
			
		||||
            self.session.add(new_expense)
 | 
			
		||||
            await self.add_tags(new_expense, request.expense.tags)
 | 
			
		||||
            await self.session.commit()
 | 
			
		||||
            return UpdateExpenseResponse(ok=True, message='Запись о расходах успешно создана')
 | 
			
		||||
 | 
			
		||||
        expense.name = request.expense.name
 | 
			
		||||
        expense.amount = request.expense.amount
 | 
			
		||||
        expense.comment = request.expense.comment
 | 
			
		||||
        expense.spent_date = request.expense.spent_date
 | 
			
		||||
        self.session.add(expense)
 | 
			
		||||
        await self.add_tags(expense, request.expense.tags)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return UpdateExpenseResponse(ok=True, message='Запись о расходах успешно изменена')
 | 
			
		||||
 | 
			
		||||
    async def delete_expense(self, expense_id) -> DeleteExpenseResponse:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            delete(Expense)
 | 
			
		||||
            .where(Expense.id == expense_id)
 | 
			
		||||
        )
 | 
			
		||||
        await self.session.execute(stmt)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return DeleteExpenseResponse(ok=True, message='Запись о расходах успешно удалена')
 | 
			
		||||
 | 
			
		||||
    async def get_all_tags(self) -> GetAllExpenseTagsResponse:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(ExpenseTag)
 | 
			
		||||
            .order_by(ExpenseTag.id)
 | 
			
		||||
        )
 | 
			
		||||
        tags = await self.session.execute(stmt)
 | 
			
		||||
        return GetAllExpenseTagsResponse(tags=tags.scalars().all())
 | 
			
		||||
 | 
			
		||||
    async def get_tag_by_id(self, expense_tag_id: int) -> Optional[ExpenseTag]:
 | 
			
		||||
        return await self.session.get(ExpenseTag, expense_tag_id)
 | 
			
		||||
 | 
			
		||||
    async def get_tag_by_name(self, expense_tag_name: str) -> Optional[ExpenseTag]:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(ExpenseTag)
 | 
			
		||||
            .where(ExpenseTag.name == expense_tag_name)
 | 
			
		||||
        )
 | 
			
		||||
        tag = await self.session.scalar(stmt)
 | 
			
		||||
        return tag
 | 
			
		||||
 | 
			
		||||
    async def create_tag(self, request: CreateExpenseTagRequest) -> CreateExpenseTagResponse:
 | 
			
		||||
        tag = await self.get_tag_by_name(request.tag.name)
 | 
			
		||||
        if tag:
 | 
			
		||||
            return CreateExpenseTagResponse(ok=False, message='Такой тег уже есть.')
 | 
			
		||||
 | 
			
		||||
        tag_dict = request.tag.model_dump()
 | 
			
		||||
        stmt = (
 | 
			
		||||
            insert(ExpenseTag)
 | 
			
		||||
            .values(**tag_dict)
 | 
			
		||||
        )
 | 
			
		||||
        await self.session.execute(stmt)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return CreateExpenseTagResponse(ok=True, message='Тег успешно создан.')
 | 
			
		||||
 | 
			
		||||
    async def update_tag(self, request: UpdateExpenseTagRequest) -> UpdateExpenseTagResponse:
 | 
			
		||||
        tag = await self.get_tag_by_name(request.tag.name)
 | 
			
		||||
        if tag:
 | 
			
		||||
            return UpdateExpenseTagResponse(ok=False, message='Тег с таким названием уже есть.')
 | 
			
		||||
 | 
			
		||||
        tag = await self.get_tag_by_id(request.tag.id)
 | 
			
		||||
        if not tag:
 | 
			
		||||
            return UpdateExpenseTagResponse(ok=False, message='Тег не найден.')
 | 
			
		||||
 | 
			
		||||
        tag_dict = request.tag.model_dump()
 | 
			
		||||
        del tag_dict['id']
 | 
			
		||||
        stmt = (
 | 
			
		||||
            update(ExpenseTag)
 | 
			
		||||
            .where(ExpenseTag.id == request.tag.id)
 | 
			
		||||
            .values(**tag_dict)
 | 
			
		||||
        )
 | 
			
		||||
        await self.session.execute(stmt)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return UpdateExpenseTagResponse(ok=True, message='Тег успешно изменен.')
 | 
			
		||||
 | 
			
		||||
    async def delete_tag(self, tag_id: int) -> DeleteExpenseTagResponse:
 | 
			
		||||
        tag = await self.get_tag_by_id(tag_id)
 | 
			
		||||
        if not tag:
 | 
			
		||||
            return DeleteExpenseTagResponse(ok=False, message='Тег не найден.')
 | 
			
		||||
        if len(tag.expenses) > 0:
 | 
			
		||||
            return DeleteExpenseTagResponse(ok=False, message='Тег прикреплен к записи о расходах.')
 | 
			
		||||
 | 
			
		||||
        stmt = (
 | 
			
		||||
            delete(ExpenseTag)
 | 
			
		||||
            .where(ExpenseTag.id == tag_id)
 | 
			
		||||
        )
 | 
			
		||||
        await self.session.execute(stmt)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return DeleteExpenseTagResponse(ok=True, message='Тег удален')
 | 
			
		||||
@@ -3,24 +3,24 @@ from datetime import date
 | 
			
		||||
from sqlalchemy import select, func, Subquery, cast, CTE
 | 
			
		||||
from sqlalchemy.dialects.postgresql import TIMESTAMP
 | 
			
		||||
 | 
			
		||||
from models import PaymentRecord, Expense, expenses_expense_tags
 | 
			
		||||
from models import PaymentRecord
 | 
			
		||||
from schemas.statistics import CommonProfitFilters
 | 
			
		||||
from services.base import BaseService
 | 
			
		||||
from services.statistics.common import generate_date_range
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExpensesStatisticsService(BaseService):
 | 
			
		||||
class PaymentStatisticsService(BaseService):
 | 
			
		||||
    date_from: date
 | 
			
		||||
    date_to: date
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _fill_date_gaps(expenses: Subquery, all_dates: CTE) -> Subquery:
 | 
			
		||||
    def _fill_date_gaps(payments: Subquery, all_dates: CTE) -> Subquery:
 | 
			
		||||
        return (
 | 
			
		||||
            select(
 | 
			
		||||
                all_dates.c.date,
 | 
			
		||||
                (all_dates.c.expenses + func.coalesce(expenses.c.expenses, 0)).label("expenses"),
 | 
			
		||||
                (all_dates.c.expenses + func.coalesce(payments.c.expenses, 0)).label("expenses"),
 | 
			
		||||
            )
 | 
			
		||||
            .join(expenses, all_dates.c.date == expenses.c.date, isouter=True)
 | 
			
		||||
            .join(payments, all_dates.c.date == payments.c.date, isouter=True)
 | 
			
		||||
            .order_by(all_dates.c.date)
 | 
			
		||||
            .subquery()
 | 
			
		||||
        )
 | 
			
		||||
@@ -40,58 +40,24 @@ class ExpensesStatisticsService(BaseService):
 | 
			
		||||
        expenses_with_filled_gaps = self._fill_date_gaps(expenses, all_dates)
 | 
			
		||||
        return expenses_with_filled_gaps
 | 
			
		||||
 | 
			
		||||
    def _get_additional_expenses_sub(self, tag_id: int) -> Subquery:
 | 
			
		||||
        all_dates = generate_date_range(self.date_from, self.date_to, ["expenses"])
 | 
			
		||||
 | 
			
		||||
        expenses = (
 | 
			
		||||
            select(Expense)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if tag_id != -1:
 | 
			
		||||
            expenses = (
 | 
			
		||||
                expenses
 | 
			
		||||
                .join(expenses_expense_tags)
 | 
			
		||||
                .where(expenses_expense_tags.c.expense_tag_id == tag_id)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        expenses = expenses.subquery()
 | 
			
		||||
 | 
			
		||||
        expenses = (
 | 
			
		||||
            select(
 | 
			
		||||
                func.sum(expenses.c.amount).label("expenses"),
 | 
			
		||||
                cast(expenses.c.spent_date, TIMESTAMP(timezone=False)).label("date"),
 | 
			
		||||
            )
 | 
			
		||||
            .where(expenses.c.spent_date.between(self.date_from, self.date_to))
 | 
			
		||||
            .group_by("date")
 | 
			
		||||
            .subquery()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        expenses_with_filled_gaps = self._fill_date_gaps(expenses, all_dates)
 | 
			
		||||
        return expenses_with_filled_gaps
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _apply_expenses(deals_by_dates: Subquery, expenses_subquery: Subquery):
 | 
			
		||||
    def _apply_payments(deals_by_dates: Subquery, expenses_subquery: Subquery):
 | 
			
		||||
        return (
 | 
			
		||||
            select(
 | 
			
		||||
                deals_by_dates.c.date,
 | 
			
		||||
                deals_by_dates.c.deals_count,
 | 
			
		||||
                deals_by_dates.c.revenue,
 | 
			
		||||
                (func.coalesce(deals_by_dates.c.profit, 0) - func.coalesce(expenses_subquery.c.expenses, 0)).label(
 | 
			
		||||
                    "profit"),
 | 
			
		||||
                (func.coalesce(deals_by_dates.c.profit, 0) - func.coalesce(expenses_subquery.c.expenses, 0))
 | 
			
		||||
                .label("profit"),
 | 
			
		||||
                (deals_by_dates.c.expenses + expenses_subquery.c.expenses).label("expenses"),
 | 
			
		||||
            )
 | 
			
		||||
            .join(expenses_subquery, expenses_subquery.c.date == deals_by_dates.c.date)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def apply_expenses(self, filters: CommonProfitFilters, deals_by_dates: Subquery):
 | 
			
		||||
    def apply_payments(self, filters: CommonProfitFilters, deals_by_dates: Subquery):
 | 
			
		||||
        self.date_from, self.date_to = filters.date_range
 | 
			
		||||
 | 
			
		||||
        # Apply salary expenses
 | 
			
		||||
        salary_expenses = self._get_payment_records_sub()
 | 
			
		||||
        deals_by_dates = self._apply_expenses(deals_by_dates, salary_expenses)
 | 
			
		||||
 | 
			
		||||
        # Apply additional expenses
 | 
			
		||||
        additional_expenses = self._get_additional_expenses_sub(filters.tag_id)
 | 
			
		||||
        deals_by_dates = self._apply_expenses(deals_by_dates, additional_expenses)
 | 
			
		||||
        deals_by_dates = self._apply_payments(deals_by_dates, salary_expenses)
 | 
			
		||||
 | 
			
		||||
        return deals_by_dates
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,8 @@ from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataReq
 | 
			
		||||
    GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters
 | 
			
		||||
from services.base import BaseService
 | 
			
		||||
from services.statistics.common import generate_date_range
 | 
			
		||||
from services.statistics.expenses_statistics import ExpensesStatisticsService
 | 
			
		||||
from services.statistics.expenses_statistics import PaymentStatisticsService
 | 
			
		||||
from services.statistics.transactions_statistics import TransactionsStatisticsService
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProfitStatisticsService(BaseService):
 | 
			
		||||
@@ -302,13 +303,18 @@ class ProfitStatisticsService(BaseService):
 | 
			
		||||
        sub_grouped_by_deals = self._group_by_deals(sub_union)
 | 
			
		||||
        sub_deals_grouped_by_date = self._group_by_date(sub_grouped_by_deals).subquery()
 | 
			
		||||
 | 
			
		||||
        expenses_statistics_service = ExpensesStatisticsService(self.session)
 | 
			
		||||
        stmt_deals_applied_expenses = expenses_statistics_service.apply_expenses(
 | 
			
		||||
        expenses_statistics_service = PaymentStatisticsService(self.session)
 | 
			
		||||
        stmt_deals_applied_expenses = expenses_statistics_service.apply_payments(
 | 
			
		||||
            self.filters,
 | 
			
		||||
            sub_deals_grouped_by_date
 | 
			
		||||
        )
 | 
			
		||||
        transactions_statistics_service = TransactionsStatisticsService(self.session)
 | 
			
		||||
        stmt_deals_applied_transactions = transactions_statistics_service.apply_transactions(
 | 
			
		||||
            self.filters,
 | 
			
		||||
            stmt_deals_applied_expenses.subquery()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        result = await self.session.execute(stmt_deals_applied_expenses)
 | 
			
		||||
        result = await self.session.execute(stmt_deals_applied_transactions)
 | 
			
		||||
        rows = result.all()
 | 
			
		||||
        return rows
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										93
									
								
								services/statistics/transactions_statistics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								services/statistics/transactions_statistics.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
			
		||||
from datetime import date
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import select, func, Subquery, cast, CTE, case, or_, and_
 | 
			
		||||
from sqlalchemy.dialects.postgresql import TIMESTAMP
 | 
			
		||||
 | 
			
		||||
from models import Transaction, transactions_transaction_tags, TransactionTag
 | 
			
		||||
from schemas.statistics import CommonProfitFilters
 | 
			
		||||
from services.base import BaseService
 | 
			
		||||
from services.statistics.common import generate_date_range
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionsStatisticsService(BaseService):
 | 
			
		||||
    date_from: date
 | 
			
		||||
    date_to: date
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _fill_date_gaps(expenses: Subquery, all_dates: CTE) -> Subquery:
 | 
			
		||||
        return (
 | 
			
		||||
            select(
 | 
			
		||||
                all_dates.c.date,
 | 
			
		||||
                (all_dates.c.expenses + func.coalesce(expenses.c.expenses, 0)).label("expenses"),
 | 
			
		||||
                (all_dates.c.revenue + func.coalesce(expenses.c.revenue, 0)).label("revenue"),
 | 
			
		||||
            )
 | 
			
		||||
            .join(expenses, all_dates.c.date == expenses.c.date, isouter=True)
 | 
			
		||||
            .order_by(all_dates.c.date)
 | 
			
		||||
            .subquery()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _get_additional_transactions_sub(self, income_tag_id: int, expense_tag_id: int) -> Subquery:
 | 
			
		||||
        all_dates = generate_date_range(self.date_from, self.date_to, ["expenses", "revenue"])
 | 
			
		||||
 | 
			
		||||
        filtered_tags_sub = (
 | 
			
		||||
            select(TransactionTag)
 | 
			
		||||
            .where(
 | 
			
		||||
                or_(
 | 
			
		||||
                    and_(TransactionTag.is_income == True,
 | 
			
		||||
                         or_(income_tag_id == -1, TransactionTag.id == income_tag_id)),
 | 
			
		||||
                    and_(TransactionTag.is_income == False,
 | 
			
		||||
                         or_(expense_tag_id == -1, TransactionTag.id == expense_tag_id)),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            .subquery()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        transaction_ids = (
 | 
			
		||||
            select(transactions_transaction_tags.c.transaction_id)
 | 
			
		||||
            .join(filtered_tags_sub, filtered_tags_sub.c.id == transactions_transaction_tags.c.transaction_tag_id)
 | 
			
		||||
            .group_by(transactions_transaction_tags.c.transaction_id)
 | 
			
		||||
            .subquery()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        transactions = (
 | 
			
		||||
            select(Transaction)
 | 
			
		||||
            .join(transaction_ids, transaction_ids.c.transaction_id == Transaction.id)
 | 
			
		||||
            .subquery()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        transactions = (
 | 
			
		||||
            select(
 | 
			
		||||
                func.sum(case((transactions.c.is_income, transactions.c.amount), else_=0)).label("revenue"),
 | 
			
		||||
                func.sum(case((~transactions.c.is_income, transactions.c.amount), else_=0)).label("expenses"),
 | 
			
		||||
                cast(transactions.c.spent_date, TIMESTAMP(timezone=False)).label("date"),
 | 
			
		||||
            )
 | 
			
		||||
            .where(transactions.c.spent_date.between(self.date_from, self.date_to))
 | 
			
		||||
            .group_by("date")
 | 
			
		||||
            .subquery()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        expenses_with_filled_gaps = self._fill_date_gaps(transactions, all_dates)
 | 
			
		||||
        return expenses_with_filled_gaps
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _apply_transactions(deals_by_dates: Subquery, transactions: Subquery):
 | 
			
		||||
        return (
 | 
			
		||||
            select(
 | 
			
		||||
                deals_by_dates.c.date,
 | 
			
		||||
                deals_by_dates.c.deals_count,
 | 
			
		||||
                (deals_by_dates.c.revenue + transactions.c.revenue).label("revenue"),
 | 
			
		||||
                (func.coalesce(deals_by_dates.c.profit, 0) - func.coalesce(transactions.c.expenses, 0) + func.coalesce(
 | 
			
		||||
                    transactions.c.revenue, 0))
 | 
			
		||||
                .label("profit"),
 | 
			
		||||
                (deals_by_dates.c.expenses + transactions.c.expenses).label("expenses"),
 | 
			
		||||
            )
 | 
			
		||||
            .join(transactions, transactions.c.date == deals_by_dates.c.date)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def apply_transactions(self, filters: CommonProfitFilters, deals_by_dates: Subquery):
 | 
			
		||||
        self.date_from, self.date_to = filters.date_range
 | 
			
		||||
 | 
			
		||||
        additional_expenses = self._get_additional_transactions_sub(filters.income_tag_id, filters.expense_tag_id)
 | 
			
		||||
        deals_by_dates = self._apply_transactions(deals_by_dates, additional_expenses)
 | 
			
		||||
 | 
			
		||||
        return deals_by_dates
 | 
			
		||||
							
								
								
									
										186
									
								
								services/transaction.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								services/transaction.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,186 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
import math
 | 
			
		||||
from fastapi import HTTPException
 | 
			
		||||
from fastapi import status
 | 
			
		||||
from sqlalchemy import delete, select, func, update, insert, and_
 | 
			
		||||
 | 
			
		||||
from models import User
 | 
			
		||||
from models.transaction import Transaction, TransactionTag
 | 
			
		||||
from schemas.base import PaginationSchema
 | 
			
		||||
from schemas.transaction import *
 | 
			
		||||
from services.base import BaseService
 | 
			
		||||
from utils.dependecies import is_valid_pagination
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionsService(BaseService):
 | 
			
		||||
    async def get_all(self, pagination: PaginationSchema, request: GetAllTransactionsRequest) -> GetAllTransactionsResponse:
 | 
			
		||||
        if not is_valid_pagination(pagination):
 | 
			
		||||
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid pagination')
 | 
			
		||||
        page = max(0, pagination.page - 1)
 | 
			
		||||
        is_income = request.is_income
 | 
			
		||||
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(Transaction)
 | 
			
		||||
            .where(Transaction.is_income == is_income)
 | 
			
		||||
            .order_by(Transaction.spent_date.desc())
 | 
			
		||||
            .offset(page * pagination.items_per_page)
 | 
			
		||||
            .limit(pagination.items_per_page)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        total_records = await self.session.scalar(
 | 
			
		||||
            select(func.count())
 | 
			
		||||
            .select_from(Transaction)
 | 
			
		||||
            .where(Transaction.is_income == is_income)
 | 
			
		||||
        )
 | 
			
		||||
        if not total_records:
 | 
			
		||||
            return GetAllTransactionsResponse(
 | 
			
		||||
                transactions=[],
 | 
			
		||||
                pagination_info=PaginationInfoSchema(
 | 
			
		||||
                    total_pages=0,
 | 
			
		||||
                    total_items=0
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        total_items = total_records
 | 
			
		||||
        total_pages = math.ceil(total_records / pagination.items_per_page)
 | 
			
		||||
 | 
			
		||||
        transactions = await self.session.execute(stmt)
 | 
			
		||||
        transactions = transactions.scalars().all()
 | 
			
		||||
        response = GetAllTransactionsResponse(
 | 
			
		||||
            transactions=transactions,
 | 
			
		||||
            pagination_info=PaginationInfoSchema(
 | 
			
		||||
                total_items=total_items,
 | 
			
		||||
                total_pages=total_pages
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
    async def get_by_id(self, transaction_id: int) -> Optional[Transaction]:
 | 
			
		||||
        transaction = await self.session.get(Transaction, transaction_id)
 | 
			
		||||
        return transaction
 | 
			
		||||
 | 
			
		||||
    async def add_tags(self, transaction: Transaction, tag_names: list[str]):
 | 
			
		||||
        tags = []
 | 
			
		||||
        for tag_name in tag_names:
 | 
			
		||||
            existing_tag = await self.get_tag_by_name(tag_name, transaction.is_income)
 | 
			
		||||
            if existing_tag:
 | 
			
		||||
                tags.append(existing_tag)
 | 
			
		||||
            else:
 | 
			
		||||
                tag = TransactionTag(name=tag_name, is_income=transaction.is_income)
 | 
			
		||||
                self.session.add(tag)
 | 
			
		||||
                tags.append(tag)
 | 
			
		||||
 | 
			
		||||
        transaction.tags = tags
 | 
			
		||||
 | 
			
		||||
    async def update_transaction(self, user: User, request: UpdateTransactionRequest) -> UpdateTransactionResponse:
 | 
			
		||||
        transaction = await self.get_by_id(request.transaction.id or -1)
 | 
			
		||||
 | 
			
		||||
        if not transaction:
 | 
			
		||||
            new_transaction = Transaction(
 | 
			
		||||
                created_at=datetime.datetime.now(),
 | 
			
		||||
                name=request.transaction.name,
 | 
			
		||||
                comment=request.transaction.comment,
 | 
			
		||||
                amount=request.transaction.amount,
 | 
			
		||||
                spent_date=request.transaction.spent_date,
 | 
			
		||||
                created_by_user_id=user.id,
 | 
			
		||||
                is_income=request.transaction.is_income,
 | 
			
		||||
            )
 | 
			
		||||
            self.session.add(new_transaction)
 | 
			
		||||
            await self.add_tags(new_transaction, request.transaction.tags)
 | 
			
		||||
            await self.session.commit()
 | 
			
		||||
            return UpdateTransactionResponse(ok=True, message='Запись успешно создана')
 | 
			
		||||
 | 
			
		||||
        transaction.name = request.transaction.name
 | 
			
		||||
        transaction.amount = request.transaction.amount
 | 
			
		||||
        transaction.comment = request.transaction.comment
 | 
			
		||||
        transaction.spent_date = request.transaction.spent_date
 | 
			
		||||
        self.session.add(transaction)
 | 
			
		||||
        await self.add_tags(transaction, request.transaction.tags)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return UpdateTransactionResponse(ok=True, message='Запись успешно изменена')
 | 
			
		||||
 | 
			
		||||
    async def delete_transaction(self, expense_id) -> DeleteTransactionResponse:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            delete(Transaction)
 | 
			
		||||
            .where(Transaction.id == expense_id)
 | 
			
		||||
        )
 | 
			
		||||
        await self.session.execute(stmt)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return DeleteTransactionResponse(ok=True, message='Запись успешно удалена')
 | 
			
		||||
 | 
			
		||||
    async def get_all_tags(self) -> GetAllTransactionTagsResponse:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(TransactionTag)
 | 
			
		||||
            .order_by(TransactionTag.id)
 | 
			
		||||
        )
 | 
			
		||||
        tags = await self.session.execute(stmt)
 | 
			
		||||
        return GetAllTransactionTagsResponse(tags=tags.scalars().all())
 | 
			
		||||
 | 
			
		||||
    async def get_tags(self, is_income: bool) -> GetTransactionTagsResponse:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(TransactionTag)
 | 
			
		||||
            .where(TransactionTag.is_income == is_income)
 | 
			
		||||
            .order_by(TransactionTag.id)
 | 
			
		||||
        )
 | 
			
		||||
        tags = await self.session.execute(stmt)
 | 
			
		||||
        return GetTransactionTagsResponse(tags=tags.scalars().all())
 | 
			
		||||
 | 
			
		||||
    async def get_tag_by_id(self, tag_id: int) -> Optional[TransactionTag]:
 | 
			
		||||
        return await self.session.get(TransactionTag, tag_id)
 | 
			
		||||
 | 
			
		||||
    async def get_tag_by_name(self, tag_name: str, is_income: bool) -> Optional[TransactionTag]:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(TransactionTag)
 | 
			
		||||
            .where(and_(TransactionTag.name == tag_name, TransactionTag.is_income == is_income))
 | 
			
		||||
        )
 | 
			
		||||
        tag = await self.session.scalar(stmt)
 | 
			
		||||
        return tag
 | 
			
		||||
 | 
			
		||||
    async def create_tag(self, request: CreateTransactionTagRequest) -> CreateTransactionTagResponse:
 | 
			
		||||
        tag = await self.get_tag_by_name(request.tag.name, request.tag.is_income)
 | 
			
		||||
        if tag:
 | 
			
		||||
            return CreateTransactionTagResponse(ok=False, message='Такой тег уже есть.')
 | 
			
		||||
 | 
			
		||||
        tag_dict = request.tag.model_dump()
 | 
			
		||||
        stmt = (
 | 
			
		||||
            insert(TransactionTag)
 | 
			
		||||
            .values(**tag_dict)
 | 
			
		||||
        )
 | 
			
		||||
        await self.session.execute(stmt)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return CreateTransactionTagResponse(ok=True, message='Тег успешно создан.')
 | 
			
		||||
 | 
			
		||||
    async def update_tag(self, request: UpdateTransactionTagRequest) -> UpdateTransactionTagResponse:
 | 
			
		||||
        tag = await self.get_tag_by_name(request.tag.name, request.tag.is_income)
 | 
			
		||||
        if tag:
 | 
			
		||||
            return UpdateTransactionTagResponse(ok=False, message='Тег с таким названием уже есть.')
 | 
			
		||||
 | 
			
		||||
        tag = await self.get_tag_by_id(request.tag.id)
 | 
			
		||||
        if not tag:
 | 
			
		||||
            return UpdateTransactionTagResponse(ok=False, message='Тег не найден.')
 | 
			
		||||
 | 
			
		||||
        tag_dict = request.tag.model_dump()
 | 
			
		||||
        del tag_dict['id']
 | 
			
		||||
        stmt = (
 | 
			
		||||
            update(TransactionTag)
 | 
			
		||||
            .where(TransactionTag.id == request.tag.id)
 | 
			
		||||
            .values(**tag_dict)
 | 
			
		||||
        )
 | 
			
		||||
        await self.session.execute(stmt)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return UpdateTransactionTagResponse(ok=True, message='Тег успешно изменен.')
 | 
			
		||||
 | 
			
		||||
    async def delete_tag(self, tag_id: int) -> DeleteTransactionTagResponse:
 | 
			
		||||
        tag = await self.get_tag_by_id(tag_id)
 | 
			
		||||
        if not tag:
 | 
			
		||||
            return DeleteTransactionTagResponse(ok=False, message='Тег не найден.')
 | 
			
		||||
        if len(tag.transactions) > 0:
 | 
			
		||||
            return DeleteTransactionTagResponse(ok=False, message='Тег прикреплен к записи о расходах/доходах.')
 | 
			
		||||
 | 
			
		||||
        stmt = (
 | 
			
		||||
            delete(TransactionTag)
 | 
			
		||||
            .where(TransactionTag.id == tag_id)
 | 
			
		||||
        )
 | 
			
		||||
        await self.session.execute(stmt)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return DeleteTransactionTagResponse(ok=True, message='Тег удален')
 | 
			
		||||
		Reference in New Issue
	
	Block a user