From 8346e1a2a9daca9b35bf01206a3d27b359777ad7 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Date: Thu, 8 Aug 2024 15:48:37 +0100 Subject: [PATCH 1/2] chore: added route tag for documentation --- api/v1/routes/statistics.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/api/v1/routes/statistics.py b/api/v1/routes/statistics.py index 08fe72ecb..97d68f962 100644 --- a/api/v1/routes/statistics.py +++ b/api/v1/routes/statistics.py @@ -7,7 +7,7 @@ from api.v1.services.user import oauth2_scheme from api.v1.services.analytics import analytics_service, AnalyticsServices -statistics = APIRouter(prefix='/statistics') +statistics = APIRouter(prefix='/statistics', tags=['Statistics']) def get_current_month_date_range(): @@ -18,18 +18,14 @@ def get_current_month_date_range(): return start_date, end_date - - - @statistics.get('', status_code=status.HTTP_200_OK) async def get_analytics_summary( - token: Annotated[str, Depends(oauth2_scheme)], + token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[Session, Depends(get_db)], analytics_service: Annotated[AnalyticsServices, Depends()], start_date: datetime = None, end_date: datetime = None ): - """ Retrieves analytics summary data for an organization or super admin. Args: From 4c1fc8149efc5d7ba34ae9d184fb463ec840663d Mon Sep 17 00:00:00 2001 From: Oluwanifemi Date: Thu, 8 Aug 2024 17:27:12 +0100 Subject: [PATCH 2/2] fix: updated statistics route to appropriate standard --- api/v1/routes/__init__.py | 2 - api/v1/routes/dashboard.py | 44 ++++++++++++++++-- api/v1/routes/statistics.py | 41 ----------------- api/v1/schemas/analytics.py | 33 ++++++++++---- api/v1/services/analytics.py | 48 ++++++++++++++------ tests/v1/analytics/test_analytics_summary.py | 45 +++++++++--------- 6 files changed, 120 insertions(+), 93 deletions(-) delete mode 100644 api/v1/routes/statistics.py diff --git a/api/v1/routes/__init__.py b/api/v1/routes/__init__.py index 6643adb7b..f3d9083d6 100644 --- a/api/v1/routes/__init__.py +++ b/api/v1/routes/__init__.py @@ -43,7 +43,6 @@ from api.v1.routes.privacy import privacies from api.v1.routes.settings import settings from api.v1.routes.terms_and_conditions import terms_and_conditions -from api.v1.routes.statistics import statistics api_version_one = APIRouter(prefix="/api/v1") @@ -89,4 +88,3 @@ api_version_one.include_router(team) api_version_one.include_router(terms_and_conditions) api_version_one.include_router(product_comment) -api_version_one.include_router(statistics) diff --git a/api/v1/routes/dashboard.py b/api/v1/routes/dashboard.py index 728e6d558..43dc4d65c 100644 --- a/api/v1/routes/dashboard.py +++ b/api/v1/routes/dashboard.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, status from api.db.database import get_db from sqlalchemy.orm import Session @@ -11,11 +11,24 @@ DashboardSingleProductResponse, DashboardProductListResponse ) +from typing import Annotated +from fastapi.security import OAuth2 +from datetime import datetime, timedelta +from api.v1.services.user import oauth2_scheme +from api.v1.services.analytics import analytics_service, AnalyticsServices dashboard = APIRouter(prefix="/dashboard", tags=['Dashboard']) +def get_current_month_date_range(): + now = datetime.utcnow() + start_date = datetime(now.year, now.month, 1) + end_date = (start_date + timedelta(days=32) + ).replace(day=1) - timedelta(seconds=1) + return start_date, end_date + + @dashboard.get("/products/count", response_model=DashboardProductCountResponse) async def get_products_count( db: Session = Depends(get_db), @@ -28,7 +41,7 @@ async def get_products_count( message="Products count fetched successfully", data={"count": len(products)} ) - + @dashboard.get("/products", response_model=DashboardProductListResponse) async def get_products( @@ -56,7 +69,7 @@ async def get_products( message="Products fetched successfully", data=payment_data ) - + @dashboard.get("/products/{product_id}", response_model=DashboardSingleProductResponse) async def get_product( @@ -79,4 +92,27 @@ async def get_product( "archived": prod.archived, "created_at": prod.created_at.isoformat(), } - ) \ No newline at end of file + ) + + +@dashboard.get('/statistics', status_code=status.HTTP_200_OK) +async def get_analytics_summary( + token: Annotated[str, Depends(oauth2_scheme)], + db: Annotated[Session, Depends(get_db)], + analytics_service: Annotated[AnalyticsServices, Depends()], + start_date: datetime = None, + end_date: datetime = None +): + """ + Retrieves analytics summary data for an organization or super admin. + Args: + token: access_token + db: database Session object + start_date: start date for filtering + end_date: end date for filtering + Returns: + analytics response: contains the analytics summary data + """ + if not start_date or not end_date: + start_date, end_date = get_current_month_date_range() + return analytics_service.get_analytics_summary(token=token, db=db, start_date=start_date, end_date=end_date) diff --git a/api/v1/routes/statistics.py b/api/v1/routes/statistics.py deleted file mode 100644 index 97d68f962..000000000 --- a/api/v1/routes/statistics.py +++ /dev/null @@ -1,41 +0,0 @@ -from fastapi import status, Depends, APIRouter -from typing import Annotated -from sqlalchemy.orm import Session -from fastapi.security import OAuth2 -from datetime import datetime, timedelta -from api.db.database import get_db -from api.v1.services.user import oauth2_scheme -from api.v1.services.analytics import analytics_service, AnalyticsServices - -statistics = APIRouter(prefix='/statistics', tags=['Statistics']) - - -def get_current_month_date_range(): - now = datetime.utcnow() - start_date = datetime(now.year, now.month, 1) - end_date = (start_date + timedelta(days=32) - ).replace(day=1) - timedelta(seconds=1) - return start_date, end_date - - -@statistics.get('', status_code=status.HTTP_200_OK) -async def get_analytics_summary( - token: Annotated[str, Depends(oauth2_scheme)], - db: Annotated[Session, Depends(get_db)], - analytics_service: Annotated[AnalyticsServices, Depends()], - start_date: datetime = None, - end_date: datetime = None -): - """ - Retrieves analytics summary data for an organization or super admin. - Args: - token: access_token - db: database Session object - start_date: start date for filtering - end_date: end date for filtering - Returns: - analytics response: contains the analytics summary data - """ - if not start_date or not end_date: - start_date, end_date = get_current_month_date_range() - return analytics_service.get_analytics_summary(token=token, db=db, start_date=start_date, end_date=end_date) diff --git a/api/v1/schemas/analytics.py b/api/v1/schemas/analytics.py index b757696d5..17f887b03 100644 --- a/api/v1/schemas/analytics.py +++ b/api/v1/schemas/analytics.py @@ -12,18 +12,35 @@ class AnalyticsChartsResponse(BaseModel): data: Dict +class Metrics(BaseModel): + """ + Base schema for metrics with current and previous month values and percentage difference. + """ + current_month: Union[float, int] + previous_month: Union[float, int] + percentage_difference: str + + +class ActiveUsersMetrics(BaseModel): + """ + Schema for active users metrics with the current value and the difference from an hour ago. + """ + current: int + difference_an_hour_ago: int + + class SuperAdminMetrics(BaseModel): - total_revenue: Dict[str, Union[float, str]] - total_products: Dict[str, Union[int, str]] - total_users: Dict[str, Union[int, str]] - lifetime_sales: Dict[str, Union[float, str]] + total_revenue: Metrics + total_products: Metrics + total_users: Metrics + lifetime_sales: Metrics class UserMetrics(BaseModel): - total_revenue: Dict[str, Union[float, str]] - subscriptions: Dict[str, Union[int, str]] - sales: Dict[str, Union[int, str]] - active_now: Dict[str, Union[int, str]] + revenue: Metrics + subscriptions: Metrics + orders: Metrics + active_users: ActiveUsersMetrics class AnalyticsSummaryResponse(BaseModel): diff --git a/api/v1/services/analytics.py b/api/v1/services/analytics.py index 4a39200a4..5645e64b3 100644 --- a/api/v1/services/analytics.py +++ b/api/v1/services/analytics.py @@ -143,16 +143,37 @@ def get_analytics_summary(self, token: Annotated[OAuth2, Depends(oauth2_scheme)] if user.is_super_admin: data = self.get_summary_data_super_admin(db, start_date, end_date) - message = "Dashboard statistics retrieved successfully" + message = "Admin Statistics Fetched" else: - user_organization = (db.query(user_organization_association) - .filter_by(user_id=user.id).first()) + user_organization = db.query( + user_organization_association).filter_by(user_id=user.id).first() if not user_organization: - raise HTTPException( - status_code=403, detail="User is not part of any organization") - data = self.get_summary_data_organization( - db, user_organization.organization_id, start_date, end_date) - message = "Dashboard statistics retrieved successfully" + data = { + "revenue": { + "current_month": 0, + "previous_month": 0, + "percentage_difference": "0.00%" + }, + "subscriptions": { + "current_month": 0, + "previous_month": 0, + "percentage_difference": "0.00%" + }, + "orders": { + "current_month": 0, + "previous_month": 0, + "percentage_difference": "0.00%" + }, + "active_users": { + "current": 0, + "difference_an_hour_ago": 0 + } + } + message = "User is not part of any organization" + else: + data = self.get_summary_data_organization( + db, user_organization.organization_id, start_date, end_date) + message = "User Statistics Fetched" return AnalyticsSummaryResponse( message=message, @@ -232,7 +253,7 @@ def get_summary_data_organization(self, db: Session, org_id: str, start_date: da )).scalar() or 0 return { - "total_revenue": { + "revenue": { "current_month": total_revenue, "previous_month": last_month_revenue, "percentage_difference": f"{self.calculate_percentage_increase(last_month_revenue, total_revenue)}%" @@ -242,15 +263,14 @@ def get_summary_data_organization(self, db: Session, org_id: str, start_date: da "previous_month": last_month_subscriptions, "percentage_difference": f"{self.calculate_percentage_increase(last_month_subscriptions, subscriptions)}%" }, - "sales": { + "orders": { "current_month": sales, "previous_month": last_month_sales, "percentage_difference": f"{self.calculate_percentage_increase(last_month_sales, sales)}%" }, - "active_now": { - "current_hour": active_now, - "previous_hour": active_previous_hour, - "percentage_difference": f"{self.calculate_percentage_increase(active_previous_hour, active_now)}%" + "active_users": { + "current": active_now, + "difference_an_hour_ago": active_now - active_previous_hour } } diff --git a/tests/v1/analytics/test_analytics_summary.py b/tests/v1/analytics/test_analytics_summary.py index d26ec46af..d9237d1dc 100644 --- a/tests/v1/analytics/test_analytics_summary.py +++ b/tests/v1/analytics/test_analytics_summary.py @@ -2,7 +2,7 @@ from fastapi.testclient import TestClient from unittest.mock import MagicMock, patch from datetime import datetime, timedelta -from api.v1.routes.statistics import get_analytics_summary +from api.v1.routes.dashboard import get_analytics_summary from api.v1.services.analytics import AnalyticsServices from api.v1.schemas.analytics import AnalyticsSummaryResponse from main import app @@ -48,7 +48,7 @@ def mock_db_session(): def test_statistics_summary_super_admin(mock_analytics_service, mock_oauth2_scheme, mock_get_current_user_super_admin, mock_db_session): expected_response = AnalyticsSummaryResponse( - message="Dashboard statistics retrieved successfully", + message="Admin Statistics Fetched", status='success', status_code=200, data={ @@ -80,22 +80,22 @@ def test_statistics_summary_super_admin(mock_analytics_service, mock_oauth2_sche end_date = datetime.utcnow() response = client.get( - "/api/v1/statistics", + "/api/v1/dashboard/statistics", headers={"Authorization": f"Bearer {token}"}, - params={"start_date": "2024-07-09T00:00:00", "end_date": "2024-08-08T00:00:00"} + params={"start_date": "2024-07-09T00:00:00", + "end_date": "2024-08-08T00:00:00"} ) assert response.status_code == 200 - def test_statistics_summary_user(mock_analytics_service, mock_oauth2_scheme, mock_get_current_user_user, mock_db_session): expected_response = AnalyticsSummaryResponse( - message="Dashboard statistics retrieved successfully", + message="User Statistics Fetched", status='success', status_code=200, data={ - "total_revenue": { + "revenue": { "current_month": 5000, "previous_month": 4500, "percentage_difference": "11.11%" @@ -105,15 +105,14 @@ def test_statistics_summary_user(mock_analytics_service, mock_oauth2_scheme, moc "previous_month": 90, "percentage_difference": "11.11%" }, - "sales": { + "orders": { "current_month": 150, "previous_month": 135, "percentage_difference": "11.11%" }, - "active_now": { - "current_hour": 25, - "previous_hour": 22, - "percentage_difference": "13.64%" + "active_users": { + "current": 25, + "difference_an_hour_ago": 13 } } ) @@ -123,22 +122,22 @@ def test_statistics_summary_user(mock_analytics_service, mock_oauth2_scheme, moc end_date = datetime.utcnow() response = client.get( - "/api/v1/statistics", + "/api/v1/dashboard/statistics", headers={"Authorization": f"Bearer {token}"}, - params={"start_date": "2024-07-09T00:00:00", "end_date": "2024-08-08T00:00:00"} + params={"start_date": "2024-07-09T00:00:00", + "end_date": "2024-08-08T00:00:00"} ) assert response.status_code == 200 - def test_statistics_summary_no_dates(mock_analytics_service, mock_oauth2_scheme, mock_get_current_user_user, mock_db_session): expected_response = AnalyticsSummaryResponse( - message="Dashboard statistics retrieved successfully", + message="User Statistics Fetched", status='success', status_code=200, data={ - "total_revenue": { + "revenue": { "current_month": 3000, "previous_month": 2700, "percentage_difference": "11.11%" @@ -148,15 +147,14 @@ def test_statistics_summary_no_dates(mock_analytics_service, mock_oauth2_scheme, "previous_month": 67, "percentage_difference": "11.94%" }, - "sales": { + "orders": { "current_month": 120, "previous_month": 108, "percentage_difference": "11.11%" }, - "active_now": { - "current_hour": 20, - "previous_hour": 18, - "percentage_difference": "11.11%" + "active_users": { + "current": 25, + "difference_an_hour_ago": 11 } } ) @@ -164,9 +162,8 @@ def test_statistics_summary_no_dates(mock_analytics_service, mock_oauth2_scheme, token = "user_token" response = client.get( - "/api/v1/statistics", + "/api/v1/dashboard/statistics", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 -