Syncing promo codes across OTA channels

A promotion that goes live on the direct site at 09:00 but does not reach an OTA until 09:40 is a forty-minute parity violation with your name on it. Syncing promo codes across channels is the problem of activating and deactivating a campaign everywhere it is in scope, atomically enough that no channel ever advertises a discount another channel is missing. This how-to sits under Promotional Discount Orchestration in the Dynamic Pricing Rule Engines & Optimization pillar, and it leans on the delivery mechanics established in the ingestion pillar.

Prerequisites

  • Python 3.11+ and an async runtime (asyncio)
  • httpx for async channel calls (already a dependency of the ingestion stack)
  • Channel adapters exposing activate/deactivate for a campaign, per the Channel Manager Integration Patterns contract
  • A campaign store with scope, discount, and validity window fields
  • Idempotency keys so a retried activation never double-applies

Step 1 — Model the campaign and its channel scope

A campaign carries the channels it targets and a single idempotency key reused across retries. The scope must never include a channel that would break the parity obligations governed by Rate Parity Compliance Across Booking Channels.

python
from __future__ import annotations

import asyncio
import logging
from dataclasses import dataclass

import httpx

logger = logging.getLogger("pricing.promo_sync")


@dataclass(frozen=True)
class Campaign:
    campaign_id: str
    percent_off: float
    channels: tuple[str, ...]
    idempotency_key: str

Step 2 — Activate on all channels concurrently, then verify

Fan the activation out concurrently for speed, but treat the campaign as live only once every channel acknowledges. Gather results and detect partial success explicitly rather than assuming the whole set succeeded.

python
async def activate_on_channel(client: httpx.AsyncClient, base_url: str,
                              campaign: Campaign) -> bool:
    try:
        resp = await client.post(
            f"{base_url}/promotions",
            json={"campaign_id": campaign.campaign_id, "percent_off": campaign.percent_off},
            headers={"Idempotency-Key": campaign.idempotency_key},
            timeout=10.0,
        )
        resp.raise_for_status()
        return True
    except httpx.HTTPError as exc:
        logger.error("activation failed on %s: %s", base_url, exc)
        return False


async def activate_campaign(campaign: Campaign,
                            endpoints: dict[str, str]) -> dict[str, bool]:
    async with httpx.AsyncClient() as client:
        results = await asyncio.gather(*(
            activate_on_channel(client, endpoints[ch], campaign)
            for ch in campaign.channels
        ))
    status = dict(zip(campaign.channels, results))
    if not all(status.values()):
        failed = [ch for ch, ok in status.items() if not ok]
        logger.warning("partial activation for %s; failed: %s",
                       campaign.campaign_id, failed)
    return status

Step 3 — Reconcile partial failures before declaring the campaign live

Do not flip the campaign to “active” in your own store until every channel confirms. On partial failure, retry the missing channels with the same idempotency key — safe precisely because the key makes a repeat activation a no-op on channels that already succeeded.

python
async def sync_until_consistent(campaign: Campaign, endpoints: dict[str, str],
                                max_rounds: int = 3) -> bool:
    pending = set(campaign.channels)
    for round_no in range(1, max_rounds + 1):
        subset = Campaign(campaign.campaign_id, campaign.percent_off,
                          tuple(pending), campaign.idempotency_key)
        status = await activate_campaign(subset, endpoints)
        pending = {ch for ch, ok in status.items() if not ok}
        if not pending:
            logger.info("campaign %s consistent after round %d",
                        campaign.campaign_id, round_no)
            return True
        await asyncio.sleep(2 ** round_no)   # backoff between rounds
    logger.error("campaign %s still inconsistent: %s", campaign.campaign_id, pending)
    return False

Gating the “active” flag on full consistency is the whole trick: a campaign that is live in your database but missing on one OTA is exactly the state that produces a parity complaint.

Verification and testing

Assert that a campaign is only marked live when every channel acknowledges, using a stub that fails one channel once.

python
import asyncio


def test_partial_then_consistent(monkeypatch) -> None:
    calls = {"ota_a": 0}

    async def fake(campaign, endpoints):
        calls["ota_a"] += 1
        ok_a = calls["ota_a"] > 1          # ota_a fails on first attempt
        return {"direct": True, "ota_a": ok_a}

    monkeypatch.setattr("pricing.promo_sync.activate_campaign", fake)
    campaign = Campaign("SUMMER25", 0.25, ("direct", "ota_a"), "idem-1")
    ok = asyncio.run(sync_until_consistent(campaign, {}))
    assert ok is True
    assert calls["ota_a"] == 2

Common pitfalls and edge cases

  • Marking live on partial success. The most common parity bug; require all-channel acknowledgment first.
  • Missing idempotency key. Retries then double-apply the discount on channels that already succeeded.
  • Deactivation drift. Expiries can be lost like activations; reconcile active campaigns on a schedule, not only on events.
  • Clock skew across channels. Schedule activation by a shared UTC instant, not each channel’s local clock.
  • Unbounded retries. Cap the rounds and alert on persistent inconsistency instead of looping forever.