From c15ecf432c9c37b12a9d78413291922ad3cd1e80 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 11 Feb 2026 15:40:53 +0530 Subject: [PATCH 01/30] First Commit --- backend/python/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/python/README.md b/backend/python/README.md index 1522f5e23..f1df257ec 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -455,3 +455,4 @@ docker compose down # Stop MongoDB docker compose ps # List running containers docker compose logs -f # View logs ``` + \ No newline at end of file From 89205ed749a3fcc479db2ed70a3dff61f92e1a09 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Feb 2026 12:45:28 +0530 Subject: [PATCH 02/30] Build GET API using hexagonal architecture --- backend/python/django_app/adapters/api/views.py | 15 +++++++++++++++ .../python/django_app/application/use_cases.py | 8 ++++++++ backend/python/django_app/domain/services.py | 6 ++++++ backend/python/django_app/ports/hello_port.py | 6 ++++++ backend/python/django_app/urls.py | 7 +++---- 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 backend/python/django_app/adapters/api/views.py create mode 100644 backend/python/django_app/application/use_cases.py create mode 100644 backend/python/django_app/domain/services.py create mode 100644 backend/python/django_app/ports/hello_port.py diff --git a/backend/python/django_app/adapters/api/views.py b/backend/python/django_app/adapters/api/views.py new file mode 100644 index 000000000..9a404ea0c --- /dev/null +++ b/backend/python/django_app/adapters/api/views.py @@ -0,0 +1,15 @@ +from django_app.application.use_cases import HelloUseCase +from django_app.domain.services import HelloService +from django.http import JsonResponse +def hello_view(request): + + name=request.GET.get("name","World") + + services=HelloService() + use_case=HelloUseCase(services) + + message=use_case.execute(name) + + return JsonResponse({ + "message":message + }) \ No newline at end of file diff --git a/backend/python/django_app/application/use_cases.py b/backend/python/django_app/application/use_cases.py new file mode 100644 index 000000000..21559f255 --- /dev/null +++ b/backend/python/django_app/application/use_cases.py @@ -0,0 +1,8 @@ + +class HelloUseCase(): + + def __init__(self,hello_service): + self.hello_service=hello_service + + def execute(self,name)->str: + return self.hello_service.hello(name) \ No newline at end of file diff --git a/backend/python/django_app/domain/services.py b/backend/python/django_app/domain/services.py new file mode 100644 index 000000000..2ba0bd36e --- /dev/null +++ b/backend/python/django_app/domain/services.py @@ -0,0 +1,6 @@ +from django_app.ports.hello_port import HelloPort + +class HelloService(HelloPort): + + def hello(self, name:str)->str: + return f"Hello {name}" \ No newline at end of file diff --git a/backend/python/django_app/ports/hello_port.py b/backend/python/django_app/ports/hello_port.py new file mode 100644 index 000000000..4f441b5d6 --- /dev/null +++ b/backend/python/django_app/ports/hello_port.py @@ -0,0 +1,6 @@ +from abc import ABC,abstractmethod + +class HelloPort(ABC): + @abstractmethod + def hello(self, name:str)->str: + pass \ No newline at end of file diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index 0418448be..b8d636779 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -1,11 +1,10 @@ from django.contrib import admin from django.urls import path -from django.http import HttpResponse +from django.http import JsonResponse +from django_app.adapters.api.views import hello_view -def hello_world(request): - return HttpResponse("Hello, world! This is our interneers-lab Django server.") urlpatterns = [ path('admin/', admin.site.urls), - path('hello/', hello_world), + path('hello/', hello_view), ] From a0d60fc498d4a65c64eced1330abbba1158b1e81 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Thu, 19 Feb 2026 14:11:52 +0530 Subject: [PATCH 03/30] added warehouse app in urls --- backend/python/django_app/urls.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index b8d636779..cbe70e113 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -1,10 +1,11 @@ from django.contrib import admin -from django.urls import path +from django.urls import path,include from django.http import JsonResponse -from django_app.adapters.api.views import hello_view +from django_app.adapters.api.views import hello_view urlpatterns = [ path('admin/', admin.site.urls), path('hello/', hello_view), + path ('warehouse/', include("warehouse.urls")) ] From af5e4ff339eb3cf515adb7a75896bd58d20230e9 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Thu, 19 Feb 2026 14:15:01 +0530 Subject: [PATCH 04/30] APIs will only perform in-memory operations --- backend/python/warehouse/__init__.py | 0 backend/python/warehouse/admin.py | 3 + backend/python/warehouse/apps.py | 5 + .../python/warehouse/migrations/__init__.py | 0 backend/python/warehouse/models.py | 13 +++ backend/python/warehouse/storage.py | 4 + backend/python/warehouse/tests.py | 3 + backend/python/warehouse/urls.py | 10 ++ backend/python/warehouse/views.py | 99 +++++++++++++++++++ 9 files changed, 137 insertions(+) create mode 100644 backend/python/warehouse/__init__.py create mode 100644 backend/python/warehouse/admin.py create mode 100644 backend/python/warehouse/apps.py create mode 100644 backend/python/warehouse/migrations/__init__.py create mode 100644 backend/python/warehouse/models.py create mode 100644 backend/python/warehouse/storage.py create mode 100644 backend/python/warehouse/tests.py create mode 100644 backend/python/warehouse/urls.py create mode 100644 backend/python/warehouse/views.py diff --git a/backend/python/warehouse/__init__.py b/backend/python/warehouse/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/warehouse/admin.py b/backend/python/warehouse/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/backend/python/warehouse/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/python/warehouse/apps.py b/backend/python/warehouse/apps.py new file mode 100644 index 000000000..ae7e6913a --- /dev/null +++ b/backend/python/warehouse/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class WarehouseConfig(AppConfig): + name = 'warehouse' diff --git a/backend/python/warehouse/migrations/__init__.py b/backend/python/warehouse/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/warehouse/models.py b/backend/python/warehouse/models.py new file mode 100644 index 000000000..b2af9ba29 --- /dev/null +++ b/backend/python/warehouse/models.py @@ -0,0 +1,13 @@ +from django.db import models +from decimal import Decimal +# Create your models here. + + +class Product: + id: int + name: str + description: str + category: str + price: Decimal + brand: str + quantity: int diff --git a/backend/python/warehouse/storage.py b/backend/python/warehouse/storage.py new file mode 100644 index 000000000..63476bb67 --- /dev/null +++ b/backend/python/warehouse/storage.py @@ -0,0 +1,4 @@ +from .models import Product + +PRODUCTS = [] +CURRENT_ID = 1 \ No newline at end of file diff --git a/backend/python/warehouse/tests.py b/backend/python/warehouse/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/backend/python/warehouse/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/python/warehouse/urls.py b/backend/python/warehouse/urls.py new file mode 100644 index 000000000..b8023a259 --- /dev/null +++ b/backend/python/warehouse/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .views import delete_product, list_all_products, create_product, list_product_by_id, update_product + +urlpatterns = [ + path('', list_all_products), + path('create/', create_product), + path('/', list_product_by_id), + path('/update/', update_product), + path('/delete/', delete_product) +] diff --git a/backend/python/warehouse/views.py b/backend/python/warehouse/views.py new file mode 100644 index 000000000..f379ad1f4 --- /dev/null +++ b/backend/python/warehouse/views.py @@ -0,0 +1,99 @@ +import json +from django.http import JsonResponse +from .storage import PRODUCTS, CURRENT_ID +from django.views.decorators.csrf import csrf_exempt +import warehouse.storage as storage +# Create your views here. + + +def list_all_products(request): + if request.method != "GET": + return JsonResponse({"error": "Only GET allowed"}, status=405) + + return JsonResponse(PRODUCTS, safe=False) + + +@csrf_exempt +def create_product(request): + if request.method != "POST": + return JsonResponse({"error": "Only POST allowed"}, status=405) + + try : + data = json.loads(request.body) + + # Validate data here + required_fields = ["name", "description", "category", "price", "brand", "quantity"] + + for field in required_fields: + if field not in data: + return JsonResponse({"error": f"Missing field: {field}"}, status=400) + + if float(data["price"]) < 0: + return JsonResponse({"error": "Price must be non-negative"}, status=400) + + if int(data["quantity"]) < 0: + return JsonResponse({"error": "Quantity must be non-negative"}, status=400) + + # Create product + new_product = { + "id": storage.CURRENT_ID + 1, + "name": data["name"], + "description": data["description"], + "category": data["category"], + "price": data["price"], + "brand": data["brand"], + "quantity": data["quantity"] + } + PRODUCTS.append(new_product) + storage.CURRENT_ID +=1 + + return JsonResponse(new_product, status=201) + + + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + +def list_product_by_id(request, product_id): + if request.method != "GET": + return JsonResponse({"error": "Only GET allowed"}, status=405) + + for product in PRODUCTS: + if product["id"] == product_id: + return JsonResponse(product) + + return JsonResponse({"error": "Product not found"}, status=404) + + +@csrf_exempt +def update_product(request, product_id): + if request.method != "PUT": + return JsonResponse({"error": "Only PUT allowed"}, status=405) + + for product in PRODUCTS: + if(product["id"] == product_id): + try: + data = json.loads(request.body) + # Update fields + for key in ["name", "description", "category", "price", "brand", "quantity"]: + if key in data: + product[key] = data[key] + + return JsonResponse(product) + + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + return JsonResponse({"error": "Product not found"}, status=404) + + +@csrf_exempt +def delete_product(request, product_id): + if request.method != "DELETE": + return JsonResponse({"error": "Only DELETE allowed"}, status=405) + + for i, product in enumerate(PRODUCTS): + if product["id"] == product_id: + del PRODUCTS[i] + return JsonResponse({"message": "Product deleted"}) + + return JsonResponse({"error": "Product not found"}, status=404) \ No newline at end of file From d4a4ec594a2eda6ffd93d70378196a0d35690fb7 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sun, 1 Mar 2026 12:02:16 +0530 Subject: [PATCH 05/30] created ProductService layer --- backend/python/django_app/settings.py | 11 ++++ backend/python/django_app/urls.py | 3 +- backend/python/product_service/__init__.py | 0 backend/python/product_service/admin.py | 3 ++ backend/python/product_service/apps.py | 5 ++ .../product_service/migrations/__init__.py | 0 backend/python/product_service/models.py | 7 +++ backend/python/product_service/repository.py | 21 ++++++++ backend/python/product_service/services.py | 27 ++++++++++ backend/python/product_service/tests.py | 3 ++ backend/python/product_service/urls.py | 9 ++++ backend/python/product_service/views.py | 54 +++++++++++++++++++ 12 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 backend/python/product_service/__init__.py create mode 100644 backend/python/product_service/admin.py create mode 100644 backend/python/product_service/apps.py create mode 100644 backend/python/product_service/migrations/__init__.py create mode 100644 backend/python/product_service/models.py create mode 100644 backend/python/product_service/repository.py create mode 100644 backend/python/product_service/services.py create mode 100644 backend/python/product_service/tests.py create mode 100644 backend/python/product_service/urls.py create mode 100644 backend/python/product_service/views.py diff --git a/backend/python/django_app/settings.py b/backend/python/django_app/settings.py index 2d7ea95db..560ceaa4f 100644 --- a/backend/python/django_app/settings.py +++ b/backend/python/django_app/settings.py @@ -37,6 +37,8 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "warehouse", + "product_service", ] MIDDLEWARE = [ @@ -121,3 +123,12 @@ # https://docs.djangoproject.com/en/6.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +from mongoengine import connect + +from mongoengine import connect + +connect( + db="product_db", + host="mongodb://root:example@localhost:27019/product_db?authSource=admin" +) diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index cbe70e113..fdd308453 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -7,5 +7,6 @@ urlpatterns = [ path('admin/', admin.site.urls), path('hello/', hello_view), - path ('warehouse/', include("warehouse.urls")) + path('warehouse/', include("warehouse.urls")), + path('product_service/', include("product_service.urls")), ] diff --git a/backend/python/product_service/__init__.py b/backend/python/product_service/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/product_service/admin.py b/backend/python/product_service/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/backend/python/product_service/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/python/product_service/apps.py b/backend/python/product_service/apps.py new file mode 100644 index 000000000..c6d389b00 --- /dev/null +++ b/backend/python/product_service/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProductServiceConfig(AppConfig): + name = 'product_service' diff --git a/backend/python/product_service/migrations/__init__.py b/backend/python/product_service/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/product_service/models.py b/backend/python/product_service/models.py new file mode 100644 index 000000000..d4e9b78a4 --- /dev/null +++ b/backend/python/product_service/models.py @@ -0,0 +1,7 @@ +from mongoengine import Document, StringField, FloatField + +class Product(Document): + name=StringField(required=True) + description=StringField() + price=FloatField(required=True) + \ No newline at end of file diff --git a/backend/python/product_service/repository.py b/backend/python/product_service/repository.py new file mode 100644 index 000000000..e8f041caf --- /dev/null +++ b/backend/python/product_service/repository.py @@ -0,0 +1,21 @@ +from .models import Product + +class ProductRepository: + + @staticmethod + def create(data): + product = Product(**data) + product.save() + return product + + @staticmethod + def get_all(): + return Product.objects() + + @staticmethod + def get_by_id(product_id): + return Product.objects(id=product_id).first() + + @staticmethod + def delete_by_id(product_id): + Product.objects(id=product_id).delete() \ No newline at end of file diff --git a/backend/python/product_service/services.py b/backend/python/product_service/services.py new file mode 100644 index 000000000..c51de9b0f --- /dev/null +++ b/backend/python/product_service/services.py @@ -0,0 +1,27 @@ +from .repository import ProductRepository + +class ProductServices(): + + @staticmethod + def create_product(data): + if data.get("price", 0) <= 0: + raise ValueError("Given Price is not valid") + + return ProductRepository.create(data) + + @staticmethod + def list_products(): + return ProductRepository.get_all() + + @staticmethod + def get_product(product_id): + product = ProductRepository.get_by_id(product_id) + + if not product: + raise ValueError("Product not found") + + return product + + @staticmethod + def delete_product(product_id): + return ProductRepository.delete_by_id(product_id) diff --git a/backend/python/product_service/tests.py b/backend/python/product_service/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/backend/python/product_service/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/python/product_service/urls.py b/backend/python/product_service/urls.py new file mode 100644 index 000000000..a47782294 --- /dev/null +++ b/backend/python/product_service/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path("", views.list_products), + path("create/", views.create_product), + path("/", views.get_product), + path("delete//", views.delete_product), +] \ No newline at end of file diff --git a/backend/python/product_service/views.py b/backend/python/product_service/views.py new file mode 100644 index 000000000..85a8a5426 --- /dev/null +++ b/backend/python/product_service/views.py @@ -0,0 +1,54 @@ +import json +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from .services import ProductServices + +@csrf_exempt +def create_product(request): + if request.method == "POST": + data = json.loads(request.body) + try: + product = ProductServices.create_product(data) + return JsonResponse({ + "id": str(product.id), + "message": "Product created" + }) + except ValueError as er: + return JsonResponse({"error": str(er)}, status=400) + + return JsonResponse({"error" : "POST method not used"}, status=400) + + +def list_products(request): + products = ProductServices.list_products() + + return JsonResponse([ + { + "id": str(p.id), + "name": p.name, + "description": p.description, + "price": p.price + } + for p in products + ], safe=False) + + +def get_product(request, product_id): + try: + product=ProductServices.get_product(product_id) + return JsonResponse({ + "id" : str(product.id), + "name": product.name, + "description" : product.description, + "price" : product.price, + }) + except ValueError as er: + return JsonResponse({"error" : str(er)}, status=404) + +@csrf_exempt +def delete_product(request, product_id): + if request.method == "DELETE": + ProductServices.delete_product(product_id) + return JsonResponse({"message":"Deleted"}) + else: + return JsonResponse({"error" : "DELETE method not used"}, status=400) \ No newline at end of file From a7e49190552b38ec4c29ab77235ee1ce150a8db9 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Mar 2026 09:51:30 +0530 Subject: [PATCH 06/30] Update product model --- backend/python/product_service/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/python/product_service/models.py b/backend/python/product_service/models.py index d4e9b78a4..295c84ec2 100644 --- a/backend/python/product_service/models.py +++ b/backend/python/product_service/models.py @@ -1,7 +1,11 @@ -from mongoengine import Document, StringField, FloatField +from mongoengine import Document, StringField, FloatField, IntField class Product(Document): name=StringField(required=True) description=StringField() + category = StringField(required=True) price=FloatField(required=True) + brand=StringField() + quantity=IntField(required=True) + \ No newline at end of file From c9571bd3b5e1007ac80eb8047b9beff89993e1bc Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Mar 2026 10:02:57 +0530 Subject: [PATCH 07/30] remove unnecessary files --- backend/python/django_app/adapters/api/views.py | 15 --------------- .../python/django_app/application/use_cases.py | 8 -------- backend/python/django_app/domain/services.py | 6 ------ backend/python/django_app/ports/hello_port.py | 6 ------ 4 files changed, 35 deletions(-) delete mode 100644 backend/python/django_app/adapters/api/views.py delete mode 100644 backend/python/django_app/application/use_cases.py delete mode 100644 backend/python/django_app/domain/services.py delete mode 100644 backend/python/django_app/ports/hello_port.py diff --git a/backend/python/django_app/adapters/api/views.py b/backend/python/django_app/adapters/api/views.py deleted file mode 100644 index 9a404ea0c..000000000 --- a/backend/python/django_app/adapters/api/views.py +++ /dev/null @@ -1,15 +0,0 @@ -from django_app.application.use_cases import HelloUseCase -from django_app.domain.services import HelloService -from django.http import JsonResponse -def hello_view(request): - - name=request.GET.get("name","World") - - services=HelloService() - use_case=HelloUseCase(services) - - message=use_case.execute(name) - - return JsonResponse({ - "message":message - }) \ No newline at end of file diff --git a/backend/python/django_app/application/use_cases.py b/backend/python/django_app/application/use_cases.py deleted file mode 100644 index 21559f255..000000000 --- a/backend/python/django_app/application/use_cases.py +++ /dev/null @@ -1,8 +0,0 @@ - -class HelloUseCase(): - - def __init__(self,hello_service): - self.hello_service=hello_service - - def execute(self,name)->str: - return self.hello_service.hello(name) \ No newline at end of file diff --git a/backend/python/django_app/domain/services.py b/backend/python/django_app/domain/services.py deleted file mode 100644 index 2ba0bd36e..000000000 --- a/backend/python/django_app/domain/services.py +++ /dev/null @@ -1,6 +0,0 @@ -from django_app.ports.hello_port import HelloPort - -class HelloService(HelloPort): - - def hello(self, name:str)->str: - return f"Hello {name}" \ No newline at end of file diff --git a/backend/python/django_app/ports/hello_port.py b/backend/python/django_app/ports/hello_port.py deleted file mode 100644 index 4f441b5d6..000000000 --- a/backend/python/django_app/ports/hello_port.py +++ /dev/null @@ -1,6 +0,0 @@ -from abc import ABC,abstractmethod - -class HelloPort(ABC): - @abstractmethod - def hello(self, name:str)->str: - pass \ No newline at end of file From eccc141a9410ba7469e0fc8221292f6f01633ee3 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Mar 2026 10:12:05 +0530 Subject: [PATCH 08/30] create ProductCategory service layer --- backend/python/product_category/__init__.py | 0 backend/python/product_category/admin.py | 3 +++ backend/python/product_category/apps.py | 5 +++++ backend/python/product_category/migrations/__init__.py | 0 backend/python/product_category/models.py | 0 backend/python/product_category/repository.py | 0 backend/python/product_category/services.py | 0 backend/python/product_category/tests.py | 3 +++ backend/python/product_category/views.py | 3 +++ 9 files changed, 14 insertions(+) create mode 100644 backend/python/product_category/__init__.py create mode 100644 backend/python/product_category/admin.py create mode 100644 backend/python/product_category/apps.py create mode 100644 backend/python/product_category/migrations/__init__.py create mode 100644 backend/python/product_category/models.py create mode 100644 backend/python/product_category/repository.py create mode 100644 backend/python/product_category/services.py create mode 100644 backend/python/product_category/tests.py create mode 100644 backend/python/product_category/views.py diff --git a/backend/python/product_category/__init__.py b/backend/python/product_category/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/product_category/admin.py b/backend/python/product_category/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/backend/python/product_category/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/python/product_category/apps.py b/backend/python/product_category/apps.py new file mode 100644 index 000000000..23c00572f --- /dev/null +++ b/backend/python/product_category/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProductCategoryConfig(AppConfig): + name = 'product_category' diff --git a/backend/python/product_category/migrations/__init__.py b/backend/python/product_category/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/product_category/models.py b/backend/python/product_category/models.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/product_category/repository.py b/backend/python/product_category/repository.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/product_category/services.py b/backend/python/product_category/services.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/product_category/tests.py b/backend/python/product_category/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/backend/python/product_category/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/python/product_category/views.py b/backend/python/product_category/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/backend/python/product_category/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 4180221ef80354d3a39a4810f4fc169e82362797 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Mar 2026 10:32:55 +0530 Subject: [PATCH 09/30] Model products belonging to a category --- backend/python/product_category/models.py | 5 +++++ backend/python/product_service/models.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/python/product_category/models.py b/backend/python/product_category/models.py index e69de29bb..810376aff 100644 --- a/backend/python/product_category/models.py +++ b/backend/python/product_category/models.py @@ -0,0 +1,5 @@ +from mongoengine import Document, StringField + +class ProductCategory(Document): + name = StringField(required=True) + description = StringField() \ No newline at end of file diff --git a/backend/python/product_service/models.py b/backend/python/product_service/models.py index 295c84ec2..c351459c5 100644 --- a/backend/python/product_service/models.py +++ b/backend/python/product_service/models.py @@ -1,9 +1,10 @@ -from mongoengine import Document, StringField, FloatField, IntField +from mongoengine import Document, ReferenceField, StringField, FloatField, IntField +from product_category.models import ProductCategory class Product(Document): name=StringField(required=True) description=StringField() - category = StringField(required=True) + category = ReferenceField(ProductCategory, required=True) price=FloatField(required=True) brand=StringField() quantity=IntField(required=True) From bbc5bc30fcdb1a9687d65b1766ce6b3ea8380659 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Mar 2026 11:27:01 +0530 Subject: [PATCH 10/30] Add CRUD APIs for a product category --- backend/python/django_app/settings.py | 1 + backend/python/django_app/urls.py | 4 +- backend/python/product_category/repository.py | 33 +++++++++ backend/python/product_category/services.py | 28 ++++++++ backend/python/product_category/urls.py | 10 +++ backend/python/product_category/views.py | 72 ++++++++++++++++++- 6 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 backend/python/product_category/urls.py diff --git a/backend/python/django_app/settings.py b/backend/python/django_app/settings.py index 560ceaa4f..e84abbc67 100644 --- a/backend/python/django_app/settings.py +++ b/backend/python/django_app/settings.py @@ -39,6 +39,7 @@ "django.contrib.staticfiles", "warehouse", "product_service", + "product_category", ] MIDDLEWARE = [ diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index fdd308453..ed68d3887 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -1,12 +1,10 @@ from django.contrib import admin from django.urls import path,include from django.http import JsonResponse -from django_app.adapters.api.views import hello_view - urlpatterns = [ path('admin/', admin.site.urls), - path('hello/', hello_view), path('warehouse/', include("warehouse.urls")), path('product_service/', include("product_service.urls")), + path('product_category/', include("product_category.urls")), ] diff --git a/backend/python/product_category/repository.py b/backend/python/product_category/repository.py index e69de29bb..516afbe3f 100644 --- a/backend/python/product_category/repository.py +++ b/backend/python/product_category/repository.py @@ -0,0 +1,33 @@ +from .models import ProductCategory + +class ProductCategoryRepository: + @staticmethod + def create(data): + product_category = ProductCategory(**data) + product_category.save() + return product_category + + @staticmethod + def get_all(): + return ProductCategory.objects() + + @staticmethod + def get_by_id(product_category_id): + return ProductCategory.objects(id=product_category_id).first() + + @staticmethod + def update(product_category_id, data): + product_category = ProductCategory.objects(id=product_category_id).first() + if product_category: + for key, value in data.items(): + setattr(product_category, key, value) + product_category.save() + return product_category + + @staticmethod + def delete(product_category_id): + product_category = ProductCategory.objects(id=product_category_id).first() + if product_category: + product_category.delete() + return product_category + diff --git a/backend/python/product_category/services.py b/backend/python/product_category/services.py index e69de29bb..7412a1327 100644 --- a/backend/python/product_category/services.py +++ b/backend/python/product_category/services.py @@ -0,0 +1,28 @@ +from .repository import ProductCategoryRepository + +class ProductCategoryService: + @staticmethod + def create_product_category(data): + return ProductCategoryRepository.create(data) + + @staticmethod + def get_all_product_categories(): + return ProductCategoryRepository.get_all() + + @staticmethod + def get_product_category_by_id(product_category_id): + product_category = ProductCategoryRepository.get_by_id(product_category_id) + if not product_category: + raise ValueError("Product Category not found") + return product_category + + @staticmethod + def update_product_category(product_category_id, data): + product_category = ProductCategoryRepository.get_by_id(product_category_id) + if not product_category: + raise ValueError("Product Category not found") + return ProductCategoryRepository.update(product_category_id, data) + + @staticmethod + def delete_product_category(product_category_id): + return ProductCategoryRepository.delete(product_category_id) \ No newline at end of file diff --git a/backend/python/product_category/urls.py b/backend/python/product_category/urls.py new file mode 100644 index 000000000..93df67b83 --- /dev/null +++ b/backend/python/product_category/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path("", views.get_all_product_categories), + path("create/", views.create_product_category), + path("/", views.get_product_category_by_id), + path("update//", views.update_product_category), + path("delete//", views.delete_product_category), +] diff --git a/backend/python/product_category/views.py b/backend/python/product_category/views.py index 91ea44a21..5808f0846 100644 --- a/backend/python/product_category/views.py +++ b/backend/python/product_category/views.py @@ -1,3 +1,71 @@ -from django.shortcuts import render +import json +from .services import ProductCategoryService +from django.views.decorators.csrf import csrf_exempt +from django.http import JsonResponse -# Create your views here. +@csrf_exempt +def create_product_category(request): + if request.method == 'POST': + data = json.loads(request.body) + product_category = ProductCategoryService.create_product_category(data) + return JsonResponse({ + "id": str(product_category.id), + "name": product_category.name, + "description": product_category.description, + "message": "Product category created" + }) + return {"error": "Invalid request method"} + +def get_all_product_categories(request): + if request.method == 'GET': + product_categories = ProductCategoryService.get_all_product_categories() + return JsonResponse({ + "product_category": [ + { + "id": str(pc.id), + "name": pc.name, + "description": pc.description + } + for pc in product_categories + ] + }) + return {"error": "Invalid request method"} + +def get_product_category_by_id(request, product_category_id): + if request.method == 'GET': + try: + product_category = ProductCategoryService.get_product_category_by_id(product_category_id) + return JsonResponse({ + "id": str(product_category.id), + "name": product_category.name, + "description": product_category.description + }) + except ValueError as e: + return {"error": str(e)} + return {"error": "Invalid request method"} + +@csrf_exempt +def update_product_category(request, product_category_id): + if request.method == 'PUT': + data = json.loads(request.body) + try: + product_category = ProductCategoryService.update_product_category(product_category_id, data) + return JsonResponse({ + "id": str(product_category.id), + "name": product_category.name, + "description": product_category.description + }) + except ValueError as e: + return {"error": str(e)} + return {"error": "Invalid request method"} + +@csrf_exempt +def delete_product_category(request, product_category_id): + if request.method == 'DELETE': + product_category = ProductCategoryService.delete_product_category(product_category_id) + if product_category: + return JsonResponse({"message": "Product category deleted"}) + else: + return JsonResponse({"error": "Product category not found"}) + return {"error": "Invalid request method"} + \ No newline at end of file From cdc7ca8e1afd19ac955213557568593b4763fd5d Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Mar 2026 11:49:12 +0530 Subject: [PATCH 11/30] Add APIs to fetch a list of products belonging to a category --- backend/python/product_service/repository.py | 4 ++++ backend/python/product_service/services.py | 4 ++++ backend/python/product_service/urls.py | 1 + backend/python/product_service/views.py | 17 +++++++++++++++++ 4 files changed, 26 insertions(+) diff --git a/backend/python/product_service/repository.py b/backend/python/product_service/repository.py index e8f041caf..d9c461b39 100644 --- a/backend/python/product_service/repository.py +++ b/backend/python/product_service/repository.py @@ -12,6 +12,10 @@ def create(data): def get_all(): return Product.objects() + @staticmethod + def get_all_by_category_id(category_id): + return Product.objects(category=category_id) + @staticmethod def get_by_id(product_id): return Product.objects(id=product_id).first() diff --git a/backend/python/product_service/services.py b/backend/python/product_service/services.py index c51de9b0f..ffbc7c968 100644 --- a/backend/python/product_service/services.py +++ b/backend/python/product_service/services.py @@ -21,6 +21,10 @@ def get_product(product_id): raise ValueError("Product not found") return product + + @staticmethod + def list_products_by_category_id(category_id): + return ProductRepository.get_all_by_category_id(category_id) @staticmethod def delete_product(product_id): diff --git a/backend/python/product_service/urls.py b/backend/python/product_service/urls.py index a47782294..cd9465f8a 100644 --- a/backend/python/product_service/urls.py +++ b/backend/python/product_service/urls.py @@ -6,4 +6,5 @@ path("create/", views.create_product), path("/", views.get_product), path("delete//", views.delete_product), + path("products-by-category//", views.list_products_by_category_id) ] \ No newline at end of file diff --git a/backend/python/product_service/views.py b/backend/python/product_service/views.py index 85a8a5426..10ff84382 100644 --- a/backend/python/product_service/views.py +++ b/backend/python/product_service/views.py @@ -45,6 +45,23 @@ def get_product(request, product_id): except ValueError as er: return JsonResponse({"error" : str(er)}, status=404) +@csrf_exempt +def list_products_by_category_id(request, category_id): + if request.method == "GET": + products = ProductServices.list_products_by_category_id(category_id) + + return JsonResponse([ + { + "id": str(p.id), + "name": p.name, + "description": p.description, + "price": p.price + } + for p in products + ], safe=False) + else: + return JsonResponse({"error" : "GET method not used"}, status=400) + @csrf_exempt def delete_product(request, product_id): if request.method == "DELETE": From 76cdcd068ed661754a783cfc4ab2debbc795473f Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Mar 2026 12:37:59 +0530 Subject: [PATCH 12/30] Add APIs to add/remove products from categories --- backend/python/product_service/models.py | 2 +- backend/python/product_service/repository.py | 18 ++++++ backend/python/product_service/services.py | 14 +++++ backend/python/product_service/urls.py | 4 +- backend/python/product_service/views.py | 63 +++++++++++++++++--- 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/backend/python/product_service/models.py b/backend/python/product_service/models.py index c351459c5..17c68f666 100644 --- a/backend/python/product_service/models.py +++ b/backend/python/product_service/models.py @@ -4,7 +4,7 @@ class Product(Document): name=StringField(required=True) description=StringField() - category = ReferenceField(ProductCategory, required=True) + category = ReferenceField(ProductCategory) price=FloatField(required=True) brand=StringField() quantity=IntField(required=True) diff --git a/backend/python/product_service/repository.py b/backend/python/product_service/repository.py index d9c461b39..816da7d6b 100644 --- a/backend/python/product_service/repository.py +++ b/backend/python/product_service/repository.py @@ -1,4 +1,5 @@ from .models import Product +from product_category.models import ProductCategory class ProductRepository: @@ -16,6 +17,23 @@ def get_all(): def get_all_by_category_id(category_id): return Product.objects(category=category_id) + @staticmethod + def remove_category_from_product(product_id): + product = Product.objects(id=product_id).first() + if product: + product.category = None + product.save() + return product + + @staticmethod + def add_category_to_product(product_id, category_id): + product = Product.objects(id=product_id).first() + category = ProductCategory.objects(id=category_id).first() + if product: + product.category = category + product.save() + return product + @staticmethod def get_by_id(product_id): return Product.objects(id=product_id).first() diff --git a/backend/python/product_service/services.py b/backend/python/product_service/services.py index ffbc7c968..464068803 100644 --- a/backend/python/product_service/services.py +++ b/backend/python/product_service/services.py @@ -26,6 +26,20 @@ def get_product(product_id): def list_products_by_category_id(category_id): return ProductRepository.get_all_by_category_id(category_id) + @staticmethod + def remove_category_from_product(product_id): + product = ProductRepository.remove_category_from_product(product_id) + if not product: + raise ValueError("Product not found") + return product + + @staticmethod + def add_category_to_product(product_id, category_id): + product = ProductRepository.add_category_to_product(product_id, category_id) + if not product: + raise ValueError("Product not found") + return product + @staticmethod def delete_product(product_id): return ProductRepository.delete_by_id(product_id) diff --git a/backend/python/product_service/urls.py b/backend/python/product_service/urls.py index cd9465f8a..0a3290072 100644 --- a/backend/python/product_service/urls.py +++ b/backend/python/product_service/urls.py @@ -6,5 +6,7 @@ path("create/", views.create_product), path("/", views.get_product), path("delete//", views.delete_product), - path("products-by-category//", views.list_products_by_category_id) + path("products-by-category//", views.list_products_by_category_id), + path("remove-category//", views.remove_category_from_product), + path("add-category///", views.add_category_to_product), ] \ No newline at end of file diff --git a/backend/python/product_service/views.py b/backend/python/product_service/views.py index 10ff84382..fde2cf34d 100644 --- a/backend/python/product_service/views.py +++ b/backend/python/product_service/views.py @@ -22,25 +22,36 @@ def create_product(request): def list_products(request): products = ProductServices.list_products() - return JsonResponse([ - { - "id": str(p.id), - "name": p.name, - "description": p.description, - "price": p.price - } - for p in products - ], safe=False) + response_data = [] + for p in products: + + raw_category = p._data.get('category') + + cat_id = str(raw_category.id) if raw_category else None + + response_data.append({ + "id": str(p.id), + "name": p.name, + "description": p.description, + "price": p.price, + "category": cat_id + }) + + return JsonResponse(response_data, safe=False) def get_product(request, product_id): try: product=ProductServices.get_product(product_id) + raw_category = product._data.get('category') + + cat_id = str(raw_category.id) if raw_category else None return JsonResponse({ "id" : str(product.id), "name": product.name, "description" : product.description, "price" : product.price, + "category": cat_id }) except ValueError as er: return JsonResponse({"error" : str(er)}, status=404) @@ -62,6 +73,40 @@ def list_products_by_category_id(request, category_id): else: return JsonResponse({"error" : "GET method not used"}, status=400) +@csrf_exempt +def remove_category_from_product(request, product_id): + if request.method == "PATCH": + try: + product = ProductServices.remove_category_from_product(product_id) + return JsonResponse({ + "id": str(product.id), + "name": product.name, + "description": product.description, + "price": product.price, + "category": None + }) + except ValueError as er: + return JsonResponse({"error": str(er)}, status=404) + else: + return JsonResponse({"error" : "PATCH method not used"}, status=400) + +@csrf_exempt +def add_category_to_product(request, product_id, category_id): + if request.method == "PATCH": + try: + product = ProductServices.add_category_to_product(product_id, category_id) + return JsonResponse({ + "id": str(product.id), + "name": product.name, + "description": product.description, + "price": product.price, + "category": str(product.category.id) if product.category else None + }) + except ValueError as er: + return JsonResponse({"error": str(er)}, status=404) + else: + return JsonResponse({"error" : "PATCH method not used"}, status=400) + @csrf_exempt def delete_product(request, product_id): if request.method == "DELETE": From f7f09a67d9bbd8a66b8f5bab3a80c70ddf377be7 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Mar 2026 15:59:15 +0530 Subject: [PATCH 13/30] Add validation for brand requirement --- backend/python/product_service/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/python/product_service/models.py b/backend/python/product_service/models.py index 17c68f666..9b8e07222 100644 --- a/backend/python/product_service/models.py +++ b/backend/python/product_service/models.py @@ -1,12 +1,18 @@ from mongoengine import Document, ReferenceField, StringField, FloatField, IntField from product_category.models import ProductCategory + + class Product(Document): name=StringField(required=True) description=StringField() category = ReferenceField(ProductCategory) price=FloatField(required=True) - brand=StringField() + brand=StringField(required=True) quantity=IntField(required=True) + def clean(self): + if not self.brand or self.brand.strip() == "": + raise ValueError("Brand cannot be empty or whitespace") + \ No newline at end of file From 63788b3b3e30e365ebc07d3e22a6ed675950a57a Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Sat, 14 Mar 2026 16:01:12 +0530 Subject: [PATCH 14/30] handle existing products not having brand --- .../management/commands/migrate_brands.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/python/product_service/management/commands/migrate_brands.py diff --git a/backend/python/product_service/management/commands/migrate_brands.py b/backend/python/product_service/management/commands/migrate_brands.py new file mode 100644 index 000000000..632af5b3c --- /dev/null +++ b/backend/python/product_service/management/commands/migrate_brands.py @@ -0,0 +1,15 @@ +from product_service.models import Product +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + help = "Migrate brands from old format to new format" + + def handle(self, *args, **options): + products = Product.objects() + for product in products: + if not product.brand or product.brand.strip() == "": + product.brand = "Unknown" + product.save() + + self.stdout.write(self.style.SUCCESS("Successfully migrated brands for all products")) + \ No newline at end of file From 9de930515c8317c450e45362d116c8811ba2bbe5 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Mon, 16 Mar 2026 17:47:36 +0530 Subject: [PATCH 15/30] unit tests for ProductCategoryService mocking the repository layers --- backend/python/product_category/tests.py | 3 -- .../tests/unit/test_services.py | 50 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) delete mode 100644 backend/python/product_category/tests.py create mode 100644 backend/python/product_category/tests/unit/test_services.py diff --git a/backend/python/product_category/tests.py b/backend/python/product_category/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/backend/python/product_category/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/python/product_category/tests/unit/test_services.py b/backend/python/product_category/tests/unit/test_services.py new file mode 100644 index 000000000..b81fa0ad4 --- /dev/null +++ b/backend/python/product_category/tests/unit/test_services.py @@ -0,0 +1,50 @@ +import pytest +from product_category.services import ProductCategoryService + +from unittest.mock import patch + +@patch("product_category.services.ProductCategoryRepository") +def test_create_product_category(mock_repo): + + data = { + "name": "Fruits", + "description": "All kinds of fruits" + } + + mock_repo.create.return_value = data + + result = ProductCategoryService.create_product_category(data) + assert result["name"] == data["name"] + mock_repo.create.assert_called_once_with(data) + +@patch("product_category.services.ProductCategoryRepository") +def test_get_all_product_categories(mock_repo): + mock_repo.get_all.return_value = [ + {"name": "Fruits", "description": "All kinds of fruits"}, + {"name": "Vegetables", "description": "All kinds of vegetables"} + ] + + result = ProductCategoryService.get_all_product_categories() + assert len(result) == 2 + mock_repo.get_all.assert_called_once() + +@patch("product_category.services.ProductCategoryRepository") +def test_get_product_category_by_id(mock_repo): + mock_repo.get_by_id.return_value = {"name": "Fruits", "description": "All kinds of fruits"} + + result = ProductCategoryService.get_product_category_by_id(1) + assert result["name"] == "Fruits" + mock_repo.get_by_id.assert_called_once_with(1) + +@patch("product_category.services.ProductCategoryRepository") +def test_update_product_category(mock_repo): + mock_repo.get_by_id.return_value = {"name": "Fruits", "description": "All kinds of fruits"} + mock_repo.update.return_value = {"name": "Fruits", "description": "Fresh fruits"} + + data = {"description": "Fresh fruits"} + result = ProductCategoryService.update_product_category(1, data) + assert result["description"] == "Fresh fruits" + mock_repo.get_by_id.assert_called_once_with(1) + mock_repo.update.assert_called_once_with(1, data) + + \ No newline at end of file From b864edd226caae9d97bdd4a4bbae6c4e08def444 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Mon, 16 Mar 2026 17:50:31 +0530 Subject: [PATCH 16/30] unit tests for ProductService, mocking the repository layers --- backend/python/product_service/tests.py | 3 - .../tests/unit/test_service.py | 133 ++++++++++++++++++ 2 files changed, 133 insertions(+), 3 deletions(-) delete mode 100644 backend/python/product_service/tests.py create mode 100644 backend/python/product_service/tests/unit/test_service.py diff --git a/backend/python/product_service/tests.py b/backend/python/product_service/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/backend/python/product_service/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/python/product_service/tests/unit/test_service.py b/backend/python/product_service/tests/unit/test_service.py new file mode 100644 index 000000000..679247283 --- /dev/null +++ b/backend/python/product_service/tests/unit/test_service.py @@ -0,0 +1,133 @@ +import pytest +from unittest.mock import patch + +from product_service.services import ProductServices + +@patch("product_service.services.ProductRepository") +def test_create_product(mock_repo): + + data = { + "name": "Apple", + "description": "Fresh red apple", + "price": 10.0, + "brand": "Test Brand", + "quantity": 5 + } + + mock_repo.create.return_value = data + + result = ProductServices.create_product(data) + assert result["name"] == data["name"] + mock_repo.create.assert_called_once_with(data) + +@patch("product_service.services.ProductRepository") +def test_create_product_with_invalid_price(mock_repo): + data = { + "name": "Apple", + "description": "Fresh red apple", + "price": -5.0, + "brand": "Test Brand", + "quantity": 5 + } + + with pytest.raises(ValueError) as excinfo: + ProductServices.create_product(data) + + assert str(excinfo.value) == "Given Price is not valid" + mock_repo.create.assert_not_called() + +@patch("product_service.services.ProductRepository") +def test_get_product_by_id(mock_repo): + product_id = "12345" + expected_product = { + "id": product_id, + "name": "Apple", + "description": "Fresh red apple", + "price": 10.0, + "brand": "Test Brand", + "quantity": 5 + } + + mock_repo.get_by_id.return_value = expected_product + + result = ProductServices.get_product(product_id) + assert result["id"] == expected_product["id"] + mock_repo.get_by_id.assert_called_once_with(product_id) + +@patch("product_service.services.ProductRepository") +def test_get_product_by_id_not_found(mock_repo): + product_id = "12345" + mock_repo.get_by_id.return_value = None + + with pytest.raises(ValueError) as excinfo: + ProductServices.get_product(product_id) + + assert str(excinfo.value) == "Product not found" + mock_repo.get_by_id.assert_called_once_with(product_id) + +@patch("product_service.services.ProductRepository") +def test_list_products_by_category_id(mock_repo): + category_id = "cat123" + expected_products = [ + { + "id": "prod1", + "name": "Apple", + "description": "Fresh red apple", + "price": 10.0, + "brand": "Test Brand", + "quantity": 5 + }, + { + "id": "prod2", + "name": "Banana", + "description": "Ripe yellow banana", + "price": 5.0, + "brand": "Test Brand", + "quantity": 10 + } + ] + + mock_repo.get_all_by_category_id.return_value = expected_products + + result = ProductServices.list_products_by_category_id(category_id) + assert len(result) == len(expected_products) + mock_repo.get_all_by_category_id.assert_called_once_with(category_id) + +@patch("product_service.services.ProductRepository") +def test_add_category_to_product(mock_repo): + product_id = "prod123" + category_id = "cat123" + expected_product = { + "id": product_id, + "name": "Apple", + "description": "Fresh red apple", + "price": 10.0, + "brand": "Test Brand", + "quantity": 5, + "category": category_id + } + + mock_repo.add_category_to_product.return_value = expected_product + + result = ProductServices.add_category_to_product(product_id, category_id) + assert result["category"] == expected_product["category"] + mock_repo.add_category_to_product.assert_called_once_with(product_id, category_id) + +@patch("product_service.services.ProductRepository") +def test_remove_category_from_product(mock_repo): + product_id = "prod123" + expected_product = { + "id": product_id, + "name": "Apple", + "description": "Fresh red apple", + "price": 10.0, + "brand": "Test Brand", + "quantity": 5, + "category": None + } + + mock_repo.remove_category_from_product.return_value = expected_product + + result = ProductServices.remove_category_from_product(product_id) + assert result["category"] is None + mock_repo.remove_category_from_product.assert_called_once_with(product_id) \ No newline at end of file From b4172df109eb1db1bfe3145c8032877f8e7b02be Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Mon, 16 Mar 2026 21:51:50 +0530 Subject: [PATCH 17/30] write seed scripts and setup tests for writing integration tests --- backend/python/conftest.py | 66 ++++++++++++++++++++++++++++++++++++++ backend/python/pytest.ini | 3 ++ 2 files changed, 69 insertions(+) create mode 100644 backend/python/conftest.py create mode 100644 backend/python/pytest.ini diff --git a/backend/python/conftest.py b/backend/python/conftest.py new file mode 100644 index 000000000..ed2919f41 --- /dev/null +++ b/backend/python/conftest.py @@ -0,0 +1,66 @@ +import pytest +from pymongo import MongoClient +from mongoengine import disconnect, connect + +@pytest.fixture(scope="session") +def mongo_client(): + client = MongoClient("mongodb://root:example@localhost:27019/?authSource=admin") + yield client + client.close() + +@pytest.fixture(scope="session", autouse=True) +def setup_database(): + disconnect() + connect( + db="test_db", + host="mongodb://root:example@localhost:27019/test_db?authSource=admin" + ) + yield + disconnect() + + +@pytest.fixture(scope="function") +def test_db(mongo_client): + db = mongo_client["test_db"] + + for collection in db.list_collection_names(): + db[collection].delete_many({}) + + yield db + + for collection in db.list_collection_names(): + db[collection].delete_many({}) + +@pytest.fixture +def seeded_categories(test_db): + from product_category.models import ProductCategory + + cat1 = ProductCategory(name="Fruits", description="Fresh fruits").save() + cat2 = ProductCategory(name="Vegetables", description="Fresh vegetables").save() + + return [cat1.id, cat2.id] + +from product_service.models import Product + +@pytest.fixture +def seeded_products(seeded_categories): + p1 = Product( + name="Apple", + price=10, + category=str(seeded_categories[0]), + brand="Fruit Brand", + quantity=50 + ).save() + + p2 = Product( + name="Carrot", + price=8, + category=str(seeded_categories[1]), + brand="Veg Brand", + quantity=40 + ).save() + + return [p1.id, p2.id] + + + \ No newline at end of file diff --git a/backend/python/pytest.ini b/backend/python/pytest.ini new file mode 100644 index 000000000..044e42026 --- /dev/null +++ b/backend/python/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE = django_app.settings +python_files = test_*.py *_tests.py \ No newline at end of file From 219479df358ffdf29ecad5b275d901f1fc3b8821 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Mon, 16 Mar 2026 21:53:45 +0530 Subject: [PATCH 18/30] Write an integration test that tests the various APIs --- .../tests/integration/test_api.py | 56 +++++++++++++ backend/python/product_category/urls.py | 10 +-- backend/python/product_category/views.py | 8 +- .../tests/integration/test_product_api.py | 83 +++++++++++++++++++ backend/python/product_service/urls.py | 14 ++-- backend/python/product_service/views.py | 2 +- 6 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 backend/python/product_category/tests/integration/test_api.py create mode 100644 backend/python/product_service/tests/integration/test_product_api.py diff --git a/backend/python/product_category/tests/integration/test_api.py b/backend/python/product_category/tests/integration/test_api.py new file mode 100644 index 000000000..4c30bac28 --- /dev/null +++ b/backend/python/product_category/tests/integration/test_api.py @@ -0,0 +1,56 @@ +from django.urls import reverse + +def test_get_all_product_categories(client, seeded_categories): + url = reverse("get_all_product_categories") + response = client.get(url) + + assert response.status_code == 200 + + data = response.json() + product_category_data = data["product_category"] + + assert len(product_category_data) == 2 + + names = [c["name"] for c in product_category_data] + + assert "Fruits" in names + assert "Vegetables" in names + +def test_get_product_category_by_id(client, seeded_categories): + category_id = seeded_categories[0] + url = reverse("get_product_category_by_id", args=[category_id]) + response = client.get(url) + assert response.status_code == 200 + product_category_data = response.json() + assert product_category_data["id"] == str(category_id) + assert product_category_data["name"] == "Fruits" + +def test_create_product_category(client): + url = reverse("create_product_category") + data = {"name": "Dairy"} + response = client.post(url, data, content_type="application/json") + assert response.status_code == 200 + product_category_data = response.json() + assert product_category_data["name"] == "Dairy" + +def test_update_product_category(client, seeded_categories): + category_id = seeded_categories[0] + url = reverse("update_product_category", args=[category_id]) + data = {"name": "Fresh Fruits"} + response = client.put(url, data, content_type="application/json") + assert response.status_code == 200 + product_category_data = response.json() + assert product_category_data["id"] == str(category_id) + assert product_category_data["name"] == "Fresh Fruits" + +def test_delete_product_category(client, seeded_categories): + category_id = seeded_categories[0] + url = reverse("delete_product_category", args=[category_id]) + response = client.delete(url) + + assert response.status_code == 200 + # Verify the category is deleted + get_url = reverse("get_product_category_by_id", args=[category_id]) + get_response = client.get(get_url) + assert get_response.status_code == 404 + diff --git a/backend/python/product_category/urls.py b/backend/python/product_category/urls.py index 93df67b83..b54dad914 100644 --- a/backend/python/product_category/urls.py +++ b/backend/python/product_category/urls.py @@ -2,9 +2,9 @@ from . import views urlpatterns = [ - path("", views.get_all_product_categories), - path("create/", views.create_product_category), - path("/", views.get_product_category_by_id), - path("update//", views.update_product_category), - path("delete//", views.delete_product_category), + path("", views.get_all_product_categories , name="get_all_product_categories"), + path("create/", views.create_product_category , name="create_product_category"), + path("/", views.get_product_category_by_id, name="get_product_category_by_id"), + path("update//", views.update_product_category, name="update_product_category"), + path("delete//", views.delete_product_category, name="delete_product_category"), ] diff --git a/backend/python/product_category/views.py b/backend/python/product_category/views.py index 5808f0846..72b0893f3 100644 --- a/backend/python/product_category/views.py +++ b/backend/python/product_category/views.py @@ -41,7 +41,7 @@ def get_product_category_by_id(request, product_category_id): "description": product_category.description }) except ValueError as e: - return {"error": str(e)} + return JsonResponse({"error": str(e)} , status=404) return {"error": "Invalid request method"} @csrf_exempt @@ -64,8 +64,10 @@ def delete_product_category(request, product_category_id): if request.method == 'DELETE': product_category = ProductCategoryService.delete_product_category(product_category_id) if product_category: - return JsonResponse({"message": "Product category deleted"}) + return JsonResponse({"message": "Product category deleted"}, status=200) else: - return JsonResponse({"error": "Product category not found"}) + return JsonResponse({"error": "Product category not found"}, status=404) return {"error": "Invalid request method"} + + \ No newline at end of file diff --git a/backend/python/product_service/tests/integration/test_product_api.py b/backend/python/product_service/tests/integration/test_product_api.py new file mode 100644 index 000000000..e8d19af58 --- /dev/null +++ b/backend/python/product_service/tests/integration/test_product_api.py @@ -0,0 +1,83 @@ +from django.urls import reverse + +def test_list_products(client, seeded_products): + url = reverse("list_products") + response = client.get(url) + + assert response.status_code == 200 + + products_data = response.json() + + assert len(products_data) == 2 + + names = [p["name"] for p in products_data] + + assert "Apple" in names + assert "Carrot" in names + +def test_get_product_by_id(client, seeded_products): + product_id = seeded_products[0] + url = reverse("get_product", args=[product_id]) + response = client.get(url) + assert response.status_code == 200 + product_data = response.json() + assert product_data["id"] == str(product_id) + assert product_data["name"] == "Apple" + +def test_create_product(client, seeded_categories): + url = reverse("create_product") + data = { + "name": "Banana", + "price": 12, + "category": str(seeded_categories[0]), + "brand": "Fruit Brand", + "quantity": 30 + } + response = client.post(url, data, content_type="application/json") + assert response.status_code == 200 + get_url=reverse("get_product", args=[response.json()["id"]]) + get_response = client.get(get_url) + product_data = get_response.json() + assert product_data["name"] == "Banana" + +def test_delete_product(client, seeded_products): + product_id = seeded_products[0] + url = reverse("delete_product", args=[product_id]) + response = client.delete(url) + + assert response.status_code == 200 + # Verify the product is deleted + get_url = reverse("get_product", args=[product_id]) + get_response = client.get(get_url) + assert get_response.status_code == 404 + +def test_list_products_by_category_id(client, seeded_categories, seeded_products): + category_id = seeded_categories[0] + url = reverse("list_products_by_category_id", args=[category_id]) + response = client.get(url) + assert response.status_code == 200 + products_data = response.json() + assert len(products_data) == 1 + assert products_data[0]["name"] == "Apple" + +def test_remove_category_from_product(client, seeded_products): + product_id = seeded_products[0] + url = reverse("remove_category_from_product", args=[product_id]) + response = client.patch(url) + print(response) + assert response.status_code == 200 + get_url = reverse("get_product", args=[product_id]) + get_response = client.get(get_url) + product_data = get_response.json() + assert product_data["category"] is None + +def test_add_category_to_product(client, seeded_products, seeded_categories): + product_id = seeded_products[0] + category_id = seeded_categories[1] + url = reverse("add_category_to_product", args=[product_id, category_id]) + response = client.patch(url) + assert response.status_code == 200 + get_url = reverse("get_product", args=[product_id]) + get_response = client.get(get_url) + product_data = get_response.json() + assert product_data["category"] == str(category_id) \ No newline at end of file diff --git a/backend/python/product_service/urls.py b/backend/python/product_service/urls.py index 0a3290072..98e587be2 100644 --- a/backend/python/product_service/urls.py +++ b/backend/python/product_service/urls.py @@ -2,11 +2,11 @@ from . import views urlpatterns = [ - path("", views.list_products), - path("create/", views.create_product), - path("/", views.get_product), - path("delete//", views.delete_product), - path("products-by-category//", views.list_products_by_category_id), - path("remove-category//", views.remove_category_from_product), - path("add-category///", views.add_category_to_product), + path("", views.list_products, name="list_products"), + path("create/", views.create_product, name="create_product"), + path("/", views.get_product , name="get_product"), + path("delete//", views.delete_product, name="delete_product"), + path("products-by-category//", views.list_products_by_category_id, name="list_products_by_category_id"), + path("remove-category//", views.remove_category_from_product, name="remove_category_from_product"), + path("add-category///", views.add_category_to_product, name="add_category_to_product"), ] \ No newline at end of file diff --git a/backend/python/product_service/views.py b/backend/python/product_service/views.py index fde2cf34d..e87784113 100644 --- a/backend/python/product_service/views.py +++ b/backend/python/product_service/views.py @@ -84,7 +84,7 @@ def remove_category_from_product(request, product_id): "description": product.description, "price": product.price, "category": None - }) + },status=200) except ValueError as er: return JsonResponse({"error": str(er)}, status=404) else: From 1f58885ad0de71fca46684c343560df1de5d1d6c Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Thu, 26 Mar 2026 21:35:23 +0530 Subject: [PATCH 19/30] Improve test coverage --- backend/python/.coverage | Bin 0 -> 53248 bytes backend/python/.coveragerc | 3 + .../tests/integration/test_api.py | 46 +++++++++- .../tests/unit/test_services.py | 12 ++- backend/python/product_category/views.py | 12 +-- .../tests/integration/test_product_api.py | 82 +++++++++++++++++- .../tests/unit/test_service.py | 20 ++++- 7 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 backend/python/.coverage create mode 100644 backend/python/.coveragerc diff --git a/backend/python/.coverage b/backend/python/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..cf662b9a27b7db2f6f22a3fd895275db21b3a593 GIT binary patch literal 53248 zcmeI4Yit}>6~||G#ydN^JA2oU_1YMd2^Bb|wu2KWkOYM^jS}bCKoSaUx|!V_+tcjs zY-VPin8-DoMoI;r;2RnD>yD*mb<6!$c7Af#jw8F&BRlTjze{!ZsT;y7ETg094%ISG zt0mo1Cyl(W8pWKE)f}TZr8?8PpSx`9IXY&+J{YxV#xYi&uBD9}lydYbkSJM3L9=Gn zlltrk%ix{O(a$*U4iM3esUppxZt#i^tCl{gTY538+pZNGjNEW|__6u0lWjGiq!dhV_j9HG$s1>n`!2DDDl3Y~CEdgrR6Q!9Lzl3sSN@FW;lG<~eKg7nl- znAuz`7czkSk#eC#8wX0bCw9zcC7YFXOs}$Uxoz&!mbLmeG^dZ8wy0aGTr^IV^=jjf zsI`(KtQtjgM5UDg4|rcBnEY5b&w*F!XY5mXxO7TuWyfUOX=wcPIDAFVcZQ_ox^?`8 zQ_N8)o^-}=?COf#;560UOLCxgNOrx(ckFj;=AP@`uY?I|;mUb1J)*)-o=$1C6k9kMku<&*} z61dK|98B(7*UWXQE|4Ctu7)f9K`A*fz@L+u8+oneB~pV6`7L>uFzO#F=$c)&{6O+% zLDDZ#i;{jH@s2=sMx|hK$3U~{c=^)f-b$z25H5s3Vx%nrLP?H6cAtzk}!F0cP^!0;6Ampie(SgUNJH&E$Rh?C6Y^ zhiFpL3=6s}#9(&>P-(`T2AQ>tS$3HJ(oN=`bnL0w>k8DiefE%Rm}Q6V{h(>v1`Mlo z*s{0yd)1+7&8AVv9#S)=nb)tP z?oJz}PVw}#zD&*?jULb(-7yNfO0zIym}8yRY&C1?aL^k|nPEU|`cA7{nLTjCkD+dD zhKGbV^Bvm6>2dl^XUQRYDOXv?OUZlhyCppCp!dD z8ZMFQor?mmV@NmMGth|WHhacJ=jZY+kix@sg3psh4*qaK0!RP}AOR$R1dsp{Kmter z2_OL^fCSba0f7(j5}p4C$jcnL4qvz+0VIF~kN^@u0!RP}AOR$R1dsp{KmzYb0?~jl zNc?{b`CyzE2Kv4K0r<$+_~x+>(|__0kZT;dMqYnMRuGLw0!RP}AOR$R1dsp{Kmter z2_OL^fCQ=pqQW5W-v$VTg#p>S3qXJW?+bm6BTtbcQDc{57h>C#e<|Nk9#W#w7ox=| ziTpHTMx^j}!l`gneqNrFH--KV1#m$ENB{{S0VIF~kihB_*xM&^mHwSuQb%ogdNfrk z+tVjg^ua}|sKbuYyp~C2wCqW}m`jyrooTa}Dp_W(oOROh#t41Nv}RK?hJMjbr}ZK*lB?bs=JpxQ~%)wHJ$;*)U@ zKfDU!?7>f% z4YX7#-|4D8Q!sNni(Y%B71z7Th%)uRuU_-%9rwtN<1J|BCRRa zU08Y|J%_}~`qo5z;J!HxbJOkvVr8&BJx`Zj@^+=f%GUPOn#@c=ymLyd99|9aW%E*> zkUKw(KPXo2S=o82Q>F&S3eDEjg7aSS-N6MN!X4j zZwfuVu=0W4m*#(cZ2Ro|*-Ni4y}EQ+k`nZA;wF)L=t|FZA<_*iechK&FD%@MMY`xl z*DK|xt}kru>x7L>otGbddj5w?|9I)<{JgXH_3MAWeMst$!;ufgXO1o|+*r8rrx$+z z>g~Cuxo4MdJ$YZ4z@c99#+6%(yc~n|p4jIX76%mtR^P2$KDhAXXBMsuc126s z;HF>)O*{CYNK`ZSI`(m;0Uw-(T+FMOlvV?D>E4S&l4{ zf0KWbzmcot4{#UYSLA2pC*%j@N%AeY5Aap;74ju=9_|Lr5R*(34GQ3b1dsp{Kmter z2_OL^fCP{L5q&EC~cexB Date: Thu, 26 Mar 2026 23:52:24 +0530 Subject: [PATCH 20/30] Add Makefile --- Makefile | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..0ae246ed2 --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +# Variables +VENV = venv +VENV = venv +BACKEND_DIR = backend\python +FRONTEND_DIR = frontend +PYTHON = $(VENV)\Scripts\python +PIP = $(VENV)\Scripts\pip + +# Create virtual environment +venv: + python -m venv $(VENV) + +# Install backend dependencies +install-backend: venv + $(PIP) install --upgrade pip + $(PIP) install -r $(BACKEND_DIR)\requirements.txt + +# Install frontend dependencies +install-frontend: + cd $(FRONTEND_DIR) && yarn install + +# Setup everything +setup: install-backend install-frontend + +# Run backend +backend: + cd $(BACKEND_DIR) && ../../$(PYTHON) manage.py runserver + +# Run frontend +frontend: + cd $(FRONTEND_DIR) && yarn start + +#containerize backend +containerize-backend: + cd $(BACKEND_DIR) && docker compose up --build + +#decontainerize backend +decontainerize-backend: + cd $(BACKEND_DIR) && docker compose down + +# Run tests +test: + cd $(BACKEND_DIR) && $(PYTHON) -m pytest + +# Run coverage +coverage: + cd $(BACKEND_DIR) && $(PYTHON) -m pytest --cov=. --cov-report=term-missing + +html-coverage: + cd $(BACKEND_DIR) && $(PYTHON) -m pytest --cov=. --cov-report=html + +# Lint +lint: + $(PYTHON) -m flake8 . + +# Format +format: + $(PYTHON) -m black . \ No newline at end of file From d43af2a7b31d256ae0f99486b95511195d2ed8ec Mon Sep 17 00:00:00 2001 From: Pushkar06p <152164252+Pushkar06p@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:00:34 +0530 Subject: [PATCH 21/30] Revise README for project details and setup instructions Updated project title, structure, features, and setup instructions in README.md. --- README.md | 194 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 112 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 2d09412d9..c2c57c75c 100644 --- a/README.md +++ b/README.md @@ -1,127 +1,157 @@ -# Interneers Lab +# Product Service & Product Category API -Welcome to the **Interneers Lab** repository! This serves as a minimal starter kit for learning and experimenting with: -- **Django** (Python) -- **Golang** (Go) -- **React** (with TypeScript) -- **MongoDB** (via Docker Compose) -- Development environment in **VSCode** (recommended) +This repository contains backend services for managing **Products** and **Product Categories**. +The project includes APIs, service layers, unit tests, integration tests, and automated development workflows using a **Makefile**. -**Important:** Use the **same email** you shared during onboarding when configuring Git and related tools. That ensures consistency across all internal systems. - -### Project structure +--- +## Project Structure ``` -backend/ - go/ # Golang backend (see backend/go/README.md) - python/ # Django (Python) backend (see backend/python/README.md) -frontend/ # React + TypeScript (see frontend/README.md) +├── backend/ +│ ├── python/ +│ │ ├── django_app/ +│ │ ├── htmlcov/ +│ │ ├── product_service/ +│ │ ├── product_category/ +│ │ ├── warehouse/ +│ │ ├── docker-compose.yaml +│ │ ├── pytest.ini +│ │ └── requirements.txt +| | +│ └── go/ +│ +├── frontend/ +│ +├── Makefile +├── README.md +└── CHANGELOG.md ``` +--- + +## Features + +* Product CRUD APIs +* Product Category APIs +* Service layer abstraction +* Unit and integration tests +* Test coverage reporting +* Automated development commands using Makefile --- -## Table of Contents +## Prerequisites + +Before running the project ensure the following are installed: -1. [Getting Started with Git & Forking](#getting-started-with-git-and-forking) -2. [Prerequisites & where to find them](#prerequisites--where-to-find-them) -3. [Setting up & running](#setting-up--running) -4. [Development Workflow](#development-workflow) - - [Pushing Your First Change](#pushing-your-first-change) -5. [Making your first change](#making-your-first-change) -6. [Running Tests](#running-tests) -7. [Frontend Setup](#frontend-setup) -8. [Further Reading](#further-reading) +* Python 3.10+ +* pip +* Node.js (for frontend) +* Yarn +* Make --- -## Getting Started with Git and Forking +## Setup Instructions -### 1. Setting up Git and the Repo +Clone the repository: -1. **Install Git** (if not already): - - **macOS**: [Homebrew](https://brew.sh/) users can run `brew install git`. - - **Windows**: Use [Git for Windows](https://gitforwindows.org/). - - **Linux**: Install via your distro's package manager, e.g., `sudo apt-get install git` (Ubuntu/Debian). +```bash +git clone +cd interneers-lab +``` -2. **Configure Git** with your name and email: - ```bash - git config --global user.name "Your Name" - git config --global user.email "your.email@example.com" # Use the same email you shared during onboarding - ``` +Create virtual environment and install dependencies: -3. **What is Forking?** +```bash +make setup +``` - Forking a repository on GitHub creates your own copy under your GitHub account, where you can make changes independently without affecting the original repo. Later, you can make pull requests to merge changes back if needed. +This command will: -4. Fork the Rippling/interneers-lab repository (ensure you're in the correct org or your personal GitHub account, as directed). -5. **Clone** your forked repo: - ```bash - git clone git@github.com:/interneers-lab.git - cd interneers-lab - ``` +* Create a Python virtual environment +* Install backend dependencies +* Install frontend dependencies -## Prerequisites & where to find them +--- -Prerequisites (Python, Go, Node, Docker, etc.) and how to verify your setup are documented in each part of the repo: +## Running the Backend -- **[backend/python/README.md](backend/python/README.md)** — Python/Django, virtualenv, MongoDB -- **[backend/go/README.md](backend/go/README.md)** — Go, MongoDB -- **[frontend/README.md](frontend/README.md)** — Node, Yarn, React +Start the backend server: -Use the README for the part you're working on. +```bash +make backend +``` --- -## Setting up & running +## Running the Frontend -Setup and run instructions live in the domain READMEs: +Start the frontend application: -- **Python backend:** [backend/python/README.md](backend/python/README.md) — venv, dependencies, `runserver`, Docker Compose for MongoDB -- **Go backend:** [backend/go/README.md](backend/go/README.md) — `make setup`, `make build-and-run`, Docker Compose -- **Frontend:** [frontend/README.md](frontend/README.md) +```bash +make frontend +``` --- -## Development Workflow +## Running Tests -### Making your first change +Execute all tests: -Step-by-step tutorials live in the domain READMEs: +```bash +make test +``` -- **[backend/python/README.md](backend/python/README.md)** — Django starters (e.g. Hello World, Hello {name} API) -- **[backend/go/README.md](backend/go/README.md)** — Go hello-world and APIs -- **[frontend/README.md](frontend/README.md)** — React hello-world and APIs +This will run **pytest test suites** including unit tests. -### Pushing Your First Change +--- -1. **Stage and commit**: - ```bash - git add . - git commit -m "Your descriptive commit message" - ``` -2. **Push to your forked repo (main branch by default):** - ```bash - git push origin main - ``` +## Running Tests with Coverage + +Generate test coverage: + +```bash +make coverage +``` + +Example output: + +``` +---------- coverage ---------- +Name Stmts Miss Cover +---------------------------------------------- +product_service/service.py 45 3 93% +product_category/service.py 30 2 93% +---------------------------------------------- +TOTAL 75 5 93% +``` --- -## Running Tests +## Development Workflow -See the domain READMEs for how to run tests in each stack: +Typical development workflow: -- [backend/python/README.md](backend/python/README.md) -- [backend/go/README.md](backend/go/README.md) -- [frontend/README.md](frontend/README.md) +```bash +make setup +make test +make coverage +``` --- -## Further Reading +## Testing Overview + +The project includes: + +### Unit Tests + +* Test service layer logic +* Mock repository layers + +### Integration Tests -Each domain has detailed README with links to relevant docs. In general: +* Validate API endpoints +* Test full request-response flow -- **Django:** [docs.djangoproject.com](https://docs.djangoproject.com/) -- **React:** [react.dev](https://react.dev/learn) -- **Go:** [go.dev/doc](https://go.dev/doc/) -- **MongoDB:** [docs.mongodb.com](https://docs.mongodb.com/) -- **Docker Compose:** [docs.docker.com/compose](https://docs.docker.com/compose/) +Integration tests may require additional services (e.g. MongoDB) and may be skipped if dependencies are unavailable. From d353f78cda317b3133594a54960cd1ae7df9b20e Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 1 Apr 2026 13:10:31 +0530 Subject: [PATCH 22/30] refactor: merge product_category into product_service --- backend/python/product_category/__init__.py | 0 backend/python/product_category/admin.py | 3 - backend/python/product_category/apps.py | 5 - .../product_category/migrations/__init__.py | 0 backend/python/product_category/models.py | 5 - backend/python/product_category/repository.py | 33 ---- backend/python/product_category/services.py | 28 ---- .../tests/integration/test_api.py | 98 ------------ .../tests/unit/test_services.py | 60 ------- backend/python/product_category/urls.py | 10 -- backend/python/product_category/views.py | 73 --------- backend/python/product_service/services.py | 45 ------ .../tests/unit/test_service.py | 147 ------------------ backend/python/product_service/views.py | 116 -------------- backend/python/warehouse/__init__.py | 0 backend/python/warehouse/admin.py | 3 - backend/python/warehouse/apps.py | 5 - .../python/warehouse/migrations/__init__.py | 0 backend/python/warehouse/models.py | 13 -- backend/python/warehouse/storage.py | 4 - backend/python/warehouse/tests.py | 3 - backend/python/warehouse/urls.py | 10 -- backend/python/warehouse/views.py | 99 ------------ 23 files changed, 760 deletions(-) delete mode 100644 backend/python/product_category/__init__.py delete mode 100644 backend/python/product_category/admin.py delete mode 100644 backend/python/product_category/apps.py delete mode 100644 backend/python/product_category/migrations/__init__.py delete mode 100644 backend/python/product_category/models.py delete mode 100644 backend/python/product_category/repository.py delete mode 100644 backend/python/product_category/services.py delete mode 100644 backend/python/product_category/tests/integration/test_api.py delete mode 100644 backend/python/product_category/tests/unit/test_services.py delete mode 100644 backend/python/product_category/urls.py delete mode 100644 backend/python/product_category/views.py delete mode 100644 backend/python/product_service/services.py delete mode 100644 backend/python/product_service/tests/unit/test_service.py delete mode 100644 backend/python/product_service/views.py delete mode 100644 backend/python/warehouse/__init__.py delete mode 100644 backend/python/warehouse/admin.py delete mode 100644 backend/python/warehouse/apps.py delete mode 100644 backend/python/warehouse/migrations/__init__.py delete mode 100644 backend/python/warehouse/models.py delete mode 100644 backend/python/warehouse/storage.py delete mode 100644 backend/python/warehouse/tests.py delete mode 100644 backend/python/warehouse/urls.py delete mode 100644 backend/python/warehouse/views.py diff --git a/backend/python/product_category/__init__.py b/backend/python/product_category/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/python/product_category/admin.py b/backend/python/product_category/admin.py deleted file mode 100644 index 8c38f3f3d..000000000 --- a/backend/python/product_category/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/backend/python/product_category/apps.py b/backend/python/product_category/apps.py deleted file mode 100644 index 23c00572f..000000000 --- a/backend/python/product_category/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ProductCategoryConfig(AppConfig): - name = 'product_category' diff --git a/backend/python/product_category/migrations/__init__.py b/backend/python/product_category/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/python/product_category/models.py b/backend/python/product_category/models.py deleted file mode 100644 index 810376aff..000000000 --- a/backend/python/product_category/models.py +++ /dev/null @@ -1,5 +0,0 @@ -from mongoengine import Document, StringField - -class ProductCategory(Document): - name = StringField(required=True) - description = StringField() \ No newline at end of file diff --git a/backend/python/product_category/repository.py b/backend/python/product_category/repository.py deleted file mode 100644 index 516afbe3f..000000000 --- a/backend/python/product_category/repository.py +++ /dev/null @@ -1,33 +0,0 @@ -from .models import ProductCategory - -class ProductCategoryRepository: - @staticmethod - def create(data): - product_category = ProductCategory(**data) - product_category.save() - return product_category - - @staticmethod - def get_all(): - return ProductCategory.objects() - - @staticmethod - def get_by_id(product_category_id): - return ProductCategory.objects(id=product_category_id).first() - - @staticmethod - def update(product_category_id, data): - product_category = ProductCategory.objects(id=product_category_id).first() - if product_category: - for key, value in data.items(): - setattr(product_category, key, value) - product_category.save() - return product_category - - @staticmethod - def delete(product_category_id): - product_category = ProductCategory.objects(id=product_category_id).first() - if product_category: - product_category.delete() - return product_category - diff --git a/backend/python/product_category/services.py b/backend/python/product_category/services.py deleted file mode 100644 index 7412a1327..000000000 --- a/backend/python/product_category/services.py +++ /dev/null @@ -1,28 +0,0 @@ -from .repository import ProductCategoryRepository - -class ProductCategoryService: - @staticmethod - def create_product_category(data): - return ProductCategoryRepository.create(data) - - @staticmethod - def get_all_product_categories(): - return ProductCategoryRepository.get_all() - - @staticmethod - def get_product_category_by_id(product_category_id): - product_category = ProductCategoryRepository.get_by_id(product_category_id) - if not product_category: - raise ValueError("Product Category not found") - return product_category - - @staticmethod - def update_product_category(product_category_id, data): - product_category = ProductCategoryRepository.get_by_id(product_category_id) - if not product_category: - raise ValueError("Product Category not found") - return ProductCategoryRepository.update(product_category_id, data) - - @staticmethod - def delete_product_category(product_category_id): - return ProductCategoryRepository.delete(product_category_id) \ No newline at end of file diff --git a/backend/python/product_category/tests/integration/test_api.py b/backend/python/product_category/tests/integration/test_api.py deleted file mode 100644 index a67703ecc..000000000 --- a/backend/python/product_category/tests/integration/test_api.py +++ /dev/null @@ -1,98 +0,0 @@ -from django.urls import reverse - -def test_get_all_product_categories(client, seeded_categories): - url = reverse("get_all_product_categories") - response = client.get(url) - - assert response.status_code == 200 - - data = response.json() - product_category_data = data["product_category"] - - assert len(product_category_data) == 2 - - names = [c["name"] for c in product_category_data] - - assert "Fruits" in names - assert "Vegetables" in names - -def test_get_all_product_categories_no_get(client): - url = reverse("get_all_product_categories") - response = client.post(url) - assert response.status_code == 400 - -def test_get_product_category_by_id(client, seeded_categories): - category_id = seeded_categories[0] - url = reverse("get_product_category_by_id", args=[category_id]) - response = client.get(url) - assert response.status_code == 200 - product_category_data = response.json() - assert product_category_data["id"] == str(category_id) - assert product_category_data["name"] == "Fruits" - -def test_get_product_category_by_id_no_get(client, seeded_categories): - category_id = seeded_categories[0] - url = reverse("get_product_category_by_id", args=[category_id]) - response = client.post(url) - assert response.status_code == 400 - -def test_create_product_category(client): - url = reverse("create_product_category") - data = {"name": "Dairy"} - response = client.post(url, data, content_type="application/json") - assert response.status_code == 200 - product_category_data = response.json() - assert product_category_data["name"] == "Dairy" - -def test_create_product_category_no_post(client): - url = reverse("create_product_category") - response = client.get(url) - assert response.status_code == 400 - -def test_update_product_category(client, seeded_categories): - category_id = seeded_categories[0] - url = reverse("update_product_category", args=[category_id]) - data = {"name": "Fresh Fruits"} - response = client.put(url, data, content_type="application/json") - assert response.status_code == 200 - product_category_data = response.json() - assert product_category_data["id"] == str(category_id) - assert product_category_data["name"] == "Fresh Fruits" - -def test_update_product_category_not_found(client): - category_id = "000000000000000000000000" - url = reverse("update_product_category", args=[category_id]) - data = {"name": "Nonexistent"} - response = client.put(url, data, content_type="application/json") - assert response.status_code == 404 - -def test_update_product_category_no_put(client, seeded_categories): - category_id = seeded_categories[0] - url = reverse("update_product_category", args=[category_id]) - response = client.get(url) - assert response.status_code == 400 - -def test_delete_product_category(client, seeded_categories): - category_id = seeded_categories[0] - url = reverse("delete_product_category", args=[category_id]) - response = client.delete(url) - - assert response.status_code == 200 - # Verify the category is deleted - get_url = reverse("get_product_category_by_id", args=[category_id]) - get_response = client.get(get_url) - assert get_response.status_code == 404 - -def test_delete_product_category_not_found(client): - # category id must be it must be a 12-byte input or a 24-character hex string - category_id="000000000000000000000000" - url = reverse("delete_product_category", args=[category_id]) - response = client.delete(url) - assert response.status_code == 404 - -def test_delete_product_category_no_delete(client, seeded_categories): - category_id = seeded_categories[0] - url = reverse("delete_product_category", args=[category_id]) - response = client.get(url) - assert response.status_code == 400 - diff --git a/backend/python/product_category/tests/unit/test_services.py b/backend/python/product_category/tests/unit/test_services.py deleted file mode 100644 index 1319bc30d..000000000 --- a/backend/python/product_category/tests/unit/test_services.py +++ /dev/null @@ -1,60 +0,0 @@ -import pytest -from product_category.services import ProductCategoryService - -from unittest.mock import patch - -@patch("product_category.services.ProductCategoryRepository") -def test_create_product_category(mock_repo): - - data = { - "name": "Fruits", - "description": "All kinds of fruits" - } - - mock_repo.create.return_value = data - - result = ProductCategoryService.create_product_category(data) - assert result["name"] == data["name"] - mock_repo.create.assert_called_once_with(data) - -@patch("product_category.services.ProductCategoryRepository") -def test_get_all_product_categories(mock_repo): - mock_repo.get_all.return_value = [ - {"name": "Fruits", "description": "All kinds of fruits"}, - {"name": "Vegetables", "description": "All kinds of vegetables"} - ] - - result = ProductCategoryService.get_all_product_categories() - assert len(result) == 2 - mock_repo.get_all.assert_called_once() - -@patch("product_category.services.ProductCategoryRepository") -def test_get_product_category_by_id(mock_repo): - mock_repo.get_by_id.return_value = {"name": "Fruits", "description": "All kinds of fruits"} - - result = ProductCategoryService.get_product_category_by_id(1) - assert result["name"] == "Fruits" - mock_repo.get_by_id.assert_called_once_with(1) - -@patch("product_category.services.ProductCategoryRepository") -def test_update_product_category(mock_repo): - mock_repo.get_by_id.return_value = {"name": "Fruits", "description": "All kinds of fruits"} - mock_repo.update.return_value = {"name": "Fruits", "description": "Fresh fruits"} - - data = {"description": "Fresh fruits"} - result = ProductCategoryService.update_product_category(1, data) - assert result["description"] == "Fresh fruits" - mock_repo.get_by_id.assert_called_once_with(1) - mock_repo.update.assert_called_once_with(1, data) - -@patch("product_category.services.ProductCategoryRepository") -def test_update_product_category_not_found(mock_repo): - mock_repo.get_by_id.return_value = None - - data = {"description": "Fresh fruits"} - with pytest.raises(ValueError) as excinfo: - ProductCategoryService.update_product_category(1, data) - - assert str(excinfo.value) == "Product Category not found" - mock_repo.get_by_id.assert_called_once_with(1) - mock_repo.update.assert_not_called() \ No newline at end of file diff --git a/backend/python/product_category/urls.py b/backend/python/product_category/urls.py deleted file mode 100644 index b54dad914..000000000 --- a/backend/python/product_category/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path("", views.get_all_product_categories , name="get_all_product_categories"), - path("create/", views.create_product_category , name="create_product_category"), - path("/", views.get_product_category_by_id, name="get_product_category_by_id"), - path("update//", views.update_product_category, name="update_product_category"), - path("delete//", views.delete_product_category, name="delete_product_category"), -] diff --git a/backend/python/product_category/views.py b/backend/python/product_category/views.py deleted file mode 100644 index d7259d9f3..000000000 --- a/backend/python/product_category/views.py +++ /dev/null @@ -1,73 +0,0 @@ -import json -from .services import ProductCategoryService -from django.views.decorators.csrf import csrf_exempt -from django.http import JsonResponse - -@csrf_exempt -def create_product_category(request): - if request.method == 'POST': - data = json.loads(request.body) - product_category = ProductCategoryService.create_product_category(data) - return JsonResponse({ - "id": str(product_category.id), - "name": product_category.name, - "description": product_category.description, - "message": "Product category created" - }) - return JsonResponse({"error": "Invalid request method"},status=400) - -def get_all_product_categories(request): - if request.method == 'GET': - product_categories = ProductCategoryService.get_all_product_categories() - return JsonResponse({ - "product_category": [ - { - "id": str(pc.id), - "name": pc.name, - "description": pc.description - } - for pc in product_categories - ] - }) - return JsonResponse({"error": "Invalid request method"}, status=400) - -def get_product_category_by_id(request, product_category_id): - if request.method == 'GET': - try: - product_category = ProductCategoryService.get_product_category_by_id(product_category_id) - return JsonResponse({ - "id": str(product_category.id), - "name": product_category.name, - "description": product_category.description - }) - except ValueError as e: - return JsonResponse({"error": str(e)} , status=404) - return JsonResponse({"error": "Invalid request method"}, status=400) - -@csrf_exempt -def update_product_category(request, product_category_id): - if request.method == 'PUT': - data = json.loads(request.body) - try: - product_category = ProductCategoryService.update_product_category(product_category_id, data) - return JsonResponse({ - "id": str(product_category.id), - "name": product_category.name, - "description": product_category.description - }) - except ValueError as e: - return JsonResponse({"error": str(e)}, status=404) - return JsonResponse({"error": "Invalid request method"}, status=400) - -@csrf_exempt -def delete_product_category(request, product_category_id): - if request.method == 'DELETE': - product_category = ProductCategoryService.delete_product_category(product_category_id) - if product_category: - return JsonResponse({"message": "Product category deleted"}, status=200) - else: - return JsonResponse({"error": "Product category not found"}, status=404) - return JsonResponse({"error": "Invalid request method"}, status=400) - - - \ No newline at end of file diff --git a/backend/python/product_service/services.py b/backend/python/product_service/services.py deleted file mode 100644 index 464068803..000000000 --- a/backend/python/product_service/services.py +++ /dev/null @@ -1,45 +0,0 @@ -from .repository import ProductRepository - -class ProductServices(): - - @staticmethod - def create_product(data): - if data.get("price", 0) <= 0: - raise ValueError("Given Price is not valid") - - return ProductRepository.create(data) - - @staticmethod - def list_products(): - return ProductRepository.get_all() - - @staticmethod - def get_product(product_id): - product = ProductRepository.get_by_id(product_id) - - if not product: - raise ValueError("Product not found") - - return product - - @staticmethod - def list_products_by_category_id(category_id): - return ProductRepository.get_all_by_category_id(category_id) - - @staticmethod - def remove_category_from_product(product_id): - product = ProductRepository.remove_category_from_product(product_id) - if not product: - raise ValueError("Product not found") - return product - - @staticmethod - def add_category_to_product(product_id, category_id): - product = ProductRepository.add_category_to_product(product_id, category_id) - if not product: - raise ValueError("Product not found") - return product - - @staticmethod - def delete_product(product_id): - return ProductRepository.delete_by_id(product_id) diff --git a/backend/python/product_service/tests/unit/test_service.py b/backend/python/product_service/tests/unit/test_service.py deleted file mode 100644 index e01c3adcc..000000000 --- a/backend/python/product_service/tests/unit/test_service.py +++ /dev/null @@ -1,147 +0,0 @@ -import pytest -from unittest.mock import patch - -from product_service.services import ProductServices - -@patch("product_service.services.ProductRepository") -def test_create_product(mock_repo): - - data = { - "name": "Apple", - "description": "Fresh red apple", - "price": 10.0, - "brand": "Test Brand", - "quantity": 5 - } - - mock_repo.create.return_value = data - - result = ProductServices.create_product(data) - assert result["name"] == data["name"] - mock_repo.create.assert_called_once_with(data) - -@patch("product_service.services.ProductRepository") -def test_create_product_with_invalid_price(mock_repo): - data = { - "name": "Apple", - "description": "Fresh red apple", - "price": -5.0, - "brand": "Test Brand", - "quantity": 5 - } - - with pytest.raises(ValueError) as excinfo: - ProductServices.create_product(data) - - assert str(excinfo.value) == "Given Price is not valid" - mock_repo.create.assert_not_called() - -@patch("product_service.services.ProductRepository") -def test_get_product_by_id(mock_repo): - product_id = "12345" - expected_product = { - "id": product_id, - "name": "Apple", - "description": "Fresh red apple", - "price": 10.0, - "brand": "Test Brand", - "quantity": 5 - } - - mock_repo.get_by_id.return_value = expected_product - - result = ProductServices.get_product(product_id) - assert result["id"] == expected_product["id"] - mock_repo.get_by_id.assert_called_once_with(product_id) - -@patch("product_service.services.ProductRepository") -def test_get_product_by_id_not_found(mock_repo): - product_id = "12345" - mock_repo.get_by_id.return_value = None - - with pytest.raises(ValueError) as excinfo: - ProductServices.get_product(product_id) - - assert str(excinfo.value) == "Product not found" - mock_repo.get_by_id.assert_called_once_with(product_id) - -@patch("product_service.services.ProductRepository") -def test_list_products_by_category_id(mock_repo): - category_id = "cat123" - expected_products = [ - { - "id": "prod1", - "name": "Apple", - "description": "Fresh red apple", - "price": 10.0, - "brand": "Test Brand", - "quantity": 5 - }, - { - "id": "prod2", - "name": "Banana", - "description": "Ripe yellow banana", - "price": 5.0, - "brand": "Test Brand", - "quantity": 10 - } - ] - - mock_repo.get_all_by_category_id.return_value = expected_products - - result = ProductServices.list_products_by_category_id(category_id) - assert len(result) == len(expected_products) - mock_repo.get_all_by_category_id.assert_called_once_with(category_id) - -@patch("product_service.services.ProductRepository") -def test_add_category_to_product(mock_repo): - # category_id and product_id should be a valid ObjectId, it must be a 12-byte input - product_id = "0000000000000000000000000" - category_id = "0000000000000000000000000" - expected_product = { - "id": product_id, - "name": "Apple", - "description": "Fresh red apple", - "price": 10.0, - "brand": "Test Brand", - "quantity": 5, - "category": category_id - } - - mock_repo.add_category_to_product.return_value = expected_product - - result = ProductServices.add_category_to_product(product_id, category_id) - assert result["category"] == expected_product["category"] - mock_repo.add_category_to_product.assert_called_once_with(product_id, category_id) - -def test_add_category_to_product_not_found(): - with patch("product_service.services.ProductRepository") as mock_repo: - product_id = "0000000000000000000000000" - category_id = "000000000000000000000000" - mock_repo.add_category_to_product.return_value = None - - with pytest.raises(ValueError) as excinfo: - ProductServices.add_category_to_product(product_id, category_id) - - assert str(excinfo.value) == "Product not found" - mock_repo.add_category_to_product.assert_called_once_with(product_id, category_id) - -@patch("product_service.services.ProductRepository") -def test_remove_category_from_product(mock_repo): - product_id = "prod123" - expected_product = { - "id": product_id, - "name": "Apple", - "description": "Fresh red apple", - "price": 10.0, - "brand": "Test Brand", - "quantity": 5, - "category": None - } - - mock_repo.remove_category_from_product.return_value = expected_product - - result = ProductServices.remove_category_from_product(product_id) - assert result["category"] is None - mock_repo.remove_category_from_product.assert_called_once_with(product_id) - diff --git a/backend/python/product_service/views.py b/backend/python/product_service/views.py deleted file mode 100644 index e87784113..000000000 --- a/backend/python/product_service/views.py +++ /dev/null @@ -1,116 +0,0 @@ -import json -from django.http import JsonResponse -from django.views.decorators.csrf import csrf_exempt -from .services import ProductServices - -@csrf_exempt -def create_product(request): - if request.method == "POST": - data = json.loads(request.body) - try: - product = ProductServices.create_product(data) - return JsonResponse({ - "id": str(product.id), - "message": "Product created" - }) - except ValueError as er: - return JsonResponse({"error": str(er)}, status=400) - - return JsonResponse({"error" : "POST method not used"}, status=400) - - -def list_products(request): - products = ProductServices.list_products() - - response_data = [] - for p in products: - - raw_category = p._data.get('category') - - cat_id = str(raw_category.id) if raw_category else None - - response_data.append({ - "id": str(p.id), - "name": p.name, - "description": p.description, - "price": p.price, - "category": cat_id - }) - - return JsonResponse(response_data, safe=False) - - -def get_product(request, product_id): - try: - product=ProductServices.get_product(product_id) - raw_category = product._data.get('category') - - cat_id = str(raw_category.id) if raw_category else None - return JsonResponse({ - "id" : str(product.id), - "name": product.name, - "description" : product.description, - "price" : product.price, - "category": cat_id - }) - except ValueError as er: - return JsonResponse({"error" : str(er)}, status=404) - -@csrf_exempt -def list_products_by_category_id(request, category_id): - if request.method == "GET": - products = ProductServices.list_products_by_category_id(category_id) - - return JsonResponse([ - { - "id": str(p.id), - "name": p.name, - "description": p.description, - "price": p.price - } - for p in products - ], safe=False) - else: - return JsonResponse({"error" : "GET method not used"}, status=400) - -@csrf_exempt -def remove_category_from_product(request, product_id): - if request.method == "PATCH": - try: - product = ProductServices.remove_category_from_product(product_id) - return JsonResponse({ - "id": str(product.id), - "name": product.name, - "description": product.description, - "price": product.price, - "category": None - },status=200) - except ValueError as er: - return JsonResponse({"error": str(er)}, status=404) - else: - return JsonResponse({"error" : "PATCH method not used"}, status=400) - -@csrf_exempt -def add_category_to_product(request, product_id, category_id): - if request.method == "PATCH": - try: - product = ProductServices.add_category_to_product(product_id, category_id) - return JsonResponse({ - "id": str(product.id), - "name": product.name, - "description": product.description, - "price": product.price, - "category": str(product.category.id) if product.category else None - }) - except ValueError as er: - return JsonResponse({"error": str(er)}, status=404) - else: - return JsonResponse({"error" : "PATCH method not used"}, status=400) - -@csrf_exempt -def delete_product(request, product_id): - if request.method == "DELETE": - ProductServices.delete_product(product_id) - return JsonResponse({"message":"Deleted"}) - else: - return JsonResponse({"error" : "DELETE method not used"}, status=400) \ No newline at end of file diff --git a/backend/python/warehouse/__init__.py b/backend/python/warehouse/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/python/warehouse/admin.py b/backend/python/warehouse/admin.py deleted file mode 100644 index 8c38f3f3d..000000000 --- a/backend/python/warehouse/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/backend/python/warehouse/apps.py b/backend/python/warehouse/apps.py deleted file mode 100644 index ae7e6913a..000000000 --- a/backend/python/warehouse/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class WarehouseConfig(AppConfig): - name = 'warehouse' diff --git a/backend/python/warehouse/migrations/__init__.py b/backend/python/warehouse/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/python/warehouse/models.py b/backend/python/warehouse/models.py deleted file mode 100644 index b2af9ba29..000000000 --- a/backend/python/warehouse/models.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.db import models -from decimal import Decimal -# Create your models here. - - -class Product: - id: int - name: str - description: str - category: str - price: Decimal - brand: str - quantity: int diff --git a/backend/python/warehouse/storage.py b/backend/python/warehouse/storage.py deleted file mode 100644 index 63476bb67..000000000 --- a/backend/python/warehouse/storage.py +++ /dev/null @@ -1,4 +0,0 @@ -from .models import Product - -PRODUCTS = [] -CURRENT_ID = 1 \ No newline at end of file diff --git a/backend/python/warehouse/tests.py b/backend/python/warehouse/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/backend/python/warehouse/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/python/warehouse/urls.py b/backend/python/warehouse/urls.py deleted file mode 100644 index b8023a259..000000000 --- a/backend/python/warehouse/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path -from .views import delete_product, list_all_products, create_product, list_product_by_id, update_product - -urlpatterns = [ - path('', list_all_products), - path('create/', create_product), - path('/', list_product_by_id), - path('/update/', update_product), - path('/delete/', delete_product) -] diff --git a/backend/python/warehouse/views.py b/backend/python/warehouse/views.py deleted file mode 100644 index f379ad1f4..000000000 --- a/backend/python/warehouse/views.py +++ /dev/null @@ -1,99 +0,0 @@ -import json -from django.http import JsonResponse -from .storage import PRODUCTS, CURRENT_ID -from django.views.decorators.csrf import csrf_exempt -import warehouse.storage as storage -# Create your views here. - - -def list_all_products(request): - if request.method != "GET": - return JsonResponse({"error": "Only GET allowed"}, status=405) - - return JsonResponse(PRODUCTS, safe=False) - - -@csrf_exempt -def create_product(request): - if request.method != "POST": - return JsonResponse({"error": "Only POST allowed"}, status=405) - - try : - data = json.loads(request.body) - - # Validate data here - required_fields = ["name", "description", "category", "price", "brand", "quantity"] - - for field in required_fields: - if field not in data: - return JsonResponse({"error": f"Missing field: {field}"}, status=400) - - if float(data["price"]) < 0: - return JsonResponse({"error": "Price must be non-negative"}, status=400) - - if int(data["quantity"]) < 0: - return JsonResponse({"error": "Quantity must be non-negative"}, status=400) - - # Create product - new_product = { - "id": storage.CURRENT_ID + 1, - "name": data["name"], - "description": data["description"], - "category": data["category"], - "price": data["price"], - "brand": data["brand"], - "quantity": data["quantity"] - } - PRODUCTS.append(new_product) - storage.CURRENT_ID +=1 - - return JsonResponse(new_product, status=201) - - - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON"}, status=400) - -def list_product_by_id(request, product_id): - if request.method != "GET": - return JsonResponse({"error": "Only GET allowed"}, status=405) - - for product in PRODUCTS: - if product["id"] == product_id: - return JsonResponse(product) - - return JsonResponse({"error": "Product not found"}, status=404) - - -@csrf_exempt -def update_product(request, product_id): - if request.method != "PUT": - return JsonResponse({"error": "Only PUT allowed"}, status=405) - - for product in PRODUCTS: - if(product["id"] == product_id): - try: - data = json.loads(request.body) - # Update fields - for key in ["name", "description", "category", "price", "brand", "quantity"]: - if key in data: - product[key] = data[key] - - return JsonResponse(product) - - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON"}, status=400) - - return JsonResponse({"error": "Product not found"}, status=404) - - -@csrf_exempt -def delete_product(request, product_id): - if request.method != "DELETE": - return JsonResponse({"error": "Only DELETE allowed"}, status=405) - - for i, product in enumerate(PRODUCTS): - if product["id"] == product_id: - del PRODUCTS[i] - return JsonResponse({"message": "Product deleted"}) - - return JsonResponse({"error": "Product not found"}, status=404) \ No newline at end of file From 04a8d9991aa53446e5351e4a7017175a1ce37a40 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 1 Apr 2026 13:20:46 +0530 Subject: [PATCH 23/30] refactor(api): convert verb-based URLs to RESTful endpoints --- backend/python/django_app/urls.py | 6 ++-- backend/python/product_service/urls.py | 42 +++++++++++++++++++++----- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index ed68d3887..2e680d234 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -4,7 +4,5 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('warehouse/', include("warehouse.urls")), - path('product_service/', include("product_service.urls")), - path('product_category/', include("product_category.urls")), -] + path('', include("product_service.urls")), +] \ No newline at end of file diff --git a/backend/python/product_service/urls.py b/backend/python/product_service/urls.py index 98e587be2..e78776360 100644 --- a/backend/python/product_service/urls.py +++ b/backend/python/product_service/urls.py @@ -1,12 +1,38 @@ from django.urls import path -from . import views +from .controllers import product, category urlpatterns = [ - path("", views.list_products, name="list_products"), - path("create/", views.create_product, name="create_product"), - path("/", views.get_product , name="get_product"), - path("delete//", views.delete_product, name="delete_product"), - path("products-by-category//", views.list_products_by_category_id, name="list_products_by_category_id"), - path("remove-category//", views.remove_category_from_product, name="remove_category_from_product"), - path("add-category///", views.add_category_to_product, name="add_category_to_product"), + path("products/", product.products, name="products"), + # GET -> list products + # POST -> create product + + path("products//", product.product_detail, name="product_detail"), + # GET -> get product + # PUT -> update product + # PATCH -> partial update + # DELETE -> delete product + +] + +urlpatterns += [ + path("categories/", category.categories, name="categories"), + # GET -> list categories + # POST -> create category + + path( + "categories//", + category.category_detail, + name="category_detail", + ), + # GET -> retrieve category + # PUT -> update category + # PATCH -> partial update + # DELETE -> delete category + + path( + "categories//products/", + category.list_products_by_category_id, + name="products_by_category", + ), + # GET -> list products by category ] \ No newline at end of file From 81209ce161a6e8ff757f3b6ce4ebd753a4268474 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 1 Apr 2026 13:25:29 +0530 Subject: [PATCH 24/30] feat: implement product and category controllers and introduce reusable serializers, validators, and response helpers --- .../product_service/controllers/category.py | 93 +++++++++++++++++++ .../product_service/controllers/product.py | 80 ++++++++++++++++ backend/python/product_service/responses.py | 26 ++++++ backend/python/product_service/serializers.py | 21 +++++ backend/python/product_service/validators.py | 33 +++++++ 5 files changed, 253 insertions(+) create mode 100644 backend/python/product_service/controllers/category.py create mode 100644 backend/python/product_service/controllers/product.py create mode 100644 backend/python/product_service/responses.py create mode 100644 backend/python/product_service/serializers.py create mode 100644 backend/python/product_service/validators.py diff --git a/backend/python/product_service/controllers/category.py b/backend/python/product_service/controllers/category.py new file mode 100644 index 000000000..b008d2c12 --- /dev/null +++ b/backend/python/product_service/controllers/category.py @@ -0,0 +1,93 @@ +import json + +from rest_framework.decorators import api_view +from django.http import JsonResponse +from ..services.product_service import ProductService +from ..services.category_service import CategoryService +from ..serializers import * +from ..exceptions import * +from ..validators import * +from ..responses import * + +product_service = ProductService() +category_service = CategoryService() + +@api_view(["GET", "POST"]) +def categories(request): + + if request.method == "GET": + categories = category_service.get_all_categories() + serialized_categories = [serialize_catgeory(category) for category in categories] + return success_response("categories",serialized_categories, 200) + + elif request.method == "POST": + try: + data = json.loads(request.body) + data=validate_category(data) + category = category_service.create_category(data) + serialized_category = serialize_catgeory(category) + return success_response("category created",serialized_category, 201) + except InvalidData as e: + return error_response(str(e), 400) + + else: + return invalid_method_response() +@api_view(["GET","PUT","PATCH","DELETE"]) +def category_detail(request, category_id): + + if request.method == "GET": + try: + category = category_service.get_category(category_id) + serialized_category = serialize_catgeory(category) + return success_response("category",serialized_category, 200) + except CategoryError as e: + return error_response(e.message, e.status_code) + + elif request.method == "PUT": + try: + data = json.loads(request.body) + data=validate_category(data) + category = category_service.update_category(category_id, data) + serialized_category = serialize_catgeory(category) + return success_response("category updated",serialized_category, 200) + except CategoryError as e: + return error_response(e.message, e.status_code) + except InvalidData as e: + return error_response(str(e), 400) + + elif request.method == "PATCH": + try: + data = json.loads(request.body) + data=validate_category(data,[]) + category = category_service.update_category(category_id, data, fields_required=False) + serialized_category = serialize_catgeory(category) + return success_response("category updated",serialized_category, 200) + except CategoryError as e: + return error_response(e.message, e.status_code) + except InvalidData as e: + return error_response(str(e), 400) + + elif request.method == "DELETE": + try: + category=category_service.delete_category(category_id) + return success_response("category deleted",serialize_catgeory(category), 200) + except CategoryError as e: + return error_response(e.message, e.status_code) + else: + return invalid_method_response() + + +@api_view(["GET"]) +def list_products_by_category_id(request, category_id): + if request.method == "GET": + try: + sort_by=request.GET.get("sort_by","-updated_at") + category_service.get_category(category_id) + except CategoryError as e: + return error_response(e.message, e.status_code) + + products = product_service.list_products_by_category_id(category_id, sort_by) + serialized_products = [serialize_product(product) for product in products] + return success_response("products",serialized_products, 200) + else: + return invalid_method_response() \ No newline at end of file diff --git a/backend/python/product_service/controllers/product.py b/backend/python/product_service/controllers/product.py new file mode 100644 index 000000000..9b477ae34 --- /dev/null +++ b/backend/python/product_service/controllers/product.py @@ -0,0 +1,80 @@ +import json + +from rest_framework.decorators import api_view +from ..services.product_service import ProductService +from ..services.category_service import CategoryService +from ..serializers import * +from ..exceptions import * +from ..validators import * +from ..responses import * + +product_service = ProductService() +category_service = CategoryService() +@api_view(["GET", "POST"]) +def products(request): + + if request.method == "GET": + sort_by=request.GET.get("sort_by","-updated_at") + products = product_service.list_products(sort_by) + serialized_products = [serialize_product(product) for product in products] + return success_response("products",serialized_products, 200) + + elif request.method == "POST": + try: + data = json.loads(request.body) + data=validate_product(data) + product = product_service.create_product(data) + serialized_product = serialize_product(product) + return success_response("product created",serialized_product, 201) + except InvalidData as e: + return error_response(str(e), 400) + else: + return invalid_method_response() + +@api_view(["GET","PUT","PATCH","DELETE"]) +def product_detail(request, product_id): + + if request.method == "GET": + try: + product = product_service.get_product(product_id) + serialized_product = serialize_product(product) + return success_response("product",serialized_product, 200) + except ProductError as e: + return error_response(e.message, e.status_code) + + + elif request.method == "PUT": + try: + data = json.loads(request.body) + data=validate_product(data) + product = product_service.update_product(product_id, data) + serialized_product = serialize_product(product) + return success_response("product updated",serialized_product, 200) + except (ProductError,CategoryError) as e: + return error_response(e.message,e.status_code) + except InvalidData as e: + return error_response(str(e), 400) + + elif request.method == "PATCH": + try: + data = json.loads(request.body) + data=validate_product(data, []) + product = product_service.update_product(product_id, data) + serialized_product = serialize_product(product) + return success_response("product updated",serialized_product, 200) + except ProductError as e: + return error_response(e.message, e.status_code) + except InvalidData as e: + return error_response(str(e), 400) + + elif request.method == "DELETE": + + try: + product = product_service.delete_product(product_id) + return success_response("product deleted",serialize_product(product), 200) + except ProductError as e: + return error_response(e.message, e.status_code) + + else: + return invalid_method_response() + \ No newline at end of file diff --git a/backend/python/product_service/responses.py b/backend/python/product_service/responses.py new file mode 100644 index 000000000..68ff503dd --- /dev/null +++ b/backend/python/product_service/responses.py @@ -0,0 +1,26 @@ +from rest_framework.response import Response +def success_response(data_name,data, status_code): + return Response( + { + "success": True, + data_name : data + }, + status=status_code, + ) + +def error_response(message, status_code): + return Response( + { + "success": False, + "error": message + }, + status=status_code, + ) +def invalid_method_response(): + return Response( + { + "success": False, + "error": "Invalid request method" + }, + status=400, + ) \ No newline at end of file diff --git a/backend/python/product_service/serializers.py b/backend/python/product_service/serializers.py new file mode 100644 index 000000000..15a2b5bce --- /dev/null +++ b/backend/python/product_service/serializers.py @@ -0,0 +1,21 @@ +def serialize_product(product): + return { + "id": str(product.id), + "name": product.name, + "description": product.description, + "category": str(product.category.id) if product.category else None, + "price": product.price, + "brand": product.brand, + "quantity": product.quantity, + "created_at": product.created_at, + "updated_at": product.updated_at + } + +def serialize_catgeory(category): + return { + "id": str(category.id), + "name": category.name, + "description": category.description, + "created_at": category.created_at, + "updated_at": category.updated_at, + } diff --git a/backend/python/product_service/validators.py b/backend/python/product_service/validators.py new file mode 100644 index 000000000..afeb428ef --- /dev/null +++ b/backend/python/product_service/validators.py @@ -0,0 +1,33 @@ +from .exceptions import InvalidData + + +required_product_fields = ["name", "price", "brand", "quantity"] +required_catgory_fields = ["name"] +def validate_product(data, required_fields=required_product_fields): + for field in required_fields: + if field not in data: + raise InvalidData(f"Missing field: {field}") + + if data.get("price") and float(data["price"]) < 0: + raise InvalidData("Price must be non-negative") + + if data.get("quantity") and int(data["quantity"]) < 0: + raise InvalidData("Quantity must be non-negative") + + if data.get("name") and data["name"].strip() == "": + raise InvalidData("Name cannot be empty or whitespace") + + if data.get("brand") and data["brand"].strip() == "": + raise InvalidData("Brand cannot be empty or whitespace") + + return data + +def validate_category(data, required_fields=required_catgory_fields): + for field in required_fields: + if field not in data: + raise InvalidData(f"Missing field: {field}") + + if data.get("name") and data["name"].strip() == "": + raise InvalidData("Name cannot be empty or whitespace") + + return data \ No newline at end of file From af5d8c6a2e8d1e8abeff880a235ba739c61eae35 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 1 Apr 2026 13:27:19 +0530 Subject: [PATCH 25/30] feat(service): implement product and category service layer with exception handling --- backend/python/product_service/exceptions.py | 53 ++++++++++++ .../services/category_service.py | 44 ++++++++++ .../services/product_service.py | 80 +++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 backend/python/product_service/exceptions.py create mode 100644 backend/python/product_service/services/category_service.py create mode 100644 backend/python/product_service/services/product_service.py diff --git a/backend/python/product_service/exceptions.py b/backend/python/product_service/exceptions.py new file mode 100644 index 000000000..55b953697 --- /dev/null +++ b/backend/python/product_service/exceptions.py @@ -0,0 +1,53 @@ +class ProductError(Exception): + def __init__(self, message, status_code=400): + self.message = message + self.status_code = status_code + super().__init__(message) + +class ProductNotFound(ProductError): + def __init__(self, product_id=None, status_code=404): + message = f"Product not found." + if product_id: + message += f" Product ID: {product_id}" + else: + message += f" Product ID: None" + super().__init__(message, status_code) + +class InvalidProductId(ProductError): + def __init__(self, product_id=None): + message = f"Invalid product ID." + if product_id: + message += f" Product ID: {product_id}" + else: + message += f" Product ID: None" + super().__init__(message) + +class InvalidData(Exception): + def __init__(self, message=None): + if message: + super().__init__(message) + else: + super().__init__("Invalid data") + +class CategoryError(Exception): + def __init__(self, message, status_code=400): + self.message = message + self.status_code = status_code + super().__init__(message) +class InvalidCategoryId(CategoryError): + def __init__(self, category_id=None): + message = f"Invalid category ID." + if category_id: + message += f" Category ID: {category_id}" + else: + message += f" Category ID: None" + super().__init__(message, status_code=400) + +class CategoryNotFound(CategoryError): + def __init__(self, category_id): + message = f"Category not found ." + if category_id: + message += f" Category ID: {category_id}" + else: + message += f" Category ID: None" + super().__init__(message, status_code=404) \ No newline at end of file diff --git a/backend/python/product_service/services/category_service.py b/backend/python/product_service/services/category_service.py new file mode 100644 index 000000000..674244eda --- /dev/null +++ b/backend/python/product_service/services/category_service.py @@ -0,0 +1,44 @@ +from bson import ObjectId +from ..repository import ProductRepository, CategoryRepository +from ..exceptions import * +from ..validators import * + +class CategoryService: + + def __init__(self, category_repository=CategoryRepository()): + self.category_repository = category_repository + + def create_category(self, data): + return self.category_repository.create(data) + + def get_all_categories(self): + return self.category_repository.get_all() + + def get_category(self, category_id): + + if not ObjectId.is_valid(category_id): + raise InvalidCategoryId(category_id=category_id) + + category = self.category_repository.get_by_id(category_id) + + if not category: + raise CategoryNotFound(category_id=category_id) + + return category + + def update_category(self, category_id, data, fields_required=True): + + category = self.get_category(category_id) + + for key, value in data.items(): + setattr(category, key, value) + + return self.category_repository.update(category) + + def delete_category(self, category_id): + + category = self.get_category(category_id) + + self.category_repository.delete(category_id) + + return category \ No newline at end of file diff --git a/backend/python/product_service/services/product_service.py b/backend/python/product_service/services/product_service.py new file mode 100644 index 000000000..3db99f1dc --- /dev/null +++ b/backend/python/product_service/services/product_service.py @@ -0,0 +1,80 @@ +from bson import ObjectId + +from ..repository import ProductRepository, CategoryRepository +from ..exceptions import ( + InvalidProductId, + ProductNotFound, + InvalidCategoryId, + CategoryNotFound, +) + + +class ProductService: + + def __init__(self, product_repository=None, category_repository=None): + self.product_repository = product_repository or ProductRepository() + self.category_repository = category_repository or CategoryRepository() + + # ---------- CREATE ---------- + + def create_product(self, data): + return self.product_repository.create(data) + + # ---------- READ ---------- + + def list_products(self, sort_by): + return self.product_repository.get_all(sort_by) + + def get_product(self, product_id): + self._validate_product_id(product_id) + + product = self.product_repository.get_by_id(product_id) + + if not product: + raise ProductNotFound(product_id=product_id) + + return product + + def list_products_by_category_id(self, category_id, sort_by): + return self.product_repository.get_all_by_category_id(category_id, sort_by) + + # ---------- UPDATE ---------- + + def update_product(self, product_id, data): + product = self.get_product(product_id) + + for key, value in data.items(): + + if key == "category": + value = self._validate_category(value) + + setattr(product, key, value) + + return self.product_repository.update(product) + + # ---------- DELETE ---------- + + def delete_product(self, product_id): + product = self.get_product(product_id) + + self.product_repository.delete(product_id) + + return product + + # ---------- HELPERS ---------- + + def _validate_product_id(self, product_id): + if not ObjectId.is_valid(product_id): + raise InvalidProductId(product_id=product_id) + + def _validate_category(self, category_id): + + if not ObjectId.is_valid(category_id): + raise InvalidCategoryId(category_id=category_id) + + category = self.category_repository.get_by_id(category_id) + + if not category: + raise CategoryNotFound(category_id=category_id) + + return category \ No newline at end of file From 2f5ecb35ed7890889216e379672d885e243f45a7 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 1 Apr 2026 13:28:26 +0530 Subject: [PATCH 26/30] feat: update product and category models and repositories --- backend/python/product_service/models.py | 31 ++++++-- backend/python/product_service/repository.py | 83 +++++++++++++------- 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/backend/python/product_service/models.py b/backend/python/product_service/models.py index 9b8e07222..3e531ddfa 100644 --- a/backend/python/product_service/models.py +++ b/backend/python/product_service/models.py @@ -1,18 +1,35 @@ -from mongoengine import Document, ReferenceField, StringField, FloatField, IntField -from product_category.models import ProductCategory +import datetime +from mongoengine import Document, ReferenceField, StringField, FloatField, IntField, DateTimeField, NULLIFY +class Category(Document): + name=StringField(required=True) + description=StringField() + created_at = DateTimeField(default=datetime.datetime.now) + updated_at = DateTimeField(default=datetime.datetime.now) + + def save(self, *args, **kwargs): + + self.updated_at = datetime.datetime.now() + return super().save(*args, **kwargs) + + + class Product(Document): name=StringField(required=True) description=StringField() - category = ReferenceField(ProductCategory) + category = ReferenceField(Category,reverse_delete_rule=NULLIFY) # reverse_delete_rule=NULLIFY price=FloatField(required=True) brand=StringField(required=True) quantity=IntField(required=True) + created_at = DateTimeField(default=datetime.datetime.now) + updated_at = DateTimeField(default=datetime.datetime.now) + + def save(self, *args, **kwargs): - def clean(self): - if not self.brand or self.brand.strip() == "": - raise ValueError("Brand cannot be empty or whitespace") + self.updated_at = datetime.datetime.now() + return super().save(*args, **kwargs) + + - \ No newline at end of file diff --git a/backend/python/product_service/repository.py b/backend/python/product_service/repository.py index 816da7d6b..172f9c799 100644 --- a/backend/python/product_service/repository.py +++ b/backend/python/product_service/repository.py @@ -1,5 +1,6 @@ -from .models import Product -from product_category.models import ProductCategory +from bson import ObjectId +from .models import Product, Category + class ProductRepository: @@ -8,36 +9,64 @@ def create(data): product = Product(**data) product.save() return product - + @staticmethod - def get_all(): - return Product.objects() - + def get_all(sort_by): + allowed_sorts = ['name', '-name', 'price', '-price', 'created_at', '-created_at', 'updated_at', '-updated_at'] + + if sort_by not in allowed_sorts: + sort_by = '-updated_at' + return Product.objects().order_by(sort_by) + @staticmethod - def get_all_by_category_id(category_id): - return Product.objects(category=category_id) - + def get_by_id(id): + return Product.objects(id=id).first() + @staticmethod - def remove_category_from_product(product_id): - product = Product.objects(id=product_id).first() - if product: - product.category = None - product.save() - return product - - @staticmethod - def add_category_to_product(product_id, category_id): - product = Product.objects(id=product_id).first() - category = ProductCategory.objects(id=category_id).first() + def update(product): + product.save() + return product + + @staticmethod + def delete(id): + product = Product.objects(id=id).first() if product: - product.category = category - product.save() + product.delete() return product @staticmethod - def get_by_id(product_id): - return Product.objects(id=product_id).first() - + def get_all_by_category_id(category_id, sort_by): + allowed_sorts = ['name', '-name', 'price', '-price', 'created_at', '-created_at', 'updated_at', '-updated_at'] + + if sort_by not in allowed_sorts: + sort_by = '-updated_at' + return Product.objects(category=category_id).order_by(sort_by) + + +class CategoryRepository: + + @staticmethod + def create(data): + category = Category(**data) + category.save() + return category + + @staticmethod + def get_all(): + return Category.objects() + + @staticmethod + def get_by_id(category_id,): + return Category.objects(id=category_id).first() + + @staticmethod + def update(category): + category.save() + return category + @staticmethod - def delete_by_id(product_id): - Product.objects(id=product_id).delete() \ No newline at end of file + def delete(category_id): + category = Category.objects(id=category_id).first() + if category: + category.delete() + return category \ No newline at end of file From a4261f763db91e56391184c0ef4be4ecb1c94f57 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 1 Apr 2026 13:31:30 +0530 Subject: [PATCH 27/30] test: add API integration tests and service unit tests --- backend/python/.coverage | Bin 53248 -> 53248 bytes backend/python/conftest.py | 8 +- .../tests/integration/test_category_api.py | 88 +++++++ .../tests/integration/test_product_api.py | 215 ++++++++---------- .../tests/unit/test_category_service.py | 113 +++++++++ .../tests/unit/test_product_service.py | 192 ++++++++++++++++ 6 files changed, 496 insertions(+), 120 deletions(-) create mode 100644 backend/python/product_service/tests/integration/test_category_api.py create mode 100644 backend/python/product_service/tests/unit/test_category_service.py create mode 100644 backend/python/product_service/tests/unit/test_product_service.py diff --git a/backend/python/.coverage b/backend/python/.coverage index cf662b9a27b7db2f6f22a3fd895275db21b3a593..9273cce3279268417408b79c24a867f5934b45c6 100644 GIT binary patch delta 1563 zcmZvaZA@EL7{`0td)xbZo_qUp3yc98jmZ`XppXc23^ZnxQ8c3nU2!(0LLJar3UTTv zL_fL2=4h7U<)tNOup>{bC4NLlr8FW4=X7dpZ@3m&;L2k zbI$LMPS{2#Y#;7ogpZ1!V}vhWJzHO)^Gg<(h5;xei{yP$PprymrBeP`?vsn9tI|7? zSNujyiF<^3VN|H(mo8}Pr@XyGXDwsZ#+ifFMt>8(I}#sACF6a4(d1w#5>7>X;>qLX ziQ_FEOqJ$RBpDq{#0LhWgSge|Hr{A{#w>OZMw78{U+gU`#*LR-4vSQ-ILHE*Tv_*D19 zAwMyQuK|W}tnP3sP6zWV)Qi3ui6&ApyayiUm(lZEyCg$9L@#4@E!uj5ytdsUwr$C8 zTbO&Oywqy~re4}g`I;ZM_wZ;P&a=GQR$(|1!y)P%#{8i=W1qjyd`>ak-5(o3+s7HC zg$^b4$Gf9_=DY`4e76Rpu=O@_U$zh%d<}7N2p%B^$P&LST$iqqlk#q%N*I&-#f!?v zN|k(7DiHTdCy@JDp6z6jx)$8OjZ1DGY@i!6M{3P6lziA8BeChawzGD3u!tIu^X`n< zWsGz-an9#iq^t$YP0F0s#UkWz@P&dM;Hup$vc&@ptXd z8)}GNW%EzUT{4fB6lD?VgKh;o1jHT|X_^5f$qBtIg53P_s$20*h`ccu+Q&J3?3mY# z2sw&&<8D_o!{I|TUMM_zyR1Sd(-yc3t8g8zzz=W{zJzi307fAN5$J?gXoLzV0gkMb zS#p_7Be!=+TGUxa(8(Xor~FpYW3HVw)*o$9;>PuUsF}+wW)|B6H!|7tnW?EvAlqI& z^?Ls#<96Zx9RjFAAX0_l}sgOlwZp5TYm{qJlkub3;($Uh>; zxGIxBTVMTc-M4b{9wSO<=-jPie}B%%BCd;+?_0-9{=4x62xio^@%Bpk_u10>4GxV* z<1u|>`n&q&pGxn~*y@Sh!KN93uvKs4!sum6YjtY%tIEIVFOOWdvF3y!+H1? zdSMt&!7YfxWtf0-a0MYe4QaDILv$)m4;64z5nWUSCly|!qN-FRpyDP}JPH+Arou^7 p6p@NcprZ3sSO*n5y)|Rw?1g4~jis*D#^K!ZA@Eb6z*+r@ArMr?e|4sW148385(3H<6}7R$LLClKb%eqT?5^0B5ebOg^{k* zm}ue;o%fg75{WL+Xq+0N)6EPQ6Y(Rfd26VB50yCu7P^)OSvG6-apTK&Uwyx z&pGdtJ3iqUpKv^1$w;TG+8ODcqvu-db>WN+Euc(izigHa!DjC}eegCmK)I69AM=zIvB?Z(N@om>~7c?X(%tE^TA)Bk;GVsh|BBX4hcx9G%wPviw08tak}6j zU(Rnau7*XUHQcm0S1g@2XXLDGP$Tb=)m_aBe)rWJPqbrd}u0HuxKUgm2(FdD$S<&0NPq$#o_hiVYh!S_r>AX(cIY7 zU!}#;4A1+_ux|x;uX*q;Ib;tWbkEF*LSdQ6Zqslt3}09(H0drh>RdCYF6Q1V{W`aj z%Vml$Eq%Av#aCz;snP)!s!KK1%wOc{+PUYg^vC`$K!u;}B&$2_6O^Uo!y!z(s!qgNep-Ch=Cz>eQ zIl)Ask>?RL{*!r*W0{;txH`^hMw|@Cn#k614ig=W-;P*ox7Xyo_L}iE+mrnF4HHt^ zu#;g$kuZ|YX4By@8x-MB_zix7dH5RF|2cdNAHutE9$v)p62BBI9&YU|9+d2L-#G#fin2C+_= 1 + +def test_delete_category(client, seeded_categories): + + category_id = str(seeded_categories[0]) + + url=reverse("category_detail", args=[category_id]) + response = client.delete(url) + + assert response.status_code == 200 + diff --git a/backend/python/product_service/tests/integration/test_product_api.py b/backend/python/product_service/tests/integration/test_product_api.py index a979ebb1d..2a19da02b 100644 --- a/backend/python/product_service/tests/integration/test_product_api.py +++ b/backend/python/product_service/tests/integration/test_product_api.py @@ -1,157 +1,140 @@ +import pytest from django.urls import reverse -import json +from rest_framework.test import APIClient + +@pytest.fixture +def client(): + return APIClient() def test_list_products(client, seeded_products): - url = reverse("list_products") + + url = reverse("products") + response = client.get(url) assert response.status_code == 200 - products_data = response.json() + assert "products" in response.data + assert len(response.data["products"]) == 2 - assert len(products_data) == 2 +def test_list_products_sorted(client, seeded_products): - names = [p["name"] for p in products_data] + url = reverse("products") - assert "Apple" in names - assert "Carrot" in names + response = client.get(url, {"sort_by": "price"}) -def test_get_product_by_id(client, seeded_products): - product_id = seeded_products[0] - url = reverse("get_product", args=[product_id]) - response = client.get(url) assert response.status_code == 200 - product_data = response.json() - assert product_data["id"] == str(product_id) - assert product_data["name"] == "Apple" + + products = response.data["products"] + + assert products[0]["price"] <= products[1]["price"] def test_create_product(client, seeded_categories): - url = reverse("create_product") + + category_id = str(seeded_categories[0]) + data = { "name": "Banana", - "price": 12, - "category": str(seeded_categories[0]), + "price": 5, + "category": category_id, "brand": "Fruit Brand", "quantity": 30 } - response = client.post(url, data, content_type="application/json") - assert response.status_code == 200 - get_url=reverse("get_product", args=[response.json()["id"]]) - get_response = client.get(get_url) - product_data = get_response.json() - assert product_data["name"] == "Banana" -def test_create_product_invalid_price(client, seeded_categories): - url = reverse("create_product") + url = reverse("products") + + response = client.post(url, data, format="json") + + assert response.status_code == 201 + + product = response.data["product created"] + + assert product["name"] == "Banana" + assert product["price"] == 5 + +def test_create_product_invalid_data(client): + data = { - "name": "Banana", - "price": -5, - "category": str(seeded_categories[0]), - "brand": "Fruit Brand", - "quantity": 30 + "name": "", + "price": -10 } - response = client.post(url, data, content_type="application/json") + + url = reverse("products") + + response = client.post(url, data, format="json") + assert response.status_code == 400 - assert response.json()["error"] == "Given Price is not valid" -import json +def test_get_product(client, seeded_products): + + product_id = str(seeded_products[0]) + + url = reverse("product_detail", args=[product_id]) + + response = client.get(url) + + assert response.status_code == 200 + + product = response.data["product"] + + assert product["name"] == "Apple" -def test_create_product_not_by_post(client, seeded_categories): - url = reverse("create_product") +def test_update_product(client, seeded_products, seeded_categories): + + product_id = str(seeded_products[0]) + category_id = str(seeded_categories[0]) data = { - "name": "Banana", + "name": "Green Apple", "price": 12, - "category": str(seeded_categories[0]), + "category": category_id, "brand": "Fruit Brand", - "quantity": 30 + "quantity": 60 } - response = client.put( - url, - data=json.dumps(data), - content_type="application/json" - ) + url = reverse("product_detail", args=[product_id]) - assert response.status_code == 400 + response = client.put(url, data, format="json") -def test_delete_product(client, seeded_products): - product_id = seeded_products[0] - url = reverse("delete_product", args=[product_id]) - response = client.delete(url) - - assert response.status_code == 200 - # Verify the product is deleted - get_url = reverse("get_product", args=[product_id]) - get_response = client.get(get_url) - assert get_response.status_code == 404 - -def test_list_products_by_category_id(client, seeded_categories, seeded_products): - category_id = seeded_categories[0] - url = reverse("list_products_by_category_id", args=[category_id]) - response = client.get(url) assert response.status_code == 200 - products_data = response.json() - assert len(products_data) == 1 - assert products_data[0]["name"] == "Apple" - -def test_list_products_by_category_not_by_get(client, seeded_categories): - category_id = seeded_categories[0] - url = reverse("list_products_by_category_id", args=[category_id]) - response = client.post(url) - assert response.status_code == 400 -def test_remove_category_from_product(client, seeded_products): - product_id = seeded_products[0] - url = reverse("remove_category_from_product", args=[product_id]) - response = client.patch(url) - assert response.status_code == 200 - get_url = reverse("get_product", args=[product_id]) - get_response = client.get(get_url) - product_data = get_response.json() - assert product_data["category"] is None - -def test_remove_category_from_product_not_found(client): - product_id = "000000000000000000000000" - url = reverse("remove_category_from_product", args=[product_id]) - response = client.patch(url) - assert response.status_code == 404 - - -def test_remove_category_from_product_not_by_patch(client, seeded_products): - product_id = seeded_products[0] - url = reverse("remove_category_from_product", args=[product_id]) - response = client.post(url) - assert response.status_code == 400 - -def test_add_category_to_product(client, seeded_products, seeded_categories): - product_id = seeded_products[0] - category_id = seeded_categories[1] - url = reverse("add_category_to_product", args=[product_id, category_id]) - response = client.patch(url) + updated_product = response.data["product updated"] + + assert updated_product["name"] == "Green Apple" + assert updated_product["price"] == 12 + +def test_patch_product(client, seeded_products): + + product_id = str(seeded_products[0]) + + data = { + "price": 20 + } + + url = reverse("product_detail", args=[product_id]) + + response = client.patch(url, data, format="json") + assert response.status_code == 200 - get_url = reverse("get_product", args=[product_id]) - get_response = client.get(get_url) - product_data = get_response.json() - assert product_data["category"] == str(category_id) -def test_add_category_to_product_not_found(client, seeded_categories): - product_id = "000000000000000000000000" - category_id = seeded_categories[0] - url = reverse("add_category_to_product", args=[product_id, category_id]) - response = client.patch(url) - assert response.status_code == 404 + updated_product = response.data["product updated"] -def test_add_category_to_product_not_by_patch(client, seeded_products, seeded_categories): - product_id = seeded_products[0] - category_id = seeded_categories[1] + assert updated_product["price"] == 20 - url = reverse("add_category_to_product", args=[product_id, category_id]) +def test_delete_product(client, seeded_products): - response = client.post(url) - - print(response.status_code) - print(response.json()) + product_id = str(seeded_products[0]) - assert response.status_code == 400 - assert response.json()["error"] == "PATCH method not used" \ No newline at end of file + url = reverse("product_detail", args=[product_id]) + + response = client.delete(url) + + assert response.status_code == 200 + +def test_get_product_invalid_id(client): + + url = reverse("product_detail", args=["invalid-id"]) + + response = client.get(url) + + assert response.status_code == 400 or response.status_code == 404 \ No newline at end of file diff --git a/backend/python/product_service/tests/unit/test_category_service.py b/backend/python/product_service/tests/unit/test_category_service.py new file mode 100644 index 000000000..40f18c5ad --- /dev/null +++ b/backend/python/product_service/tests/unit/test_category_service.py @@ -0,0 +1,113 @@ +import pytest +from bson import ObjectId + +from product_service.services.category_service import CategoryService +from product_service.exceptions import ( + InvalidCategoryId, + CategoryNotFound +) + +def test_create_category(mocker): + + repo = mocker.Mock() + + data = {"name": "Electronics"} + + repo.create.return_value = data + + service = CategoryService(repo) + + result = service.create_category(data) + + assert result == data + repo.create.assert_called_once_with(data) + +def test_get_all_categories(mocker): + + repo = mocker.Mock() + + categories = [ + {"name": "Electronics"}, + {"name": "Books"} + ] + + repo.get_all.return_value = categories + + service = CategoryService(repo) + + result = service.get_all_categories() + + assert result == categories + repo.get_all.assert_called_once() + +def test_get_category_success(mocker): + + repo = mocker.Mock() + + category_id = str(ObjectId()) + category = {"_id": category_id, "name": "Electronics"} + + repo.get_by_id.return_value = category + + service = CategoryService(repo) + + result = service.get_category(category_id) + + assert result == category + +def test_get_category_invalid_id(mocker): + + service = CategoryService(mocker.Mock()) + + with pytest.raises(InvalidCategoryId): + service.get_category("invalid-id") + +def test_get_category_not_found(mocker): + + repo = mocker.Mock() + + repo.get_by_id.return_value = None + + service = CategoryService(repo) + + category_id = str(ObjectId()) + + with pytest.raises(CategoryNotFound): + service.get_category(category_id) + +def test_update_category(mocker): + + repo = mocker.Mock() + + category_id = str(ObjectId()) + + category = mocker.Mock() + + repo.get_by_id.return_value = category + repo.update.return_value = category + + service = CategoryService(repo) + + data = {"name": "Updated Category"} + + result = service.update_category(category_id, data) + + repo.update.assert_called_once_with(category) + assert result == category + +def test_delete_category(mocker): + + repo = mocker.Mock() + + category_id = str(ObjectId()) + + category = {"_id": category_id} + + repo.get_by_id.return_value = category + + service = CategoryService(repo) + + result = service.delete_category(category_id) + + repo.delete.assert_called_once_with(category_id) + assert result == category \ No newline at end of file diff --git a/backend/python/product_service/tests/unit/test_product_service.py b/backend/python/product_service/tests/unit/test_product_service.py new file mode 100644 index 000000000..ecba6547e --- /dev/null +++ b/backend/python/product_service/tests/unit/test_product_service.py @@ -0,0 +1,192 @@ +import pytest +from bson import ObjectId + +from product_service.services.product_service import ProductService +from product_service.exceptions import ( + InvalidProductId, + ProductNotFound, + InvalidCategoryId, + CategoryNotFound +) + +def test_create_product(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + data = {"name": "Laptop", "price": 1000} + + product_repo.create.return_value = data + + service = ProductService(product_repo, category_repo) + + result = service.create_product(data) + + assert result == data + product_repo.create.assert_called_once_with(data) + +def test_list_products(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + products = [{"name": "Laptop"}, {"name": "Phone"}] + + product_repo.get_all.return_value = products + + service = ProductService(product_repo, category_repo) + + result = service.list_products("-price") + + assert result == products + product_repo.get_all.assert_called_once_with("-price") + +def test_get_product_success(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + product_id = str(ObjectId()) + product = {"_id": product_id, "name": "Laptop"} + + product_repo.get_by_id.return_value = product + + service = ProductService(product_repo, category_repo) + + result = service.get_product(product_id) + + assert result == product + +def test_get_product_invalid_id(mocker): + + service = ProductService(mocker.Mock(), mocker.Mock()) + + with pytest.raises(InvalidProductId): + service.get_product("invalid-id") + + +def test_get_product_not_found(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + product_repo.get_by_id.return_value = None + + service = ProductService(product_repo, category_repo) + + product_id = str(ObjectId()) + + with pytest.raises(ProductNotFound): + service.get_product(product_id) + +def test_update_product(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + product_id = str(ObjectId()) + + product = mocker.Mock() + product_repo.get_by_id.return_value = product + product_repo.update.return_value = product + + service = ProductService(product_repo, category_repo) + + data = {"name": "Updated Laptop"} + + result = service.update_product(product_id, data) + + product_repo.update.assert_called_once_with(product) + +def test_update_product_with_category(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + product_id = str(ObjectId()) + category_id = str(ObjectId()) + + product = mocker.Mock() + + product_repo.get_by_id.return_value = product + product_repo.update.return_value = product + + category = {"name": "Electronics"} + category_repo.get_by_id.return_value = category + + service = ProductService(product_repo, category_repo) + + data = {"category": category_id} + + service.update_product(product_id, data) + + category_repo.get_by_id.assert_called_once_with(category_id) + +def test_delete_product(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + product_id = str(ObjectId()) + + product = {"_id": product_id} + + product_repo.get_by_id.return_value = product + + service = ProductService(product_repo, category_repo) + + result = service.delete_product(product_id) + + product_repo.delete.assert_called_once_with(product_id) + assert result == product + +def test_list_products_by_category_id(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + products = [{"name": "Laptop"}] + + product_repo.get_all_by_category_id.return_value = products + + service = ProductService(product_repo, category_repo) + + result = service.list_products_by_category_id("cat123", "-price") + + assert result == products + product_repo.get_all_by_category_id.assert_called_once_with("cat123", "-price") + + +def test_update_product_invalid_category_id(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + product_id = str(ObjectId()) + + product_repo.get_by_id.return_value = mocker.Mock() + + service = ProductService(product_repo, category_repo) + + data = {"category": "invalid-id"} + + with pytest.raises(InvalidCategoryId): + service.update_product(product_id, data) + +def test_update_product_category_not_found(mocker): + + product_repo = mocker.Mock() + category_repo = mocker.Mock() + + product_id = str(ObjectId()) + category_id = str(ObjectId()) + + product_repo.get_by_id.return_value = mocker.Mock() + category_repo.get_by_id.return_value = None + + service = ProductService(product_repo, category_repo) + + data = {"category": category_id} + + with pytest.raises(CategoryNotFound): + service.update_product(product_id, data) \ No newline at end of file From d23e668a5f82f4b11bd74944b32aaf49ee19110a Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 1 Apr 2026 13:32:32 +0530 Subject: [PATCH 28/30] update Makefile --- Makefile | 62 ++++++++++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index 0ae246ed2..a225cf521 100644 --- a/Makefile +++ b/Makefile @@ -1,58 +1,48 @@ +# ----------------------------- # Variables -VENV = venv -VENV = venv -BACKEND_DIR = backend\python +# ----------------------------- +BACKEND_DIR = backend/python FRONTEND_DIR = frontend -PYTHON = $(VENV)\Scripts\python -PIP = $(VENV)\Scripts\pip +VENV = venv + +PYTHON = $(BACKEND_DIR)/$(VENV)/Scripts/python +PIP = $(BACKEND_DIR)/$(VENV)/Scripts/pip + +# ----------------------------- +# Setup +# ----------------------------- -# Create virtual environment venv: - python -m venv $(VENV) + cd $(BACKEND_DIR) && python -m venv $(VENV) -# Install backend dependencies install-backend: venv $(PIP) install --upgrade pip - $(PIP) install -r $(BACKEND_DIR)\requirements.txt + $(PIP) install -r $(BACKEND_DIR)/requirements.txt -# Install frontend dependencies install-frontend: cd $(FRONTEND_DIR) && yarn install -# Setup everything setup: install-backend install-frontend -# Run backend -backend: - cd $(BACKEND_DIR) && ../../$(PYTHON) manage.py runserver +# ----------------------------- +# Run servers +# ----------------------------- -# Run frontend -frontend: - cd $(FRONTEND_DIR) && yarn start +run-backend: + cd $(BACKEND_DIR) && $(VENV)/Scripts/python manage.py runserver -#containerize backend -containerize-backend: - cd $(BACKEND_DIR) && docker compose up --build +run-frontend: + cd $(FRONTEND_DIR) && yarn start -#decontainerize backend -decontainerize-backend: - cd $(BACKEND_DIR) && docker compose down +# ----------------------------- +# Testing +# ----------------------------- -# Run tests test: - cd $(BACKEND_DIR) && $(PYTHON) -m pytest + cd $(BACKEND_DIR) && $(VENV)/Scripts/python -m pytest -# Run coverage coverage: - cd $(BACKEND_DIR) && $(PYTHON) -m pytest --cov=. --cov-report=term-missing + cd $(BACKEND_DIR) && $(VENV)/Scripts/python -m pytest --cov html-coverage: - cd $(BACKEND_DIR) && $(PYTHON) -m pytest --cov=. --cov-report=html - -# Lint -lint: - $(PYTHON) -m flake8 . - -# Format -format: - $(PYTHON) -m black . \ No newline at end of file + cd $(BACKEND_DIR) && $(VENV)/Scripts/python -m pytest --cov --cov-report=html From 10cbfec97b4d0d11b1252b5829bafabb8ddb3170 Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 1 Apr 2026 13:35:17 +0530 Subject: [PATCH 29/30] refactor: rename product_service app to products --- backend/python/conftest.py | 4 ++-- backend/python/django_app/settings.py | 3 +-- backend/python/{product_service => products}/__init__.py | 0 backend/python/{product_service => products}/admin.py | 0 backend/python/{product_service => products}/apps.py | 0 .../{product_service => products}/controllers/category.py | 0 .../{product_service => products}/controllers/product.py | 0 backend/python/{product_service => products}/exceptions.py | 0 .../management/commands/migrate_brands.py | 2 +- .../{product_service => products}/migrations/__init__.py | 0 backend/python/{product_service => products}/models.py | 0 backend/python/{product_service => products}/repository.py | 0 backend/python/{product_service => products}/responses.py | 0 backend/python/{product_service => products}/serializers.py | 0 .../services/category_service.py | 0 .../{product_service => products}/services/product_service.py | 0 .../tests/integration/test_category_api.py | 0 .../tests/integration/test_product_api.py | 0 .../tests/unit/test_category_service.py | 4 ++-- .../tests/unit/test_product_service.py | 4 ++-- backend/python/{product_service => products}/urls.py | 0 backend/python/{product_service => products}/validators.py | 0 22 files changed, 8 insertions(+), 9 deletions(-) rename backend/python/{product_service => products}/__init__.py (100%) rename backend/python/{product_service => products}/admin.py (100%) rename backend/python/{product_service => products}/apps.py (100%) rename backend/python/{product_service => products}/controllers/category.py (100%) rename backend/python/{product_service => products}/controllers/product.py (100%) rename backend/python/{product_service => products}/exceptions.py (100%) rename backend/python/{product_service => products}/management/commands/migrate_brands.py (91%) rename backend/python/{product_service => products}/migrations/__init__.py (100%) rename backend/python/{product_service => products}/models.py (100%) rename backend/python/{product_service => products}/repository.py (100%) rename backend/python/{product_service => products}/responses.py (100%) rename backend/python/{product_service => products}/serializers.py (100%) rename backend/python/{product_service => products}/services/category_service.py (100%) rename backend/python/{product_service => products}/services/product_service.py (100%) rename backend/python/{product_service => products}/tests/integration/test_category_api.py (100%) rename backend/python/{product_service => products}/tests/integration/test_product_api.py (100%) rename backend/python/{product_service => products}/tests/unit/test_category_service.py (95%) rename backend/python/{product_service => products}/tests/unit/test_product_service.py (97%) rename backend/python/{product_service => products}/urls.py (100%) rename backend/python/{product_service => products}/validators.py (100%) diff --git a/backend/python/conftest.py b/backend/python/conftest.py index b885d2a73..22f9ced54 100644 --- a/backend/python/conftest.py +++ b/backend/python/conftest.py @@ -33,14 +33,14 @@ def test_db(mongo_client): @pytest.fixture def seeded_categories(test_db): - from product_service.models import Category + from products.models import Category cat1 = Category(name="Fruits", description="Fresh fruits").save() cat2 = Category(name="Vegetables", description="Fresh vegetables").save() return [str(cat1.id), str(cat2.id)] -from product_service.models import Product +from products.models import Product @pytest.fixture def seeded_products(seeded_categories): diff --git a/backend/python/django_app/settings.py b/backend/python/django_app/settings.py index e84abbc67..5095a1a00 100644 --- a/backend/python/django_app/settings.py +++ b/backend/python/django_app/settings.py @@ -37,9 +37,8 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "warehouse", + "rest_framework", "product_service", - "product_category", ] MIDDLEWARE = [ diff --git a/backend/python/product_service/__init__.py b/backend/python/products/__init__.py similarity index 100% rename from backend/python/product_service/__init__.py rename to backend/python/products/__init__.py diff --git a/backend/python/product_service/admin.py b/backend/python/products/admin.py similarity index 100% rename from backend/python/product_service/admin.py rename to backend/python/products/admin.py diff --git a/backend/python/product_service/apps.py b/backend/python/products/apps.py similarity index 100% rename from backend/python/product_service/apps.py rename to backend/python/products/apps.py diff --git a/backend/python/product_service/controllers/category.py b/backend/python/products/controllers/category.py similarity index 100% rename from backend/python/product_service/controllers/category.py rename to backend/python/products/controllers/category.py diff --git a/backend/python/product_service/controllers/product.py b/backend/python/products/controllers/product.py similarity index 100% rename from backend/python/product_service/controllers/product.py rename to backend/python/products/controllers/product.py diff --git a/backend/python/product_service/exceptions.py b/backend/python/products/exceptions.py similarity index 100% rename from backend/python/product_service/exceptions.py rename to backend/python/products/exceptions.py diff --git a/backend/python/product_service/management/commands/migrate_brands.py b/backend/python/products/management/commands/migrate_brands.py similarity index 91% rename from backend/python/product_service/management/commands/migrate_brands.py rename to backend/python/products/management/commands/migrate_brands.py index 632af5b3c..96bc18234 100644 --- a/backend/python/product_service/management/commands/migrate_brands.py +++ b/backend/python/products/management/commands/migrate_brands.py @@ -1,4 +1,4 @@ -from product_service.models import Product +from products.models import Product from django.core.management.base import BaseCommand class Command(BaseCommand): diff --git a/backend/python/product_service/migrations/__init__.py b/backend/python/products/migrations/__init__.py similarity index 100% rename from backend/python/product_service/migrations/__init__.py rename to backend/python/products/migrations/__init__.py diff --git a/backend/python/product_service/models.py b/backend/python/products/models.py similarity index 100% rename from backend/python/product_service/models.py rename to backend/python/products/models.py diff --git a/backend/python/product_service/repository.py b/backend/python/products/repository.py similarity index 100% rename from backend/python/product_service/repository.py rename to backend/python/products/repository.py diff --git a/backend/python/product_service/responses.py b/backend/python/products/responses.py similarity index 100% rename from backend/python/product_service/responses.py rename to backend/python/products/responses.py diff --git a/backend/python/product_service/serializers.py b/backend/python/products/serializers.py similarity index 100% rename from backend/python/product_service/serializers.py rename to backend/python/products/serializers.py diff --git a/backend/python/product_service/services/category_service.py b/backend/python/products/services/category_service.py similarity index 100% rename from backend/python/product_service/services/category_service.py rename to backend/python/products/services/category_service.py diff --git a/backend/python/product_service/services/product_service.py b/backend/python/products/services/product_service.py similarity index 100% rename from backend/python/product_service/services/product_service.py rename to backend/python/products/services/product_service.py diff --git a/backend/python/product_service/tests/integration/test_category_api.py b/backend/python/products/tests/integration/test_category_api.py similarity index 100% rename from backend/python/product_service/tests/integration/test_category_api.py rename to backend/python/products/tests/integration/test_category_api.py diff --git a/backend/python/product_service/tests/integration/test_product_api.py b/backend/python/products/tests/integration/test_product_api.py similarity index 100% rename from backend/python/product_service/tests/integration/test_product_api.py rename to backend/python/products/tests/integration/test_product_api.py diff --git a/backend/python/product_service/tests/unit/test_category_service.py b/backend/python/products/tests/unit/test_category_service.py similarity index 95% rename from backend/python/product_service/tests/unit/test_category_service.py rename to backend/python/products/tests/unit/test_category_service.py index 40f18c5ad..be52bdd7c 100644 --- a/backend/python/product_service/tests/unit/test_category_service.py +++ b/backend/python/products/tests/unit/test_category_service.py @@ -1,8 +1,8 @@ import pytest from bson import ObjectId -from product_service.services.category_service import CategoryService -from product_service.exceptions import ( +from products.services.category_service import CategoryService +from products.exceptions import ( InvalidCategoryId, CategoryNotFound ) diff --git a/backend/python/product_service/tests/unit/test_product_service.py b/backend/python/products/tests/unit/test_product_service.py similarity index 97% rename from backend/python/product_service/tests/unit/test_product_service.py rename to backend/python/products/tests/unit/test_product_service.py index ecba6547e..8acf0a573 100644 --- a/backend/python/product_service/tests/unit/test_product_service.py +++ b/backend/python/products/tests/unit/test_product_service.py @@ -1,8 +1,8 @@ import pytest from bson import ObjectId -from product_service.services.product_service import ProductService -from product_service.exceptions import ( +from products.services.product_service import ProductService +from products.exceptions import ( InvalidProductId, ProductNotFound, InvalidCategoryId, diff --git a/backend/python/product_service/urls.py b/backend/python/products/urls.py similarity index 100% rename from backend/python/product_service/urls.py rename to backend/python/products/urls.py diff --git a/backend/python/product_service/validators.py b/backend/python/products/validators.py similarity index 100% rename from backend/python/product_service/validators.py rename to backend/python/products/validators.py From 574d42ada21bf185334a7865e4cab2d9afbb5d2f Mon Sep 17 00:00:00 2001 From: Pushkar06p Date: Wed, 1 Apr 2026 15:52:02 +0530 Subject: [PATCH 30/30] fix: update imports after renaming product_service to products --- backend/python/django_app/settings.py | 2 +- backend/python/django_app/urls.py | 2 +- backend/python/products/apps.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/python/django_app/settings.py b/backend/python/django_app/settings.py index 5095a1a00..6a1998122 100644 --- a/backend/python/django_app/settings.py +++ b/backend/python/django_app/settings.py @@ -38,7 +38,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", - "product_service", + "products", ] MIDDLEWARE = [ diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index 2e680d234..5647a254d 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -4,5 +4,5 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('', include("product_service.urls")), + path('', include("products.urls")), ] \ No newline at end of file diff --git a/backend/python/products/apps.py b/backend/python/products/apps.py index c6d389b00..bb7bd52ba 100644 --- a/backend/python/products/apps.py +++ b/backend/python/products/apps.py @@ -2,4 +2,4 @@ class ProductServiceConfig(AppConfig): - name = 'product_service' + name = 'products'