Promotions Domain - Signal Handlers

Source File: api/nextango/apps/promotions/signals.py Last Updated: 2026-07-08

Overview

Real-time synchronization between the four Sanity-synced promotional models (PromotionalCampaign, PromoCode, ABTest, Promotion) and Sanity CMS. Each of the four models has a post_save sync handler. Three of them (PromotionalCampaign, PromoCode, ABTest) also carry a post_delete handler that logs and takes no outbound action; Promotion has no delete handler at all. Separate cache-invalidation receivers fire on save and delete of PromotionalCampaign and PromoCode, and on save of SaleTransactionAppliedPromotion (invalidating the auto-promotion analytics cache).

Loop prevention

Each post_save handler runs the same sequence before syncing:

  1. LOAD_TESTING_MODE environment check, skips all sync during load testing.
  2. Thread-local sync context check (should_skip_sync_to_sanity()), skips when the save originated from processing an inbound Sanity webhook.
  3. Instance type verification.
  4. instance._sync_source == 'sanity_webhook' check.
  5. Cache-based webhook-processing lock (webhook_processing:<model>:<pk>).
  6. Cache-based Django-sync lock (django_sync:<model>:<pk>), set for 5 minutes around the sync call, cleared in a finally block.
  7. Rapid-save detection (rapid_save:<model>:<pk>, 2-second window), guards against a webhook-save-webhook ping-pong.

The sync call itself goes through SyncOrchestrator.orchestrate_sync_to_sanity(django_instance, domain_handler, created), with promotional_campaign_sync_handler as the domain handler for all four models. This is the shared framework also used by other Sanity-synced domains, not a promotions-specific sync path.

Deletion handling

The three post_delete handlers (PromotionalCampaign, PromoCode, ABTest) log and take no outbound action: "outbound Sanity deletion disabled per P13-4/Q7". This is a deliberate platform decision, not an unimplemented stub, deletions in Django do not propagate to Sanity for these models. Promotion deletion is unhandled, there is no delete receiver for it, which produces the same end-state (no outbound deletion) through absence rather than an explicit disabled handler.

Handler list

Modelpost_savepost_delete
PromotionalCampaignsync_promotional_campaign_to_sanitydelete_promotional_campaign_from_sanity (logs only)
PromoCodesync_promo_code_to_sanitydelete_promo_code_from_sanity (logs only)
ABTestsync_ab_test_to_sanitydelete_ab_test_from_sanity (logs only)
Promotionsync_promotion_to_sanitynone

Cache-invalidation receivers, distinct from the sync handlers: invalidate_campaign_cache (save and delete of PromotionalCampaign), invalidate_promocode_cache (save and delete of PromoCode), and invalidate_auto_promotion_analytics_on_save (save of SaleTransactionAppliedPromotion).

Cache key reference

Key patternPurposeDuration
webhook_processing:<model>:<pk>Webhook processing in progressSet by the webhook receiver
django_sync:<model>:<pk>Django-initiated sync lock5 minutes, auto-expires
rapid_save:<model>:<pk>Rapid successive save detection2 seconds

Was this page helpful?