Promotions Domain - Serializers

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

Overview

Four serializers, one per model: PromotionalCampaignSerializer, PromoCodeSerializer, ABTestSerializer, PromotionSerializer. All Django model fields are snake_case. Each serializer overrides to_internal_value() to accept a Sanity webhook payload (camelCase, nested reference objects) and translate it to the snake_case shape the model expects, only fields actually present in the incoming payload are translated, which makes partial webhook updates safe.


PromotionalCampaignSerializer

Exposes: PromotionalCampaign

Key fields: campaign_name, campaign_type, date_range, target_stores (write path accepts Sanity reference objects), target_products, markdown_schedule, performance_metrics, ab_test_id, is_active. Sync fields (sanity_id, last_synced_at, sync_enabled) and audit fields are read-only.

Computed fields (read-only):

  • start_date, end_date, extracted from date_range.
  • promo_codes_count, obj.promo_codes.count().
  • target_stores, on read, resolves each Sanity _ref in target_stores to a Django Store UUID via a bulk lookup; unresolved refs are logged as warnings and dropped from the response. Note the write path (to_internal_value) accepts the raw Sanity reference shape for this same field name, the resolved-UUID list is a read-only override.

Webhook transformation: campaignType label values (e.g. "Seasonal Sale") are mapped back to their database value (seasonal_sale) via the reverse of CAMPAIGN_TYPE_CHOICES. abTestId reference objects are unwrapped to the bare _ref string.


PromoCodeSerializer

Exposes: PromoCode

Key fields: name, code, discount_effect, allow_combination, applies_to, date_range, usage_restrictions, current_usage_count, is_active, campaigns. applicable_stores is write_only, the resolved read view is applicable_stores_resolved (see below).

Computed fields (read-only): discount_type, discount_value, usage_limit, times_used (source current_usage_count), is_valid (obj.is_valid_for_use()), campaigns_count, applicable_stores_resolved (Sanity _ref → Django Store UUID, same resolution pattern as PromotionalCampaignSerializer.target_stores).

Validation:

  • validate_code: uppercases and strips, requires alphanumeric only, 3-20 characters.
  • validate_discount_effect: requires a type in percentage, fixedAmount, freeShipping, bogo, bogoDiscounted; percentage must be 1-100; fixed amount must be > 0; BOGO types require buyQuantity and getQuantity >= 1, and bogoDiscounted additionally requires getDiscountPercentage between 1 and 100.
  • validate_applies_to: scope must be one of order, products, collections, shipping; products requires targetProducts, collections requires targetCollections. Defaults to {"scope": "order"} when empty.

ABTestSerializer

Exposes: ABTest

Key fields: name, campaigns, metric_to_track, test_configuration, variant_a_impressions, variant_a_conversions, variant_b_impressions, variant_b_conversions, performance_data, is_active, test_duration.

Computed fields (read-only): variant_a_conversion_rate, variant_b_conversion_rate, statistical_significance, campaigns_count.

Validation: validate_test_duration checks ISO date format and that the end date is after the start date, same logic as PromotionalCampaignSerializer.validate_date_range.


PromotionSerializer

Exposes: Promotion

Handles inbound Sanity webhook payloads for the auto-apply promotion document type.

Key fields: name, discount_effect, applies_to, allow_combination, date_range, applicable_stores, is_active, images. discount_type is a read-only SerializerMethodField extracting discount_effect.get('type').

Validation: validate_discount_effect is identical to PromoCodeSerializer.validate_discount_effect, same discount-type vocabulary and BOGO field requirements.

Webhook transformation: to_internal_value() maps discountEffectdiscount_effect, appliesToapplies_to, allowCombinationallow_combination, dateRangedate_range, applicableStoresapplicable_stores, isActiveis_active, only fields present in the payload are transformed.


Field name convention

Every serializer field on the Django side is snake_case. Sanity webhook payloads arrive camelCase; the to_internal_value() override on each serializer is the single translation point, there is no shared bidirectional_field_mapper utility in this domain. When writing directly through the Django REST API (not via webhook), send snake_case field names, the serializers also accept the already-snake_case shape as a fallback for that path (for example, admin form submissions).


  • Models - Model definitions and JSON field structures
  • Services - PromotionService business logic
  • Views - REST API endpoints using these serializers
  • Signals - Real-time Sanity sync

Was this page helpful?