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:
LOAD_TESTING_MODEenvironment check, skips all sync during load testing.- Thread-local sync context check (
should_skip_sync_to_sanity()), skips when the save originated from processing an inbound Sanity webhook. - Instance type verification.
instance._sync_source == 'sanity_webhook'check.- Cache-based webhook-processing lock (
webhook_processing:<model>:<pk>). - Cache-based Django-sync lock (
django_sync:<model>:<pk>), set for 5 minutes around the sync call, cleared in afinallyblock. - 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
| Model | post_save | post_delete |
|---|---|---|
PromotionalCampaign | sync_promotional_campaign_to_sanity | delete_promotional_campaign_from_sanity (logs only) |
PromoCode | sync_promo_code_to_sanity | delete_promo_code_from_sanity (logs only) |
ABTest | sync_ab_test_to_sanity | delete_ab_test_from_sanity (logs only) |
Promotion | sync_promotion_to_sanity | none |
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 pattern | Purpose | Duration |
|---|---|---|
webhook_processing:<model>:<pk> | Webhook processing in progress | Set by the webhook receiver |
django_sync:<model>:<pk> | Django-initiated sync lock | 5 minutes, auto-expires |
rapid_save:<model>:<pk> | Rapid successive save detection | 2 seconds |
Related Documentation
- Models - Sanity-synced model definitions
- Services - PromotionService business logic
- Views - REST API endpoints
- Integrations Domain - SyncOrchestrator and sync framework