Promotions - Models Documentation

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

Overview

The Promotions domain has two discount mechanisms sharing the same discount vocabulary. PromoCode is a customer-entered code, standalone or grouped under a PromotionalCampaign. Promotion is an auto-apply discount evaluated against every cart with no code required. Both use the same discount_effect / applies_to JSON shapes, and both feed the shared clamp engine in services.py that bounds total discount to the cart subtotal.

Four immutable ledger models back the domain's usage counters and analytics: PromoUsageLedger, CampaignConversion, ABTestConversionLedger, and SaleTransactionAppliedPromotion. Row existence in a ledger is authoritative for whether the corresponding counter increment happened for a given transaction, which makes the increment safe to retry from Celery.

All models except SaleTransactionAppliedPromotion inherit from SanityIntegratedModel (bidirectional Sanity sync) and AuditableMixin (created_at, updated_at, created_by, updated_by). Fields on Sanity-synced models are snake_case in Django and camelCase in the corresponding Sanity schema; the sync layer translates between the two.


PromotionalCampaign

Purpose: Groups one or more PromoCode records (and ABTest records) under a shared marketing campaign with its own date range, store targeting, and performance metrics.

Base class: SanityIntegratedModel, AuditableMixin

Fields

FieldTypeDescription
campaign_nameCharField(255)Campaign display name
campaign_typeCharField(50), choicesseasonal_sale, flash_sale, clearance, influencer, customer_loyalty
date_rangeJSONField{"startDate": ..., "endDate": ...}. Sanity-facing keys stay camelCase inside JSON payloads.
target_storesJSONField (list)Array of Sanity store _ref objects; empty means all stores
target_productsJSONFieldProduct/collection targeting configuration
markdown_scheduleJSONField (list)Progressive discount schedule entries
performance_metricsJSONFieldImpressions, conversions, revenue, conversion rate. See Services
ab_test_idCharField(255)Sanity _id of an associated ABTest
is_activeBooleanFieldMaster on/off switch
start_date, end_datepropertyRead-only, extracted from date_range

Cross-links: PromotionalCampaignSerializer, PromotionService.get_active_campaigns_for_store


PromoCode

Purpose: Customer-entered discount code with a modular discount effect, application scope, usage limits, and store applicability.

Base class: SanityIntegratedModel, AuditableMixin

Fields

FieldTypeDescription
nameCharField(255)Internal descriptive name, not shown to customers
codeCharField(20), uniqueCustomer-facing code, auto-uppercased on save
discount_effectJSONField{"type": "percentage"|"fixedAmount"|"freeShipping"|"bogo"|"bogoDiscounted", ...}
allow_combinationBooleanFieldWhen True, stacks with other stackable discounts. When False, competes against other non-stackable discounts for the same cart
applies_toJSONField{"scope": "order"|"products"|"collections"|"shipping", ...}
date_rangeJSONFieldValid date window
usage_restrictionsJSONFieldmaxUses, maxUsesPerCustomer, customer-tier restrictions
applicable_storesJSONField (list)Empty means valid at all stores
current_usage_countPositiveIntegerFieldCompared against usage_restrictions.maxUses
is_activeBooleanFieldMaster on/off switch, checked before all other validation
campaignsM2M(PromotionalCampaign)related_name='promo_codes'
discount_type, discount_value, usage_limit, times_usedpropertyBackward-compatible accessors into discount_effect / usage_restrictions / current_usage_count

Usage increment: current_usage_count is incremented only after a matching row is written to PromoUsageLedger for the transaction. See PromoUsageLedger below. Applying a code to a cart does not increment the count.

Cross-links: PromoCodeSerializer, PromotionService._calculate_promo_discount


ABTest

Purpose: A/B testing framework tracking variant impressions, conversions, and statistical significance for campaign optimization.

Base class: SanityIntegratedModel, AuditableMixin

Fields

FieldTypeDescription
nameCharField(255)Test identifier
campaignsM2M(PromotionalCampaign)related_name='ab_tests'
metric_to_trackCharField(100)e.g. conversion_rate, average_order_value
test_configurationJSONFieldVariant definitions and traffic split
variant_a_impressions, variant_a_conversions, variant_b_impressions, variant_b_conversionsPositiveIntegerFieldCounters, updated only through record_impression() / record_conversion()
performance_dataJSONFieldDetailed per-variant analytics
is_activeBooleanFieldControls whether the test is running
test_durationJSONFieldStart/end/actual-end dates
variant_a_conversion_rate, variant_b_conversion_ratepropertyconversions / impressions * 100, 0 when no impressions
statistical_significancepropertyTrue once combined impressions reach 1000

Methods

record_impression(variant) and record_conversion(variant) increment the matching counter using a database-level F() expression (ABTest.objects.filter(pk=self.pk).update(...)), which avoids a read-modify-write race under concurrent requests. variant is 'a' or 'b', case-insensitive; any other value is logged as a warning and ignored.

Cross-links: ABTestSerializer, record-impression / record-conversion actions


Promotion

Purpose: Auto-apply discount evaluated against every cart at checkout time, with no customer-entered code. Authored in Sanity, synced to Django, evaluated against cart contents and inventory.

Base class: SanityIntegratedModel, AuditableMixin Table: promotion

Fields

FieldTypeDescription
nameCharField(255)Display name
discount_effectJSONFieldSame shape as PromoCode.discount_effect
applies_toJSONFieldSame shape as PromoCode.applies_to
allow_combinationBooleanFieldStacking rule, same semantics as PromoCode.allow_combination
date_rangeJSONFieldValid date window
applicable_storesJSONField (list)Empty means all stores
is_activeBooleanFieldInactive promotions are skipped during checkout evaluation
imagesJSONField (list)Sanity image references; the first entry is the hero/banner image
campaignsM2M(PromotionalCampaign)Campaign association

Checkout evaluation

PromotionService._collect_fired_promotions() filters active Promotion records against a transaction: date range validity, store scope, and an inventory gate (stock must exceed zero for standard discounts, or exceed buy_quantity + get_quantity for BOGO types). Stackable promotions all apply; non-stackable promotions compete on post-clamp discount contribution and the best one wins. See the clamp engine for how the resulting discount amount is computed against the cart.

Cross-links: PromotionSerializer, PromotionViewSet


SaleTransactionAppliedPromotion

Purpose: Immutable snapshot of each auto-apply Promotion applied to a transaction. Preserves the promotion's state at the moment of checkout even if the original Promotion record is later modified or deleted.

Base class: plain Django model (not Sanity-synced) Table: promotion_applied_transaction

Fields

FieldTypeDescription
transactionFK(SaleTransaction)CASCADE, related_name='applied_promotions'
promotionFK(Promotion)SET_NULL, null/blank. Goes NULL if the Promotion is later deleted
promotion_snapshot_idUUIDFieldDjango pk of the Promotion at time of sale, preserved after deletion
promotion_nameCharField(255)Name snapshot
discount_effectJSONFieldFull discount effect snapshot
discount_amountDecimalField(10,2)Concrete discount amount applied to this transaction

Constraints: UniqueConstraint(transaction, promotion_snapshot_id), one row per promotion per transaction.

Rows are never updated after creation and are used by refund calculations (itemized refund amounts) and analytics (PromotionAnalyticsService.get_auto_promotion_performance, grouped by promotion_snapshot_id).

Cross-links: Analytics Service, Transactions Domain


PromoUsageLedger

Purpose: Audit-trail row per (promo_code, sale_transaction) usage increment. Row existence is authoritative for whether PromoCode.current_usage_count was incremented for a given transaction.

Base class: TimestampedModel Table: promo_usage_ledger

FieldTypeDescription
promo_codeFK(PromoCode)CASCADE, related_name='usage_ledger'
sale_transactionFK(SaleTransaction)CASCADE, related_name='promo_usage_ledger'
applied_atDateTimeFieldauto_now_add

Constraints: UniqueConstraint(promo_code, sale_transaction), the database-level dedup backstop that makes the Celery-driven usage increment safe to retry.


CampaignConversion

Purpose: Audit-trail row per (campaign, sale_transaction) conversion. Row existence is authoritative for whether PromotionService.track_campaign_conversion() was invoked for a given transaction.

Base class: TimestampedModel Table: campaign_conversion

FieldTypeDescription
campaignFK(PromotionalCampaign)CASCADE, related_name='conversions'
sale_transactionFK(SaleTransaction)CASCADE, related_name='campaign_conversions'
recorded_atDateTimeFieldauto_now_add
is_new_customerBooleanFieldCaptured at conversion time

Constraints: UniqueConstraint(campaign, sale_transaction).


ABTestConversionLedger

Purpose: Audit-trail row per (ab_test, sale_transaction) conversion. Row existence is authoritative for whether ABTest.record_conversion() was invoked for a given test/transaction pair.

Base class: TimestampedModel Table: ab_test_conversion_ledger

FieldTypeDescription
ab_testFK(ABTest)CASCADE, related_name='conversion_ledger'
sale_transactionFK(SaleTransaction)CASCADE, related_name='ab_test_conversion_ledger'
variantCharField(10)The AB test variant (a/b) at conversion time
recorded_atDateTimeFieldauto_now_add

Constraints: UniqueConstraint(ab_test, sale_transaction).


Model Relationships

PromotionalCampaign ←──M2M──→ PromoCode ──FK──→ PromoUsageLedger ──FK──→ SaleTransaction

        ├──M2M──→ ABTest ──FK──→ ABTestConversionLedger ──FK──→ SaleTransaction

        └──FK──→ CampaignConversion ──FK──→ SaleTransaction

Promotion ──M2M──→ PromotionalCampaign
Promotion ──FK──→ SaleTransactionAppliedPromotion ──FK──→ SaleTransaction

  • Services - PromotionService business logic and the clamp engine
  • Signals - Sanity sync
  • Views - REST API endpoints
  • README - Domain overview

Was this page helpful?