Files
portal/docs/superpowers/plans/2026-06-20-acceptance-23-csv-import-plan.md
T
Дмитрий dc14afdedd docs(приёмка): поправки планов по находкам ревью (Татарстан/капча/lpimp/воронка/№23/append-only)
Закрыты 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>
2026-06-21 05:38:37 +03:00

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/balanceHistoricalImportService.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 (статусы сделок).