Products - Services Documentation

Location: api/nextango/apps/products/services.py, api/nextango/apps/products/availability.py Last Updated: 2026-07-06

Overview

The Products services layer holds business logic for product display, dashboard metrics, SKU lookup, Sanity CMS sync, attribute filtering, and Elasticsearch search. services.py defines three service classes plus shared query expression helpers:

  • ProductService - product display, dashboard metrics, SKU lookup, and Sanity sync
  • AttributeService - attribute filtering and synchronization
  • ProductSearchService - Elasticsearch-backed product search
  • Query expression helpers - live-inventory annotations and a NULL-safe valuation expression shared across readers

availability.py is the store-availability seam. Every store-scoped "is this purchasable at store X" question routes through it, anchored on InventoryLevel records. Analytics and smart filter services live in their own modules, documented on the Analytics Service and Smart Filter Service pages.


Query Expression Helpers

Module-level functions shared by dashboard readers and analytics call sites.

variant_live_inventory_annotation()

Location: services.py:19-43

Returns an OuterRef/Subquery annotation that sums InventoryLevel.stock_on_hand across active inventory levels per ProductVariant, coalesced to 0. Dashboard readers use it so reported inventory is live stock, not the stock_initial snapshot. The helper aggregates physical inventory only; callers preserve the unlimited-stock semantics for digital variants through their own digital_inventory filters.

product_live_inventory_annotation()

Location: services.py:46-64

Same shape per Product: sums InventoryLevel.stock_on_hand across active inventory levels of the product's non-digital variants, coalesced to 0.

inventory_valuation_expression()

Location: services.py:67-86

NULL-safe per-row inventory valuation: stock_on_hand multiplied by Coalesce(current_cost, 0.00), typed as DecimalField end to end. Rows with a NULL current_cost contribute zero explicitly instead of producing NULL products that Sum() silently omits. Callers wrap the expression with Sum(...) for aggregates or use it directly in .annotate() for row-level display.


ProductService

Location: services.py:89-492

Service class for product-related business logic. Handles the dual structure: Product.variants JSONField plus ProductVariant model records.

Methods

get_products_for_display(filters=None)

Location: services.py:95-231 Decorators: @log_execution_time, @cache_result(timeout=300) varying on filters

Fetches products with calculated metrics for dashboard and storefront display. Reads from the replica database when USE_REPLICA_DATABASE is configured, prefetches variant records with their active inventory levels to avoid N+1 queries, and orders results by name.

Parameters:

  • filters (dict, optional): Filter criteria

Filter Options:

{
    'show_hidden': False,       # Include hidden products
    'brand': 'brand_uuid',      # Django Brand UUID, resolved to brand_sanity_id
    'product_type': 'type_uuid',# Django ProductType UUID, resolved to product_type_sanity_id
    'in_stock': True,           # Filter by inventory availability
    'featured': True            # Filter by featured variants
}

The brand and product_type filters resolve the Django UUID to the denormalized indexed brand_sanity_id / product_type_sanity_id columns. An unknown UUID returns an empty result, not an error.

Returns:

  • list: List of product data dictionaries with calculated fields

Response Structure:

{
    'id': 'uuid',
    'sanity_id': 'sanity_document_id',
    'name': 'Product Name',
    'slug': 'product-slug',
    'hidden': False,
    'shortDescription': 'Description text',
    'is_in_stock': True,
    'total_inventory': 150,
    'variant_count': 4,
    'featured_variant_count': 2,
    'price_range': {
        'min_price': 29.99,
        'max_price': 59.99,
        'min_sale_price': 24.99,
        'max_sale_price': 49.99
    },
    'organization': {...},
    'seo': {...},
    'created_at': 'timestamp',
    'updated_at': 'timestamp'
}

total_inventory sums live InventoryLevel.stock_on_hand across the active inventory levels of the product's non-digital variants. Digital variants are excluded from the total but any digital variant marks the product in stock regardless of the count. Price ranges come from the variant records, with sale prices considered only where on_sale is set.

Usage:

from apps.products.services import ProductService

# Get all visible products
products = ProductService.get_products_for_display()

# Get in-stock products from a specific brand
products = ProductService.get_products_for_display({
    'brand': brand.id,
    'in_stock': True
})

get_dashboard_summary()

Location: services.py:233-297 Decorators: @log_execution_time, @cache_result(timeout=300)

Calculates dashboard summary statistics across Product and ProductVariant models. Inventory figures read live InventoryLevel.stock_on_hand through the annotation helpers above, not the stock_initial snapshot. Reads from the replica database when configured.

Returns:

  • dict: Dashboard summary data

Response Structure:

{
    # Product statistics
    'total_products': 150,
    'visible_products': 145,
    'hidden_products': 5,

    # Variant statistics
    'total_variants': 450,
    'featured_variants': 85,
    'on_sale_variants': 120,

    # Inventory statistics
    'total_inventory': 5420,
    'in_stock_products': 135,
    'low_stock_products': 25,
    'out_of_stock_products': 15,
    'in_stock_variants': 380,
    'low_stock_variants': 45,
    'out_of_stock_variants': 70
}

Low stock means live inventory above 0 and at or below 10, at both variant and product level. A digital variant counts as in stock regardless of inventory levels; digital variants are excluded from total_inventory and from the low-stock and out-of-stock counts.


get_product_by_sku(sku)

Location: services.py:299-314

Finds a product and specific variant by SKU using the ProductVariant model. A single query with select_related returns both objects.

Parameters:

  • sku (str): The SKU to search for

Returns:

  • tuple: (Product, ProductVariant) if found, (None, None) if not found

get_variants_by_skus(skus)

Location: services.py:316-329

Bulk lookup of ProductVariant records by exact SKU match, with product data joined via select_related.

Parameters:

  • skus (list): List of SKUs to look up

Returns:

  • dict: Mapping of SKU to ProductVariant instance. SKUs with no match are absent from the dict.

sync_from_sanity()

Location: services.py:331-388

Manual sync path that polls the Sanity client directly and creates or updates both the Product.variants JSONField and ProductVariant records. This is not the canonical product sync entrypoint; production webhook traffic flows through ProductSyncHandler in integrations.sync.product_sync. This method backs the admin-triggered sync action.

Product upserts use update_or_create keyed on sanity_id, so the operation is idempotent, and each product sync calls sync_variants_to_model_records() to materialize variant records. Errors on individual products are counted and the sync continues.

Returns:

  • dict: Sync result summary

Response Structure:

{
    'success': True,
    'synced_count': 150,
    'error_count': 2,
    'total_attempted': 152,
    'variant_records_created': 450,
    'variant_records_updated': 125
}

A failure before the per-product loop returns {'success': False, 'error': '<message>'}.


sync_variant_to_jsonfield(variant)

Location: services.py:436-492

Syncs a ProductVariant model change back to the Product.variants JSONField (the reverse cascade in the three-source-of-truth contract). Called when ProductVariant records change outside of Sanity sync.

Parameters:

  • variant (ProductVariant): The variant that was updated

The method finds the matching entry in the JSONField by SKU and replaces it with a fresh snapshot, or appends the snapshot when the SKU is not present. The save touches only the variants field.

Variant Snapshot Structure:

{
    'name': 'Medium - Blue',
    'sku': 'SHIRT-MD-BLU',
    'featured': False,
    'onSale': True,
    'price': 29.99,
    'salePrice': 24.99,
    'stockInitial': 15,
    'digitalInventory': False,
    'attributes': [...],
    'dimensions': {...},
    'images': [...],
    'basePromoCode': {'_type': 'reference', '_ref': 'promo_id'}  # only when set
}

basePromoCode is omitted when the variant has no promo code; absence in the document is the correct shape for an unset Sanity reference.


AttributeService

Location: services.py:495-594

Service class for attribute-related business logic. Handles the Sanity-aligned Attribute model with embedded type definitions.

Methods

get_attributes_for_filters()

Location: services.py:501-531 Decorators: @log_execution_time, @cache_result(timeout=600)

Gets attributes that should be shown in product filters. Queries attributes with is_shown=True, ordered by display_order then name, and skips any attribute without an attribute_type definition.

Returns:

  • list: List of attribute data for building filter UI

Response Structure:

[
    {
        'id': 'uuid',
        'sanity_id': 'sanity_attribute_id',
        'name': 'Size',
        'featured': True,
        'displayOrder': 1,
        'type': 'attributeTextList',
        'options': ['Small', 'Medium', 'Large', 'X-Large'],
        'option_count': 4
    },
    {
        'id': 'uuid',
        'sanity_id': 'sanity_color_id',
        'name': 'Color',
        'featured': True,
        'displayOrder': 2,
        'type': 'attributeColorSwatch',
        'options': [
            {'name': 'Red', 'hex': '#FF0000', 'value': 'red'},
            {'name': 'Blue', 'hex': '#0000FF', 'value': 'blue'}
        ],
        'option_count': 2
    }
]

sync_from_sanity()

Location: services.py:533-567

Triggers sync of attributes from Sanity CMS. Upserts use update_or_create keyed on sanity_id; errors on individual attributes are counted and the sync continues.

Returns:

  • dict: Sync result summary

Response Structure:

{
    'success': True,
    'synced_count': 15,
    'error_count': 0,
    'total_attempted': 15
}

ProductSearchService

Location: services.py:597-746

Service class for Elasticsearch-based product search against the ProductDocument index, working with the dual structure (JSONField variants plus ProductVariant records).

Methods

search(query=None, filters=None, page=1, page_size=20)

Location: services.py:603-746

Performs product search using Elasticsearch with embedded variants. Without a query the search matches all documents; with a query it runs a multi-match with AUTO fuzziness. Hidden products are always excluded.

Parameters:

  • query (str, optional): Full-text search query
  • filters (dict, optional): Filter criteria
  • page (int): 1-based page number, mapped to ES from/size
  • page_size (int): Results per page

Filter Options:

{
    'in_stock': True,           # Boolean or string 'true'/'false'
    'category': 'category_ref', # organization.productType term
    'min_price': 19.99,         # Minimum price (float)
    'max_price': 99.99,         # Maximum price (float)
    'featured': True,           # Boolean or string 'true'/'false'
    'store_id': 'uuid_or_sanity_id'  # Store-scoped availability
}

Returns:

  • elasticsearch_dsl.response.Response: The requested page of hits. hits.total.value is the full match count regardless of page.

Search Fields:

[
    'name^3',                   # Product name (boosted 3x)
    'shortDescription^2',       # Product description (boosted 2x)
    'variants.name^2',          # Variant names (boosted 2x)
    'variants.sku^2',           # Variant SKUs (boosted 2x)
    'organization.tags'         # Organization tags
]

Store-scoped behavior: The store_id parameter accepts a Django UUID or a Sanity ID, resolved through resolve_store_id_param. A resolved store adds a nested filter on variants.inventory_by_store requiring both the store match and is_available=True, so a zero-stock or fully reserved entry never matches. When the search is store-scoped, the doc-level has_inventory term is dropped and in_stock is answered from live per-store inventory instead of the global snapshot. An unresolvable store_id applies no store filter; the search degrades to the unfiltered result rather than returning empty.

Other behavior:

  • Non-store-scoped in_stock filters on the doc-level has_inventory snapshot
  • Price filters run as nested range queries on variants.price; non-numeric values are ignored
  • Results sort by relevance, then name ascending
  • Pagination maps to ES from/size with a max_result_window of 10000. A page starting at or beyond the window returns zero hits but still reports an accurate total
  • Elasticsearch errors are re-raised for the caller to handle

Usage:

from apps.products.services import ProductSearchService

# Store-scoped search with pagination
response = ProductSearchService.search(
    query='shirt',
    filters={'in_stock': True, 'store_id': store.id},
    page=2,
    page_size=20,
)
for hit in response:
    print(f"{hit.name} ({hit.slug})")
print(f"Total matches: {response.hits.total.value}")

Store Availability Seam

Location: availability.py

The single place that answers "is this variant or product purchasable at store X". Availability is anchored on InventoryLevel, the authoritative store-specific record, never on Product.store_location (metadata) or ProductVariant.stock_initial (a one-time seed). Store-scoped consumers, including the catalog viewsets, search, the storefront detail and cart paths, and reconciliation, route through these helpers. A guard test (products/tests/test_store_availability_enforcement.py) forbids product views from re-deriving their own store-availability filters, so the boundary stays enforced by construction.

Functions

available_inventory_q(store)

Location: availability.py:19-22

Returns the canonical Q predicate: an active InventoryLevel at store with sellable quantity, meaning stock_on_hand strictly above stock_reserved.

Parameters:

  • store (Store): The store to test against

Returns:

  • Q: Predicate for use in InventoryLevel filters

inventory_available_at(store)

Location: availability.py:25-28

Returns the InventoryLevel queryset of rows sellable at store.

products_available_at(store, queryset=None)

Location: availability.py:31-42

Filters a Product queryset (all products when omitted) to products with at least one sellable variant at store, using a single Exists subquery on the indexed InventoryLevel.product FK.

Returns:

  • QuerySet[Product]

variants_available_at(store, queryset=None)

Location: availability.py:45-55

Filters a ProductVariant queryset (all variants when omitted) to variants sellable at store, via an Exists subquery matching InventoryLevel.variant_sku to the variant's sku.

Returns:

  • QuerySet[ProductVariant]

available_variant_skus_at(store, product=None)

Location: availability.py:58-65

Returns the set of variant SKUs sellable at store, optionally scoped to one product. Runs one query, then membership tests happen in memory, which suits filtering already-serialized variant_records on the detail path without N+1 queries.

Returns:

  • set[str]: SKUs with sellable inventory at the store

Caching and Monitoring

get_products_for_display and get_dashboard_summary cache results for 5 minutes; get_attributes_for_filters caches for 10 minutes. The display query caches per filter combination. These methods also log execution time through @log_execution_time.



File Reference

  • Query expression helpers: services.py:19-86
  • ProductService: services.py:89-492
  • AttributeService: services.py:495-594
  • ProductSearchService: services.py:597-746
  • Store availability seam: availability.py:1-65

Was this page helpful?