// 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 }