Files
Bridge-and-Join-s/internal/m2mcore/enrich.go
T
zuevav 9737c787f9 feat: живой цикл M2M с НРД + мастер установки ключа на флешку
Инфраструктура M2M (живой обмен с НРД через ИШ):
- обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение
  ответа; INFO → ждём Decision; идемпотентность поллера
- fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO
- сырой XML ответа НРД в карточке заявки (для пересылки в ТП)
- тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes,
  4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта
- редирект из теста сразу в карточку заявки

Мастер установки ключа Валидаты на флешку (admin/setup/keywizard):
- пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник
  сертификатов (CRL) → перезапуск+проверка ИШ → готово
- привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен,
  bj-server остаётся в песочнице
- сохранение структуры профиля архива (spr<N>), перечисление съёмных USB

Прочее:
- пакет-доказательство для ТП НРД + форма регистрации участника M2M
- эталонные образцы робота (DOC/m2m_robot_samples)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 00:03:21 +03:00

168 lines
6.3 KiB
Go

package m2mcore
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
)
// ClaimInput — входная заявка из ЛК (после OpenAPI-парсинга).
type ClaimInput struct {
InvestorClientID string
TransferringDepositoryINN m2m.OrganizationINN
ReceivingDepositoryINN m2m.OrganizationINN
CostInfo m2m.CostInfo
IIAAgreement *m2m.IIAAgreementDetails
Securities []ClaimSecurityInput
// InvestorDocument, если задан, переопределяет документ инвестора из
// анкеты Fansy. Нужен для тестов с роботом НРД, где код сценария
// кодируется в серии ДУЛ (DocumentSeries). Для обычных заявок — nil
// (личность берётся из анкеты-источника).
InvestorDocument *ClientDocument
}
// ClaimSecurityInput — одна ЦБ в заявке.
type ClaimSecurityInput struct {
SecurityCode m2m.SecurityCode
Details m2m.SecurityDetails
Quantity m2m.Quantity
}
// SenderReceiver — отправитель и получатель в Header (коды депонентов).
type SenderReceiver struct {
SenderCode m2m.DeponentCode
ReceiverCode m2m.DeponentCode
}
// referenceIDChars — алфавит для генерации ReferenceID (5 случайных
// символов после префикса "M2M" и даты).
const referenceIDChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// NewReferenceID генерирует идентификатор операции в формате
// "M2M" + YYYYMMDD + 5 случайных символов из [A-Z0-9].
// Длина — ровно 16, как требует XSD ReferenceIdType.
func NewReferenceID(at time.Time) (m2m.ReferenceID, error) {
suffix := make([]byte, 5)
for i := range suffix {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(referenceIDChars))))
if err != nil {
return "", fmt.Errorf("m2mcore: NewReferenceID rand: %w", err)
}
suffix[i] = referenceIDChars[n.Int64()]
}
day := at.UTC()
id := fmt.Sprintf("M2M%04d%02d%02d%s",
day.Year(), int(day.Month()), day.Day(), suffix)
out := m2m.ReferenceID(id)
if err := out.Validate(); err != nil {
return "", err
}
return out, nil
}
// EnrichRequest собирает M2MTransferRequest из заявки ЛК и данных Fansy.
// Шаги:
// 1. Поднять анкету клиента.
// 2. Поднять депо-счета у передающего депозитария и проверить остатки.
// 3. Сгенерировать GUID, ReferenceID для каждой ЦБ, CreationTimestamp.
// 4. Заполнить структуру и провалидировать.
func EnrichRequest(
ctx context.Context,
store FansyStore,
claim ClaimInput,
codes SenderReceiver,
) (*m2m.M2MTransferRequest, error) {
client, err := store.GetClientByID(ctx, claim.InvestorClientID)
if err != nil {
return nil, fmt.Errorf("m2mcore: GetClientByID: %w", err)
}
// Переопределение документа инвестора (тест с роботом: серия ДУЛ = код
// сценария). Меняем только удостоверение личности, ФИО оставляем из анкеты.
if claim.InvestorDocument != nil {
client.Document = *claim.InvestorDocument
}
accounts, err := store.GetDepoAccounts(ctx, claim.InvestorClientID, claim.TransferringDepositoryINN)
if err != nil {
return nil, fmt.Errorf("m2mcore: GetDepoAccounts: %w", err)
}
if len(accounts) == 0 {
return nil, fmt.Errorf("m2mcore: у клиента нет активных счетов в передающем депозитарии")
}
guid, err := NewUUIDv4()
if err != nil {
return nil, err
}
now := time.Now()
securities := make([]m2m.RequestSecurity, 0, len(claim.Securities))
for _, sec := range claim.Securities {
refID, err := NewReferenceID(now)
if err != nil {
return nil, err
}
// Берём первый активный счёт как минимум; реальная логика выбора
// settlement_accounts будет в M2 (по типу ЦБ и торговому разделу).
settlement := make([]m2m.RequestSettlementAccount, 0, len(accounts))
for _, a := range accounts {
settlement = append(settlement, m2m.RequestSettlementAccount{
SettlementRequisites: m2m.SettlementRequisites{INN: a.DepositoryINN},
SettlementLocation: m2m.SettlementDepositoryLocation{
DeponentCode: a.DeponentCode,
AccountID: a.AccountID,
SectionID: a.SectionID,
},
})
}
securities = append(securities, m2m.RequestSecurity{
ReferenceID: refID,
SecurityCode: sec.SecurityCode,
SecurityDetails: sec.Details,
Quantity: sec.Quantity,
SettlementAccount: settlement,
IsolationStatus: m2m.IsolationSGDN,
})
}
req := &m2m.M2MTransferRequest{
Header: m2m.RequestHeader{
GUID: m2m.UUID(guid),
CreationTimestamp: nsdxml.Now(),
SenderCode: codes.SenderCode,
ReceiverCode: codes.ReceiverCode,
CostInfo: claim.CostInfo,
IIAAgreementDetails: claim.IIAAgreement,
},
Data: m2m.RequestData{
InvestorInformation: m2m.InvestorInformation{
LastName: client.LastName,
FirstName: client.FirstName,
MiddleName: client.MiddleName,
IdentityDocument: m2m.IdentityDocument{
DocumentType: client.Document.DocumentType,
DocumentNumber: m2m.IdentityDocSerial(client.Document.Number),
},
},
TransferringDepository: m2m.SettlementRequisites{INN: claim.TransferringDepositoryINN},
ReceivingDepository: m2m.SettlementRequisites{INN: claim.ReceivingDepositoryINN},
TransferredSecurities: m2m.RequestTransferredSecurities{Securities: securities},
},
}
if claim.IIAAgreement != nil {
req.Header.IIAAgreementDetails = claim.IIAAgreement
}
if client.Document.Series != "" {
series := m2m.IdentityDocSerial(client.Document.Series)
req.Data.InvestorInformation.IdentityDocument.DocumentSeries = &series
}
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("m2mcore: собранный M2MTransferRequest невалиден: %w", err)
}
return req, nil
}