Skip to content

ТЗ — ЭДО (Диадок/СБИС) (cms/edo)

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

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

Обмен юридически значимыми документами для B2B-контрагентов через операторов ЭДО (Диадок, СБИС): отправка УПД/актов из cms/commerce-invoices, отслеживание статусов подписания, приём входящих документов от контрагентов.

  • Коннекторы Диадок, СБИС: отправка документов оператору поверх cms/integrations-bus
  • Отправка УПД/актов, сформированных в cms/commerce-invoices, контрагенту через ЭДО
  • Отслеживание статусов подписания (draft → sent → delivered → signed | rejected)
  • Приём входящих документов от контрагентов с уведомлением ответственного
  • Журнал документооборота с историей статусов по каждому документу
  • Роуминг между операторами ЭДО (отправитель на одном операторе, контрагент — на другом)
  • Контроль срока действия сертификата УКЭП с заблаговременными алертами

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

requires: cms/integrations-bus, cms/commerce-b2b, cms/commerce-invoices

Поведение при выключении: документы формируются и хранятся в системе (в cms/commerce-invoices), но не отправляются через ЭДО — обмен с контрагентом переходит на ручной (email/бумага) до включения модуля; документы в статусе, отличном от draft, остаются видимыми в Filament только для чтения (история сохраняется).

Обязательная зависимость cms/integrations-bus — при её выключении/сбое (правило жизненного цикла §3 стандарта) весь модуль деградирует целиком: отправка и опрос статусов недоступны с понятным сообщением «шина интеграций недоступна», документы не теряются, накопленная очередь отправки продолжается после восстановления.

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

ТаблицаКлючевые поляПримечание
cms_edo_documentsid, invoice_id, provider_code, external_id, type, status, rejection_reason (nullable), corrected_document_id (nullable), lock_versionдокумент, отправленный через ЭДО
cms_edo_status_logid, document_id, status, reason (nullable), changed_atистория статусов подписания
cms_edo_certificatesid, provider_code, subject_inn, valid_from, valid_to, status, last_alert_sent_at (nullable)сертификаты УКЭП по операторам, отслеживание истечения

Индексы: FK invoice_id/document_id/corrected_document_idconstrained()->index(); type/status (documents) и status (certificates) — PHP Enum; идемпотентность отправки — уникальный индекс (invoice_id, type) на cms_edo_documents (повторная генерация УПД/акта по тому же счёту не создаёт второй документ в ЭДО); идемпотентность приёма — уникальный индекс (provider_code, external_id) (повторная доставка вебхука от оператора не создаёт дубль входящего документа); cms_edo_status_log — журнал, append-only, BRIN по changed_at; cms_edo_documents.lock_version — optimistic lock на конкурентную правку статуса из двух источников (ручное действие в Filament + вебхук оператора одновременно, §4 стандарта).

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

Входы:

ИсточникДанные/поляЧем валидируется
POST /admin/edo/documents/{invoice}/sendinvoice (path)FormRequest + Policy edo.manage; счёт/акт обязан существовать в cms/commerce-invoices; действующий сертификат УКЭП обязателен — попытка с истёкшим сертификатом отклоняется до постановки в очередь (см. «Крайние случаи»); Idempotency-Key на мутации
Событие InvoiceDocumentGenerated (cms/commerce-invoices, канал 1)order_id, document_id, type, numberтриггер автоотправки при edo.auto_send_on_invoice=true; слушатель тонкий — кладёт job, не отправляет синхронно
Вебхук оператора ЭДО (через cms/integrations-bus)external_id, status, reason (при rejected), signed_at (при signed)коннектор оператора классифицирует и нормализует payload перед передачей в шину; (provider_code, external_id) — идемпотентность приёма
POST /admin/edo/documents/{document}/reject-and-resenddocument (path), корректирующий payload из cms/commerce-invoicesFormRequest + Policy edo.manage; исходный документ обязан быть в статусе rejected
GET /admin/edo/documents, /admin/edo/incoming, /admin/edo/certificatesfilter[status], filter[provider_code], sort, cursorFormRequest whitelist (конвенция API ядра)

Выходы:

ПотребительДанныеФормат
cms/integrations-bus (requires)исходящий вызов коннектора оператораpayload по ConnectorContract
Filament (документы, входящие, сертификаты)таблицы со статус-бейджамисписок
cms:edo:poll-status --json, cms:edo:doctor --json, cms:edo:check-certificates --jsonрезультат опроса/диагностикиJSON
события EdoDocumentSent/EdoDocumentSigned/EdoDocumentRejected/EdoDocumentReceived/EdoCertificateExpiringSoonсм. «События и обмен»payload события (канал 1)
cms/notifications-bus (мягкая интеграция)уведомление ответственного менеджера (отклонение/входящий документ)письмо/сообщение по шаблону модуля

Whitelist-принцип: вызов отправки без действующего сертификата, вебхук с незарегистрированным provider_code или payload вне схемы коннектора — отклоняется (§11 стандарта).

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

КлючТипДефолтaffectsPageCacheОписание
edo.enabled_providerstringnullнетКод активного оператора ЭДО (пусто = модуль фактически выключен — kill-switch)
edo.auto_send_on_invoiceboolfalseнетАвтоотправка документа при создании счёта/акта
edo.status_poll_minutesint30нетПериодичность опроса статусов подписания
edo.cert_expiry_alert_daysarray[30, 14, 7]нетЗа сколько дней до истечения сертификата эскалировать алерт
edo.roaming_lookup_enabledbooltrueнетИскать контрагента по ИНН+КПП через роуминг оператора при отправке
edo.reject_notify_managerbooltrueнетУведомлять ответственного менеджера при отклонении документа контрагентом
edo.max_send_retry_attemptsint3нетЧисло ретраев отправки при недоступности оператора (передаётся в cms/integrations-bus)

Секреты доступа к API оператора ЭДО (сертификат, токен) — только .env.

API

МетодПутьДоступНазначение
POST/api/v1/admin/edo/documents/{invoice}/sendadmin (edo.manage)Отправка документа контрагенту через ЭДО
POST/api/v1/admin/edo/documents/{document}/reject-and-resendadmin (edo.manage)Создание исправленной версии отклонённого документа
GET/api/v1/admin/edo/documentsadmin (edo.view)Список документов со статусами подписания
GET/api/v1/admin/edo/incomingadmin (edo.view)Входящие документы от контрагентов
GET/api/v1/admin/edo/certificatesadmin (edo.view)Статус сертификатов УКЭП по операторам

Списки — keyset-пагинация, не OFFSET.

Компоненты

Filament: ресурс документов ЭДО со статус-бейджами (draft/sent/delivered/ signed/rejected), список входящих документов, ресурс сертификатов с индикатором истечения. Команды: cms:edo:poll-status --json, cms:edo:doctor --json, cms:edo:check-certificates --json (алерты по edo.cert_expiry_alert_days).

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

СобытиеКогдаPayload
EdoDocumentSentдокумент отправлен оператору ЭДОdocument_id, invoice_id, provider_code
EdoDocumentSignedконтрагент подписал документdocument_id, signed_at
EdoDocumentRejectedконтрагент отклонил документdocument_id, reason
EdoDocumentReceivedполучен входящий документ от контрагентаdocument_id, provider_code, external_id
EdoCertificateExpiringSoonсертификат УКЭП истекает в пределах cert_expiry_alert_daysprovider_code, valid_to, days_left

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

Сущность/модульКаналНаправлениеЧто происходит
cms/commerce-invoicesсобытие InvoiceDocumentGenerated (канал 1)inтриггер автоотправки документа при auto_send_on_invoice=true
cms/commerce-b2bсервис-вызов (requires)inреквизиты контрагента (ИНН/КПП) для поиска в роуминге и адресации отправки
cms/integrations-busrequires (канал 4)outвсе внешние HTTP-вызовы к оператору ЭДО, ретраи и circuit breaker — на уровне шины
cms/notifications-bus (мягкая интеграция)сервис-вызов + очередьoutуведомление ответственного менеджера при отклонении/входящем документе; недоступность шины уведомлений не блокирует фиксацию факта в журнале статусов
cms/healthсобытия EdoCertificateExpiringSoon/сбои опросаoutалерты об истекающем сертификате и отставании очереди опроса
cms/audit (если включён)события модуляoutфиксация отправок/отклонений/приёма в аудит-лог

Весь обмен с оператором ЭДО — через cms/integrations-bus, FilterBus не используется.

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

  • Очередь edo: постановка отправки документа (при auto_send_on_invoice или ручном запуске из Filament) — асинхронно, вызов уходит через cms/integrations-bus с его ретраями/backoff; недоступность оператора не теряет документ — статус остаётся draft/send_failed, отправка повторяется по общей политике ретраев шины;
  • опрос статусов подписания — по расписанию (edo.status_poll_minutes), именованная очередь edo, идемпотентен (повторный опрос не создаёт дублирующую запись в истории);
  • cms:edo:check-certificates (ежедневно) — проверка сроков действия сертификатов, эскалация алертов по edo.cert_expiry_alert_days (30/14/7 дней), не более одного алерта на порог (last_alert_sent_at предотвращает повтор);
  • входящий документ от контрагента не теряется при недоступности уведомления — уведомление ответственного менеджера ретраится отдельно от факта приёма документа (факт приёма фиксируется первым, уведомление — потребитель события EdoDocumentReceived).

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

  • Ожидаемый объём: средний B2B-сайт — десятки–первые сотни документов в сутки; крупный — первые тысячи (регулярная пакетная отправка УПД по отгрузкам);
  • горячие пути и бюджет запросов: отправка документа и опрос статусов — не горячий путь страницы, а редкое действие пользователя/системы (менеджер отправляет документ вручную, либо разовое авторасписание опроса), не рендер публичной страницы; постановка отправки в очередь — ≤2 запроса (проверка сертификата + вставка/обновление cms_edo_documents с уникальным индексом (invoice_id, type) вместо повторной генерации); опрос статусов батчами (не по одному документу за вызов, где API оператора это позволяет), без N+1 по количеству документов;
  • критичные индексы: (invoice_id, type) — идемпотентность отправки; (provider_code, external_id) — идемпотентность приёма и дедуп вебхуков; составной (status, provider_code) на cms_edo_documents — под выборку «что опросить» планировщиком; BRIN по changed_at на cms_edo_status_log; уникальный (provider_code, subject_inn) на cms_edo_certificates;
  • что кешируется: реестр активных сертификатов (provider_code → valid_to/status) — тег edo:certificates, инвалидация по cms:edo:check-certificates и правке в Filament; что не кешируется: статусы документов — читаются напрямую из БД (юридическая значимость требует консистентности, не кеша, аналогично cms/integrations-bus).

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

  • Опрос статусов идемпотентен — повторный опрос не создаёт дублирующую запись в истории.
  • Приём вебхука идемпотентен по (provider_code, external_id) — повторная доставка не создаёт дубль входящего документа.
  • Отправка с истёкшим сертификатом УКЭП блокируется на уровне сервиса до постановки в очередь — понятная ошибка «сертификат истёк, отправка невозможна», не 500 и не отправка неподписанным документом.
  • Секреты доступа к API оператора ЭДО (сертификат, токен) — только .env, через cms/integrations-bus (credentials_ref), не в БД cms_edo_*.
  • ПДн-паспорт (матрица v2.2): модуль оперирует реквизитами юрлиц (ИНН/КПП из cms/commerce-b2b), собственных таблиц с ПДн физлиц не ведёт — «ПДн не храню» в части структурированных полей. Исключение: cms_edo_status_log.reason (причина отклонения от оператора/контрагента) — свободный текст, теоретически может содержать ФИО контактного лица контрагента, если оператор их туда подставляет; поле не индексируется полнотекстово и не попадает в аналитику/логи вне журнала статусов. Персональные данные подписанта документа (ФИО, должность) находятся внутри самого файла документа — файл принадлежит cms/commerce-invoices, не модулю edo.
  • Матрица ролей: edo.view — просмотр документов/входящих/сертификатов (менеджер, админ, studio); edo.manage — отправка, создание исправленной версии, сброс/принятие настроек, ручной запуск опроса (админ, studio); менеджер без .manage видит статусы, но не инициирует отправку — опасное действие (внешняя юридически значимая отправка).
  • Права: edo.view, edo.manage.

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

Админ:

  • пустой список документов — «документов ещё не отправлялось»; пустой список входящих — «входящих документов пока не было»;
  • статус-бейджи на списке документов (draft/sent/delivered/signed/rejected) цветокодированы, отклонённые — с видимой причиной без перехода в карточку;
  • массовое действие: повторная постановка на отправку нескольких send_failed документов (чекбоксы + кнопка, под edo.manage);
  • дашборд сертификатов — истекающий в пределах cert_expiry_alert_days сертификат выделен предупреждением на общем экране модуля, не только в списке сертификатов;
  • ошибки на человеческом языке: «сертификат УКЭП истёк 3 дня назад, отправка заблокирована — обновите сертификат» вместо стектрейса; «контрагент не найден в роуминге оператора — уточните ИНН/КПП или настройте роуминг» вместо тихой потери документа;
  • подтверждение необратимого: создание исправленной версии отклонённого документа — явное действие с указанием, что произойдёт с исходным (остаётся в истории, новый документ — отдельная запись со ссылкой).

Посетитель: модуль не имеет публичного UI — контрагент взаимодействует с документом через собственный кабинет оператора ЭДО (вне контроля сайта); косвенно посетитель (сотрудник компании-контрагента в кабинете B2B, если подключён cms/commerce-b2b) видит статус документа как часть карточки заказа/счёта — отображение статуса, не создание документа.

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

  • документ отклонён контрагентом → статусная машина draft → sent → delivered → signed | rejected; rejected требует причины от оператора (rejection_reason), событие EdoDocumentRejected, уведомление ответственного менеджера (если edo.reject_notify_manager=true), возможность создать исправленный документ — новая запись cms_edo_documents со ссылкой corrected_document_id на отклонённый (история не перезаписывается);
  • роуминг между операторами ЭДО (отправитель на одном операторе, контрагент — на другом) → при edo.roaming_lookup_enabled=true поиск контрагента по ИНН+КПП через роуминг оператора перед отправкой; контрагент не найден или роуминг не настроен → внятная ошибка «контрагент не найден в роуминге, настройте роуминг у оператора или уточните реквизиты», документ остаётся draft, не теряется тихо;
  • сертификат УКЭП истекает → алерт заранее (30/14/7 дней до истечения, EdoCertificateExpiringSoon, эскалация повторными алертами по мере приближения срока); попытка отправки с истёкшим сертификатом блокируется с понятной ошибкой на уровне сервиса — не падает как 500 и не уходит неподписанным документом;
  • дубль документа — повторная генерация УПД/акта по тому же invoice_id не создаёт второй документ в ЭДО: уникальный индекс (invoice_id, type) отклоняет повторную вставку, повторный запрос отдаёт уже существующий документ (идемпотентность отправки);
  • оператор ЭДО недоступен/таймаут при отправке → ретраи с backoff на уровне cms/integrations-bus, документ остаётся в статусе draft/send_failed, не теряется; исчерпание ретраев переводит вызов в dead-letter шины — видно в cms/integrations-bus, документ не считается отправленным;
  • входящий документ дублируется оператором (повторная доставка вебхука) → идемпотентность по (provider_code, external_id) — повторный вебхук не создаёт вторую запись входящего документа, обновляет существующую при изменении статуса;
  • гонка: вебхук оператора и ручное действие в Filament меняют статус одновременноlock_version (optimistic lock, §4 стандарта) отклоняет конфликтующую запись 409 вместо «последний победил» молча; повторный запрос читает актуальный статус;
  • выключение cms/integrations-bus (обязательная зависимость) → модуль деградирует целиком: отправка/опрос недоступны с понятным сообщением, накопленные draft-документы и очередь не теряются, обработка продолжается после восстановления зависимости (правило §3 стандарта — сбой обязательной зависимости, не «тихий» отказ);
  • пустой/огромный входящий payload → пустой payload от оператора (некорректный вебхук) отклоняется на уровне коннектора с логированием, не создаёт «пустой» документ; огромный документ (вложение) хранится через медиатеку/файловое хранилище ядра, не в JSONB журнала вызовов шины (см. cms/integrations-bus — лимит логируемого payload).

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

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

Миграция legacy-данных (§16 стандарта): прямого донора с боевыми данными ЭДО нет. Если у клиента есть архив документов из прежней системы (ERP/1С/личный кабинет оператора), cms:edo:import-legacy --source=<профиль> переносит только историю (документ + статусы, без повторной отправки уже подписанных документов) — идемпотентно по (provider_code, external_id), --dry-run с отчётом расхождений; если у клиента нет такого архива — раздел помечается «не применимо» в docs/module.md конкретного проекта.

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

  • [ ] Мок API оператора ЭДО: тест отправки документа и получения статуса подписания
  • [ ] Отклонение документа фиксирует причину, уведомляет менеджера, разрешает создание исправленной версии со ссылкой на исходный
  • [ ] Роуминг: поиск контрагента по ИНН+КПП через оператора, отсутствие роуминга/контрагента даёт понятную ошибку, документ не теряется
  • [ ] Отправка с истёкшим сертификатом УКЭП блокируется с понятной ошибкой, не отправляется неподписанной
  • [ ] Алерт об истечении сертификата срабатывает по каждому порогу cert_expiry_alert_days не более одного раза
  • [ ] Повторная генерация УПД/акта по тому же invoice_id+type не создаёт второй документ в ЭДО (идемпотентность)
  • [ ] Опрос статусов идемпотентен — повторный опрос не создаёт дублирующую запись в истории
  • [ ] Повторная доставка вебхука входящего документа не создаёт дубль ((provider_code, external_id))
  • [ ] Конкурентная правка статуса (вебхук + ручное действие) отклоняется 409 (lock_version), не «последний победил»
  • [ ] При выключении cms/integrations-bus модуль деградирует целиком с понятным сообщением, документы не теряются
  • [ ] При выключении модуля edo документы формируются, но не отправляются, без ошибок в UI
  • [ ] Входящий документ от контрагента не теряется при недоступности уведомления (ретрай уведомления отдельно от факта приёма)
  • [ ] Права edo.view/edo.manage разграничивают просмотр документов и отправку/создание исправленной версии через ЭДО
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут; тестовая БД только edo_test, migrate:fresh запрещён

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