diff --git a/api/views.py b/api/views.py index efd5d14..daf33a7 100644 --- a/api/views.py +++ b/api/views.py @@ -54,6 +54,9 @@ class UserProfileView(APIView): "last_name": user.last_name, "email": user.email, "phone": profile.phone if profile else user.username, + "gender": profile.gender if profile else "Prefer not to say", + "wallet_balance": float(profile.wallet_balance) if profile else 0.0, + "loyalty_coins": profile.loyalty_coins if profile else 200, "address": profile.default_address if profile else "" }) @@ -83,18 +86,27 @@ class UserProfileView(APIView): profile, _ = CustomerProfile.objects.get_or_create(user=user) if 'phone' in request.data: profile.phone = request.data['phone'] + if 'gender' in request.data: + profile.gender = request.data['gender'] + if 'wallet_balance' in request.data: + profile.wallet_balance = request.data['wallet_balance'] + if 'loyalty_coins' in request.data: + profile.loyalty_coins = request.data['loyalty_coins'] if 'address' in request.data: profile.default_address = request.data['address'] profile.save() - audit_logger.info(f"AUDIT - User Profile Updated: Username '{user.username}', Email '{user.email}'") + audit_logger.info(f"AUDIT - User Profile Updated: Username '{user.username}', Email '{user.email}', Gender '{profile.gender}'") return Response({ "message": "Profile updated successfully", "username": user.username, "email": user.email, "first_name": user.first_name, - "last_name": user.last_name + "last_name": user.last_name, + "gender": profile.gender, + "wallet_balance": float(profile.wallet_balance), + "loyalty_coins": profile.loyalty_coins }, status=status.HTTP_200_OK) def send_msg91_whatsapp_direct(mobile_number, otp_code, template_name="mobile_number_verification"): diff --git a/customerwebsitebackend/urls.py b/customerwebsitebackend/urls.py index c8fbb59..c2a71da 100644 --- a/customerwebsitebackend/urls.py +++ b/customerwebsitebackend/urls.py @@ -8,7 +8,10 @@ from django.urls import path from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from api.views import UserRegisterView, UserProfileView, SendWhatsAppOTPView, VerifyOTPView from products.views import CategoryListView, ProductListView, ProductDetailView -from orders.views import CartView, CartItemDetailView, CheckoutView, PaymentSuccessView, OrderHistoryView, OrderDetailView +from orders.views import ( + CartView, CartItemDetailView, CheckoutView, PaymentSuccessView, OrderHistoryView, OrderDetailView, + AddressView, AddressDetailView, WishlistView, WalletTransactionView, ProductReviewView +) urlpatterns = [ path('', home_view, name='home'), @@ -26,6 +29,7 @@ urlpatterns = [ path('api/v1/customer/categories/', CategoryListView.as_view(), name='categories'), path('api/v1/customer/products/', ProductListView.as_view(), name='products'), path('api/v1/customer/products//', ProductDetailView.as_view(), name='product-detail'), + path('api/v1/customer/reviews/', ProductReviewView.as_view(), name='reviews'), # Cart & Checkout APIs path('api/v1/customer/cart/', CartView.as_view(), name='cart'), @@ -36,4 +40,10 @@ urlpatterns = [ # Orders path('api/v1/customer/orders/', OrderHistoryView.as_view(), name='order-history'), path('api/v1/customer/orders//', OrderDetailView.as_view(), name='order-detail'), + + # Addresses, Wishlist, Wallet + path('api/v1/customer/addresses/', AddressView.as_view(), name='addresses'), + path('api/v1/customer/addresses//', AddressDetailView.as_view(), name='address-detail'), + path('api/v1/customer/wishlist/', WishlistView.as_view(), name='wishlist'), + path('api/v1/customer/wallet/', WalletTransactionView.as_view(), name='wallet'), ] diff --git a/orders/migrations/0003_customerprofile_gender_customerprofile_loyalty_coins_and_more.py b/orders/migrations/0003_customerprofile_gender_customerprofile_loyalty_coins_and_more.py new file mode 100644 index 0000000..353818d --- /dev/null +++ b/orders/migrations/0003_customerprofile_gender_customerprofile_loyalty_coins_and_more.py @@ -0,0 +1,95 @@ +# Generated by Django 6.1 on 2026-10-04 14:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0002_sellerorder_remove_cartitem_product_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='customerprofile', + name='gender', + field=models.CharField(blank=True, default='Prefer not to say', max_length=30), + ), + migrations.AddField( + model_name='customerprofile', + name='loyalty_coins', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='customerprofile', + name='wallet_balance', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12), + ), + migrations.AddField( + model_name='order', + name='cancellation_reason', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='order', + name='return_reason', + field=models.TextField(blank=True, null=True), + ), + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=150)), + ('phone', models.CharField(max_length=20)), + ('street', models.CharField(max_length=255)), + ('area', models.CharField(blank=True, max_length=255)), + ('city', models.CharField(max_length=100)), + ('state', models.CharField(max_length=100)), + ('pincode', models.CharField(max_length=10)), + ('address_type', models.CharField(default='Home', max_length=20)), + ('is_default', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ProductReview', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_id', models.IntegerField()), + ('rating', models.IntegerField(default=5)), + ('title', models.CharField(max_length=255)), + ('comment', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_reviews', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='WalletTransaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('transaction_id', models.CharField(max_length=50, unique=True)), + ('title', models.CharField(max_length=255)), + ('type', models.CharField(max_length=20)), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('status', models.CharField(default='Completed', max_length=50)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wallet_transactions', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Wishlist', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_id', models.IntegerField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wishlist_items', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'product_id')}, + }, + ), + ] diff --git a/orders/models.py b/orders/models.py index 8ebd8b7..7ca6e41 100644 --- a/orders/models.py +++ b/orders/models.py @@ -5,12 +5,65 @@ from products.models import Product class CustomerProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile', db_constraint=False) phone = models.CharField(max_length=20, blank=True) + gender = models.CharField(max_length=30, default='Prefer not to say', blank=True) + wallet_balance = models.DecimalField(max_digits=12, decimal_places=2, default=0.00) + loyalty_coins = models.IntegerField(default=00) default_address = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.user.username +class Address(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='addresses') + full_name = models.CharField(max_length=150) + phone = models.CharField(max_length=20) + street = models.CharField(max_length=255) + area = models.CharField(max_length=255, blank=True) + city = models.CharField(max_length=100) + state = models.CharField(max_length=100) + pincode = models.CharField(max_length=10) + address_type = models.CharField(max_length=20, default='Home') + is_default = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.full_name} - {self.city} ({self.address_type})" + +class WalletTransaction(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='wallet_transactions') + transaction_id = models.CharField(max_length=50, unique=True) + title = models.CharField(max_length=255) + type = models.CharField(max_length=20) # 'credit' or 'debit' + amount = models.DecimalField(max_digits=10, decimal_places=2) + status = models.CharField(max_length=50, default='Completed') + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.transaction_id} - {self.type} ₹{self.amount}" + +class Wishlist(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='wishlist_items') + product_id = models.IntegerField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('user', 'product_id') + + def __str__(self): + return f"Wishlist item {self.product_id} for {self.user.username}" + +class ProductReview(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='product_reviews') + product_id = models.IntegerField() + rating = models.IntegerField(default=5) + title = models.CharField(max_length=255) + comment = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Review by {self.user.username} - {self.rating} stars" + class Cart(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='cart', db_constraint=False) created_at = models.DateTimeField(auto_now_add=True) @@ -49,6 +102,8 @@ class Order(models.Model): total_amount = models.DecimalField(max_digits=10, decimal_places=2) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') shipping_address = models.TextField() + cancellation_reason = models.TextField(blank=True, null=True) + return_reason = models.TextField(blank=True, null=True) placed_at = models.DateTimeField(auto_now_add=True) def __str__(self): diff --git a/orders/serializers.py b/orders/serializers.py index 73b913c..4f3922b 100644 --- a/orders/serializers.py +++ b/orders/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Cart, CartItem, Order, SellerOrder +from .models import Cart, CartItem, Order, SellerOrder, Address, WalletTransaction, Wishlist, ProductReview from products.serializers import ProductSerializer class CartItemSerializer(serializers.ModelSerializer): @@ -36,4 +36,26 @@ class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order - fields = ['id', 'username', 'razorpay_order_id', 'razorpay_payment_id', 'total_amount', 'status', 'shipping_address', 'placed_at'] + fields = ['id', 'username', 'razorpay_order_id', 'razorpay_payment_id', 'total_amount', 'status', 'shipping_address', 'cancellation_reason', 'return_reason', 'placed_at'] + +class AddressSerializer(serializers.ModelSerializer): + class Meta: + model = Address + fields = ['id', 'full_name', 'phone', 'street', 'area', 'city', 'state', 'pincode', 'address_type', 'is_default', 'created_at'] + +class WalletTransactionSerializer(serializers.ModelSerializer): + class Meta: + model = WalletTransaction + fields = ['id', 'transaction_id', 'title', 'type', 'amount', 'status', 'created_at'] + +class WishlistSerializer(serializers.ModelSerializer): + class Meta: + model = Wishlist + fields = ['id', 'product_id', 'created_at'] + +class ProductReviewSerializer(serializers.ModelSerializer): + username = serializers.CharField(source='user.username', read_only=True) + + class Meta: + model = ProductReview + fields = ['id', 'username', 'product_id', 'rating', 'title', 'comment', 'created_at'] diff --git a/orders/views.py b/orders/views.py index 206768d..8070b91 100644 --- a/orders/views.py +++ b/orders/views.py @@ -189,11 +189,24 @@ class OrderHistoryView(APIView): return Response(response_data) class OrderDetailView(APIView): - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.AllowAny] + + def get_user(self, request): + if request.user and request.user.is_authenticated: + return request.user + username = request.query_params.get('username') or request.query_params.get('phone') + if username: + clean_digits = ''.join(c for c in str(username) if c.isdigit()) + raw_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits + from django.contrib.auth.models import User + from django.db.models import Q + return User.objects.filter(Q(username__icontains=raw_10) | Q(username=username)).first() + return None def get(self, request, pk): + user = self.get_user(request) try: - order = Order.objects.get(id=pk, user=request.user) + order = Order.objects.get(id=pk) if not user else Order.objects.get(id=pk, user=user) except Order.DoesNotExist: return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND) @@ -213,3 +226,207 @@ class OrderDetailView(APIView): }) return Response(order_data) + + def put(self, request, pk): + user = self.get_user(request) + try: + order = Order.objects.get(id=pk) if not user else Order.objects.get(id=pk, user=user) + except Order.DoesNotExist: + return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND) + + if 'status' in request.data: + order.status = request.data['status'] + if 'cancellation_reason' in request.data: + order.cancellation_reason = request.data['cancellation_reason'] + order.status = 'cancelled' + if 'return_reason' in request.data: + order.return_reason = request.data['return_reason'] + order.save() + return Response({"message": "Order updated successfully"}) + +class AddressView(APIView): + permission_classes = [permissions.AllowAny] + + def get_user(self, request): + if request.user and request.user.is_authenticated: + return request.user + username = request.query_params.get('username') or request.query_params.get('phone') or request.data.get('username') or request.data.get('phone') + if username: + clean_digits = ''.join(c for c in str(username) if c.isdigit()) + raw_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits + from django.contrib.auth.models import User + from django.db.models import Q + return User.objects.filter(Q(username__icontains=raw_10) | Q(username=username)).first() + return None + + def get(self, request): + user = self.get_user(request) + if not user: + return Response([], status=status.HTTP_200_OK) + from .models import Address + from .serializers import AddressSerializer + addresses = Address.objects.filter(user=user).order_by('-is_default', '-created_at') + serializer = AddressSerializer(addresses, many=True) + return Response(serializer.data) + + def post(self, request): + user = self.get_user(request) + if not user: + return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND) + from .models import Address + from .serializers import AddressSerializer + is_default = request.data.get('is_default', False) + if is_default: + Address.objects.filter(user=user).update(is_default=False) + address = Address.objects.create( + user=user, + full_name=request.data.get('full_name', user.username), + phone=request.data.get('phone', user.username), + street=request.data.get('street', ''), + area=request.data.get('area', ''), + city=request.data.get('city', ''), + state=request.data.get('state', ''), + pincode=request.data.get('pincode', ''), + address_type=request.data.get('address_type', 'Home'), + is_default=is_default + ) + serializer = AddressSerializer(address) + return Response(serializer.data, status=status.HTTP_201_CREATED) + +class AddressDetailView(APIView): + permission_classes = [permissions.AllowAny] + + def delete(self, request, pk): + from .models import Address + Address.objects.filter(id=pk).delete() + return Response({"message": "Address deleted"}) + +class WishlistView(APIView): + permission_classes = [permissions.AllowAny] + + def get_user(self, request): + if request.user and request.user.is_authenticated: + return request.user + username = request.query_params.get('username') or request.query_params.get('phone') or request.data.get('username') or request.data.get('phone') + if username: + clean_digits = ''.join(c for c in str(username) if c.isdigit()) + raw_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits + from django.contrib.auth.models import User + from django.db.models import Q + return User.objects.filter(Q(username__icontains=raw_10) | Q(username=username)).first() + return None + + def get(self, request): + user = self.get_user(request) + if not user: + return Response([], status=status.HTTP_200_OK) + from .models import Wishlist + items = Wishlist.objects.filter(user=user).values_list('product_id', flat=True) + return Response(list(items)) + + def post(self, request): + user = self.get_user(request) + if not user: + return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND) + product_id = request.data.get('product_id') + from .models import Wishlist + item, created = Wishlist.objects.get_or_create(user=user, product_id=product_id) + if not created: + item.delete() + return Response({"message": "Removed from wishlist", "in_wishlist": False}) + return Response({"message": "Added to wishlist", "in_wishlist": True}) + +class WalletTransactionView(APIView): + permission_classes = [permissions.AllowAny] + + def get_user(self, request): + if request.user and request.user.is_authenticated: + return request.user + username = request.query_params.get('username') or request.query_params.get('phone') or request.data.get('username') or request.data.get('phone') + if username: + clean_digits = ''.join(c for c in str(username) if c.isdigit()) + raw_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits + from django.contrib.auth.models import User + from django.db.models import Q + return User.objects.filter(Q(username__icontains=raw_10) | Q(username=username)).first() + return None + + def get(self, request): + user = self.get_user(request) + if not user: + return Response([], status=status.HTTP_200_OK) + from .models import WalletTransaction + from .serializers import WalletTransactionSerializer + txns = WalletTransaction.objects.filter(user=user).order_by('-created_at') + serializer = WalletTransactionSerializer(txns, many=True) + return Response(serializer.data) + + def post(self, request): + user = self.get_user(request) + if not user: + return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND) + from .models import WalletTransaction, CustomerProfile + from .serializers import WalletTransactionSerializer + import uuid + tx_type = request.data.get('type', 'credit') + amount = float(request.data.get('amount', 0)) + tx_id = f"TXN-{uuid.uuid4().hex[:8].upper()}" + + tx = WalletTransaction.objects.create( + user=user, + transaction_id=tx_id, + title=request.data.get('title', f"Wallet {tx_type.capitalize()}"), + type=tx_type, + amount=amount, + status='Completed' + ) + + profile, _ = CustomerProfile.objects.get_or_create(user=user) + if tx_type == 'credit': + profile.wallet_balance += amount + else: + profile.wallet_balance = max(0, profile.wallet_balance - amount) + profile.save() + + serializer = WalletTransactionSerializer(tx) + return Response(serializer.data, status=status.HTTP_201_CREATED) + +class ProductReviewView(APIView): + permission_classes = [permissions.AllowAny] + + def get(self, request): + product_id = request.query_params.get('product_id') + from .models import ProductReview + from .serializers import ProductReviewSerializer + qs = ProductReview.objects.all().order_by('-created_at') + if product_id: + qs = qs.filter(product_id=product_id) + serializer = ProductReviewSerializer(qs, many=True) + return Response(serializer.data) + + def post(self, request): + username = request.data.get('username') or request.data.get('phone') + user = None + if request.user and request.user.is_authenticated: + user = request.user + elif username: + clean_digits = ''.join(c for c in str(username) if c.isdigit()) + raw_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits + from django.contrib.auth.models import User + from django.db.models import Q + user = User.objects.filter(Q(username__icontains=raw_10) | Q(username=username)).first() + + if not user: + return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND) + + from .models import ProductReview + from .serializers import ProductReviewSerializer + review = ProductReview.objects.create( + user=user, + product_id=request.data.get('product_id'), + rating=int(request.data.get('rating', 5)), + title=request.data.get('title', ''), + comment=request.data.get('comment', '') + ) + serializer = ProductReviewSerializer(review) + return Response(serializer.data, status=status.HTTP_201_CREATED)