# Постбэк-приёмник пилота — спека (стаб)

Cloudflare Pages Functions в проекте `reten-pilot` (каталог `pilot/functions/pb/`).
Назначение: принять событие оператора (reg/dep), найти сохранённый click-id сети
и ударить S2S-постбэк сети с ретраями и логом. Это СТАБ для обкатки петли —
боевой приёмник станет частью Storage.

Форматы постбэков сетей — из аудита `research/providers.json`; всё, что аудитом
не покрыто, помечено «проверить в кабинете» (см. `pilot/PILOT-MAPPING.md`).

## Поток данных

```
клик по рекламе сети
  └─ URL лендинга: &mm_kdm={click_id}(Kadam) / &mm_exo={conversions_tracking}(Exo)
       └─ GTM: magicpix-заглушка ловит параметры, хранит localStorage+кука,
          шлёт POST /pb/collect {uid, kadam_click_id?, exo_tag?}
            └─ KV: click:<uid> (TTL 90 дней — максимум окна конверсии Exo)

событие продукта (reg | dep)
  └─ обкатка: GTM-тег «S2S-форвардер» (на паузе по умолчанию) или тест-консоль
     бой:     серверный вебхук продукта → те же эндпоинты
       ├─ GET/POST /pb/kadam?event=reg|dep&uid=…(или click_id=…)
       │    └─ GET https://kdtrk.net/ru/postback/?data=<click_id>   (ретраи ×3)
       └─ GET/POST /pb/exo?event=lp|reg|dep&uid=…(или tag=…)&value=…
            └─ GET http://s.magsrv.com/tag.php?goal=<GOAL_ID>&tag=<tag>&value=…
               успех = HTTP 2xx И тело "OK"                        (ретраи ×3)
```

## Эндпоинты

### POST /pb/collect
Захват click-id при визите. Тело — JSON (шлётся как text/plain ради
sendBeacon без preflight; Content-Type не проверяется), query-параметры
имеют приоритет над телом.

| поле | обяз. | описание |
|---|---|---|
| `uid` | да | идентификатор визитёра от magicpix (`_mm_uid`) |
| `kadam_click_id` | нет | значение макроса Kadam `{click_id}` из URL |
| `exo_tag` | нет | click ID ExoClick из `{conversions_tracking}`; по аудиту 150–700 символов — валидируем верхнюю границу (400 при >700) |
| `page` | нет | путь страницы (обрезается до 300 симв.) |

Хранение: KV `click:<uid>` → merge-объект `{kadam_click_id, exo_tag, page, ts}`,
TTL 90 дней. Ответ: `{ok, uid, stored}` + CORS `*` (эндпоинт зовётся с лендингов
операторов; секретов не отдаёт).

### GET|POST /pb/kadam
Событие → постбэк Kadam. Параметры: `event` (reg|dep, обяз.), `click_id` или
`uid` (поиск в KV), `value`, `product` (только в лог), `dry=1`.

Цель: `KADAM_POSTBACK_BASE + "?data=" + click_id`. Формат из аудита:
`https://kdtrk.net/ru/postback/?data={subid}`. Точный URL, выдаваемый аккаунту,
и разделение reg/dep на стороне Kadam — НЕ документированы в аудите: событие
пишем в лог, разделение выясняется в кабинете (вероятно — конфигурацией
S2S-аудиторий/трекера). Успех: HTTP 2xx.

Ошибки: 400 — нет event/click_id; 502 — сеть не ответила 2xx после ретраев.

### GET|POST /pb/exo
Событие → постбэк ExoClick. Параметры: `event` (lp|reg|dep, обяз.), `tag` или
`uid` (поиск в KV), `value` (сумма — для dep), `product` (в лог), `dry=1`.

Цель: `EXO_POSTBACK_BASE + "?goal=<EXO_GOAL_{EVENT}>&tag=<tag>[&value=…]"`.
Формат из аудита: `GET http://s.magsrv.com/tag.php?goal&tag&value`, ответ "OK";
https в аудите не зафиксирован — если кабинет подтвердит, сменить
`EXO_POSTBACK_BASE`. Успех: HTTP 2xx И тело `"OK"` (после trim).

Ошибки: 400 — нет event/tag или tag >700 симв.; 409 — GOAL_ID не подставлен
(плейсхолдер `GOAL_ID_*`); 502 — нет «OK» после ретраев.

Дубликаты: пиксельная цель (тег GTM) и S2S на ту же цель считаются дважды —
на бою один канал на цель (см. TEST-PLAN).

### GET /pb/log?limit=50[&full=1]
Последние записи лога (новые сверху, максимум 200). По умолчанию — краткие
сводки из KV-metadata (`{at, type, net, event, dry, ok, status}`) одним
KV-запросом: N+1 чтений упёрлись бы в лимит 50 сабреквестов Workers.
`?full=1` — полные записи (target, tried) для первых ≤40 ключей.
KV eventually consistent — свежая запись может появиться с задержкой до
~минуты. TTL записей 7 дней.

### GET /pb/health
Самопроверка: KV-биндинг, TEST_MODE, требуется ли ключ, настроены ли
EXO_GOAL_* (без раскрытия значений). Без ключа.

## Ретраи

До 3 попыток с паузами 400 мс / 1500 мс. Каждая попытка (статус, тело до
200 симв., ошибки сети) — в массиве `tried` записи лога. Неуспех после всех
попыток → HTTP 502 вызывающему (продукт может переслать событие повторно —
дедупликации на стабе нет, это осознанное упрощение).

## Тестовый режим

`TEST_MODE=1` (глобально, vars) или `?dry=1` (на запрос): постбэк НЕ
отправляется, в лог пишется полный target-URL с пометкой dry-run, ответ —
`{ok:true, dry:true, target}`. Снимать TEST_MODE только после подстановки
боевых GOAL_ID и проверки логов.

## Конфигурация (vars / .dev.vars — секреты в код не коммитим)

| var | по умолчанию | смысл |
|---|---|---|
| `TEST_MODE` | `"1"` | dry-run всех постбэков |
| `KADAM_POSTBACK_BASE` | `https://kdtrk.net/ru/postback/` | из аудита; проверить в кабинете |
| `EXO_POSTBACK_BASE` | `http://s.magsrv.com/tag.php` | из аудита; https — проверить |
| `EXO_GOAL_LP/REG/DEP` | `GOAL_ID_*` (плейсхолдеры) | ID целей после регистрации |
| `PB_KEY` | пусто | опциональный ключ `?key=`; боевое значение — секретом (`wrangler pages secret put PB_KEY`) |

KV-биндинг: `PB_KV` (namespace `reten_pilot_pb`).

## Контракт dataLayer (для GTM-blueprint)

```js
// регистрация
dataLayer.push({ event: 'reg', product: 'brand-a' });        // product — опционален
// депозит
dataLayer.push({ event: 'dep', value: 50, product: 'brand-a' }); // value — сумма
```

Триггеры контейнера: custom event `reg` / `dep`. Переменные: `DLV — product`,
`DLV — value`. Заглушка magicpix дополнительно пушит `mm_ready` с `mm_uid`.

## Чего стаб сознательно НЕ делает

- Дедупликация повторных событий (бой: idempotency-ключ по uid+event).
- Подпись/валидация источника события (бой: серверный вебхук с секретом).
- Отправка в Kadam API (аудитории/конверсии читаем руками по TEST-PLAN).
- Очереди на ретраи дольше 2 секунд (бой: Cloudflare Queues).
