Web Commerce Domain - Services Documentation
Sources:
api/nextango/apps/web_commerce/services/cart_service.pyapi/nextango/apps/web_commerce/services/checkout_service.pyapi/nextango/apps/web_commerce/services/reservation_service.pyapi/nextango/apps/web_commerce/services/session_service.pyapi/nextango/apps/web_commerce/services/lifecycle_service.pyapi/nextango/apps/web_commerce/services/email_service.py
Last Updated: 2026-07-08
Overview
The Web Commerce services layer orchestrates web checkout flows. Following the pos_gateway pattern, services coordinate existing domain services (TransactionService, PaymentService, InventoryAdjustmentService, FulfillmentService) rather than implementing business logic themselves.
Service classes: CartService, CheckoutService, ReservationService, SessionService, LifecycleService, OrderEmailService.
CartService
Purpose: Cart validation and composition against the transactions.Cart model's line_items JSONField.
Source: services/cart_service.py
add_to_cart(cart_id, sku, quantity, store_id)
Adds an item to the cart. Looks up the ProductVariant by SKU, checks InventoryLevel.stock_on_hand - InventoryLevel.stock_reserved for the given store, then either merges the quantity into an existing line item or appends a new one (using variant.effective_price so sale pricing is respected). Returns {'success': False, 'error': ...} if the cart, SKU, or store inventory record is missing, or if requested quantity exceeds availability.
update_cart_item(cart_id, variant_id, quantity, store_id)
Updates a line item's quantity. quantity == 0 removes the item. Otherwise re-checks stock_on_hand - stock_reserved availability and recalculates total_price.
validate_cart(cart_id, store_id)
Re-validates every line item against current inventory and pricing. Returns critical severity errors for a product no longer available, unavailable at the store, or with insufficient inventory; warning severity for a price change since the item was added (compared against variant.effective_price).
validate_cart_availability(cart_items, store)
Focused availability check that runs inside the checkout-completion transaction to prevent overselling. Takes raw cart items and a Store instance; uses InventoryLevel.objects.select_for_update() to lock each row while computing stock_on_hand - stock_reserved. Returns {'all_available': bool, 'items': [...]} with a reason (product_not_found, not_available_at_store, insufficient_stock) for each unavailable item.
remove_from_cart(cart_id, sku)
Removes a line item by SKU.
clear_cart(cart_id)
Sets line_items to an empty list.
get_cart(cart_id)
Returns the raw cart dict (cart_id, line_items, item_count, status).
CheckoutService
Purpose: Orchestrates the checkout flow by composing TransactionService, PaymentService, InventoryAdjustmentService, and FulfillmentService. Never implements business logic directly.
Source: services/checkout_service.py
Ownership gate: _owned_checkout(checkout_session_id, web_session)
Every method below that operates on an existing checkout session loads it through this helper. It raises CheckoutSession.DoesNotExist when web_session is None, when checkout_session_id is malformed, or when the session exists but is not owned by web_session. Callers convert that exception into one generic not-found response, so ownership and existence are never disclosed to a non-owning caller.
initiate_checkout(cart_id, web_session, store_id, fulfillment_method, pickup_scheduled_time=None)
Starts checkout. web_session must be the resolved, authoritative session (never a body-supplied id); the call fails closed with {'success': False, 'error': 'Not authorized for this cart'} unless the cart is ACTIVE and its web_session FK matches. Validates the cart is non-empty and passes CartService.validate_cart(), then creates a CheckoutSession in checkout_initiated status with a 30-minute expiry.
Inventory is not reserved at this step. Reservation happens synchronously inside complete_checkout(), when transaction line items are created. ReservationService.reserve_for_checkout() exists as scaffolding for a possible future initiate-time reservation flow but is not called from this path.
Returns {'success': True, 'checkout_session_id': ..., 'expires_at': ...}.
complete_checkout(checkout_session_id, payment_method_id, payment_data, checkout_form_data=None, web_session=None)
Completes checkout inside a single transaction.atomic() block. Sequence:
- Loads the checkout session via
_owned_checkout(); returns "Checkout session not found" if unowned or missing. - Re-checks session expiry and completion status.
- Re-validates inventory availability with
CartService.validate_cart_availability()to prevent overselling between checkout initiation and completion; returns{'success': False, 'error': 'insufficient_inventory', ...}with per-item detail if anything has gone unavailable. - Creates the
SaleTransactionviaTransactionService.create_transaction(employee=None, ...), setsfulfillmentMethod,sourceCheckoutId,payment_timing, and aguest_access_tokenfor non-customer (guest) checkouts. - Adds each cart line item via
TransactionService.add_product_to_transaction_without_recalc(), then reserves inventory per line item viaInventoryAdjustmentService.process_movement(); the reservation takes effect immediately within the checkout. - Applies a promo code if provided in
checkout_form_data['promo_code']: adds it toappliedPromoCodes, then callsPromotionService.track_campaign_impression()for each active campaign linked to the code (non-fatal, logged as a warning on failure). - Calls
sale_transaction.recalculate_totals(), which internally callsPromotionService.evaluate_transaction(). If no explicit promo code was provided, this triggers automatic promotion evaluation: active campaigns in date range are filtered by store and validated against product/collection scope, and the highest-value matching discount is applied without customer action. - Snapshots the auto-applied promotions (
_capture_applied_promotions()) and distributes their per-item discount to eachSaleLineItem.lineDiscount(_distribute_line_item_discounts()). - Processes payment when
payment_timing == 'immediate': validates the payment method server-side (PaymentMethodService.validate_method_for_transaction(), the authoritative check; the storefront's method list fromOnlinePaymentMethodsViewis UX only), charges viaPaymentOrchestrationService.charge_card_not_present(), then applies the result viaPaymentService.apply_payment_to_transaction(). A non-Stripe processor reaching this path raisesProcessorNotImplemented(HTTP 501) rather than leaking a raw exception. Arequires_actionresult (3DS) returns early inside the atomic block: the transaction is saved withPAYMENT_REQUIRES_ACTIONstatus, resolved fulfillment data is snapshotted ontoCheckoutSession.fulfillment_data, and the caller returnsrequires_action: truewith the Stripeclient_secret. Inventory stays reserved; fulfillment is not yet created. - Commits reserved inventory via
InventoryAdjustmentService.commit_reserved_inventory()when payment was collected; otherwise inventory stays reserved (not committed) until pickup, forpayment_timing == 'at_pickup'. - Creates
Fulfillment/Shipmentrecords:FulfillmentService.create_pickup_fulfillment()for pickup orders withpickup_customer_info, orFulfillmentService.create_from_checkout()pluscreate_initial_shipment()for shipping orders. Optionally saves a new address to the customer's profile whensave_to_profileis set. - Creates a
PickupSchedulefor pickup orders with apickup_scheduled_time(failure here does not fail the checkout). - Sends an order confirmation email asynchronously after the checkout commits, gated by the
ENABLE_ORDER_EMAILSenvironment variable. - Marks the checkout session
completed, links the transaction, and defers cart clearing and reservation-context commit to run asynchronously after the checkout commits, past the customer-perceived completion of the request.
A payment failure raises an internal _CheckoutPaymentFailedError inside the atomic block, which rolls back the transaction, line items, and inventory reservations before the outer handler persists payment_pending status on the checkout session and returns {'success': False, 'error': 'Payment failed', 'details': ...}.
Returns on success: {'success': True, 'transaction_id': ..., 'checkout_session_id': ..., 'total_amount': ..., 'receipt_token': ...}. receipt_token is None when the fulfillment has no customer email or receipt-token generation is unavailable in the environment.
cancel_checkout(checkout_session_id, web_session=None)
Loads the checkout session via _owned_checkout(). Rejects cancelling a completed session. Releases reservations via ReservationService.release_reservations() and sets status to abandoned.
get_checkout_status(checkout_session_id, web_session=None)
Loads the checkout session via _owned_checkout() and returns its status, including pickup_display_time converted to the store's local timezone (store.timezone; falls back to UTC formatting on a conversion error) and store_pay_at_store_enabled.
ReservationService
Purpose: Manages inventory reservations for web checkout using InventoryLevel.stock_reserved directly, with CheckoutReservationContext as the audit trail. Distinct from the POS InventoryReservation, which ties to TerminalSession.
Source: services/reservation_service.py
Raises InsufficientInventoryError when requested quantity exceeds available inventory.
reserve_for_checkout(checkout_session)
For each cart line item, locks the matching InventoryLevel row with select_for_update(), checks stock_on_hand - stock_reserved >= requested, increments stock_reserved, and creates a CheckoutReservationContext audit record. Runs inside transaction.atomic(): any failure rolls back all reservations for the call. Not called from the normal checkout path (see CheckoutService.initiate_checkout()); reservation there happens through InventoryAdjustmentService instead.
release_reservations(checkout_session)
Finds every active CheckoutReservationContext for the session, decrements InventoryLevel.stock_reserved for each, and marks the context released. Atomic; a no-op returning released_count: 0 when there is nothing active to release.
commit_reservations(checkout_session)
Marks every active reservation context as committed. Does not touch InventoryLevel values; inventory decrement is InventoryAdjustmentService's responsibility.
expire_old_reservations()
Finds active reservation contexts past expires_at, releases their inventory, and marks them expired. Intended to be called periodically (see Tasks).
SessionService
Purpose: Resolves session identity, manages the guest/customer cart association, and issues the cart-session token used as a cookie-independent fallback credential.
Source: services/session_service.py
resolve_web_session(request)
Returns the valid, unexpired WebSession for the current Django session_key, or None. Cookie-only: does not consider the X-Cart-Session-Token header.
resolve_authorized_web_session(request)
The identity used by every cart/checkout/payment authorization gate. Tries X-Cart-Session-Token first (guest-only, matched by SHA-256 hash against WebSession.cart_token_hash), falling back to resolve_web_session(). The token is preferred because in cookie-hostile contexts (embedded webviews, ITP) the cookie round-trips intermittently while a client-stored token is stable; preferring it lets every gate resolve the same guest session consistently. A post-login request has no resolvable token (it is cleared on merge) and falls back to the cookie automatically.
enforce_single_active_cart(web_session, keep_cart)
Abandons every ACTIVE cart on web_session except keep_cart, so at most one ACTIVE cart exists per web session. Called at every cart-creation point (guest session create, store change, session-status resolution) to prevent divergence between the cart a session reports and the cart it stored.
create_guest_session(request, email=None, store_id=None)
Creates or reuses a WebSession for the current Django session, creates or reuses its ACTIVE cart, calls enforce_single_active_cart(), and mints a cart-session token. A dashboard-type session is left untouched (its TTL is not truncated) and reused for storefront tracking rather than overwritten. Session identity is stored under request.session['storefront_session_id'], distinct from the dashboard auth layer's web_session_id key, even though both share the same cookie domain.
change_store(request, new_store_id)
Validates the new store, creates a fresh ACTIVE cart tied to the resolved WebSession, abandons prior ACTIVE carts via enforce_single_active_cart(), updates the session's store_id, and re-mints the cart-session token.
merge_guest_cart_on_login(guest_web_session, customer, new_web_session=None)
Merges a guest cart into the logging-in customer's cart. If a guest cart exists, invalidates its cart_token_hash immediately regardless of merge outcome, so a reused token can never resolve a post-merge session. Merges line_items by variant_id (summing quantities on match), marks the guest cart merged, and re-points the resulting cart's web_session FK to new_web_session when provided.
get_or_create_cart_for_customer(customer, web_session=None)
Returns the customer's ACTIVE cart, creating one if none exists. If a cart exists but is owned by a different session, re-points its web_session FK to the logging-in session (multi-device policy: the cart follows the most recent login).
associate_cart_with_session(cart_id, web_session_id)
Checks whether a CheckoutSession already links the cart and web session; otherwise returns a message indicating the association happens when checkout is actually initiated. Optional helper, not required by the checkout flow.
LifecycleService
Purpose: Coordinates periodic cleanup of expired sessions, abandoned carts, and old audit records. Pure orchestration; no business logic of its own.
Source: services/lifecycle_service.py
cleanup_expired_checkout_sessions()
Finds checkout_initiated/payment_pending/payment_processing sessions past expires_at, releases their reservations via ReservationService.release_reservations(), and marks each expired. Returns {'sessions_cleaned': N, 'reservations_released': N}.
cleanup_abandoned_carts(age_hours=24)
Marks ACTIVE carts with no active checkout session and updated_at older than age_hours as abandoned.
cleanup_stale_guest_sessions(age_days=30)
Deletes guest WebSession records inactive for age_days with no active checkout session, abandoning their ACTIVE carts first.
cleanup_old_reservation_contexts(age_days=90)
Deletes committed or released CheckoutReservationContext records older than age_days.
OrderEmailService
Source: services/email_service.py
Purpose: Sends order confirmation and shipment notification emails for web commerce orders. Uses the Resend API by default, with Django SMTP (Hostinger) as an alternative selected by the ORDER_EMAIL_PROVIDER environment variable.
Called from the checkout flow:
send_order_confirmation(): called aftercomplete_checkout()succeeds, when aFulfillmentrecord exists.send_pickup_order_confirmation(): called for pickup orders; content adapts based on whether the order is paid in full or has a balance due at pickup.send_order_ready_email(): called when a pickup order is marked ready for collection, triggered via a Celery task.
send_order_confirmation(transaction, fulfillment)
Sends an order confirmation email with a tracking link (guest orders only, via guest_access_token) and full shipping details: order number, date, total, a line items table, subtotal, tax, shipping cost, and shipping address. Returns bool.
send_pickup_order_confirmation(transaction, fulfillment, pickup_schedule=None)
Sends a pickup order confirmation. A fully-paid order gets a "PAID IN FULL" banner and subject line; a balance-due order gets a "Balance Due" banner with the amount. Includes store name, address, phone, and pickup time converted to store.timezone. Returns bool.
send_order_ready_email(transaction, fulfillment)
Sends a notification that a pickup order is ready for collection. Returns bool.
Private helpers
_send_email() routes to Resend or Hostinger based on ORDER_EMAIL_PROVIDER (hostinger selects SMTP; any other value, default resend, selects the Resend API). _send_via_resend() and _send_via_hostinger() implement each transport.
Related Documentation
- Models:
CheckoutSession,CheckoutReservationContext - Views: API endpoints
- Transactions Services:
TransactionService,FulfillmentService - Payments Services:
PaymentService,PaymentOrchestrationService