Users - Models

Location: api/nextango/apps/users/models.py Last Updated: 2026-07-08

Overview

The Users domain stores every person the platform knows about, plus the role-specific data attached to them. User is the single Sanity-synced identity record. Employee, Customer, Manager, Influencer, and Vip are role records that mirror Sanity's roleEmployee, roleCustomer, roleManager, roleInfluencer, and roleVip object types, but they are derived data: the authoritative role payload lives on User.user_roles_data, and the role rows are rebuilt from it. Role rows are created by a post-save signal on every save except sync-sourced saves, which skip the signal and instead rely on an explicit sync_roles_to_model_records() call (see Signals). UserProductCollection and SavedAddress round out the domain (wishlists/curations and checkout addresses), and LoyaltyLedger records the audit trail behind a customer's loyalty points balance.

All Sanity-synced models in this domain use camelCase field names on the Sanity side and snake_case on the Django side; field names below are the Django (snake_case) names unless noted.

User

Base class: SanityIntegratedModel

The platform identity record. Mirrors Sanity's user.js schema and doubles as the Django auth user (USERNAME_FIELD = 'email').

FieldTypeDescription
nameCharFieldFull name
first_name, last_nameCharField, nullableOptional given/family name split
emailEmailField, uniqueLogin identifier
phoneCharField, nullable
statusCharField (active, inactive, suspended)Gates authentication (see Authentication)
created_dateDateTimeFieldSet on creation
user_roles_dataJSONFieldThe Sanity userRoles array, stored as-is. Source of truth for role assignment
collectionsJSONFieldReferences to owned UserProductCollection documents
credentialsJSONFieldLicence/permit credential objects synced from Sanity
passwordCharField, nullableHashed via set_password() (Django make_password)
has_password, password_set_atBooleanField, DateTimeFieldPassword lifecycle tracking, excluded from Sanity sync
is_staff, is_superuserBooleanFieldDjango admin access, driven by a manager role's adminAccess sub-object rather than set directly

A functional index on Lower('email') backs the identity-reconcile lookup that matches an inbound Sanity user document to an existing Django row by normalized email (models.py:441-446).

Role helpers: get_primary_role() and get_all_roles() read user_roles_data and map Sanity role type strings (roleEmployee, roleCustomer, roleManager, roleInfluencer, roleVip) to their Django equivalents (employee, customer, manager, influencer, vip). has_role(role_type) checks whether the corresponding role row exists. sync_roles_to_model_records() rebuilds Employee/Customer/Manager/Influencer/Vip rows from user_roles_data, creating or updating one row per role present in the array and deleting rows for roles no longer present.

Cross-links: UserSerializer, UserService, role-sync signal.

Employee

Base class: SanityIntegratedModel, AuditableMixin

Derived from a roleEmployee entry in the parent user's user_roles_data. Not independently synced outbound (_outbound_sync_enabled = False); it syncs via the parent User document.

FieldTypeDescription
userForeignKey → User (related_name='employee_roles')Parent user
employee_idCharFieldIdentifier used for PIN login
pin_codeCharField, nullable4 or 6-digit PIN for POS access
hourly_rateDecimalField, nullable
commission_rateDecimalField, default 0Percentage
store_assignmentsJSONFieldArray of Sanity store references
hire_dateDateField, nullable

Sanity's schema permits only one Employee role per user; the sync path matches on user alone rather than user + employee_id.

Cross-links: EmployeeSerializer / EmployeeRoleSerializer, UserAuthenticationService.authenticate_pin_login, PINLoginSerializer.

Customer

Base class: SanityIntegratedModel, AuditableMixin

Derived from a roleCustomer entry.

FieldTypeDescription
userForeignKey → User (related_name='customer_roles')Parent user
loyalty_pointsIntegerField, default 0Current balance (see LoyaltyLedger)
store_creditDecimalField, default 0Aggregated from active StoreCredit records via recalculate_store_credit()
wishlistJSONFieldReferences to UserProductCollection documents
marketing_opt_inBooleanField, default False
preferred_storeCharField, nullableSanity store reference
order_historyJSONFieldReferences to sale transactions

recalculate_store_credit() sums remainingBalance across the customer's active StoreCredit rows and persists the total to store_credit; it runs automatically when a linked StoreCredit record is saved (see Signals).

Cross-links: CustomerSerializer / CustomerRoleSerializer, CustomerAnalyticsService.

Manager

Base class: SanityIntegratedModel, AuditableMixin

Derived from a roleManager entry. Carries the adminAccess privilege data that gates User.is_staff / User.is_superuser.

FieldTypeDescription
userForeignKey → User (related_name='manager_roles')Parent user
employee_idCharFieldIdentifier used for PIN login
pin_codeCharField, nullable
hourly_rateDecimalField, nullable
departmentCharField, nullable
reporting_levelCharField (Store Manager, Regional Manager, Corporate)
override_permissionsJSONFieldArray of permission strings (canOverridePrice, canIssueRefund, canOverrideDiscount)
reports_toCharField, nullableSanity reference to a supervising user
store_assignmentsJSONFieldArray of Sanity store references. An empty array on a manager row grants unrestricted store access rather than no access (see Permissions)

Cross-links: ManagerSerializer / ManagerRoleSerializer, Permissions store-scope gate.

Influencer

Base class: SanityIntegratedModel, AuditableMixin

Derived from a roleInfluencer entry.

FieldTypeDescription
userForeignKey → User (related_name='influencer_roles')Parent user
social_handleCharField
commission_rateDecimalField, nullable
follower_countIntegerField, nullable
discount_codeCharField, nullableSanity reference to an associated promo code

Vip

Base class: SanityIntegratedModel, AuditableMixin

Derived from a roleVip entry.

FieldTypeDescription
userForeignKey → User (related_name='vip_roles')Parent user
vip_tierCharField (Gold, Platinum, Diamond), nullable
personal_shopper_idCharField, nullableSanity reference to a personal shopper user
exclusive_accessJSONFieldArray of access strings (earlyProductAccess, privateSales, vipEvents, conciergeService)
special_discount_rateDecimalField, nullable

UserProductCollection

Base class: SanityIntegratedModel, AuditableMixin

A user-owned collection of inventory items: personal wishlist, influencer curation, or staff pick. Backs storefront discovery pages and personal shopping lists.

FieldTypeDescription
title, slug (unique), descriptionCharField / SlugField / TextField
ownerForeignKey → User (related_name='product_collections')
collection_typeCharField (wishlist, curation, staff-pick)
is_public, is_featuredBooleanFieldFeatured collections must be curation or staff-pick
share_tokenCharField, unique, nullableSharing URL token for private collections
itemsManyToManyField → products.InventoryLevel (related_name='user_collections')Store-specific product variants in the collection
statisticsJSONFieldItem count, total/average price, on-sale count, recomputed by update_statistics() whenever items change
notification_settings, curation_metadata, staff_pick_metadataJSONFieldType-specific extension data

Cross-links: UserProductCollectionSerializer, UserProductCollectionViewSet.

SavedAddress

Base class: SanityIntegratedModel, AuditableMixin

A reusable checkout address. Embedded in the Sanity user record's savedAddresses array rather than synced as a standalone document (_outbound_sync_enabled = False).

FieldTypeDescription
userForeignKey → User (related_name='saved_addresses')
labelCharFielde.g. "Home", "Work"
is_defaultBooleanFieldSetting one address as default clears the flag on the user's other addresses
first_name, last_name, company, address, apartment, city, region, postal_code, country, phoneCharFieldShipping address fields
created_at, updated_atDateTimeFieldSet on creation / on every save

LoyaltyLedger

Base class: TimestampedModel

An immutable, append-only record of every loyalty point event for a Customer. Customer.loyalty_points is the authoritative running balance; ledger entries let the balance be reconstructed and audited.

FieldTypeDescription
customerForeignKey → Customer (related_name='loyalty_ledger')
event_typeCharField (earn, redeem, reverse, adjustment), not editable
pointsIntegerField, not editableSigned delta; a check constraint forbids a zero-point entry
balance_afterIntegerField, not editableCustomer.loyalty_points immediately after this event
sale_transactionForeignKey → transactions.SaleTransaction, nullableThe sale that triggered the event, if any
notesTextField, not editable

A unique constraint prevents more than one earn entry per (customer, sale_transaction) pair, so a transaction cannot double-credit loyalty points on retry.

Was this page helpful?