📚 BusinessPlatform Docs

Серверный импорт каталога

Обновлено: 17 октября 2026, Europe/Moscow. Общий Commerce API для любых зарегистрированных приложений; Velvio — первый потребитель. Импорт не создаёт заказов и не обращается к платёжным провайдерам. Это отдельный механизм от прежнего импорта WB в кабинете.

Подключение и последовательность

База: https://app.irisaicrm.shop/api/v1/commerce/projects/{projectId}. Проект предварительно создаётся обычным provisioning Commerce API. Нужен короткоживущий серверный RSA JWT существующего клиента: aud=business-platform, его issuer/sub/kid. Приватный ключ остаётся у приложения-потребителя; браузер обращается к собственному BFF. Новых секретов импорта для потребителя нет.

Права: commerce:imports:read для истории, статуса и шаблона; commerce:imports:write для загрузки и подтверждения. Для полного сценария выдавать оба права. Общие commerce:read/commerce:write также покрывают соответствующие операции импорта. Сингулярный commerce:import не принимается. Явно настроенная совместимость с velvio:read/velvio:write сохраняется только для клиентов с LegacyVelvioScopes.

  1. Загрузить файл или URL YML с новым Idempotency-Key и X-Request-Id.
  2. Опросить задание до ready или failed. Разбор и предварительный отчёт ещё не меняют каталог и не скачивают изображения.
  3. Показать пользователю прогноз, предупреждения и ошибки. Явно подтвердить apply с выбранным режимом и новым ключом.
  4. Опросить до completed или failed, показать окончательный отчёт. Закрытие браузера не останавливает серверную работу.
Метод и путь Вход Успешный ответ
POST /imports multipart: source, file, необязательный sourceKey 202, непосредственно Job
POST /imports/yml-url JSON {"url":"https://example.org/catalog.xml","sourceKey":"default"} 202, непосредственно Job
GET /imports?offset=0&limit=20 offset 0..100000, limit 1..100 200, {items: Job[],total,offset,limit}
GET /imports/{jobId} UUID задания своего проекта 200, непосредственно Job
POST /imports/{jobId}/apply {"mode":"update_existing"} или {"mode":"create_only"} 202, непосредственно Job
GET /imports/template/shopify.csv — 200, CSV с совместимыми заголовками Shopify

Ответы загрузки и подтверждения не обёрнуты в data или job. Повтор запроса с тем же ключом возвращает первоначальные байты ответа 202 и Idempotency-Replayed: true; текущее состояние нужно читать GET. Для multipart сравнивается содержимое файла и полей, случайная граница multipart не влияет на повтор. Изменённое содержимое с прежним ключом — 409 idempotency_conflict.

Точный Job DTO

{
  "id": "00000000-0000-0000-0000-000000000001",
  "source": "shopify_csv",
  "fileName": "catalog.csv",
  "status": "ready",
  "summary": {
    "total": 2, "created": 1, "updated": 0, "skipped": 0, "errors": 1,
    "imagesTotal": 3, "imagesImported": 0, "imagesFailed": 0
  },
  "issues": [
    {"severity":"error","code":"invalid_price","message":"Price must be nonnegative RUB with at most two decimal places.","row":3,"externalId":"invalid-product"}
  ],
  "createdAt": "2026-10-17T07:00:00Z",
  "updatedAt": "2026-10-17T07:00:01Z",
  "error": null
}

Состояния: uploaded → parsing → ready → applying → completed; неустранимая ошибка задания ведёт в failed. error — nullable string с кодом ошибки задания. issues всегда массив; severity — warning/error, code/message — строки, row — nullable integer, externalId — nullable string. Номер — исходная строка таблицы; у YML это порядковый номер offer. Сообщения API диагностические, BFF может переводить их по code.

В ready created/updated — прогноз для update_existing, а errors — число исключённых продуктов. Прогноз пересчитывается относительно живого каталога при применении. Во время applying и после completed created/updated/skipped — фактически зафиксированные продукты, не варианты и не число SQL-запросов. При create_only прогноз обновлений превратится в skipped. Ошибочные продукты не применяются, остальные продолжают работу: completed может иметь errors > 0. Ошибка изображения — предупреждение и imagesFailed, а не ошибка всего продукта. Общий отказ инфраструктуры после пяти попыток даёт failed; уже зафиксированные продукты сохраняются. Повторный импорт использует upsert.

Форматы и соответствие модели

Каталог можно выгрузить через CSV/YML экспорт. Канонический Shopify header дополнен необязательной стандартной колонкой product.metafields.businessplatform.catalog: экспорт BP использует её для обратимого переноса расширенных полей. Импорт прежних файлов без неё сохраняется; обычные поля ограничены 32000 символами, эта JSON-колонка — 4 MiB с проверкой структуры и привязки SKU. Новая схема БД для этого не требуется.

source Файл Идентичность и особенности
yandex_yml .xml/.yml или URL yml_catalog/shop; offer.id — вариант, group_id группирует товар; категории с parentId, picture в исходном порядке, param — атрибуты; UTF-8/объявленная XML-кодировка
yandex_market_xlsx .xlsx Именованные колонки артикула/названия/цены/остатка; поиск строки заголовков среди первых 30 строк; инструкции пропускаются
wildberries_xlsx .xlsx Артикул продавца, название, цена, остаток и фото; процентная скидка преобразуется в текущую/зачёркнутую цену, если нет отдельной конечной цены
ozon_xlsx .xlsx и настоящий бинарный .xls Общий профиль именованных колонок: Артикул, Название товара, Цена, Количество, Категория, изображения; ID source не меняется для XLS
shopify_csv UTF-8 .csv, разделитель запятая Handle/URL handle — продукт, SKU — вариант; Option1..3, Price/Compare-at, stock, Published/Status; строки только с фото не становятся вариантами; Image Position задаёт порядок

Shopify template — поддерживаемое подмножество официальных совместимых заголовков, без собственного формата Velvio. Поддерживаются также новые названия колонок Shopify (URL handle, Description, SKU, Price, Product image URL и другие). Это не заявление, что файл побайтно совпадает с текущим официальным примером. Несколько складских локаций Shopify не импортируются.

Yandex/WB/Ozon меняют таблицы по категории. Неизвестная структура возвращает unsupported_template, без угадывания колонок по позиции. Таблица только с баркодом и количеством WB не является полным каталогом. Формулы XLSX отклоняются, макросы/внешние ссылки не допускаются. Бинарный XLS читается ExcelDataReader: формулы не вычисляются, библиотека возвращает сохранённые значения ячеек. Синтетический настоящий XLS проверяет бинарный тракт; актуальные категорийные шаблоны Ozon целиком не подтверждены из-за недоступности официального сайта при исследовании. Перед массовым импортом нового шаблона сначала проверять dry-run.

Продукт Commerce хранит название, описание, статус active/draft, категорию, изображения и атрибуты; варианты — SKU, цену RUB, compare-at, доступный остаток, активность и характеристики. Неподдерживаемая валюта отклоняется, конвертации нет. HTML описания превращается в текст. Если исходный SKU несовместим с ограничениями BP, создаётся стабильный ASCII SKU, исходный сохраняется в атрибутах и внешних привязках; отчёт содержит sku_generated.

Идентичность: tenant + source + sourceKey + externalId. sourceKey по умолчанию default; отдельные магазины/фиды одного формата должны иметь разные стабильные sourceKey (1..100 ASCII букв/цифр/точек/дефисов/подчёркиваний). Смена sourceKey означает новый источник. Существующий SKU может сопоставить продукт, но неоднозначные SKU/чужие продукты не объединяются автоматически. Архивные сущности не восстанавливаются молча. Категории создаются по иерархии, их внешние ID сохраняются; переименование/смена родителя сохраняют ID и проверяются на цикл.

update_existing создаёт новые и обновляет сопоставленные продукты; create_only пропускает существующие целиком. Отсутствующие в файле продукты и варианты не удаляются и не архивируются. Отсутствующая колонка остатка сохраняет прежнее значение; переданный stock задаёт доступное количество по правилам Commerce API, с сохранением уже существующих резервов. Пустой список картинок не удаляет прежние; если все новые картинки не скачались, старые остаются.

Изображения, лимиты и защита

Worker сам скачивает фотографии, проверяет MIME/сигнатуру/размер/габариты PNG, JPEG, WebP и сохраняет SHA256-объекты в собственном хранилище. SVG/GIF/HTML не принимаются. Это проверка формата и границ, без полной перекодировки изображения. В каталог записываются только собственные URL. Порядок сохраняется, одинаковые байты дедуплицируются; ошибки отдельных фото не останавливают другие строки.

Файл до 100 MiB, 2000 продуктов / 10000 вариантов, 100 вариантов на продукт, 20 фото на продукт, 1000 фото / 128 MiB загрузок на задание, до 8 MiB на фото, до 16000 px на сторону и 40 MP. Неудачные загрузки тоже расходуют бюджет. Не больше трёх queued/running заданий на проект и двух одновременно принимаемых upload на процесс. ZIP: до 512 элементов / 64 MiB распаковки / 16 MiB на элемент, ограничение коэффициента сжатия. XML без DTD/внешних сущностей, с пределами размера/узлов/глубины. Файл/URL сохраняются до нормализации, затем освобождаются; история и нормализованный план остаются. Удерживаемые исходные файлы ограничены 512 MiB на tenant.

URL — только HTTP:80 / HTTPS:443, без userinfo/fragment, с повторной проверкой каждого redirect (до трёх). Проверяются все DNS-адреса, socket закрепляется за проверенным публичным адресом; loopback, приватные, link-local, служебные/зарезервированные IPv4/IPv6 запрещены. Прокси окружения и автоматический redirect отключены. Лимит применяется к потоку, не только к Content-Length.

Частые отказы: 400 validation_error/url_blocked, 401 unauthorized, 403 forbidden, 404 not_found (включая чужой job), 409 idempotency_conflict/import_not_ready/import_storage_quota, 413 request_too_large, 429 import_queue_full/import_upload_busy, 503 import_disabled. Ошибки разбора сохраняются в Job: invalid_file/unsupported_template/invalid_price/duplicate_sku/zip_unsafe и другие. Ошибки картинок: image_type/image_size/image_timeout/download_size/url_blocked/object_store_failed. Клиент не должен считать список кодов закрытым.

Хранение и сопровождение

Миграция 040 добавляет только commerce_import_jobs/rows/images/refs. Очередь, leases, планы и отчёты — PostgreSQL; Redis/RabbitMQ для импорта не требуются. Worker использует SKIP LOCKED, lease 90 секунд и heartbeat 20 секунд; token fencing защищает от старого worker. Применение одного продукта, variants, refs, outbox и checkpoint — одна SQL-транзакция. Задание целиком допускает частичный успех.

{"CommerceImports":{"Enabled":true,"RunWorker":true,"PollSeconds":2,"Storage":{"Provider":"FileSystem","RootPath":"/var/lib/businessplatform/import-objects","PublicBaseUrl":"https://app.irisaicrm.shop/media/imports"}}}

FileSystem требует постоянного volume вне контейнера и резервирования каталога; свободный диск должен оставаться не меньше 512 MiB. Route /media/imports/{businessId}/{sha256}.{ext} публичный, immutable и nosniff; угадываемых пользовательских имён нет. Nginx пропускает только этот префикс без Basic Auth. Вариант S3 поддерживает HTTPS endpoint/bucket/region/accessKey/secretKey + PublicBaseUrl, SigV4; секреты только в защищённой конфигурации. Наличие реализации S3 не означает, что внешний бакет настроен.

Nginx для /api/v1/commerce/: 101m (100 MiB + multipart), timeout 75 секунд; JWT по-прежнему обязателен. У upload deadline 60 секунд, у остальных Commerce команд 15 секунд, GET 5 секунд. При выключенном CommerceImports API возвращает 503. Перед включением применить только 040 через DbInspector, записав schema_migrations в той же транзакции; не использовать migrate-all/seed на рабочей БД. Откат приложения сохраняет staging/history/objects, импорт отключается конфигурацией. Автоматического удаления истории/объектов пока нет: согласовать retention и мониторить объём, очередь, старые leases и failed jobs.

Выпуск и подтверждённая готовность

17 октября 2026 развёрнут 20261017-095952, код 4f4faa6fe76944928f29def2ed0576dd61bc7914, через GitHub Actions/Packages run 39011920133. Образ ядра закреплён на sha256:b57d2e279b75c20f962fc6d1b3ea9487faeeccf84c44d6ee3ff9666f6d45e111. Миграция 040 и её запись в schema_migrations применены атомарно; существующие количества строк не изменились. Импорт и worker включены, storage — FileSystem с постоянным volume /opt/bp/import-objects, URL https://app.irisaicrm.shop/media/imports. Внешний S3-бакет этим выпуском не настраивался. Лендинг/витрины/соседние сервисы не обновлялись.

CI run 39011920144: 113 import + 227 Commerce + 59 Velvio + 35 regression checks, все четыре задачи зелёные; 42 страницы документации и ссылки проверены. Живой smoke у issuer выполнил 37 HTTP-запросов через production API: узкие scopes, точный Job DTO, dry-run без каталога, partial apply, собственное публичное фото, отказ небезопасного фото, идемпотентность, upsert, create_only и история. Тестовый бизнес неопубликован, товар архивирован; заказов/платёжных действий не было. Квитанции: docs/development/catalog-import-deployment.json, catalog-import-production-smoke.json, catalog-import-checks.json. Полная пользовательская приёмка интерфейса Velvio остаётся отдельным этапом.

Проверки и источники

tools/CommerceImportChecks работает только с пустой тестовой БД bp_commerce_test_import* на loopback/postgres-test. Проверяет HTTP/JWT/точные DTO, парсеры XLSX и настоящий XLS, опасные файлы/URL, dry-run без изменений, частичный успех, повторный upsert, owned images, рестарт, потерю lease и SQL rollback при отказе outbox. .github/workflows/checks.yml содержит отдельную PostgreSQL 17 задачу. Подтверждение конкретного выпуска и живого smoke хранится отдельно в docs/development, результаты прежнего Commerce выпуска не считаются проверкой нового импорта.

Первичные источники, проверенные при подготовке 17 октября 2026:

Остаётся отдельно: проверка полного набора актуальных категорийных шаблонов маркетплейсов, очистка истории/сиротских объектов по согласованной политике, несколько складов, пользовательская приёмка через интерфейс Velvio.