Order Preview Handler
When customers initiate checkout from either the shopping cart or product detail page, the system renders an order confirmation interface. The frontend must transmit different parameter sets based on the origin:
- Direct Purchase: Product variant ID and requested quantity
- Cart Checkout: List of selected product variant IDs only
The backend preview handler performs these operations:
- Accepts POST parameters from the client
- Validates user authentication status
- Verifies product IDs are present; redirects to cart if empty
- Fetches shiping addresses and product details from the database
- Determines quantity source (direct input or cart retrieval)
- Calculates line-item totals, merchandise subtotal, and grand total including shipping
- Enriches product objects with computed properties for template rendering
Preview View Implementation
from django.shortcuts import render, redirect
from django.views.generic import View
from django.core.urlresolvers import reverse
from utils.views import RequireAuthMixin
from users.models import Address
from goods.models import ProductSKU
from django_redis import get_redis_connection
class OrderPreviewView(RequireAuthMixin, View):
def post(self, request):
product_ids = request.POST.getlist('product_ids')
quantity = request.POST.get('quantity')
if not product_ids:
return redirect(reverse('cart:summary'))
current_user = request.user
try:
shipping_address = Address.objects.filter(user=current_user).latest('created_at')
except Address.DoesNotExist:
shipping_address = None
redis_client = get_redis_connection('default')
order_items = []
merchandise_total = 0
item_count = 0
shipping_fee = 10
if quantity is None:
cart_data = redis_client.hgetall(f'user_cart_{current_user.id}')
for pid in product_ids:
try:
product = ProductSKU.objects.get(id=pid)
except ProductSKU.DoesNotExist:
return redirect(reverse('cart:summary'))
qty = int(cart_data.get(str(pid).encode(), 0))
if qty == 0:
continue
item_total = product.unit_price * qty
product.subtotal = item_total
product.quantity = qty
order_items.append(product)
merchandise_total += item_total
item_count += qty
else:
try:
qty = int(quantity)
except (ValueError, TypeError):
return redirect(reverse('products:detail', args=(product_ids[0],)))
try:
product = ProductSKU.objects.get(id=product_ids[0])
except ProductSKU.DoesNotExist:
return redirect(reverse('cart:summary'))
if qty > product.inventory:
return redirect(reverse('products:detail', args=(product_ids[0],)))
item_total = product.unit_price * qty
product.subtotal = item_total
product.quantity = qty
order_items.append(product)
merchandise_total = item_total
item_count = qty
redis_client.hset(f'user_cart_{current_user.id}', product_ids[0], qty)
grand_total = merchandise_total + shipping_fee
context = {
'items': order_items,
'address': shipping_address,
'item_count': item_count,
'merchandise_total': merchandise_total,
'grand_total': grand_total,
'shipping_fee': shipping_fee,
'product_id_str': ','.join(product_ids),
}
return render(request, 'checkout_preview.html', context)
Order Commit Handler
After confirmation, the frontend submits the complete order payload including user ID, address identifier, payment method, and product identifiers. The backend faces three critical challenges:
Technical Considerations
Database Transactions: Ensure atomicity across order header and line item creation.
from django.db import transaction
checkpoint = transaction.savepoint()
transaction.savepoint_rollback(checkpoint)
transaction.savepoint_commit(checkpoint)
Concurrency Control: Prevent overselling when multiple users purchase simultaneously. Solutions include pessimistic locking, optimistic locking, or queue-based processing. This implementation uses optimistic locking:
UPDATE product_sku SET inventory=new_inventory, units_sold=new_sales
WHERE id=product_id AND inventory=old_inventory;
Order Number Generation: Create unique identifiers using timestamp and user ID.
from datetime import datetime
order_number = datetime.now().strftime('%Y%m%d%H%M%S') + f'{user.id:06d}'
Commit View Implementation
from django.views.generic import View
from django.http import JsonResponse
from utils.views import RequireAuthMixin, AtomicTransactionMixin
from orders.models import OrderHeader, OrderLine
from goods.models import ProductSKU
from django_redis import get_redis_connection
from datetime import datetime
from django.db import transaction
class OrderCommitView(RequireAuthMixin, AtomicTransactionMixin, View):
def post(self, request):
addr_id = request.POST.get('address_id')
payment_method = request.POST.get('payment_method')
product_id_str = request.POST.get('product_ids')
try:
shipping_address = Address.objects.get(id=addr_id, user=request.user)
except Address.DoesNotExist:
return JsonResponse({'status': 'error', 'msg': 'Invalid shipping address'})
if int(payment_method) not in OrderHeader.PAYMENT_CHOICES:
return JsonResponse({'status': 'error', 'msg': 'Unsupported payment method'})
product_ids = product_id_str.split(',')
redis_client = get_redis_connection('default')
cart_data = redis_client.hgetall(f'user_cart_{request.user.id}')
order_number = datetime.now().strftime('%Y%m%d%H%M%S') + f'{request.user.id:06d}'
txn_point = transaction.savepoint()
try:
order = OrderHeader.objects.create(
order_number=order_number,
customer=request.user,
shipping_address=shipping_address,
payment_method=int(payment_method),
shipping_cost=10,
merchandise_total=0,
status='pending'
)
total_items = 0
order_value = 0
for pid in product_ids:
for attempt in range(3):
try:
product = ProductSKU.objects.get(id=pid)
except ProductSKU.DoesNotExist:
transaction.savepoint_rollback(txn_point)
return JsonResponse({'status': 'error', 'msg': 'Product not found'})
qty = int(cart_data.get(pid.encode(), 0))
if qty > product.inventory:
transaction.savepoint_rollback(txn_point)
return JsonResponse({'status': 'error', 'msg': f'Insufficient stock for {product.name}'})
new_inventory = product.inventory - qty
new_sales = product.units_sold + qty
updated = ProductSKU.objects.filter(
id=pid,
inventory=product.inventory
).update(inventory=new_inventory, units_sold=new_sales)
if not updated and attempt < 2:
continue
elif not updated and attempt == 2:
transaction.savepoint_rollback(txn_point)
return JsonResponse({'status': 'error', 'msg': 'Order processing failed'})
OrderLine.objects.create(
order=order,
product=product,
quantity=qty,
unit_price=product.unit_price
)
order_value += product.unit_price * qty
total_items += qty
break
order.merchandise_total = order_value
order.grand_total = order_value + 10
order.save()
except Exception as exc:
transaction.savepoint_rollback(txn_point)
return JsonResponse({'status': 'error', 'msg': 'Order creation failed'})
transaction.savepoint_commit(txn_point)
pipeline = redis_client.pipeline()
pipeline.hdel(f'user_cart_{request.user.id}', *product_ids)
pipeline.execute()
return JsonResponse({'status': 'success', 'msg': 'Order placed successfully'})
Authentication and Transaction Mixins
Custom mixins enforce JSON-based authentication and transaction wraping:
from django.http import JsonResponse
from functools import wraps
from django.db import transaction
def json_auth_required(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated():
return JsonResponse({'status': 'unauthorized', 'msg': 'Authentication required'})
return view_func(request, *args, **kwargs)
return wrapper
class RequireAuthMixin(object):
@classmethod
def as_view(cls, **initkwargs):
view = super().as_view(**initkwargs)
return json_auth_required(view)
class AtomicTransactionMixin(object):
@classmethod
def as_view(cls, **initkwargs):
view = super().as_view(**initkwargs)
return transaction.atomic(view)
URL Configuration
Root URL patterns:
from django.conf.urls import url, include
urlpatterns = [
url(r'^orders/', include('orders.urls', namespace='orders')),
]
Application-specific routes:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^preview$', views.OrderPreviewView.as_view(), name='preview'),
url(r'^submit$', views.OrderCommitView.as_view(), name='submit'),
]
Upon successful order submission, the system persists order headers and line items to the database, then updates the Redis cart by removing purchased items.