feat: add cart, checkout, payment, and order management views with serialization and routing
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 23s

This commit is contained in:
vickytechkey 2026-10-04 20:26:09 +05:30
parent c45ec9511c
commit b73cab8f92
6 changed files with 418 additions and 7 deletions

View file

@ -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"):

View file

@ -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/<int:pk>/', 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/<int:pk>/', OrderDetailView.as_view(), name='order-detail'),
# Addresses, Wishlist, Wallet
path('api/v1/customer/addresses/', AddressView.as_view(), name='addresses'),
path('api/v1/customer/addresses/<int:pk>/', 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'),
]

View file

@ -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')},
},
),
]

View file

@ -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):

View file

@ -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']

View file

@ -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)