5fa6ea6ab1
Реализован внутренний робот-эмулятор в 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).
224 lines
8.8 KiB
Go
224 lines
8.8 KiB
Go
// 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
|
||
}
|