customerwebsitebackend/orders/views.py
vickytechkey b85ff4fed6
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 31s
feat: implement cart, checkout, and order views for customer website backend
2026-10-10 14:47:33 +05:30

901 lines
39 KiB
Python

import json
import requests
from django.conf import settings
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='Shadowfax',
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'] = []
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 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'] = []
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)
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)
if order.status.lower() in ['shipped', 'delivered']:
return Response({
'error': 'Order has already been shipped and cannot be cancelled directly. Please initiate a return request instead.'
}, 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 Shadowfax production 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:
# 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
awb_number = (order.awb_number if order else awb_query) or None
raw_status = (order.status if order else 'pending').lower()
# 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
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': 'Shadowfax Express' if awb_number else 'Awaiting Dispatch',
'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)
result = {
'order_id': order.id if order else order_id,
'awb_number': awb_number or (order.awb_number if order else None),
'carrier': 'Shadowfax Express' if (order and order.awb_number) else 'Awaiting Dispatch',
'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 '',
'live_tracking': customer_live_tracking
}
return Response(result)