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 }