Тема
ТЗ — ЭДО (Диадок/СБИС) (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_documents | id, invoice_id, provider_code, external_id, type, status, rejection_reason (nullable), corrected_document_id (nullable), lock_version | документ, отправленный через ЭДО |
cms_edo_status_log | id, document_id, status, reason (nullable), changed_at | история статусов подписания |
cms_edo_certificates | id, provider_code, subject_inn, valid_from, valid_to, status, last_alert_sent_at (nullable) | сертификаты УКЭП по операторам, отслеживание истечения |
Индексы: FK invoice_id/document_id/corrected_document_id — constrained()->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}/send | invoice (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-resend | document (path), корректирующий payload из cms/commerce-invoices | FormRequest + Policy edo.manage; исходный документ обязан быть в статусе rejected |
GET /admin/edo/documents, /admin/edo/incoming, /admin/edo/certificates | filter[status], filter[provider_code], sort, cursor | FormRequest 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_provider | string | null | нет | Код активного оператора ЭДО (пусто = модуль фактически выключен — kill-switch) |
edo.auto_send_on_invoice | bool | false | нет | Автоотправка документа при создании счёта/акта |
edo.status_poll_minutes | int | 30 | нет | Периодичность опроса статусов подписания |
edo.cert_expiry_alert_days | array | [30, 14, 7] | нет | За сколько дней до истечения сертификата эскалировать алерт |
edo.roaming_lookup_enabled | bool | true | нет | Искать контрагента по ИНН+КПП через роуминг оператора при отправке |
edo.reject_notify_manager | bool | true | нет | Уведомлять ответственного менеджера при отклонении документа контрагентом |
edo.max_send_retry_attempts | int | 3 | нет | Число ретраев отправки при недоступности оператора (передаётся в cms/integrations-bus) |
Секреты доступа к API оператора ЭДО (сертификат, токен) — только .env.
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| POST | /api/v1/admin/edo/documents/{invoice}/send | admin (edo.manage) | Отправка документа контрагенту через ЭДО |
| POST | /api/v1/admin/edo/documents/{document}/reject-and-resend | admin (edo.manage) | Создание исправленной версии отклонённого документа |
| GET | /api/v1/admin/edo/documents | admin (edo.view) | Список документов со статусами подписания |
| GET | /api/v1/admin/edo/incoming | admin (edo.view) | Входящие документы от контрагентов |
| GET | /api/v1/admin/edo/certificates | admin (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_days | provider_code, valid_to, days_left |
Таблица взаимодействий:
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
cms/commerce-invoices | событие InvoiceDocumentGenerated (канал 1) | in | триггер автоотправки документа при auto_send_on_invoice=true |
cms/commerce-b2b | сервис-вызов (requires) | in | реквизиты контрагента (ИНН/КПП) для поиска в роуминге и адресации отправки |
cms/integrations-bus | requires (канал 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запрещён