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 outABANDONED- 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.