Закрыты doc-находки двух ревью-сессий (точность планов перед прод-прогоном): - GAP-1: Татарстан 16→19 (16=Мордовия, конституц. порядок ст.65, НЕ ГИБДД) — свод, план №3 (C-2), PR2 (сноска: P5 [16]=Мордовия, не Татарстан). Сверено по RussianRegions.php. - M-2/GAP-3: капча = NullCaptchaVerifier БЕЗУСЛОВНО (не только local) — план №18. - lpimp-status: lpimp_ → 401 на биллинг/api-keys (не 403), 403 только на admin, /api/billing/charges читается — план №25. - N-6: воронка статусов НЕ форсится (любой валидный slug, нет state-machine) — план №13. - №23-tx: импорт пишет 1 нулевую historical_import строку; сверять баланс/lead_charges, не число balance_transactions — план №23. - append-only: lead_charges защищён GRANT'ом (prod/crm_app_user), не триггером → на dev-superuser правка пройдёт (ложный GREEN); гонять на проде — план №12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
Приёмка liderra.ru — ПУНКТ №23: Импорт сделок из CSV (без списания, без дублей)
Для исполнителя: формат PR1 (одобрен 20.06). Каждый шаг =
🔧 Код-факт (file:line)·🎬 Действие·📋 ОТЧЁТ (было→ожидали→стало + 📸/📝)·❌ Если не так. Техника «под капотом»; владельцу — человеческие карточки.
Цель: показать, что клиент может загрузить свою историю сделок из файла (CSV) — например, перенести базу из старой CRM — и при этом: сделки появляются в кабинете; деньги НЕ списываются (импорт — это перенос, а не покупка заявок); повторная загрузка того же файла не задваивает сделки; статусы из файла раскладываются по воронке портала (неизвестные — через мастер сопоставления).
Что снимаем глазами: Сделки (импортированные появились), Баланс (не изменился), мастер статусов. Живые 📸.
Источник сценария: свод проверок №23 (IMP-OK/IDEMP/STATUS/VALID/ISO); R3b «онбординг/UI».
🔧 Код-факты (подтверждено чтением 20.06)
- Импорт НЕ списывает деньги —
upsertRowсоздаётDealнапрямую, безLedgerService/charge/balance — HistoricalImportService.php:170-179. - Без дублей (идемпотентность) — дедуп по
(tenant_id, source_crm_id)черезwebhook_dedup_keys+ advisory-lock; повтор → обновляет существующую сделку (статус/контакт), новую не создаёт — :146-168. - Статусы из файла → воронка —
StatusRuToSlugMapper+ пер-клиентские правила сопоставления; неизвестные статусы собираются (мастер сопоставления) — resolveStatus :115-122, loadStatusOverrides :92-103. - Изоляция —
SET LOCAL app.current_tenant_id+where(tenant_id)на каждом шаге — :138-139, :101. - Битые строки не валят импорт — ошибочная строка логируется и пропускается, остальные импортируются — :77.
- След в журнале ПДн — на каждую импортированную сделку — запись обработки ПДн — :189-198.
Шаг 23-1 — Загрузить CSV → сделки появились
🔧 Код: HistoricalImportService::import :35-88; контроллер — ImportController.
🎬 Действие (лок): загрузить тест-CSV с несколькими сделками; зафиксировать число сделок «до».
📋 ОТЧЁТ:
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Сделки | N | N + число строк файла | (слот прогона) | 📸 Сделки (выросли на импорт) |
| Источник | — | помечены как импортные | (слот) | 📸 карточка (проект type=import) |
💡 Что внутри: клиент может перенести в портал свою прошлую базу сделок одним файлом — не вбивая руками. Портал читает файл и заводит сделки в кабинете.
❌ Если не так: сделки не появились / число не совпало → разобрать (формат файла, валидация).
Шаг 23-2 — Баланс НЕ списан (импорт ≠ покупка)
🔧 Код: upsertRow :170-179 (нет списания). 🎬 Действие (лок): сверить баланс «до» и «после» импорта.
📋 ОТЧЁТ:
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Баланс | X ₽ | не изменился (импорт не списывает) | (слот) | 📸 Баланс до = после |
| Списания | — | нет новых | (слот) | 📝 lead_charges не выросло |
NB (№23-tx): импорт пишет одну нулевую строку
type=historical_import, amount_rub=0вbalance_transactions(HistoricalImportService.php:281-295) — это аудит-след, баланс не меняется иlead_chargesне растёт. Сверять надо значение баланса иlead_charges, НЕ число строкbalance_transactions(одна нулевая строка появится — это норма). При импорте 0 валидных строк — строка не пишется.
💡 Что внутри: загрузка своей истории — это не покупка новых заявок, поэтому портал за неё денег не берёт. Баланс остаётся прежним: импортируй сколько угодно своих старых сделок — это бесплатно.
❌ Если не так: импорт списал деньги → денежный дефект (клиент платит за свою же историю).
Шаг 23-3 — Повторная загрузка не задваивает
🔧 Код: дедуп — :146-168. 🎬 Действие (лок): загрузить тот же файл ещё раз.
📋 ОТЧЁТ:
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Число сделок | N+импорт | не выросло (дубли не созданы) | (слот) | 📸 Сделки (число то же) |
| Существующие | — | обновились (статус/контакт), не задвоились | (слот) | 📝 один deal на source_crm_id |
💡 Что внутри: если случайно загрузить тот же файл дважды — портал не создаст вторые копии. Он узнаёт уже знакомые сделки по их номеру и просто обновляет, а не плодит дубликаты.
❌ Если не так: повтор задвоил сделки → дефект идемпотентности.
Шаг 23-4 — Статусы из файла → воронка (мастер для неизвестных)
🔧 Код: resolveStatus :115-122; правила — loadStatusOverrides :92-103. 🎬 Действие (лок): в файле — разные статусы (известные и нестандартные); пройти мастер сопоставления для неизвестных.
📋 ОТЧЁТ:
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Известные статусы | — | разложены по воронке портала | (слот) | 📸 Сделки (статусы проставлены) |
| Неизвестные статусы | — | предложены к сопоставлению (мастер) | (слот) | 📸 мастер статусов |
💡 Что внутри: в старой системе у клиента статусы могли называться по-своему. Портал известные сам раскладывает по своей воронке, а незнакомые показывает в мастере: «как это назвать у нас?» — клиент один раз сопоставляет, и дальше всё ложится автоматически.
❌ Если не так: статусы потеряны / нет мастера для неизвестных → дефект.
Шаг 23-5 — Изоляция и устойчивость (внутр.)
🔧 Код: изоляция — :138-139; пропуск битых строк — :77. 🎬 Действие: проверить, что импорт попал только в свой кабинет; битые строки не сорвали импорт.
📋 ОТЧЁТ:
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Принадлежность | — | импорт только своему клиенту | (слот) | 📝 все сделки своего тенанта |
| Битая строка | — | пропущена с логом, остальные импортированы | (слот) | 📝 лог import.row_failed |
❌ Если не так: импорт ушёл чужому / одна битая строка сорвала весь файл → дефект.
Verification №23 (выход из пункта)
- 📸 CSV-импорт → сделки появились (помечены импортными).
- 📸 Баланс не изменился; новых списаний нет.
- 📸 Повтор того же файла не задваивает (обновляет).
- 📸 Статусы разложены по воронке; мастер для неизвестных.
- 📝 Только свой кабинет; битые строки пропущены, не валят импорт.
📸 vs 📝 в этом пункте
- 📸 Скриншот: Сделки (импортные), Баланс до/после, мастер статусов.
- 📝 Текст: отсутствие списаний, дедуп по
source_crm_id, лог битых строк, принадлежность тенанту.
Грабли
- Импорт не списывает — ключевой денежный инвариант, сверять баланс до/после.
- Идемпотентность по
source_crm_id— для теста повтора слать тот же файл (те же id). - Связка с №5 (идемпотентность доставки) и №13 (статусы сделок).