9737c787f9
Инфраструктура 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>
168 lines
6.3 KiB
Go
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
|
|
}
|