Integrations - Sync Services Domain Reference Logic

Location: api/nextango/apps/integrations/sync/services/*_reference_logic.py Last Updated: 2026-07-06

Overview

Domain reference logic files implement self-contained business reference generation and resolution for each domain. These files follow Google's architectural purity approach with dependency inversion pattern - extracting domain-specific logic from the central reference_resolver.py.

Architecture Goal: Each domain defines its own reference format and resolution strategy, enabling the reference_resolver to remain domain-agnostic while supporting domain-specific business rules.


Common Pattern

All reference logic files follow the same structure: a generator that reads a reference id from an instance, and a resolver that reads an instance back from a reference id.

Reference Generation

Reference generation only reads. A generator returns the instance's existing sanity_id if it has one, or a deterministic synthetic id derived from the sync document type and the primary key. It never writes to the database and never mutates the instance. The object's own sync path is the single writer of sanity_id, and it assigns the same deterministic id, so a reference built here and the object's document converge on one id without the reference path touching the database.

from .sanity_refs import derive_synthetic_sanity_id

def generate_employee_reference(django_instance: models.Model) -> str:
    """Return the Employee reference id (existing sanity_id, or a synthetic one)."""
    if hasattr(django_instance, 'sanity_id') and django_instance.sanity_id:
        return django_instance.sanity_id
    # Pure: deterministic synthetic id, no DB write on the ref-build read path.
    return derive_synthetic_sanity_id(django_instance, 'userEmployee')

derive_synthetic_sanity_id(instance, sanity_type) returns f"{sanity_type}_{instance.pk}". It takes the document type explicitly so each generator passes the prefix that matches its own self-sync, keeping the reference id and the document id aligned. Source: sync/services/sanity_refs.py.

Reference Resolution

A resolver reads a Django instance back from a reference id. The shared resolve_dual_shape(model_class, reference_id) helper closes the round-trip for both the current id shape and legacy stored data. A non-UUID reference can only match sanity_id. A UUID-shaped reference can match the Django primary key or a UUID-shaped sanity_id, so a reference minted before a document synced still resolves. The helper is field-aware and primary-key-type-aware: a model without a sanity_id field returns None rather than raising, and an integer-primary-key model does not raise on a UUID-shaped reference.

from .sanity_refs import resolve_dual_shape

def resolve_product_reference(reference_id: str, model_class) -> Optional[models.Model]:
    """Resolve a product reference: dual-shape first, then legacy slug/name/sku."""
    inst = resolve_dual_shape(model_class, reference_id)
    if inst:
        return inst
    # legacy business-field lookups (slug, name, sku) retained as fallback
    ...

The product, transaction, and user resolvers try resolve_dual_shape first and keep their existing business-field lookups (slug, name, SKU, transaction identifiers) as a fallback, so no reference that resolved before regresses. The other domain resolvers do not use it and have no Django-UUID-pk tolerance: the zones resolvers match sanity_id exactly and then fall back to business fields, and the store resolver parses the business reference directly (store_code__iexact, then name). Source: sync/services/sanity_refs.py and the per-domain *_reference_logic.py files.


Domain Files

1. user_reference_logic.py

File: api/nextango/apps/integrations/sync/services/user_reference_logic.py

Functions

# Base User
generate_user_reference(django_instance)str
parse_user_reference(reference_string) → Optional[User]

# Employee
generate_employee_reference(django_instance)str
parse_employee_reference(reference_string) → Optional[Employee]

# Customer
generate_customer_reference(django_instance)str
parse_customer_reference(reference_string) → Optional[Customer]

# Manager
generate_manager_reference(django_instance)str
parse_manager_reference(reference_string) → Optional[Manager]

# Influencer
generate_influencer_reference(django_instance)str
parse_influencer_reference(reference_string) → Optional[Influencer]

# VIP
generate_vip_reference(django_instance)str
parse_vip_reference(reference_string) → Optional[VIP]

# UserRole
generate_user_role_reference(django_instance)str
parse_user_role_reference(reference_string) → Optional[UserRole]

Reference Formats

ModelFormatExample
Useruser_{uuid}user_a1b2c3d4-e5f6-7890-abcd-ef1234567890
EmployeeuserEmployee_{uuid}userEmployee_b2c3d4e5-f6a7-8901-bcde-f12345678901
CustomeruserCustomer_{uuid}userCustomer_c3d4e5f6-a7b8-9012-cdef-123456789012
ManageruserManager_{uuid}userManager_d4e5f6a7-b8c9-0123-def1-234567890123
InfluenceruserInfluencer_{uuid}userInfluencer_e5f6a7b8-c9d0-1234-ef12-345678901234
VIPuserVip_{uuid}userVip_f6a7b8c9-d0e1-2345-f123-456789012345
UserRoleuserRole_{uuid}userRole_a7b8c9d0-e1f2-3456-1234-567890123456

Source: api/nextango/apps/integrations/sync/services/user_reference_logic.py


2. product_reference_logic.py

File: api/nextango/apps/integrations/sync/services/product_reference_logic.py

Functions

generate_product_reference(django_instance)str
parse_product_reference(reference_string) → Optional[Product]

generate_product_variant_reference(django_instance)str
parse_product_variant_reference(reference_string) → Optional[ProductVariant]

generate_category_reference(django_instance)str
parse_category_reference(reference_string) → Optional[Category]

generate_brand_reference(django_instance)str
parse_brand_reference(reference_string) → Optional[Brand]

Reference Formats

ModelFormatExample
Productproduct_{uuid}product_p1r2o3d4-u5c6-7890-abcd-ef1234567890
ProductVariantproductVariant_{uuid}productVariant_v1a2r3i4-a5n6-7890-bcde-f12345678901
Categorycategory_{uuid}category_c1a2t3e4-g5o6-7890-cdef-123456789012
Brandbrand_{uuid}brand_b1r2a3n4-d5e6-7890-def1-234567890123

Resolution: resolve_product_reference tries resolve_dual_shape first (a sanity_id, or a legacy Django-UUID primary key), then falls back to the legacy slug, name, and sku lookups. The dual-shape attempt makes the generator's own sanity_id reference resolve back to the instance.

Source: api/nextango/apps/integrations/sync/services/product_reference_logic.py


3. store_reference_logic.py

File: api/nextango/apps/integrations/sync/services/store_reference_logic.py

Functions

generate_store_reference(django_instance)str
parse_store_reference(reference_string) → Optional[Store]

generate_store_location_reference(django_instance)str
parse_store_location_reference(reference_string) → Optional[StoreLocation]

Reference Formats

ModelFormatExample
Storestore_{uuid}store_s1t2o3r4-e5a6-7890-abcd-ef1234567890
StoreLocationstoreLocation_{uuid}storeLocation_l1o2c3a4-t5i6-7890-bcde-f12345678901

Special Logic: generate_store_reference returns the store's sanity_id when present, otherwise a business-meaningful fallback from store_code (store_<code>), or the store name. The store_code fallback lowercases the code but does not strip punctuation, so it round-trips against the resolver's store_code__iexact lookup. resolve_store_reference parses the store_<identifier> business reference and looks the store up by store_code__iexact, then by name; it does not use resolve_dual_shape. The dual-shape-style helpers in this file, resolve_store_location_refs and resolve_store_id_param, serve InventoryLevel.store_location reference resolution and a store query-parameter helper; they are not the registered store domain resolver.

Source: api/nextango/apps/integrations/sync/services/store_reference_logic.py


4. transaction_reference_logic.py

File: api/nextango/apps/integrations/sync/services/transaction_reference_logic.py

Functions

generate_sale_transaction_reference(django_instance)str
generate_payment_reference(django_instance)str
generate_return_reference(django_instance)str
generate_store_credit_reference(django_instance)str
generate_payment_method_reference(django_instance)str

resolve_transaction_reference(reference_data, model_class) → Optional[models.Model]

Reference Formats

Synthetic ids use the sync document type as the prefix, so a reference and the object's own document id align.

ModelFormatNote
SaleTransactionsalesTransaction_{pk}Synthetic prefix matches the transaction self-sync document type
TransactionPaymenttransactionPayment_{pk}Payment references use the transactionPayment prefix so they resolve to the payment document Sanity holds

Special Logic: generate_sale_transaction_reference and generate_payment_reference return the instance's sanity_id when present, otherwise derive_synthetic_sanity_id(instance, 'salesTransaction') and derive_synthetic_sanity_id(instance, 'transactionPayment'). Neither writes to the database; the object's own self-sync persists the matching deterministic id. The payment prefix is transactionPayment, which is the payment document type the self-sync uses, so a payment reference resolves to a real document rather than dangling.

resolve_transaction_reference tries resolve_dual_shape first (a sanity_id, or a legacy Django-UUID primary key), then falls back to the business-field lookups (transaction_id, receipt_number, and related identifiers), so the generator's sanity_id reference now round-trips.

Source: api/nextango/apps/integrations/sync/services/transaction_reference_logic.py


5. task_reference_logic.py

File: api/nextango/apps/integrations/sync/services/task_reference_logic.py

Functions

generate_task_reference(django_instance)str
parse_task_reference(reference_string) → Optional[Task]

generate_task_comment_reference(django_instance)str
parse_task_comment_reference(reference_string) → Optional[TaskComment]

Reference Formats

ModelFormatExample
Tasktask_{uuid}task_t1a2s3k4-5678-90ab-cdef-123456789abc
TaskCommenttaskComment_{uuid}taskComment_c1o2m3-4567-89ab-cdef-123456789abc

Source: api/nextango/apps/integrations/sync/services/task_reference_logic.py


6. zones_reference_logic.py

File: api/nextango/apps/integrations/sync/services/zones_reference_logic.py

Functions

generate_zone_reference(django_instance)str
parse_zone_reference(reference_string) → Optional[Zone]

generate_zone_region_reference(django_instance)str
parse_zone_region_reference(reference_string) → Optional[ZoneRegion]

Reference Formats

ModelFormatExample
Zonezone_{uuid}zone_z1o2n3e4-5678-90ab-cdef-123456789abc
ZoneRegionzoneRegion_{uuid}zoneRegion_r1e2g3-4567-89ab-cdef-123456789abc

Special Logic: generate_zone_reference builds a business-meaningful reference from the zone name, scoping it with the store code when the zone has a store location (zone_<store_code>_<zone_name>, or zone_<zone_name> without one). It reads only; it does not write a sanity_id.

Source: api/nextango/apps/integrations/sync/services/zones_reference_logic.py


7. sync_reconciliation.py

File: api/nextango/apps/integrations/sync/services/sync_reconciliation.py

Purpose

Periodic reconciliation service that detects and resolves sync drift between Django and Sanity.

Key Functions

def reconcile_domain_data(domain: str, model_class_key: str) -> Dict[str, Any]:
    """
    Reconcile all instances of a model between Django and Sanity.

    Process:
    1. Fetch all Django instances with sanity_id
    2. Fetch corresponding Sanity documents
    3. Compare timestamps and data
    4. Identify drift (Django newer, Sanity newer, or different)
    5. Resolve conflicts based on reconciliation strategy
    6. Return reconciliation report

    Args:
        domain: Domain name (users, products, stores, etc.)
        model_class_key: Django model class key (e.g., 'users.User')

    Returns:
        {
            'domain': str,
            'model_class_key': str,
            'total_checked': int,
            'drift_detected': int,
            'conflicts_resolved': int,
            'conflicts_unresolved': int,
            'reconciliation_actions': List[dict]
        }

    Strategies:
        - 'django_wins': Always sync Django → Sanity
        - 'sanity_wins': Always sync Sanity → Django
        - 'newest_wins': Compare timestamps, sync newer
        - 'manual_review': Flag for manual resolution
    """


def detect_missing_sanity_ids() -> Dict[str, List[int]]:
    """
    Detect Django instances without sanity_id assigned.

    Returns:
        {
            'users.User': [1, 5, 12],  # PKs without sanity_id
            'products.Product': [3, 8],
            ...
        }
    """


def detect_orphaned_sanity_documents() -> Dict[str, List[str]]:
    """
    Detect Sanity documents without corresponding Django instances.

    Returns:
        {
            'user': ['user-orphan-123', 'user-orphan-456'],
            'product': ['product-orphan-789'],
            ...
        }
    """


def schedule_full_reconciliation(domains: List[str] = None):
    """
    Schedule full reconciliation for specified domains.

    Queues Celery tasks for each domain/model combination.
    Runs during low-traffic periods (configurable schedule).
    """

Reconciliation Strategies:

StrategyDescriptionUse Case
django_winsDjango is source of truthAfter bulk Django imports
sanity_winsSanity is source of truthAfter bulk Sanity imports
newest_winsCompare _updatedAt timestampsNormal drift resolution
manual_reviewFlag for human reviewCritical data or high-value conflicts

Source: api/nextango/apps/integrations/sync/services/sync_reconciliation.py


Integration with reference_resolver.py

The reference_resolver.py uses these domain files through dependency injection:

# api/nextango/apps/integrations/sync/services/reference_resolver.py

class ReferenceResolver:
    def __init__(self):
        self._reference_generators = {}
        self._reference_parsers = {}
        self._register_domain_logic()

    def _register_domain_logic(self):
        """Register domain-specific reference logic."""

        # User domain
        from .user_reference_logic import (
            generate_user_reference,
            parse_user_reference,
            # ... more user functions
        )
        self._reference_generators['users.User'] = generate_user_reference
        self._reference_parsers['user'] = parse_user_reference

        # Product domain
        from .product_reference_logic import (
            generate_product_reference,
            parse_product_reference,
            # ... more product functions
        )
        self._reference_generators['products.Product'] = generate_product_reference
        self._reference_parsers['product'] = parse_product_reference

        # ... register all other domains ...

    def generate_reference(self, django_instance: models.Model) -> str:
        """
        Generate reference using registered domain logic.

        Args:
            django_instance: Any Django model instance

        Returns:
            Sanity reference ID

        Raises:
            ValueError: If no generator registered for model type
        """
        model_key = f"{django_instance._meta.app_label}.{django_instance._meta.model_name}"
        generator = self._reference_generators.get(model_key)

        if not generator:
            raise ValueError(f"No reference generator for: {model_key}")

        return generator(django_instance)

    def parse_reference(self, reference_string: str, doc_type: str) -> Optional[models.Model]:
        """
        Parse reference using registered domain logic.

        Args:
            reference_string: Sanity reference ID
            doc_type: Document type (user, product, store, etc.)

        Returns:
            Django model instance or None
        """
        parser = self._reference_parsers.get(doc_type)

        if not parser:
            logger.warning(f"No reference parser for: {doc_type}")
            return None

        return parser(reference_string)

This pattern enables:

  • Domain logic isolation
  • Easy addition of new domains
  • Testing domain logic independently
  • No circular dependencies between domains

  • Reference Resolver: docs/overview/integrations/services.md - Central reference resolution service
  • Sync Handlers: docs/overview/integrations/services.md - Domain sync handlers that use these references
  • Cross-Domain Resolver: docs/overview/integrations/tasks.md - Task-based dependency resolution
  • Dependency Fetcher: docs/overview/integrations/tasks.md - Pre-check dependency fetching

Summary

Domain Reference Logic Traits:

  • Self-contained domain-specific logic
  • Dependency inversion pattern (domains register with central resolver)
  • Reference generation reads only: no domain reference generator writes to the database on the reference-build path
  • The object's own self-sync is the single writer of sanity_id, assigning the deterministic id a synthetic reference derives
  • Dual-shape resolution in the product, transaction, and user resolvers: each tolerates a sanity_id and a legacy Django-UUID primary key, with business-field lookups retained as a fallback; the other domain resolvers have no Django-UUID-pk tolerance

Common Patterns:

  1. sanity_id-first where the model syncs by id: The store, product, sale-transaction, payment, employee, and manager generators return an existing sanity_id before deriving a fallback. The zone, return, store-credit, payment-method, and product-variant generators build business-meaningful references without checking sanity_id.
  2. Pure generation: A synthetic id comes from derive_synthetic_sanity_id, which reads the primary key and the document type and writes nothing.
  3. Deterministic convergence: The synthetic prefix matches the object's self-sync document type, so a reference and the object's document share one id without the reference path persisting anything.
  4. Dual-shape resolution where the generator emits a sanity_id: resolve_dual_shape runs first in the product, transaction, and user resolvers; legacy slug, name, SKU, and transaction-identifier lookups follow as a fallback. The store, zones, task, and procurement resolvers do not use it.

This architecture keeps reference generation off the write path, which matters on the transaction hot path, while preventing circular dependencies and letting each domain evolve independently.

Was this page helpful?