Stores - Multi-Location Management
Domain: Stores
Location: api/nextango/apps/stores/
Last Updated: 2026-07-09
Overview
The Stores app manages physical store locations: identity and address data, operating hours, tax jurisdiction assignment, manager assignments, store settings (pickup, returns, appointments), URL slugs for the storefront, and per-zone compliance audit records. Store and TaxJurisdiction sync bidirectionally with Sanity CMS; the Site grouping layer is Django-owned and does not sync.
Documentation Quick Links
| File | Description |
|---|---|
| models.md | Store, TaxJurisdiction, Site, SiteResolver, ComplianceCheck |
| serializers.md | Webhook processing, settings flattening, compliance-profile bridge |
| views.md | Live endpoints and ViewSets |
| services.md | StoreService summary and DashboardService aggregation |
| signals.md | Slug derivation, outbound sync, cache invalidation, inventory backfill |
| dashboard_home.md | Dashboard home endpoint schema |
| examples.md | Usage examples |
Ownership Model
- Store and TaxJurisdiction are authored in Sanity Studio and mirrored in Django with bidirectional sync.
- Site is Django-owned. It does not sync to or from Sanity in either direction; the Sanity
sitedocument type is dormant. Site is served only through the/sites/REST endpoint and is parked as a seam for post-MVP tenancy. - Store.slug is Django-owned even though Store syncs: it is derived from the store name on save and never authored in or synced to Sanity.
- ComplianceCheck records are created in Django and synced outbound to Sanity asynchronously.
- The store-as-url multi-site concept (companyInfo to site to store host routing) was reversed to a minimal footprint; the storefront is effectively single-site today. Do not treat multi-site host routing as live behavior.
API Endpoints
All Stores routes are root-router registrations (nextango/urls.py:60-63); the app-level stores/urls.py router is not mounted.
| Endpoint | Methods | Description |
|---|---|---|
/stores/ | GET, POST | List (cached 600s, active stores only) and create |
/stores/{store_code}/ | GET, PUT, PATCH, DELETE | Retrieve (summary shape), update, delete. Lookup accepts store_code, Django UUID, or Sanity ID |
/stores/{store_code}/dashboard_home/ | GET | Today's KPIs, top products, low stock, recent transactions |
/sites/ | GET, POST | Django-owned site grouping, filterable by ?domain=, ?is_default= |
/sites/{id}/ | GET, PUT, PATCH, DELETE | Individual site operations |
/tax-jurisdictions/ | GET, POST | Tax jurisdiction list (unpaginated array) and create |
/tax-jurisdictions/{sanity_id}/ | GET, PUT, PATCH, DELETE | Lookup by Sanity document ID |
/compliance-checks/ | GET, POST | Compliance audit records |
/compliance-checks/{check_id}/ | GET, PUT, PATCH, DELETE | Lookup by business check_id |
Zone and planogram endpoints live in the Zones domain at /zones/ and /planograms/.
All endpoints inherit the platform default permission: IsActiveUser in production, AllowAny in DEBUG (settings/base.py:251-253).
Full endpoint documentation: views.md
Architecture Highlights
Tax jurisdiction system
Stores reference a TaxJurisdiction by Sanity ID (PROTECT on delete). The jurisdiction's total_rate is auto-calculated from its nested rate components on save, and Store.tax_rate exposes it to transaction processing. A store with no jurisdiction, or a jurisdiction with no calculated rate, applies 0% tax with a logged warning; a jurisdiction marked is_exempt applies 0% deliberately. Jurisdiction changes cascade a re-sync of every store using them.
Store settings and the compliance move
Operational settings live in the settings JSONField at exact Sanity paths (settings.pickupEnabled, settings.returnToInventory, settings.payAtStoreEnabled). The regulated-retail compliance policy (age gate, credential types, permits) moved off settings to a top-level regulatedCompliance object backed by the store's compliance profile; the serializer dual-reads for legacy content and scrubs the moved keys from settings. See serializers.md.
Store-as-url slug
Every store gets a unique, URL-routable slug derived from its name on save (pre_save signal), so every active store is reachable at /{slug}/ on the storefront. The slug is Django-owned and never synced. See signals.md.
Signal-based automation
Beyond sync, saving a Store invalidates the cached store list, and creating a Store queues an inventory backfill so a new store is stocked with available-everywhere variants instead of appearing empty. See signals.md.
Common Use Cases
- Store setup: create a store, assign its tax jurisdiction, configure settings and hours in Sanity Studio
- Multi-location operations: stores across different tax jurisdictions with shared or separate rates
- Default store: exactly one
is_defaultstore loads first in the dashboard; setting a new default auto-clears the old one - POS terminal lookup: terminals resolve their store by store_code or Sanity ID at login
- Compliance verification: record and review per-zone audit checks
- Dashboard: per-store summary and trading snapshot endpoints
Related Domains
- Zones: zone organization; live zone/planogram endpoints
- Users: manager assignments via
manager_idsbidirectional sync - Transactions: store-scoped transaction processing and tax application
- Products: store-scoped inventory levels
- Compliance: the store compliance profile behind
regulatedCompliance - Integrations: the sync framework and store sync handler
Next Steps
- Start with models.md for the data structure and ownership boundaries
- Then views.md for the live endpoints
- Review serializers.md for webhook processing and the compliance bridge
- Check signals.md for automation
- See services.md for the summary and dashboard aggregations