Пошаговые планы было→ожидали→стало с код-фактами 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>
9.2 KiB
Приёмка liderra.ru — ПУНКТ №5: Идемпотентность (повтор заявки = одно списание, без дублей)
Для исполнителя: формат PR1 (одобрен 20.06). Каждый шаг =
🔧 Код-факт (file:line)·🎬 Действие·📋 ОТЧЁТ (было→ожидали→стало + 📸/📝)·❌ Если не так. Техника «под капотом»; владельцу — человеческие карточки.
Цель: показать, что если одна и та же заявка придёт повторно (поставщик может прислать дубль, может быть техническая ретрансляция) — портал не задвоит: не создаст вторую сделку и не спишет деньги ещё раз. Один реальный лид = одна сделка = одно списание, при любом числе повторов.
Что снимаем глазами: Сделки (после повтора — та же одна сделка, не две) и Баланс (после повтора — не изменился, второго списания нет). Живые 📸.
Источник сценария: свод проверок №5 (D-IDEMP/DELIV/MERGE); R3 «движок».
🔧 Код-факты (подтверждено чтением 20.06)
- Дедуп по
vidна входе — уsupplier_leads.vidUNIQUE INDEX; повтор того жеvid→ ответ 200already_processedбез повторной обработки — SupplierWebhookController.php:29-30, :107-112. Новыйvid→ 202accepted— :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 (статусы).