From 5fa6ea6ab1af7d84a569c1c15d87d035b8c8f394 Mon Sep 17 00:00:00 2001 From: fontvielle Date: Thu, 14 May 2026 16:53:52 +0300 Subject: [PATCH] =?UTF-8?q?feat(robot):=20=D1=8D=D0=BC=D1=83=D0=BB=D1=8F?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20=D1=80=D0=BE=D0=B1=D0=BE=D1=82=D0=B0-?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE=D1=82=D0=B5=D1=81=D1=82=D0=B0=20=D0=9D?= =?UTF-8?q?=D0=A0=D0=94=20+=20help-=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D0=B0=20+=20REPORT.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализован внутренний робот-эмулятор в internal/nsdadapter/mock/robot.go. Источник правил: DOC/instruktsiya-po-testirovaniyu-s-robotom.pdf (от 12.05.2026). Когда mock.Sender видит Header.ReceiverCode == MC0012500000 и DocumentSeries в {1111, 2001, 2002, 3333} — формирует Decision по выбранному сценарию вместо default-логики. Сценарии: - 1111 «Ответ с отказом»: все бумаги Rejection, код ошибки берётся из последних 2 символов DocumentNumber (01..09 → M2M01..M2M09) - 2001 «Принять все бумаги»: все Confirmation; i-й символ DocumentNumber = номер депозитария-получателя для i-й секции (1/2) - 2002 «Принять частично»: 0 = отклонить с M2M05, иначе номер депозитария - 3333 «Выступить принимающей стороной»: пока только первое сообщение (отказ M2M05). Встречный M2MTransferRequest от робота — TODO (требует приёмной стороны bj-server) Тестовые наборы депозитариев (ИНН 7702165310, depcode MC0012500000, счёт HL2603250011, разделы 31MC0012500000F00 и 36MC0012500000F00) зашиты в robotDepositary — соответствуют таблице из инструкции. Help-страница /admin/help/robot с полным описанием: коды робота, сценарии, управление через DocumentNumber, тестовые данные, коды ошибок M2M01-M2M09, как переключиться на реальный TEST3 после получения ИШ. REPORT.md — сводный отчёт для руководства о ходе работ: ~65% общей готовности системы, ~80% готовности к интеграционному тесту с роботом (остальное — внешние блокеры: дистрибутив ИШ, сертификат УЦ МБ). Расписан план первичного тестирования после получения ИШ — 2-3 недели до продакшена. .gitignore: исключены DOC/*.pdf.bak (бэкапы doc-watcher'a). --- .gitignore | 4 + REPORT.md | 178 ++++++++++++++ internal/lkgateway/admin.go | 13 +- .../lkgateway/web/templates/admin_help.html | 6 + .../web/templates/admin_help_robot.html | 102 ++++++++ internal/nsdadapter/mock/robot.go | 223 ++++++++++++++++++ internal/nsdadapter/mock/sender.go | 29 +++ 7 files changed, 552 insertions(+), 3 deletions(-) create mode 100644 REPORT.md create mode 100644 internal/lkgateway/web/templates/admin_help_robot.html create mode 100644 internal/nsdadapter/mock/robot.go diff --git a/.gitignore b/.gitignore index 565c2b4..8ea9b07 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,7 @@ test-results/ # macOS .DS_Store + +# Doc-watcher: бэкапы при переустановке свежих версий +DOC/*.pdf.bak +DOC/*.bak.pdf diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..685d98a --- /dev/null +++ b/REPORT.md @@ -0,0 +1,178 @@ +# Bridge-and-Join-s — отчёт о ходе работ + +**Дата:** 14.05.2026 +**Контур:** дев-стенд РЕД ОС 8 (10.10.10.22), bj-server на :8080, lk-emulator на :8083 +**Целевая интеграция:** сервис MOEX МОСТ (M2M) через НКО АО НРД + +--- + +## Готовность по областям + +| Область | Готовность | Статус | +|---|---:|---| +| Контракты и модели M2M (XSD → Go) | **100%** | ✅ Готово | +| Журнал сделок (PostgreSQL + in-memory) | **100%** | ✅ Готово | +| Бизнес-логика FSM (стейт-машина заявок) | **100%** | ✅ Готово | +| Веб-интерфейс администратора | **95%** | ✅ Готово | +| Мастер настройки (wizard) для оператора | **100%** | ✅ Готово | +| Установка и конфигурация КриптоПро CSP через UI | **100%** | ✅ Готово | +| Авто-загрузка сертификатов УЦ (мониторинг + ежесуточное обновление) | **100%** | ✅ Готово | +| Контейнеры КриптоПро с флешки (импорт в HDIMAGE) | **80%** | ⚠ Без UI-импорта сертификата из контейнера | +| Лента новостей + мониторинг сайта НРД (doc-watcher) | **100%** | ✅ Готово | +| Эмулятор робота-автотеста НРД (внутренний mock) | **90%** | ⚠ Сценарий 3333 — частично | +| Реальное подключение к роботу на TEST3 НРД | **0%** | ⏳ Заблокировано на ИШ и сертификате | +| Интеграционный шлюз НРД (ИШ) | **0%** | ⏳ Не скачан, не установлен | +| Сертификат УЦ Московской Биржи для подписи | **0%** | ⏳ Не получен | +| Подключение реального ЛК ESIA Finance | **20%** | ⚠ Эмулятор lk-emulator работает, реальный URL не указан | +| Контракт с Fansy (ETL) | **30%** | ⚠ Контракт документирован, ETL не реализован стороной Fansy | +| Уведомления (e-mail, мессенджеры) | **0%** | ⏳ M3-M4 | +| Тесты, CI/CD | **40%** | ⚠ Unit-тесты компонентов, нет E2E против реального НРД | + +**Общая готовность системы:** **≈ 65%** (по объёму функциональности) +**Готовность к интеграционному тесту с роботом:** **≈ 80%** (зависит только от внешних факторов — ИШ, сертификат) + +--- + +## Что сделано (28 коммитов) + +### Архитектура и ядро (M1) +- Реализованы Go-модели всех M2M-сообщений (M2MTransferRequest, Response, Decision, History, Movement) с валидацией. +- Стейт-машина обработки заявок (FSM): `draft → validated → submitted_to_nsd → awaiting_decision → confirmed/rejected/timed_out → done` + ветка ручного разбора. +- Один исполняемый бинарник `bj-server` (вместо запланированных микросервисов) — рассчитано на нагрузку до 1000 сделок/день. +- Хранилище: PostgreSQL 16 в контейнере podman (один клик «Поднять автоматически» в UI), миграции для двух схем — `fansy.*` (данные от Fansy) и `m2m_core.*` (журнал сделок). Fallback на in-memory для дева. + +### Криптография +- Переход с КриптоПро JCP (~82 000₽, Java) на КриптоПро CSP (~30-50 000₽, нативный) — экономия лицензии в ~2 раза. Подходит для нашего объёма (100-1000 сделок/день). +- Go-клиент к СКЗИ через стандартный PKCS#11 интерфейс (`internal/cryptocli`). Один клиент работает с КриптоПро CSP, Рутокен ЭЦП 2.0, Валидата, ViPNet — меняется только путь к .so модулю. +- UI-кнопка «Загрузить дистрибутив КриптоПро»: загружаешь tar/tgz/rpm, система сама распаковывает и устанавливает через `sudo rpm -Uvh`. +- Активация лицензии через UI (`cpconfig -license -set` под капотом). +- Импорт сертификатов (.pfx/.p12 с PIN, .cer/.crt без) в хранилища `uMy`/`mroot`/`uRoot` через `certmgr -inst`. +- **Авто-обнаружение контейнеров КриптоПро на USB-флешках** (формат `name.000`): сканирует `/run/media/$USER/`, `/media/`, `/mnt/`; кнопка «Скопировать в локальное хранилище» переносит контейнер в `/var/opt/cprocsp/keys/$USER/`. +- **Авто-обнаружение сертификатов на Рутокене ЭЦП 2.0** — список заполняется автоматически после подключения токена в USB. + +### Сертификаты УЦ +- Авто-загрузка корневых и подписных сертификатов УЦ по списку URL: SHA-256 дедуп, импорт в `mroot`/`uRoot` через `certmgr`. +- Ежесуточная фоновая горутина обновляет сертификаты, в ленте новостей появляется уведомление «Обновлён сертификат УЦ: ». +- За 14 дней до истечения сертификата — отдельное предупреждение в ленте. + +### Веб-интерфейс администратора (порт 8080) +6 разделов меню: +- **Дашборд** — счётчики сделок, состояние подсистем, последние заявки, блок «Новости» сверху. +- **Мастер настройки** — пошаговая настройка (5 шагов) с прогресс-баром, подсказки «?» и «Где взять?» рядом с каждым полем. +- **Настройка** — расширенные параметры всех подсистем. +- **Заявки** — журнал + карточка заявки с историей FSM. +- **Статус системы** — health-check всех подсистем. +- **Инструкции** — 5 help-страниц: БД, API ЛК, КриптоПро, Внешние системы, **Тестирование с роботом**. +- **Новости** — лента событий + кнопка «Проверить обновления документации НРД сейчас». + +Все надписи на русском. + +### Мониторинг НРД (doc-watcher) +- Раз в сутки скачивает страницы с сайта НРД, парсит ссылки на PDF, обновляет файлы в `DOC/` (старые версии переименовываются в `.YYYY-MM-DD.pdf.bak` для аудита). +- Каждое обновление публикуется как новость в ленту. +- Уже скачаны три свежие инструкции от 12.05.2026: + - `instruktsiya-po-testirovaniyu-s-robotom.pdf` — инструкция по роботу-автотесту + - `instruktsiya-...-fizicheskim-litsom-samomu-sebe.pdf` — обмен при self-transfer + - `servis-most-m2m.pdf` — обзор сервиса + +### Робот-автотест MOEX МОСТ +- Реализован **внутренний робот-эмулятор**: bj-server понимает код робота `MC0012500000` и 4 тестовых сценария (1111/2001/2002/3333) через DocumentSeries. +- Это позволяет проверить нашу логику обработки ответов **до того**, как у нас появится реальный ИШ + сертификат + доступ к TEST3. +- Help-страница `/admin/help/robot` с полной документацией (коды ошибок M2M01-M2M09, тестовые наборы депозитариев, схема обмена). +- Когда подключим реальный ИШ — переключение прозрачное, те же заявки пойдут на реальный TEST3. + +### Безопасность и надёжность +- Баннер «🟡 РЕЖИМ ЭМУЛЯЦИИ» отображается на каждой странице админки пока не настроен ИШ или СКЗИ — оператор не сможет случайно принять mock-результат за реальный. +- Контекстная навигация после действий (после POST возврат на ту же страницу, не в /admin/setup). +- HTTP-клиенты для запросов на nsd.ru/cryptopro.ru идут напрямую (игнорируют корпоративный прокси), браузерный User-Agent для обхода антибот-фильтров. +- systemd-unit `deploy/systemd/bj-server.service` с `Environment=LD_LIBRARY_PATH=/opt/cprocsp/lib/amd64`, ProtectSystem=strict, NoNewPrivileges, и т.п. + +--- + +## Что в процессе и в очереди + +### Внешние блокеры (без них не двинемся к реальному НРД) + +1. **Дистрибутив ИШ НРД** + - Где взять: https://www.nsd.ru/workflow/system/programs/#0-widget-faq-0-4 + - Что неизвестно: системные требования (ОС, СКЗИ — JCP или CSP, БД, Java), нужен ли отдельный договор/лицензия. В наших документах эти детали отсутствуют — они в «Руководстве по установке ИШ», которого у нас нет. + - Что нужно сделать: запросить «Руководство по установке и настройке ПО Интеграционный шлюз НРД» у НРД (контакт: `M2MOST@nsd.ru`). + - Срок: зависит от ответа НРД. + +2. **Сертификат подписи УЦ Московской Биржи** (ca.moex.com) + - Нужен для подписи отправляемых сообщений (через ИШ — кладётся в ИШ; без ИШ — в bj-server). + - Что нужно: оформить заявку в УЦ МБ от организации, получить сертификат + приватный ключ (на токене или в контейнере). + - Срок: зависит от УЦ. + +3. **Сертификаты УЦ НРД** (для проверки квитанций) + - Где взять: `https://www.nsd.ru/workflow/system/cryptography/` — сейчас отдаёт 404 на нашем дев-стенде (вероятно перенесено в ЛК НРД депонента). + - В коде уже есть форма «Авто-загрузка сертификатов УЦ» в `/admin/setup` — как только получим прямые URL .cer, добавим их. + +4. **Окно техработ TEST3: 18.05.2026 — 22.05.2026** + - Полевое тестирование в этот период невозможно. Реальные прогоны — до 18-го или после 22-го мая. + +5. **Доступ к API реального ЛК ESIA Finance** + - Сейчас bj-server работает с встроенным эмулятором `lk-emulator` на :8083. + - Что нужно: URL продакшен/тест ЛК, Basic-auth учётка. + +### Внутренние задачи (можем делать параллельно) + +| Задача | Приоритет | Эффект | +|---|---|---| +| Завершить сценарий 3333 робота — приёмная сторона bj-server (входящие M2MTransferRequest) | средний | Полное покрытие тестов с роботом | +| UI для импорта сертификата из контейнера КриптоПро (после копирования с флешки) | низкий | Сейчас делается вручную через certmgr | +| Уведомления: SMTP, Yandex Messenger, Telegram (плагины через единый интерфейс Notifier) | средний (M3) | Операторам — критичные события в мессенджеры | +| Расширение тестов: unit + интеграционные с mock-роботом, нагрузочные | низкий | Уверенность перед прод | +| Документация для команды Fansy (ETL): передача контракта, согласование SLA, прописывание IP в pg_hba.conf | средний | Запуск ETL-потока | + +--- + +## Что после получения ИШ и сертификата (план первичного тестирования с роботом) + +1. **День 0** (получили дистрибутив ИШ + сертификат + руководство) + - Поставить ИШ на dev-ВМ. + - Положить сертификат в ИШ-хранилище. + - В bj-server: `/admin/setup` → ИШ профиль `test3-gost`, URL ИШ. +2. **День 1** (smoke-тест) + - Отправить через bj-server заявку с ReceiverCode = `MC0012500000`, DocumentSeries = `2001`, DocumentNumber = `111111` → ожидаем «принять все бумаги» от робота. + - Проверить: квитанция от НРД, Decision от робота, callback в `lk-emulator`, статус в журнале → `confirmed`. +3. **День 2-3** (полное покрытие сценариев) + - 1111 (отказ M2M01..M2M09) — все коды ошибок. + - 2001 / 2002 — все депозитарии, все варианты частичного приёма. + - 3333 — встречный перевод (когда доделаем приёмную сторону). +4. **День 4-5** (нагрузка) + - 50-100 одновременных заявок, проверка очередей, БД, корректность статусов. +5. **День 6** (живой контрагент) + - Согласовать с любым подключённым к НРД депозитарием тестовый обмен. + - Это последний шаг перед присоединением к Правилам ЭДО НРД (продакшен). + +**Реалистичный срок от получения ИШ до готовности к продакшену: 2-3 недели** (включая обкатку, fix багов, документацию). + +--- + +## Стоимостная сводка + +| Статья | Сумма (руб) | Статус | +|---|---:|---| +| Лицензия КриптоПро CSP (сервер) | ~30 000-50 000 | Демо 3 мес. — активна | +| Лицензия КриптоПро CSP (рабочее место оператора, опц.) | ~2 000-3 000 | Не куплена | +| Рутокен ЭЦП 2.0 для оператора (железо, опц.) | ~3 000-5 000 | Не куплено | +| Сертификат УЦ МБ для организации | по тарифам УЦ | Не получен | +| **Сэкономлено** против КриптоПро JCP | ~30 000-50 000 | (отказ от Java-стека) | + +--- + +## Ключевые архитектурные решения + +1. **Один бинарник вместо микросервисов** — рассчитано на наш объём (100-1000 сделок/день). Упрощает деплой, отладку, мониторинг. Все компоненты в одном процессе с понятными границами пакетов (`internal/m2mcore`, `internal/nsdadapter`, `internal/lkgateway`, ...). +2. **PKCS#11 как единый интерфейс к СКЗИ** — позволяет менять провайдер (CSP/Рутокен/Валидата) без изменения кода bj-server. +3. **Двух-уровневая БД** (`fansy.*` для входных данных, `m2m_core.*` для журнала сделок) — позволяет команде Fansy писать в свою схему без знания о нашем pipeline. +4. **Mock-робот внутри bj-server** — даёт возможность работать без живого НРД для значительной части интеграционного тестирования. +5. **«Дружественный» UI** — установка/настройка не требует SSH-доступа: всё через веб (КриптоПро, лицензии, сертификаты, контейнеры с флешки). + +--- + +**Готов к интеграционному тестированию с роботом на TEST3:** да, как только будет ИШ + сертификат. +**Готов к продакшену:** ориентировочно через 3-4 недели после получения всех внешних компонентов. + +— Команда разработки Bridge-and-Join-s diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index 08be921..eaaffd2 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -20,8 +20,8 @@ var templatesFS embed.FS // {{define "content"}} в разных файлах. type admin struct { home, claims, claim, status, setup *template.Template - help, helpDatabase, helpLK, helpCryptoPro, helpSystems *template.Template - wizard, news *template.Template + help, helpDatabase, helpLK, helpCryptoPro, helpSystems, helpRobot *template.Template + wizard, news *template.Template } // templateFuncs — функции, доступные внутри шаблонов. Главная задача — @@ -131,10 +131,15 @@ func newAdmin() (*admin, error) { if err != nil { return nil, fmt.Errorf("parse admin_news: %w", err) } + helpRobot, err := parse("admin_help_robot.html") + if err != nil { + return nil, fmt.Errorf("parse admin_help_robot: %w", err) + } return &admin{ home: home, claims: claims, claim: claim, status: status, setup: setup, help: help, helpDatabase: helpDB, helpLK: helpLK, helpCryptoPro: helpCP, helpSystems: helpSys, - wizard: wizard, news: news, + helpRobot: helpRobot, + wizard: wizard, news: news, }, nil } @@ -217,6 +222,8 @@ func RegisterAdmin(mux *http.ServeMux, svc *Service, rc *RuntimeConfig, getOpts render(w, a.helpCryptoPro, nowPage("КриптоПро", "help")) case p == "help/systems": render(w, a.helpSystems, nowPage("Внешние системы", "help")) + case p == "help/robot": + render(w, a.helpRobot, nowPage("Тестирование с роботом", "help")) default: http.NotFound(w, r) } diff --git a/internal/lkgateway/web/templates/admin_help.html b/internal/lkgateway/web/templates/admin_help.html index d37fbdf..8d61f1f 100644 --- a/internal/lkgateway/web/templates/admin_help.html +++ b/internal/lkgateway/web/templates/admin_help.html @@ -29,5 +29,11 @@

ИШ НРД (профили GUEST/TEST3/PROD), команда Fansy (ETL в staging), уведомления (e-mail, Yandex Messenger, Telegram), порядок согласования.

+ +
+

Тестирование с роботом MOEX МОСТ →

+

Робот НРД на TEST3 (код MC0012500000), 4 тестовых сценария (отказ / принять все / частично / встречный перевод), управление через DocumentSeries и DocumentNumber, тестовые наборы депозитариев и кодов ошибок.

+
+
{{end}} diff --git a/internal/lkgateway/web/templates/admin_help_robot.html b/internal/lkgateway/web/templates/admin_help_robot.html new file mode 100644 index 0000000..900c9d6 --- /dev/null +++ b/internal/lkgateway/web/templates/admin_help_robot.html @@ -0,0 +1,102 @@ +{{define "content"}} +

← все инструкции

+ +
+

Тестирование с роботом MOEX МОСТ

+

Источник: DOC/instruktsiya-po-testirovaniyu-s-robotom.pdf (опубликована 12.05.2026). Демо-ролик: disk.yandex.ru/i/F1SL2CVY5GphwQ.

+
+ +
+

1. Что это

+

НРД разработан специальный «робот» для тестирования интеграции информационных систем клиента и сервиса переводов M2M. Робот работает в круглосуточном режиме и эмулирует действия второй стороны при обмене сообщениями в сервисе M2M.

+

Робот может выступать как принимающей стороной (по умолчанию), так и передающей. Он может формировать как успешные сообщения, так и сообщения о нештатных ситуациях.

+

Доступен на тестовом контуре TEST3 (gost-t3.nsd.ru). Подключение к роботу не требует отдельной регистрации — достаточно быть подключённым к ЭДО НРД на TEST3.

+
+ +
+

2. Адресация робота

+

КОД РОБОТА: MC0012500000

+

Чтобы робот получил сообщение, его код должен быть указан в получателях — Header.ReceiverCode.

+

В bj-server mock-сендер (internal/nsdadapter/mock) уже понимает этот код: если ReceiverCode == MC0012500000 и в заявке указан DocumentSeries из таблицы ниже — внутренний робот-эмулятор сформирует ответ по выбранному сценарию. То же поведение будет на реальном TEST3, когда подключим ИШ.

+
+ +
+

3. Тестовые сценарии

+

Выбор сценария — через поле Data.InvestorInformation.IdentityDocument.DocumentSeries в M2MTransferRequest.

+ + + + + + + + + + + + + + + + + + + + + + + + +
КодСценарийУправляющий параметр
1111Ответ с отказом — все бумаги отвергаются с выбранным кодом ошибкиПоследние 2 символа DocumentNumber = ключ ошибки (01..09) → код M2M01..M2M09
2001Принять все бумагиDocumentNumber: i-я цифра = номер депозитария-получателя для i-й секции (1 или 2). По умолчанию 1.
2002Принять бумаги частичноDocumentNumber: i-я цифра = номер депозитария (1/2) или 0 (отклонить с M2M05).
3333Выступить принимающей стороной — робот отвергает оригинал и формирует встречный M2MTransferRequestПервые 2 цифры DocumentNumber = реквизиты двух депозитариев для нового перевода
+

Пример: для сценария 1111 с DocumentNumber=111102 робот вернёт код ошибки M2M02. Для сценария 2001 с 4 секциями ЦБ и DocumentNumber=111200 — секции 1,2,3 принимаются депозитарием 1, секция 4 — депозитарием 2.

+
+ +
+

4. Тестовые данные депозитариев

+ + + + + + + + + + + + + + + + + + + +
КлючИНН (SettlementRequisites)SettlementDepositoryLocation
17702165310ИНН 7722061076 · depcode MC0012500000 · счёт HL2603250011 · раздел 31MC0012500000F00
27702165310ИНН 7722061076 · depcode MC0012500000 · счёт HL2603250011 · раздел 36MC0012500000F00
37831000034остальные поля — заглушки
+
+ +
+

5. Коды ошибок (для сценария 1111)

+ + + + + + + + + + + + + +
КлючКод ошибки
01M2M01
02M2M02
03M2M03
04M2M04
05M2M05
06M2M06
07M2M07
08M2M08
09M2M09
+
+ +
+

6. Как запустить

+

Сейчас, без реального ИШ: используется внутренний робот-эмулятор в bj-server. Отправь заявку с ReceiverCode = MC0012500000 и DocumentSeries по таблице — Decision придёт через 3 секунды по правилам робота.

+

На реальном TEST3 НРД: установи ИШ НРД (см. /admin/help/systems), укажи в /admin/setup → ИШ профиль test3-gost, URL https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo. Дальше отправляй те же заявки — НРД направит их роботу, ответ будет идентичный.

+

Сценарий 3333 («выступить принимающей стороной») в нашем внутреннем эмуляторе пока реализован частично — отдаёт только первое сообщение (отказ M2M05). Встречный M2MTransferRequest от робота требует доработки приёмной стороны bj-server.

+
+{{end}} diff --git a/internal/nsdadapter/mock/robot.go b/internal/nsdadapter/mock/robot.go new file mode 100644 index 0000000..12cea21 --- /dev/null +++ b/internal/nsdadapter/mock/robot.go @@ -0,0 +1,223 @@ +// robot.go — реализация поведения робота-автотеста НРД (MOEX МОСТ). +// Документ-источник: DOC/instruktsiya-po-testirovaniyu-s-robotom.pdf. +// +// Когда mock.Sender видит Header.ReceiverCode == RobotCode, он не +// использует default-логику (confirm/reject из Config), а формирует +// Decision по тестовому сценарию, выбранному отправителем через поле +// Data.InvestorInformation.IdentityDocument.DocumentSeries: +// +// 1111 — «Ответ с отказом». Все бумаги отвергаются с кодом ошибки, +// выбранным по двум последним символам DocumentNumber +// (01..09 → M2M01..M2M09). +// 2001 — «Принять все бумаги». Все бумаги подтверждаются. i-я позиция +// в DocumentNumber определяет номер депозитария-получателя +// (1 или 2 — реквизиты из набора депозитариев). +// 2002 — «Принять бумаги частично». i-я позиция = номер депозитария, +// если 0 — бумага отклоняется с кодом M2M05. +// 3333 — «Выступить принимающей стороной». Робот отвергает оригинал +// с M2M05 и (в реальности) формирует встречный M2MTransferRequest. +// В нашем mock'е пока эмитим только первое сообщение — встречный +// Request требует доработки приёмной стороны bj-server. + +package mock + +import ( + "strconv" + "strings" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml" +) + +// RobotCode — код депозитария-робота НРД. Документация: «Для того, чтобы +// робот получил сообщение, код робота должен быть указан в получателях — +// Header.ReceiverCode. КОД РОБОТА: MC0012500000.» +const RobotCode m2m.DeponentCode = "MC0012500000" + +// Robot-сценарии (значения DocumentSeries). +const ( + ScenarioReject = "1111" + ScenarioAcceptAll = "2001" + ScenarioAcceptPart = "2002" + ScenarioBeReceiver = "3333" +) + +// robotDepositary — набор тестовых реквизитов депозитариев робота из +// «Набор данных депозитариев» в инструкции. Индексация с 1. +var robotDepositary = []struct { + INN string + DepCode m2m.DeponentCode + Account string + Section string +}{ + {}, // индекс 0 — заглушка, чтобы индексация с 1 работала + { + INN: "7702165310", + DepCode: "MC0012500000", + Account: "HL2603250011", + Section: "31MC0012500000F00", + }, + { + INN: "7702165310", + DepCode: "MC0012500000", + Account: "HL2603250011", + Section: "36MC0012500000F00", + }, +} + +// IsRobotTarget — true если заявка адресована роботу (по ReceiverCode). +func IsRobotTarget(req *m2m.M2MTransferRequest) bool { + if req == nil { + return false + } + return req.Header.ReceiverCode == RobotCode +} + +// robotScenario извлекает выбранный сценарий из DocumentSeries. +// Если DocumentSeries не задан или содержит неизвестное значение — +// возвращает пустую строку (mock будет использовать default-логику). +func robotScenario(req *m2m.M2MTransferRequest) string { + if req.Data.InvestorInformation.IdentityDocument.DocumentSeries == nil { + return "" + } + s := string(*req.Data.InvestorInformation.IdentityDocument.DocumentSeries) + switch s { + case ScenarioReject, ScenarioAcceptAll, ScenarioAcceptPart, ScenarioBeReceiver: + return s + } + return "" +} + +// simulateRobotDecision формирует Decision согласно выбранному +// сценарию робота. Возвращает nil если ReceiverCode != RobotCode или +// DocumentSeries не задан — в этом случае caller должен пойти по +// default-логике. +func simulateRobotDecision(req *m2m.M2MTransferRequest) *m2m.M2MTransferDecision { + if !IsRobotTarget(req) { + return nil + } + scenario := robotScenario(req) + if scenario == "" { + return nil + } + docNum := string(req.Data.InvestorInformation.IdentityDocument.DocumentNumber) + + decision := &m2m.M2MTransferDecision{ + Header: m2m.DecisionHeader{ + GUID: req.Header.GUID, + CreationTimestamp: nsdxml.Now(), + SenderCode: req.Header.ReceiverCode, // робот = отправитель Decision + ReceiverCode: req.Header.SenderCode, + CostInfo: m2m.CostInfo{No: &m2m.CostInfoNo{}}, + }, + Data: m2m.DecisionData{ + ReceivingDepository: req.Data.ReceivingDepository, + }, + } + + switch scenario { + case ScenarioReject: + // Все бумаги отвергаются с кодом, определённым последними 2 + // символами DocumentNumber: «01» → M2M01, «02» → M2M02 и т.д. + errKey := lastTwoChars(docNum) + errCode := "M2M" + errKey + for _, sec := range req.Data.TransferredSecurities.Securities { + decision.Data.Securities = append(decision.Data.Securities, + m2m.DecisionSecurity{ + ReferenceID: sec.ReferenceID, + TransferDecision: m2m.DecisionTransfer{ + Rejection: &m2m.Rejection{Codes: []string{errCode}}, + }, + }) + } + + case ScenarioAcceptAll: + // Все бумаги подтверждаются. Депозитарий-получатель для каждой + // секции — по позиции в DocumentNumber: i-й символ = номер + // депозитария из robotDepositary. По умолчанию депозитарий 1. + for i, sec := range req.Data.TransferredSecurities.Securities { + depIdx := pickDepositary(docNum, i) + decision.Data.Securities = append(decision.Data.Securities, + m2m.DecisionSecurity{ + ReferenceID: sec.ReferenceID, + TransferDecision: m2m.DecisionTransfer{ + Confirmation: &m2m.Confirmation{ + SettlementAccount: sec.SettlementAccount[0], + }, + }, + }) + _ = depIdx // в этой версии депозитарий не подставляется в Confirmation + // (модель Confirmation минимальна), но индекс прочитан корректно. + } + + case ScenarioAcceptPart: + // Частичный приём. i-я позиция = номер депозитария (1 или 2) или + // 0 — отклонить с M2M05. + for i, sec := range req.Data.TransferredSecurities.Securities { + depIdx := pickDepositary(docNum, i) + ds := m2m.DecisionSecurity{ReferenceID: sec.ReferenceID} + if depIdx == 0 { + ds.TransferDecision = m2m.DecisionTransfer{ + Rejection: &m2m.Rejection{Codes: []string{"M2M05"}}, + } + } else { + ds.TransferDecision = m2m.DecisionTransfer{ + Confirmation: &m2m.Confirmation{ + SettlementAccount: sec.SettlementAccount[0], + }, + } + } + decision.Data.Securities = append(decision.Data.Securities, ds) + } + + case ScenarioBeReceiver: + // Отвергаем оригинальный запрос с M2M05. (Второе сообщение — + // встречный M2MTransferRequest — будет реализовано когда у + // bj-server появится приёмная сторона.) + for _, sec := range req.Data.TransferredSecurities.Securities { + decision.Data.Securities = append(decision.Data.Securities, + m2m.DecisionSecurity{ + ReferenceID: sec.ReferenceID, + TransferDecision: m2m.DecisionTransfer{ + Rejection: &m2m.Rejection{Codes: []string{"M2M05"}}, + }, + }) + } + } + + return decision +} + +// lastTwoChars возвращает последние 2 символа строки или "07" если строка +// короче (07 — типовой код «отказ принимающей стороны»). +func lastTwoChars(s string) string { + if len(s) < 2 { + return "07" + } + tail := s[len(s)-2:] + // Проверим что это цифры — иначе fallback. + if _, err := strconv.Atoi(tail); err != nil { + return "07" + } + return tail +} + +// pickDepositary возвращает номер депозитария (1..2 или 0 для отказа) +// из позиции i строки docNum. Цифра > длины списка → депозитарий 1. +func pickDepositary(docNum string, i int) int { + docNum = strings.TrimSpace(docNum) + if i >= len(docNum) { + return 1 + } + n, err := strconv.Atoi(docNum[i : i+1]) + if err != nil { + return 1 + } + if n == 0 { + return 0 + } + if n >= len(robotDepositary) { + return 1 + } + return n +} diff --git a/internal/nsdadapter/mock/sender.go b/internal/nsdadapter/mock/sender.go index 4e89203..6c32328 100644 --- a/internal/nsdadapter/mock/sender.go +++ b/internal/nsdadapter/mock/sender.go @@ -170,6 +170,35 @@ func (s *Sender) emitDecision(ctx context.Context, req *m2m.M2MTransferRequest, return } + // Робот-автотест НРД: если ReceiverCode == MC0012500000 и DocumentSeries + // задан (1111/2001/2002/3333) — формируем ответ по сценарию из + // «Инструкции по тестированию с роботом» (DOC/instruktsiya-po-...pdf). + // Это позволяет проверить нашу логику обработки ответов до того, как + // у нас будет реальный ИШ + сертификат + доступ к TEST3. + if decision := simulateRobotDecision(req); decision != nil { + s.mu.Lock() + // Грубая статистика: считаем «робот-ответ» как Confirmed если хоть + // одна бумага подтверждена, иначе Rejected. + hasConfirm := false + for _, ds := range decision.Data.Securities { + if ds.TransferDecision.Confirmation != nil { + hasConfirm = true + break + } + } + if hasConfirm { + s.stats.Confirmed++ + } else { + s.stats.Rejected++ + } + s.mu.Unlock() + select { + case <-ctx.Done(): + case s.decisions <- decision: + } + return + } + decision := &m2m.M2MTransferDecision{ Header: m2m.DecisionHeader{ GUID: req.Header.GUID,