From 73a1117802bc73074fd6a17471ba28242b0eab6d Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 14:02:27 +0530 Subject: [PATCH 01/30] Added docker-compose for db, and dummy.py to generate dummy data --- blocktrack_backend/docker-compose.yaml | 24 +++++++++++ blocktrack_backend/dummy.py | 56 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 blocktrack_backend/docker-compose.yaml create mode 100644 blocktrack_backend/dummy.py diff --git a/blocktrack_backend/docker-compose.yaml b/blocktrack_backend/docker-compose.yaml new file mode 100644 index 0000000..75b2c15 --- /dev/null +++ b/blocktrack_backend/docker-compose.yaml @@ -0,0 +1,24 @@ +services: + db: + image: postgres:15 + container_name: blocktrack_postgres + restart: always + environment: + POSTGRES_USER: blockuser + POSTGRES_PASSWORD: blockpass + POSTGRES_DB: blocktrack_db + ports: + - "15432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + adminer: + image: adminer + container_name: blocktrack_adminer + restart: always + ports: + - "8080:8080" + depends_on: + - db +volumes: + postgres_data: \ No newline at end of file diff --git a/blocktrack_backend/dummy.py b/blocktrack_backend/dummy.py new file mode 100644 index 0000000..e8ac157 --- /dev/null +++ b/blocktrack_backend/dummy.py @@ -0,0 +1,56 @@ +from supplier_request.models import SupplierRequest +from orders.models import Order, OrderDetails +from django.utils import timezone +import random +import uuid + +# Dummy data for SupplierRequest +statuses = ['pending', 'received'] + +for i in range(10): + SupplierRequest.objects.create( + supplier_id=random.randint(1, 5), + created_at=timezone.now(), + expected_delivery_date=timezone.now() + timezone.timedelta(days=random.randint(1, 10)), + product_id=random.randint(100, 105), + count=round(random.uniform(1, 100), 2), + status=random.choice(statuses), + received_at=timezone.now() if random.choice([True, False]) else None, + warehouse_id=random.randint(1, 3), + unit_price=round(random.uniform(10, 500), 2) + ) + +# Dummy data for Order and OrderDetails +order_statuses = ['Preparing', 'Shipped', 'Delivered', 'Cancelled'] + +# Sample addresses +cities = ['Colombo', 'Kandy', 'Galle', 'Jaffna'] +countries = ['Sri Lanka', 'India', 'USA', 'UK'] +zipcodes = ['00100', '00200', '01000', '10001'] + +# Generate Orders +for i in range(15): + order = Order.objects.create( + product=f"Product {random.randint(1, 50)}", + customer=f"Customer {random.randint(1, 20)}", + status=random.choice(order_statuses), + blockchain_tx_id=str(uuid.uuid4()), + ipfs_hash=str(uuid.uuid4()), + # created_at is auto_now_add + ) + + # Generate between 1 and 5 details for each order + details_count = random.randint(1, 5) + for _ in range(details_count): + OrderDetails.objects.create( + order_id=order, + product_id=random.randint(100, 200), + count=random.randint(1, 20), + warehouse_id=random.randint(1, 5), + address=f"{random.randint(1, 999)} Main Street", + zipcode=random.choice(zipcodes), + city=random.choice(cities), + country=random.choice(countries) + ) + +print("Dummy data generation complete.") From d28b26e98887c6ef0920bb5f4c8950deed0a7264 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 14:02:43 +0530 Subject: [PATCH 02/30] Updated requirements.txt and gitignore --- blocktrack_backend/.gitignore | 1 + blocktrack_backend/requirements.txt | 3 +++ 2 files changed, 4 insertions(+) diff --git a/blocktrack_backend/.gitignore b/blocktrack_backend/.gitignore index 9ec524c..e6f79dd 100644 --- a/blocktrack_backend/.gitignore +++ b/blocktrack_backend/.gitignore @@ -8,3 +8,4 @@ env/ .vscode/ .DS_Store *.pem +.env \ No newline at end of file diff --git a/blocktrack_backend/requirements.txt b/blocktrack_backend/requirements.txt index e3ee165..43a2b92 100644 --- a/blocktrack_backend/requirements.txt +++ b/blocktrack_backend/requirements.txt @@ -12,3 +12,6 @@ six==1.17.0 sqlparse==0.5.3 urllib3==2.4.0 varint==1.0.2 +django-filter +psycopg2-binary +python-dotenv \ No newline at end of file From 2221ae45d2c750540ab05c8ae725621db6de2427 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 14:03:33 +0530 Subject: [PATCH 03/30] Added new app for supplier requests --- .../supplier_request/__init__.py | 0 blocktrack_backend/supplier_request/admin.py | 3 + blocktrack_backend/supplier_request/apps.py | 6 ++ .../migrations/0001_initial.py | 29 ++++++ .../supplier_request/migrations/__init__.py | 0 blocktrack_backend/supplier_request/models.py | 18 ++++ .../supplier_request/serializers.py | 7 ++ blocktrack_backend/supplier_request/tests.py | 81 +++++++++++++++++ blocktrack_backend/supplier_request/urls.py | 14 +++ blocktrack_backend/supplier_request/views.py | 90 +++++++++++++++++++ 10 files changed, 248 insertions(+) create mode 100644 blocktrack_backend/supplier_request/__init__.py create mode 100644 blocktrack_backend/supplier_request/admin.py create mode 100644 blocktrack_backend/supplier_request/apps.py create mode 100644 blocktrack_backend/supplier_request/migrations/0001_initial.py create mode 100644 blocktrack_backend/supplier_request/migrations/__init__.py create mode 100644 blocktrack_backend/supplier_request/models.py create mode 100644 blocktrack_backend/supplier_request/serializers.py create mode 100644 blocktrack_backend/supplier_request/tests.py create mode 100644 blocktrack_backend/supplier_request/urls.py create mode 100644 blocktrack_backend/supplier_request/views.py diff --git a/blocktrack_backend/supplier_request/__init__.py b/blocktrack_backend/supplier_request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blocktrack_backend/supplier_request/admin.py b/blocktrack_backend/supplier_request/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/blocktrack_backend/supplier_request/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/blocktrack_backend/supplier_request/apps.py b/blocktrack_backend/supplier_request/apps.py new file mode 100644 index 0000000..4e4398e --- /dev/null +++ b/blocktrack_backend/supplier_request/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SupplierRequestConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'supplier_request' diff --git a/blocktrack_backend/supplier_request/migrations/0001_initial.py b/blocktrack_backend/supplier_request/migrations/0001_initial.py new file mode 100644 index 0000000..43a033f --- /dev/null +++ b/blocktrack_backend/supplier_request/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2 on 2025-05-08 06:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='SupplierRequest', + fields=[ + ('request_id', models.AutoField(primary_key=True, serialize=False)), + ('supplier_id', models.IntegerField()), + ('created_at', models.DateTimeField()), + ('expected_delivery_date', models.DateTimeField()), + ('product_id', models.IntegerField()), + ('count', models.FloatField()), + ('status', models.CharField(choices=[('pending', 'Pending'), ('received', 'Received')], max_length=10)), + ('received_at', models.DateTimeField(blank=True, null=True)), + ('warehouse_id', models.IntegerField()), + ('unit_price', models.FloatField()), + ], + ), + ] diff --git a/blocktrack_backend/supplier_request/migrations/__init__.py b/blocktrack_backend/supplier_request/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blocktrack_backend/supplier_request/models.py b/blocktrack_backend/supplier_request/models.py new file mode 100644 index 0000000..5119220 --- /dev/null +++ b/blocktrack_backend/supplier_request/models.py @@ -0,0 +1,18 @@ +from django.db import models + +class SupplierRequest(models.Model): + STATUS_CHOICES = [ + ('pending', 'Pending'), + ('received', 'Received'), + ] + + request_id = models.AutoField(primary_key=True) + supplier_id = models.IntegerField() + created_at = models.DateTimeField() + expected_delivery_date = models.DateTimeField() + product_id = models.IntegerField() + count = models.FloatField() + status = models.CharField(max_length=10, choices=STATUS_CHOICES) + received_at = models.DateTimeField(null=True, blank=True) + warehouse_id = models.IntegerField() + unit_price = models.FloatField() diff --git a/blocktrack_backend/supplier_request/serializers.py b/blocktrack_backend/supplier_request/serializers.py new file mode 100644 index 0000000..ef6d034 --- /dev/null +++ b/blocktrack_backend/supplier_request/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from .models import SupplierRequest + +class SupplierRequestSerializer(serializers.ModelSerializer): + class Meta: + model = SupplierRequest + fields = '__all__' diff --git a/blocktrack_backend/supplier_request/tests.py b/blocktrack_backend/supplier_request/tests.py new file mode 100644 index 0000000..331c901 --- /dev/null +++ b/blocktrack_backend/supplier_request/tests.py @@ -0,0 +1,81 @@ +from rest_framework.test import APITestCase +from rest_framework import status +from django.utils import timezone + +from .models import SupplierRequest + + +class SupplierRequestTests(APITestCase): + def setUp(self): + self.now = timezone.now() + self.supplier_request = SupplierRequest.objects.create( + supplier_id=1, + created_at=self.now, + expected_delivery_date=self.now, + product_id=101, + count=50.0, + status='pending', + received_at=None, + warehouse_id=1, + unit_price=20.5 + ) + self.base_url = '/api/v0/supplier-request/' + + def test_create_supplier_request(self): + url = self.base_url + data = { + 'supplier_id': 2, + 'created_at': timezone.now().isoformat(), + 'expected_delivery_date': timezone.now().isoformat(), + 'product_id': 102, + 'count': 75.0, + 'status': 'pending', + 'received_at': None, + 'warehouse_id': 1, + 'unit_price': 25.0 + } + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertIn('request_id', response.data) + + def test_get_supplier_requests(self): + url = self.base_url + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsInstance(response.data, list) + self.assertEqual(len(response.data), 1) + + def test_get_supplier_request_by_id(self): + url = f"{self.base_url}{self.supplier_request.request_id}/" + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['supplier_id'], self.supplier_request.supplier_id) + + def test_update_supplier_request_status(self): + url = f"{self.base_url}{self.supplier_request.request_id}/status/" + response = self.client.patch(url, {'status': 'received'}, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['status'], 'received') + + def test_partial_update_supplier_request(self): + url = f"{self.base_url}{self.supplier_request.request_id}/" + response = self.client.patch(url, {'count': 60.0}, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.supplier_request.refresh_from_db() + self.assertEqual(self.supplier_request.count, 60.0) + + def test_delete_supplier_request(self): + url = f"{self.base_url}{self.supplier_request.request_id}/" + response = self.client.delete(url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertFalse( + SupplierRequest.objects.filter(request_id=self.supplier_request.request_id).exists() + ) + + def test_get_supplier_requests_by_warehouse(self): + warehouse_id = self.supplier_request.warehouse_id + url = f"{self.base_url}warehouse/{warehouse_id}/" + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + for req in response.data: + self.assertEqual(req['warehouse_id'], warehouse_id) diff --git a/blocktrack_backend/supplier_request/urls.py b/blocktrack_backend/supplier_request/urls.py new file mode 100644 index 0000000..b1fa4dd --- /dev/null +++ b/blocktrack_backend/supplier_request/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from .views import ( + SupplierRequestListCreate, + SupplierRequestByWarehouse, + SupplierRequestStatusUpdate, + SupplierRequestGetOrPartialUpdate +) + +urlpatterns = [ + path('supplier-request/', SupplierRequestListCreate.as_view()), + path('supplier-request/warehouse//', SupplierRequestByWarehouse.as_view()), + path('supplier-request//status/', SupplierRequestStatusUpdate.as_view()), + path('supplier-request//', SupplierRequestGetOrPartialUpdate.as_view()), +] diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py new file mode 100644 index 0000000..d2ffae4 --- /dev/null +++ b/blocktrack_backend/supplier_request/views.py @@ -0,0 +1,90 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from .models import SupplierRequest +from .serializers import SupplierRequestSerializer + + +class SupplierRequestListCreate(APIView): + @swagger_auto_schema( + request_body=SupplierRequestSerializer, + responses={201: SupplierRequestSerializer()} + ) + def post(self, request): + serializer = SupplierRequestSerializer(data=request.data) + if serializer.is_valid(): + # custom logic here + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + 'request_id', openapi.IN_QUERY, + description="Optional request_id to filter", + type=openapi.TYPE_STRING + ) + ], + responses={200: SupplierRequestSerializer(many=True)} + ) + def get(self, request): + requests = SupplierRequest.objects.all() + serializer = SupplierRequestSerializer(requests, many=True) + return Response(serializer.data) + + +class SupplierRequestByWarehouse(APIView): + def get(self, request, warehouse_id): + requests = SupplierRequest.objects.filter(warehouse_id=warehouse_id) + serializer = SupplierRequestSerializer(requests, many=True) + return Response(serializer.data) + + +class SupplierRequestStatusUpdate(APIView): + def patch(self, request, request_id): + try: + req = SupplierRequest.objects.get(request_id=request_id) + except SupplierRequest.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + if 'status' not in request.data: + return Response({'error': 'Status is required'}, status=status.HTTP_400_BAD_REQUEST) + + # custom logic here + req.status = request.data['status'] + req.save() + return Response(SupplierRequestSerializer(req).data) + + +class SupplierRequestGetOrPartialUpdate(APIView): + def get(self, request, request_id): + try: + req = SupplierRequest.objects.get(request_id=request_id) + except SupplierRequest.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + serializer = SupplierRequestSerializer(req) + return Response(serializer.data) + + def patch(self, request, request_id): + try: + req = SupplierRequest.objects.get(request_id=request_id) + except SupplierRequest.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + serializer = SupplierRequestSerializer(req, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, request_id): + try: + req = SupplierRequest.objects.get(request_id=request_id) + except SupplierRequest.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + req.delete() + return Response(status=status.HTTP_204_NO_CONTENT) From b392baf13c3b5222a9a0bada42e82bc75dd1463e Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 14:04:03 +0530 Subject: [PATCH 04/30] Added swagger ui, and updated settings.py to use env variables --- .../blocktrack_backend/settings.py | 22 ++++++++++++------- blocktrack_backend/blocktrack_backend/urls.py | 19 +++++++++++++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/blocktrack_backend/blocktrack_backend/settings.py b/blocktrack_backend/blocktrack_backend/settings.py index dbbef6e..95b7935 100644 --- a/blocktrack_backend/blocktrack_backend/settings.py +++ b/blocktrack_backend/blocktrack_backend/settings.py @@ -9,6 +9,10 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.2/ref/settings/ """ +import os +from dotenv import load_dotenv +load_dotenv() + from pathlib import Path @@ -20,10 +24,11 @@ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-b^0a637n!kbfd340ul=p@p^3w-17v+ni5bw^(7#&5_br)^z0pj' + +SECRET_KEY = os.getenv('SECRET_KEY', 'your-default-secret-key') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.getenv('DEBUG') ALLOWED_HOSTS = [] @@ -41,6 +46,8 @@ 'orders', 'corsheaders', 'django_filters', + 'supplier_request', + 'drf_yasg' ] MIDDLEWARE = [ @@ -81,14 +88,13 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'blocktrack_db', - 'USER': 'blockuser', - 'PASSWORD': 'blockpass', - 'HOST': 'localhost', - 'PORT': '5432', + 'NAME': os.getenv('DB_NAME'), + 'USER': os.getenv('DB_USER'), + 'PASSWORD': os.getenv('DB_PASSWORD'), + 'HOST': os.getenv('DB_HOST', 'localhost'), + 'PORT': os.getenv('DB_PORT'), } } - REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'] } diff --git a/blocktrack_backend/blocktrack_backend/urls.py b/blocktrack_backend/blocktrack_backend/urls.py index 6e04dc4..b84ae01 100644 --- a/blocktrack_backend/blocktrack_backend/urls.py +++ b/blocktrack_backend/blocktrack_backend/urls.py @@ -17,7 +17,24 @@ from django.contrib import admin from django.urls import path, include +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="Your API", + default_version='v1', + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + urlpatterns = [ path('admin/', admin.site.urls), - path('api/', include('orders.urls')), + path('api/v0/', include('orders.urls')), + path('api/v0/', include('supplier_request.urls')), + + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), ] From 0a5f98baa6491f19b4491ddb0036a05978547958 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 14:04:18 +0530 Subject: [PATCH 05/30] Updated order.models to have OrderDetails --- .../orders/migrations/0002_orderdetails.py | 28 +++++++++++++++++++ ...03_remove_order_id_alter_order_order_id.py | 22 +++++++++++++++ blocktrack_backend/orders/models.py | 15 +++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 blocktrack_backend/orders/migrations/0002_orderdetails.py create mode 100644 blocktrack_backend/orders/migrations/0003_remove_order_id_alter_order_order_id.py diff --git a/blocktrack_backend/orders/migrations/0002_orderdetails.py b/blocktrack_backend/orders/migrations/0002_orderdetails.py new file mode 100644 index 0000000..79fc75f --- /dev/null +++ b/blocktrack_backend/orders/migrations/0002_orderdetails.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2 on 2025-05-08 06:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='OrderDetails', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_id', models.IntegerField()), + ('count', models.IntegerField()), + ('warehouse_id', models.IntegerField()), + ('address', models.CharField(max_length=255)), + ('zipcode', models.CharField(max_length=20)), + ('city', models.CharField(max_length=100)), + ('country', models.CharField(max_length=100)), + ('order_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='details', to='orders.order')), + ], + ), + ] diff --git a/blocktrack_backend/orders/migrations/0003_remove_order_id_alter_order_order_id.py b/blocktrack_backend/orders/migrations/0003_remove_order_id_alter_order_order_id.py new file mode 100644 index 0000000..d965e04 --- /dev/null +++ b/blocktrack_backend/orders/migrations/0003_remove_order_id_alter_order_order_id.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2 on 2025-05-08 08:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0002_orderdetails'), + ] + + operations = [ + migrations.RemoveField( + model_name='order', + name='id', + ), + migrations.AlterField( + model_name='order', + name='order_id', + field=models.AutoField(primary_key=True, serialize=False), + ), + ] diff --git a/blocktrack_backend/orders/models.py b/blocktrack_backend/orders/models.py index f0848b2..66d725b 100644 --- a/blocktrack_backend/orders/models.py +++ b/blocktrack_backend/orders/models.py @@ -1,7 +1,7 @@ from django.db import models class Order(models.Model): - order_id = models.CharField(max_length=100, unique=True) + order_id = models.AutoField(primary_key=True) product = models.CharField(max_length=255) customer = models.CharField(max_length=255) status = models.CharField(max_length=50, choices=[ @@ -16,3 +16,16 @@ class Order(models.Model): def __str__(self): return f"Order {self.order_id} - {self.status}" + +class OrderDetails(models.Model): + order_id = models.ForeignKey(Order, related_name='details', on_delete=models.CASCADE) + product_id = models.IntegerField() + count = models.IntegerField() + warehouse_id = models.IntegerField() + address = models.CharField(max_length=255) + zipcode = models.CharField(max_length=20) + city = models.CharField(max_length=100) + country = models.CharField(max_length=100) + + def __str__(self): + return f"Order {self.order.order_id} - Product {self.product_id}" From ed7b1161a927e2af7cf300e2b235357e700e68a9 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 18:03:23 +0530 Subject: [PATCH 06/30] Added route to export swagger.json --- blocktrack_backend/blocktrack_backend/urls.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/blocktrack_backend/blocktrack_backend/urls.py b/blocktrack_backend/blocktrack_backend/urls.py index b84ae01..96411df 100644 --- a/blocktrack_backend/blocktrack_backend/urls.py +++ b/blocktrack_backend/blocktrack_backend/urls.py @@ -37,4 +37,9 @@ path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path( + 'swagger.json', + schema_view.without_ui(cache_timeout=0), + name='schema-swagger-ui-json' + ), ] From 32262bc3779d9d26ca642cbe6cabeb9066fda467 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 18:04:27 +0530 Subject: [PATCH 07/30] Added new model OrderProduct to add multiple products to order --- blocktrack_backend/orders/models.py | 33 ++++++++++++++--------- blocktrack_backend/orders/serializers.py | 34 ++++++++++++++++++++++-- blocktrack_backend/orders/urls.py | 1 + 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/blocktrack_backend/orders/models.py b/blocktrack_backend/orders/models.py index 66d725b..d30d192 100644 --- a/blocktrack_backend/orders/models.py +++ b/blocktrack_backend/orders/models.py @@ -2,30 +2,37 @@ class Order(models.Model): order_id = models.AutoField(primary_key=True) - product = models.CharField(max_length=255) - customer = models.CharField(max_length=255) + # product = models.CharField(max_length=255) + user_id = models.IntegerField() status = models.CharField(max_length=50, choices=[ - ('Preparing', 'Preparing'), + ('Pending', 'Pending'), + ('Accepted', 'Accepted'), ('Shipped', 'Shipped'), ('Delivered', 'Delivered'), ('Cancelled', 'Cancelled'), ]) - blockchain_tx_id = models.CharField(max_length=255) # Fabric transaction ID - ipfs_hash = models.CharField(max_length=255) # IPFS CID + blockchain_tx_id = models.CharField(max_length=255) + ipfs_hash = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Order {self.order_id} - {self.status}" class OrderDetails(models.Model): - order_id = models.ForeignKey(Order, related_name='details', on_delete=models.CASCADE) - product_id = models.IntegerField() - count = models.IntegerField() + order_id = models.ForeignKey(Order, related_name='order_details', on_delete=models.CASCADE) warehouse_id = models.IntegerField() - address = models.CharField(max_length=255) - zipcode = models.CharField(max_length=20) - city = models.CharField(max_length=100) - country = models.CharField(max_length=100) + nearest_city = models.CharField(max_length=255) + latitude = models.CharField(max_length=32) + longitude = models.CharField(max_length=32) def __str__(self): - return f"Order {self.order.order_id} - Product {self.product_id}" + return f"Order {self.order.order_id} - Details" + +class OrderProduct(models.Model): + order = models.ForeignKey(Order, related_name='order_products', on_delete=models.CASCADE) + product_id = models.IntegerField() + count = models.IntegerField() + unit_price = models.FloatField() + + + diff --git a/blocktrack_backend/orders/serializers.py b/blocktrack_backend/orders/serializers.py index 568fd6c..83eeda6 100644 --- a/blocktrack_backend/orders/serializers.py +++ b/blocktrack_backend/orders/serializers.py @@ -1,7 +1,37 @@ from rest_framework import serializers -from .models import Order +from .models import Order, OrderDetails, OrderProduct + + +class OrderProductSerializer(serializers.ModelSerializer): + class Meta: + model = OrderProduct + fields = ['product_id', 'count', 'unit_price'] + +class OrderDetailsSerializer(serializers.ModelSerializer): + class Meta: + model = OrderDetails + fields = ['warehouse_id', 'nearest_city', 'latitude', 'longitude'] class OrderSerializer(serializers.ModelSerializer): + details = OrderDetailsSerializer() + products = OrderProductSerializer(many=True) + class Meta: model = Order - fields = '__all__' + fields = ['order_id', 'user_id', 'status', + 'blockchain_tx_id', 'ipfs_hash', 'created_at', + 'details', 'products'] + read_only_fields = ['order_id', 'created_at'] + + def create(self, validated_data): + details_data = validated_data.pop('details') + products_data = validated_data.pop('products') + + order = Order.objects.create(**validated_data) + + OrderDetails.objects.create(order=order, **details_data) + + for prod in products_data: + OrderProduct.objects.create(order_id=order, **prod) + + return order \ No newline at end of file diff --git a/blocktrack_backend/orders/urls.py b/blocktrack_backend/orders/urls.py index d164c01..e3a55c2 100644 --- a/blocktrack_backend/orders/urls.py +++ b/blocktrack_backend/orders/urls.py @@ -6,4 +6,5 @@ path('read-order//', ReadOrderView.as_view()), path('orders/', OrderListCreateView.as_view(), name='order-list-create'), path('orders//', OrderDetailView.as_view(), name='order-detail'), + # path("api/legacy/order//", ReadOrderLegacyView.as_view()), ] From ee7d2cb2123df8759712316d2943ed8664948db7 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 18:36:02 +0530 Subject: [PATCH 08/30] Updated migrations --- ...ress_orderdetails_nearest_city_and_more.py | 71 +++++++++++++++++++ ..._customer_remove_order_product_and_more.py | 27 +++++++ .../migrations/0006_alter_order_user_id.py | 18 +++++ 3 files changed, 116 insertions(+) create mode 100644 blocktrack_backend/orders/migrations/0004_rename_address_orderdetails_nearest_city_and_more.py create mode 100644 blocktrack_backend/orders/migrations/0005_remove_order_customer_remove_order_product_and_more.py create mode 100644 blocktrack_backend/orders/migrations/0006_alter_order_user_id.py diff --git a/blocktrack_backend/orders/migrations/0004_rename_address_orderdetails_nearest_city_and_more.py b/blocktrack_backend/orders/migrations/0004_rename_address_orderdetails_nearest_city_and_more.py new file mode 100644 index 0000000..e8bd85e --- /dev/null +++ b/blocktrack_backend/orders/migrations/0004_rename_address_orderdetails_nearest_city_and_more.py @@ -0,0 +1,71 @@ +# Generated by Django 5.2 on 2025-05-08 12:09 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0003_remove_order_id_alter_order_order_id'), + ] + + operations = [ + migrations.RenameField( + model_name='orderdetails', + old_name='address', + new_name='nearest_city', + ), + migrations.RemoveField( + model_name='orderdetails', + name='city', + ), + migrations.RemoveField( + model_name='orderdetails', + name='count', + ), + migrations.RemoveField( + model_name='orderdetails', + name='country', + ), + migrations.RemoveField( + model_name='orderdetails', + name='product_id', + ), + migrations.RemoveField( + model_name='orderdetails', + name='zipcode', + ), + migrations.AddField( + model_name='orderdetails', + name='latitude', + field=models.CharField(default=None, max_length=32), + preserve_default=False, + ), + migrations.AddField( + model_name='orderdetails', + name='longitude', + field=models.CharField(default=None, max_length=32), + preserve_default=False, + ), + migrations.AlterField( + model_name='order', + name='status', + field=models.CharField(choices=[('Pending', 'Pending'), ('Accepted', 'Accepted'), ('Shipped', 'Shipped'), ('Delivered', 'Delivered'), ('Cancelled', 'Cancelled')], max_length=50), + ), + migrations.AlterField( + model_name='orderdetails', + name='order_id', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_details', to='orders.order'), + ), + migrations.CreateModel( + name='OrderProduct', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_id', models.IntegerField()), + ('count', models.IntegerField()), + ('unit_price', models.FloatField()), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_products', to='orders.order')), + ], + ), + ] diff --git a/blocktrack_backend/orders/migrations/0005_remove_order_customer_remove_order_product_and_more.py b/blocktrack_backend/orders/migrations/0005_remove_order_customer_remove_order_product_and_more.py new file mode 100644 index 0000000..a792bea --- /dev/null +++ b/blocktrack_backend/orders/migrations/0005_remove_order_customer_remove_order_product_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2 on 2025-05-08 12:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0004_rename_address_orderdetails_nearest_city_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='order', + name='customer', + ), + migrations.RemoveField( + model_name='order', + name='product', + ), + migrations.AddField( + model_name='order', + name='user_id', + field=models.IntegerField(default=None), + preserve_default=False, + ), + ] diff --git a/blocktrack_backend/orders/migrations/0006_alter_order_user_id.py b/blocktrack_backend/orders/migrations/0006_alter_order_user_id.py new file mode 100644 index 0000000..66b3bee --- /dev/null +++ b/blocktrack_backend/orders/migrations/0006_alter_order_user_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-05-08 12:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0005_remove_order_customer_remove_order_product_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='user_id', + field=models.IntegerField(default=0), + ), + ] From 5572d6b91e04ac2b4642666eaa9c1e0d34db639d Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 18:40:12 +0530 Subject: [PATCH 09/30] Updated migrations and Models --- .../migrations/0006_alter_order_user_id.py | 18 ------------------ .../0006_rename_order_orderproduct_order_id.py | 18 ++++++++++++++++++ blocktrack_backend/orders/models.py | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) delete mode 100644 blocktrack_backend/orders/migrations/0006_alter_order_user_id.py create mode 100644 blocktrack_backend/orders/migrations/0006_rename_order_orderproduct_order_id.py diff --git a/blocktrack_backend/orders/migrations/0006_alter_order_user_id.py b/blocktrack_backend/orders/migrations/0006_alter_order_user_id.py deleted file mode 100644 index 66b3bee..0000000 --- a/blocktrack_backend/orders/migrations/0006_alter_order_user_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2 on 2025-05-08 12:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('orders', '0005_remove_order_customer_remove_order_product_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='order', - name='user_id', - field=models.IntegerField(default=0), - ), - ] diff --git a/blocktrack_backend/orders/migrations/0006_rename_order_orderproduct_order_id.py b/blocktrack_backend/orders/migrations/0006_rename_order_orderproduct_order_id.py new file mode 100644 index 0000000..66ca677 --- /dev/null +++ b/blocktrack_backend/orders/migrations/0006_rename_order_orderproduct_order_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-05-08 13:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0005_remove_order_customer_remove_order_product_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='orderproduct', + old_name='order', + new_name='order_id', + ), + ] diff --git a/blocktrack_backend/orders/models.py b/blocktrack_backend/orders/models.py index d30d192..60d3d2f 100644 --- a/blocktrack_backend/orders/models.py +++ b/blocktrack_backend/orders/models.py @@ -29,7 +29,7 @@ def __str__(self): return f"Order {self.order.order_id} - Details" class OrderProduct(models.Model): - order = models.ForeignKey(Order, related_name='order_products', on_delete=models.CASCADE) + order_id = models.ForeignKey(Order, related_name='order_products', on_delete=models.CASCADE) product_id = models.IntegerField() count = models.IntegerField() unit_price = models.FloatField() From 406e98917f6737b9cc8f6afe28a0b886866acf7b Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 18:45:43 +0530 Subject: [PATCH 10/30] Added dummy script to generate dummy data --- blocktrack_backend/dummy.py | 41 ++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/blocktrack_backend/dummy.py b/blocktrack_backend/dummy.py index e8ac157..a5f6222 100644 --- a/blocktrack_backend/dummy.py +++ b/blocktrack_backend/dummy.py @@ -1,5 +1,5 @@ from supplier_request.models import SupplierRequest -from orders.models import Order, OrderDetails +from orders.models import Order, OrderDetails, OrderProduct from django.utils import timezone import random import uuid @@ -29,28 +29,31 @@ zipcodes = ['00100', '00200', '01000', '10001'] # Generate Orders -for i in range(15): + +statuses = ['Pending', 'Accepted', 'Shipped', 'Delivered', 'Cancelled'] + +for i in range(5): order = Order.objects.create( - product=f"Product {random.randint(1, 50)}", - customer=f"Customer {random.randint(1, 20)}", - status=random.choice(order_statuses), - blockchain_tx_id=str(uuid.uuid4()), - ipfs_hash=str(uuid.uuid4()), - # created_at is auto_now_add + user_id=i, + status=random.choice(statuses), + blockchain_tx_id=f"tx_{i:04}", + ipfs_hash=f"QmHash{i:04}" + ) + + OrderDetails.objects.create( + order_id=order, + warehouse_id=random.randint(1000, 9999), + nearest_city=f"City-{i}", + latitude=f"{6.9 + i:.4f}", + longitude=f"{79.8 + i:.4f}" ) - # Generate between 1 and 5 details for each order - details_count = random.randint(1, 5) - for _ in range(details_count): - OrderDetails.objects.create( + for j in range(3): + OrderProduct.objects.create( order_id=order, - product_id=random.randint(100, 200), - count=random.randint(1, 20), - warehouse_id=random.randint(1, 5), - address=f"{random.randint(1, 999)} Main Street", - zipcode=random.choice(zipcodes), - city=random.choice(cities), - country=random.choice(countries) + product_id=100 + j, + count=random.randint(1, 10), + unit_price=round(random.uniform(10.0, 100.0), 2) ) print("Dummy data generation complete.") From e74c91bae78b9e0ccc83beaf4aed3095aa6ffcce Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 18:56:27 +0530 Subject: [PATCH 11/30] Update README --- README.md | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2bde27a..1ae30d7 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,43 @@ Open your browser at: [http://localhost:4200](http://localhost:4200) --- +### 7. Start the Database with Docker Compose +To start the database, use the provided `docker-compose.yaml` file: +```bash +cd blocktrack_backend +sudo docker-compose up -d +``` +This will start the PostgreSQL database required for the backend. + +### 8. Environment Variables +Ensure the following environment variables are set for the backend: + +```env +DATABASE_NAME=blocktrack_db +DATABASE_USER=blocktrack_user +DATABASE_PASSWORD=securepassword +DATABASE_HOST=localhost +DATABASE_PORT=5432 +SECRET_KEY=your_secret_key_here +DEBUG=True +ALLOWED_HOSTS=* +``` + +### 9. Environment Variables +Run the dummy.py as follows: +```bash +# On Linux/MacOS +python manage.py shell < dummy.py + +# On Windows +python manage.py shell < dummy.py +``` + +--- + ## πŸ”Œ API Endpoints -| Method | URL | Description | -|--------|-------------------------------|--------------------------------| -| POST | `/api/create-order/` | Create order + file + IPFS | -| GET | `/api/read-order//` | Retrieve order from blockchain | +Run the django app and visit `/swagger` to view the swagger-UI docs --- From 34904b9c5938f88430fe0783b65eab52fd1633b3 Mon Sep 17 00:00:00 2001 From: Himath Samarakoon <114925949+moonlander101@users.noreply.github.com> Date: Thu, 8 May 2025 18:58:41 +0530 Subject: [PATCH 12/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ae30d7..7932557 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ DEBUG=True ALLOWED_HOSTS=* ``` -### 9. Environment Variables +### 9. Dummy Data Run the dummy.py as follows: ```bash # On Linux/MacOS From b92b40e018d7554136c73ee033cba945021e8ecc Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 20:48:50 +0530 Subject: [PATCH 13/30] Updated View to filter orders by warehouse_id --- blocktrack_backend/dummy.py | 22 ++++------------- ...rderdetails_order_id_orderdetails_order.py | 24 +++++++++++++++++++ ...0008_rename_order_id_orderproduct_order.py | 18 ++++++++++++++ blocktrack_backend/orders/models.py | 4 ++-- blocktrack_backend/orders/serializers.py | 24 ++++++++++++------- blocktrack_backend/orders/urls.py | 3 ++- blocktrack_backend/orders/views.py | 13 +++++++++- 7 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 blocktrack_backend/orders/migrations/0007_remove_orderdetails_order_id_orderdetails_order.py create mode 100644 blocktrack_backend/orders/migrations/0008_rename_order_id_orderproduct_order.py diff --git a/blocktrack_backend/dummy.py b/blocktrack_backend/dummy.py index a5f6222..a7fa301 100644 --- a/blocktrack_backend/dummy.py +++ b/blocktrack_backend/dummy.py @@ -2,11 +2,9 @@ from orders.models import Order, OrderDetails, OrderProduct from django.utils import timezone import random -import uuid -# Dummy data for SupplierRequest +# SupplierRequest dummy data statuses = ['pending', 'received'] - for i in range(10): SupplierRequest.objects.create( supplier_id=random.randint(1, 5), @@ -20,28 +18,18 @@ unit_price=round(random.uniform(10, 500), 2) ) -# Dummy data for Order and OrderDetails -order_statuses = ['Preparing', 'Shipped', 'Delivered', 'Cancelled'] - -# Sample addresses -cities = ['Colombo', 'Kandy', 'Galle', 'Jaffna'] -countries = ['Sri Lanka', 'India', 'USA', 'UK'] -zipcodes = ['00100', '00200', '01000', '10001'] - -# Generate Orders - -statuses = ['Pending', 'Accepted', 'Shipped', 'Delivered', 'Cancelled'] - +# Orders and OrderDetails dummy data +order_statuses = ['Pending', 'Accepted', 'Shipped', 'Delivered', 'Cancelled'] for i in range(5): order = Order.objects.create( user_id=i, - status=random.choice(statuses), + status=random.choice(order_statuses), blockchain_tx_id=f"tx_{i:04}", ipfs_hash=f"QmHash{i:04}" ) OrderDetails.objects.create( - order_id=order, + order=order, warehouse_id=random.randint(1000, 9999), nearest_city=f"City-{i}", latitude=f"{6.9 + i:.4f}", diff --git a/blocktrack_backend/orders/migrations/0007_remove_orderdetails_order_id_orderdetails_order.py b/blocktrack_backend/orders/migrations/0007_remove_orderdetails_order_id_orderdetails_order.py new file mode 100644 index 0000000..99ce284 --- /dev/null +++ b/blocktrack_backend/orders/migrations/0007_remove_orderdetails_order_id_orderdetails_order.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2 on 2025-05-08 14:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0006_rename_order_orderproduct_order_id'), + ] + + operations = [ + migrations.RemoveField( + model_name='orderdetails', + name='order_id', + ), + migrations.AddField( + model_name='orderdetails', + name='order', + field=models.OneToOneField(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='details', to='orders.order'), + preserve_default=False, + ), + ] diff --git a/blocktrack_backend/orders/migrations/0008_rename_order_id_orderproduct_order.py b/blocktrack_backend/orders/migrations/0008_rename_order_id_orderproduct_order.py new file mode 100644 index 0000000..793ef4d --- /dev/null +++ b/blocktrack_backend/orders/migrations/0008_rename_order_id_orderproduct_order.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-05-08 14:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0007_remove_orderdetails_order_id_orderdetails_order'), + ] + + operations = [ + migrations.RenameField( + model_name='orderproduct', + old_name='order_id', + new_name='order', + ), + ] diff --git a/blocktrack_backend/orders/models.py b/blocktrack_backend/orders/models.py index 60d3d2f..9fd25dc 100644 --- a/blocktrack_backend/orders/models.py +++ b/blocktrack_backend/orders/models.py @@ -19,7 +19,7 @@ def __str__(self): return f"Order {self.order_id} - {self.status}" class OrderDetails(models.Model): - order_id = models.ForeignKey(Order, related_name='order_details', on_delete=models.CASCADE) + order = models.OneToOneField(Order, related_name='details', on_delete=models.CASCADE) warehouse_id = models.IntegerField() nearest_city = models.CharField(max_length=255) latitude = models.CharField(max_length=32) @@ -29,7 +29,7 @@ def __str__(self): return f"Order {self.order.order_id} - Details" class OrderProduct(models.Model): - order_id = models.ForeignKey(Order, related_name='order_products', on_delete=models.CASCADE) + order = models.ForeignKey(Order, related_name='order_products', on_delete=models.CASCADE) product_id = models.IntegerField() count = models.IntegerField() unit_price = models.FloatField() diff --git a/blocktrack_backend/orders/serializers.py b/blocktrack_backend/orders/serializers.py index 83eeda6..5a71f68 100644 --- a/blocktrack_backend/orders/serializers.py +++ b/blocktrack_backend/orders/serializers.py @@ -1,26 +1,28 @@ from rest_framework import serializers from .models import Order, OrderDetails, OrderProduct - class OrderProductSerializer(serializers.ModelSerializer): class Meta: model = OrderProduct fields = ['product_id', 'count', 'unit_price'] +class MinimalOrderProductSerializer(serializers.ModelSerializer): + class Meta: + model = OrderProduct + fields = ['product_id', 'count'] + class OrderDetailsSerializer(serializers.ModelSerializer): class Meta: model = OrderDetails fields = ['warehouse_id', 'nearest_city', 'latitude', 'longitude'] class OrderSerializer(serializers.ModelSerializer): - details = OrderDetailsSerializer() - products = OrderProductSerializer(many=True) + products = OrderProductSerializer(many=True, read_only=True, source='order_products') + details = OrderDetailsSerializer(read_only=True) class Meta: model = Order - fields = ['order_id', 'user_id', 'status', - 'blockchain_tx_id', 'ipfs_hash', 'created_at', - 'details', 'products'] + fields = ['order_id', 'user_id', 'status', 'blockchain_tx_id', 'ipfs_hash', 'created_at', 'products', 'details'] read_only_fields = ['order_id', 'created_at'] def create(self, validated_data): @@ -28,10 +30,16 @@ def create(self, validated_data): products_data = validated_data.pop('products') order = Order.objects.create(**validated_data) - OrderDetails.objects.create(order=order, **details_data) for prod in products_data: OrderProduct.objects.create(order_id=order, **prod) - return order \ No newline at end of file + return order + +class MinimalOrderSerializer(serializers.ModelSerializer): + products = MinimalOrderProductSerializer(many=True, read_only=True, source='order_products') + + class Meta: + model = Order + fields = ['order_id', 'products'] diff --git a/blocktrack_backend/orders/urls.py b/blocktrack_backend/orders/urls.py index e3a55c2..c0ff6bc 100644 --- a/blocktrack_backend/orders/urls.py +++ b/blocktrack_backend/orders/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from .views import CreateOrderView, ReadOrderView, OrderListCreateView, OrderDetailView +from .views import CreateOrderView, OrderByWarehouse, ReadOrderView, OrderListCreateView, OrderDetailView urlpatterns = [ path("create-order/", CreateOrderView.as_view()), path('read-order//', ReadOrderView.as_view()), path('orders/', OrderListCreateView.as_view(), name='order-list-create'), path('orders//', OrderDetailView.as_view(), name='order-detail'), + path('orders/warehouse/', OrderByWarehouse.as_view(), name='order-detail-by-warehouse') # path("api/legacy/order//", ReadOrderLegacyView.as_view()), ] diff --git a/blocktrack_backend/orders/views.py b/blocktrack_backend/orders/views.py index e2f130d..37663d0 100644 --- a/blocktrack_backend/orders/views.py +++ b/blocktrack_backend/orders/views.py @@ -3,7 +3,7 @@ from rest_framework.parsers import MultiPartParser from rest_framework import generics from .models import Order -from .serializers import OrderSerializer +from .serializers import MinimalOrderSerializer, OrderSerializer import subprocess import tempfile import os @@ -133,6 +133,17 @@ class OrderListCreateView(generics.ListCreateAPIView): filter_backends = [DjangoFilterBackend] filterset_fields = ['status'] +class OrderByWarehouse(APIView): + def get(self, request, warehouse_id): + queryset = Order.objects.filter(details__warehouse_id=warehouse_id) + + minimal = request.GET.get('minimal', False) + + if (minimal): + serializer = MinimalOrderSerializer(queryset, many=True) + else: + serializer = OrderSerializer(queryset, many=True) + return Response(serializer.data) class OrderDetailView(generics.RetrieveUpdateAPIView): queryset = Order.objects.all() From bbd72be5a6ed3d94e9e999f1bf0819750e9d74f8 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 20:54:19 +0530 Subject: [PATCH 14/30] Updated swagger --- blocktrack_backend/orders/views.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/blocktrack_backend/orders/views.py b/blocktrack_backend/orders/views.py index 37663d0..ec8613d 100644 --- a/blocktrack_backend/orders/views.py +++ b/blocktrack_backend/orders/views.py @@ -10,6 +10,9 @@ from .ipfs_utils import upload_to_ipfs from django_filters.rest_framework import DjangoFilterBackend +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + FABRIC_BASE = "/Users/ravishan/hyperledger-fabric/fabric-samples" BIN_PATH = f"{FABRIC_BASE}/bin" TEST_NETWORK = f"{FABRIC_BASE}/test-network" @@ -134,12 +137,20 @@ class OrderListCreateView(generics.ListCreateAPIView): filterset_fields = ['status'] class OrderByWarehouse(APIView): + @swagger_auto_schema( + manual_parameters=[ + # Define query parameter "test" + openapi.Parameter( + 'minimal', openapi.IN_QUERY, description="Option to reduce parameters to only order_id and products", type=openapi.TYPE_BOOLEAN + ) + ] + ) def get(self, request, warehouse_id): queryset = Order.objects.filter(details__warehouse_id=warehouse_id) minimal = request.GET.get('minimal', False) - if (minimal): + if (eval(minimal[0].upper() + minimal[1:])): serializer = MinimalOrderSerializer(queryset, many=True) else: serializer = OrderSerializer(queryset, many=True) From b545a6cde581cab138ea76e3b79bdcefd49b45ac Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 22:07:15 +0530 Subject: [PATCH 15/30] Updated Supplier_Request model to store data related to quality, updated tests --- ...ective_supplierrequest_quality_and_more.py | 29 +++++++++++++++++++ blocktrack_backend/supplier_request/models.py | 9 ++++++ blocktrack_backend/supplier_request/tests.py | 19 ++++++++---- blocktrack_backend/supplier_request/views.py | 20 ++++++------- 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 blocktrack_backend/supplier_request/migrations/0002_supplierrequest_is_defective_supplierrequest_quality_and_more.py diff --git a/blocktrack_backend/supplier_request/migrations/0002_supplierrequest_is_defective_supplierrequest_quality_and_more.py b/blocktrack_backend/supplier_request/migrations/0002_supplierrequest_is_defective_supplierrequest_quality_and_more.py new file mode 100644 index 0000000..5da3823 --- /dev/null +++ b/blocktrack_backend/supplier_request/migrations/0002_supplierrequest_is_defective_supplierrequest_quality_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2 on 2025-05-08 16:27 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('supplier_request', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='supplierrequest', + name='is_defective', + field=models.BooleanField(blank=True, null=True), + ), + migrations.AddField( + model_name='supplierrequest', + name='quality', + field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)]), + ), + migrations.AlterField( + model_name='supplierrequest', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('received', 'Received'), ('returned', 'Returned'), ('rejected', 'Rejected')], max_length=10), + ), + ] diff --git a/blocktrack_backend/supplier_request/models.py b/blocktrack_backend/supplier_request/models.py index 5119220..0f5ff0c 100644 --- a/blocktrack_backend/supplier_request/models.py +++ b/blocktrack_backend/supplier_request/models.py @@ -1,9 +1,13 @@ from django.db import models +from django.core.validators import MinValueValidator, MaxValueValidator class SupplierRequest(models.Model): STATUS_CHOICES = [ ('pending', 'Pending'), + ('accepted', 'Accepted'), ('received', 'Received'), + ('returned', "Returned"), + ('rejected', 'Rejected'), ] request_id = models.AutoField(primary_key=True) @@ -16,3 +20,8 @@ class SupplierRequest(models.Model): received_at = models.DateTimeField(null=True, blank=True) warehouse_id = models.IntegerField() unit_price = models.FloatField() + quality = models.IntegerField(validators=[ + MinValueValidator(0), + MaxValueValidator(10) + ], blank=True, null=True) + is_defective = models.BooleanField(blank=True, null=True) diff --git a/blocktrack_backend/supplier_request/tests.py b/blocktrack_backend/supplier_request/tests.py index 331c901..9e459e3 100644 --- a/blocktrack_backend/supplier_request/tests.py +++ b/blocktrack_backend/supplier_request/tests.py @@ -1,10 +1,8 @@ from rest_framework.test import APITestCase from rest_framework import status from django.utils import timezone - from .models import SupplierRequest - class SupplierRequestTests(APITestCase): def setUp(self): self.now = timezone.now() @@ -17,7 +15,9 @@ def setUp(self): status='pending', received_at=None, warehouse_id=1, - unit_price=20.5 + unit_price=20.5, + quality=7, + is_defective=False ) self.base_url = '/api/v0/supplier-request/' @@ -32,7 +32,9 @@ def test_create_supplier_request(self): 'status': 'pending', 'received_at': None, 'warehouse_id': 1, - 'unit_price': 25.0 + 'unit_price': 25.0, + 'quality': 9, + 'is_defective': False } response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -59,10 +61,17 @@ def test_update_supplier_request_status(self): def test_partial_update_supplier_request(self): url = f"{self.base_url}{self.supplier_request.request_id}/" - response = self.client.patch(url, {'count': 60.0}, format='json') + patch_data = { + 'count': 60.0, + 'quality': 8, + 'is_defective': True + } + response = self.client.patch(url, patch_data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) self.supplier_request.refresh_from_db() self.assertEqual(self.supplier_request.count, 60.0) + self.assertEqual(self.supplier_request.quality, 8) + self.assertTrue(self.supplier_request.is_defective) def test_delete_supplier_request(self): url = f"{self.base_url}{self.supplier_request.request_id}/" diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py index d2ffae4..613fff9 100644 --- a/blocktrack_backend/supplier_request/views.py +++ b/blocktrack_backend/supplier_request/views.py @@ -21,16 +21,16 @@ def post(self, request): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @swagger_auto_schema( - manual_parameters=[ - openapi.Parameter( - 'request_id', openapi.IN_QUERY, - description="Optional request_id to filter", - type=openapi.TYPE_STRING - ) - ], - responses={200: SupplierRequestSerializer(many=True)} - ) + # @swagger_auto_schema( + # manual_parameters=[ + # openapi.Parameter( + # 'request_id', openapi.IN_QUERY, + # description="Optional request_id to filter", + # type=openapi.TYPE_STRING + # ) + # ], + # responses={200: SupplierRequestSerializer(many=True)} + # ) def get(self, request): requests = SupplierRequest.objects.all() serializer = SupplierRequestSerializer(requests, many=True) From 762c3e9be1914da915cce22d457dd10784c46bc4 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 23:04:18 +0530 Subject: [PATCH 16/30] Added new view to get supplier requests by supplier_id --- blocktrack_backend/supplier_request/urls.py | 6 ++++-- blocktrack_backend/supplier_request/views.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/blocktrack_backend/supplier_request/urls.py b/blocktrack_backend/supplier_request/urls.py index b1fa4dd..460cad7 100644 --- a/blocktrack_backend/supplier_request/urls.py +++ b/blocktrack_backend/supplier_request/urls.py @@ -3,12 +3,14 @@ SupplierRequestListCreate, SupplierRequestByWarehouse, SupplierRequestStatusUpdate, - SupplierRequestGetOrPartialUpdate + SupplierRequestGetOrPartialUpdate, + SupplierRequestBySupplier ) urlpatterns = [ path('supplier-request/', SupplierRequestListCreate.as_view()), path('supplier-request/warehouse//', SupplierRequestByWarehouse.as_view()), path('supplier-request//status/', SupplierRequestStatusUpdate.as_view()), - path('supplier-request//', SupplierRequestGetOrPartialUpdate.as_view()), + path('supplier-request/request//', SupplierRequestGetOrPartialUpdate.as_view()), + path('supplier-request/supplier//', SupplierRequestBySupplier.as_view()) ] diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py index 613fff9..09d3697 100644 --- a/blocktrack_backend/supplier_request/views.py +++ b/blocktrack_backend/supplier_request/views.py @@ -37,6 +37,12 @@ def get(self, request): return Response(serializer.data) +class SupplierRequestBySupplier(APIView): + def get(self, request, supplier_id): + requests = SupplierRequest.objects.filter(supplier_id=supplier_id) + serializer = SupplierRequestSerializer(requests, many=True) + return Response(serializer.data) + class SupplierRequestByWarehouse(APIView): def get(self, request, warehouse_id): requests = SupplierRequest.objects.filter(warehouse_id=warehouse_id) From 3281cba2bce6e31f87c89cb32444edf3e3fbbed0 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Thu, 8 May 2025 23:27:25 +0530 Subject: [PATCH 17/30] Added View to update Status --- blocktrack_backend/orders/urls.py | 7 ++++--- blocktrack_backend/orders/views.py | 28 +++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/blocktrack_backend/orders/urls.py b/blocktrack_backend/orders/urls.py index c0ff6bc..59a7779 100644 --- a/blocktrack_backend/orders/urls.py +++ b/blocktrack_backend/orders/urls.py @@ -1,11 +1,12 @@ from django.urls import path -from .views import CreateOrderView, OrderByWarehouse, ReadOrderView, OrderListCreateView, OrderDetailView +from .views import CreateOrderView, OrderByWarehouse, ReadOrderView, OrderListCreateView, OrderDetailView, OrderStatusUpdateView urlpatterns = [ path("create-order/", CreateOrderView.as_view()), path('read-order//', ReadOrderView.as_view()), path('orders/', OrderListCreateView.as_view(), name='order-list-create'), - path('orders//', OrderDetailView.as_view(), name='order-detail'), - path('orders/warehouse/', OrderByWarehouse.as_view(), name='order-detail-by-warehouse') + path('orders//', OrderDetailView.as_view(), name='order-detail'), + path('orders/warehouse/', OrderByWarehouse.as_view(), name='order-detail-by-warehouse'), + path('orders//status/', OrderStatusUpdateView.as_view(), name='status-update') # path("api/legacy/order//", ReadOrderLegacyView.as_view()), ] diff --git a/blocktrack_backend/orders/views.py b/blocktrack_backend/orders/views.py index ec8613d..403d5f0 100644 --- a/blocktrack_backend/orders/views.py +++ b/blocktrack_backend/orders/views.py @@ -1,7 +1,7 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.parsers import MultiPartParser -from rest_framework import generics +from rest_framework import generics, status from .models import Order from .serializers import MinimalOrderSerializer, OrderSerializer import subprocess @@ -160,3 +160,29 @@ class OrderDetailView(generics.RetrieveUpdateAPIView): queryset = Order.objects.all() serializer_class = OrderSerializer lookup_field = 'order_id' + +class OrderStatusUpdateView(APIView): + @swagger_auto_schema( + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['status'], + properties={ + 'status': openapi.Schema( + type=openapi.TYPE_STRING, + enum=['Pending', 'Accepted', 'Shipped', 'Delivered', 'Cancelled'] + ) + } + ), + responses={200: OrderSerializer()} + ) + def patch(self, request, order_id): + try: + order = Order.objects.get(order_id=order_id) + except Order.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + serializer = OrderSerializer(order, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 139d6c6ffa7266873f0900e8e1959a704f60bb59 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Fri, 9 May 2025 00:25:38 +0530 Subject: [PATCH 18/30] Added New route to get metrics from supplier requests, and updated dummy.py script --- blocktrack_backend/dummy.py | 12 +++++----- .../supplier_request/serializers.py | 2 +- blocktrack_backend/supplier_request/urls.py | 6 +++-- blocktrack_backend/supplier_request/views.py | 22 +++++++++++++++++++ 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/blocktrack_backend/dummy.py b/blocktrack_backend/dummy.py index a7fa301..a70f90a 100644 --- a/blocktrack_backend/dummy.py +++ b/blocktrack_backend/dummy.py @@ -3,9 +3,9 @@ from django.utils import timezone import random -# SupplierRequest dummy data -statuses = ['pending', 'received'] -for i in range(10): +statuses = ['pending', 'accepted', 'received', 'returned', 'rejected'] + +for _ in range(10): SupplierRequest.objects.create( supplier_id=random.randint(1, 5), created_at=timezone.now(), @@ -15,7 +15,9 @@ status=random.choice(statuses), received_at=timezone.now() if random.choice([True, False]) else None, warehouse_id=random.randint(1, 3), - unit_price=round(random.uniform(10, 500), 2) + unit_price=round(random.uniform(10, 500), 2), + quality=random.randint(0, 10) if random.choice([True, False]) else None, + is_defective=random.choice([True, False, None]) ) # Orders and OrderDetails dummy data @@ -38,7 +40,7 @@ for j in range(3): OrderProduct.objects.create( - order_id=order, + order=order, product_id=100 + j, count=random.randint(1, 10), unit_price=round(random.uniform(10.0, 100.0), 2) diff --git a/blocktrack_backend/supplier_request/serializers.py b/blocktrack_backend/supplier_request/serializers.py index ef6d034..ad53e41 100644 --- a/blocktrack_backend/supplier_request/serializers.py +++ b/blocktrack_backend/supplier_request/serializers.py @@ -4,4 +4,4 @@ class SupplierRequestSerializer(serializers.ModelSerializer): class Meta: model = SupplierRequest - fields = '__all__' + fields = '__all__' \ No newline at end of file diff --git a/blocktrack_backend/supplier_request/urls.py b/blocktrack_backend/supplier_request/urls.py index 460cad7..200324c 100644 --- a/blocktrack_backend/supplier_request/urls.py +++ b/blocktrack_backend/supplier_request/urls.py @@ -4,7 +4,8 @@ SupplierRequestByWarehouse, SupplierRequestStatusUpdate, SupplierRequestGetOrPartialUpdate, - SupplierRequestBySupplier + SupplierRequestBySupplier, + SupplierRequestMetrics ) urlpatterns = [ @@ -12,5 +13,6 @@ path('supplier-request/warehouse//', SupplierRequestByWarehouse.as_view()), path('supplier-request//status/', SupplierRequestStatusUpdate.as_view()), path('supplier-request/request//', SupplierRequestGetOrPartialUpdate.as_view()), - path('supplier-request/supplier//', SupplierRequestBySupplier.as_view()) + path('supplier-request/supplier//', SupplierRequestBySupplier.as_view()), + path('supplier-request/metrics//', SupplierRequestMetrics.as_view()) ] diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py index 09d3697..1ec6019 100644 --- a/blocktrack_backend/supplier_request/views.py +++ b/blocktrack_backend/supplier_request/views.py @@ -94,3 +94,25 @@ def delete(self, request, request_id): return Response(status=status.HTTP_404_NOT_FOUND) req.delete() return Response(status=status.HTTP_204_NO_CONTENT) + +class SupplierRequestMetrics(APIView): + def get(self, request, supplier_id): + data = SupplierRequest.objects.filter(supplier_id=supplier_id).values( + 'is_defective', 'quality', 'count', 'unit_price', 'status', 'expected_delivery_date', 'received_at' + ) + + total = len(data) + defective_count = sum(1 for d in data if d.get('is_defective')) + returned_count = sum(1 for d in data if d.get('status') == "returned") + q_sum = sum(d.get('quality', 0) or 0 for d in data) + + metrics = { + "total_requests": total, + "defective_count" : defective_count, + "defective_rate": defective_count / total, + "return_count" : returned_count, + "returned_rate": returned_count / total, + "quality_score": q_sum / total, + "data" : data + } + return Response(metrics) \ No newline at end of file From b0a65e4030d80db1bf14b7f80f0d25e04317bd57 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Fri, 9 May 2025 02:09:21 +0530 Subject: [PATCH 19/30] Added more metric data --- blocktrack_backend/supplier_request/views.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py index 1ec6019..57b7953 100644 --- a/blocktrack_backend/supplier_request/views.py +++ b/blocktrack_backend/supplier_request/views.py @@ -104,15 +104,23 @@ def get(self, request, supplier_id): total = len(data) defective_count = sum(1 for d in data if d.get('is_defective')) returned_count = sum(1 for d in data if d.get('status') == "returned") + received_count = sum(1 for d in data if d.get('status') == "received") q_sum = sum(d.get('quality', 0) or 0 for d in data) + # Calculate on-time delivery rate + on_time_count = sum( + 1 for d in data if d.get('received_at') and d.get('expected_delivery_date') and d['received_at'] <= d['expected_delivery_date'] + ) + metrics = { "total_requests": total, - "defective_count" : defective_count, - "defective_rate": defective_count / total, - "return_count" : returned_count, - "returned_rate": returned_count / total, - "quality_score": q_sum / total, - "data" : data + "defective_count": defective_count, + "defective_rate": defective_count / total if total > 0 else 0, + "return_count": returned_count, + "returned_rate": returned_count / total if total > 0 else 0, + "quality_score": q_sum / total if total > 0 else 0, + "on_time_delivery_rate": on_time_count / total if total > 0 else 0, + "fill_rate": received_count / total if total > 0 else 0, + "data": data } return Response(metrics) \ No newline at end of file From b33296deadd1f25d805689e45300985206290bd6 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Fri, 9 May 2025 11:21:21 +0530 Subject: [PATCH 20/30] added new view to filter orders by user_id --- blocktrack_backend/orders/urls.py | 3 ++- blocktrack_backend/orders/views.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/blocktrack_backend/orders/urls.py b/blocktrack_backend/orders/urls.py index 59a7779..c21239b 100644 --- a/blocktrack_backend/orders/urls.py +++ b/blocktrack_backend/orders/urls.py @@ -1,11 +1,12 @@ from django.urls import path -from .views import CreateOrderView, OrderByWarehouse, ReadOrderView, OrderListCreateView, OrderDetailView, OrderStatusUpdateView +from .views import CreateOrderView, OrderByWarehouse, ReadOrderView, OrderListCreateView, OrderDetailView, OrderStatusUpdateView, UserOrderListView urlpatterns = [ path("create-order/", CreateOrderView.as_view()), path('read-order//', ReadOrderView.as_view()), path('orders/', OrderListCreateView.as_view(), name='order-list-create'), path('orders//', OrderDetailView.as_view(), name='order-detail'), + path('orders/vendor//', UserOrderListView.as_view(), name='order-by-user'), path('orders/warehouse/', OrderByWarehouse.as_view(), name='order-detail-by-warehouse'), path('orders//status/', OrderStatusUpdateView.as_view(), name='status-update') # path("api/legacy/order//", ReadOrderLegacyView.as_view()), diff --git a/blocktrack_backend/orders/views.py b/blocktrack_backend/orders/views.py index 403d5f0..af3ff1e 100644 --- a/blocktrack_backend/orders/views.py +++ b/blocktrack_backend/orders/views.py @@ -161,6 +161,15 @@ class OrderDetailView(generics.RetrieveUpdateAPIView): serializer_class = OrderSerializer lookup_field = 'order_id' + +class UserOrderListView(generics.ListAPIView): + serializer_class = OrderSerializer + + def get_queryset(self): + user_id = self.kwargs['user_id'] + return Order.objects.filter(user_id=user_id) + + class OrderStatusUpdateView(APIView): @swagger_auto_schema( request_body=openapi.Schema( From 97b4fb06bb678e109b75135cb9b359999f68ea48 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Fri, 9 May 2025 21:33:16 +0530 Subject: [PATCH 21/30] Added kafka producer to send events, updated patch request --- blocktrack_backend/orders/__init__.py | 33 +++++++++++++++++++ blocktrack_backend/orders/views.py | 47 +++++++++++++++++++++++++-- blocktrack_backend/requirements.txt | 3 +- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/blocktrack_backend/orders/__init__.py b/blocktrack_backend/orders/__init__.py index e69de29..641e1f9 100644 --- a/blocktrack_backend/orders/__init__.py +++ b/blocktrack_backend/orders/__init__.py @@ -0,0 +1,33 @@ +from kafka import KafkaProducer +from kafka.errors import KafkaError +import logging + +logger = logging.getLogger(__name__) + +try: + kafka_producer = KafkaProducer( + bootstrap_servers='localhost:9092', + value_serializer=lambda v: json.dumps(v).encode('utf-8'), + retries=5, + acks='all' # Wait for all replicas to acknowledge + ) + logger.info("Kafka producer initialized successfully") +except KafkaError as e: + kafka_producer = None + logger.error(f"Failed to initialize Kafka producer: {str(e)}") + +def send_to_kafka(topic, data): + """Helper function to send messages to Kafka with error handling""" + if not kafka_producer: + logger.warning("Kafka producer not available, skipping message") + return False + + try: + future = kafka_producer.send(topic, data) + # Wait for the message to be delivered + record_metadata = future.get(timeout=10) + logger.info(f"Message sent to {record_metadata.topic}:{record_metadata.partition}:{record_metadata.offset}") + return True + except KafkaError as e: + logger.error(f"Failed to send message to Kafka: {str(e)}") + return False \ No newline at end of file diff --git a/blocktrack_backend/orders/views.py b/blocktrack_backend/orders/views.py index af3ff1e..527bac8 100644 --- a/blocktrack_backend/orders/views.py +++ b/blocktrack_backend/orders/views.py @@ -1,3 +1,5 @@ +import json +from . import send_to_kafka from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.parsers import MultiPartParser @@ -13,6 +15,7 @@ from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi + FABRIC_BASE = "/Users/ravishan/hyperledger-fabric/fabric-samples" BIN_PATH = f"{FABRIC_BASE}/bin" TEST_NETWORK = f"{FABRIC_BASE}/test-network" @@ -179,6 +182,14 @@ class OrderStatusUpdateView(APIView): 'status': openapi.Schema( type=openapi.TYPE_STRING, enum=['Pending', 'Accepted', 'Shipped', 'Delivered', 'Cancelled'] + ), + 'warehouse_location': openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'latitude': openapi.Schema(type=openapi.TYPE_NUMBER, description="Latitude of the warehouse"), + 'longitude': openapi.Schema(type=openapi.TYPE_NUMBER, description="Longitude of the warehouse") + }, + description="Location of the warehouse" ) } ), @@ -188,9 +199,39 @@ def patch(self, request, order_id): try: order = Order.objects.get(order_id=order_id) except Order.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - - serializer = OrderSerializer(order, data=request.data, partial=True) + return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND) + + new_status = request.data.get('status') + warehouse_location = request.data.get('warehouse_location') + + new_data = {} + + if warehouse_location: + origin_longitude = warehouse_location.get('longitude') + origin_latitude = warehouse_location.get('latitude') + + # Validate status against model choices if provided + if new_status: + valid_statuses = [choice[0] for choice in Order._meta.get_field('status').choices] + + if new_status not in valid_statuses: + return Response({ + "error": "Invalid status value", + "status": new_status, + }, status=status.HTTP_400_BAD_REQUEST) + + new_data['status'] = new_status + + # Create the event and push to kafka + event = { + "order_id": order_id, + "origin": {"lat": origin_latitude, "lng": origin_longitude}, + "destination": {"lat": order.details.longitude, "lng": order.details.longitude}, + "demand": order.demand if hasattr(order, 'demand') else 10 + } + send_to_kafka('orders.created', event) + + serializer = OrderSerializer(order, data=new_data, partial=True) if serializer.is_valid(): serializer.save() return Response(serializer.data) diff --git a/blocktrack_backend/requirements.txt b/blocktrack_backend/requirements.txt index 43a2b92..79347fd 100644 --- a/blocktrack_backend/requirements.txt +++ b/blocktrack_backend/requirements.txt @@ -14,4 +14,5 @@ urllib3==2.4.0 varint==1.0.2 django-filter psycopg2-binary -python-dotenv \ No newline at end of file +python-dotenv +kafka-python \ No newline at end of file From 05800b9184e2e7e7454b9d92e000a093f0abd7f1 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Fri, 9 May 2025 22:21:40 +0530 Subject: [PATCH 22/30] Small bug fixes for kafka --- blocktrack_backend/orders/__init__.py | 1 + blocktrack_backend/orders/views.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/blocktrack_backend/orders/__init__.py b/blocktrack_backend/orders/__init__.py index 641e1f9..2d360fa 100644 --- a/blocktrack_backend/orders/__init__.py +++ b/blocktrack_backend/orders/__init__.py @@ -1,6 +1,7 @@ from kafka import KafkaProducer from kafka.errors import KafkaError import logging +import json logger = logging.getLogger(__name__) diff --git a/blocktrack_backend/orders/views.py b/blocktrack_backend/orders/views.py index 527bac8..1a5b9c3 100644 --- a/blocktrack_backend/orders/views.py +++ b/blocktrack_backend/orders/views.py @@ -227,10 +227,10 @@ def patch(self, request, order_id): "order_id": order_id, "origin": {"lat": origin_latitude, "lng": origin_longitude}, "destination": {"lat": order.details.longitude, "lng": order.details.longitude}, - "demand": order.demand if hasattr(order, 'demand') else 10 + "demand": 10 } send_to_kafka('orders.created', event) - + serializer = OrderSerializer(order, data=new_data, partial=True) if serializer.is_valid(): serializer.save() From 572f560754ae6e698e8a0426114b247037279c2f Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Sat, 10 May 2025 13:21:11 +0530 Subject: [PATCH 23/30] Added new view to fetch supplier-requests --- .../blocktrack_backend/settings.py | 2 +- blocktrack_backend/docker-compose.yaml | 6 +-- blocktrack_backend/supplier_request/urls.py | 6 ++- blocktrack_backend/supplier_request/views.py | 49 ++++++++++++++++++- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/blocktrack_backend/blocktrack_backend/settings.py b/blocktrack_backend/blocktrack_backend/settings.py index 95b7935..fc62278 100644 --- a/blocktrack_backend/blocktrack_backend/settings.py +++ b/blocktrack_backend/blocktrack_backend/settings.py @@ -30,7 +30,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG') -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["localhost", "127.0.0.1"] # Application definition diff --git a/blocktrack_backend/docker-compose.yaml b/blocktrack_backend/docker-compose.yaml index 75b2c15..8c3e225 100644 --- a/blocktrack_backend/docker-compose.yaml +++ b/blocktrack_backend/docker-compose.yaml @@ -1,5 +1,5 @@ services: - db: + blocktrack_postgres: image: postgres:15 container_name: blocktrack_postgres restart: always @@ -8,7 +8,7 @@ services: POSTGRES_PASSWORD: blockpass POSTGRES_DB: blocktrack_db ports: - - "15432:5432" + - "15433:5432" volumes: - postgres_data:/var/lib/postgresql/data @@ -19,6 +19,6 @@ services: ports: - "8080:8080" depends_on: - - db + - blocktrack_postgres volumes: postgres_data: \ No newline at end of file diff --git a/blocktrack_backend/supplier_request/urls.py b/blocktrack_backend/supplier_request/urls.py index 200324c..e57023f 100644 --- a/blocktrack_backend/supplier_request/urls.py +++ b/blocktrack_backend/supplier_request/urls.py @@ -5,7 +5,8 @@ SupplierRequestStatusUpdate, SupplierRequestGetOrPartialUpdate, SupplierRequestBySupplier, - SupplierRequestMetrics + SupplierRequestMetrics, + SupplierRequestWithNames ) urlpatterns = [ @@ -14,5 +15,6 @@ path('supplier-request//status/', SupplierRequestStatusUpdate.as_view()), path('supplier-request/request//', SupplierRequestGetOrPartialUpdate.as_view()), path('supplier-request/supplier//', SupplierRequestBySupplier.as_view()), - path('supplier-request/metrics//', SupplierRequestMetrics.as_view()) + path('supplier-request/metrics//', SupplierRequestMetrics.as_view()), + path('supplier-request/details/', SupplierRequestWithNames.as_view()) ] diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py index 57b7953..33fd5df 100644 --- a/blocktrack_backend/supplier_request/views.py +++ b/blocktrack_backend/supplier_request/views.py @@ -3,7 +3,8 @@ from rest_framework import status from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi - +import os +import requests from .models import SupplierRequest from .serializers import SupplierRequestSerializer @@ -123,4 +124,48 @@ def get(self, request, supplier_id): "fill_rate": received_count / total if total > 0 else 0, "data": data } - return Response(metrics) \ No newline at end of file + return Response(metrics) + + +class SupplierRequestWithNames(APIView): + def get(self, request): + supplier_requests = SupplierRequest.objects.all() + serializer = SupplierRequestSerializer(supplier_requests, many=True) + data = serializer.data + + # Get service URLs from environment variables + user_service_url = os.environ.get('USER_SERVICE_URL', 'http://127.0.0.1:8002') + warehouse_service_url = os.environ.get('WAREHOUSE_SERVICE_URL', 'http://127.0.0.1:8001') + print(warehouse_service_url) + # Enrich data with supplier names and product names + for item in data: + # Fetch supplier name + try: + supplier_id = item.get('supplier_id') + supplier_response = requests.get(f"{user_service_url}/api/v1/core/suppliers/{supplier_id}/info/") + if supplier_response.status_code == 200: + supplier_data = supplier_response.json() + supplier_user_data = supplier_data.get('user') + print(supplier_data) + item['supplier_name'] = supplier_user_data.get('first_name', 'Unknown') + " " + supplier_user_data.get('last_name', 'Unknown') + else: + item['supplier_name'] = 'Unknown' + except Exception as e: + item['supplier_name'] = 'Error fetching supplier' + print(f"Error fetching supplier info: {str(e)}") + + # Fetch product name + try: + product_id = item.get('product_id') + product_response = requests.get(f"{warehouse_service_url}/api/product/products/{product_id}/") + + if product_response.status_code == 200: + product_data = product_response.json() + item['product_name'] = product_data.get('product_name', 'Unknown') + else: + item['product_name'] = 'Unknown' + except Exception as e: + item['product_name'] = 'Error fetching product' + print(f"Error fetching product info: {str(e)}") + + return Response(data) \ No newline at end of file From 10990ae303e2720243e2618f747565530bb5a05e Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Sat, 10 May 2025 13:47:49 +0530 Subject: [PATCH 24/30] Updated endpoint mapping, and added status filter for requesting suppler-request details --- blocktrack_backend/supplier_request/urls.py | 4 ++-- blocktrack_backend/supplier_request/views.py | 23 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/blocktrack_backend/supplier_request/urls.py b/blocktrack_backend/supplier_request/urls.py index e57023f..af0f7fd 100644 --- a/blocktrack_backend/supplier_request/urls.py +++ b/blocktrack_backend/supplier_request/urls.py @@ -11,10 +11,10 @@ urlpatterns = [ path('supplier-request/', SupplierRequestListCreate.as_view()), - path('supplier-request/warehouse//', SupplierRequestByWarehouse.as_view()), + # path('supplier-request/warehouse//', SupplierRequestByWarehouse.as_view()), path('supplier-request//status/', SupplierRequestStatusUpdate.as_view()), path('supplier-request/request//', SupplierRequestGetOrPartialUpdate.as_view()), path('supplier-request/supplier//', SupplierRequestBySupplier.as_view()), path('supplier-request/metrics//', SupplierRequestMetrics.as_view()), - path('supplier-request/details/', SupplierRequestWithNames.as_view()) + path('supplier-request/warehouse//', SupplierRequestWithNames.as_view()) ] diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py index 33fd5df..57e0e72 100644 --- a/blocktrack_backend/supplier_request/views.py +++ b/blocktrack_backend/supplier_request/views.py @@ -128,8 +128,24 @@ def get(self, request, supplier_id): class SupplierRequestWithNames(APIView): - def get(self, request): - supplier_requests = SupplierRequest.objects.all() + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + 'status', openapi.IN_QUERY, + description="Optional status to filter supplier requests", + type=openapi.TYPE_STRING + ) + ], + responses={200: SupplierRequestSerializer(many=True)} + ) + def get(self, request, warehouse_id): + status = request.query_params.get('status', None) + + if status: + supplier_requests = SupplierRequest.objects.filter(warehouse_id=warehouse_id, status=status) + else: + supplier_requests = SupplierRequest.objects.filter(warehouse_id=warehouse_id) + serializer = SupplierRequestSerializer(supplier_requests, many=True) data = serializer.data @@ -137,9 +153,12 @@ def get(self, request): user_service_url = os.environ.get('USER_SERVICE_URL', 'http://127.0.0.1:8002') warehouse_service_url = os.environ.get('WAREHOUSE_SERVICE_URL', 'http://127.0.0.1:8001') print(warehouse_service_url) + + # Enrich data with supplier names and product names for item in data: # Fetch supplier name + try: supplier_id = item.get('supplier_id') supplier_response = requests.get(f"{user_service_url}/api/v1/core/suppliers/{supplier_id}/info/") From 12c739d25db8d6696a561d3b030c042a744151fc Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Sat, 10 May 2025 14:15:20 +0530 Subject: [PATCH 25/30] Refined the status filter --- blocktrack_backend/supplier_request/views.py | 31 +++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py index 57e0e72..50f5190 100644 --- a/blocktrack_backend/supplier_request/views.py +++ b/blocktrack_backend/supplier_request/views.py @@ -132,18 +132,35 @@ class SupplierRequestWithNames(APIView): manual_parameters=[ openapi.Parameter( 'status', openapi.IN_QUERY, - description="Optional status to filter supplier requests", + description="Optional status to filter supplier requests. Can be a single status or a list of statuses.", type=openapi.TYPE_STRING ) ], - responses={200: SupplierRequestSerializer(many=True)} + responses={ + 200: openapi.Response( + description="List of supplier requests with enriched data", + schema=SupplierRequestSerializer(many=True) + ), + 400: "Bad Request", + 500: "Internal Server Error" + } ) def get(self, request, warehouse_id): - status = request.query_params.get('status', None) - - if status: - supplier_requests = SupplierRequest.objects.filter(warehouse_id=warehouse_id, status=status) - else: + status_param = request.query_params.get('status', None) + + try: + # Base query filtering by warehouse + supplier_requests = SupplierRequest.objects.filter(warehouse_id=warehouse_id) + + # Apply status filtering if provided + if status_param: + # Split comma-separated statuses into a list + status_list = [s.strip() for s in status_param.split(',')] + supplier_requests = supplier_requests.filter(status__in=status_list) + + except Exception as e: + print(f"Error filtering requests: {e}") + # Fallback to all requests for this warehouse supplier_requests = SupplierRequest.objects.filter(warehouse_id=warehouse_id) serializer = SupplierRequestSerializer(supplier_requests, many=True) From fd417147f19b718b156a8258ff1a27bd1d540c37 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Sat, 10 May 2025 15:51:14 +0530 Subject: [PATCH 26/30] Blockchain changes to make it functional --- blocktrack_backend/orders/views.py | 84 +- test-network/core.yaml | 801 +++++++++++++++++++ test-network/scripts/invoke_the_chaincode.sh | 62 ++ test-network/scripts/setup_the_chaincode.sh | 80 ++ 4 files changed, 990 insertions(+), 37 deletions(-) create mode 100644 test-network/core.yaml create mode 100755 test-network/scripts/invoke_the_chaincode.sh create mode 100755 test-network/scripts/setup_the_chaincode.sh diff --git a/blocktrack_backend/orders/views.py b/blocktrack_backend/orders/views.py index 1a5b9c3..2958a33 100644 --- a/blocktrack_backend/orders/views.py +++ b/blocktrack_backend/orders/views.py @@ -1,4 +1,5 @@ import json +from pathlib import Path from . import send_to_kafka from rest_framework.views import APIView from rest_framework.response import Response @@ -16,10 +17,15 @@ from drf_yasg import openapi -FABRIC_BASE = "/Users/ravishan/hyperledger-fabric/fabric-samples" -BIN_PATH = f"{FABRIC_BASE}/bin" -TEST_NETWORK = f"{FABRIC_BASE}/test-network" +# πŸ”§ Dynamically determine project base +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent # β†’ blocktrack_backend/ +FABRIC_BASE = PROJECT_ROOT / "test-network" / ".." +FABRIC_BASE = FABRIC_BASE.resolve() +BIN_PATH = FABRIC_BASE / "bin" +CONFIG_PATH = FABRIC_BASE / "config" +TEST_NETWORK = FABRIC_BASE / "test-network" +SCRIPT_PATH = FABRIC_BASE / "test-network" / "scripts" / "invoke_order.sh" def get_fabric_env(): env = os.environ.copy() env["PATH"] = f"{BIN_PATH}:" + env["PATH"] @@ -31,30 +37,24 @@ def get_fabric_env(): env["CORE_PEER_ADDRESS"] = "localhost:7051" return env + + class CreateOrderView(APIView): parser_classes = [MultiPartParser] def post(self, request): print("πŸ“₯ Received POST request") - - fabric_env = os.environ.copy() - fabric_env["PATH"] = "/Users/ravishan/hyperledger-fabric/fabric-samples/bin:" + fabric_env["PATH"] - fabric_env["FABRIC_CFG_PATH"] = "/Users/ravishan/hyperledger-fabric/fabric-samples/config" - fabric_env["CORE_PEER_LOCALMSPID"] = "Org1MSP" - fabric_env["CORE_PEER_TLS_ENABLED"] = "true" - fabric_env["CORE_PEER_TLS_ROOTCERT_FILE"] = "/Users/ravishan/hyperledger-fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" - fabric_env["CORE_PEER_MSPCONFIGPATH"] = "/Users/ravishan/hyperledger-fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" - fabric_env["CORE_PEER_ADDRESS"] = "localhost:7051" + fabric_env = get_fabric_env() order_id = request.data.get("order_id") status = request.data.get("status") timestamp = request.data.get("timestamp") file = request.FILES.get("document") - print("πŸ“ order_id:", order_id) - print("πŸ“ status:", status) - print("πŸ“ timestamp:", timestamp) - print("πŸ“Ž file:", file.name if file else "No file") + print(f"πŸ“ order_id: {order_id}") + print(f"πŸ“ status: {status}") + print(f"πŸ“ timestamp: {timestamp}") + print(f"πŸ“Ž file: {file.name if file else 'No file'}") try: # Save file temporarily @@ -68,26 +68,33 @@ def post(self, request): cid = upload_to_ipfs(tmp_path) print("🧬 IPFS CID:", cid) - # Prepare command + # Prepare chaincode invoke args import json args_json = json.dumps({ "function": "CreateOrder", "Args": [order_id, status, timestamp, cid] }) - script_path = "/Users/ravishan/hyperledger-fabric/fabric-samples/scripts/invoke_order.sh" - command = f"{script_path} '{args_json}'" - print("πŸš€ FULL PEER INVOKE COMMAND:\n", command) + print("πŸ”§ Executing:", SCRIPT_PATH, args_json) - result = subprocess.run(command, shell=True, capture_output=True, text=True, env=fabric_env) + result = subprocess.run( + [str(SCRIPT_PATH), args_json], + capture_output=True, + text=True, + env=fabric_env + ) - print("βœ… Blockchain STDOUT:\n", result.stdout) - print("❌ Blockchain STDERR:\n", result.stderr) + print("βœ… STDOUT:", result.stdout) + print("❌ STDERR:", result.stderr) if result.returncode != 0: - raise Exception("Chaincode invoke failed") + raise Exception(f"Invoke script failed:\n{result.stderr}") - return Response({"message": "Order created", "cid": cid, "blockchain_response": result.stdout}) + return Response({ + "message": "Order created", + "cid": cid, + "blockchain_response": result.stdout.strip() + }) except Exception as e: return Response({ @@ -100,25 +107,27 @@ class ReadOrderView(APIView): def get(self, request, order_id): fabric_env = get_fabric_env() - command = f""" -peer chaincode query \ ---tls \ ---cafile {TEST_NETWORK}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ ---peerAddresses localhost:7051 \ ---tlsRootCertFiles {TEST_NETWORK}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ --C mychannel -n ordercc \ --c '{{"Args":["ReadOrder", "{order_id}"]}}' -""" - print("FULL COMMAND:\n", command) + # Safely formatted single-line command + command = [ + "peer", "chaincode", "query", + "--tls", + "--cafile", str(TEST_NETWORK / "organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"), + "--peerAddresses", "localhost:7051", + "--tlsRootCertFiles", str(TEST_NETWORK / "organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt"), + "-C", "mychannel", + "-n", "ordercc", + "-c", f'{{"Args":["ReadOrder", "{order_id}"]}}' + ] + print("πŸ” Executing ReadOrder command for:", order_id) try: - result = subprocess.run(command, shell=True, capture_output=True, text=True, env=fabric_env) + result = subprocess.run(command, capture_output=True, text=True, env=fabric_env) if result.returncode != 0: return Response({ "error": f"Failed to read order '{order_id}' from blockchain", - "details": result.stderr + "details": result.stderr.strip() }, status=404) return Response({ @@ -133,6 +142,7 @@ def get(self, request, order_id): }, status=500) + class OrderListCreateView(generics.ListCreateAPIView): queryset = Order.objects.all() serializer_class = OrderSerializer diff --git a/test-network/core.yaml b/test-network/core.yaml new file mode 100644 index 0000000..723817e --- /dev/null +++ b/test-network/core.yaml @@ -0,0 +1,801 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################### +# +# Peer section +# +############################################################################### +peer: + + # The peer id provides a name for this peer instance and is used when + # naming docker resources. + id: jdoe + + # The networkId allows for logical separation of networks and is used when + # naming docker resources. + networkId: dev + + # The Address at local network interface this Peer will listen on. + # By default, it will listen on all network interfaces + listenAddress: 0.0.0.0:7051 + + # The endpoint this peer uses to listen for inbound chaincode connections. + # If this is commented-out, the listen address is selected to be + # the peer's address (see below) with port 7052 + # chaincodeListenAddress: 0.0.0.0:7052 + + # The endpoint the chaincode for this peer uses to connect to the peer. + # If this is not specified, the chaincodeListenAddress address is selected. + # And if chaincodeListenAddress is not specified, address is selected from + # peer address (see below). If specified peer address is invalid then it + # will fallback to the auto detected IP (local IP) regardless of the peer + # addressAutoDetect value. + # chaincodeAddress: 0.0.0.0:7052 + + # When used as peer config, this represents the endpoint to other peers + # in the same organization. For peers in other organization, see + # gossip.externalEndpoint for more info. + # When used as CLI config, this means the peer's endpoint to interact with + address: 0.0.0.0:7051 + + # Whether the Peer should programmatically determine its address + # This case is useful for docker containers. + # When set to true, will override peer address. + addressAutoDetect: false + + # Settings for the Peer's gateway server. + gateway: + # Whether the gateway is enabled for this Peer. + enabled: true + # endorsementTimeout is the duration the gateway waits for a response + # from other endorsing peers before returning a timeout error to the client. + endorsementTimeout: 30s + # broadcastTimeout is the duration the gateway waits for a response + # from ordering nodes before returning a timeout error to the client. + broadcastTimeout: 30s + # dialTimeout is the duration the gateway waits for a connection + # to other network nodes. + dialTimeout: 2m + + + # Keepalive settings for peer server and clients + keepalive: + # Interval is the duration after which if the server does not see + # any activity from the client it pings the client to see if it's alive + interval: 7200s + # Timeout is the duration the server waits for a response + # from the client after sending a ping before closing the connection + timeout: 20s + # MinInterval is the minimum permitted time between client pings. + # If clients send pings more frequently, the peer server will + # disconnect them + minInterval: 60s + # Client keepalive settings for communicating with other peer nodes + client: + # Interval is the time between pings to peer nodes. This must + # greater than or equal to the minInterval specified by peer + # nodes + interval: 60s + # Timeout is the duration the client waits for a response from + # peer nodes before closing the connection + timeout: 20s + # DeliveryClient keepalive settings for communication with ordering + # nodes. + deliveryClient: + # Interval is the time between pings to ordering nodes. This must + # greater than or equal to the minInterval specified by ordering + # nodes. + interval: 60s + # Timeout is the duration the client waits for a response from + # ordering nodes before closing the connection + timeout: 20s + + + # Gossip related configuration + gossip: + # Bootstrap set to initialize gossip with. + # This is a list of other peers that this peer reaches out to at startup. + # Important: The endpoints here have to be endpoints of peers in the same + # organization, because the peer would refuse connecting to these endpoints + # unless they are in the same organization as the peer. + bootstrap: 127.0.0.1:7051 + + # NOTE: orgLeader and useLeaderElection parameters are mutual exclusive. + # Setting both to true would result in the termination of the peer + # since this is undefined state. If the peers are configured with + # useLeaderElection=false, make sure there is at least 1 peer in the + # organization that its orgLeader is set to true. + + # Defines whenever peer will initialize dynamic algorithm for + # "leader" selection, where leader is the peer to establish + # connection with ordering service and use delivery protocol + # to pull ledger blocks from ordering service. + useLeaderElection: false + # Statically defines peer to be an organization "leader", + # where this means that current peer will maintain connection + # with ordering service and disseminate block across peers in + # its own organization. Multiple peers or all peers in an organization + # may be configured as org leaders, so that they all pull + # blocks directly from ordering service. + orgLeader: true + + # Interval for membershipTracker polling + membershipTrackerInterval: 5s + + # Overrides the endpoint that the peer publishes to peers + # in its organization. For peers in foreign organizations + # see 'externalEndpoint' + endpoint: + # Maximum count of blocks stored in memory + maxBlockCountToStore: 10 + # Max time between consecutive message pushes(unit: millisecond) + maxPropagationBurstLatency: 10ms + # Max number of messages stored until a push is triggered to remote peers + maxPropagationBurstSize: 10 + # Number of times a message is pushed to remote peers + propagateIterations: 1 + # Number of peers selected to push messages to + propagatePeerNum: 3 + # Determines frequency of pull phases(unit: second) + # Must be greater than digestWaitTime + responseWaitTime + pullInterval: 4s + # Number of peers to pull from + pullPeerNum: 3 + # Determines frequency of pulling state info messages from peers(unit: second) + requestStateInfoInterval: 4s + # Determines frequency of pushing state info messages to peers(unit: second) + publishStateInfoInterval: 4s + # Maximum time a stateInfo message is kept until expired + stateInfoRetentionInterval: + # Time from startup certificates are included in Alive messages(unit: second) + publishCertPeriod: 10s + # Should we skip verifying block messages or not (currently not in use) + skipBlockVerification: false + # Dial timeout(unit: second) + dialTimeout: 3s + # Connection timeout(unit: second) + connTimeout: 2s + # Buffer size of received messages + recvBuffSize: 20 + # Buffer size of sending messages + sendBuffSize: 200 + # Time to wait before pull engine processes incoming digests (unit: second) + # Should be slightly smaller than requestWaitTime + digestWaitTime: 1s + # Time to wait before pull engine removes incoming nonce (unit: milliseconds) + # Should be slightly bigger than digestWaitTime + requestWaitTime: 1500ms + # Time to wait before pull engine ends pull (unit: second) + responseWaitTime: 2s + # Alive check interval(unit: second) + aliveTimeInterval: 5s + # Alive expiration timeout(unit: second) + aliveExpirationTimeout: 25s + # Reconnect interval(unit: second) + reconnectInterval: 25s + # Max number of attempts to connect to a peer + maxConnectionAttempts: 120 + # Message expiration factor for alive messages + msgExpirationFactor: 20 + # This is an endpoint that is published to peers outside of the organization. + # If this isn't set, the peer will not be known to other organizations and will not be exposed via service discovery. + externalEndpoint: + # Leader election service configuration + election: + # Longest time peer waits for stable membership during leader election startup (unit: second) + startupGracePeriod: 15s + # Interval gossip membership samples to check its stability (unit: second) + membershipSampleInterval: 1s + # Time passes since last declaration message before peer decides to perform leader election (unit: second) + leaderAliveThreshold: 10s + # Time between peer sends propose message and declares itself as a leader (sends declaration message) (unit: second) + leaderElectionDuration: 5s + + pvtData: + # pullRetryThreshold determines the maximum duration of time private data corresponding for a given block + # would be attempted to be pulled from peers until the block would be committed without the private data + pullRetryThreshold: 60s + # As private data enters the transient store, it is associated with the peer's ledger's height at that time. + # transientstoreMaxBlockRetention defines the maximum difference between the current ledger's height upon commit, + # and the private data residing inside the transient store that is guaranteed not to be purged. + # Private data is purged from the transient store when blocks with sequences that are multiples + # of transientstoreMaxBlockRetention are committed. + transientstoreMaxBlockRetention: 1000 + # pushAckTimeout is the maximum time to wait for an acknowledgement from each peer + # at private data push at endorsement time. + pushAckTimeout: 3s + # Block to live pulling margin, used as a buffer + # to prevent peer from trying to pull private data + # from peers that is soon to be purged in next N blocks. + # This helps a newly joined peer catch up to current + # blockchain height quicker. + btlPullMargin: 10 + # the process of reconciliation is done in an endless loop, while in each iteration reconciler tries to + # pull from the other peers the most recent missing blocks with a maximum batch size limitation. + # reconcileBatchSize determines the maximum batch size of missing private data that will be reconciled in a + # single iteration. + reconcileBatchSize: 10 + # reconcileSleepInterval determines the time reconciler sleeps from end of an iteration until the beginning + # of the next reconciliation iteration. + reconcileSleepInterval: 1m + # reconciliationEnabled is a flag that indicates whether private data reconciliation is enable or not. + reconciliationEnabled: true + # skipPullingInvalidTransactionsDuringCommit is a flag that indicates whether pulling of invalid + # transaction's private data from other peers need to be skipped during the commit time and pulled + # only through reconciler. + skipPullingInvalidTransactionsDuringCommit: false + # implicitCollectionDisseminationPolicy specifies the dissemination policy for the peer's own implicit collection. + # When a peer endorses a proposal that writes to its own implicit collection, below values override the default values + # for disseminating private data. + # Note that it is applicable to all channels the peer has joined. The implication is that requiredPeerCount has to + # be smaller than the number of peers in a channel that has the lowest numbers of peers from the organization. + implicitCollectionDisseminationPolicy: + # requiredPeerCount defines the minimum number of eligible peers to which the peer must successfully + # disseminate private data for its own implicit collection during endorsement. Default value is 0. + requiredPeerCount: 0 + # maxPeerCount defines the maximum number of eligible peers to which the peer will attempt to + # disseminate private data for its own implicit collection during endorsement. Default value is 1. + maxPeerCount: 1 + + # Gossip state transfer related configuration + state: + # indicates whenever state transfer is enabled or not + # default value is false, i.e. state transfer is active + # and takes care to sync up missing blocks allowing + # lagging peer to catch up to speed with rest network. + # Keep in mind that when peer.gossip.useLeaderElection is true + # and there are several peers in the organization, + # or peer.gossip.useLeaderElection is false alongside with + # peer.gossip.orgleader being false, the peer's ledger may lag behind + # the rest of the peers and will never catch up due to state transfer + # being disabled. + enabled: false + # checkInterval interval to check whether peer is lagging behind enough to + # request blocks via state transfer from another peer. + checkInterval: 10s + # responseTimeout amount of time to wait for state transfer response from + # other peers + responseTimeout: 3s + # batchSize the number of blocks to request via state transfer from another peer + batchSize: 10 + # blockBufferSize reflects the size of the re-ordering buffer + # which captures blocks and takes care to deliver them in order + # down to the ledger layer. The actual buffer size is bounded between + # 0 and 2*blockBufferSize, each channel maintains its own buffer + blockBufferSize: 20 + # maxRetries maximum number of re-tries to ask + # for single state transfer request + maxRetries: 3 + + # TLS Settings + tls: + # Require server-side TLS + enabled: false + # Require client certificates / mutual TLS for inbound connections. + # Note that clients that are not configured to use a certificate will + # fail to connect to the peer. + clientAuthRequired: false + # X.509 certificate used for TLS server + cert: + file: tls/server.crt + # Private key used for TLS server + key: + file: tls/server.key + # rootcert.file represents the trusted root certificate chain used for verifying certificates + # of other nodes during outbound connections. + # It is not required to be set, but can be used to augment the set of TLS CA certificates + # available from the MSPs of each channel’s configuration. + rootcert: + file: tls/ca.crt + # If mutual TLS is enabled, clientRootCAs.files contains a list of additional root certificates + # used for verifying certificates of client connections. + # It augments the set of TLS CA certificates available from the MSPs of each channel’s configuration. + # Minimally, set your organization's TLS CA root certificate so that the peer can receive join channel requests. + clientRootCAs: + files: + - tls/ca.crt + # Private key used for TLS when making client connections. + # If not set, peer.tls.key.file will be used instead + clientKey: + file: + # X.509 certificate used for TLS when making client connections. + # If not set, peer.tls.cert.file will be used instead + clientCert: + file: + + # Authentication contains configuration parameters related to authenticating + # client messages + authentication: + # the acceptable difference between the current server time and the + # client's time as specified in a client request message. + # timewindow is checked on requests to the delivery service only. + timewindow: 15m + + # Path on the file system where peer will store data (eg ledger). This + # location must be access control protected to prevent unintended + # modification that might corrupt the peer operations. + # The path may be relative to FABRIC_CFG_PATH or an absolute path. + fileSystemPath: /var/hyperledger/production + + # BCCSP (Blockchain crypto provider): Select which crypto implementation or + # library to use + BCCSP: + Default: SW + # Settings for the SW crypto provider (i.e. when DEFAULT: SW) + SW: + # TODO: The default Hash and Security level needs refactoring to be + # fully configurable. Changing these defaults requires coordination + # SHA2 is hardcoded in several places, not only BCCSP + Hash: SHA2 + Security: 256 + # Location of Key Store + FileKeyStore: + # If "", defaults to 'mspConfigPath'/keystore + KeyStore: + # Settings for the PKCS#11 crypto provider (i.e. when DEFAULT: PKCS11) + PKCS11: + # Location of the PKCS11 module library + Library: + # Token Label + Label: + # User PIN + Pin: + Hash: + Security: + SoftwareVerify: + Immutable: + AltID: + KeyIds: + + # Path on the file system where peer will find MSP local configurations + # The path may be relative to FABRIC_CFG_PATH or an absolute path. + mspConfigPath: msp + + # Identifier of the local MSP + # ----!!!!IMPORTANT!!!-!!!IMPORTANT!!!-!!!IMPORTANT!!!!---- + # Deployers need to change the value of the localMspId string. + # In particular, the name of the local MSP ID of a peer needs + # to match the name of one of the MSPs in each of the channel + # that this peer is a member of. Otherwise this peer's messages + # will not be identified as valid by other nodes. + localMspId: SampleOrg + + # CLI common client config options + client: + # connection timeout + connTimeout: 3s + + # Delivery service related config + deliveryclient: + # Enables this peer to disseminate blocks it pulled from the ordering service + # via gossip. + # Note that 'gossip.state.enabled' controls point to point block replication + # of blocks committed in the past. + blockGossipEnabled: true + # It sets the total time the delivery service may spend in reconnection + # attempts until its retry logic gives up and returns an error, + # ignored if peer is a static leader + reconnectTotalTimeThreshold: 3600s + + # It sets the delivery service <-> ordering service node connection timeout + connTimeout: 3s + + # It sets the delivery service maximal delay between consecutive retries. + # Time between retries will have exponential backoff until hitting this threshold. + reConnectBackoffThreshold: 3600s + + # A list of orderer endpoint addresses which should be overridden + # when found in channel configurations. + addressOverrides: + # - from: + # to: + # caCertsFile: + # - from: + # to: + # caCertsFile: + + # Type for the local MSP - by default it's of type bccsp + localMspType: bccsp + + # Used with Go profiling tools only in none production environment. In + # production, it should be disabled (eg enabled: false) + profile: + enabled: false + listenAddress: 0.0.0.0:6060 + + # Handlers defines custom handlers that can filter and mutate + # objects passing within the peer, such as: + # Auth filter - reject or forward proposals from clients + # Decorators - append or mutate the chaincode input passed to the chaincode + # Endorsers - Custom signing over proposal response payload and its mutation + # Valid handler definition contains: + # - A name which is a factory method name defined in + # core/handlers/library/library.go for statically compiled handlers + # - library path to shared object binary for pluggable filters + # Auth filters and decorators are chained and executed in the order that + # they are defined. For example: + # authFilters: + # - + # name: FilterOne + # library: /opt/lib/filter.so + # - + # name: FilterTwo + # decorators: + # - + # name: DecoratorOne + # - + # name: DecoratorTwo + # library: /opt/lib/decorator.so + # Endorsers are configured as a map that its keys are the endorsement system chaincodes that are being overridden. + # Below is an example that overrides the default ESCC and uses an endorsement plugin that has the same functionality + # as the default ESCC. + # If the 'library' property is missing, the name is used as the constructor method in the builtin library similar + # to auth filters and decorators. + # endorsers: + # escc: + # name: DefaultESCC + # library: /etc/hyperledger/fabric/plugin/escc.so + handlers: + authFilters: + - + name: DefaultAuth + - + name: ExpirationCheck # This filter checks identity x509 certificate expiration + decorators: + - + name: DefaultDecorator + endorsers: + escc: + name: DefaultEndorsement + library: + validators: + vscc: + name: DefaultValidation + library: + + # library: /etc/hyperledger/fabric/plugin/escc.so + # Number of goroutines that will execute transaction validation in parallel. + # By default, the peer chooses the number of CPUs on the machine. Set this + # variable to override that choice. + # NOTE: overriding this value might negatively influence the performance of + # the peer so please change this value only if you know what you're doing + validatorPoolSize: + + # The discovery service is used by clients to query information about peers, + # such as - which peers have joined a certain channel, what is the latest + # channel config, and most importantly - given a chaincode and a channel, + # what possible sets of peers satisfy the endorsement policy. + discovery: + enabled: true + # Whether the authentication cache is enabled or not. + authCacheEnabled: true + # The maximum size of the cache, after which a purge takes place + authCacheMaxSize: 1000 + # The proportion (0 to 1) of entries that remain in the cache after the cache is purged due to overpopulation + authCachePurgeRetentionRatio: 0.75 + # Whether to allow non-admins to perform non channel scoped queries. + # When this is false, it means that only peer admins can perform non channel scoped queries. + orgMembersAllowedAccess: false + + # Limits is used to configure some internal resource limits. + limits: + # Concurrency limits the number of concurrently running requests to a service on each peer. + # Currently this option is only applied to endorser service and deliver service. + # When the property is missing or the value is 0, the concurrency limit is disabled for the service. + concurrency: + # endorserService limits concurrent requests to endorser service that handles chaincode deployment, query and invocation, + # including both user chaincodes and system chaincodes. + endorserService: 2500 + # deliverService limits concurrent event listeners registered to deliver service for blocks and transaction events. + deliverService: 2500 + # gatewayService limits concurrent requests to gateway service that handles the submission and evaluation of transactions. + gatewayService: 500 + + # Since all nodes should be consistent it is recommended to keep + # the default value of 100MB for MaxRecvMsgSize & MaxSendMsgSize + # Max message size in bytes GRPC server and client can receive + maxRecvMsgSize: 104857600 + # Max message size in bytes GRPC server and client can send + maxSendMsgSize: 104857600 + +############################################################################### +# +# VM section +# +############################################################################### +vm: + + # Endpoint of the vm management system. For docker can be one of the following in general + # unix:///var/run/docker.sock + # http://localhost:2375 + # https://localhost:2376 + # If you utilize external chaincode builders and don't need the default Docker chaincode builder, + # the endpoint should be unconfigured so that the peer's Docker health checker doesn't get registered. + endpoint: unix:///var/run/docker.sock + + # settings for docker vms + docker: + tls: + enabled: false + ca: + file: docker/ca.crt + cert: + file: docker/tls.crt + key: + file: docker/tls.key + + # Enables/disables the standard out/err from chaincode containers for + # debugging purposes + attachStdout: false + + # Parameters on creating docker container. + # Container may be efficiently created using ipam & dns-server for cluster + # NetworkMode - sets the networking mode for the container. Supported + # standard values are: `host`(default),`bridge`,`ipvlan`,`none`. + # Dns - a list of DNS servers for the container to use. + # Note: `Privileged` `Binds` `Links` and `PortBindings` properties of + # Docker Host Config are not supported and will not be used if set. + # LogConfig - sets the logging driver (Type) and related options + # (Config) for Docker. For more info, + # https://docs.docker.com/engine/admin/logging/overview/ + # Note: Set LogConfig using Environment Variables is not supported. + hostConfig: + NetworkMode: host + Dns: + # - 192.168.0.1 + LogConfig: + Type: json-file + Config: + max-size: "50m" + max-file: "5" + Memory: 2147483648 + +############################################################################### +# +# Chaincode section +# +############################################################################### +chaincode: + + # The id is used by the Chaincode stub to register the executing Chaincode + # ID with the Peer and is generally supplied through ENV variables + # the `path` form of ID is provided when installing the chaincode. + # The `name` is used for all other requests and can be any string. + id: + path: + name: + + # Generic builder environment, suitable for most chaincode types + builder: $(DOCKER_NS)/fabric-ccenv:$(TWO_DIGIT_VERSION) + + # Enables/disables force pulling of the base docker images (listed below) + # during user chaincode instantiation. + # Useful when using moving image tags (such as :latest) + pull: false + + golang: + # golang will never need more than baseos + runtime: $(DOCKER_NS)/fabric-baseos:$(TWO_DIGIT_VERSION) + + # whether or not golang chaincode should be linked dynamically + dynamicLink: false + + java: + # This is an image based on java:openjdk-8 with addition compiler + # tools added for java shim layer packaging. + # This image is packed with shim layer libraries that are necessary + # for Java chaincode runtime. + runtime: $(DOCKER_NS)/fabric-javaenv:$(TWO_DIGIT_VERSION) + + node: + # This is an image based on node:$(NODE_VER)-alpine + runtime: $(DOCKER_NS)/fabric-nodeenv:$(TWO_DIGIT_VERSION) + + # List of directories to treat as external builders and launchers for + # chaincode. The external builder detection processing will iterate over the + # builders in the order specified below. + # If you don't need to fallback to the default Docker builder, also unconfigure vm.endpoint above. + # To override this property via env variable use CORE_CHAINCODE_EXTERNALBUILDERS: [{name: x, path: dir1}, {name: y, path: dir2}] + # The path must be an absolute path. + externalBuilders: + - name: ccaas_builder + path: /opt/hyperledger/ccaas_builder + propagateEnvironment: + - CHAINCODE_AS_A_SERVICE_BUILDER_CONFIG + + + # The maximum duration to wait for the chaincode build and install process + # to complete. + installTimeout: 300s + + # Timeout duration for starting up a container and waiting for Register + # to come through. + startuptimeout: 300s + + # Timeout duration for Invoke and Init calls to prevent runaway. + # This timeout is used by all chaincodes in all the channels, including + # system chaincodes. + # Note that during Invoke, if the image is not available (e.g. being + # cleaned up when in development environment), the peer will automatically + # build the image, which might take more time. In production environment, + # the chaincode image is unlikely to be deleted, so the timeout could be + # reduced accordingly. + executetimeout: 30s + + # There are 2 modes: "dev" and "net". + # In dev mode, user runs the chaincode after starting peer from + # command line on local machine. + # In net mode, peer will run chaincode in a docker container. + mode: net + + # keepalive in seconds. In situations where the communication goes through a + # proxy that does not support keep-alive, this parameter will maintain connection + # between peer and chaincode. + # A value <= 0 turns keepalive off + keepalive: 0 + + # enabled system chaincodes + system: + _lifecycle: enable + cscc: enable + lscc: enable + qscc: enable + + # Logging section for the chaincode container + logging: + # Default level for all loggers within the chaincode container + level: info + # Override default level for the 'shim' logger + shim: warning + # Format for the chaincode container logs + format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}' + +############################################################################### +# +# Ledger section - ledger configuration encompasses both the blockchain +# and the state +# +############################################################################### +ledger: + + blockchain: + + state: + # stateDatabase - options are "goleveldb", "CouchDB" + # goleveldb - default state database stored in goleveldb. + # CouchDB - store state database in CouchDB + stateDatabase: goleveldb + # Limit on the number of records to return per query + totalQueryLimit: 100000 + couchDBConfig: + # It is recommended to run CouchDB on the same server as the peer, and + # not map the CouchDB container port to a server port in docker-compose. + # Otherwise proper security must be provided on the connection between + # CouchDB client (on the peer) and server. + couchDBAddress: 127.0.0.1:5984 + # This username must have read and write authority on CouchDB + username: + # The password is recommended to pass as an environment variable + # during start up (eg CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD). + # If it is stored here, the file must be access control protected + # to prevent unintended users from discovering the password. + password: + # Number of retries for CouchDB errors + maxRetries: 3 + # Number of retries for CouchDB errors during peer startup. + # The delay between retries doubles for each attempt. + # Default of 10 retries results in 11 attempts over 2 minutes. + maxRetriesOnStartup: 10 + # CouchDB request timeout (unit: duration, e.g. 20s) + requestTimeout: 35s + # Limit on the number of records per each CouchDB query + # Note that chaincode queries are only bound by totalQueryLimit. + # Internally the chaincode may execute multiple CouchDB queries, + # each of size internalQueryLimit. + internalQueryLimit: 1000 + # Limit on the number of records per CouchDB bulk update batch + maxBatchUpdateSize: 1000 + # Create the _global_changes system database + # This is optional. Creating the global changes database will require + # additional system resources to track changes and maintain the database + createGlobalChangesDB: false + # CacheSize denotes the maximum mega bytes (MB) to be allocated for the in-memory state + # cache. Note that CacheSize needs to be a multiple of 32 MB. If it is not a multiple + # of 32 MB, the peer would round the size to the next multiple of 32 MB. + # To disable the cache, 0 MB needs to be assigned to the cacheSize. + cacheSize: 64 + + history: + # enableHistoryDatabase - options are true or false + # Indicates if the history of key updates should be stored. + # All history 'index' will be stored in goleveldb, regardless if using + # CouchDB or alternate database for the state. + enableHistoryDatabase: true + + pvtdataStore: + # the maximum db batch size for converting + # the ineligible missing data entries to eligible missing data entries + collElgProcMaxDbBatchSize: 5000 + # the minimum duration (in milliseconds) between writing + # two consecutive db batches for converting the ineligible missing data entries to eligible missing data entries + collElgProcDbBatchesInterval: 1000 + # The missing data entries are classified into two categories: + # (1) prioritized + # (2) deprioritized + # Initially, all missing data are in the prioritized list. When the + # reconciler is unable to fetch the missing data from other peers, + # the unreconciled missing data would be moved to the deprioritized list. + # The reconciler would retry deprioritized missing data after every + # deprioritizedDataReconcilerInterval (unit: minutes). Note that the + # interval needs to be greater than the reconcileSleepInterval + deprioritizedDataReconcilerInterval: 60m + # The frequency to purge private data (in number of blocks). + # Private data is purged from the peer's private data store based on + # the collection property blockToLive or an explicit chaincode call to PurgePrivateData(). + purgeInterval: 100 + # Whether to log private data keys purged from private data store (INFO level) when explicitly purged via chaincode + purgedKeyAuditLogging: true + + snapshots: + # Path on the file system where peer will store ledger snapshots + # The path must be an absolute path. + rootDir: /var/hyperledger/production/snapshots + +############################################################################### +# +# Operations section +# +############################################################################### +operations: + # host and port for the operations server + listenAddress: 127.0.0.1:9443 + + # TLS configuration for the operations endpoint + tls: + # TLS enabled + enabled: false + + # path to PEM encoded server certificate for the operations server + # The paths in this section may be relative to FABRIC_CFG_PATH or an absolute path. + cert: + file: + + # path to PEM encoded server key for the operations server + key: + file: + + # most operations service endpoints require client authentication when TLS + # is enabled. clientAuthRequired requires client certificate authentication + # at the TLS layer to access all resources. + clientAuthRequired: false + + # paths to PEM encoded ca certificates to trust for client authentication + clientRootCAs: + files: [] + +############################################################################### +# +# Metrics section +# +############################################################################### +metrics: + # metrics provider is one of statsd, prometheus, or disabled + provider: disabled + + # statsd configuration + statsd: + # network type: tcp or udp + network: udp + + # statsd server address + address: 127.0.0.1:8125 + + # the interval at which locally cached counters and gauges are pushed + # to statsd; timings are pushed immediately + writeInterval: 10s + + # prefix is prepended to all emitted statsd metrics + prefix: diff --git a/test-network/scripts/invoke_the_chaincode.sh b/test-network/scripts/invoke_the_chaincode.sh new file mode 100755 index 0000000..8e73ed9 --- /dev/null +++ b/test-network/scripts/invoke_the_chaincode.sh @@ -0,0 +1,62 @@ +#!/bin/bash + + +set -e + +# Parse input JSON +ORDER_JSON=$1 +ORDER_ID=$(echo "$ORDER_JSON" | jq -r '.Args[0]') +STATUS=$(echo "$ORDER_JSON" | jq -r '.Args[1]') +TIMESTAMP=$(echo "$ORDER_JSON" | jq -r '.Args[2]') +CID=$(echo "$ORDER_JSON" | jq -r '.Args[3]') + +echo "πŸ“¦ Order ID : $ORDER_ID" +echo "πŸ“„ Status : $STATUS" +echo "⏰ Timestamp : $TIMESTAMP" +echo "🧬 IPFS CID : $CID" + +# Set paths +BASE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +BIN_PATH="$BASE_DIR/bin" +CONFIG_PATH="$BASE_DIR/config" +TEST_NETWORK="$BASE_DIR/test-network" +ORG_PATH="$TEST_NETWORK/organizations" + +# Set env +export PATH="$BIN_PATH:$PATH" +export FABRIC_CFG_PATH="$CONFIG_PATH" +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_TLS_ROOTCERT_FILE="$ORG_PATH/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" +export CORE_PEER_MSPCONFIGPATH="$ORG_PATH/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" +export CORE_PEER_ADDRESS=localhost:7051 + +# πŸ›  Invoke chaincode and capture TXID +set +e +RESULT=$(peer chaincode invoke \ + -o localhost:7050 \ + --ordererTLSHostnameOverride orderer.example.com \ + --tls \ + --cafile "$ORG_PATH/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" \ + -C mychannel \ + -n ordercc \ + --peerAddresses localhost:7051 \ + --tlsRootCertFiles "$ORG_PATH/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" \ + --peerAddresses localhost:9051 \ + --tlsRootCertFiles "$ORG_PATH/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" \ + -c "{\"function\":\"CreateOrder\",\"Args\":[\"$ORDER_ID\",\"$STATUS\",\"$TIMESTAMP\",\"$CID\"]}" +) + +STATUS=$? +set -e + +# Print and forward result +echo "$RESULT" + +if [ $STATUS -ne 0 ]; then + echo "❌ Chaincode invoke failed" + exit 1 +else + echo "βœ… Chaincode invoke successful" + exit 0 +fi diff --git a/test-network/scripts/setup_the_chaincode.sh b/test-network/scripts/setup_the_chaincode.sh new file mode 100755 index 0000000..475eba3 --- /dev/null +++ b/test-network/scripts/setup_the_chaincode.sh @@ -0,0 +1,80 @@ +set -e + +echo "πŸ” Shutting down any previous network..." +./network.sh down + +echo "πŸš€ Starting the network..." +./network.sh up createChannel -ca + +echo "πŸ“¦ Packaging chaincode..." +../bin/peer lifecycle chaincode package ordercc.tar.gz \ + --path ../chaincode-order \ + --lang golang \ + --label ordercc_1.0 + +echo "πŸ“₯ Installing chaincode on Org1..." +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_ADDRESS=localhost:7051 +export CORE_PEER_MSPCONFIGPATH=organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +../bin/peer lifecycle chaincode install ordercc.tar.gz + +echo "πŸ“₯ Installing chaincode on Org2..." +export CORE_PEER_LOCALMSPID="Org2MSP" +export CORE_PEER_ADDRESS=localhost:9051 +export CORE_PEER_MSPCONFIGPATH=organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt +../bin/peer lifecycle chaincode install ordercc.tar.gz + +PACKAGE_ID=$(../bin/peer lifecycle chaincode queryinstalled | grep "Package ID" | awk '{print $3}' | sed 's/,$//') +echo "πŸ”‘ Package ID: $PACKAGE_ID" + +echo "βœ… Approving chaincode for Org1..." +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_ADDRESS=localhost:7051 +export CORE_PEER_MSPCONFIGPATH=organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +../bin/peer lifecycle chaincode approveformyorg \ + --channelID mychannel \ + --name ordercc \ + --version 1.0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ + --orderer localhost:7050 \ + --ordererTLSHostnameOverride orderer.example.com + +echo "βœ… Approving chaincode for Org2..." +export CORE_PEER_LOCALMSPID="Org2MSP" +export CORE_PEER_ADDRESS=localhost:9051 +export CORE_PEER_MSPCONFIGPATH=organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt +../bin/peer lifecycle chaincode approveformyorg \ + --channelID mychannel \ + --name ordercc \ + --version 1.0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ + --orderer localhost:7050 \ + --ordererTLSHostnameOverride orderer.example.com + +echo "πŸ“Œ Committing chaincode definition..." +../bin/peer lifecycle chaincode commit \ + --channelID mychannel \ + --name ordercc \ + --version 1.0 \ + --sequence 1 \ + --tls \ + --orderer localhost:7050 \ + --ordererTLSHostnameOverride orderer.example.com \ + --cafile organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ + --peerAddresses localhost:7051 \ + --tlsRootCertFiles organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ + --peerAddresses localhost:9051 \ + --tlsRootCertFiles organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt + +echo "βœ… Chaincode deployed successfully! You're ready to use the backend." From 7fd11c6ce7c7d012c63679ba5d06431a8ef2e737 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Sat, 10 May 2025 16:56:34 +0530 Subject: [PATCH 27/30] Updated views to incorporate blockchain on order creation --- ..._blockchain_tx_id_alter_order_ipfs_hash.py | 23 +++ .../0010_alter_orderproduct_order.py | 19 ++ blocktrack_backend/orders/models.py | 6 +- blocktrack_backend/orders/serializers.py | 15 +- blocktrack_backend/orders/views.py | 163 +++++++++++++----- chaincode-order/order.go | 4 +- 6 files changed, 177 insertions(+), 53 deletions(-) create mode 100644 blocktrack_backend/orders/migrations/0009_alter_order_blockchain_tx_id_alter_order_ipfs_hash.py create mode 100644 blocktrack_backend/orders/migrations/0010_alter_orderproduct_order.py diff --git a/blocktrack_backend/orders/migrations/0009_alter_order_blockchain_tx_id_alter_order_ipfs_hash.py b/blocktrack_backend/orders/migrations/0009_alter_order_blockchain_tx_id_alter_order_ipfs_hash.py new file mode 100644 index 0000000..2ffedb1 --- /dev/null +++ b/blocktrack_backend/orders/migrations/0009_alter_order_blockchain_tx_id_alter_order_ipfs_hash.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2 on 2025-05-10 11:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0008_rename_order_id_orderproduct_order'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='blockchain_tx_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='order', + name='ipfs_hash', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/blocktrack_backend/orders/migrations/0010_alter_orderproduct_order.py b/blocktrack_backend/orders/migrations/0010_alter_orderproduct_order.py new file mode 100644 index 0000000..ceabf05 --- /dev/null +++ b/blocktrack_backend/orders/migrations/0010_alter_orderproduct_order.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2 on 2025-05-10 11:18 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0009_alter_order_blockchain_tx_id_alter_order_ipfs_hash'), + ] + + operations = [ + migrations.AlterField( + model_name='orderproduct', + name='order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='orders.order'), + ), + ] diff --git a/blocktrack_backend/orders/models.py b/blocktrack_backend/orders/models.py index 9fd25dc..4bbb510 100644 --- a/blocktrack_backend/orders/models.py +++ b/blocktrack_backend/orders/models.py @@ -11,8 +11,8 @@ class Order(models.Model): ('Delivered', 'Delivered'), ('Cancelled', 'Cancelled'), ]) - blockchain_tx_id = models.CharField(max_length=255) - ipfs_hash = models.CharField(max_length=255) + blockchain_tx_id = models.CharField(max_length=255, blank=True, null=True) + ipfs_hash = models.CharField(max_length=255, blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -29,7 +29,7 @@ def __str__(self): return f"Order {self.order.order_id} - Details" class OrderProduct(models.Model): - order = models.ForeignKey(Order, related_name='order_products', on_delete=models.CASCADE) + order = models.ForeignKey(Order, related_name='products', on_delete=models.CASCADE) product_id = models.IntegerField() count = models.IntegerField() unit_price = models.FloatField() diff --git a/blocktrack_backend/orders/serializers.py b/blocktrack_backend/orders/serializers.py index 5a71f68..08047c9 100644 --- a/blocktrack_backend/orders/serializers.py +++ b/blocktrack_backend/orders/serializers.py @@ -17,8 +17,8 @@ class Meta: fields = ['warehouse_id', 'nearest_city', 'latitude', 'longitude'] class OrderSerializer(serializers.ModelSerializer): - products = OrderProductSerializer(many=True, read_only=True, source='order_products') - details = OrderDetailsSerializer(read_only=True) + products = OrderProductSerializer(many=True) + details = OrderDetailsSerializer() class Meta: model = Order @@ -26,19 +26,24 @@ class Meta: read_only_fields = ['order_id', 'created_at'] def create(self, validated_data): + # Extract details data details_data = validated_data.pop('details') - products_data = validated_data.pop('products') + products_data = validated_data.pop('products', []) + # Create the order order = Order.objects.create(**validated_data) + + # Create the OrderDetails object OrderDetails.objects.create(order=order, **details_data) + # Create OrderProduct objects if provided for prod in products_data: - OrderProduct.objects.create(order_id=order, **prod) + OrderProduct.objects.create(order=order, **prod) return order class MinimalOrderSerializer(serializers.ModelSerializer): - products = MinimalOrderProductSerializer(many=True, read_only=True, source='order_products') + products = MinimalOrderProductSerializer(many=True) class Meta: model = Order diff --git a/blocktrack_backend/orders/views.py b/blocktrack_backend/orders/views.py index 2958a33..84747c7 100644 --- a/blocktrack_backend/orders/views.py +++ b/blocktrack_backend/orders/views.py @@ -25,7 +25,7 @@ BIN_PATH = FABRIC_BASE / "bin" CONFIG_PATH = FABRIC_BASE / "config" TEST_NETWORK = FABRIC_BASE / "test-network" -SCRIPT_PATH = FABRIC_BASE / "test-network" / "scripts" / "invoke_order.sh" +SCRIPT_PATH = FABRIC_BASE / "test-network" / "scripts" / "invoke_the_chaincode.sh" def get_fabric_env(): env = os.environ.copy() env["PATH"] = f"{BIN_PATH}:" + env["PATH"] @@ -37,7 +37,35 @@ def get_fabric_env(): env["CORE_PEER_ADDRESS"] = "localhost:7051" return env +def invoke_create_order(order_id, timestamp, status, cid = ""): + fabric_env = get_fabric_env() + + import json + args_json = json.dumps({ + "function": "CreateOrder", + "Args": [order_id, status, timestamp, cid] + }) + + print("πŸ”§ Executing:", SCRIPT_PATH, args_json) + + result = subprocess.run( + [str(SCRIPT_PATH), args_json], + capture_output=True, + text=True, + env=fabric_env + ) + + print("βœ… STDOUT:", result.stdout) + print("❌ STDERR:", result.stderr) + if result.returncode != 0: + raise Exception(f"Invoke script failed:\n{result.stderr}") + + return Response({ + "message": "Order created", + "cid": cid, + "blockchain_response": result.stdout.strip() + }) class CreateOrderView(APIView): parser_classes = [MultiPartParser] @@ -149,6 +177,46 @@ class OrderListCreateView(generics.ListCreateAPIView): filter_backends = [DjangoFilterBackend] filterset_fields = ['status'] + def create(self, request, *args, **kwargs): + # Standard serializer validation + print(request.data) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + # Set default status if not provided + if 'status' not in serializer.validated_data: + serializer.validated_data['status'] = 'Pending' + + # Save the order to database + order = serializer.save() + + # Register on blockchain + try: + # Format timestamp + timestamp = order.created_at.isoformat() + + # Call the blockchain function with order data + invoke_create_order( + order_id=str(order.order_id), + timestamp=timestamp, + status=order.status, + cid="" # Empty CID since we don't have a file + ) + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + except Exception as e: + # Log the error but keep the order in database + print(f"Blockchain registration failed: {str(e)}") + + headers = self.get_success_headers(serializer.data) + return Response({ + "warning": "Order created in database but blockchain registration failed", + "details": str(e), + "order": serializer.data + }, status=status.HTTP_201_CREATED, headers=headers) + class OrderByWarehouse(APIView): @swagger_auto_schema( manual_parameters=[ @@ -159,15 +227,20 @@ class OrderByWarehouse(APIView): ] ) def get(self, request, warehouse_id): - queryset = Order.objects.filter(details__warehouse_id=warehouse_id) + try: + queryset = Order.objects.filter(details__warehouse_id=warehouse_id) + + minimal = request.GET.get('minimal', False) + + if (eval(minimal[0].upper() + minimal[1:])): + serializer = MinimalOrderSerializer(queryset, many=True) + else: + serializer = OrderSerializer(queryset, many=True) + return Response(serializer.data) + except Exception as e: + return Response({"error": "An unexpected error occurred", "details": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - minimal = request.GET.get('minimal', False) - if (eval(minimal[0].upper() + minimal[1:])): - serializer = MinimalOrderSerializer(queryset, many=True) - else: - serializer = OrderSerializer(queryset, many=True) - return Response(serializer.data) class OrderDetailView(generics.RetrieveUpdateAPIView): queryset = Order.objects.all() @@ -207,42 +280,46 @@ class OrderStatusUpdateView(APIView): ) def patch(self, request, order_id): try: - order = Order.objects.get(order_id=order_id) - except Order.DoesNotExist: - return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND) + try: + order = Order.objects.get(order_id=order_id) + except Order.DoesNotExist: + return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND) - new_status = request.data.get('status') - warehouse_location = request.data.get('warehouse_location') - - new_data = {} + new_status = request.data.get('status') + warehouse_location = request.data.get('warehouse_location') + + new_data = {} - if warehouse_location: - origin_longitude = warehouse_location.get('longitude') - origin_latitude = warehouse_location.get('latitude') - - # Validate status against model choices if provided - if new_status: - valid_statuses = [choice[0] for choice in Order._meta.get_field('status').choices] + if warehouse_location: + origin_longitude = warehouse_location.get('longitude') + origin_latitude = warehouse_location.get('latitude') - if new_status not in valid_statuses: - return Response({ - "error": "Invalid status value", - "status": new_status, - }, status=status.HTTP_400_BAD_REQUEST) + # Validate status against model choices if provided + if new_status: + valid_statuses = [choice[0] for choice in Order._meta.get_field('status').choices] + + if new_status not in valid_statuses: + return Response({ + "error": "Invalid status value", + "status": new_status, + }, status=status.HTTP_400_BAD_REQUEST) + + new_data['status'] = new_status + + # Create the event and push to kafka + event = { + "order_id": order_id, + "origin": {"lat": origin_latitude, "lng": origin_longitude}, + "destination": {"lat": order.details.longitude, "lng": order.details.longitude}, + "demand": 10 + } + send_to_kafka('orders.created', event) - new_data['status'] = new_status - - # Create the event and push to kafka - event = { - "order_id": order_id, - "origin": {"lat": origin_latitude, "lng": origin_longitude}, - "destination": {"lat": order.details.longitude, "lng": order.details.longitude}, - "demand": 10 - } - send_to_kafka('orders.created', event) - - serializer = OrderSerializer(order, data=new_data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer = OrderSerializer(order, data=new_data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response({"error": "An unexpected error occurred", "details": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/chaincode-order/order.go b/chaincode-order/order.go index 724eeaf..9eda753 100644 --- a/chaincode-order/order.go +++ b/chaincode-order/order.go @@ -15,7 +15,7 @@ type Order struct { ID string `json:"ID"` Status string `json:"Status"` Timestamp string `json:"Timestamp"` - DocumentHash string `json:"DocumentHash"` + // DocumentHash string `json:"DocumentHash"` } func (s *SmartContract) CreateOrder(ctx contractapi.TransactionContextInterface, id, status, timestamp, docHash string) error { @@ -23,7 +23,7 @@ func (s *SmartContract) CreateOrder(ctx contractapi.TransactionContextInterface, ID: id, Status: status, Timestamp: timestamp, - DocumentHash: docHash, + // DocumentHash: docHash, } orderJSON, err := json.Marshal(order) if err != nil { From f149fd7a5d560c7f74e8acd8d0fce9dbc7b030f7 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Sat, 10 May 2025 18:00:41 +0530 Subject: [PATCH 28/30] Removed unit_price from post request to create Supplier Request, instead its fetched from warehouse --- .../0003_alter_supplierrequest_unit_price.py | 18 ++++++ blocktrack_backend/supplier_request/models.py | 2 +- .../supplier_request/serializers.py | 3 +- blocktrack_backend/supplier_request/views.py | 62 ++++++++++++------- 4 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 blocktrack_backend/supplier_request/migrations/0003_alter_supplierrequest_unit_price.py diff --git a/blocktrack_backend/supplier_request/migrations/0003_alter_supplierrequest_unit_price.py b/blocktrack_backend/supplier_request/migrations/0003_alter_supplierrequest_unit_price.py new file mode 100644 index 0000000..5624ea1 --- /dev/null +++ b/blocktrack_backend/supplier_request/migrations/0003_alter_supplierrequest_unit_price.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-05-10 12:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('supplier_request', '0002_supplierrequest_is_defective_supplierrequest_quality_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='supplierrequest', + name='unit_price', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/blocktrack_backend/supplier_request/models.py b/blocktrack_backend/supplier_request/models.py index 0f5ff0c..8443832 100644 --- a/blocktrack_backend/supplier_request/models.py +++ b/blocktrack_backend/supplier_request/models.py @@ -19,7 +19,7 @@ class SupplierRequest(models.Model): status = models.CharField(max_length=10, choices=STATUS_CHOICES) received_at = models.DateTimeField(null=True, blank=True) warehouse_id = models.IntegerField() - unit_price = models.FloatField() + unit_price = models.FloatField(blank=True, null=True) quality = models.IntegerField(validators=[ MinValueValidator(0), MaxValueValidator(10) diff --git a/blocktrack_backend/supplier_request/serializers.py b/blocktrack_backend/supplier_request/serializers.py index ad53e41..9696ca6 100644 --- a/blocktrack_backend/supplier_request/serializers.py +++ b/blocktrack_backend/supplier_request/serializers.py @@ -4,4 +4,5 @@ class SupplierRequestSerializer(serializers.ModelSerializer): class Meta: model = SupplierRequest - fields = '__all__' \ No newline at end of file + fields = '__all__' + read_only_fields = ['unit_price'] \ No newline at end of file diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py index 50f5190..3eb7a20 100644 --- a/blocktrack_backend/supplier_request/views.py +++ b/blocktrack_backend/supplier_request/views.py @@ -1,3 +1,4 @@ +from decimal import Decimal from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -8,30 +9,51 @@ from .models import SupplierRequest from .serializers import SupplierRequestSerializer +# Get service URLs from environment variables +user_service_url = os.environ.get('USER_SERVICE_URL', 'http://127.0.0.1:8002') +warehouse_service_url = os.environ.get('WAREHOUSE_SERVICE_URL', 'http://127.0.0.1:8001') class SupplierRequestListCreate(APIView): + def get_unit_price(self, product_id, supplier_id): + response = requests.get(f"{warehouse_service_url}/api/product/supplier-products/") + + if response.status_code == 200: + supplier_products = response.json() + + # Find the matching product for this supplier + for item in supplier_products: + if item['product'] == product_id and item['supplier_id'] == supplier_id: + return Decimal(item['supplier_price']) + + # If no match found, raise error + raise Exception(f"No price found for product {product_id} from supplier {supplier_id}") + else: + print(f"Failed to get supplier products: {response.status_code}") + return None + @swagger_auto_schema( request_body=SupplierRequestSerializer, responses={201: SupplierRequestSerializer()} ) def post(self, request): - serializer = SupplierRequestSerializer(data=request.data) - if serializer.is_valid(): - # custom logic here - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - # @swagger_auto_schema( - # manual_parameters=[ - # openapi.Parameter( - # 'request_id', openapi.IN_QUERY, - # description="Optional request_id to filter", - # type=openapi.TYPE_STRING - # ) - # ], - # responses={200: SupplierRequestSerializer(many=True)} - # ) + try: + serializer = SupplierRequestSerializer(data=request.data) + if serializer.is_valid(): + data = serializer.validated_data + + print("data given by this is", data["product_id"]) + type + data['unit_price'] = self.get_unit_price(data["product_id"],data["supplier_id"]) + instance = SupplierRequest.objects.create(**data) + return Response(SupplierRequestSerializer(instance).data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response({ + "error": "Failed to create supplier request", + "details": str(e) + }, status=500) + + def get(self, request): requests = SupplierRequest.objects.all() serializer = SupplierRequestSerializer(requests, many=True) @@ -165,12 +187,6 @@ def get(self, request, warehouse_id): serializer = SupplierRequestSerializer(supplier_requests, many=True) data = serializer.data - - # Get service URLs from environment variables - user_service_url = os.environ.get('USER_SERVICE_URL', 'http://127.0.0.1:8002') - warehouse_service_url = os.environ.get('WAREHOUSE_SERVICE_URL', 'http://127.0.0.1:8001') - print(warehouse_service_url) - # Enrich data with supplier names and product names for item in data: From 112fafd9a5f27d51403a812d4a8656ac42debd92 Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Sat, 10 May 2025 22:05:48 +0530 Subject: [PATCH 29/30] Added new filter method to filter supplier_requests by status when hitting supplier-request/supplier/ --- blocktrack_backend/requirements.txt | 4 +- blocktrack_backend/supplier_request/urls.py | 2 +- blocktrack_backend/supplier_request/views.py | 50 ++++++++++++++++++-- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/blocktrack_backend/requirements.txt b/blocktrack_backend/requirements.txt index 79347fd..9f6b17a 100644 --- a/blocktrack_backend/requirements.txt +++ b/blocktrack_backend/requirements.txt @@ -15,4 +15,6 @@ varint==1.0.2 django-filter psycopg2-binary python-dotenv -kafka-python \ No newline at end of file +kafka-python +django-cors-headers +drf_yasg \ No newline at end of file diff --git a/blocktrack_backend/supplier_request/urls.py b/blocktrack_backend/supplier_request/urls.py index af0f7fd..972d4c4 100644 --- a/blocktrack_backend/supplier_request/urls.py +++ b/blocktrack_backend/supplier_request/urls.py @@ -17,4 +17,4 @@ path('supplier-request/supplier//', SupplierRequestBySupplier.as_view()), path('supplier-request/metrics//', SupplierRequestMetrics.as_view()), path('supplier-request/warehouse//', SupplierRequestWithNames.as_view()) -] +] \ No newline at end of file diff --git a/blocktrack_backend/supplier_request/views.py b/blocktrack_backend/supplier_request/views.py index 3eb7a20..2d5af4f 100644 --- a/blocktrack_backend/supplier_request/views.py +++ b/blocktrack_backend/supplier_request/views.py @@ -60,11 +60,55 @@ def get(self, request): return Response(serializer.data) + class SupplierRequestBySupplier(APIView): + + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + 'status', openapi.IN_QUERY, + description="Optional list of statuses to filter by. Can be a list of statuses like ?status=received&status=accepted.", + type=openapi.TYPE_STRING, + collectionFormat='multi', # Allows multiple occurrences of the 'status' parameter + required=False + ) + ], + responses={ + 200: openapi.Response( + description="List of supplier requests with enriched product data", + schema=SupplierRequestSerializer(many=True) + ), + 400: "Bad Request", + 500: "Internal Server Error" + } + ) def get(self, request, supplier_id): - requests = SupplierRequest.objects.filter(supplier_id=supplier_id) - serializer = SupplierRequestSerializer(requests, many=True) - return Response(serializer.data) + # Extract status from query parameter + status = request.query_params.getlist('status') + + # Filter supplier requests by supplier_id and status + reqs = SupplierRequest.objects.filter(supplier_id=supplier_id) + if status: + reqs = reqs.filter(status__in=status) + + serializer = SupplierRequestSerializer(reqs, many=True) + data = serializer.data + + # Enrich data with product information + for item in data: + try: + product_id = item.get('product_id') + product_response = requests.get(f"{warehouse_service_url}/api/product/products/{product_id}/") + if product_response.status_code == 200: + product_data = product_response.json() + item['product_name'] = product_data.get('product_name', 'Unknown') + else: + item['product_name'] = 'Unknown' + except Exception as e: + item['product_name'] = 'Unknown' + print(f"Error fetching product info: {str(e)}") + + return Response(data) class SupplierRequestByWarehouse(APIView): def get(self, request, warehouse_id): From 3afd304fd37dcc81991d964278e1ce80b8a2324e Mon Sep 17 00:00:00 2001 From: moonlander101 Date: Sun, 11 May 2025 01:23:33 +0530 Subject: [PATCH 30/30] Updated ipfs compose file --- test-network/compose/compose-ipfs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-network/compose/compose-ipfs.yaml b/test-network/compose/compose-ipfs.yaml index 75eb689..422589e 100755 --- a/test-network/compose/compose-ipfs.yaml +++ b/test-network/compose/compose-ipfs.yaml @@ -5,7 +5,7 @@ services: ports: - "4001:4001" # Swarm - "5001:5001" # API - - "8080:8080" # Gateway + - "8989:8080" # Gateway volumes: - ipfs_data:/data/ipfs - ipfs_staging:/export