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 fromdate_range.promo_codes_count,obj.promo_codes.count().target_stores, on read, resolves each Sanity_refintarget_storesto a DjangoStoreUUID 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 atypeinpercentage,fixedAmount,freeShipping,bogo,bogoDiscounted; percentage must be 1-100; fixed amount must be > 0; BOGO types requirebuyQuantityandgetQuantity>= 1, andbogoDiscountedadditionally requiresgetDiscountPercentagebetween 1 and 100.validate_applies_to:scopemust be one oforder,products,collections,shipping;productsrequirestargetProducts,collectionsrequirestargetCollections. 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 discountEffect → discount_effect, appliesTo → applies_to, allowCombination → allow_combination, dateRange → date_range, applicableStores → applicable_stores, isActive → is_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).