Products - Usage Examples & Patterns

Location: api/nextango/apps/products/ Last Updated: 2026-07-06

Overview

Working examples for the common developer paths through the products domain: catalog reads, the two public search endpoints, multi-store inventory queries, the inventory mutation workflow, and ORM query patterns.

All catalog and inventory endpoints require authentication. /search/ and /search/smart-filter/ are public. Raw per-store stock numbers (stock_on_hand, stock_reserved, stock_available, store_inventory, total_inventory) reach only privileged callers: users with the employee or manager role, and POS-session principals. Everyone else, including the storefront service principal and anonymous search callers, receives binary availability (is_in_stock, is_available) plus prices. See the Access Model in the views documentation.


Table of Contents

  1. Catalog Reads
  2. Public Search
  3. Multi-Store Inventory Queries
  4. Inventory Mutation Workflow
  5. Dashboard Summaries
  6. ORM Query Patterns

Catalog Reads

Store-Scoped Product List

Use Case: Storefront category page showing products sellable at one store.

GET /products/?store_id=550e8400-e29b-41d4-a716-446655440000&category=7f9c2f4e-1b2a-4c3d-9e8f-001122334455
Authorization: Bearer <token>
{
  "count": 42,
  "next": "http://api/products/?page=2&store_id=550e8400-e29b-41d4-a716-446655440000",
  "previous": null,
  "results": [
    {
      "id": "a1b2c3d4-0000-0000-0000-000000000001",
      "name": "Classic T-Shirt",
      "slug": "classic-t-shirt",
      "short_description": "Heavyweight cotton tee",
      "variant_records": [
        {
          "sku": "SKU-TSHIRT-RED-L",
          "name": "Red / Large",
          "price": "29.99",
          "effective_price": "29.99",
          "is_in_stock": true
        }
      ]
    }
  ]
}

With store_id, the list excludes products with no sellable variant at that store, and variant_records contains only variants sellable there. Non-privileged callers do not receive total_inventory, store_inventory, stock_available, or stock_reserved; privileged callers get the full ProductVariantSerializer shape. An unresolvable store_id returns an empty result set, not an error.

Retrieve Product with Store Availability Signal

Use Case: Product detail page that signals unavailability without hiding the product.

GET /products/classic-t-shirt/?store_id=550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer <token>
{
  "id": "a1b2c3d4-0000-0000-0000-000000000001",
  "name": "Classic T-Shirt",
  "slug": "classic-t-shirt",
  "variant_records": [
    {
      "sku": "SKU-TSHIRT-RED-L",
      "name": "Red / Large",
      "price": "29.99",
      "is_in_stock": true
    }
  ],
  "available_at_store": true
}

The product is always returned even when nothing is sellable at the store. available_at_store is the signal; when it is false, variant_records is empty.

POS Barcode Lookup

Use Case: A scanner at the register reads a UPC; the terminal resolves it to a variant.

GET /product-variants/?barcode=012345678905
Authorization: Bearer <token>
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": "b2c3d4e5-0000-0000-0000-000000000002",
      "sku": "SKU-TSHIRT-RED-L",
      "upc": "012345678905",
      "name": "Red / Large",
      "product_name": "Classic T-Shirt",
      "price": "29.99",
      "sale_price": "24.99",
      "effective_price": "24.99",
      "on_sale": true,
      "is_in_stock": true
    }
  ]
}

barcode matches exactly on either sku or upc. POS sessions are privileged callers, so they also receive store_inventory, stock_available, and stock_reserved.

Variant Inventory Status

Use Case: Quick stock check from variant-level fields, no per-store quantities.

GET /product-variants/SKU-TSHIRT-RED-L/inventory_status/
Authorization: Bearer <token>
{
  "sku": "SKU-TSHIRT-RED-L",
  "name": "Red / Large",
  "product": "Classic T-Shirt",
  "stock_initial": 100,
  "digital_inventory": false,
  "unlimited_stock": false,
  "is_in_stock": true,
  "price": "29.99",
  "sale_price": "24.99",
  "effective_price": "24.99",
  "on_sale": true,
  "featured": false
}

stock_initial is the immutable creation-time seed, not a live counter. Live quantities live on InventoryLevel and come from the /inventory-levels/ endpoints below.


Store-Scoped Full-Text Search

Use Case: Storefront search box. No authentication required.

GET /search/?q=t-shirt&store_id=550e8400-e29b-41d4-a716-446655440000&in_stock=true&page=1&page_size=20
{
  "results": [
    {
      "id": "a1b2c3d4-0000-0000-0000-000000000001",
      "name": "Classic T-Shirt",
      "slug": "classic-t-shirt",
      "short_description": "Heavyweight cotton tee",
      "hidden": false,
      "variants": [
        {
          "sku": "SKU-TSHIRT-RED-L",
          "name": "Red / Large",
          "price": 29.99,
          "salePrice": 24.99,
          "onSale": true,
          "stockInitial": 100,
          "inventory_by_store": [
            {
              "store_id": "550e8400-e29b-41d4-a716-446655440000",
              "store_sanity_id": "store-downtown",
              "store_name": "Downtown Store",
              "is_available": true
            }
          ]
        }
      ],
      "price_range": {"min_price": 24.99, "max_price": 29.99},
      "has_inventory": true,
      "total_inventory": 100
    }
  ],
  "total": 8,
  "page": 1,
  "page_size": 20,
  "total_pages": 1,
  "took": 12,
  "query": "t-shirt",
  "filters": {"store_id": "550e8400-e29b-41d4-a716-446655440000", "in_stock": true}
}

The variant shape is the Elasticsearch document (camelCase keys such as stockInitial, the immutable seed). Non-privileged callers get the inventory_by_store shape shown here; staff and POS-session callers additionally receive stock_on_hand and stock_reserved in each entry. Invalid parameters return 400 with field errors.

Smart Filter with Facets and Collapsed Cards

Use Case: Faceted product grid with one card per product. No authentication required.

GET /search/smart-filter/?store_id=550e8400-e29b-41d4-a716-446655440000&collapse_variants=true&attributes[Color][]=Red&sort=price-asc&page=1
{
  "products": [
    {
      "card_kind": "variant",
      "id": "a1b2c3d4-0000-0000-0000-000000000001_SKU-TSHIRT-RED-L",
      "product_name": "Classic T-Shirt",
      "product_slug": "classic-t-shirt",
      "product_short_description": "Heavyweight cotton tee",
      "name": "Red / Large",
      "sku": "SKU-TSHIRT-RED-L",
      "price": 29.99,
      "sale_price": 24.99,
      "effective_price": 24.99,
      "on_sale": true,
      "is_in_stock": true,
      "featured": false,
      "store_name": "Downtown Store",
      "image_urls": []
    },
    {
      "card_kind": "product",
      "id": "a1b2c3d4-0000-0000-0000-000000000007",
      "product_name": "Hoodie",
      "product_slug": "hoodie",
      "product_short_description": "Fleece-lined hoodie",
      "matched_variant_count": 3,
      "price_min": 49.99,
      "price_max": 59.99,
      "on_sale": false,
      "featured": true,
      "is_in_stock": true,
      "store_name": "Downtown Store",
      "image_urls": []
    }
  ],
  "total": 2,
  "total_variants": 2,
  "aggregations": {
    "brands": [{"_id": "brand-uuid", "name": "Acme", "count": 2}],
    "categories": [{"_id": "category-uuid", "name": "Apparel", "count": 2}],
    "productTypes": [{"_id": "type-uuid", "name": "Tops", "count": 2}],
    "collections": [],
    "attributes": {
      "featured": [{"name": "Color", "type": "attributeColorSwatch", "colors": [{"name": "Red", "hex": "#FF0000", "count": 2, "available": true}]}],
      "regular": [{"name": "Size", "type": "attributeTextList", "options": [{"name": "Large", "count": 2, "available": true}]}]
    }
  },
  "page": 1,
  "page_size": 20,
  "filters": {"attributes": {"Color": ["Red"]}, "sort": "price-asc", "store_id": "550e8400-e29b-41d4-a716-446655440000", "collapse": true}
}

collapse_variants=true collapses each product to a single card: card_kind: "variant" when exactly one variant matched the filters, card_kind: "product" with matched_variant_count and a price_min/price_max range when several did. Without collapse_variants (the carousel path), every matched variant is its own row and rows carry no card_kind.

Browsing requests (no search text, store_id present, and no brand, category, productType, collection, attributes, on_sale, or low_stock filter active) serve aggregations from a pre-generated per-store manifest when one exists; availability flags are recalculated against current store inventory either way. Any of those filters falls back to real-time aggregations. Staff and POS-session callers additionally receive stock_available on each row; everyone else keeps is_in_stock and store_name only. See Smart Filter Service.


Multi-Store Inventory Queries

Get Inventory for a Variant Across All Stores

Use Case: Dashboard product detail dialog showing inventory by store.

GET /inventory-levels/by_product/?variant_sku=SKU-TSHIRT-RED-L
Authorization: Bearer <token>
{
  "product_id": null,
  "variant_sku": "SKU-TSHIRT-RED-L",
  "stores_count": 2,
  "total_stock_on_hand": 250,
  "total_stock_reserved": 25,
  "total_available": 225,
  "inventory_levels": [
    {
      "id": "c3d4e5f6-0000-0000-0000-000000000003",
      "variant_sku": "SKU-TSHIRT-RED-L",
      "variant_name": "Red / Large",
      "store_name": "Downtown Store",
      "stock_initial": 100,
      "stock_on_hand": 100,
      "stock_reserved": 10,
      "stock_lifetime_received": 150,
      "available_quantity": 90,
      "reorder_point": 10,
      "stock_status": "in_stock"
    },
    {
      "id": "d4e5f6a7-0000-0000-0000-000000000004",
      "variant_sku": "SKU-TSHIRT-RED-L",
      "variant_name": "Red / Large",
      "store_name": "Mall Store",
      "stock_initial": 100,
      "stock_on_hand": 150,
      "stock_reserved": 15,
      "stock_lifetime_received": 200,
      "available_quantity": 135,
      "reorder_point": 10,
      "stock_status": "in_stock"
    }
  ]
}

Each entry is the full InventoryLevelSerializer shape (costing, reorder settings, and analytics fields omitted here for brevity, but present in real responses). At least one of product_id or variant_sku is required; otherwise the endpoint returns 400.

Get All Inventory for a Single Store

Use Case: Store manager dashboard showing every tracked item at their location.

GET /inventory-levels/by_store/?store_id=550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer <token>
{
  "store_id": "550e8400-e29b-41d4-a716-446655440000",
  "total_items": 1234,
  "total_stock_on_hand": 5678,
  "total_stock_reserved": 123,
  "total_available": 5555,
  "inventory_levels": [
    {
      "id": "c3d4e5f6-0000-0000-0000-000000000003",
      "variant_sku": "SKU-TSHIRT-RED-L",
      "variant_name": "Red / Large",
      "stock_on_hand": 100,
      "stock_reserved": 10,
      "available_quantity": 90,
      "reorder_point": 10,
      "stock_status": "in_stock"
    }
  ]
}

store_id is required (400 without it) and must be the store's Django UUID: this action re-filters on the raw parameter, so a Sanity ID fails with a server error here. The Sanity-ID dual shape works on the list endpoint (GET /inventory-levels/?store_id=) only. The role-aware store gate applies: staff callers with non-empty store assignments must be assigned to the target store.

Low Stock Alerts

Use Case: Reorder report for the purchasing team.

GET /inventory-levels/low_stock_alerts/?threshold=10
Authorization: Bearer <token>
{
  "threshold": 10,
  "low_stock_count": 23,
  "low_stock_items": [
    {
      "id": "e5f6a7b8-0000-0000-0000-000000000005",
      "product_name": "Classic T-Shirt",
      "variant_sku": "SKU-TSHIRT-RED-L",
      "variant_name": "Red / Large",
      "store_name": "Downtown Store",
      "stock_on_hand": 5,
      "stock_reserved": 0,
      "available_quantity": 5,
      "reorder_point": 10,
      "reorder_quantity": 50,
      "stock_status": "low_stock"
    }
  ]
}

Matches 0 < stock_on_hand <= threshold, ordered by stock_on_hand ascending. threshold defaults to 10.

Inventory Summary

Use Case: Headline numbers for an inventory dashboard, filterable by any list parameter.

GET /inventory-levels/summary/?store_id=550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer <token>
{
  "total_items": 1234,
  "total_stock_on_hand": 45678,
  "total_stock_reserved": 1234,
  "total_available_quantity": 44444,
  "low_stock_count": 23,
  "out_of_stock_count": 5,
  "total_inventory_value": 456789.00,
  "low_stock_threshold": 10
}

total_inventory_value sums stock_on_hand * cost_per_unit over rows with a cost set. For richer analytics (health, turnover, dead stock, movement summaries) use the /inventory-levels/analytics/... endpoints; see Analytics Service.


Inventory Mutation Workflow

The three mutation actions require IsEmployeeOrManager and run inside a database transaction with a row lock on the target record, so concurrent writes serialize instead of clobbering each other. Adjust live stock through these endpoints rather than direct ORM writes; the endpoints are the locked write path.

Reserve Stock During Checkout

POST /inventory-levels/c3d4e5f6-0000-0000-0000-000000000003/reserve_quantity/
Authorization: Bearer <token>
Content-Type: application/json

{"quantity": 3}
{
  "message": "Inventory reserved successfully",
  "stock_reserved": 3,
  "total_reserved": 13,
  "available_quantity": 87
}

Returns 400 for a non-positive or non-integer quantity, or when the request exceeds available stock (stock_on_hand - stock_reserved); 404 when the record does not exist.

Release a Reservation on Cart Abandonment

POST /inventory-levels/c3d4e5f6-0000-0000-0000-000000000003/release_reservation/
Authorization: Bearer <token>
Content-Type: application/json

{"quantity": 3}
{
  "message": "Inventory reservation released successfully",
  "quantity_released": 3,
  "total_reserved": 10,
  "available_quantity": 90
}

Returns 400 when releasing more than is currently reserved.

Adjust On-Hand Quantity

Use Case: Shrinkage, damage, or a physical count correction.

POST /inventory-levels/c3d4e5f6-0000-0000-0000-000000000003/adjust_quantity/
Authorization: Bearer <token>
Content-Type: application/json

{"adjustment": -5, "reason": "Damaged stock"}
{
  "message": "Inventory adjusted successfully",
  "old_quantity": 100,
  "new_quantity": 95,
  "adjustment": -5,
  "reason": "Damaged stock"
}

Returns 400 for a non-integer adjustment or one that would drive stock_on_hand negative.


Dashboard Summaries

Catalog Dashboard Summary

Use Case: The headline tiles on the products dashboard. Requires IsEmployeeOrManager.

GET /products/dashboard_summary/
Authorization: Bearer <token>
{
  "total_products": 150,
  "visible_products": 142,
  "hidden_products": 8,
  "total_variants": 487,
  "featured_variants": 32,
  "on_sale_variants": 54,
  "total_inventory": 45678,
  "in_stock_products": 130,
  "low_stock_products": 9,
  "out_of_stock_products": 11,
  "in_stock_variants": 440,
  "low_stock_variants": 28,
  "out_of_stock_variants": 19
}

total_inventory and the stock buckets read live InventoryLevel.stock_on_hand sums across non-digital variants, not the stock_initial snapshots. The low-stock threshold is 10. For sales-based product analytics (/products/analytics/top-products/ and friends) see Analytics Service.


ORM Query Patterns

Aggregate Inventory Across Stores for a SKU

from django.db.models import Sum
from nextango.apps.products.models import InventoryLevel

inventory_levels = InventoryLevel.objects.filter(
    variant_sku__iexact='SKU-TSHIRT-RED-L',
    is_active=True,
).select_related('store', 'variant')

totals = inventory_levels.aggregate(
    total_on_hand=Sum('stock_on_hand'),
    total_reserved=Sum('stock_reserved'),
)

available = (totals['total_on_hand'] or 0) - (totals['total_reserved'] or 0)

Prefetch Inventory Levels Through the Variant FK

InventoryLevel.variant is a real foreign key with related_name='inventory_levels', so variant listings prefetch their store inventory in one extra query instead of one query per row.

from nextango.apps.products.models import ProductVariant

variants = ProductVariant.objects.select_related('product').prefetch_related(
    'inventory_levels__store'
).filter(product__hidden=False)

for variant in variants:
    total_available = sum(
        inv.stock_available
        for inv in variant.inventory_levels.all()
        if inv.is_active
    )
    print(f"{variant.sku}: {total_available} units available")

stock_available is a model property computed as max(0, stock_on_hand - stock_reserved). Note the related names: a product's variant records hang off product.variant_records (the variants attribute on Product is the Sanity JSONField, not a manager), and inventory rows hang off variant.inventory_levels.

Find Variants Available in Multiple Stores

from django.db.models import Count, Q
from nextango.apps.products.models import ProductVariant

multi_store_variants = ProductVariant.objects.annotate(
    store_count=Count(
        'inventory_levels',
        filter=Q(
            inventory_levels__stock_on_hand__gt=0,
            inventory_levels__is_active=True,
        ),
    )
).filter(store_count__gte=3).select_related('product')

For "is this variant purchasable at store X" checks, use the availability seam in products/availability.py rather than hand-rolled filters: a variant is available at a store when it has an active InventoryLevel there with stock_on_hand strictly above stock_reserved.


Was this page helpful?