From f5606d57d6abc54a59e1fd0b10fb168826d70f60 Mon Sep 17 00:00:00 2001 From: Arafat Olayiwola Date: Fri, 26 Jul 2024 13:54:53 +0100 Subject: [PATCH] add API for fetching most frequent ordered product and its tests --- api_doc.txt | 16 +++++ api_doc.txt.save | 145 +++++++++++++++++++++++++++++++++++++++++++ inventory/urls.py | 3 +- inventory/views.py | 37 ++++++++++- test/test_product.py | 30 ++++++++- 5 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 api_doc.txt.save diff --git a/api_doc.txt b/api_doc.txt index 86bb846..57393bb 100644 --- a/api_doc.txt +++ b/api_doc.txt @@ -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: [ { diff --git a/api_doc.txt.save b/api_doc.txt.save new file mode 100644 index 0000000..8c8685e --- /dev/null +++ b/api_doc.txt.save @@ -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 + } + ] diff --git a/inventory/urls.py b/inventory/urls.py index adea396..738591b 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -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' @@ -15,6 +15,7 @@ path('orders//status/', OrderStatusUpdate.as_view(), name='order_status_update'), path('report/stock/', LowStockReportView.as_view(), name='low-stock-report'), path('report/sales//', 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') ] \ No newline at end of file diff --git a/inventory/views.py b/inventory/views.py index 54fbd82..133442b 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -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 @@ -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) \ No newline at end of file + 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) + + \ No newline at end of file diff --git a/test/test_product.py b/test/test_product.py index 5ad938c..8f78a8d 100644 --- a/test/test_product.py +++ b/test/test_product.py @@ -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 @@ -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."