Тема
ТЗ — 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_sessions | id, direction (in|out), status, started_at, finished_at, error | сессия обмена CommerceML |
cms_integration_1c_mappings | id, entity_type, external_id, internal_id | маппинг складов/типов цен/справочников на модели ядра |
cms_integration_1c_conflicts | id, session_id, entity_type, external_value (json), internal_value (json), resolution | журнал конфликтов и их разрешения |
Индексы: FK session_id — constrained() + index(); direction/status/resolution — PHP Enum; external_value/internal_value — json() + 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/import | catalog.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-пагинация |
| Разработчик/health | cms:integration-1c:doctor --json | доступность точки обмена, валидность маппингов | JSON |
Настройки (группа integration-1c)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
integration-1c.enabled | bool | false | нет | Включение модуля целиком |
integration-1c.sync_paused | bool | false | нет | Kill-switch: приостановить обмен без выключения модуля (точка обмена отвечает «обмен временно приостановлен», расписание не запускается) |
integration-1c.schedule_cron | string | "0 */2 * * *" | нет | Расписание автообмена |
integration-1c.import_batch_size | int | 500 | нет | Размер батча импорта товаров/остатков |
integration-1c.max_file_size_mb | int | 100 | нет | Максимальный размер файла обмена; превышение — отказ на шаге file с понятной ошибкой |
integration-1c.manual_run_cooldown_minutes | int | 5 | нет | Минимальный интервал между ручными запусками (защита от повторных кликов и гонки с расписанием) |
integration-1c.master_catalog_source | string | "1c" | нет | Кто мастер справочников (фиксировано 1c; см. ⚠️ противоречие в «Крайние случаи») |
integration-1c.auth_login | string | "" | нет | Логин Basic Auth для обмена (пароль — только .env) |
integration-1c.allowed_ips | string[] | [] | нет | IP/CIDR-whitelist точки обмена; пусто = без ограничения по IP |
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET/POST | /api/v1/integration-1c/exchange | Basic Auth (CommerceML) | Точка обмена CommerceML 2 (init/file/import) |
| GET | /api/v1/admin/integration-1c/sessions | admin (integration-1c.view) | Журнал сессий обмена |
| POST | /api/v1/admin/integration-1c/run | admin (integration-1c.manage) | Ручной запуск обмена |
| GET | /api/v1/admin/integration-1c/conflicts | admin (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/import | 5 — очередь | integration-1c → cms/import | батч товаров/остатков/цен передаётся генерик-импортёру пакетами import_batch_size |
cms/commerce-stock | 4 — requires, прямой сервис-вызов | integration-1c → commerce-stock | запись остатков по складам через публичный сервис владельца |
cms/commerce-pricing | 4 — requires, прямой сервис-вызов | integration-1c → commerce-pricing | запись цен по типам цен через публичный сервис владельца |
cms/commerce-orders | 1 — событие (слушает OrderPaid) | commerce-orders → integration-1c | сигнал «заказ готов к выгрузке», сам заказ читается через сервис владельца, не raw SQL |
cms/integrations-bus | 5 — очередь / внешний | 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_paused | cms: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_idCommerceML — повторная присылка (в том числе намеренная) того же файла не создаёт вторую сессию с нуля и не дублирует импортированные записи (см. «Крайние случаи»). - Батчевый импорт остатков/цен идемпотентен — повторный импорт того же файла не дублирует записи.
- ПДн заказов передаются в 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_moduleper сущность/поле (товар, цена, остаток) должна жить на уровне владельца данных —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запрещён