Web Commerce - Signal Handlers

Location: api/nextango/apps/web_commerce/signals.py Last Updated: 2026-07-08

Overview

The Web Commerce signal handlers manage lifecycle events for checkout sessions and web sessions, ensuring automatic cleanup and inventory reservation release when sessions expire or are deleted.


Architecture

┌─────────────────────────────────────────────────────────────┐
│                 CheckoutSession                             │
│        (status changes, deletion)                           │
└──────────────────────┬──────────────────────────────────────┘
                       │ post_save / pre_delete
┌──────────────────────▼──────────────────────────────────────┐
│              Signal Handlers                                │
│                                                             │
│   handle_checkout_session_status_change()                   │
│   cleanup_checkout_reservations()                           │
└──────────────────────┬──────────────────────────────────────┘

┌──────────────────────▼──────────────────────────────────────┐
│              ReservationService                             │
│        release_reservations(checkout_session)               │
└──────────────────────┬──────────────────────────────────────┘

┌──────────────────────▼──────────────────────────────────────┐
│              InventoryLevel                                 │
│        stock_reserved -= released_qty                       │
└─────────────────────────────────────────────────────────────┘

Signal Handlers

handle_checkout_session_status_change()

Source: signals.py:16-56

Signal: post_save on CheckoutSession

Purpose: Automatically release inventory reservations when checkout session transitions to a terminal state.

Terminal States:

  • EXPIRED - Session timed out
  • ABANDONED - User abandoned checkout

Logic:

if instance.status in [EXPIRED, ABANDONED]:
    # Check for active reservations (avoid duplicate release)
    if CheckoutReservationContext.objects.filter(
        checkout_session=instance,
        status='active'
    ).exists():
        ReservationService.release_reservations(instance)

Why Skip New Sessions:

  • Newly created sessions (created=True) shouldn't trigger release
  • Only status changes on existing sessions matter

cleanup_websession_carts()

Source: signals.py:59-100

Signal: pre_delete on WebSession

Purpose: Release inventory reservations before WebSession deletion to prevent orphaned data.

Logic:

# Find active checkout sessions for this web session
active_checkouts = CheckoutSession.objects.filter(
    web_session=instance,
    status__in=[CHECKOUT_INITIATED, PAYMENT_PENDING, PAYMENT_PROCESSING]
)

# Release reservations and mark as abandoned
for checkout in active_checkouts:
    ReservationService.release_reservations(checkout)
    checkout.status = ABANDONED
    checkout.save()

Note: Cart cleanup is not needed here because Cart is tied to Customer, not WebSession.


cleanup_checkout_reservations()

Source: signals.py:103-127

Signal: pre_delete on CheckoutSession

Purpose: Release inventory reservations before checkout session deletion.

Logic:

# Only release if not already completed (completed = inventory committed)
if instance.status != COMPLETED:
    ReservationService.release_reservations(instance)

Why Check Status:

  • Completed checkouts have committed inventory (not reserved)
  • Releasing completed checkout would incorrectly reduce committed inventory

Reservation Release Flow

┌────────────────────────────────────────────────────────────┐
│                Checkout Session                            │
│                                                            │
│   Status: CHECKOUT_INITIATED                               │
│   Reservations: [SKU1: 2, SKU2: 1]                         │
└──────────────────────────────────────────────────────────┘

                       │ Session expires / User abandons

┌────────────────────────────────────────────────────────────┐
│             post_save signal fires                         │
│                                                            │
│   Status: EXPIRED                                          │
│   → handle_checkout_session_status_change()                │
└──────────────────────────────────────────────────────────┘

                       │ ReservationService.release_reservations()

┌────────────────────────────────────────────────────────────┐
│             Inventory Restored                             │
│                                                            │
│   InventoryLevel(SKU1).stock_reserved -= 2                  │
│   InventoryLevel(SKU2).stock_reserved -= 1                  │
│   CheckoutReservationContext.status = 'released'           │
└────────────────────────────────────────────────────────────┘

Error Handling

All signal handlers wrap operations in try/except to prevent cascade failures:

try:
    ReservationService.release_reservations(checkout)
except Exception as e:
    logger.error(
        f"Failed to release reservations for checkout {checkout.session_id}: {e}",
        exc_info=True
    )
    # Don't re-raise - allow deletion to proceed

Rationale: Reservation cleanup failures shouldn't block session/checkout deletion.


  • Tasks - Periodic cleanup tasks
  • Services - ReservationService and LifecycleService
  • Models - CheckoutSession and CheckoutReservationContext

Was this page helpful?