Files
Bridge-and-Join-s/internal/m2mcore/enrich.go
T
zuevav da12a9ec1c feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk)
- трансферные таблицы FTV_M2M: создание при установке, события, справочник участников
- сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO
- боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК
- онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML)
- модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:41:12 +03:00

183 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) {
req, err := EnrichRequestUnchecked(ctx, store, claim, codes)
if err != nil {
return nil, err
}
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("m2mcore: собранный M2MTransferRequest невалиден: %w", err)
}
return req, nil
}
// EnrichRequestUnchecked — шаги 1–3 EnrichRequest без проверки формата: пакет нужен и
// тогда, когда данные заявки некорректны (журнал заявок, отказ через учётную систему).
func EnrichRequestUnchecked(
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
}
return req, nil
}