Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/statistics #22

Merged
merged 12 commits into from
Aug 5, 2023
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ SQLAlchemy==2.0.4
SQLAlchemy-Utils==0.40.0
starlette==0.25.0
uvicorn==0.20.0
python-dateutil~=2.8.2
84 changes: 81 additions & 3 deletions src/routers/user.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from typing import List
from typing import List, Optional

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from starlette import status
from starlette.exceptions import HTTPException

import services
from database import get_db
from dependencies import get_current_user
from dependencies import (
get_current_user,
transform_date_or_422,
transform_exact_date_or_422,
)
from models import User
from schemas import UserBalance, UserModel
from schemas import UserBalance, UserModel, UserTotalExpenses, UserTotalReplenishments

router = APIRouter(
prefix="/users",
Expand All @@ -34,3 +40,75 @@ def read_user_info(
current_user: User = Depends(get_current_user),
) -> UserModel:
return db.query(User).filter_by(id=current_user.id).one()


@router.get("/total-expenses/", response_model=UserTotalExpenses)
def read_user_total_expenses(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
year_month: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
) -> UserTotalExpenses:
if year_month and (start_date or end_date):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Cannot use filter_date with start_date or end_date",
)
elif (start_date and not end_date) or (end_date and not start_date):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Both start_date and end_date are required",
)
elif year_month:
filter_date = transform_date_or_422(year_month)
return services.user_total_expenses(
db, current_user.id, filter_date=filter_date
)
elif start_date and end_date:
start_date = transform_exact_date_or_422(start_date)
end_date = transform_exact_date_or_422(end_date)
return services.user_total_expenses(
db,
current_user.id,
start_date=start_date,
end_date=end_date,
)
else:
return services.user_total_expenses(db, current_user.id)


@router.get("/total-replenishments/", response_model=UserTotalReplenishments)
def read_user_total_expenses(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
year_month: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
) -> UserTotalReplenishments:
if year_month and (start_date or end_date):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Cannot use filter_date with start_date or end_date",
)
elif (start_date and not end_date) or (end_date and not start_date):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Both start_date and end_date are required",
)
elif year_month:
filter_date = transform_date_or_422(year_month)
return services.user_total_replenishments(
db, current_user.id, filter_date=filter_date
)
elif start_date and end_date:
start_date = transform_exact_date_or_422(start_date)
end_date = transform_exact_date_or_422(end_date)
return services.user_total_replenishments(
db,
current_user.id,
start_date=start_date,
end_date=end_date,
)
else:
return services.user_total_replenishments(db, current_user.id)
2 changes: 1 addition & 1 deletion src/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .user import BaseUser, UserModel
from .user import BaseUser, UserModel, UserTotalExpenses, UserTotalReplenishments
from .group import (
AboutCategory,
AboutUser,
Expand Down
10 changes: 10 additions & 0 deletions src/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,13 @@ class UserModel(BaseUser):
first_name: str
last_name: str
picture: Optional[str]


class UserTotalExpenses(BaseModel):
amount: float
percentage_increase: float


class UserTotalReplenishments(BaseModel):
amount: float
percentage_increase: float
7 changes: 6 additions & 1 deletion src/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@
update_replenishment,
delete_replenishment,
)
from .user import get_user, calculate_user_balance
from .user import (
get_user,
calculate_user_balance,
user_total_expenses,
user_total_replenishments,
)
155 changes: 153 additions & 2 deletions src/services/user.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from typing import Optional
from typing import Optional, Union
from dateutil.relativedelta import relativedelta

from starlette import status
from starlette.exceptions import HTTPException
from sqlalchemy.orm import Session
from sqlalchemy.sql.functions import coalesce, sum
from sqlalchemy import and_, exc, extract
from pydantic.schema import date

from models import Expense, Replenishment, User
from schemas import UserBalance
from schemas import UserBalance, UserTotalExpenses, UserTotalReplenishments


def get_user(db: Session, login: str) -> Optional[User]:
Expand All @@ -22,3 +27,149 @@ def calculate_user_balance(db: Session, user_id: int) -> UserBalance:
user_balance = replenishments - expenses
user_balance = UserBalance(balance=user_balance)
return user_balance


def get_total_actions_for_month(
db: Session,
user_id: int,
year: date,
month: date,
model: Union[Expense, Replenishment],
) -> float:
return db.query(
coalesce(
sum(model.amount).filter(
and_(
model.user_id == user_id,
extract("year", model.time) == year,
extract("month", model.time) == month,
)
),
0,
)
).one()[0]


def get_total_actions_for_time_range(
db: Session,
user_id: int,
start_date: date,
end_date: date,
model: Union[Expense, Replenishment],
) -> float:
return db.query(
coalesce(
sum(model.amount).filter(
and_(
model.user_id == user_id,
model.time >= start_date,
model.time <= end_date,
)
),
0,
)
).one()[0]


def user_total_expenses(
db: Session,
user_id: int,
filter_date: Optional[date] = None,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
) -> UserTotalExpenses:
if filter_date and start_date or filter_date and end_date:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Too many arguments! It is necessary to select either a month or a start date and an end date!",
)
(amount,) = db.query(
coalesce(sum(Expense.amount).filter(Expense.user_id == user_id), 0)
).one()
percentage_increase = 0

if filter_date:
year, month = filter_date.year, filter_date.month
amount = get_total_actions_for_month(db, user_id, year, month, Expense)

filter_date = filter_date - relativedelta(months=1)
previous_year, previous_month = filter_date.year, filter_date.month
previous_month_amount = get_total_actions_for_month(
db, user_id, previous_year, previous_month, Expense
)

if previous_month_amount != 0:
percentage_increase = (
amount - previous_month_amount
) / previous_month_amount

elif start_date and end_date:
amount = get_total_actions_for_time_range(
db, user_id, start_date, end_date, Expense
)

days_difference = (end_date - start_date).days
zero_date = start_date - relativedelta(days=days_difference)
previous_days_amount = get_total_actions_for_time_range(
db, user_id, zero_date, start_date, Expense
)

if previous_days_amount != 0:
percentage_increase = (amount - previous_days_amount) / previous_days_amount

total_expenses = UserTotalExpenses(
amount=amount, percentage_increase=percentage_increase
)
return total_expenses


def user_total_replenishments(
db: Session,
user_id: int,
filter_date: Optional[date] = None,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
) -> UserTotalReplenishments:
if filter_date and start_date or filter_date and end_date:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Too many arguments! It is necessary to select either a month or a start date and an end date!",
)
(amount,) = db.query(
coalesce(sum(Replenishment.amount).filter(Replenishment.user_id == user_id), 0)
).one()
percentage_increase = 0

if filter_date:
year, month = filter_date.year, filter_date.month
amount = get_total_actions_for_month(db, user_id, year, month, Replenishment)

filter_date = filter_date - relativedelta(months=1)
previous_year, previous_month = filter_date.year, filter_date.month
previous_month_amount = get_total_actions_for_month(
db, user_id, previous_year, previous_month, Replenishment
)

if previous_month_amount != 0:
percentage_increase = (
amount - previous_month_amount
) / previous_month_amount

elif start_date and end_date:
amount = get_total_actions_for_time_range(
db, user_id, start_date, end_date, Replenishment
)

days_difference = (end_date - start_date).days
zero_date = start_date - relativedelta(days=days_difference)
previous_days_amount = get_total_actions_for_time_range(
db, user_id, zero_date, start_date, Replenishment
)

if previous_days_amount != 0:
percentage_increase = (amount - previous_days_amount) / previous_days_amount

total_replenishments = UserTotalReplenishments(
amount=amount, percentage_increase=percentage_increase
)
return total_replenishments
Loading
Loading