Files
Bridge-and-Join-s/internal/nsdadapter/mock/robot.go
T
fontvielle 5fa6ea6ab1 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).
2026-05-14 16:53:52 +03:00

224 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}