Django E-commerce Order Processing Implementation

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:

  1. Accepts POST parameters from the client
  2. Validates user authentication status
  3. Verifies product IDs are present; redirects to cart if empty
  4. Fetches shiping addresses and product details from the database
  5. Determines quantity source (direct input or cart retrieval)
  6. Calculates line-item totals, merchandise subtotal, and grand total including shipping
  7. 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.

Tags: Django Redis optimistic-locking Database-Transactions E-commerce

Posted on Thu, 25 Jun 2026 16:57:47 +0000 by stevieontario