Web Commerce Domain - Views Documentation

Documentation of Web Commerce domain API endpoints.

Source: api/nextango/apps/web_commerce/views.py

Last Updated: 2026-07-08

Overview

Web Commerce views are plain APIView classes, not a router-registered ViewSet. The app's URLconf mounts at web-commerce/ on the root URLconf (nextango/urls.py:167), so every endpoint below is an absolute path under /web-commerce/. None of the app's views are registered on the root DefaultRouter.

The storefront's Next.js API proxy (storefront/web/app/api/[[...path]]/route.ts) maps its own /api/foo/bar paths onto Django's http://api:8000/foo/bar; that /api/ prefix belongs to the storefront proxy layer, not to Django's URLconf, and is not part of the endpoint path documented here. See the domain README for the proxy.

Every view sets authentication_classes = [] and permission_classes = [AllowAny] (consumer/public posture). Authorization is enforced separately per endpoint, by cart or checkout ownership, not by DRF permission classes. Views delegate business logic to CartService, CheckoutService, and SessionService.

Ownership-based authorization

Cart and checkout endpoints do not trust session['cart_id'] by byte-equality. Authorization is ownership-based:

  • Identity resolution (SessionService.resolve_authorized_web_session(), services/session_service.py:82-108): tries the X-Cart-Session-Token header first (guest-only, SHA-256 hashed against WebSession.cart_token_hash), falling back to the cookie-resolved WebSession for the current Django session key. The token is primary because in cookie-hostile contexts (embedded webviews, ITP) the session cookie round-trips intermittently while a client-held token does not.
  • Cart ownership (_authorize_cart_access(), views.py:59-91): a cart request is authorized only when the cart is ACTIVE and its web_session FK matches the resolved web session. All 5 cart endpoints (CartView, CartAddItemView, CartUpdateItemView, CartRemoveItemView, CartClearView) use this gate.
  • Checkout ownership (CheckoutService._owned_checkout(), services/checkout_service.py:87-106): a checkout session is loaded only when its web_session FK matches the resolved session; a mismatch or missing session raises CheckoutSession.DoesNotExist, which every caller maps to one generic 403/404 response so existence and ownership are never disclosed.
  • CheckoutCompleteView additionally enforces ownership before the idempotency lookup (authorize_before_idempotency(), views.py:927-950), so an unowned request cannot even reach the cached-response short-circuit.

Session Management

GuestSessionCreateView

POST /web-commerce/session/create/

Creates a guest WebSession and an empty Cart for an unauthenticated storefront visitor. No auth required; CSRF-exempt (called before any session exists).

Request body:

{
    "email": "[email protected]",
    "store_id": "uuid-here"
}

Response (201):

{
    "success": true,
    "cart_id": "uuid",
    "cart_token": "opaque-token-or-null",
    "expires_at": "2026-07-08T20:00:00Z"
}

cart_token is a raw, one-time-shown cart-session token minted at session establishment (only for guest sessions; null for a reused non-guest session). Only its SHA-256 hash is persisted server-side, on WebSession.cart_token_hash.

Errors: 400 on invalid body or an unknown store_id; 500 on unexpected failure.


ChangeStoreView

POST /web-commerce/session/store/

Changes the active store for the current session. Because inventory is store-specific, the existing cart is abandoned and a new empty cart is created for the new store. No auth required; CSRF-exempt.

Request body: { "store_id": "uuid" }

Response (200):

{
    "success": true,
    "cart_id": "uuid",
    "cart_token": "opaque-token-or-null",
    "message": "Store changed successfully. Your cart has been cleared."
}

Errors: 400 if store_id is missing, malformed, or does not resolve to a Store.


SessionStatusView

GET /web-commerce/auth/session/

Validates the session and returns verified cart/store/user context. Called on every storefront page load. No auth required; CSRF-exempt.

Identity resolves through SessionService.resolve_authorized_web_session(). If no valid WebSession exists, the storefront session keys are cleared and the response reports authenticated: false. Otherwise the view resolves the session's single ACTIVE cart (SessionService.enforce_single_active_cart() abandons any other ACTIVE cart on the same web session), realigns session['cart_id'] to the cart actually returned, and includes store_settings (pickup_enabled, tax_rate) when a store is set.

If the request carries no X-Cart-Session-Token header, the view mints one for a guest session and returns it in cart_token; a non-guest session never receives one.

Response (unauthenticated / no session):

{
    "authenticated": false,
    "cart_id": null,
    "store_id": null,
    "web_session_id": null,
    "store_settings": null
}

Response (active session):

{
    "authenticated": true,
    "web_session_id": "uuid",
    "cart_id": "uuid",
    "store_id": "uuid",
    "store_settings": { "pickup_enabled": true, "tax_rate": 0.0725 },
    "cart_token": "opaque-token",
    "user": {
        "name": "John Doe",
        "first_name": "John",
        "last_name": "Doe",
        "email": "[email protected]",
        "phone": "",
        "customer_id": "uuid",
        "loyalty_points": 0
    }
}

user is present only when web_session.user is set. Any unexpected exception is swallowed and the view returns the unauthenticated shape rather than a 500, so a storefront page load never hard-fails on session resolution.


Cart Operations

All cart endpoints require _authorize_cart_access() to pass; a failed check returns 403 { "success": false, "error": "Cart does not belong to this session" } before any cart lookup happens.

CartView

GET /web-commerce/cart/{cart_id}/

Returns the serialized cart (CartSerializer) enriched with a promotion preview computed directly against the cart's JSON line items (PromotionService.preview_from_cart_items()), not a SaleTransaction. Each line item is annotated with promotion_discount, effective_price, and applied_promotions; the cart payload also carries active_promotions, promotion_savings, and free_shipping_active. Promotion enrichment failures are caught and logged; the cart itself still returns.

Response (200): CartSerializer data plus the promotion annotations above. Errors: 403 not owned; 404 cart not found.


CartAddItemView

POST /web-commerce/cart/{cart_id}/add/

Adds a product to the cart by SKU via CartService.add_to_cart().

Request body: { "sku": "ABC123", "quantity": 2, "store_id": "uuid" }

Response (200): { "success": true, "message": "...", "cart": { CartSerializer data } } Errors: 400 on serializer validation failure or a CartService rejection (SKU not found, insufficient inventory); 403 not owned; 500 unexpected.


CartUpdateItemView

PATCH /web-commerce/cart/{cart_id}/update/

Updates a line item's quantity via CartService.update_cart_item(). The SKU is resolved to a ProductVariant id before calling the service; store_id comes from request.session['store_id'], not the request body.

Request body: { "sku": "ABC123", "quantity": 3 }

Response (200): updated cart. Errors: 400 serializer/service failure; 403 not owned; 404 SKU not found; 500 unexpected.


CartRemoveItemView

POST /web-commerce/cart/{cart_id}/remove/

Removes a line item by SKU via CartService.remove_from_cart().

Request body: { "sku": "ABC123" }

Response (200): updated cart without the removed item. Errors: 400 serializer/service failure; 403 not owned; 500 unexpected.


CartClearView

POST /web-commerce/cart/{cart_id}/clear/

Clears all line items via CartService.clear_cart() without deleting the cart record. Also called internally by ChangeStoreView's flow.

Response (200): { "success": true, "message": "Cart cleared" } Errors: 400 service failure; 403 not owned; 500 unexpected.


Checkout Flow

CheckoutInitiateView

POST /web-commerce/checkout/initiate/

Begins checkout: creates a CheckoutSession in checkout_initiated status with a 30-minute expiry. Identity comes from the resolved session, never a body-supplied web_session_id; the cart must be ACTIVE and owned by that session before the serializer even runs its DB lookup (_cart_owned_and_active(), views.py:43-56). Inventory is not reserved at this step; it is reserved synchronously inside CheckoutCompleteView.

Request body:

{
    "cart_id": "uuid",
    "store_id": "uuid",
    "fulfillment_method": "ship",
    "pickup_scheduled_time": "ISO datetime"
}

fulfillment_method is one of ship, pickup, digital. pickup_scheduled_time is required for pickup.

Response (201): { "success": true, "checkout": { CheckoutSessionSerializer data } } Errors: 403 (generic, no existence disclosure) when the resolved session does not own the cart; 400 serializer/service validation failure; 500 unexpected.


CheckoutCompleteView

POST /web-commerce/checkout/complete/

Completes checkout: processes payment, creates the SaleTransaction, reserves and commits inventory, and creates Fulfillment/Shipment records. Delegates entirely to CheckoutService.complete_checkout() (see Services for the full orchestration sequence).

This view mixes in IdempotencyMixin and enforces checkout ownership in authorize_before_idempotency() before the idempotency-key lookup runs, so an unowned replay cannot receive another session's cached response.

Request body:

{
    "checkout_session_id": "uuid",
    "payment_method_id": "uuid",
    "payment_data": {},
    "checkout_form_data": {
        "email": "[email protected]",
        "shipping_address": { "firstName": "John", "lastName": "Doe", "company": "",
            "address": "123 Main St", "apartment": "Apt 4B", "city": "Portland",
            "region": "OR", "postalCode": "97201", "country": "USA", "phone": "503-555-1234" },
        "billing_address": {},
        "billing_same_as_shipping": true,
        "delivery_method": { "id": "standard", "title": "Standard Shipping",
            "turnaround": "5-7 business days", "price": "$10.00", "priceValue": 10.00 },
        "payment_details": { "nameOnCard": "John Doe", "cardNumber": "4111111111111111", "cardType": "visa" },
        "shipping_cost": 10.00,
        "saved_address_id": "uuid (optional)",
        "save_to_profile": false
    }
}

Response (200, immediate payment success):

{
    "success": true,
    "transaction_id": "TXN-20260708-A1B2C3D4",
    "checkout_session_id": "uuid",
    "total_amount": "129.99",
    "receipt_token": "token-or-null"
}

Response (200, 3DS required):

{
    "success": true,
    "requires_action": true,
    "transaction_id": "TXN-20260708-A1B2C3D4",
    "payment_intent_id": "pi_...",
    "client_secret": "pi_..._secret_..."
}

Errors: 400 serializer validation, payment failure ({ "success": false, "error": "Payment failed", "details": "..." }), or insufficient inventory re-checked at completion time; 403 not authorized for the checkout; 500 unexpected.


CheckoutCancelView

POST /web-commerce/checkout/cancel/

Cancels an active checkout session and releases any reserved inventory via CheckoutService.cancel_checkout(). Identity is resolved but not required to be present; the service fails closed on a non-owning or missing session with one generic response.

Request body: { "checkout_session_id": "uuid" }

Response (200): { "success": true, "message": "Checkout cancelled, inventory released" } Errors: 400 serializer/service failure (includes "checkout session not found" and "cannot cancel completed checkout"); 500 unexpected.


CheckoutStatusView

GET /web-commerce/checkout/{checkout_session_id}/status/

Polls the current status of a checkout session via CheckoutService.get_checkout_status(), fail-closed to the owning session.

Response (200):

{
    "success": true,
    "checkout_session": {
        "session_id": "uuid",
        "status": "checkout_initiated",
        "fulfillment_method": "pickup",
        "expires_at": "2026-07-08T20:00:00Z",
        "is_expired": false,
        "transaction_id": null,
        "pickup_scheduled_time": "2026-07-09T18:00:00Z",
        "pickup_display_time": "Thursday 06:00 PM",
        "store_pay_at_store_enabled": true
    }
}

pickup_display_time is rendered in the store's local timezone (store.timezone); it falls back to a UTC-formatted string if the timezone conversion fails.

Errors: 404 checkout session not found or not owned; 500 unexpected.


Payment Methods

OnlinePaymentMethodsView

GET /web-commerce/payment-methods/?store_id=<id>

Lists active, online-available payment methods for a store, restricted to the storefront-safe field subset (no employee-only fields). This list is UX only; the authoritative enforcement is PaymentMethodService.validate_method_for_transaction() at checkout completion.

Response (200):

{
    "success": true,
    "payment_methods": [
        { "id": "uuid", "name": "Credit Card", "method_type": "card",
          "is_active": true, "is_online_available": true,
          "minimum_amount": "0.00", "maximum_amount": null,
          "icon_url": "...", "description": "...", "display_order": 1 }
    ]
}

Errors: 400 missing store_id; 500 unexpected.


Guest Order Tracking

GuestOrderLookupView

POST /web-commerce/guest-order/

Lets a guest customer look up an order using the guest_access_token returned at checkout completion, without authentication. Throttled with AnonRateThrottle to resist token enumeration.

Request body: { "access_token": "32-character-alphanumeric-token" }

Response (200):

{
    "success": true,
    "transaction": { "transactionId": "TXN-20260708-A1B2C3D4", "status": "completed", "totalAmount": "129.99", "line_items": [] },
    "fulfillment": { "id": "uuid", "status": "shipped", "shipping_city": "Portland", "shipping_region": "OR",
                      "shipments": [ { "carrier_name": "UPS", "tracking_number": "1Z999AA10123456784", "status": "in_transit", "items": [] } ] }
}

Errors: 400 missing access_token; 404 order not found (a generic message shared by "invalid" and "expired" to prevent enumeration); 429 rate limit exceeded.


Saved Addresses

SavedAddressesView

GET /web-commerce/saved-addresses/

Returns the authenticated user's saved shipping addresses, ordered default-first then most-recently-added. Identity uses SessionService.resolve_web_session() (cookie-only, not the token-primary resolver), so it never leaks another user's saved-address PII through a stale or foreign token. Returns an empty list (not 401) for guests, since checkout must still work without an account.

Response (authenticated):

{
    "success": true,
    "addresses": [
        { "id": "uuid", "label": "Home", "is_default": true,
          "first_name": "John", "last_name": "Doe", "company": "",
          "address": "123 Main St", "apartment": "Apt 4B", "city": "Portland",
          "region": "OR", "postal_code": "97201", "country": "USA", "phone": "503-555-1234" }
    ]
}

Response (guest / unauthenticated): { "success": true, "addresses": [] }


  • Models: CheckoutSession, CheckoutReservationContext
  • Services: CartService, CheckoutService, ReservationService, SessionService
  • Serializers: request/response serialization
  • Transactions Views: FulfillmentViewSet, ShipmentViewSet

Was this page helpful?