Skip to content

ТЗ — 1С-обмен (CommerceML) (cms/integration-1c)

Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке

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

Двусторонний обмен данными с 1С по стандартному протоколу CommerceML 2 (авторизация, init, file, import): каталог/остатки/цены из 1С на сайт, заказы с сайта в 1С, статусы заказов обратно. Конфликт-политика: 1С — мастер справочников (товары, цены, остатки), сайт — мастер заказов.

  • Приём выгрузки CommerceML 2 от 1С (catalog.xml, offers.xml) — этапы init/file/import
  • Импорт каталога/остатков/цен батчево через cms/import, маппинг на cms/commerce-stock/cms/commerce-pricing (складов и типов цен на модели ядра)
  • Выгрузка заказов сайта в формате CommerceML для забора 1С
  • Обратный приём статусов заказов из 1С (изменение статуса, номер отгрузки)
  • Расписание автообмена (по cron) и ручной запуск обмена из Filament
  • Журнал сессий обмена с диагностикой ошибок парсинга/маппинга
  • Конфликт-политика: при расхождении справочников — 1С побеждает, заказы — сайт побеждает

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

requires: cms/import, cms/commerce-stock, cms/commerce-pricing · suggests: cms/integrations-bus

Поведение при выключении: каталог/остатки/цены перестают обновляться из 1С (сайт работает с последним импортированным состоянием), заказы накапливаются в очереди выгрузки до включения модуля — заказы не теряются.

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

ТаблицаКлючевые поляПримечание
cms_integration_1c_sessionsid, direction (in|out), status, started_at, finished_at, errorсессия обмена CommerceML
cms_integration_1c_mappingsid, entity_type, external_id, internal_idмаппинг складов/типов цен/справочников на модели ядра
cms_integration_1c_conflictsid, session_id, entity_type, external_value (json), internal_value (json), resolutionжурнал конфликтов и их разрешения

Индексы: FK session_idconstrained() + index(); direction/status/resolution — PHP Enum; external_value/internal_valuejson() + cast array; cms_integration_1c_sessions/cms_integration_1c_conflicts — журналы, append-only, BRIN по created_at; уникальный индекс на entity_type+external_id в mappings (тот же индекс защищает upsert при повторном импорте от дублей).

ПДн-паспорт. Собственные таблицы модуля ПДн не хранят (маппинги и сессии оперируют техническими id, не персональными данными). Однако при выгрузке заказов в 1С (канал «сайт → 1С») в CommerceML-документ передаются ФИО, адрес доставки, телефон покупателя — это законная цель обработки (исполнение заказа), поля берутся из cms/commerce-orders на лету и не дублируются в таблицах модуля. В событиях (Integration1cOrderExported и др.) и в журнале сессий ПДн не логируются — только order_id/session_id. «Забыть по запросу» для этих данных обрабатывает владелец — cms/commerce-orders; модуль лишь передал их синхронно в 1С в момент выгрузки, повторной выгрузки после удаления не происходит.

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

Принцип whitelist: всё, что не перечислено ниже как вход, модуль обязан отвергать — неизвестный метод в точке обмена, посторонний IP (если включён whitelist), файл не по схеме CommerceML — 4xx/понятная ошибка, не тихий проброс дальше.

Входы

ИсточникКаналЧто приходитПроверка
Выгрузка CommerceML от 1СHTTP /api/v1/integration-1c/exchange, шаги init/file/importcatalog.xml, offers.xml — товары, цены, остатки, справочникиBasic Auth (не подпись — см. «Безопасность»), whitelist шагов протокола, Content-Type, размер файла ≤ max_file_size_mb
Ручной запуск обменаFilament / cms:integration-1c:runкоманда «начать сессию»integration-1c.manage, кулдаун manual_run_cooldown_minutes
Расписаниеcron (schedule_cron)автозапуск сессиивнутренний, входа извне нет
Факт заказа/оплатысобытие OrderPaid (канал 1) от cms/commerce-ordersсигнал «есть что выгружать в 1С»внутренний, не внешний вход

Точка обмена CommerceML — не «вебхук» в привычном смысле ядра (HMAC-подпись, cms/webhooks-in): 1С сама инициирует сессию по расписанию своего регламентного задания и аутентифицируется Basic Auth на каждом шаге init/file/import. Вместо проверки подписи — проверка учётных данных на каждом запросе и whitelist допустимых шагов протокола; всё вне этого (произвольные HTTP-методы, неизвестные query-параметры) отклоняется 401/422 без обработки тела.

Выходы

ПолучательКаналЧто отдаётсяФормат
1С (та же точка обмена, шаг file на скачивание)HTTP-ответorders.xml — заказы, ожидающие выгрузкиCommerceML 2 XML
Подписчики ядра/модулейсобытие (канал 1)Integration1cImportCompleted, Integration1cOrderExported, Integration1cConflictDetected, Integration1cExchangeFailedсм. «События и обмен»
АдминFilament / /api/v1/admin/integration-1c/*журнал сессий, журнал конфликтовJSON, keyset-пагинация
Разработчик/healthcms:integration-1c:doctor --jsonдоступность точки обмена, валидность маппинговJSON

Настройки (группа integration-1c)

КлючТипДефолтaffectsPageCacheОписание
integration-1c.enabledboolfalseнетВключение модуля целиком
integration-1c.sync_pausedboolfalseнетKill-switch: приостановить обмен без выключения модуля (точка обмена отвечает «обмен временно приостановлен», расписание не запускается)
integration-1c.schedule_cronstring"0 */2 * * *"нетРасписание автообмена
integration-1c.import_batch_sizeint500нетРазмер батча импорта товаров/остатков
integration-1c.max_file_size_mbint100нетМаксимальный размер файла обмена; превышение — отказ на шаге file с понятной ошибкой
integration-1c.manual_run_cooldown_minutesint5нетМинимальный интервал между ручными запусками (защита от повторных кликов и гонки с расписанием)
integration-1c.master_catalog_sourcestring"1c"нетКто мастер справочников (фиксировано 1c; см. ⚠️ противоречие в «Крайние случаи»)
integration-1c.auth_loginstring""нетЛогин Basic Auth для обмена (пароль — только .env)
integration-1c.allowed_ipsstring[][]нетIP/CIDR-whitelist точки обмена; пусто = без ограничения по IP

API

МетодПутьДоступНазначение
GET/POST/api/v1/integration-1c/exchangeBasic Auth (CommerceML)Точка обмена CommerceML 2 (init/file/import)
GET/api/v1/admin/integration-1c/sessionsadmin (integration-1c.view)Журнал сессий обмена
POST/api/v1/admin/integration-1c/runadmin (integration-1c.manage)Ручной запуск обмена
GET/api/v1/admin/integration-1c/conflictsadmin (integration-1c.view)Журнал конфликтов маппинга

Журналы сессий/конфликтов — keyset-пагинация, не OFFSET.

Компоненты

Filament: журнал сессий обмена, таблица маппингов складов/типов цен, журнал конфликтов, переключатель sync_paused рядом с enabled (два разных действия — см. «Настройки»). Команды: cms:integration-1c:run --json, cms:integration-1c:doctor --json (проверка доступности точки обмена и валидности маппингов).

Демо-контент и фронтенд-бюджет — не применимо. Модуль не регистрирует блоки/виджеты и ничего не рендерит на публичном сайте (чисто админ-инструмент обмена): сидеров demo-данных для галереи блоков не требуется, JS/CSS-бюджет и a11y-требования к формам ограничиваются стандартной админкой Filament, отдельного фронтенд-кода у модуля нет.

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

СобытиеКогдаPayload
Integration1cImportCompletedимпорт каталога/остатков/цен завершёнsession_id, entities_count
Integration1cOrderExportedзаказ выгружен в CommerceML для 1Сorder_id, session_id
Integration1cConflictDetectedобнаружено расхождение справочниковsession_id, entity_type, resolution
Integration1cExchangeFailedсессия обмена завершилась ошибкойsession_id, direction, error

Обмен с 1С — по протоколу CommerceML напрямую (точка /api/v1/integration-1c/exchange); маппинги и импорт батчей идут через cms/import и cms/integrations-bus.

Взаимодействия

Сущность/модульКаналНаправлениеЧто происходит
cms/import5 — очередьintegration-1c → cms/importбатч товаров/остатков/цен передаётся генерик-импортёру пакетами import_batch_size
cms/commerce-stock4 — requires, прямой сервис-вызовintegration-1c → commerce-stockзапись остатков по складам через публичный сервис владельца
cms/commerce-pricing4 — requires, прямой сервис-вызовintegration-1c → commerce-pricingзапись цен по типам цен через публичный сервис владельца
cms/commerce-orders1 — событие (слушает OrderPaid)commerce-orders → integration-1cсигнал «заказ готов к выгрузке», сам заказ читается через сервис владельца, не raw SQL
cms/integrations-bus5 — очередь / внешнийintegration-1c → integrations-busопциональное логирование/маршрутизация внешнего обмена при suggests включённом

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

Приём файла по протоколу CommerceML — тонкий и синхронный (1С ждёт ответ success в рамках HTTP-запроса): контроллер только аутентифицирует, валидирует whitelist шага и сохраняет файл, тяжёлая обработка (parse/import/маппинг) уходит в очередь. Импорт/выгрузка — именованная очередь integration-1c, батчами (import_batch_size) через Bus::batch с прогрессом, видимым в Filament. Расписание — schedule_cron через ScheduleRegistrar ядра; ручной запуск — из Filament, с учётом manual_run_cooldown_minutes. Ошибка парсинга одного файла не прерывает сессию — изоляция ошибок внутри батча.

Мини-ранбук (типовые инциденты):

СимптомЧто проверитьКоманда
Обмен «завис» (сессия в status=running дольше обычного)Отставание очереди integration-1c, живость воркераcms:integration-1c:doctor --json, php artisan queue:failed
Товары/остатки разошлись с 1СЖурнал конфликтов, последняя успешная сессияGET /api/v1/admin/integration-1c/conflicts, cms:integration-1c:run --dry-run
1С жалуется, что сайт не отвечаетДоступность точки обмена, allowed_ips, статус sync_pausedcms:integration-1c:doctor --json, GET /api/v1/system/health

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

  • Ожидаемые объёмы: каталог 1С — оценочно десятки–сотни тысяч SKU, остатки — тот же порядок × число складов; журнал сессий и конфликтов растёт монотонно (append-only, ретеншн — см. «Тесты и приёмка» и §15 стандарта).
  • Горячие пути: точка обмена не участвует в page-cache (не публичная страница); бюджет применяется к самому импорту, а не к HTTP-ответу.
  • Бюджет батчевого импорта: без N+1 — маппинг external_id → internal_id через индексированный upsert (уникальный индекс entity_type+external_id), не построчный SELECT; Bus::batch чанками по import_batch_size (дефолт 500) с параллельной утилизацией воркеров очереди integration-1c.
  • Индексы сошлись с «Моделью данных»: уникальный на mappings, BRIN на журнальных таблицах по created_at.
  • Явное правило: весь обмен с 1С — только из очереди, никогда не выполняется синхронно в ответ на запрос пользователя/админа; HTTP-слой точки обмена ограничен приёмом/выдачей файла и постановкой job.

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

  • Точка обмена /api/v1/integration-1c/exchange защищена Basic Auth, пароль — только .env, логин — в настройках (несекретный).
  • Усиление Basic Auth: опциональный IP/CIDR-whitelist (allowed_ips) — многие инсталляции 1С обмениваются с фиксированного IP офиса/сервера; ротация пароля — регламентная процедура (смена в .env + перезапуск), не автоматизируется модулем, но описана в docs/module.md.
  • Защита от повторной атаки тем же файлом (аналог replay для не-подписанного канала): идемпотентность по хэшу содержимого файла + session_id CommerceML — повторная присылка (в том числе намеренная) того же файла не создаёт вторую сессию с нуля и не дублирует импортированные записи (см. «Крайние случаи»).
  • Батчевый импорт остатков/цен идемпотентен — повторный импорт того же файла не дублирует записи.
  • ПДн заказов передаются в 1С только в составе выгрузки заказов (см. «ПДн-паспорт» в «Модель данных»), в логах и событиях не оседают.
  • Права: integration-1c.view, integration-1c.manage.

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

Рольintegration-1c.view (журнал/конфликты)integration-1c.manage (ручной запуск, sync_paused)Настройки/логин/пароль
studio✅ (включая .env)
админ клиента✅ кроме .env-секретов
менеджер
редактор

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

  • Админ видит статус синхронизации: время и итог последнего успешного обмена (entities_count), прогресс текущего батча («импортировано 3200 из 5000»).
  • Журнал обменов со скачиваемым построчным отчётом ошибок парсинга/маппинга — не только агрегат «N ошибок», а конкретные строки с причиной.
  • Ошибки на человеческом языке: «1С не ответила за 30 секунд» вместо «cURL error 28», «файл превышает 100 МБ» вместо кода протокола — маппинг технических исключений в lang/ при рендере в Filament.
  • Подтверждение необратимых операций: принудительный запуск при незавершённой предыдущей сессии («Предыдущий обмен ещё выполняется — прервать и начать новый?») — по умолчанию отклоняется, требует явного подтверждения повышенной роли (см. «Крайние случаи», п.1).

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

  • Конфликт двух одновременных обменов (расписание сработало, пока идёт ручной запуск, либо 1С открыла новую сессию init до завершения предыдущей) → распределённый лок на сессию обмена; второй запуск отклоняется с понятным сообщением («обмен уже выполняется, начат в HH:MM»), параллельный запуск невозможен.
  • Частичный обмен упал на середине (например, на 50% товаров) → идемпотентность по external_id: при повторном запуске выполняется докат — доимпорт оставшегося, а не откат всего файла и не повторный импорт уже обработанных строк. Импорт батчами принципиально не атомарен целиком (один файл = много Bus::batch-чанков); единица идемпотентности — строка каталога, не файл.
  • ⚠️ Противоречие: кто мастер при нескольких одновременных интеграциях.integration-1c.master_catalog_source фиксирован как 1c, но если на сайте включены одновременно cms/integration-1c и cms/erp (а в перспективе — integration-marketplaces), у каждого модуля своё локальное представление о мастерстве, единого арбитра нет. Разрешение: явная карта владения owner_module per сущность/поле (товар, цена, остаток) должна жить на уровне владельца данных — cms/commerce-catalog/cms/commerce-pricing/ cms/commerce-stock — а не быть настройкой внутри одного из модулей-источников; до появления этого механизма в ядре — эксплуатационное правило: одновременное включение cms/integration-1c и cms/erp с пересекающимся набором синхронизируемых сущностей не поддерживается без ручного разведения по сущностям (см. симметричное описание в ТЗ cms/erp).
  • Кодировка windows-1251 во входящих файлах CommerceML → корректная перекодировка на входе перед парсингом (типичная кодировка выгрузок 1С); невалидная кодировка или битый XML → внятная ошибка парсинга в журнале сессии, не 500 и не молчаливый пропуск файла.
  • Расхождение маппинга (external_id ссылается на несуществующую внутреннюю сущность — склад/тип цены удалён вручную) → строка пропускается с записью в cms_integration_1c_conflicts/отчёт расхождений, сессия продолжается, не прерывается.
  • CommerceML-совместимость: 1С прислала неожиданную структуру XML (смена версии протокола или кастомная выгрузка конфигурации 1С) → парсинг с graceful degradation — распознанные секции импортируются, нераспознанные логируются, алерт разработчику (cms/health), сессия не падает целиком.
  • Точка обмена недоступна для 1С (сайт лёг, таймаут, деплой) → 1С ретраит по своей внутренней логике регламентного задания; модуль должен быть идемпотентен к повторной присылке того же файла (см. «Безопасность» — хэш файла + session_id), повторный приём не создаёт дублей.
  • Пустая выгрузка от 1С (0 товаров в catalog.xml) → сессия завершается успешно с entities_count: 0, не считается ошибкой автоматически, но если предыдущие сессии были ненулевыми — аномалия, видимая в журнале (потенциальный сигнал для алерта).
  • Файл обмена превышает max_file_size_mb → отклоняется на шаге file с понятной ошибкой протокола, не падает на попытке загрузить файл целиком в память.

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

Донор: — (новая разработка).

Начальная загрузка каталога (первый обмен на новом сайте) — не отдельный legacy-импортёр, а частный случай обычного обмена: тот же cms:integration-1c:run, только без предыдущей сессии для сравнения. Отдельный legacy-импортёр по §16 стандарта модулю не требуется — донора с боевыми данными для миграции нет; миграция с другой учётной системы (не 1С) — задача модуля cms/erp с соответствующим адаптером, не этого модуля.

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

  • [ ] Контрактный тест: полный цикл CommerceML init → file → import на моке выгрузки 1С
  • [ ] Батчевый импорт остатков/цен идемпотентен — повторный импорт того же файла не дублирует записи
  • [ ] Конфликт-политика применяется корректно: 1С побеждает в справочниках, сайт — в заказах (тест)
  • [ ] Обмен выполняется по расписанию и вручную, оба пути логируются в cms_integration_1c_sessions
  • [ ] При выключении модуля заказы накапливаются в очереди выгрузки без потери
  • [ ] Ошибка парсинга одного файла не прерывает всю сессию обмена (изоляция ошибок)
  • [ ] Права integration-1c.manage разграничивают просмотр журнала и ручной запуск обмена
  • [ ] Мок 1С-стороны: контрактный тест на идемпотентность повторной присылки одного файла (хэш+session_id)
  • [ ] Контрактный тест деградации: точка обмена недоступна → сайт работает на последнем импортированном состоянии, без 5xx на публичных страницах
  • [ ] Тест конфликта двух одновременных обменов — второй запуск отклонён с понятным сообщением, лок снимается по завершении/по таймауту
  • [ ] Тест перекодировки windows-1251 и обработки битого/несовместимого XML (не 500, запись в журнал)
  • [ ] Тест лимита max_file_size_mb — файл сверх лимита отклоняется на шаге file
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут; тестовая БД только integration_1c_test, migrate:fresh запрещён

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