Users - Signals
Location: api/nextango/apps/users/signals.py
Last Updated: 2026-07-08
Overview
Signals in this domain do two jobs: keep the derived role models (Employee, Customer, Manager, Influencer, Vip) in sync with User.user_roles_data, and push User changes back out to Sanity when they did not originate from a Sanity webhook. A shared loop-prevention mechanism, SyncContext, tracks whether the current save is happening inside inbound webhook processing so that processing a webhook never triggers an outbound sync back to Sanity for the same change.
create_or_update_user_role
Triggers on: User post-save.
Runs: synchronously.
Reads user.get_all_roles() and, for every role present that does not already have a corresponding model row, creates one. New rows for employee and manager roles pull employeeId, pinCode, hourlyRate, and commissionRate from the matching user_roles_data entry when present, rather than always falling back to defaults; a new influencer row generates a default social_handle from the user's name or email if Sanity did not supply one. This handler does not delete rows for roles that were removed; deletion happens inside User.sync_roles_to_model_records() (see Models), which most save paths call explicitly.
Skipped entirely when inbound webhook processing is in progress on the current thread, or when the instance carries a _skip_role_update flag (set by code that is intentionally batching role-model changes before a single user save).
sync_user_to_sanity
Triggers on: User post-save.
Runs: asynchronously. Queues a background sync task and returns immediately; does not block the request that triggered the save.
Pushes the current User state to Sanity, unless the save is part of inbound webhook processing (in which case the change already originated in Sanity and re-sending it would create a sync loop) or the user's sync_status marks it as deleted from Sanity. A bundled_sync context, used for post-transaction updates, is explicitly allowed to proceed even when other loop-prevention checks would otherwise skip it.
delete_user_from_sanity
Triggers on: User post-delete.
Runs: synchronously.
Does not delete the corresponding document in Sanity. Outbound deletion from Django is intentionally disabled: a Sanity-side delete propagates to Django via webhook as a soft-deleted state, and hard deletion is handled by a separate cleanup process, not by this signal.
sync_employee_to_sanity, sync_customer_to_sanity, sync_manager_to_sanity, sync_influencer_to_sanity, sync_vip_to_sanity
Triggers on: post-save of the corresponding role model (Employee, Customer, Manager, Influencer, Vip).
Runs: each defers to sync_user_to_sanity, so effectively asynchronous.
A change to any role model re-triggers a sync of its parent User document, since Sanity's user document is the unit of sync; there is no independent "sync just the employee role" path. Each handler applies the same webhook-context skip as sync_user_to_sanity before delegating.
recalculate_customer_store_credit
Triggers on: transactions.StoreCredit post-save.
Runs: after the enclosing transaction commits.
Recomputes the owning Customer.store_credit by summing that customer's active StoreCredit records. Keeps the aggregate balance current without a separate reconciliation job. See Customer.recalculate_store_credit().
update_collection_statistics_on_item_change
Triggers on: UserProductCollection.items M2M add, remove, or clear.
Runs: after the enclosing transaction commits.
Refreshes a collection's statistics JSONField (item count, total and average price, on-sale count) whenever its item set changes, whether the change came from the collection side or from an InventoryLevel being added to/removed from multiple collections at once.
Known Gaps
- User deletion is not synced to Sanity.
delete_user_from_sanitylogs the deletion but performs no outbound action, by design (see above). Do not rely on a Django-side user delete to remove the Sanity document. - Standalone role serializer writes do not trigger a
user_roles_dataupdate. A write throughEmployeeSerializeror similar (see Serializers) updates the role model but not the parent user's JSONField, so the two can drift until the next fullUserwrite.UserService.validate_role_consistency()(see Services) detects this class of drift.
Related Documentation
- Models:
sync_roles_to_model_records(), the method most of these signals coordinate with - Services:
UserService.sync_user_from_sanity(),validate_role_consistency() - Views:
UserViewSet - Integrations:
SyncContext, webhook processing, and the identity-reconciliation module shared with inbound sync