Stores Domain - Serializers Documentation

Source: api/nextango/apps/stores/serializers.py Last Updated: 2026-07-09

Overview

The Stores serializers transform between Django model fields and Sanity's camelCase nested payloads. The API surface is snake_case with a handful of computed camelCase blocks (settings, regulatedCompliance) that mirror the Sanity schema.

Serializers in this module:

  • TaxJurisdictionSerializer: tax jurisdiction data with camelCase mapping
  • SiteSerializer: Django-owned Site REST surface (no Sanity webhook feed)
  • StoreSerializer: conditional webhook processing, settings flattening, compliance-profile bridging
  • ZoneSerializer, ComplianceCheckSerializer, PlanogramSerializer: lightweight model serializers

TaxJurisdictionSerializer

Model: TaxJurisdiction

Source: serializers.py:6-47

FieldAccess
idread-only
namewritable
tax_rates_datawritable
total_rateread-only (recalculated by the model on save)
effective_datewritable
noteswritable
is_exemptwritable
sanity_idwritable

to_internal_value accepts both shapes: the Sanity webhook camelCase keys (taxRates to tax_rates_data, effectiveDate to effective_date, isExempt to is_exempt) and the direct snake_case keys used by DRF API clients (name, notes, is_exempt, tax_rates_data, effective_date). Only keys present in the payload are processed; everything else keeps its current value.

SiteSerializer

Model: Site

Source: serializers.py:49-115

Serves the Django-owned /sites/ REST surface (SiteViewSet: branding and config lookup by domain). Site no longer syncs to or from Sanity; the camelCase handling in to_internal_value (siteCode, isActive, isDefault, plus nested address/branding/seo objects) only tolerates legacy payload shapes and is not fed by a Sanity webhook.

FieldAccess
site_code, name, description, is_active, is_defaultwritable
address_data, branding_data, domain, seo_datawritable
domain_verified, ssl_status, nginx_statuswritable
id, sanity_id, sync metadata (last_synced_at, sync_enabled, sync_status), created_at, updated_atread-only

update() (serializers.py:103-115) enforces the single-default invariant: when a site becomes the default, is_default is cleared on every other site via a queryset .update() (which deliberately does not fire post_save signals on the others).

StoreSerializer

Model: Store

Source: serializers.py:118-694

Handles both API clients and inbound Sanity webhook payloads. Its design principles: process only the fields present in the payload (partial updates preserve everything else), flatten the Sanity settings object into individual model fields where one exists, resolve Sanity references to Django FKs, and bridge the regulated-compliance block to the store's compliance profile.

Fields

FieldAccessNotes
store_code, name, address_data, phonewritable
slugread-onlyDjango-derived on save, never client-authored (serializers.py:384-386)
sitewritableDormant C4 seam; not populated from Sanity payloads
default_currency, is_active, is_default, timezone, accepts_online_returns, requires_appointmentswritable
operating_hourswritablevalidated array
manager_idscomputed on readSanity user refs resolved to Django User UUID strings (get_manager_ids, serializers.py:324-344); unresolved refs are dropped with a warning
settingscomputed on readsee below
settings_datawrite-onlycarries the full inbound settings object
regulatedCompliancecomputed on readsee below
regulated_compliance_datawrite-onlycarries the inbound regulatedCompliance block
tax_jurisdictionwritableFK by sanity_id
tax_jurisdiction_name, total_tax_rate, tax_rateread-onlypulled from the related jurisdiction
id, sanity_id, sync metadata, created_at, updated_atread-only

The computed settings object

get_settings (serializers.py:299-322) returns the stored settings JSONField with the moved compliance keys stripped, plus computed defaults (isActive, timezone, defaultCurrency, acceptsOnlineReturns, requiresAppointments) and a taxJurisdiction reference object when a jurisdiction is assigned. The individual model fields are the source of truth; settings output reconstructs the Sanity shape.

Regulated compliance handling

The regulated-retail policy block (age gate, credential types, permits) is authored in Sanity at top-level store.regulatedCompliance, off the agnostic storeSettings object.

  • Read: get_regulatedCompliance (serializers.py:231-246) sources the block from the store's compliance profile and returns {'requireAgeGate', 'requiredCredentialTypes', 'secondaryReviewCredentialTypes', 'permits'}, or None for a store with no profile (non-regulated vertical).
  • Write: create() and update() extract compliance updates by dual-read (_extract_compliance_updates, serializers.py:248-278): the new regulatedCompliance block is preferred, with a legacy fallback to the same keys under settings so existing Sanity content is never orphaned. Extracted updates are written to the profile via get_active_vertical_provider().upsert_store_profile(), not to the Store row.
  • Scrub: the four moved keys (_MOVED_COMPLIANCE_KEYS, serializers.py:283-288) are stripped from settings before storage and before output, so they never ride settings post-move.
  • Validation: _validate_credential_policy_fields (serializers.py:150-170) rejects non-list or non-string-item credential-type payloads. _validate_permits (serializers.py:174-229) rejects malformed permit arrays: each entry must be an object with a string _key, a permitType, a status in {active, suspended, revoked}, and a payload that passes the compliance permit validators.

to_internal_value transformations

Source: serializers.py:456-574

Only keys present in the payload are transformed:

  1. Top-level camelCase mappings: storeCode, managerIds, operatingHours, isDefault.
  2. Direct fields: name, phone, is_default.
  3. address object: recursive camelCase-to-snake_case transformation into address_data.
  4. settings object: validated (credential policy + permits), stored whole via settings_data, and flattened to the individual fields timezone, default_currency, accepts_online_returns, requires_appointments, is_active.
  5. settings.taxJurisdiction._ref: resolved through the shared serializer reference resolver to a TaxJurisdiction and assigned by sanity_id. An unresolvable reference logs a warning and leaves the field unset.
  6. regulatedCompliance object: validated and stashed in regulated_compliance_data.
  7. slug is not accepted from inbound payloads; site references are not resolved from Sanity (sync severed).

Validation methods

  • validate_address requires street, city, state, country, and a postal code under either postalCode or postal_code.
  • validate_operating_hours requires an array of objects each carrying day, open, and close.
  • validate_manager_ids requires an array of strings or {'_ref': ...} reference objects.

update() default-store invariant

Source: serializers.py:604-640

When a store becomes the default, is_default is cleared on every other default store using per-instance .save(update_fields=['is_default']) so post_save fires and the change syncs outbound to Sanity. Compare SiteSerializer.update, which deliberately suppresses signals for the same operation.

ZoneSerializer

Source: serializers.py:696-701

Exposes zones.Zone with fields id, sanity_id, store_location, store_name (read-only, from store_location.name), zone_name, zone_type, is_shown. Note: the stores ViewSets use the zones app's own ZoneSerializer (stores/views.py:14); this local one is not wired into the stores views.

ComplianceCheckSerializer

Source: serializers.py:703-711

Exposes ComplianceCheck with zone_name (from zone.zone_name) and employee_name (from employee.name) as read-only computed fields. Writable: check_id, zone, employee, status, checklist_results, notes, photos. Read-only: timestamp.

PlanogramSerializer

Source: serializers.py:713-716

Exposes zones.Planogram with fields id, sanity_id, planogram_name, version. As with ZoneSerializer, the stores views import the zones app's PlanogramSerializer instead; this local one is not wired into the stores views.

Was this page helpful?