All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 39s
1051 lines
48 KiB
Python
1051 lines
48 KiB
Python
import json
|
|
import requests
|
|
from django.conf import settings
|
|
from django.db.models import Q
|
|
import logging
|
|
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, 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]
|
|
|
|
def get_cart(self, user):
|
|
cart, created = Cart.objects.get_or_create(user=user)
|
|
return cart
|
|
|
|
def get(self, request):
|
|
cart = self.get_cart(request.user)
|
|
serializer = CartSerializer(cart)
|
|
return Response(serializer.data)
|
|
|
|
def post(self, request):
|
|
cart = self.get_cart(request.user)
|
|
product_id = request.data.get('product_id')
|
|
quantity = int(request.data.get('quantity', 1))
|
|
|
|
try:
|
|
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_id=product_id,
|
|
defaults={'price_at_add': product.price, 'quantity': 0}
|
|
)
|
|
cart_item.quantity += quantity
|
|
cart_item.price_at_add = product.price
|
|
cart_item.save()
|
|
|
|
return Response({"message": "Item added to cart"})
|
|
|
|
class CartItemDetailView(APIView):
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def put(self, request, pk):
|
|
try:
|
|
item = CartItem.objects.get(id=pk, cart__user=request.user)
|
|
except CartItem.DoesNotExist:
|
|
return Response({"error": "Cart item not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
quantity = int(request.data.get('quantity', 1))
|
|
if quantity <= 0:
|
|
item.delete()
|
|
return Response({"message": "Item removed from cart"})
|
|
|
|
item.quantity = quantity
|
|
item.save()
|
|
return Response({"message": "Cart updated"})
|
|
|
|
def delete(self, request, pk):
|
|
try:
|
|
item = CartItem.objects.get(id=pk, cart__user=request.user)
|
|
except CartItem.DoesNotExist:
|
|
return Response({"error": "Cart item not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
item.delete()
|
|
return Response({"message": "Item removed from cart"})
|
|
|
|
class CheckoutView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get_user(self, request):
|
|
if request.user and request.user.is_authenticated:
|
|
return request.user
|
|
username = request.data.get('username') or request.query_params.get('username') or request.data.get('phone') 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
|
|
usr = User.objects.filter(Q(username__icontains=raw_10) | Q(username=username)).first()
|
|
if usr:
|
|
return usr
|
|
# Auto-create user if missing
|
|
return User.objects.create_user(username=clean_digits or username, password='TradhoxOtpUser2026!')
|
|
return None
|
|
|
|
def post(self, request):
|
|
user = self.get_user(request)
|
|
if not user:
|
|
return Response({"error": "User identification or phone is required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
shipping_address = request.data.get('shipping_address')
|
|
if not shipping_address:
|
|
return Response({"error": "Shipping address is required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
total_amount = float(request.data.get('total_amount') or 0.0)
|
|
items_payload = request.data.get('items', [])
|
|
|
|
# 1. Fetch items from DB cart or payload items
|
|
cart_items = []
|
|
try:
|
|
cart = Cart.objects.get(user=user)
|
|
cart_items = list(cart.items.all())
|
|
except Cart.DoesNotExist:
|
|
pass
|
|
|
|
if not total_amount and cart_items:
|
|
total_amount = sum(item.quantity * item.price_at_add for item in cart_items)
|
|
|
|
if not total_amount and items_payload:
|
|
total_amount = sum(float(it.get('price', 0)) * int(it.get('quantity', 1)) for it in items_payload)
|
|
|
|
razorpay_order_id = f"order_{uuid.uuid4().hex[:14]}"
|
|
|
|
# Create Order in PostgreSQL database
|
|
order = Order.objects.create(
|
|
user=user,
|
|
total_amount=total_amount or 999.0,
|
|
status='pending',
|
|
razorpay_order_id=razorpay_order_id,
|
|
shipping_address=shipping_address,
|
|
shipping_carrier='Pending',
|
|
shipping_status='Pending Acceptance'
|
|
)
|
|
audit_logger.info(f"AUDIT - Checkout: User '{user.username}' created order ID {order.id} for amount ${total_amount:.2f}.")
|
|
|
|
raw_pm = str(request.data.get('payment_method') or request.data.get('payment_mode') or '').upper()
|
|
payment_mode = 'COD' if ('COD' in raw_pm or 'CASH' in raw_pm) else 'Prepaid'
|
|
|
|
# Write to shared SellerOrder (api_order) table
|
|
created_seller_orders = False
|
|
if cart_items:
|
|
for item in cart_items:
|
|
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',
|
|
carrier='Pending',
|
|
tracking='Pending',
|
|
payment_mode=payment_mode
|
|
)
|
|
created_seller_orders = True
|
|
CartItem.objects.filter(cart__user=user).delete()
|
|
|
|
if not created_seller_orders and items_payload:
|
|
for it in items_payload:
|
|
prod_title = it.get('name') or it.get('title') or 'Handcrafted Product'
|
|
prod_price = float(it.get('price', total_amount))
|
|
prod_qty = int(it.get('quantity', 1))
|
|
prod_id = it.get('id') or 1
|
|
supplier_id = it.get('supplier_id')
|
|
|
|
# Try looking up product in DB to find accurate supplier_id
|
|
if not supplier_id or supplier_id == 26:
|
|
matched_prod = Product.objects.filter(Q(id=prod_id) | Q(title__iexact=prod_title) | Q(sku__iexact=str(prod_id))).first()
|
|
if matched_prod and matched_prod.supplier_id:
|
|
supplier_id = matched_prod.supplier_id
|
|
if not supplier_id:
|
|
supplier_id = 26
|
|
|
|
SellerOrder.objects.create(
|
|
supplier_id=supplier_id,
|
|
order_id=f"{order.id}-{prod_id}",
|
|
date=now().date(),
|
|
item=prod_title,
|
|
quantity=prod_qty,
|
|
customer=user.username,
|
|
total=prod_price * prod_qty,
|
|
status='Pending Acceptance',
|
|
carrier='Pending',
|
|
tracking='Pending',
|
|
payment_mode=payment_mode
|
|
)
|
|
created_seller_orders = True
|
|
|
|
if not created_seller_orders:
|
|
# Fallback seller order
|
|
first_prod = Product.objects.first()
|
|
supp_id = first_prod.supplier_id if (first_prod and first_prod.supplier_id) else 26
|
|
SellerOrder.objects.create(
|
|
supplier_id=supp_id,
|
|
order_id=f"{order.id}-1",
|
|
date=now().date(),
|
|
item="Artisan Heritage Creation",
|
|
quantity=1,
|
|
customer=user.username,
|
|
total=total_amount or 999.0,
|
|
status='Pending Acceptance',
|
|
carrier='Pending',
|
|
tracking='Pending',
|
|
payment_mode=payment_mode
|
|
)
|
|
|
|
return Response({
|
|
"message": "Order placed successfully and saved to database",
|
|
"order_id": order.id,
|
|
"id": order.id,
|
|
"razorpay_order_id": razorpay_order_id,
|
|
"total_amount": float(total_amount),
|
|
"status": "pending"
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
class PaymentSuccessView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def post(self, request):
|
|
order_id = request.data.get('order_id')
|
|
payment_id = request.data.get('razorpay_payment_id')
|
|
|
|
try:
|
|
order = Order.objects.get(id=order_id)
|
|
except Order.DoesNotExist:
|
|
return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
order.status = 'confirmed'
|
|
order.razorpay_payment_id = payment_id
|
|
order.save()
|
|
audit_logger.info(f"AUDIT - Payment Success: User '{order.user.username}' successfully paid order ID {order.id}.")
|
|
|
|
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):
|
|
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 all orders if general query, or empty list if unassigned
|
|
orders = Order.objects.all().order_by('-placed_at')[:20]
|
|
else:
|
|
orders = Order.objects.filter(user=user).order_by('-placed_at')
|
|
serializer = OrderSerializer(orders, many=True)
|
|
|
|
# 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'] = []
|
|
assigned_carrier = None
|
|
assigned_tracking = None
|
|
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
|
|
})
|
|
if s_ord.carrier and s_ord.carrier != 'Pending':
|
|
assigned_carrier = s_ord.carrier
|
|
if s_ord.tracking and s_ord.tracking != 'Pending':
|
|
assigned_tracking = s_ord.tracking
|
|
|
|
raw_awb = order_data.get('awb_number') or assigned_tracking
|
|
if raw_awb:
|
|
order_data['awb_number'] = raw_awb
|
|
if str(raw_awb).isdigit() or str(raw_awb).startswith(('698', 'DELHI', '14', '12', '179', '139', '142')):
|
|
assigned_carrier = 'Delhivery Logistics'
|
|
elif str(raw_awb).startswith(('EKART', 'ELIT')):
|
|
assigned_carrier = 'Ekart Logistics'
|
|
elif str(raw_awb).startswith('SFX'):
|
|
assigned_carrier = 'Shadowfax Express'
|
|
|
|
if assigned_carrier:
|
|
order_data['shipping_carrier'] = 'Express Delivery'
|
|
|
|
# Aggregate status
|
|
if order_data.get('status') == 'cancelled':
|
|
pass # Already marked cancelled
|
|
elif seller_orders.exists():
|
|
statuses = [o.status for o in seller_orders]
|
|
if 'Cancelled' in statuses or 'cancelled' in statuses:
|
|
order_data['status'] = 'cancelled'
|
|
elif '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.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) 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)
|
|
|
|
serializer = OrderSerializer(order)
|
|
order_data = serializer.data
|
|
|
|
seller_orders = SellerOrder.objects.filter(order_id__startswith=f"{pk}-")
|
|
order_data['items'] = []
|
|
assigned_carrier = None
|
|
assigned_tracking = None
|
|
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
|
|
})
|
|
if s_ord.carrier and s_ord.carrier != 'Pending':
|
|
assigned_carrier = s_ord.carrier
|
|
if s_ord.tracking and s_ord.tracking != 'Pending':
|
|
assigned_tracking = s_ord.tracking
|
|
|
|
raw_awb = order_data.get('awb_number') or assigned_tracking
|
|
if raw_awb:
|
|
order_data['awb_number'] = raw_awb
|
|
if str(raw_awb).isdigit() or str(raw_awb).startswith(('698', 'DELHI', '14', '12', '179', '139', '142')):
|
|
assigned_carrier = 'Delhivery Logistics'
|
|
elif str(raw_awb).startswith(('EKART', 'ELIT')):
|
|
assigned_carrier = 'Ekart Logistics'
|
|
elif str(raw_awb).startswith('SFX'):
|
|
assigned_carrier = 'Shadowfax Express'
|
|
|
|
if assigned_carrier:
|
|
order_data['shipping_carrier'] = 'Express Delivery'
|
|
|
|
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)
|
|
|
|
|
|
SHIPPING_SERVICE_URL = getattr(settings, 'SHIPPING_SERVICE_URL', 'http://127.0.0.1:8000')
|
|
|
|
def assign_shadowfax_shipping(order, user, shipping_address_str, items_list=None, payment_mode='Prepaid'):
|
|
client_order_id = f"ORD-CUST-{order.id}"
|
|
pincode = '560077'
|
|
city = 'Bengaluru'
|
|
import re
|
|
pin_match = re.search(r'\b\d{6}\b', str(shipping_address_str))
|
|
if pin_match:
|
|
pincode = pin_match.group(0)
|
|
|
|
prod_details = []
|
|
if items_list:
|
|
for it in items_list:
|
|
prod_title = getattr(it.product, 'title', f"Product-{it.product_id}") if hasattr(it, 'product') and it.product else f"Product-{it.product_id}"
|
|
prod_details.append({
|
|
'sku_name': prod_title,
|
|
'sku_code': f"SKU-{it.product_id}",
|
|
'price': float(it.price_at_add),
|
|
'quantity': it.quantity
|
|
})
|
|
else:
|
|
prod_details = [{'sku_name': 'Artisan Craft Package', 'sku_code': 'SKU-001', 'price': float(order.total_amount), 'quantity': 1}]
|
|
|
|
payload = {
|
|
'env': 'production',
|
|
'use_mock': False,
|
|
'payload': {
|
|
'order_details': {
|
|
'client_order_id': client_order_id,
|
|
'actual_weight': 500.0,
|
|
'product_value': float(order.total_amount),
|
|
'payment_mode': payment_mode,
|
|
'order_type': 'marketplace'
|
|
},
|
|
'customer_details': {
|
|
'name': getattr(user, 'first_name', '') or getattr(user, 'username', 'Customer'),
|
|
'phone': getattr(user, 'username', '9876543210'),
|
|
'address_line_1': str(shipping_address_str),
|
|
'pincode': pincode,
|
|
'city': city,
|
|
'state': 'Karnataka'
|
|
},
|
|
'pickup_details': {
|
|
'name': 'Tradhox Central Hub',
|
|
'phone': '9123456789',
|
|
'address_line_1': 'Plot 12 Industrial Area Phase 2',
|
|
'pincode': '560077',
|
|
'city': 'Bengaluru',
|
|
'state': 'Karnataka'
|
|
},
|
|
'product_details': prod_details
|
|
}
|
|
}
|
|
|
|
try:
|
|
url = f"{SHIPPING_SERVICE_URL.rstrip('/')}/api/v1/forward/create-order/"
|
|
resp = requests.post(url, json=payload, timeout=5)
|
|
if resp.status_code in [200, 201]:
|
|
data = resp.json()
|
|
awb = None
|
|
if isinstance(data, dict):
|
|
if 'data' in data and isinstance(data['data'], dict) and 'awb_number' in data['data']:
|
|
awb = data['data']['awb_number']
|
|
elif 'response' in data:
|
|
try:
|
|
parsed = json.loads(data['response']) if isinstance(data['response'], str) else data['response']
|
|
if isinstance(parsed, dict) and 'data' in parsed and 'awb_number' in parsed['data']:
|
|
awb = parsed['data']['awb_number']
|
|
except Exception:
|
|
pass
|
|
if not awb:
|
|
import time
|
|
awb = f"SFX{int(time.time())}01"
|
|
|
|
order.awb_number = awb
|
|
order.shipping_carrier = 'Shadowfax'
|
|
order.shipping_status = 'new'
|
|
order.save()
|
|
|
|
# Update SellerOrders tracking
|
|
SellerOrder.objects.filter(order_id__startswith=f"{order.id}-").update(tracking=awb, carrier='Shadowfax')
|
|
return awb
|
|
except Exception as e:
|
|
audit_logger.warning(f"Shadowfax order assignment fallback notice: {e}")
|
|
|
|
import time
|
|
awb = f"SFX{int(time.time())}01"
|
|
order.awb_number = awb
|
|
order.shipping_carrier = 'Shadowfax'
|
|
order.shipping_status = 'new'
|
|
order.save()
|
|
SellerOrder.objects.filter(order_id__startswith=f"{order.id}-").update(tracking=awb, carrier='Shadowfax')
|
|
return awb
|
|
|
|
|
|
class OrderCancelView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get_user(self, request):
|
|
if request.user and request.user.is_authenticated:
|
|
return request.user
|
|
username = request.data.get('username') or 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 post(self, request, pk=None):
|
|
raw_order_id = pk or request.data.get('order_id')
|
|
reason = request.data.get('reason', 'Customer requested cancellation prior to shipping')
|
|
user = self.get_user(request)
|
|
|
|
if not raw_order_id:
|
|
return Response({'error': 'order_id is required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Parse numeric ID if passed as formatted string (e.g., "TRDX-ORD-1" -> 1)
|
|
import re
|
|
numeric_id = None
|
|
if isinstance(raw_order_id, str):
|
|
digits = re.sub(r'[^0-9]', '', raw_order_id)
|
|
if digits:
|
|
numeric_id = int(digits)
|
|
elif isinstance(raw_order_id, int):
|
|
numeric_id = raw_order_id
|
|
|
|
order = None
|
|
from django.db.models import Q
|
|
try:
|
|
qs = Order.objects.all()
|
|
if user:
|
|
qs = qs.filter(user=user)
|
|
|
|
if numeric_id:
|
|
order = qs.filter(id=numeric_id).first()
|
|
if not order:
|
|
order = qs.filter(Q(razorpay_order_id=str(raw_order_id)) | Q(awb_number=str(raw_order_id))).first()
|
|
except Exception as e:
|
|
audit_logger.warning(f"OrderCancelView lookup error: {e}")
|
|
|
|
if not order:
|
|
return Response({'error': f'Order {raw_order_id} not found'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
st_low = str(order.status or '').lower()
|
|
ship_st_low = str(getattr(order, 'shipping_status', '') or '').lower()
|
|
in_transit_states = ['shipped', 'delivered', 'in transit', 'in_transit', 'out for delivery']
|
|
if st_low in in_transit_states or ship_st_low in in_transit_states:
|
|
return Response({
|
|
'error': 'Order has already been scanned and is in transit with courier. Cancellation is not permitted.'
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if order.status.lower() == 'cancelled':
|
|
SellerOrder.objects.filter(
|
|
Q(order_id__startswith=f"{order.id}-") |
|
|
Q(order_id=str(order.id)) |
|
|
Q(order_id=str(raw_order_id))
|
|
).update(status='Cancelled')
|
|
return Response({'message': 'Order is already cancelled', 'order': OrderSerializer(order).data})
|
|
|
|
# 1. Fetch matching seller orders for this customer order
|
|
matching_seller_orders = list(SellerOrder.objects.filter(
|
|
Q(order_id__startswith=f"{order.id}-") |
|
|
Q(order_id=str(order.id)) |
|
|
Q(order_id=str(raw_order_id))
|
|
))
|
|
|
|
# 2. Cancel pickups with courier API for each registered seller order / AWB
|
|
cancel_awbs = set()
|
|
if order.awb_number:
|
|
cancel_awbs.add((f"{order.id}", order.awb_number))
|
|
|
|
for s_ord in matching_seller_orders:
|
|
s_awb = getattr(s_ord, 'tracking', None) or order.awb_number
|
|
s_id = getattr(s_ord, 'order_id', str(order.id))
|
|
cancel_awbs.add((s_id, s_awb))
|
|
|
|
sfx_token = "7328f980a6eca01fedf37a63e39e44893481f856"
|
|
sfx_cancel_url = "https://dale.shadowfax.in/api/v3/clients/orders/cancel/"
|
|
sfx_headers = {
|
|
'Authorization': f'Token {sfx_token}',
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
}
|
|
|
|
for c_id, awb_val in cancel_awbs:
|
|
try:
|
|
# Delhivery cancellation if numerical / Delhivery format
|
|
if awb_val and (str(awb_val).isdigit() or str(awb_val).startswith(('698', 'DELHI', '14', '12'))):
|
|
try:
|
|
dlh_token = "e70dfd63665335ee6e0a1094f6d34e17a126f5a6"
|
|
dlh_url = "https://track.delhivery.com/api/p/edit"
|
|
requests.post(dlh_url, json={"waybill": str(awb_val), "cancellation": "true"}, headers={'Authorization': f'Token {dlh_token}', 'Content-Type': 'application/json'}, timeout=6)
|
|
except Exception as dlh_err:
|
|
audit_logger.warning(f"Delhivery cancel API warning for order {c_id}: {dlh_err}")
|
|
# Also try microservice endpoint first
|
|
try:
|
|
url = f"{SHIPPING_SERVICE_URL.rstrip('/')}/api/v1/forward/cancel/"
|
|
requests.post(url, json={
|
|
'env': 'production',
|
|
'use_mock': False,
|
|
'payload': {'client_order_id': c_id, 'request_id': c_id, 'awb_number': awb_val or '', 'reason': reason}
|
|
}, timeout=3)
|
|
except Exception:
|
|
pass
|
|
|
|
# Direct Shadowfax production API call
|
|
requests.post(sfx_cancel_url, json={
|
|
'request_id': c_id,
|
|
'client_order_id': c_id,
|
|
'awb_number': awb_val or '',
|
|
'cancellation_reason': reason,
|
|
'reason': reason
|
|
}, headers=sfx_headers, timeout=6)
|
|
except Exception as e:
|
|
audit_logger.warning(f"Shadowfax cancel API warning for order {c_id}: {e}")
|
|
|
|
# 3. Mark customer order and seller orders as cancelled in DB
|
|
order.status = 'cancelled'
|
|
order.cancellation_reason = reason
|
|
order.shipping_status = 'cancelled'
|
|
order.save()
|
|
|
|
SellerOrder.objects.filter(
|
|
Q(order_id__startswith=f"{order.id}-") |
|
|
Q(order_id=str(order.id)) |
|
|
Q(order_id=str(raw_order_id))
|
|
).update(status='Cancelled')
|
|
|
|
audit_logger.info(f"AUDIT - Order Cancelled: Order ID {order.id} cancelled. Shadowfax pickups cancelled for {cancel_awbs}. Reason: '{reason}'.")
|
|
|
|
return Response({
|
|
'message': 'Order cancelled successfully and Shadowfax logistics updated',
|
|
'order_id': order.id,
|
|
'status': 'cancelled',
|
|
'cancellation_reason': reason
|
|
})
|
|
|
|
|
|
class OrderTrackView(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')
|
|
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=None):
|
|
order_id = pk or request.query_params.get('order_id') or request.query_params.get('id')
|
|
awb_query = request.query_params.get('awb')
|
|
user = self.get_user(request)
|
|
|
|
order = None
|
|
if order_id:
|
|
import re
|
|
clean_digits = re.sub(r'[^0-9]', '', str(order_id))
|
|
if clean_digits:
|
|
try:
|
|
order = Order.objects.get(id=int(clean_digits))
|
|
except (Order.DoesNotExist, ValueError):
|
|
pass
|
|
if not order:
|
|
try:
|
|
order = Order.objects.get(id=order_id)
|
|
except (Order.DoesNotExist, ValueError):
|
|
pass
|
|
|
|
if not order and awb_query:
|
|
try:
|
|
order = Order.objects.get(awb_number=awb_query)
|
|
except Order.DoesNotExist:
|
|
pass
|
|
|
|
seller_order = None
|
|
if order:
|
|
seller_order = SellerOrder.objects.filter(Q(order_id__startswith=f"{order.id}-") | Q(order_id=str(order.id))).first()
|
|
elif order_id:
|
|
seller_order = SellerOrder.objects.filter(order_id=str(order_id)).first()
|
|
|
|
awb_number = (order.awb_number if order and order.awb_number else None) or (seller_order.tracking if seller_order and seller_order.tracking != 'Pending' else None) or awb_query or None
|
|
raw_status = (order.status if order else 'pending').lower()
|
|
|
|
# Dynamic carrier identification
|
|
detected_carrier = None
|
|
if order and order.shipping_carrier and order.shipping_carrier not in ('Pending', ''):
|
|
detected_carrier = order.shipping_carrier
|
|
elif seller_order and seller_order.carrier and seller_order.carrier not in ('Pending', ''):
|
|
detected_carrier = seller_order.carrier
|
|
elif awb_number:
|
|
if str(awb_number).isdigit() or str(awb_number).startswith(('698', 'DELHI', '14', '12', '179', '139', '142')):
|
|
detected_carrier = 'Delhivery Logistics'
|
|
elif str(awb_number).startswith(('EKART', 'ELIT')):
|
|
detected_carrier = 'Ekart Logistics'
|
|
elif str(awb_number).startswith('SFX'):
|
|
detected_carrier = 'Shadowfax Express'
|
|
|
|
if not detected_carrier:
|
|
detected_carrier = 'Delhivery Logistics' if (awb_number and str(awb_number).isdigit()) else ('Shadowfax Express' if awb_number else 'Awaiting Dispatch')
|
|
|
|
# Call shippingservice track API if AWB is present
|
|
live_data = None
|
|
if awb_number:
|
|
try:
|
|
url = f"{SHIPPING_SERVICE_URL.rstrip('/')}/api/v1/forward/track/?awb={awb_number}"
|
|
resp = requests.get(url, timeout=5)
|
|
if resp.status_code == 200:
|
|
res = resp.json()
|
|
if 'response' in res:
|
|
try:
|
|
live_data = json.loads(res['response']) if isinstance(res['response'], str) else res['response']
|
|
except Exception:
|
|
live_data = res
|
|
else:
|
|
live_data = res
|
|
except Exception:
|
|
pass
|
|
|
|
# Direct Delhivery live tracking check if shippingservice data is empty or upstream format
|
|
if (not live_data or 'data' not in live_data or not live_data.get('data', {}).get('tracking_history')) and (str(awb_number).isdigit() or str(awb_number).startswith(('698', 'DELHI', '14', '12'))):
|
|
try:
|
|
dlh_token = 'e70dfd63665335ee6e0a1094f6d34e17a126f5a6'
|
|
dlh_url = f"https://track.delhivery.com/api/v1/packages/json/?waybill={awb_number}"
|
|
dlh_resp = requests.get(dlh_url, headers={'Authorization': f'Token {dlh_token}'}, timeout=6)
|
|
if dlh_resp.status_code == 200:
|
|
dlh_data = dlh_resp.json()
|
|
shipments = dlh_data.get('ShipmentData', [])
|
|
if shipments:
|
|
ship = shipments[0].get('Shipment', {})
|
|
st_info = ship.get('Status', {})
|
|
curr_st = st_info.get('Status') or 'In Transit'
|
|
scans = ship.get('Scans', [])
|
|
formatted_history = []
|
|
for sc in scans:
|
|
scan_detail = sc.get('ScanDetail', {})
|
|
raw_scan = scan_detail.get('Scan', '')
|
|
instr = scan_detail.get('Instructions', '')
|
|
combined = (raw_scan + ' ' + instr).lower()
|
|
st_key = 'manifested' if 'manifest' in combined else ('picked_up' if 'picked up' in combined else ('in_transit' if 'transit' in combined or 'bag' in combined or 'center' in combined else raw_scan.lower().replace(' ', '_')))
|
|
formatted_history.append({
|
|
'status': st_key,
|
|
'status_display': instr or raw_scan,
|
|
'remarks': instr,
|
|
'location': scan_detail.get('ScannedLocation') or 'Hub',
|
|
'timestamp': scan_detail.get('ScanDateTime') or scan_detail.get('StatusDateTime')
|
|
})
|
|
norm_st = 'delivered' if 'delivered' in curr_st.lower() else ('out_for_delivery' if 'out for delivery' in curr_st.lower() else ('in_transit' if 'transit' in curr_st.lower() or any(h['status'] == 'picked_up' for h in formatted_history) else curr_st.lower().replace(' ', '_')))
|
|
live_data = {
|
|
'status': 'success',
|
|
'carrier': 'Express Delivery',
|
|
'data': {
|
|
'carrier': 'Express Delivery',
|
|
'awb_number': str(awb_number),
|
|
'current_status': norm_st,
|
|
'status_display': f"{curr_st}" + (f" ({st_info.get('Instructions', '')})" if st_info.get('Instructions') else ""),
|
|
'expected_delivery_date': ship.get('ExpectedDeliveryDate'),
|
|
'picked_up_date': ship.get('PickedupDate') or ship.get('PickUpDate'),
|
|
'tracking_history': formatted_history,
|
|
'order_details': {
|
|
'carrier': 'Express Delivery',
|
|
'awb_number': str(awb_number),
|
|
'origin': ship.get('Origin'),
|
|
'destination': ship.get('Destination'),
|
|
'expected_delivery_date': ship.get('ExpectedDeliveryDate'),
|
|
'picked_up_date': ship.get('PickedupDate') or ship.get('PickUpDate')
|
|
}
|
|
}
|
|
}
|
|
except Exception as e:
|
|
audit_logger.warning(f"Direct Delhivery track check notice: {e}")
|
|
|
|
# Update database order status if carrier confirms picked up or in transit
|
|
is_picked_up_live = False
|
|
if live_data and isinstance(live_data.get('data'), dict):
|
|
cur = str(live_data['data'].get('current_status') or '').lower()
|
|
th = live_data['data'].get('tracking_history') or []
|
|
if any(cur == s or s in cur for s in ['picked_up', 'in_transit', 'shipped', 'delivered', 'out_for_delivery']):
|
|
is_picked_up_live = True
|
|
elif any(h.get('status') in ['picked_up', 'in_transit'] for h in th):
|
|
is_picked_up_live = True
|
|
|
|
if is_picked_up_live and order:
|
|
if order.status.lower() in ['pending', 'confirmed', 'ready to ship', 'ready_to_ship']:
|
|
order.status = 'shipped'
|
|
order.shipping_status = 'in_transit'
|
|
if detected_carrier:
|
|
order.shipping_carrier = detected_carrier
|
|
order.save()
|
|
if seller_order and seller_order.status in ['Pending Acceptance', 'Ready to Ship']:
|
|
SellerOrder.objects.filter(Q(order_id__startswith=f"{order.id}-") | Q(order_id=str(order.id))).update(status='Shipped')
|
|
|
|
if not live_data or 'data' not in live_data:
|
|
placed_ts = order.placed_at.strftime('%Y-%m-%dT%H:%M:%SZ') if (order and getattr(order, 'placed_at', None)) else '2026-10-09T10:00:00Z'
|
|
|
|
tracking_history = [
|
|
{'status': 'new', 'timestamp': placed_ts, 'location': 'Customer Store'}
|
|
]
|
|
rider_details = None
|
|
|
|
if raw_status in ['confirmed', 'accepted', 'ready to ship', 'ready_to_ship']:
|
|
tracking_history.append({'status': 'accepted', 'timestamp': placed_ts, 'location': 'Seller Store'})
|
|
display_status = 'Accepted' if raw_status in ['confirmed', 'accepted'] else 'Ready to Ship'
|
|
elif raw_status in ['shipped', 'dispatched', 'in_transit']:
|
|
tracking_history.append({'status': 'accepted', 'timestamp': placed_ts, 'location': 'Seller Store'})
|
|
tracking_history.append({'status': 'picked_up', 'timestamp': placed_ts, 'location': 'Logistics Hub'})
|
|
display_status = 'Shipped'
|
|
elif raw_status in ['out_for_delivery']:
|
|
tracking_history.append({'status': 'accepted', 'timestamp': placed_ts, 'location': 'Seller Store'})
|
|
tracking_history.append({'status': 'picked_up', 'timestamp': placed_ts, 'location': 'Logistics Hub'})
|
|
tracking_history.append({'status': 'out_for_delivery', 'timestamp': placed_ts, 'location': 'Destination Hub'})
|
|
display_status = 'Out for Delivery'
|
|
elif raw_status in ['delivered']:
|
|
tracking_history.append({'status': 'accepted', 'timestamp': placed_ts, 'location': 'Seller Store'})
|
|
tracking_history.append({'status': 'picked_up', 'timestamp': placed_ts, 'location': 'Logistics Hub'})
|
|
tracking_history.append({'status': 'out_for_delivery', 'timestamp': placed_ts, 'location': 'Destination Hub'})
|
|
tracking_history.append({'status': 'delivered', 'timestamp': placed_ts, 'location': 'Customer Address'})
|
|
display_status = 'Delivered'
|
|
elif raw_status in ['cancelled', 'canceled']:
|
|
tracking_history.append({'status': 'cancelled', 'timestamp': placed_ts, 'location': 'Order Cancelled'})
|
|
display_status = 'Cancelled'
|
|
else:
|
|
display_status = 'Pending Acceptance'
|
|
|
|
live_data = {
|
|
'status': 'success',
|
|
'data': {
|
|
'awb_number': awb_number or ('Cancelled' if raw_status in ['cancelled', 'canceled'] else 'Pending Dispatch'),
|
|
'current_status': display_status,
|
|
'carrier': detected_carrier,
|
|
'tracking_history': tracking_history
|
|
}
|
|
}
|
|
|
|
# Override current_status if raw_status is cancelled
|
|
if raw_status in ['cancelled', 'canceled'] and live_data and 'data' in live_data:
|
|
live_data['data']['current_status'] = 'Cancelled'
|
|
if not any(h.get('status') == 'cancelled' for h in live_data['data'].get('tracking_history', [])):
|
|
live_data['data']['tracking_history'] = [
|
|
{'status': 'new', 'timestamp': order.placed_at.strftime('%Y-%m-%dT%H:%M:%SZ') if (order and getattr(order, 'placed_at', None)) else '2026-10-09T10:00:00Z', 'location': 'Customer Store'},
|
|
{'status': 'cancelled', 'timestamp': order.placed_at.strftime('%Y-%m-%dT%H:%M:%SZ') if (order and getattr(order, 'placed_at', None)) else '2026-10-09T10:00:00Z', 'location': 'Order Cancelled'}
|
|
]
|
|
|
|
# Ensure executive rider details are stripped for customer privacy
|
|
customer_live_tracking = dict(live_data.get('data', {})) if isinstance(live_data.get('data'), dict) else {}
|
|
customer_live_tracking.pop('rider_details', None)
|
|
carrier_name = 'Express Delivery'
|
|
customer_live_tracking['carrier'] = carrier_name
|
|
|
|
result = {
|
|
'order_id': order.id if order else order_id,
|
|
'awb_number': awb_number or (order.awb_number if order else None),
|
|
'carrier': carrier_name,
|
|
'order_status': order.status if order else raw_status,
|
|
'placed_at': order.placed_at if order else None,
|
|
'shipping_address': order.shipping_address if order else '',
|
|
'expected_delivery_date': customer_live_tracking.get('expected_delivery_date'),
|
|
'picked_up_date': customer_live_tracking.get('picked_up_date'),
|
|
'live_tracking': customer_live_tracking
|
|
}
|
|
|
|
return Response(result)
|