Версионирование условий оффера: как связать конверсию с применимой редакцией

Архитектура версионирования условий оффера: immutable snapshot, effective interval, checksum, запрет пересечений, привязка событий и impact-анализ.

Оглавление13 разделов

Изменение условий оффера нельзя хранить как перезапись одной JSON-записи. Каждая конверсия должна однозначно ссылаться на редакцию, действовавшую в выбранный бизнес-момент. Для этого условия становятся неизменяемыми версиями с effective interval, checksum, источником и журналом утверждения. Позднее событие не должно получить сегодняшние правила только потому, что пришло сегодня.

Главный инвариант

Для одного offer_id и scope в любой момент времени существует не более одной применимой версии. Опубликованная версия неизменяема; исправление создаёт новую запись.

Разделите три времени

  • occurred_at — когда произошло квалифицирующее событие
  • received_at — когда система его получила
  • decided_at — когда принято коммерческое решение

Версия условий выбирается по времени, определённому договором. Часто это occurred_at, но для отдельных моделей может использоваться click_at, registration_at или начало расчётного периода. Выбор фиксируется в offer contract и не меняется от удобства отчёта.

Immutable snapshot

Snapshot содержит не только payout. В него входят модель, валюта, квалифицирующее действие, hold, caps, допустимые источники, GEO, reason codes, правила атрибуции и ссылки на первичный документ. Нормализованные поля помогают расчёту, а полный исходный документ сохраняет доказательство контекста.

Каркас версии условий
{
  "offer_id": "offer-stable-id",
  "terms_version": "terms-2026-08-01-01",
  "scope": {
    "geo": "documented-geo",
    "traffic_source": "documented-source",
    "commercial_model": "CPA"
  },
  "effective_from": "2026-08-01T00:00:00Z",
  "effective_to": null,
  "qualification_event": "documented-event",
  "currency": "documented-iso-code",
  "source_url": "replace-with-primary-document",
  "source_document_hash": "sha256:replace-with-calculated-hash",
  "approved_by": ["commercial-owner", "compliance-owner"]
}

Пример показывает структуру и намеренно не содержит ставки или реального GEO. В production все documented/replace поля должны быть заполнены первичными данными до активации.

Полуоткрытые интервалы

Используйте границы [from, to): начало включено, конец исключён. Тогда версия A, заканчивающаяся ровно в 12:00, и версия B, начинающаяся в 12:00, не пересекаются и не оставляют щель. Timestamp хранится с timezone; текст «с понедельника» сначала превращается в однозначный instant по согласованной зоне.

PostgreSQL: версии без пересечений
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE offer_terms_versions (
  offer_id       text NOT NULL,
  scope_key      text NOT NULL,
  terms_version  text PRIMARY KEY,
  valid_during   tstzrange NOT NULL,
  terms_document jsonb NOT NULL,
  document_hash  text NOT NULL,
  source_url     text NOT NULL,
  approved_at    timestamptz NOT NULL,
  approved_by    text[] NOT NULL,
  EXCLUDE USING gist (
    offer_id WITH =,
    scope_key WITH =,
    valid_during WITH &&
  )
);

PostgreSQL range types представляют интервалы, а exclusion constraint запрещает пересечение для одинакового offer_id и scope_key. Если инфраструктура другая, тот же инвариант реализуется транзакционной проверкой и тестами, но не только проверкой интерфейса.

Scope нельзя прятать в комментарии

Одна версия может отличаться по GEO, источнику, модели оплаты или группе паблишеров. Scope нормализуется в стабильный ключ или отдельные измерения. Свободный текст «особые условия для команды» невозможно надёжно сопоставить с событием и проверить на пересечение.

Привязка события к версии

SQL: найти применимые условия по occurred_at
SELECT e.conversion_id,
       v.terms_version,
       v.document_hash
FROM conversion_events AS e
JOIN offer_terms_versions AS v
  ON v.offer_id = e.offer_id
 AND v.scope_key = e.scope_key
 AND v.valid_during @> e.occurred_at
WHERE e.conversion_id = :conversion_id;

Если запрос возвращает ноль строк, событие не получает «ближайшую» версию автоматически: оно попадает в quarantine. Две строки означают нарушение инварианта. Обе ситуации требуют исправления данных, а не эвристики.

Закрепите terms_version в событии решения

После сопоставления terms_version записывается в conversion decision и финансовое движение. Повторный расчёт использует сохранённую версию и hash. Это защищает от изменения справочника и позволяет доказать, почему одинаковые события в разные даты получили разные решения.

Новая версия: процесс публикации

  1. Получить первичный документ и effective moment.
  2. Сравнить с текущей версией на уровне нормализованных полей и текста.
  3. Определить scope и список затронутых кампаний.
  4. Проверить, что новый интервал не пересекает существующий.
  5. Рассчитать impact для открытых кликов и pending-конверсий.
  6. Получить утверждение владельцев.
  7. Атомарно закрыть старый интервал и открыть новый.
  8. Разослать машинное событие terms_version_activated.
  9. Проверить первые сопоставления и отсутствие quarantine.

Diff, пригодный для решения

  • Изменившееся поле и старое/новое значение
  • Источник и hash обеих редакций
  • Effective time и timezone
  • Затронутые GEO, источники, кампании и когорты
  • Изменение payout, cap, hold, qualification или attribution
  • Нужна ли остановка доставки или только обновление текста
  • Влияние на уже созданные pending события
  • Владелец проверки и план отката
SQL: объём открытой когорты перед активацией
SELECT e.campaign_id,
       count(*) AS open_conversions,
       min(e.occurred_at) AS oldest_event,
       max(e.occurred_at) AS newest_event
FROM conversion_events AS e
LEFT JOIN conversion_current_status AS s
  ON s.conversion_id = e.conversion_id
WHERE e.offer_id = :offer_id
  AND e.scope_key = :scope_key
  AND e.occurred_at < :new_effective_from
  AND coalesce(s.status, 'pending') = 'pending'
GROUP BY e.campaign_id
ORDER BY open_conversions DESC;

Этот отчёт показывает, сколько событий после активации продолжат созревать по старой версии. Их нельзя автоматически пересчитать по новой только из-за позднего decided_at.

Ретроактивное изменение

Если партнёр сообщает изменение задним числом, система создаёт correction request, а не редактирует valid_during молча. Запрос хранит основание, источник, объявленный интервал и предполагаемый impact. После согласования формируются новые status_version или ledger adjustments со ссылкой на correction_id.

Исторический snapshot остаётся. Отчёт может показать «как было решено» и «как скорректировано». Это важно для разбирательства: пересчитанная сумма без следа выглядит как исходное условие, которым она не была.

Caps и pacing при смене редакции

Нужно заранее определить, относится cap к календарному дню, версии или непрерывному офферу. Если версия меняется внутри дня, обнуление счётчика может вызвать перелив, а перенос без основания — недолив. Counter policy хранится в документе условий и тестируется на границе effective_from.

Кеши и доставка

CMS, tracker и отчёты могут обновиться не одновременно. Событие активации содержит terms_version и effective_from; потребители подтверждают применённую версию. До достижения согласованного состояния новые кампании не запускаются. Cache TTL не должен быть длиннее допустимого окна рассинхронизации.

Тесты инвариантов

  • Две версии одного scope не пересекаются
  • Событие на нижней границе получает новую версию
  • Событие ровно на верхней границе не получает старую
  • Поздно доставленное событие использует occurred_at
  • Неизвестный scope попадает в quarantine
  • Повторная доставка сохраняет terms_version
  • Correction создаёт новую историю и ledger entry
  • Hash опубликованного snapshot не меняется
Не используйте updated_at как версию условий

Время записи в базу не сообщает, когда правило начало действовать. Нужны отдельные effective time, terms_version и источник.

Источники и границы применимости

PostgreSQL: range types and exclusion constraints

Вывод

Версионирование условий делает расчёт воспроизводимым: immutable snapshot хранит доказательство, полуоткрытый интервал выбирает редакцию, exclusion constraint защищает от двойного применения, а terms_version остаётся в статусе и ledger. Изменение задним числом становится видимой корректировкой, а не переписанной историей.

Станьте партнёром и начните работать

Перейдите в партнёрскую программу, изучите актуальные условия и выберите подходящий формат сотрудничества.

СТАТЬ ПАРТНЁРОМ

Связанные материалы

Все статьи
Партнёрские программы

Мониторинг условий оффера: как замечать изменения до потери маржи

Практическая система мониторинга условий оффера: что фиксировать, как сравнивать версии, оценивать затронутый трафик и безопасно реагировать на изменения.

Партнёрские программы

Responsible gambling на affiliate-странице: какие элементы действительно помогают

Как встроить responsible gambling в affiliate-страницу: возрастная маркировка, нейтральная коммуникация, помощь, инструменты контроля и проверка ссылок.

Арбитраж трафика

Как начать арбитраж трафика: первый цикл от выбора задачи до разбора результата

Большое руководство для старта в арбитраже трафика: выбор задачи, оффера и источника, расчёт бюджета, настройка измерения, запуск и разбор первого теста.