Skip to content

ТЗ — Импорт данных (cms/import)

Слой: 🔵 инфра-модуль · Зрелость доноров: ★★ · Донор: masha (masha/src/app/Domain/Import/ExcelImportService.php, masha/src/app/Domain/Wb/), universal (universal/src/app/Services/Import/*) Статус: ТЗ к разработке

Назначение и возможности

Универсальный пайплайн импорта данных: источник (CSV/XML/API) → маппинг полей → валидация → батчевый upsert. Рассчитан на highload — большие файлы обрабатываются чанками через Bus::batch, без построчной нагрузки на кеш и БД.

  • Источники: загрузка файла (CSV/XML), staged-upload JSONL для машинного bulk-импорта (интеграции, по образцу api-lessons §8), внешний API (переиспользует cms/integrations-bus)
  • Конфигурируемый маппинг колонок/полей на атрибуты модели, сохраняемые маппинг-профили как отдельные сущности (cms_import_mappings), переиспользуемые между задачами
  • Батчевый upsert в доменные сущности других модулей — только через провайдес-контракт import-target (реестр cms/core-contracts, ревизия ядра 14.07.2026, п.9), не raw mass-assignment по имени модели
  • Валидация каждой строки с накоплением построчных ошибок (не прерывает весь импорт)
  • Батчевый upsert чанками через Bus::batch, идемпотентность по external_id
  • Dry-run режим — прогон валидации без записи в БД
  • Отчёт об ошибках построчно (CSV) или JSONL «строка N = результат строки N» для bulk-канала, доступный для скачивания
  • Батчевая инвалидация кеша по завершении импорта, а не на каждую строку
  • Highload-путь: COPY/чанки для PostgreSQL вместо построчных INSERT
  • Аварийная приостановка приёма новых заданий (import.new_jobs_paused) без выключения модуля

Не путать с cms:export/cms:import ядра (data-exchange.md, раздел «Перенос типов контента») — та пара переносит схемы типов контента между инсталляциями (dev → prod, manifest.json

  • apply-токен). cms/import — про пользовательские данные и файлы (прайсы, фиды, выгрузки из внешних систем) внутри одной инсталляции, механизм и модель данных полностью разные.

Зависимости и выключение

requires: ядро · suggests: cms/integrations-bus

Поведение при выключении: UI и команды импорта недоступны, ранее импортированные данные не затрагиваются — деградация, не поломка. Задания в статусе running на момент выключения не прерываются штатно (модуль не реализует принудительную остановку своих же джоб) — джоба доработает до конца, попытка создать новое задание через API/UI при выключенном модуле получает 404/деградацию UI. cms/integrations-bus отсутствует/выключен → источник «внешний API» недоступен в мастере создания задания, доступен только источник «файл»/JSONL.

Модули-владельцы доменных сущностей (каталог, товары и т.д.) не объявляются в requires/ suggests — связь односторонняя, обнаруживается через реестр provides-контрактов import-target (канал 3, ревизия ядра 14.07.2026, п.9): владелец сам решает, принимать ли bulk-импорт, реализуя контракт при bootstrap. Нет ни одной реализации — мастер предлагает только сущности ядра (PageRepository/ContentRepository/TaxonomyRepository) — штатная деградация канала 3.

Модель данных

ТаблицаКлючевые поляПримечание
cms_import_jobsid, source_type, mapping (json), status, total_rows, processed_rows, dry_runшапка задания импорта
cms_import_job_errorsid, import_job_id, row_number, errors (json), raw_row (json)построчные ошибки
cms_import_mappingsid, name, target_model, mapping (json)сохранённые пресеты маппинга

Заметки: status — PHP Enum; mapping/errors/raw_row — JSONB с касом 'array' (GIN не обязателен — структурный конфиг, не поиск); FK import_job_idconstrained() + index(). Индекс (import_job_id, row_number) на cms_import_job_errors под keyset-пагинацию отчёта; индекс по created_at под пакетную очистку по import.error_report_retention_days (BRIN-кандидат при большом объёме журнала). cms_import_jobs индексируется по status — под виджет «активные/зависшие задания» в дашборде и health-чек.

cms_import_mappings — маппинг-профиль как полноценная сущность, не встроенный JSON внутри задания: target_model в ней хранит имя реализации import-target (не произвольный Eloquent- класс), профиль переиспользуется между задачами одного типа фида (например «фид Wildberries» — один профиль, применяемый к каждой новой выгрузке); уникальный индекс (name), индекс по target_model под фильтр «профили для этой цели» в мастере создания задания.

Входные и выходные данные

Входы

ИсточникДанные/поляЧем валидируется
Filament/POST /api/v1/admin/import/jobssource_type (file/jsonl_bulk/api), file (CSV/XML) или staged-upload id, mapping/mapping_id, target (имя реализации import-target либо сущность ядра), dry_runFormRequest whitelist: source_type — enum, file — через медиатеку (MIME/размер/import.max_file_size_mb), target — только из реестра зарегистрированных import-target-реализаций + whitelist сущностей ядра (import.allowed_target_models, см. «Крайние случаи»), mapping — только объявленные атрибуты цели, без системных полей (id, created_at, deleted_at, ключи авторизации)
Содержимое CSV/XML-файлапострочные значения по колонкампострочная валидация: rules движка полей по целевой сущности (тип, обязательность, формат), кодировка приводится к UTF-8 на входе потокового чтения
Staged-upload JSONL (bulk-канал, по api-lessons §8)строка = набор полей записи, __parentId для иерархийтот же rules-барьер построчно; отчёт — JSONL, где строка N = результат строки N, лимит: 1 активная bulk-операция на клиента
Внешний API через cms/integrations-bus (suggests)тот же построчный формат, что отдаёт коннекторсхема ответа коннектора валидируется на стороне cms/integrations-bus до передачи в cms/import; данные из внешнего источника не считаются доверенными — проходят те же rules, что и файл
POST /api/v1/admin/import/mappings (сохранение профиля)name, target, mapping (json)FormRequest whitelist, target — тот же whitelist реализаций import-target, что и при создании задания

Всё, что не перечислено как вход (незадекларированные колонки, поля вне маппинга, параметры вне FormRequest-схемы) — отвергается на границе, а не игнорируется молча.

Выходы

ПотребительДанныеФормат
Filament UI / /api/v1/admin/import/jobs*статус, прогресс (processed_rows/total_rows), построчный отчёт об ошибкахконверт {data, meta}, отчёт — keyset-пагинация
Модуль-владелец цели (реализация import-target)пакет строк на батчевый upsertвызов ImportTargetContract::upsertBatch() — модуль-владелец сам пишет в свои таблицы своим сервисом, cms/import не делает mass-assignment по чужой модели напрямую (ревизия ядра 14.07.2026, п.9)
Скачиваемый отчёт об ошибкахrow_number, errors, raw_rowCSV (файловый источник) или JSONL «строка N = результат» (bulk-канал), генерируется потоково самим модулем
События ImportJobStarted/ImportChunkProcessed/ImportJobCompletedфакты для подписчиков (например cms/audit)payload — раздел «События и обмен»
Батчевая инвалидация кешатеги затронутых моделейвызов CacheTags ядра по завершении задания

Настройки (группа import)

КлючТипДефолтaffectsPageCacheОписание
import.chunk_sizeint500нетРазмер чанка для Bus::batch
import.max_file_size_mbint50нетЛимит размера загружаемого файла
import.max_rows_per_fileint200000нетЛимит строк на файл — заградительный барьер до постановки в очередь
import.dry_run_defaultbooltrueнетDry-run по умолчанию для новых заданий
import.error_report_retention_daysint30нетХранение отчётов об ошибках
import.cache_flush_batchedbooltrueдаБатчевая инвалидация кеша по завершении, не построчно
import.allowed_target_modelsarray[] (пусто = только сущности ядра)нетДополнительный whitelist сущностей ядра (PageRepository/ContentRepository/TaxonomyRepository) поверх автоматически доступных реализаций import-target; для доменных модулей ограничение — сам факт регистрации контракта, не эта настройка
import.duplicate_strategystring"update"нетПоведение при совпадении external_id: update/skip/fail
import.csv_delimiter_defaultstring","нетРазделитель CSV по умолчанию (переопределяется на задании)
import.notify_on_completionbooltrueнетУведомление ответственного администратора по завершении задания
import.new_jobs_pausedboolfalseнетKill-switch (матрица v2.2): аварийная приостановка создания новых заданий без выключения модуля — уже идущие задания доигрывают
import.jsonl_bulk_enabledbooltrueнетВключение bulk-канала JSONL для машинных интеграций (api-lessons §8)
import.jsonl_signed_url_ttl_daysint7нетTTL подписанной ссылки на JSONL-отчёт bulk-импорта

API

МетодПутьДоступНазначение
POST/api/v1/admin/import/jobsadmin (import.manage)Создание задания импорта (файл/API-источник)
GET/api/v1/admin/import/jobsadmin (import.view)Список заданий (keyset-пагинация, фильтр по статусу)
GET/api/v1/admin/import/jobs/{id}admin (import.view)Статус и прогресс задания
GET/api/v1/admin/import/jobs/{id}/errorsadmin (import.view)Построчный отчёт об ошибках
POST/api/v1/admin/import/jobs/{id}/canceladmin (import.manage)Отмена ещё не завершённого задания
GET/POST/PUT/DELETE/api/v1/admin/import/mappingsadmin (import.manage)CRUD маппинг-профилей
POST/api/v1/admin/import/bulkтокен интеграции (import.manage)Staged-upload JSONL → 202 + id (api-lessons §8)
GET/api/v1/admin/import/bulk/{id}токен интеграции (import.view)Статус bulk-задания, object_count, ссылка на JSONL-отчёт по готовности

Построчный отчёт — keyset-пагинация (не OFFSET) для больших файлов. Мутации с внешним эффектом (POST /jobs, POST /jobs/{id}/cancel, POST /bulk) — с Idempotency-Key (§7 стандарта): повторный запрос с тем же ключом не создаёт второе задание и не отменяет уже отменённое. Bulk-канал выполняется вне общего rate-limit (тяжёлая, но редкая операция; лимит — 1 активная bulk-операция на клиента, не частота запросов).

Компоненты

Filament: мастер импорта (источник → маппинг → dry-run → запуск), страница отчёта об ошибках, список заданий с фильтром по статусу, CRUD маппинг-профилей отдельной страницей (профиль переиспользуется между задачами). Команды: cms:import:run --json, cms:import:status --json. Демо-контент (матрица v2.2): не применимо — публичных блоков/виджетов нет, admin-only инструмент.

События и обмен

СобытиеКогдаPayload
ImportJobStartedзадание поставлено в очередьimport_job_id, source_type, total_rows
ImportChunkProcessedобработан очередной чанкimport_job_id, chunk_index, processed, errors_count
ImportJobCompletedзадание завершено (успех/частично/провал)import_job_id, status, total_rows, error_rows

Слушает: сервис-вызов cms/integrations-bus (по suggests) как источник данных при импорте из внешнего API вместо прямого HTTP. Provides: не декларирует свой контракт, но потребляет канонический import-target (ревизия ядра 14.07.2026, п.9) — резолвит зарегистрированные модулями-владельцами реализации через DI, аналогично тому, как cms/commerce-facets потребляет search-provider у cms/search.

Таблица взаимодействий

Сущность/модульКаналНаправлениеЧто происходит
Медиатека ядра (MediaService)сервис-вызов по requires (канал 4)inЗагруженный файл сохраняется и проверяется по типу/размеру через MediaService, не собственным кодом
CacheTags ядрасервис-вызов по requires (канал 4)outБатчевая инвалидация тегов затронутых моделей по завершении задания
RequestContext ядрасервис-вызов по requires (канал 4)insite_id/locale/city_id для скоупирования импортируемых записей, если целевая модель несёт эти измерения
cms/integrations-bus (suggests)сервис-вызов (канал 4, при включении модуля)inИсточник построчных данных при импорте из внешнего API вместо прямого HTTP
Модуль-владелец цели (реализация import-target)provides-контракт (канал 3, ревизия ядра 14.07.2026, п.9)outБатчевый upsert записей владельца по маппингу через его собственный сервис — cms/import не пишет в чужие таблицы напрямую
cms/audit (если включён)слушает событие (канал 1)in для auditФиксирует факт запуска/завершения импорта, кто запустил, сколько строк
cms/notifications-bus (если включён, import.notify_on_completion)сервис-вызов (канал 3, notification-channel)outУведомление ответственного о завершении задания; выключен — log-fallback

Фоновая работа

Очередь import — весь пайплайн (валидация, upsert, инвалидация кеша) батчами через Bus::batch, синхронного импорта в HTTP-запросе нет. Джобы идемпотентны по external_id. Чтение исходного файла — потоковое (генератор по строкам CSV/XML-парсера, не file_get_contents/SimpleXML::load целиком) вне зависимости от import.chunk_size: чанк управляет размером батча записи в БД, а не тем, сколько файла лежит в памяти одновременно. Bulk-канал JSONL — staged-upload сохраняется в хранилище, дальше обрабатывается той же очередью import, отчёт пишется построчно в JSONL по мере обработки (строка N результата доступна, не дожидаясь всего файла).

Мини-ранбук (§15 стандарта):

СимптомЧто проверитьКоманда
Задание зависло в runningглубина очереди import, живость воркераcms:import:status --json
Импорт молча не пишет в целевой модульзарегистрирован ли import-target у владельцаcms:map --json (секция provides)
Массовый сбой строк с одной ошибкойнекорректный маппинг-профильсверка cms_import_mappings
Срочно остановить приём новых импортовинцидент у целевого модуля/БДimport.new_jobs_paused=true
Bulk JSONL не завершаетсяstaged-upload просрочен, object_count не растётGET /bulk/{id}

Производительность и кеш

Ожидаемые объёмы (managed-парк студии): типовой прайс/фид — от сотен до десятков тысяч строк (каталоги услуг/товаров), фиды маркетплейсов (аналог Wildberries-фида донора) — до import.max_rows_per_file (200000) строк, файлы CSV/XML — единицы-десятки мегабайт, лимит import.max_file_size_mb (50) — заградительный барьер для файлов, приложенных не тем способом (полный дамп БД в CSV и подобное).

Горячие пути и бюджет запросов: создание задания — 1 запрос (insert cms_import_jobs) + постановка Bus::batch в очередь, без синхронного парсинга файла в HTTP-запросе; статус задания — 1 запрос по PK; список ошибок — keyset-пагинация по индексу (import_job_id, row_number), без OFFSET. Upsert — один batched-запрос на чанк (import.chunk_size строк), не построчный INSERT; на PostgreSQL highload-путь — COPY/upsert ... on conflict вместо построчных вставок.

Что кешируется: сохранённые пресеты маппинга (cms_import_mappings) — read-heavy, редко меняются, тег import:mappings, инвалидация по событию сохранения/удаления пресета. Статус и прогресс задания — не кешируются, потому что читаются с высокой частотой поллингом Filament UI во время выполнения и обязаны быть свежими на 100% — кеш дал бы админу устаревший прогресс. Инвалидация кеша целевых моделей — одним батчем по ImportJobCompleted (import.cache_flush_batched), не на каждую строку и не на каждый чанк.

Безопасность

Загружаемый файл — только через медиатеку с проверкой типа/размера (import.max_file_size_mb), маппинг колонок — FormRequest-whitelist разрешённых атрибутов модели, без маппинга на системные поля. Импорт из внешнего API — только через cms/integrations-bus, не прямым HTTP.

Векторы, специфичные для модуля:

  • XXE/entity expansion в XML-источнике — парсер XML обязан грузиться с отключёнными внешними сущностями (libxml_disable_entity_loader/аналог), иначе файл-эксплойт получает доступ к файловой системе воркера через «безобидную» загрузку XML;
  • CSV/formula injection в отчёте об ошибках — построчный raw_row может содержать значения, начинающиеся с =/+/-/@ (формулы Excel); отчёт об ошибках при генерации экранирует такие значения (ведущий апостроф/квотирование), иначе открытие CSV админом в Excel выполняет чужой код;
  • маппинг на системные/скрытые поля — whitelist FormRequest действует и на id, created_at/updated_at/deleted_at, поля авторизации и связи — не только на «очевидно опасные» бизнес-поля;
  • произвольная цель импорта — только реализации import-target, зарегистрированные модулями- владельцами, плюс whitelist сущностей ядра из import.allowed_target_models; произвольный Eloquent-класс приложения недостижим как цель в принципе (нет reflection по имени класса из пользовательского ввода — резолв только по имени зарегистрированного контракта);
  • данные из внешнего API как доверенные — payload из cms/integrations-bus проходит те же rules движка полей, что и загруженный файл; коннектор не считается «безопасным источником» по умолчанию;
  • flood создания заданий — rate-limit на POST /jobs (per-граница ядра), иначе очередь import заваливается фиктивными заданиями; bulk-канал (POST /bulk) вне общего rate-limit, но ограничен «1 активная операция на клиента» (api-lessons §8) — тот же эффект другим механизмом.

ПДн-паспорт (матрица v2.2). Собственные таблицы не хранят ПДн намеренно, но cms_import_job_errors.raw_row может содержать ПДн импортируемых записей (email/телефон в фиде) — покрывается тем же error_report_retention_days (30 дней). ПДн в целевой сущности — паспорт модуля-владельца import-target, не этого модуля. «Выгрузить/забыть по запросу» — не применимо напрямую, данные не живут дольше ретеншна отчёта об ошибках.

Матрица ролей:

РольПросмотр заданий/отчётовСоздание/отмена/маппинг-профили
studio
админ
менеджер✅ (в проектах, где импорт — операционная задача, не только техническая)
редактор

Права: import.view, import.manage.

UX-требования

Админ:

  • пустое состояние списка заданий — «Импортов ещё не было» с кнопкой «Создать задание», не пустая таблица без объяснения;
  • массовое действие на списке заданий — отмена/повторный запуск нескольких заданий одним действием (для «зависших» после сбоя внешнего API), не по одному клику;
  • построчные ошибки читаются как факт на человеческом языке: «Строка 42: поле email обязательно к заполнению» — не голый JSON errors из cms_import_job_errors; при массе однотипных ошибок — агрегация («ещё 128 строк с той же ошибкой»), не простыня из тысяч строк;
  • подтверждение перед запуском не-dry-run импорта — модальное окно с числом строк и предупреждением о необратимости upsert, особенно если пользователь явно выключил dry_run_default;
  • прогресс задания виден в реальном времени (processed_rows/total_rows), не «крутилка» без деталей — критично для файлов в десятки тысяч строк;
  • дублирующиеся external_id в самом файле (внутри одной загрузки) — не тихий overwrite, а видимая пользователю сводка «N строк переопределили более раннюю строку того же файла».

Посетитель: прямого взаимодействия с модулем нет — импорт целиком admin-only функциональность, публичный сайт не обращается к cms/import ни в одном сценарии.

Крайние случаи и типовые баги

  • битые кодировки/CSV (не UTF-8, BOM, разные разделители, кавычки/переносы строк внутри полей) → потоковый парсер определяет и приводит кодировку к UTF-8 на входе, BOM отбрасывается, разделитель — из import.csv_delimiter_default либо явно указан на задании; строка с неразрешимым конфликтом кавычек/переноса — построчная ошибка в отчёте, не падение всего файла;
  • частичный импорт с ошибками → построчный отчёт в cms_import_job_errors обязателен для каждой невалидной строки, ImportJobCompleted со статусом «частично» отражает error_rows — молчаливый пропуск строки без записи в отчёт считается багом, а не приемлемой деградацией;
  • повторный импорт того же файла → при идентичном файле совпадающие external_id обрабатываются по import.duplicate_strategy (update по умолчанию — те же значения, эффект нулевой; skip — строки не трогаются; fail — задание завершается с построчными конфликтами), дублей записей не создаётся ни при каком режиме — идемпотентность гарантируется на уровне external_id, не на уровне «файл тот же/другой»;
  • повторный импорт частично изменённого файла → строки с изменившимися значениями обновляются по duplicate_strategy=update, неизменившиеся — no-op, новые external_id — вставляются; поведение идентично «первому» импорту минус уже существующие неизменные строки;
  • огромный файл → до постановки в очередь проверяются import.max_file_size_mb и (после предварительного подсчёта строк) import.max_rows_per_file; превышение — 422 без постановки задания в очередь, а не обрыв уже выполняющейся джобы; в границах лимитов чтение — потоковое (генератор по строкам), файл целиком в память не грузится независимо от chunk_size;
  • двойной сабмит формы создания заданияIdempotency-Key на POST /jobs предотвращает создание второго идентичного задания при повторном клике/ретрае клиента;
  • параллельные джобы над одной целью → батчи Bus::batch разных заданий над одной реализацией import-target не блокируют друг друга искусственно (upsert по external_id — конфликты решает БД/сам модуль-владелец в upsertBatch()), но два параллельных задания с разными duplicate_strategy над одними и теми же строками дают недетерминированный порядок применения — задание блокируется, если для той же цели уже выполняется активное задание (лок на уровне БД), второе — в очередь после первого;
  • выключение модуля посреди выполнения → уже запущенная джоба в очереди import доигрывает до конца (диспетчер очереди не знает о состоянии модуля ядра), UI/API создания новых заданий недоступны сразу; ранее импортированные данные не откатываются;
  • отсутствие suggests-модуля cms/integrations-bus → источник «внешний API» скрыт/недоступен в мастере создания задания, доступен только «файл» — не ошибка 500 при попытке выбрать отсутствующий источник;
  • сбой/таймаут внешнего API (через cms/integrations-bus) → чанк, не дождавшийся ответа, фиксируется как построчная ошибка «источник недоступен», задание не зависает бесконечно — таймаут на уровне джобы, ретраи с backoff (правило шины интеграций), после исчерпания — ImportJobCompleted со статусом «провал»;
  • пустой файл/датасет → задание завершается статусом «успех, 0 строк», не ошибкой — админ видит явное «файл не содержит строк для импорта», а не подвисшее «processing»;
  • отсутствие измерений locale/city/site у целевой модели → если целевая модель их не несёт, импорт работает без них (правило измерений §4 стандарта — nullable, не отдельная ветка кода); если несёт — RequestContext задаёт значение по умолчанию для всей партии, а не построчно;
  • новая bulk-операция при уже активной у того же клиентаPOST /bulk отклоняется 409 «уже выполняется bulk-операция» (лимит api-lessons §8: 1 активная операция на клиента), клиент дожидается завершения или отменяет текущую, не плодит параллельные staged-upload;
  • истёк TTL подписанной ссылки на JSONL-отчёт (jsonl_signed_url_ttl_days) → ссылка возвращает 403 у объектного хранилища, не 500 у cms/import; повторный запрос статуса задания всё ещё отдаёт метаданные (object_count, время завершения), только сам файл недоступен — задание не считается «потерянным», просто отчёт истёк по TTL;
  • отсутствует реализация import-target для нужной сущности → мастер создания задания не предлагает её как цель (не ошибка, штатная деградация канала 3, см. «Зависимости и выключение»); попытка обратиться напрямую по API с несуществующим именем цели — 422 до постановки в очередь;
  • Батчевый upsert в доменные модули — зафиксированное решение (ревизия ядра 14.07.2026, п.9). Исходная версия ТЗ конфигурировала upsert в произвольную target_model, что противоречило §5 стандарта и data-exchange.md (запись в чужие таблицы только через сервис владельца), при этом жёсткой зависимости на конкретные модули-владельцы в манифесте не было и не могло быть (список целей меняется на лету в UI). Разрешение: реестр provides пополнен каноническим import-target — модуль-владелец реализует контракт (upsertBatch() со своей валидацией/событиями) и регистрируется при bootstrap; cms/import резолвит цель через DI (канал 3), никогда не пишет в чужую таблицу напрямую. «Карантин» ad-hoc целей снят — доменные модули участвуют штатно через контракт.

Донорский код

Что взятьПуть
Excel-импорт, построчная валидацияmasha/src/app/Domain/Import/ExcelImportService.php
Импорт фида Wildberries (маппинг, идемпотентность)masha/src/app/Domain/Wb/
Импортёры категорий/товаров/изображений чанкамиuniversal/src/app/Services/Import/CategoryImporter.php, ProductImporter.php, ProductImageImporter.php

Миграция legacy-данных (§16, матрица v2.2): cms/import — сам механизм, которым другие модули реализуют cms:<slug>:import-legacy (донор → маппинг-профиль → import-target). Для самого cms/import переноса нет — профили создаются администратором по месту. CategoryImporter/ ProductImporter донора — референс реализации import-target в модуле-владельце.

Тесты и приёмка

  • [ ] Контрактный тест: повторный импорт с тем же external_id не создаёт дублей (идемпотентность)
  • [ ] Health-чек модуля отражает зависшие/просроченные задания импорта
  • [ ] При выключении модуля ранее импортированные данные остаются целыми
  • [ ] Батчевый upsert не создаёт N+1: один запрос на чанк, не на строку
  • [ ] Инвалидация кеша выполняется один раз по завершении задания, не на каждую строку
  • [ ] Dry-run не пишет в БД — покрыто тестом
  • [ ] Права import.view/import.manage разграничивают просмотр и запуск заданий
  • [ ] Битые CSV (не-UTF8, BOM, разные разделители, переносы строк в кавычках) не роняют джобу, дают построчные ошибки
  • [ ] XML-парсер не резолвит внешние сущности (XXE) — контрактный тест на злонамеренный файл
  • [ ] Отчёт об ошибках экранирует формулы Excel (=/+/-/@) при скачивании CSV
  • [ ] Частичный импорт даёт полный построчный отчёт, ни одна невалидная строка не пропущена молча
  • [ ] Файл сверх import.max_file_size_mb/import.max_rows_per_file отклоняется до постановки в очередь (422), не обрывает уже запущенную джобу
  • [ ] Потоковое чтение файла — тест на файл, заведомо больший разумного лимита памяти воркера
  • [ ] Выбор цели импорта ограничен зарегистрированными import-target + whitelist сущностей ядра — попытка обратиться к незарегистрированной цели даёт 422
  • [ ] Батчевый upsert для доменной цели идёт через ImportTargetContract::upsertBatch(), не raw mass-assignment
  • [ ] Двойной сабмит POST /jobs с Idempotency-Key не создаёт второе задание
  • [ ] Отсутствие cms/integrations-bus скрывает источник «API», не роняет мастер создания задания
  • [ ] Таймаут внешнего источника фиксируется построчной ошибкой, задание не зависает
  • [ ] Bulk-канал JSONL: POST /bulk отклоняет вторую активную операцию того же клиента (409)
  • [ ] JSONL-отчёт bulk-импорта — строка N соответствует результату строки N входного файла
  • [ ] import.new_jobs_paused=true блокирует создание новых заданий, не влияет на уже идущие
  • [ ] Маппинг-профиль (cms_import_mappings) переиспользуется между заданиями без потери настроек
  • [ ] Контрактный набор cms-testing пройден, пакет протестирован в testbench-изоляции
  • [ ] Feature-тест на каждый роут модуля; тестовая БД только import_test, migrate:fresh запрещён

Внутренняя база знаний студии