Files
portal/docs/superpowers/plans/2026-06-20-acceptance-05-idempotency-plan.md
T
Дмитрий b6109e1485 docs(приёмка): 11 планов-отчётов по пунктам приёмки liderra.ru в формате PR1
Пошаговые планы было→ожидали→стало с код-фактами file:line для пунктов:
PR2 проекты, PR3 балансы, 01 распределение CAP3, 03 каскад региона,
05 идемпотентность, 07 тройная запись, 08 тариф-ступень,
09 нехватка→откат, 10 два проекта одно списание,
13 сделки лента/карточка/статусы, 14 ручная/массовые/экспорт.

NB 14: защита экспорта от CSV-инъекции в коде не найдена — помечено как находка под проверку на прогоне.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:11:27 +03:00

9.2 KiB
Raw Blame History

Приёмка liderra.ru — ПУНКТ №5: Идемпотентность (повтор заявки = одно списание, без дублей)

Для исполнителя: формат PR1 (одобрен 20.06). Каждый шаг = 🔧 Код-факт (file:line) · 🎬 Действие · 📋 ОТЧЁТ (было→ожидали→стало + 📸/📝) · ❌ Если не так. Техника «под капотом»; владельцу — человеческие карточки.

Цель: показать, что если одна и та же заявка придёт повторно (поставщик может прислать дубль, может быть техническая ретрансляция) — портал не задвоит: не создаст вторую сделку и не спишет деньги ещё раз. Один реальный лид = одна сделка = одно списание, при любом числе повторов.

Что снимаем глазами: Сделки (после повтора — та же одна сделка, не две) и Баланс (после повтора — не изменился, второго списания нет). Живые 📸.

Источник сценария: свод проверок №5 (D-IDEMP/DELIV/MERGE); R3 «движок».


🔧 Код-факты (подтверждено чтением 20.06)

  • Дедуп по vid на входеу supplier_leads.vid UNIQUE INDEX; повтор того же vid → ответ 200 already_processed без повторной обработки — SupplierWebhookController.php:29-30, :107-112. Новый vid202 accepted:130-132.
  • Защита от повторной обработки (ретраи джоба) — если лид уже processed_at → выходим, не создаём дубли сделок — RouteSupplierLeadJob.php:113-121.
  • Замок поставки на (заявка × клиент)supplier_lead_deliveries через insertOrIgnore; если строка уже есть (повтор/гонка/CSV-восстановление) → 0 → доставка считается уже сделанной, без второго списания:404-418, коммент «без второго списания» — :401.
  • Сделка несёт source_crm_id = vid:439 (связь сделки с исходным id заявки).

Шаг 5-1 — Первая заявка проходит нормально

🔧 Код: приём — SupplierWebhookController :90-132; списание — LedgerService :53-95. 🎬 Действие (лок): влить заявку с конкретным vid (например 555000111) одному клиенту; зафиксировать баланс «до» и число сделок.

📋 ОТЧЁТ:

Что Было Ожидалось Стало (факт) Чем подтверждаем
Ответ на приём 202 accepted (новая заявка) (слот прогона) 📝 ответ вебхука 202
Сделка нет 1 сделка создана (слот) 📸 Сделки клиента (1 новая)
Баланс X ₽ X − цена (одно списание) (слот) 📸 Биллинг (1 списание)

Если не так: заявка не принялась / нет сделки → проверить источник/секрет (Фаза 2).


Шаг 5-2 — Повтор той же заявки (тот же vid) → НЕ задвоилось

🔧 Код: дедуп — SupplierWebhookController :107-112; ретрай-guard — RouteSupplierLeadJob :113-121. 🎬 Действие (лок): влить ту же заявку с тем же vid (555000111) ещё раз (можно 2–3 раза).

📋 ОТЧЁТ:

Что Было Ожидалось Стало (факт) Чем подтверждаем
Ответ на повтор 200 already_processed (не принят как новый) (слот) 📝 ответ вебхука 200 already_processed
Сделки 1 всё ещё 1 (вторая не создана) (слот) 📸 Сделки клиента (по-прежнему 1)
Баланс X цена не изменился (второго списания нет) (слот) 📸 Биллинг (по-прежнему 1 списание)

💡 Что внутри: портал помнит каждую заявку по её номеру (vid). Когда приходит точно такая же — он видит «это я уже обработал» и просто отвечает «ок, уже принято», но ничего не делает заново: ни второй сделки, ни второго списания. Поэтому даже если поставщик пришлёт один и тот же лид десять раз — клиент заплатит за него один раз.

Если не так: повтор создал вторую сделку / списал деньги ещё раз / вернул 202 как новый → СТОП, денежный дефект (задвоение списаний — критично).


Шаг 5-3 — Защита и при технических повторах (ретрай/гонка/восстановление) — 📝

🔧 Код: замок поставки — RouteSupplierLeadJob :404-418. 🎬 Действие: внутренняя проверка — повтор доставки одному клиенту (ретрай фоновой задачи, гонка, восстановление из CSV) не создаёт второе списание.

📋 ОТЧЁТ:

Что Было Ожидалось Стало (факт) Чем подтверждаем
Повторная доставка клиенту замок supplier_lead_deliveries ловит → второго списания нет (слот) 📝 лог delivery_already_locked; в БД одна строка доставки

💡 Что внутри: даже если технически (сбой связи, повторный запуск фоновой задачи) портал попытается доставить ту же заявку тому же клиенту дважды — внутри стоит «замок»: на пару «заявка + клиент» поставка возможна только один раз. Второй раз деньги не спишутся.

Если не так: двойная доставка одному клиенту со вторым списанием → денежный дефект.


Verification №5 (выход из пункта)

  • 📝 Первый приём — 202 accepted; повтор того же vid — 200 already_processed.
  • 📸 После повтора сделок столько же (1), не две.
  • 📸 После повтора баланс не изменился (одно списание на реальный лид).
  • 📝 Замок supplier_lead_deliveries: одна строка на (заявка×клиент); delivery_already_locked при повторе.

📸 vs 📝 в этом пункте

  • 📸 Скриншот: Сделки (число не растёт на повторе), Биллинг (одно списание).
  • 📝 Текст: коды ответа вебхука (202/200), замок доставки, processed_at — глазами не видны.

Грабли

  • Идемпотентность завязана на vid — для теста повтора слать тот же vid; разный vid = разные заявки (так и должно).
  • Случай слияния с восстановлением из CSV (D-MERGE) — частный случай того же замка; глазами не показать, проверяется логом/БД.
  • «Won»-сделка не списывает повторно — это отдельный пункт №13 (статусы).