diff --git a/customerwebsitebackend/__pycache__/db_router.cpython-312.pyc b/customerwebsitebackend/__pycache__/db_router.cpython-312.pyc new file mode 100644 index 0000000..d78e65e Binary files /dev/null and b/customerwebsitebackend/__pycache__/db_router.cpython-312.pyc differ diff --git a/customerwebsitebackend/__pycache__/settings.cpython-312.pyc b/customerwebsitebackend/__pycache__/settings.cpython-312.pyc index 6c145d5..8154685 100644 Binary files a/customerwebsitebackend/__pycache__/settings.cpython-312.pyc and b/customerwebsitebackend/__pycache__/settings.cpython-312.pyc differ diff --git a/customerwebsitebackend/db_router.py b/customerwebsitebackend/db_router.py new file mode 100644 index 0000000..4fe1630 --- /dev/null +++ b/customerwebsitebackend/db_router.py @@ -0,0 +1,20 @@ +class DatabaseRouter: + def db_for_read(self, model, **hints): + if model._meta.model_name == 'product': + return 'productprofile' + elif model._meta.model_name in ['sellerorder', 'orderitem']: + return 'customerprofile' + return 'default' + + def db_for_write(self, model, **hints): + return self.db_for_read(model, **hints) + + def allow_relation(self, obj1, obj2, **hints): + return True + + def allow_migrate(self, db, app_label, model_name=None, **hints): + if model_name == 'product': + return db == 'productprofile' + elif model_name in ['sellerorder', 'orderitem']: + return db == 'customerprofile' + return db == 'default' diff --git a/customerwebsitebackend/settings.py b/customerwebsitebackend/settings.py index beb1e9e..9bf455d 100644 --- a/customerwebsitebackend/settings.py +++ b/customerwebsitebackend/settings.py @@ -88,9 +88,27 @@ DATABASES = { 'PASSWORD': 'vtechnosoft@123A', 'HOST': '127.0.0.1', 'PORT': '5432', + }, + 'productprofile': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'productprofile', + 'USER': 'vignesh', + 'PASSWORD': 'vtechnosoft@123A', + 'HOST': '127.0.0.1', + 'PORT': '5432', + }, + 'customerprofile': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'customerprofile', + 'USER': 'vignesh', + 'PASSWORD': 'vtechnosoft@123A', + 'HOST': '127.0.0.1', + 'PORT': '5432', } } +DATABASE_ROUTERS = ['customerwebsitebackend.db_router.DatabaseRouter'] + # Password validation # https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators diff --git a/logs/app.log b/logs/app.log index dff5994..590d0aa 100644 --- a/logs/app.log +++ b/logs/app.log @@ -5,3 +5,7 @@ INFO 2026-08-14 13:43:48,808 autoreload 57706 139697750147200 /home/vignesh/gith INFO 2026-08-14 13:43:49,076 autoreload 58191 123150790480000 Watching for file changes with StatReloader INFO 2026-08-14 13:46:26,838 autoreload 58191 123150790480000 /home/vignesh/github/customerwebsitebackend/customerwebsitebackend/settings.py changed, reloading. INFO 2026-08-14 13:46:27,112 autoreload 60038 140193920041088 Watching for file changes with StatReloader +INFO 2026-08-14 13:52:49,207 autoreload 60038 140193920041088 /home/vignesh/github/customerwebsitebackend/orders/models.py changed, reloading. +INFO 2026-08-14 13:52:49,524 autoreload 64078 126174254198912 Watching for file changes with StatReloader +INFO 2026-08-14 13:53:37,086 autoreload 64078 126174254198912 /home/vignesh/github/customerwebsitebackend/orders/models.py changed, reloading. +INFO 2026-08-14 13:53:37,360 autoreload 64566 128112213057664 Watching for file changes with StatReloader diff --git a/orders/__pycache__/models.cpython-312.pyc b/orders/__pycache__/models.cpython-312.pyc index a6c8dbc..24937f3 100644 Binary files a/orders/__pycache__/models.cpython-312.pyc and b/orders/__pycache__/models.cpython-312.pyc differ diff --git a/orders/__pycache__/serializers.cpython-312.pyc b/orders/__pycache__/serializers.cpython-312.pyc index 80b9197..6437178 100644 Binary files a/orders/__pycache__/serializers.cpython-312.pyc and b/orders/__pycache__/serializers.cpython-312.pyc differ diff --git a/orders/__pycache__/views.cpython-312.pyc b/orders/__pycache__/views.cpython-312.pyc index 728606d..a84aabe 100644 Binary files a/orders/__pycache__/views.cpython-312.pyc and b/orders/__pycache__/views.cpython-312.pyc differ diff --git a/orders/migrations/0002_sellerorder_remove_cartitem_product_and_more.py b/orders/migrations/0002_sellerorder_remove_cartitem_product_and_more.py new file mode 100644 index 0000000..bca6b40 --- /dev/null +++ b/orders/migrations/0002_sellerorder_remove_cartitem_product_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 6.1 on 2026-08-14 13:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SellerOrder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('supplier_id', models.IntegerField()), + ('order_id', models.CharField(max_length=50, unique=True)), + ('date', models.DateField()), + ('item', models.CharField(max_length=255)), + ('quantity', models.IntegerField(default=1)), + ('customer', models.CharField(max_length=255)), + ('total', models.DecimalField(decimal_places=2, max_digits=10)), + ('status', models.CharField(default='Pending Acceptance', max_length=50)), + ('carrier', models.CharField(default='Pending', max_length=100)), + ('tracking', models.CharField(default='Pending', max_length=100)), + ('eta', models.CharField(default='N/A', max_length=50)), + ], + options={ + 'db_table': 'api_order', + 'managed': False, + }, + ), + migrations.RemoveField( + model_name='cartitem', + name='product', + ), + migrations.AddField( + model_name='cartitem', + name='product_id', + field=models.IntegerField(default=1), + ), + migrations.DeleteModel( + name='OrderItem', + ), + ] diff --git a/orders/migrations/__pycache__/0002_sellerorder_remove_cartitem_product_and_more.cpython-312.pyc b/orders/migrations/__pycache__/0002_sellerorder_remove_cartitem_product_and_more.cpython-312.pyc new file mode 100644 index 0000000..446d367 Binary files /dev/null and b/orders/migrations/__pycache__/0002_sellerorder_remove_cartitem_product_and_more.cpython-312.pyc differ diff --git a/orders/models.py b/orders/models.py index 2dc7118..780820b 100644 --- a/orders/models.py +++ b/orders/models.py @@ -21,12 +21,19 @@ class Cart(models.Model): class CartItem(models.Model): cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items') - product = models.ForeignKey(Product, on_delete=models.CASCADE) + product_id = models.IntegerField(default=1) # Keep reference as simple int since product is in productprofile db quantity = models.PositiveIntegerField(default=1) price_at_add = models.DecimalField(max_digits=10, decimal_places=2) + @property + def product(self): + try: + return Product.objects.get(id=self.product_id) + except Product.DoesNotExist: + return None + def __str__(self): - return f"{self.quantity} x {self.product.title}" + return f"{self.quantity} x Product {self.product_id}" class Order(models.Model): STATUS_CHOICES = ( @@ -47,13 +54,23 @@ class Order(models.Model): def __str__(self): return f"Order {self.id} by {self.user.username}" -class OrderItem(models.Model): - order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') - product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True) - quantity = models.PositiveIntegerField() - unit_price = models.DecimalField(max_digits=10, decimal_places=2) - tracking_number = models.CharField(max_length=100, blank=True, null=True) - carrier = models.CharField(max_length=100, blank=True, null=True) +# Align to seller_central_backend api_order schema +class SellerOrder(models.Model): + supplier_id = models.IntegerField() # Raw integer referring to User in sellerprofile db + order_id = models.CharField(max_length=50, unique=True) + date = models.DateField() + item = models.CharField(max_length=255) + quantity = models.IntegerField(default=1) + customer = models.CharField(max_length=255) + total = models.DecimalField(max_digits=10, decimal_places=2) + status = models.CharField(max_length=50, default='Pending Acceptance') + carrier = models.CharField(max_length=100, default='Pending') + tracking = models.CharField(max_length=100, default='Pending') + eta = models.CharField(max_length=50, default='N/A') + + class Meta: + db_table = 'api_order' + managed = False # Managed by seller central backend migrations def __str__(self): - return f"{self.quantity} x {self.product.title if self.product else 'Deleted Product'}" + return self.order_id diff --git a/orders/serializers.py b/orders/serializers.py index edb6276..73b913c 100644 --- a/orders/serializers.py +++ b/orders/serializers.py @@ -1,16 +1,22 @@ from rest_framework import serializers -from .models import Cart, CartItem, Order, OrderItem +from .models import Cart, CartItem, Order, SellerOrder from products.serializers import ProductSerializer class CartItemSerializer(serializers.ModelSerializer): - product = ProductSerializer(read_only=True) - product_id = serializers.IntegerField(write_only=True) + product = serializers.SerializerMethodField() + product_id = serializers.IntegerField() subtotal = serializers.SerializerMethodField() class Meta: model = CartItem fields = ['id', 'product', 'product_id', 'quantity', 'price_at_add', 'subtotal'] + def get_product(self, obj): + prod = obj.product + if prod: + return ProductSerializer(prod).data + return None + def get_subtotal(self, obj): return obj.quantity * obj.price_at_add @@ -25,18 +31,9 @@ class CartSerializer(serializers.ModelSerializer): def get_total_price(self, obj): return sum(item.quantity * item.price_at_add for item in obj.items.all()) -class OrderItemSerializer(serializers.ModelSerializer): - product_title = serializers.CharField(source='product.title', read_only=True) - product_image = serializers.CharField(source='product.image_url', read_only=True) - - class Meta: - model = OrderItem - fields = ['id', 'product', 'product_title', 'product_image', 'quantity', 'unit_price', 'tracking_number', 'carrier'] - class OrderSerializer(serializers.ModelSerializer): - items = OrderItemSerializer(many=True, read_only=True) username = serializers.CharField(source='user.username', read_only=True) class Meta: model = Order - fields = ['id', 'username', 'razorpay_order_id', 'razorpay_payment_id', 'total_amount', 'status', 'shipping_address', 'placed_at', 'items'] + fields = ['id', 'username', 'razorpay_order_id', 'razorpay_payment_id', 'total_amount', 'status', 'shipping_address', 'placed_at'] diff --git a/orders/views.py b/orders/views.py index 99e48ab..206768d 100644 --- a/orders/views.py +++ b/orders/views.py @@ -3,10 +3,11 @@ audit_logger = logging.getLogger('audit') from rest_framework import status, permissions from rest_framework.response import Response from rest_framework.views import APIView -from .models import Cart, CartItem, Order, OrderItem +from .models import Cart, CartItem, Order, SellerOrder from .serializers import CartSerializer, OrderSerializer from products.models import Product import uuid +from django.utils.timezone import now class CartView(APIView): permission_classes = [permissions.IsAuthenticated] @@ -26,17 +27,17 @@ class CartView(APIView): quantity = int(request.data.get('quantity', 1)) try: - product = Product.objects.get(id=product_id, is_active=True) + product = Product.objects.get(id=product_id) except Product.DoesNotExist: return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND) cart_item, created = CartItem.objects.get_or_create( cart=cart, - product=product, + product_id=product_id, defaults={'price_at_add': product.price, 'quantity': 0} ) cart_item.quantity += quantity - cart_item.price_at_add = product.price # update price to latest + cart_item.price_at_add = product.price cart_item.save() return Response({"message": "Item added to cart"}) @@ -86,13 +87,10 @@ class CheckoutView(APIView): if not shipping_address: return Response({"error": "Shipping address is required"}, status=status.HTTP_400_BAD_REQUEST) - # Calculate total total_amount = sum(item.quantity * item.price_at_add for item in items) - - # Mock Razorpay Order Creation razorpay_order_id = f"order_{uuid.uuid4().hex[:14]}" - # Create Order + # 1. Create a local Customer Order order = Order.objects.create( user=user, total_amount=total_amount, @@ -102,17 +100,20 @@ class CheckoutView(APIView): ) audit_logger.info(f"AUDIT - Checkout: User '{user.username}' created order ID {order.id} for amount ${total_amount:.2f}.") + # 2. Write to shared SellerOrder (api_order) so sellers get notified of incoming requests for item in items: - OrderItem.objects.create( - order=order, - product=item.product, - quantity=item.quantity, - unit_price=item.price_at_add - ) - # Deduct stock - if item.product.stock >= item.quantity: - item.product.stock -= item.quantity - item.product.save() + prod = item.product + if prod: + SellerOrder.objects.create( + supplier_id=prod.supplier_id, + order_id=f"{order.id}-{prod.id}", + date=now().date(), + item=prod.title, + quantity=item.quantity, + customer=user.username, + total=item.quantity * item.price_at_add, + status='Pending Acceptance' + ) # Clear Cart items.delete() @@ -141,6 +142,9 @@ class PaymentSuccessView(APIView): order.save() audit_logger.info(f"AUDIT - Payment Success: User '{request.user.username}' successfully paid order ID {order.id} (Payment ID: {payment_id}).") + # Update matching SellerOrders status + SellerOrder.objects.filter(order_id__startswith=f"{order.id}-").update(status='Ready to Ship') + return Response({"message": "Payment verified and order confirmed"}) class OrderHistoryView(APIView): @@ -149,7 +153,40 @@ class OrderHistoryView(APIView): def get(self, request): orders = Order.objects.filter(user=request.user).order_by('-placed_at') serializer = OrderSerializer(orders, many=True) - return Response(serializer.data) + + # Merge tracking details from SellerOrders + response_data = [] + for order_data in serializer.data: + order_id = order_data['id'] + seller_orders = SellerOrder.objects.filter(order_id__startswith=f"{order_id}-") + + # Enrich with tracking details if available + order_data['items'] = [] + for s_ord in seller_orders: + order_data['items'].append({ + "product_title": s_ord.item, + "quantity": s_ord.quantity, + "unit_price": float(s_ord.total / s_ord.quantity), + "tracking_number": s_ord.tracking if s_ord.tracking != 'Pending' else None, + "carrier": s_ord.carrier if s_ord.carrier != 'Pending' else None, + "status": s_ord.status + }) + + # Aggregate status + if seller_orders.exists(): + statuses = [o.status for o in seller_orders] + if 'Pending Acceptance' in statuses: + order_data['status'] = 'pending' + elif 'Ready to Ship' in statuses: + order_data['status'] = 'confirmed' + elif 'Shipped' in statuses: + order_data['status'] = 'shipped' + elif 'Delivered' in statuses: + order_data['status'] = 'delivered' + + response_data.append(order_data) + + return Response(response_data) class OrderDetailView(APIView): permission_classes = [permissions.IsAuthenticated] @@ -159,5 +196,20 @@ class OrderDetailView(APIView): order = Order.objects.get(id=pk, user=request.user) except Order.DoesNotExist: return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND) + serializer = OrderSerializer(order) - return Response(serializer.data) + order_data = serializer.data + + seller_orders = SellerOrder.objects.filter(order_id__startswith=f"{pk}-") + order_data['items'] = [] + for s_ord in seller_orders: + order_data['items'].append({ + "product_title": s_ord.item, + "quantity": s_ord.quantity, + "unit_price": float(s_ord.total / s_ord.quantity), + "tracking_number": s_ord.tracking if s_ord.tracking != 'Pending' else None, + "carrier": s_ord.carrier if s_ord.carrier != 'Pending' else None, + "status": s_ord.status + }) + + return Response(order_data) diff --git a/products/__pycache__/models.cpython-312.pyc b/products/__pycache__/models.cpython-312.pyc index b75eaea..af6698b 100644 Binary files a/products/__pycache__/models.cpython-312.pyc and b/products/__pycache__/models.cpython-312.pyc differ diff --git a/products/__pycache__/serializers.cpython-312.pyc b/products/__pycache__/serializers.cpython-312.pyc index c3e5b80..b26aaae 100644 Binary files a/products/__pycache__/serializers.cpython-312.pyc and b/products/__pycache__/serializers.cpython-312.pyc differ diff --git a/products/__pycache__/views.cpython-312.pyc b/products/__pycache__/views.cpython-312.pyc index 878359f..d561645 100644 Binary files a/products/__pycache__/views.cpython-312.pyc and b/products/__pycache__/views.cpython-312.pyc differ diff --git a/products/migrations/0002_alter_product_options.py b/products/migrations/0002_alter_product_options.py new file mode 100644 index 0000000..1daef3d --- /dev/null +++ b/products/migrations/0002_alter_product_options.py @@ -0,0 +1,17 @@ +# Generated by Django 6.1 on 2026-08-14 13:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='product', + options={'managed': False}, + ), + ] diff --git a/products/migrations/__pycache__/0002_alter_product_options.cpython-312.pyc b/products/migrations/__pycache__/0002_alter_product_options.cpython-312.pyc new file mode 100644 index 0000000..f91b020 Binary files /dev/null and b/products/migrations/__pycache__/0002_alter_product_options.cpython-312.pyc differ diff --git a/products/models.py b/products/models.py index c1054ff..cb2d48d 100644 --- a/products/models.py +++ b/products/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.utils.text import slugify +from django.contrib.auth.models import User class Category(models.Model): name = models.CharField(max_length=100) @@ -11,25 +11,22 @@ class Category(models.Model): verbose_name_plural = "Categories" ordering = ['sort_order', 'name'] - def save(self, *args, **kwargs): - if not self.slug: - self.slug = slugify(self.name) - super().save(*args, **kwargs) - def __str__(self): return self.name class Product(models.Model): - category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products') + # Align to seller_central_backend api_product schema + supplier = models.ForeignKey(User, on_delete=models.CASCADE, related_name='products', db_constraint=False) title = models.CharField(max_length=255) + category = models.CharField(max_length=100) # CharField matching seller central price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.IntegerField(default=0) sku = models.CharField(max_length=100, unique=True) - image_url = models.URLField(max_length=500, blank=True, null=True) - description = models.TextField(blank=True) - is_active = models.BooleanField(default=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) + image = models.TextField(blank=True, null=True) # Textfield for base64 / URL + + class Meta: + db_table = 'api_product' + managed = False # Managed by seller central backend migrations def __str__(self): return self.title diff --git a/products/serializers.py b/products/serializers.py index 75fe9e0..75cce02 100644 --- a/products/serializers.py +++ b/products/serializers.py @@ -7,8 +7,9 @@ class CategorySerializer(serializers.ModelSerializer): fields = ['id', 'name', 'slug', 'is_active', 'sort_order'] class ProductSerializer(serializers.ModelSerializer): - category_name = serializers.CharField(source='category.name', read_only=True) + category_name = serializers.CharField(source='category', read_only=True) + image_url = serializers.CharField(source='image', read_only=True) class Meta: model = Product - fields = ['id', 'category', 'category_name', 'title', 'price', 'stock', 'sku', 'image_url', 'description', 'is_active'] + fields = ['id', 'category', 'category_name', 'title', 'price', 'stock', 'sku', 'image_url'] diff --git a/products/views.py b/products/views.py index 74a0d61..7534620 100644 --- a/products/views.py +++ b/products/views.py @@ -15,7 +15,7 @@ class ProductListView(generics.ListAPIView): permission_classes = [permissions.AllowAny] def get_queryset(self): - queryset = Product.objects.filter(is_active=True) + queryset = Product.objects.all() category_slug = self.request.query_params.get('category', None) search_query = self.request.query_params.get('search', None) @@ -29,6 +29,6 @@ class ProductListView(generics.ListAPIView): return queryset class ProductDetailView(generics.RetrieveAPIView): - queryset = Product.objects.filter(is_active=True) + queryset = Product.objects.all() serializer_class = ProductSerializer permission_classes = [permissions.AllowAny]