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
SanityIntegratedModelsubclass 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
| Field | Type | Description |
|---|---|---|
name | CharField(255) | Jurisdiction name, e.g. "Franklin County, OH" |
tax_rates_data | JSONField (list) | Array of tax rate objects from the Sanity taxJurisdiction schema |
total_rate | DecimalField(5,4) | Computed total rate as a decimal (0.0825 for 8.25%); recalculated on save |
effective_date | DateField | When the rates became or become effective |
notes | TextField | Additional tax requirements or conditions |
is_exempt | BooleanField | When 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.
| Field | Type | Description |
|---|---|---|
site_code | CharField(10) | Unique short code for reports and integrations, e.g. "DTN" |
name | CharField(255) | Display name for the grouping |
description | TextField | Brief overview |
is_active | BooleanField | Whether the site is operational (default True) |
address_data | JSONField (dict) | Address object (street, city, state, postalCode, country) |
branding_data | JSONField (dict) | Branding object (logo, colors, tagline) |
domain | CharField(255) | Custom storefront domain, e.g. downtown.myshop.com |
seo_data | JSONField (dict) | SEO metadata object |
is_default | BooleanField | The single fallback site; enforced by a DB partial unique constraint (stores_site_single_default) |
domain_verified | BooleanField | Whether the domain DNS record is confirmed pointing at this server |
ssl_status | CharField(20) | pending, active, failed, or not_applicable |
nginx_status | CharField(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
| Field | Type | Description |
|---|---|---|
store_code | CharField(20) | Unique business identifier |
slug | SlugField(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 |
name | CharField(255) | Display name |
address_data | JSONField (dict) | Address object from Sanity |
phone | CharField(20) | Contact phone |
default_currency | CharField(3) | ISO 4217 code, default USD |
tax_jurisdiction | FK to TaxJurisdiction | Targets sanity_id (to_field), PROTECT on delete, nullable |
is_active | BooleanField | Whether the store is operational (default True) |
is_default | BooleanField | The store loaded first in the dashboard; at most one store should be True. Auto-cleared on other stores by StoreSerializer.update() |
timezone | CharField(50) | Store timezone for hours and reports |
accepts_online_returns | BooleanField | Default True |
requires_appointments | BooleanField | Default False |
permitted_fulfillment_methods | JSONField (list) | Fulfillment method strings allowed at this store, e.g. ["counter_sale", "pickup", "delivery"]. Empty list means all methods permitted |
settings | JSONField (dict) | The Sanity storeSettings object (pickupEnabled, returnToInventory, payAtStoreEnabled, and so on) |
operating_hours | JSONField (list) | Array of per-day operating hours objects |
site | FK to Site | Targets Site.sanity_id, SET_NULL, nullable. Dormant C4 seam; not populated from Sanity |
manager_ids | JSONField (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 overaddress_data;address_postal_codeaccepts bothpostal_codeandpostalCodekeys.tax_rate(models.py:371-389): the jurisdiction'stotal_rateas a float. Returns0.0and logs a warning when no jurisdiction is assigned or the jurisdiction has no calculated rate; returns0.0without a warning when the jurisdictionis_exempt. No exception is raised, so transactions proceed with zero tax.has_valid_tax_configuration(models.py:391-401):Truewhen a jurisdiction is assigned and either explicitly exempt or carrying a positivetotal_rate. Use this to diagnose unexpected zero-tax stores.
Pickup helper methods
is_pickup_enabled()readssettings.pickupEnabled(defaultFalse).should_return_to_inventory()readssettings.returnToInventory(defaultTrue).is_pay_at_store_enabled()readssettings.payAtStoreEnabled; requirespickupEnabledto beTrue(defaultFalse).get_operating_hours_for_day(day_name)returns the matching entry fromoperating_hours, orNone.get_pickup_hours_for_day(day_name)(models.py:452-489) returns{'open', 'close', 'interval'}. Pickup-specific hours fromstorePickupInformationapply only when the day'sdifferentHoursForPickuptoggle is explicitlyTrue; 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)
| Field | Type | Description |
|---|---|---|
check_id | CharField(100) | Unique business identifier for the check |
zone | FK to zones.Zone | PROTECT; the zone audited |
task | FK to tasks.Task | SET_NULL, nullable; the task that produced the check |
employee | FK to users.Employee | PROTECT; who performed the check |
status | CharField(20) | pass, fail, or pending |
checklist_results | JSONField (dict) | Detailed per-item results |
notes | TextField | Auditor notes |
photos | JSONField (list) | Photo references |
timestamp | DateTimeField | Set 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)
Related Documentation
- Serializers
- Views
- Services
- Signals
- Products - InventoryLevel for store-scoped inventory
- Zones