From c61976572f76a65ecb676d2fc0c42d1a84aa6eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=A0=D1=94=D0=B7?= =?UTF-8?q?=D1=94=D0=BD=D0=BA=D0=BE=D0=B2?= <108422398+RezenkovD@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:21:13 +0200 Subject: [PATCH] feature: update expense options (#37) * feat: add ExpenseUpdate schema * test: update and add new tests for expenses * feat: update expenses options and ref code validation * feat: add schema CategoriesGroupDetail * fix: add field categories_group to CategoriesGroupDetail * feat: add categories group detail endpoint --- src/routers/category.py | 19 +++- src/routers/expense.py | 4 +- src/schemas/__init__.py | 3 +- src/schemas/expense.py | 5 + src/schemas/group.py | 8 ++ src/services/__init__.py | 1 + src/services/expense.py | 124 ++++++++++++++++--------- src/services/group.py | 20 ++++ tests/test_endpoints/test_expense_e.py | 52 ++++++++++- tests/test_services/test_expense_s.py | 12 ++- 10 files changed, 192 insertions(+), 56 deletions(-) diff --git a/src/routers/category.py b/src/routers/category.py index 3bce178..fb6e438 100644 --- a/src/routers/category.py +++ b/src/routers/category.py @@ -1,3 +1,5 @@ +from typing import List + from fastapi import APIRouter, Depends from sqlalchemy.orm import Session @@ -5,7 +7,13 @@ from database import get_db from dependencies import get_current_user from models import User -from schemas import CategoryModel, CategoryCreate, IconColor, CategoriesGroup +from schemas import ( + CategoryModel, + CategoryCreate, + IconColor, + CategoriesGroup, + CategoriesGroupDetail, +) router = APIRouter( prefix="/groups", @@ -13,6 +21,15 @@ ) +@router.get("/categories/", response_model=List[CategoriesGroupDetail]) +def read_categories_group_detail( + *, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +) -> List[CategoriesGroupDetail]: + return services.read_categories_group_detail(db, current_user.id) + + @router.get("/{group_id}/categories/", response_model=CategoriesGroup) def read_categories_group( *, diff --git a/src/routers/expense.py b/src/routers/expense.py index 631067f..d906ae0 100644 --- a/src/routers/expense.py +++ b/src/routers/expense.py @@ -16,7 +16,7 @@ Page, ) from models import User -from schemas import ExpenseCreate, ExpenseModel, UserExpense +from schemas import ExpenseCreate, ExpenseModel, UserExpense, ExpenseUpdate router = APIRouter( prefix="/groups", @@ -41,7 +41,7 @@ def update_expense( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), group_id: int, - expense: ExpenseCreate, + expense: ExpenseUpdate, expense_id: int, ) -> ExpenseModel: return services.update_expense(db, current_user.id, group_id, expense, expense_id) diff --git a/src/schemas/__init__.py b/src/schemas/__init__.py index 4783e02..a59b541 100644 --- a/src/schemas/__init__.py +++ b/src/schemas/__init__.py @@ -27,10 +27,11 @@ UserSpender, GroupDailyExpenses, GroupDailyExpensesDetail, + CategoriesGroupDetail, ) from .invintation import BaseInvitation, InvitationCreate, InvitationModel from .category import CategoryModel, CategoryCreate, IconColor -from .expense import ExpenseCreate, ExpenseModel, UserExpense +from .expense import ExpenseCreate, ExpenseUpdate, ExpenseModel, UserExpense from .replenishment import ( ReplenishmentCreate, UserBalance, diff --git a/src/schemas/expense.py b/src/schemas/expense.py index f2bf351..807d122 100644 --- a/src/schemas/expense.py +++ b/src/schemas/expense.py @@ -10,6 +10,11 @@ class ExpenseCreate(BaseModel): category_id: int +class ExpenseUpdate(ExpenseCreate): + group_id: int + time: datetime.datetime + + class CategoryGroup(BaseModel): group: ShortGroup category: CategoryModel diff --git a/src/schemas/group.py b/src/schemas/group.py index cfbe755..c9b7d49 100644 --- a/src/schemas/group.py +++ b/src/schemas/group.py @@ -63,6 +63,14 @@ class CategoriesGroup(BaseModel): categories_group: List[AboutCategory] +class CategoriesGroupDetail(BaseModel): + id: int + title: str + icon_url: str + color_code: str + categories_group: List[AboutCategory] + + class GroupInfo(GroupModel): members: int expenses: int diff --git a/src/services/__init__.py b/src/services/__init__.py index 0bd39d3..30cde15 100644 --- a/src/services/__init__.py +++ b/src/services/__init__.py @@ -38,6 +38,7 @@ read_group_member_daily_expenses, read_group_member_daily_expenses_detail, read_group_member_history, + read_categories_group_detail, ) from .invitation import create_invitation, read_invitations, response_invitation from .replenishment import ( diff --git a/src/services/expense.py b/src/services/expense.py index 835b1b2..716894c 100644 --- a/src/services/expense.py +++ b/src/services/expense.py @@ -1,5 +1,5 @@ import datetime -from typing import List, Optional +from typing import List, Optional, Union from pydantic.schema import date from sqlalchemy import and_, exc, extract @@ -10,59 +10,100 @@ from models import CategoryGroup, Expense, UserGroup from enums import GroupStatusEnum -from schemas import ExpenseCreate, ExpenseModel, UserExpense +from schemas import ExpenseCreate, ExpenseModel, UserExpense, ExpenseUpdate -def validate_input_data( +def validate_user_group(db: Session, user_id: int, group_id: int) -> UserGroup: + try: + db_user_group = ( + db.query(UserGroup).filter_by(group_id=group_id, user_id=user_id).one() + ) + except exc.NoResultFound: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="You are not a user of this group!", + ) + return db_user_group + + +def validate_expense( + db: Session, + user_id: int, + group_id: int, + expense_id: int, +) -> None: + try: + db.query(Expense).filter_by(id=expense_id, user_id=user_id).one() + except exc.NoResultFound: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="It's not your expense!", + ) + try: + db.query(Expense).filter_by( + id=expense_id, user_id=user_id, group_id=group_id + ).one() + except exc.NoResultFound: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="The expense does not belong to this group!", + ) + + +def validate_expense_update( db: Session, user_id: int, group_id: int, - expense: ExpenseCreate = None, - expense_id: int = None, - is_create: bool = False, + expense: ExpenseUpdate, ) -> None: try: db_user_group = ( - db.query(UserGroup).filter_by(group_id=group_id, user_id=user_id).one() + db.query(UserGroup) + .filter_by(group_id=expense.group_id, user_id=user_id) + .one() ) except exc.NoResultFound: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="You are not a user of this group!", + detail="You are not a user of the group specified to update expenses!", ) - if is_create: - if db_user_group.status == GroupStatusEnum.INACTIVE: + if db_user_group.status == GroupStatusEnum.INACTIVE: + if group_id != expense.group_id: raise HTTPException( status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="The user is not active in this group!", - ) - if expense: - try: - db.query(CategoryGroup).filter_by( - category_id=expense.category_id, - group_id=group_id, - ).one() - except exc.NoResultFound: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="The group does not have such a category!", - ) - if expense_id: - try: - db.query(Expense).filter_by(id=expense_id, user_id=user_id).one() - except exc.NoResultFound: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="It's not your expense!", + detail="The user is not active in group specified to update expenses!", ) + try: + db.query(CategoryGroup).filter_by( + category_id=expense.category_id, + group_id=expense.group_id, + ).one() + except exc.NoResultFound: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="The group specified to update expenses does not contain this category!", + ) def create_expense( db: Session, user_id: int, group_id: int, expense: ExpenseCreate ) -> ExpenseModel: - validate_input_data( - db=db, user_id=user_id, group_id=group_id, expense=expense, is_create=True - ) + db_user_group = validate_user_group(db=db, user_id=user_id, group_id=group_id) + if db_user_group.status == GroupStatusEnum.INACTIVE: + raise HTTPException( + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, + detail="The user is not active in this group!", + ) + try: + db.query(CategoryGroup).filter_by( + category_id=expense.category_id, + group_id=group_id, + ).one() + except exc.NoResultFound: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="The group does not have such a category!", + ) db_expense = Expense(**expense.dict()) db_expense.user_id = user_id db_expense.group_id = group_id @@ -80,15 +121,11 @@ def create_expense( def update_expense( - db: Session, user_id: int, group_id: int, expense: ExpenseCreate, expense_id: int + db: Session, user_id: int, group_id: int, expense: ExpenseUpdate, expense_id: int ) -> ExpenseModel: - validate_input_data( - db=db, - user_id=user_id, - group_id=group_id, - expense=expense, - expense_id=expense_id, - ) + validate_user_group(db=db, user_id=user_id, group_id=group_id) + validate_expense(db=db, user_id=user_id, group_id=group_id, expense_id=expense_id) + validate_expense_update(db=db, user_id=user_id, group_id=group_id, expense=expense) db.query(Expense).filter_by(id=expense_id).update(values={**expense.dict()}) db_expense = db.query(Expense).filter_by(id=expense_id).one() try: @@ -103,9 +140,8 @@ def update_expense( def delete_expense(db: Session, user_id: int, group_id: int, expense_id: int) -> None: - validate_input_data( - db=db, user_id=user_id, group_id=group_id, expense_id=expense_id - ) + validate_user_group(db=db, user_id=user_id, group_id=group_id) + validate_expense(db=db, user_id=user_id, group_id=group_id, expense_id=expense_id) db.query(Expense).filter_by(id=expense_id).delete() try: db.commit() diff --git a/src/services/group.py b/src/services/group.py index 389791c..1dd94a2 100644 --- a/src/services/group.py +++ b/src/services/group.py @@ -29,6 +29,7 @@ GroupMember, UserDailyExpenses, UserDailyExpensesDetail, + CategoriesGroupDetail, ) from enums import GroupStatusEnum @@ -363,6 +364,25 @@ def read_user_groups(db: Session, user_id: int) -> UserGroups: return db_query +def read_categories_group_detail( + db: Session, user_id: int +) -> List[CategoriesGroupDetail]: + db_query = ( + db.query(Group) + .options(joinedload(Group.categories_group)) + .join(UserGroup) + .filter( + and_( + UserGroup.group_id == Group.id, + UserGroup.user_id == user_id, + UserGroup.status == GroupStatusEnum.ACTIVE, + ) + ) + .all() + ) + return db_query + + def read_categories_group(db: Session, user_id: int, group_id: int) -> CategoriesGroup: try: db.query(UserGroup).filter_by( diff --git a/tests/test_endpoints/test_expense_e.py b/tests/test_endpoints/test_expense_e.py index 7d34989..21a90e2 100644 --- a/tests/test_endpoints/test_expense_e.py +++ b/tests/test_endpoints/test_expense_e.py @@ -97,7 +97,9 @@ def test_update_expense(self) -> None: json={ "descriptions": date_update_expense.descriptions, "amount": date_update_expense.amount, - "category_id": date_update_expense.category_id, + "category_id": self.category.id, + "group_id": self.second_group.id, + "time": "2018-08-03T10:51:42.990", }, ) expense_data = { @@ -107,9 +109,9 @@ def test_update_expense(self) -> None: "time": data.json()["time"], "category_group": { "group": { - "id": self.first_group.id, - "title": self.first_group.title, - "color_code": self.first_group.color_code, + "id": self.second_group.id, + "title": self.second_group.title, + "color_code": self.second_group.color_code, }, "category": {"title": self.category.title, "id": self.category.id}, "icon_url": self.icon_url, @@ -120,6 +122,48 @@ def test_update_expense(self) -> None: assert data.status_code == 200 assert data.json() == expense_data + def test_update_expense_on_other_group(self) -> None: + expense = ExpenseFactory( + user_id=self.user.id, + group_id=self.first_group.id, + category_id=self.category.id, + ) + date_update_expense = ExpenseCreate( + descriptions="descriptions", amount=999.9, category_id=self.category.id + ) + data = client.put( + f"/groups/{self.first_group.id}/expenses/{expense.id}/", + json={ + "descriptions": date_update_expense.descriptions, + "amount": date_update_expense.amount, + "category_id": date_update_expense.category_id, + "group_id": 99999, + "time": "2018-08-03T10:51:42.990", + }, + ) + assert data.status_code == 404 + + def test_update_expense_on_other_group_category(self) -> None: + expense = ExpenseFactory( + user_id=self.user.id, + group_id=self.first_group.id, + category_id=self.category.id, + ) + date_update_expense = ExpenseCreate( + descriptions="descriptions", amount=999.9, category_id=self.category.id + ) + data = client.put( + f"/groups/{self.first_group.id}/expenses/{expense.id}/", + json={ + "descriptions": date_update_expense.descriptions, + "amount": date_update_expense.amount, + "category_id": 9999, + "group_id": self.first_group.id, + "time": "2018-08-03T10:51:42.990", + }, + ) + assert data.status_code == 404 + def test_delete_expense(self) -> None: expense = ExpenseFactory( user_id=self.user.id, diff --git a/tests/test_services/test_expense_s.py b/tests/test_services/test_expense_s.py index 6a56085..3fbe0fa 100644 --- a/tests/test_services/test_expense_s.py +++ b/tests/test_services/test_expense_s.py @@ -5,7 +5,7 @@ from models import Expense from enums import GroupStatusEnum -from schemas import ExpenseCreate +from schemas import ExpenseCreate, ExpenseUpdate from services import create_expense from services.expense import update_expense, delete_expense, read_expenses from tests.factories import UserGroupFactory @@ -35,8 +35,12 @@ def test_create_expense(session, dependence_factory, activity) -> None: def test_update_expense(session, dependence_factory, activity) -> None: factories = dependence_factory activity = activity - date_update_expense = ExpenseCreate( - descriptions="descriptions", amount=999.9, category_id=activity["category"].id + date_update_expense = ExpenseUpdate( + descriptions="descriptions", + amount=999.9, + category_id=activity["category"].id, + group_id=dependence_factory["first_group"].id, + time=activity["first_expense"].time, ) data = update_expense( session, @@ -50,7 +54,7 @@ def test_update_expense(session, dependence_factory, activity) -> None: assert data.time == activity["first_expense"].time assert data.user.id == factories["first_user"].id assert data.category_group.category.id == activity["category"].id - assert data.category_group.group.id == factories["first_group"].id + assert data.category_group.group.id == dependence_factory["first_group"].id def test_delete_expense(session, dependence_factory, activity) -> None: