Stores Domain - Models Documentation

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

Overview

The Stores domain models physical store locations, the tax jurisdictions they bill under, the Django-owned Site grouping layer, and per-zone compliance audit records.

Ownership split at a glance:

  • Store and TaxJurisdiction are Sanity-owned: authored in Sanity Studio, mirrored in Django, synced bidirectionally.
  • Site is Django-owned: it does not sync to or from Sanity in either direction. It remains a SanityIntegratedModel subclass only to keep the class and table stable as a dormant seam for post-MVP tenancy (C4).
  • ComplianceCheck is created in Django and synced outbound to Sanity.

TaxJurisdiction

Purpose: Tax rate configuration for a geographic jurisdiction, with multiple nested rate components.

Source: models.py:8-93

Base class: SanityIntegratedModel

FieldTypeDescription
nameCharField(255)Jurisdiction name, e.g. "Franklin County, OH"
tax_rates_dataJSONField (list)Array of tax rate objects from the Sanity taxJurisdiction schema
total_rateDecimalField(5,4)Computed total rate as a decimal (0.0825 for 8.25%); recalculated on save
effective_dateDateFieldWhen the rates became or become effective
notesTextFieldAdditional tax requirements or conditions
is_exemptBooleanFieldWhen True, the jurisdiction is deliberately tax-exempt: rate treated as 0% with no warnings. The store still requires a jurisdiction assigned; this flags the exemption as intentional, not a configuration gap

calculate_total_rate() (models.py:54-75) sums the active entries in tax_rates_data, accepting rate or ratePercentage and isActive or is_active key spellings, converts percentage form (8.25) to decimal form (0.0825), and quantizes to 4 decimal places. The model recalculates total_rate on every full save, and on partial saves whenever tax_rates_data is in update_fields.

Exposed by TaxJurisdictionSerializer; changes propagate to related stores via sync_taxjurisdiction_to_sanity.

Site

Purpose: Business grouping layer between companyInfo (singleton) and store (individual location), providing per-site branding, a storefront domain, and cascading defaults.

Source: models.py:95-210

Base class: SanityIntegratedModel (dormant seam; see ownership note below)

Django-owned, no Sanity sync. As of the store-as-url minimal reversal, Site no longer participates in Sanity CMS sync in either direction: its Sanity site document type, the store.site reference, and all outbound and inbound sync wiring were removed (models.py:95-109 docstring; sync severance confirmed at integrations/sync/store_sync.py:94 and :554-555). It is served purely Django-side through the REST endpoint /sites/ (SiteViewSet, registered on the root router at nextango/urls.py:60) and through domain routing (SiteResolver). Do not re-add Sanity sync here without reopening the tenancy design.

FieldTypeDescription
site_codeCharField(10)Unique short code for reports and integrations, e.g. "DTN"
nameCharField(255)Display name for the grouping
descriptionTextFieldBrief overview
is_activeBooleanFieldWhether the site is operational (default True)
address_dataJSONField (dict)Address object (street, city, state, postalCode, country)
branding_dataJSONField (dict)Branding object (logo, colors, tagline)
domainCharField(255)Custom storefront domain, e.g. downtown.myshop.com
seo_dataJSONField (dict)SEO metadata object
is_defaultBooleanFieldThe single fallback site; enforced by a DB partial unique constraint (stores_site_single_default)
domain_verifiedBooleanFieldWhether the domain DNS record is confirmed pointing at this server
ssl_statusCharField(20)pending, active, failed, or not_applicable
nginx_statusCharField(20)not_configured, provisioning, active, or failed

get_stores() (models.py:199-210) returns the stores belonging to the site: stores whose site FK points here, plus, for the default site only, stores with no site set.

Multi-site caveat: the companyInfo to site to store host-routing concept is only partially built. The storefront is effectively single-site today; SiteResolver and the domain fields are forward-only infrastructure, not live multi-site behavior.

Exposed by SiteSerializer.

Store

Purpose: A physical store location, mirroring Sanity's store schema with full bidirectional sync.

Source: models.py:213-502

Base class: SanityIntegratedModel

FieldTypeDescription
store_codeCharField(20)Unique business identifier
slugSlugField(96)URL-friendly storefront path segment. Django-owned and derived from the store name on save (name, then store_code, then a unique token); never authored in Sanity and never synced. See derive_store_slug_on_save
nameCharField(255)Display name
address_dataJSONField (dict)Address object from Sanity
phoneCharField(20)Contact phone
default_currencyCharField(3)ISO 4217 code, default USD
tax_jurisdictionFK to TaxJurisdictionTargets sanity_id (to_field), PROTECT on delete, nullable
is_activeBooleanFieldWhether the store is operational (default True)
is_defaultBooleanFieldThe store loaded first in the dashboard; at most one store should be True. Auto-cleared on other stores by StoreSerializer.update()
timezoneCharField(50)Store timezone for hours and reports
accepts_online_returnsBooleanFieldDefault True
requires_appointmentsBooleanFieldDefault False
permitted_fulfillment_methodsJSONField (list)Fulfillment method strings allowed at this store, e.g. ["counter_sale", "pickup", "delivery"]. Empty list means all methods permitted
settingsJSONField (dict)The Sanity storeSettings object (pickupEnabled, returnToInventory, payAtStoreEnabled, and so on)
operating_hoursJSONField (list)Array of per-day operating hours objects
siteFK to SiteTargets Site.sanity_id, SET_NULL, nullable. Dormant C4 seam; not populated from Sanity
manager_idsJSONField (list)Manager user references, bidirectionally synced with the manager role's store assignments in the Users domain

Store settings paths are exact

Settings live at nested paths inside the settings JSONField using the exact Sanity field names: settings.pickupEnabled, settings.returnToInventory, settings.payAtStoreEnabled. The regulated-retail compliance keys (requireAgeGate, requiredCredentialTypes, secondaryReviewCredentialTypes, permits) no longer live under settings: they moved to the top-level regulatedCompliance object and are stored on the store's compliance profile, not on the Store row. The serializer scrubs them from settings and exposes them under regulatedCompliance; see serializers.md.

Properties

  • address_street, address_city, address_state, address_postal_code, address_country (models.py:341-369): flat accessors over address_data; address_postal_code accepts both postal_code and postalCode keys.
  • tax_rate (models.py:371-389): the jurisdiction's total_rate as a float. Returns 0.0 and logs a warning when no jurisdiction is assigned or the jurisdiction has no calculated rate; returns 0.0 without a warning when the jurisdiction is_exempt. No exception is raised, so transactions proceed with zero tax.
  • has_valid_tax_configuration (models.py:391-401): True when a jurisdiction is assigned and either explicitly exempt or carrying a positive total_rate. Use this to diagnose unexpected zero-tax stores.

Pickup helper methods

  • is_pickup_enabled() reads settings.pickupEnabled (default False).
  • should_return_to_inventory() reads settings.returnToInventory (default True).
  • is_pay_at_store_enabled() reads settings.payAtStoreEnabled; requires pickupEnabled to be True (default False).
  • get_operating_hours_for_day(day_name) returns the matching entry from operating_hours, or None.
  • get_pickup_hours_for_day(day_name) (models.py:452-489) returns {'open', 'close', 'interval'}. Pickup-specific hours from storePickupInformation apply only when the day's differentHoursForPickup toggle is explicitly True; otherwise operating hours are used with a default 60-minute interval, and any stale pickup data is ignored.
  • get_pickup_interval_for_day(day_name) returns just the interval in minutes.

Exposed by StoreSerializer; summarized by StoreService.

SiteResolver

Source: models.py:504-521

Domain-routing helper. resolve_site_for_domain(domain) returns the active Site whose domain matches exactly, falling back to the active default site, or None when no active sites exist. Built for host routing; wired into middleware when multi-site goes live.

ComplianceCheck

Purpose: The recorded outcome of a compliance audit for a store zone.

Source: models.py:524-541

Base class: SanityIntegratedModel (synced outbound to Sanity as the complianceCheck document type; see sync_compliance_check_to_sanity)

FieldTypeDescription
check_idCharField(100)Unique business identifier for the check
zoneFK to zones.ZonePROTECT; the zone audited
taskFK to tasks.TaskSET_NULL, nullable; the task that produced the check
employeeFK to users.EmployeePROTECT; who performed the check
statusCharField(20)pass, fail, or pending
checklist_resultsJSONField (dict)Detailed per-item results
notesTextFieldAuditor notes
photosJSONField (list)Photo references
timestampDateTimeFieldSet on creation

Exposed by ComplianceCheckSerializer.

Model Relationships

TaxJurisdiction  <-(FK, to_field=sanity_id, PROTECT)-  Store
Site             <-(FK, to_field=sanity_id, SET_NULL)- Store   (dormant C4 seam)
Store            <-(FK store_location)-                Zone (zones app)
Zone             <-(FK, PROTECT)-                      ComplianceCheck
Employee         <-(FK, PROTECT)-                      ComplianceCheck
Store.manager_ids (JSON refs)  <->  manager role store assignments (users app)

Was this page helpful?