feat(robot): эмулятор робота-автотеста НРД + help-страница + REPORT.md
Реализован внутренний робот-эмулятор в 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).
This commit is contained in:
@@ -58,3 +58,7 @@ test-results/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Doc-watcher: бэкапы при переустановке свежих версий
|
||||
DOC/*.pdf.bak
|
||||
DOC/*.bak.pdf
|
||||
|
||||
@@ -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`.
|
||||
- Ежесуточная фоновая горутина обновляет сертификаты, в ленте новостей появляется уведомление «Обновлён сертификат УЦ: <CN>».
|
||||
- За 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -29,5 +29,11 @@
|
||||
<p class="muted">ИШ НРД (профили GUEST/TEST3/PROD), команда Fansy (ETL в staging), уведомления (e-mail, Yandex Messenger, Telegram), порядок согласования.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/admin/help/robot" style="text-decoration:none">
|
||||
<div class="card" style="height:100%">
|
||||
<h2 style="color:var(--accent)">Тестирование с роботом MOEX МОСТ →</h2>
|
||||
<p class="muted">Робот НРД на TEST3 (код <code>MC0012500000</code>), 4 тестовых сценария (отказ / принять все / частично / встречный перевод), управление через DocumentSeries и DocumentNumber, тестовые наборы депозитариев и кодов ошибок.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
{{define "content"}}
|
||||
<p style="margin-bottom:16px"><a href="/admin/help">← все инструкции</a></p>
|
||||
|
||||
<div class="card">
|
||||
<h2>Тестирование с роботом MOEX МОСТ</h2>
|
||||
<p class="muted">Источник: <code>DOC/instruktsiya-po-testirovaniyu-s-robotom.pdf</code> (опубликована 12.05.2026). Демо-ролик: <a href="https://disk.yandex.ru/i/F1SL2CVY5GphwQ" target="_blank">disk.yandex.ru/i/F1SL2CVY5GphwQ</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>1. Что это</h2>
|
||||
<p>НРД разработан специальный «робот» для тестирования интеграции информационных систем клиента и сервиса переводов M2M. Робот работает <strong>в круглосуточном режиме</strong> и эмулирует действия второй стороны при обмене сообщениями в сервисе M2M.</p>
|
||||
<p>Робот может выступать как принимающей стороной (по умолчанию), так и передающей. Он может формировать как успешные сообщения, так и сообщения о нештатных ситуациях.</p>
|
||||
<p>Доступен на тестовом контуре <strong>TEST3</strong> (<code>gost-t3.nsd.ru</code>). Подключение к роботу не требует отдельной регистрации — достаточно быть подключённым к ЭДО НРД на TEST3.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>2. Адресация робота</h2>
|
||||
<p><strong>КОД РОБОТА: <code>MC0012500000</code></strong></p>
|
||||
<p>Чтобы робот получил сообщение, его код должен быть указан в получателях — <code>Header.ReceiverCode</code>.</p>
|
||||
<p class="muted">В <code>bj-server</code> mock-сендер (<code>internal/nsdadapter/mock</code>) уже понимает этот код: если <code>ReceiverCode == MC0012500000</code> и в заявке указан DocumentSeries из таблицы ниже — внутренний робот-эмулятор сформирует ответ по выбранному сценарию. То же поведение будет на реальном TEST3, когда подключим ИШ.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>3. Тестовые сценарии</h2>
|
||||
<p>Выбор сценария — через поле <code>Data.InvestorInformation.IdentityDocument.DocumentSeries</code> в M2MTransferRequest.</p>
|
||||
<table>
|
||||
<thead><tr><th>Код</th><th>Сценарий</th><th>Управляющий параметр</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>1111</code></td>
|
||||
<td><strong>Ответ с отказом</strong> — все бумаги отвергаются с выбранным кодом ошибки</td>
|
||||
<td>Последние 2 символа <code>DocumentNumber</code> = ключ ошибки (<code>01</code>..<code>09</code>) → код <code>M2M01</code>..<code>M2M09</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>2001</code></td>
|
||||
<td><strong>Принять все бумаги</strong></td>
|
||||
<td><code>DocumentNumber</code>: i-я цифра = номер депозитария-получателя для i-й секции (<code>1</code> или <code>2</code>). По умолчанию <code>1</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>2002</code></td>
|
||||
<td><strong>Принять бумаги частично</strong></td>
|
||||
<td><code>DocumentNumber</code>: i-я цифра = номер депозитария (<code>1</code>/<code>2</code>) или <code>0</code> (отклонить с <code>M2M05</code>).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>3333</code></td>
|
||||
<td><strong>Выступить принимающей стороной</strong> — робот отвергает оригинал и формирует встречный M2MTransferRequest</td>
|
||||
<td>Первые 2 цифры <code>DocumentNumber</code> = реквизиты двух депозитариев для нового перевода</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="muted" style="margin-top:8px">Пример: для сценария <code>1111</code> с <code>DocumentNumber=111102</code> робот вернёт код ошибки <code>M2M02</code>. Для сценария <code>2001</code> с 4 секциями ЦБ и <code>DocumentNumber=111200</code> — секции 1,2,3 принимаются депозитарием 1, секция 4 — депозитарием 2.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>4. Тестовые данные депозитариев</h2>
|
||||
<table>
|
||||
<thead><tr><th>Ключ</th><th>ИНН (SettlementRequisites)</th><th>SettlementDepositoryLocation</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>1</code></td>
|
||||
<td><code>7702165310</code></td>
|
||||
<td>ИНН <code>7722061076</code> · depcode <code>MC0012500000</code> · счёт <code>HL2603250011</code> · раздел <code>31MC0012500000F00</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>2</code></td>
|
||||
<td><code>7702165310</code></td>
|
||||
<td>ИНН <code>7722061076</code> · depcode <code>MC0012500000</code> · счёт <code>HL2603250011</code> · раздел <code>36MC0012500000F00</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>3</code></td>
|
||||
<td><code>7831000034</code></td>
|
||||
<td class="muted">остальные поля — заглушки</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>5. Коды ошибок (для сценария 1111)</h2>
|
||||
<table>
|
||||
<thead><tr><th>Ключ</th><th>Код ошибки</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>01</code></td><td><code>M2M01</code></td></tr>
|
||||
<tr><td><code>02</code></td><td><code>M2M02</code></td></tr>
|
||||
<tr><td><code>03</code></td><td><code>M2M03</code></td></tr>
|
||||
<tr><td><code>04</code></td><td><code>M2M04</code></td></tr>
|
||||
<tr><td><code>05</code></td><td><code>M2M05</code></td></tr>
|
||||
<tr><td><code>06</code></td><td><code>M2M06</code></td></tr>
|
||||
<tr><td><code>07</code></td><td><code>M2M07</code></td></tr>
|
||||
<tr><td><code>08</code></td><td><code>M2M08</code></td></tr>
|
||||
<tr><td><code>09</code></td><td><code>M2M09</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>6. Как запустить</h2>
|
||||
<p><strong>Сейчас, без реального ИШ:</strong> используется внутренний робот-эмулятор в bj-server. Отправь заявку с ReceiverCode = <code>MC0012500000</code> и DocumentSeries по таблице — Decision придёт через 3 секунды по правилам робота.</p>
|
||||
<p><strong>На реальном TEST3 НРД:</strong> установи ИШ НРД (см. <a href="/admin/help/systems">/admin/help/systems</a>), укажи в <a href="/admin/setup">/admin/setup</a> → ИШ профиль <code>test3-gost</code>, URL <code>https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo</code>. Дальше отправляй те же заявки — НРД направит их роботу, ответ будет идентичный.</p>
|
||||
<p class="muted">Сценарий 3333 («выступить принимающей стороной») в нашем внутреннем эмуляторе пока реализован частично — отдаёт только первое сообщение (отказ M2M05). Встречный M2MTransferRequest от робота требует доработки приёмной стороны bj-server.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user