Skip to content

Commit

Permalink
add API for fetching most frequent ordered product and its tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Horlawhumy-dev committed Jul 26, 2024
1 parent 8e7630e commit f5606d5
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 4 deletions.
16 changes: 16 additions & 0 deletions api_doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,22 @@ Inventory Report Endpoints:
- Description: Get product order sales by certain period
- Request Body: nil
- Auth: Bearer token
- Response:
{
"id": "21c7f528da",
"name": "Product beast",
"description": "Product beast Description",
"quantity": 4,
"price": 100,
"created_at": "2024-07-26T13:22:49.098161+01:00",
"updated_at": "2024-07-26T13:24:23.399251+01:00",
"owner": "Admin"
}

3. GET /api/inventory/report/order/frequent
- Description: Get product ordered frequent with most quantity
- Request Body: nil
- Auth: Bearer token
- Response:
[
{
Expand Down
145 changes: 145 additions & 0 deletions api_doc.txt.save
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
API Descriptions:

This application provides a RESTful API for Drugstoc Inventory Management BE Assessment.

Auth Endpoints:

1. POST /api/users/register/
- Description: Create a new user
- Request Body: name, email, password, password2, address
- Auth: Not required
- Response: name, email, password, password2, address

Note: The register endpoint would create normal user account. To make a account an admin, a superuser would have to login to
Django admin page and update the user account `is_admin` metadata to `true` and add `Admin` group for the user too.
Then the normal user account turn to an admin that can add, update and delete products.

2. POST /api/users/login/
- Description: login user
- Request Body: email, password
- Auth: Not required
- Response: id, metadata, refresh_token, access_token

3. GET /api/users/profile
- Description: Retrieve user profile by access token provided
- Auth: Bearer token
- Response: all user fields

4. POST /api/users/logout/
- Description: Logout auth user
- Request Body: refresh_token
- Auth: Bearer token
- Response: nil


Inventory Products Endpoints:

1. POST /api/inventory/products/add//
- Description: Create a new product by admin user
- Request Body: name, description, price, quantity, address
- Auth: Bearer token
- Response: id, owner, name, description, price, quantity, created_at, updated_at

2. GET /api/inventory/products/
- Description: List products for an admin user
- Request Body: nil
- Auth: Bearer token
- Response: list of products

3. PUT /api/inventory/products/:id/
- Description: Update product by an admin user
- Request Body: any field(s)
- Auth: Bearer token
- Response: id, owner, name, description, price, quantity, created_at, updated_at

4. DELETE /api/inventory/products/:id/
- Description: Delete product published by an admin user
- Request Body: nil
- Auth: Bearer token
- Response: nil

Note: This search functionality works when postgres database is used
5. GET /api/inventory/products/search?q=
- Description: Search for products by the specified field
- Request Body: nil
- Auth: Bearer token
- Response: list of products
- Search From: title, description


Inventory Orders Endpoints:

1. POST /api/inventory/orders/
- Description: Create a new order
- Request Body:
{
"items": [
{
"product": "0aa9ea8dce",
"quantity": 1
}
]
}
- Auth: Bearer token

- Response: order fields data

2. GET /api/inventory/orders/
- Description: List orders
- Request Body: nil
- Auth: Bearer token
- Response: list of orders
- Filters: status, date_from, date_to

3. PUT /api/inventory/orders/:id/status/
- Description: Update order status by an admin user
- Request Body:
{
"status": "completed"
}
- Auth: Bearer token
- Response: order fields data

4. DELETE /api/inventory/orders/:id/
- Description: Delete order
- Request Body: nil
- Auth: Bearer token
- Response: nil


5. GET /api/inventory/orders/:id/
- Description: Get order detail
- Request Body: nil
- Auth: Bearer token
- Response: order data


Inventory Report Endpoints:

1. GET /api/inventory/report/stock/
- Description: Get product out of stock
- Request Body: nil
- Auth: Bearer token
- Response:
[
{
"id": "0aa9ea8dce",
"name": "Product Name",
"quantity": 0,
"description": "Product Description",
"created_at": "2024-07-02T14:15:28.043625+01:00",
"updated_at": "2024-07-02T15:37:00.599740+01:00"
}
]

2. GET /api/inventory/report/sales/
- Description: Get product order sales by certain period
- Request Body: nil
- Auth: Bearer token
- Response:
[
{
"date": "2024-07-02",
"total_sales": 400
}
]
3 changes: 2 additions & 1 deletion inventory/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.urls import path
from .views import (InventoryProductList, InventoryProductCreate,
InventoryProductDetail,OrderListCreate, OrderDetail, OrderStatusUpdate,
LowStockReportView, SalesReportView, ProductSearchView)
LowStockReportView, SalesReportView, ProductSearchView, FrequentOrderedProductView)

app_name = 'inventory'

Expand All @@ -15,6 +15,7 @@
path('orders/<str:pk>/status/', OrderStatusUpdate.as_view(), name='order_status_update'),
path('report/stock/', LowStockReportView.as_view(), name='low-stock-report'),
path('report/sales/<str:period>/', SalesReportView.as_view(), name='sales-report'),
path('report/order/frequent', FrequentOrderedProductView.as_view(), name='frequent-ordered-product'),
#Search will only functional with postgres database connection
path('products/search', ProductSearchView.as_view(), name='products-search')
]
37 changes: 35 additions & 2 deletions inventory/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
from django.contrib.postgres.search import SearchQuery, SearchRank
from django_filters import rest_framework as filters

from django.db import transaction
from rest_framework import status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
Expand Down Expand Up @@ -270,4 +270,37 @@ def get(self, request):

serializer = ProductSerializer(results, many=True)
logger.info(f"Search results returned for query: {query}.")
return Response(serializer.data)
return Response(serializer.data)


from django.db.models import Sum

class FrequentOrderedProductView(APIView):

permission_classes = [permissions.IsAuthenticated]

def get(self, request, *args, **kwargs):
user = request.user
#returns the first instance of most frequent 9by quantity summation) ordered item product in the past
with transaction.atomic():
most_frequent_product = (
OrderItem.objects
.filter(order__owner=user)
.values('product')
.annotate(total_quantity=Sum('quantity'))
.order_by('total_quantity') #highest quantity down to least
.first()
)
if most_frequent_product:
product = Product.objects.get(id=most_frequent_product['product'])
serializer = ProductSerializer(product)
return Response(
{
"product_name": serializer.data.get('name'),
"total_quantity": serializer.data.get('quantity')
}
)
else:
return Response({"detail": "No frequent ordered product found."}, status=404)


30 changes: 29 additions & 1 deletion test/test_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rest_framework.test import APIClient
from users.models import User
from django.contrib.auth import authenticate
from inventory.models import Product
from inventory.models import Product, Order, OrderItem


@pytest.fixture
Expand Down Expand Up @@ -210,4 +210,32 @@ def test_order_detail(api_client, admin_user, product_data, get_token):
assert len(detail_response.data) > 1 # Ensure the product is part of the order


@pytest.mark.django_db
def test_most_frequently_ordered_product(api_client, regular_user, product_data, get_token):
token = get_token(regular_user, 'user123')
api_client.credentials(HTTP_AUTHORIZATION='Bearer ' + token)

# Create products
product1 = Product.objects.create(owner=regular_user, **product_data)

# Create orders with order items
order1 = Order.objects.create(owner=regular_user)
order2 = Order.objects.create(owner=regular_user)

item1 = OrderItem.objects.create(order=order1, product=product1, quantity=3, price=product1.price)
item2 = OrderItem.objects.create(order=order2, product=product1, quantity=4, price=product1.price)

# Call the endpoint to get the most frequently ordered product
response = api_client.get('/api/inventory/report/order/frequent')
assert response.status_code == status.HTTP_200_OK
assert response.data['product_name'] == product1.name
assert response.data['total_quantity'] > item1.quantity

@pytest.mark.django_db
def test_no_orders_for_user(api_client, regular_user, get_token):
token = get_token(regular_user, 'user123')
api_client.credentials(HTTP_AUTHORIZATION='Bearer ' + token)

response = api_client.get('/api/inventory/report/order/frequent')
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.data['detail'] == "No frequent ordered product found."

0 comments on commit f5606d5

Please sign in to comment.