1d6ab86a57
- internal/m2m/types.go: enum'ы и simple-типы из XSD НРД (M2MSchemas_260408) - internal/m2m/validators.go: pattern-валидаторы ReferenceID/ISIN/INN/UUID/SecurityCode/IdentityDocSerial/AccountID + перечисления - internal/m2m/messages.go: структуры 6 типов сообщений M2M, choice-типы через указатели, IsM2M=true автоматически в MarshalXML - internal/nsdxml/datetime.go: тип NSDDateTime (формат "YYYY-MM-DDThh:mm:ss(МСК+N)") - internal/nsdxml/codec.go: Marshal/Unmarshal XML в windows-1251 (собственный кодек CP1251, без внешних зависимостей) - internal/m2m/messages_test.go: round-trip тесты на 6 примерах + 2 эталонах из DOC/ Покрытие: m2m 73.9%, nsdxml 92.5%. make ci зелёный. Отклонение от спеки: вместо golang.org/x/text/encoding/charmap собственная таблица CP1251 на ~60 строк, потому что прокси zetit блокирует proxy.golang.org, goproxy.cn и redirect-хосты Go-модулей. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
801 lines
31 KiB
Go
801 lines
31 KiB
Go
package m2m
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
|
|
)
|
|
|
|
// Namespaces — целевые namespace из XSD M2MSchemas_260408.
|
|
const (
|
|
NSTypes = "http://nsd.ru/schemas/m2m/types"
|
|
NSRequest = "http://nsd.ru/schemas/m2m/request"
|
|
NSDecision = "http://nsd.ru/schemas/m2m/decision"
|
|
NSResponse = "http://nsd.ru/schemas/m2m/response"
|
|
NSHandbook = "http://nsd.ru/schemas/m2m/handbook"
|
|
NSHandbookReq = "http://nsd.ru/schemas/m2m/handbook/request"
|
|
NSParticipantForm = "http://nsd.ru/schemas/m2m/participant/form"
|
|
)
|
|
|
|
// ErrChoice возвращается, если в choice-типе задано не ровно одно поле.
|
|
var ErrChoice = errors.New("m2m: в choice-типе должно быть задано ровно одно поле")
|
|
|
|
// reDecimal16 — паттерн допустимого десятичного числа с не более чем
|
|
// 16 знаков после точки.
|
|
var reDecimal16 = regexp.MustCompile(`^[0-9]+(\.[0-9]{1,16})?$`)
|
|
|
|
// Decimal16 — десятичное число (XSD Decimal16): неотрицательное, до 16
|
|
// знаков после точки, до 38 значащих цифр всего. Хранится строкой ради
|
|
// точности и стабильности round-trip.
|
|
type Decimal16 string
|
|
|
|
// Validate проверяет формат и общее число цифр.
|
|
func (d Decimal16) Validate() error {
|
|
if d == "" {
|
|
return fmt.Errorf("%w: Decimal16 пустое значение", ErrInvalid)
|
|
}
|
|
if !reDecimal16.MatchString(string(d)) {
|
|
return fmt.Errorf("%w: Decimal16 %q не соответствует формату", ErrInvalid, string(d))
|
|
}
|
|
digits := 0
|
|
for _, r := range d {
|
|
if r >= '0' && r <= '9' {
|
|
digits++
|
|
}
|
|
}
|
|
if digits > 38 {
|
|
return fmt.Errorf("%w: Decimal16 %d значащих цифр, ожидается не более 38", ErrInvalid, digits)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IdentityDocument — документ, удостоверяющий личность.
|
|
type IdentityDocument struct {
|
|
DocumentType IdentityDocumentCode `xml:"http://nsd.ru/schemas/m2m/types DocumentType"`
|
|
DocumentSeries *IdentityDocSerial `xml:"http://nsd.ru/schemas/m2m/types DocumentSeries,omitempty"`
|
|
DocumentNumber IdentityDocSerial `xml:"http://nsd.ru/schemas/m2m/types DocumentNumber"`
|
|
}
|
|
|
|
// Validate проверяет тип и заполненность номера документа.
|
|
func (d IdentityDocument) Validate() error {
|
|
if err := d.DocumentType.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if d.DocumentSeries != nil {
|
|
if err := d.DocumentSeries.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return d.DocumentNumber.Validate()
|
|
}
|
|
|
|
// InvestorInformation — анкета инвестора.
|
|
type InvestorInformation struct {
|
|
LastName string `xml:"http://nsd.ru/schemas/m2m/types LastName"`
|
|
FirstName string `xml:"http://nsd.ru/schemas/m2m/types FirstName"`
|
|
MiddleName string `xml:"http://nsd.ru/schemas/m2m/types MiddleName,omitempty"`
|
|
IdentityDocument IdentityDocument `xml:"http://nsd.ru/schemas/m2m/types IdentityDocument"`
|
|
}
|
|
|
|
// Validate проверяет длину полей и документ.
|
|
func (i InvestorInformation) Validate() error {
|
|
if l := len(i.LastName); l < 1 || l > 50 {
|
|
return fmt.Errorf("%w: LastName длина %d, ожидается 1..50", ErrInvalid, l)
|
|
}
|
|
if l := len(i.FirstName); l < 1 || l > 50 {
|
|
return fmt.Errorf("%w: FirstName длина %d, ожидается 1..50", ErrInvalid, l)
|
|
}
|
|
if l := len(i.MiddleName); l > 50 {
|
|
return fmt.Errorf("%w: MiddleName длина %d, ожидается не более 50", ErrInvalid, l)
|
|
}
|
|
return i.IdentityDocument.Validate()
|
|
}
|
|
|
|
// SettlementRequisites — реквизиты депозитария (содержит только ИНН).
|
|
type SettlementRequisites struct {
|
|
INN OrganizationINN `xml:"http://nsd.ru/schemas/m2m/types INN"`
|
|
}
|
|
|
|
// Validate проверяет ИНН.
|
|
func (s SettlementRequisites) Validate() error { return s.INN.Validate() }
|
|
|
|
// SettlementDepositoryLocation — реквизиты счёта депо.
|
|
type SettlementDepositoryLocation struct {
|
|
DeponentCode string `xml:"http://nsd.ru/schemas/m2m/types DeponentCode"`
|
|
AccountID AccountID `xml:"http://nsd.ru/schemas/m2m/types AccountId"`
|
|
SectionID string `xml:"http://nsd.ru/schemas/m2m/types SectionId"`
|
|
}
|
|
|
|
// Validate проверяет длины и формат счёта.
|
|
func (s SettlementDepositoryLocation) Validate() error {
|
|
if l := len(s.DeponentCode); l < 1 || l > 50 {
|
|
return fmt.Errorf("%w: DeponentCode длина %d, ожидается 1..50", ErrInvalid, l)
|
|
}
|
|
if err := s.AccountID.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if l := len(s.SectionID); l < 1 || l > 50 {
|
|
return fmt.Errorf("%w: SectionID длина %d, ожидается 1..50", ErrInvalid, l)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RequestSettlementAccount — счёт в запросе перевода.
|
|
type RequestSettlementAccount struct {
|
|
SettlementRequisites SettlementRequisites `xml:"http://nsd.ru/schemas/m2m/types SettlementRequisites"`
|
|
SettlementLocation SettlementDepositoryLocation `xml:"http://nsd.ru/schemas/m2m/types SettlementLocation"`
|
|
}
|
|
|
|
// Validate последовательно валидирует реквизиты и место хранения.
|
|
func (a RequestSettlementAccount) Validate() error {
|
|
if err := a.SettlementRequisites.Validate(); err != nil {
|
|
return err
|
|
}
|
|
return a.SettlementLocation.Validate()
|
|
}
|
|
|
|
// DecisionSettlementAccount — счёт в решении (структурно совпадает с
|
|
// запросом, но именован отдельно в XSD).
|
|
type DecisionSettlementAccount = RequestSettlementAccount
|
|
|
|
// IIAAgreementDetails — реквизиты договора ИИС.
|
|
type IIAAgreementDetails struct {
|
|
AgreementType IIAContractType `xml:"http://nsd.ru/schemas/m2m/types AgreementType"`
|
|
AgreementNumber string `xml:"http://nsd.ru/schemas/m2m/types AgreementNumber"`
|
|
AgreementDate string `xml:"http://nsd.ru/schemas/m2m/types AgreementDate"`
|
|
BrokerINN OrganizationINN `xml:"http://nsd.ru/schemas/m2m/types BrokerINN"`
|
|
}
|
|
|
|
// Validate проверяет тип, номер договора и ИНН брокера.
|
|
func (d IIAAgreementDetails) Validate() error {
|
|
if err := d.AgreementType.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if l := len(d.AgreementNumber); l < 1 || l > 128 {
|
|
return fmt.Errorf("%w: AgreementNumber длина %d, ожидается 1..128", ErrInvalid, l)
|
|
}
|
|
if d.AgreementDate == "" {
|
|
return fmt.Errorf("%w: AgreementDate пуста", ErrInvalid)
|
|
}
|
|
return d.BrokerINN.Validate()
|
|
}
|
|
|
|
// FundShares — реквизиты пая инвестиционного фонда.
|
|
type FundShares struct {
|
|
RegNumber string `xml:"http://nsd.ru/schemas/m2m/types RegNumber"`
|
|
Class string `xml:"http://nsd.ru/schemas/m2m/types Class,omitempty"`
|
|
}
|
|
|
|
// Validate проверяет длины полей.
|
|
func (f FundShares) Validate() error {
|
|
if l := len(f.RegNumber); l < 1 || l > 256 {
|
|
return fmt.Errorf("%w: RegNumber длина %d, ожидается 1..256", ErrInvalid, l)
|
|
}
|
|
if l := len(f.Class); l > 120 {
|
|
return fmt.Errorf("%w: Class длина %d, ожидается не более 120", ErrInvalid, l)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IdentificationDetails — choice: либо рег.номер выпуска, либо ПИФ.
|
|
type IdentificationDetails struct {
|
|
RegNumber *string `xml:"http://nsd.ru/schemas/m2m/types RegNumber,omitempty"`
|
|
FundShares *FundShares `xml:"http://nsd.ru/schemas/m2m/types FundShares,omitempty"`
|
|
}
|
|
|
|
// Validate проверяет, что задано ровно одно поле choice.
|
|
func (i IdentificationDetails) Validate() error {
|
|
count := 0
|
|
if i.RegNumber != nil {
|
|
if l := len(*i.RegNumber); l > 20 {
|
|
return fmt.Errorf("%w: RegNumber длина %d, ожидается не более 20", ErrInvalid, l)
|
|
}
|
|
count++
|
|
}
|
|
if i.FundShares != nil {
|
|
if err := i.FundShares.Validate(); err != nil {
|
|
return err
|
|
}
|
|
count++
|
|
}
|
|
if count != 1 {
|
|
return fmt.Errorf("%w: IdentificationDetails задано %d полей", ErrChoice, count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SecurityDescription — описание ценной бумаги без ISIN.
|
|
type SecurityDescription struct {
|
|
SecurityClassification SecurityClassification `xml:"http://nsd.ru/schemas/m2m/types SecurityClassification"`
|
|
SecurityCategory SecurityCategory `xml:"http://nsd.ru/schemas/m2m/types SecurityCategory"`
|
|
SecurityType string `xml:"http://nsd.ru/schemas/m2m/types SecurityType,omitempty"`
|
|
SecuritySeries string `xml:"http://nsd.ru/schemas/m2m/types SecuritySeries,omitempty"`
|
|
IdentificationDetails IdentificationDetails `xml:"http://nsd.ru/schemas/m2m/types IdentificationDetails"`
|
|
}
|
|
|
|
// Validate проверяет классификацию, категорию и идентификацию.
|
|
func (s SecurityDescription) Validate() error {
|
|
if err := s.SecurityClassification.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.SecurityCategory.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if l := len(s.SecurityType); l > 256 {
|
|
return fmt.Errorf("%w: SecurityType длина %d, ожидается не более 256", ErrInvalid, l)
|
|
}
|
|
return s.IdentificationDetails.Validate()
|
|
}
|
|
|
|
// SecurityDetails — choice: либо ISIN, либо описание ценной бумаги.
|
|
type SecurityDetails struct {
|
|
ISIN *ISIN `xml:"http://nsd.ru/schemas/m2m/types ISIN,omitempty"`
|
|
SecurityInfo *SecurityDescription `xml:"http://nsd.ru/schemas/m2m/types SecurityInfo,omitempty"`
|
|
}
|
|
|
|
// Validate проверяет, что задано ровно одно поле choice.
|
|
func (s SecurityDetails) Validate() error {
|
|
count := 0
|
|
if s.ISIN != nil {
|
|
if err := s.ISIN.Validate(); err != nil {
|
|
return err
|
|
}
|
|
count++
|
|
}
|
|
if s.SecurityInfo != nil {
|
|
if err := s.SecurityInfo.Validate(); err != nil {
|
|
return err
|
|
}
|
|
count++
|
|
}
|
|
if count != 1 {
|
|
return fmt.Errorf("%w: SecurityDetails задано %d полей", ErrChoice, count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Quantity — choice: целое или дробное количество ценных бумаг.
|
|
type Quantity struct {
|
|
Whole *uint64 `xml:"http://nsd.ru/schemas/m2m/types Whole,omitempty"`
|
|
Fractional *Decimal16 `xml:"http://nsd.ru/schemas/m2m/types Fractional,omitempty"`
|
|
}
|
|
|
|
// Validate проверяет, что задано ровно одно поле choice.
|
|
func (q Quantity) Validate() error {
|
|
count := 0
|
|
if q.Whole != nil {
|
|
if *q.Whole == 0 {
|
|
return fmt.Errorf("%w: Whole должно быть положительным", ErrInvalid)
|
|
}
|
|
count++
|
|
}
|
|
if q.Fractional != nil {
|
|
if err := q.Fractional.Validate(); err != nil {
|
|
return err
|
|
}
|
|
count++
|
|
}
|
|
if count != 1 {
|
|
return fmt.Errorf("%w: Quantity задано %d полей", ErrChoice, count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CostInfoYes — тело варианта "учёт ведётся" (DecisionYesType и
|
|
// RequestYesType структурно совпадают).
|
|
type CostInfoYes struct {
|
|
Code DeponentCode `xml:"http://nsd.ru/schemas/m2m/types Code"`
|
|
}
|
|
|
|
// CostInfoNo — тело варианта "учёт не ведётся" (NoType пустой).
|
|
type CostInfoNo struct{}
|
|
|
|
// CostInfo — choice: учёт стоимости приобретения ведётся (Yes) или нет.
|
|
type CostInfo struct {
|
|
Yes *CostInfoYes `xml:"http://nsd.ru/schemas/m2m/types Yes,omitempty"`
|
|
No *CostInfoNo `xml:"http://nsd.ru/schemas/m2m/types No,omitempty"`
|
|
}
|
|
|
|
// Validate проверяет, что задано ровно одно поле choice.
|
|
func (c CostInfo) Validate() error {
|
|
count := 0
|
|
if c.Yes != nil {
|
|
if err := c.Yes.Code.Validate(); err != nil {
|
|
return err
|
|
}
|
|
count++
|
|
}
|
|
if c.No != nil {
|
|
count++
|
|
}
|
|
if count != 1 {
|
|
return fmt.Errorf("%w: CostInfo задано %d полей", ErrChoice, count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Confirmation — подтверждение приёма ценных бумаг по решению.
|
|
type Confirmation struct {
|
|
SettlementAccount DecisionSettlementAccount `xml:"http://nsd.ru/schemas/m2m/types SettlementAccount"`
|
|
}
|
|
|
|
// Validate валидирует счёт зачисления.
|
|
func (c Confirmation) Validate() error { return c.SettlementAccount.Validate() }
|
|
|
|
// Rejection — отказ от приёма ценных бумаг по решению.
|
|
type Rejection struct {
|
|
Codes []string `xml:"http://nsd.ru/schemas/m2m/types Code"`
|
|
}
|
|
|
|
// Validate проверяет, что коды отказа заданы и каждый не длиннее 6 символов.
|
|
func (r Rejection) Validate() error {
|
|
if len(r.Codes) == 0 {
|
|
return fmt.Errorf("%w: Rejection без кодов отказа", ErrInvalid)
|
|
}
|
|
for _, code := range r.Codes {
|
|
if l := len(code); l < 1 || l > 6 {
|
|
return fmt.Errorf("%w: Rejection.Code длина %d, ожидается 1..6", ErrInvalid, l)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DecisionTransfer — choice решения: подтверждение или отказ.
|
|
type DecisionTransfer struct {
|
|
Rejection *Rejection `xml:"http://nsd.ru/schemas/m2m/types Rejection,omitempty"`
|
|
Confirmation *Confirmation `xml:"http://nsd.ru/schemas/m2m/types Confirmation,omitempty"`
|
|
}
|
|
|
|
// Validate проверяет, что задано ровно одно поле choice.
|
|
func (t DecisionTransfer) Validate() error {
|
|
count := 0
|
|
if t.Rejection != nil {
|
|
if err := t.Rejection.Validate(); err != nil {
|
|
return err
|
|
}
|
|
count++
|
|
}
|
|
if t.Confirmation != nil {
|
|
if err := t.Confirmation.Validate(); err != nil {
|
|
return err
|
|
}
|
|
count++
|
|
}
|
|
if count != 1 {
|
|
return fmt.Errorf("%w: DecisionTransfer задано %d полей", ErrChoice, count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RequestHeader — заголовок сообщения "Запрос на перевод M2M".
|
|
type RequestHeader struct {
|
|
GUID UUID `xml:"http://nsd.ru/schemas/m2m/types GUID"`
|
|
CreationTimestamp nsdxml.NSDDateTime `xml:"http://nsd.ru/schemas/m2m/types CreationTimestamp"`
|
|
SenderCode DeponentCode `xml:"http://nsd.ru/schemas/m2m/types SenderCode"`
|
|
ReceiverCode DeponentCode `xml:"http://nsd.ru/schemas/m2m/types ReceiverCode"`
|
|
CostInfo CostInfo `xml:"http://nsd.ru/schemas/m2m/types CostInfo"`
|
|
IIAAgreementDetails *IIAAgreementDetails `xml:"http://nsd.ru/schemas/m2m/types IIAAgreementDetails,omitempty"`
|
|
}
|
|
|
|
// Validate валидирует поля заголовка запроса.
|
|
func (h RequestHeader) Validate() error {
|
|
if err := h.GUID.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := h.SenderCode.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := h.ReceiverCode.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := h.CostInfo.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if h.IIAAgreementDetails != nil {
|
|
if err := h.IIAAgreementDetails.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DecisionHeader — заголовок сообщения "Решение по запросу M2M".
|
|
type DecisionHeader struct {
|
|
GUID UUID `xml:"http://nsd.ru/schemas/m2m/types GUID"`
|
|
CreationTimestamp nsdxml.NSDDateTime `xml:"http://nsd.ru/schemas/m2m/types CreationTimestamp"`
|
|
SenderCode DeponentCode `xml:"http://nsd.ru/schemas/m2m/types SenderCode"`
|
|
ReceiverCode DeponentCode `xml:"http://nsd.ru/schemas/m2m/types ReceiverCode"`
|
|
CostInfo CostInfo `xml:"http://nsd.ru/schemas/m2m/types CostInfo"`
|
|
}
|
|
|
|
// Validate валидирует поля заголовка решения.
|
|
func (h DecisionHeader) Validate() error {
|
|
if err := h.GUID.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := h.SenderCode.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := h.ReceiverCode.Validate(); err != nil {
|
|
return err
|
|
}
|
|
return h.CostInfo.Validate()
|
|
}
|
|
|
|
// RequestSecurity — описание одной ценной бумаги в запросе перевода.
|
|
type RequestSecurity struct {
|
|
ReferenceID ReferenceID `xml:"http://nsd.ru/schemas/m2m/types ReferenceId"`
|
|
SecurityCode SecurityCode `xml:"http://nsd.ru/schemas/m2m/types SecurityCode"`
|
|
SecurityDetails SecurityDetails `xml:"http://nsd.ru/schemas/m2m/types SecurityDetails"`
|
|
Quantity Quantity `xml:"http://nsd.ru/schemas/m2m/types Quantity"`
|
|
SettlementAccount []RequestSettlementAccount `xml:"http://nsd.ru/schemas/m2m/types SettlementAccount"`
|
|
IsolationStatus IsolationStatus `xml:"http://nsd.ru/schemas/m2m/types IsolationStatus"`
|
|
}
|
|
|
|
// Validate валидирует все поля ценной бумаги запроса.
|
|
func (s RequestSecurity) Validate() error {
|
|
if err := s.ReferenceID.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.SecurityCode.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.SecurityDetails.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.Quantity.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if len(s.SettlementAccount) == 0 {
|
|
return fmt.Errorf("%w: SettlementAccount должен содержать хотя бы один счёт", ErrInvalid)
|
|
}
|
|
for i := range s.SettlementAccount {
|
|
if err := s.SettlementAccount[i].Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return s.IsolationStatus.Validate()
|
|
}
|
|
|
|
// RequestTransferredSecurities — список переводимых ценных бумаг.
|
|
type RequestTransferredSecurities struct {
|
|
Securities []RequestSecurity `xml:"http://nsd.ru/schemas/m2m/types Security"`
|
|
}
|
|
|
|
// Validate проверяет непустоту списка и валидирует каждую запись.
|
|
func (t RequestTransferredSecurities) Validate() error {
|
|
if len(t.Securities) == 0 {
|
|
return fmt.Errorf("%w: TransferredSecurities пуст", ErrInvalid)
|
|
}
|
|
for i := range t.Securities {
|
|
if err := t.Securities[i].Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RequestData — содержательная часть запроса. IsM2M фиксировано true и
|
|
// проставляется в MarshalXML, в структуре поле не хранится.
|
|
type RequestData struct {
|
|
InvestorInformation InvestorInformation `xml:"http://nsd.ru/schemas/m2m/types InvestorInformation"`
|
|
TransferringDepository SettlementRequisites `xml:"http://nsd.ru/schemas/m2m/types TransferringDepository"`
|
|
ReceivingDepository SettlementRequisites `xml:"http://nsd.ru/schemas/m2m/types ReceivingDepository"`
|
|
TransferredSecurities RequestTransferredSecurities `xml:"http://nsd.ru/schemas/m2m/types TransferredSecurities"`
|
|
}
|
|
|
|
// requestDataXML — внутренний alias с явным полем IsM2M для XML-кодека.
|
|
type requestDataXML struct {
|
|
IsM2M bool `xml:"http://nsd.ru/schemas/m2m/types IsM2M"`
|
|
InvestorInformation InvestorInformation `xml:"http://nsd.ru/schemas/m2m/types InvestorInformation"`
|
|
TransferringDepository SettlementRequisites `xml:"http://nsd.ru/schemas/m2m/types TransferringDepository"`
|
|
ReceivingDepository SettlementRequisites `xml:"http://nsd.ru/schemas/m2m/types ReceivingDepository"`
|
|
TransferredSecurities RequestTransferredSecurities `xml:"http://nsd.ru/schemas/m2m/types TransferredSecurities"`
|
|
}
|
|
|
|
// MarshalXML всегда эмитирует IsM2M=true первым элементом.
|
|
func (d RequestData) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
|
return e.EncodeElement(requestDataXML{
|
|
IsM2M: true,
|
|
InvestorInformation: d.InvestorInformation,
|
|
TransferringDepository: d.TransferringDepository,
|
|
ReceivingDepository: d.ReceivingDepository,
|
|
TransferredSecurities: d.TransferredSecurities,
|
|
}, start)
|
|
}
|
|
|
|
// UnmarshalXML принимает и отбрасывает IsM2M, не вынося его в структуру.
|
|
func (d *RequestData) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {
|
|
var x requestDataXML
|
|
if err := dec.DecodeElement(&x, &start); err != nil {
|
|
return err
|
|
}
|
|
d.InvestorInformation = x.InvestorInformation
|
|
d.TransferringDepository = x.TransferringDepository
|
|
d.ReceivingDepository = x.ReceivingDepository
|
|
d.TransferredSecurities = x.TransferredSecurities
|
|
return nil
|
|
}
|
|
|
|
// Validate валидирует содержательную часть запроса.
|
|
func (d RequestData) Validate() error {
|
|
if err := d.InvestorInformation.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := d.TransferringDepository.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := d.ReceivingDepository.Validate(); err != nil {
|
|
return err
|
|
}
|
|
return d.TransferredSecurities.Validate()
|
|
}
|
|
|
|
// DecisionSecurity — решение по одной ценной бумаге из запроса.
|
|
type DecisionSecurity struct {
|
|
ReferenceID ReferenceID `xml:"http://nsd.ru/schemas/m2m/types ReferenceId"`
|
|
TransferDecision DecisionTransfer `xml:"http://nsd.ru/schemas/m2m/types TransferDecision"`
|
|
}
|
|
|
|
// Validate валидирует ReferenceID и решение по бумаге.
|
|
func (s DecisionSecurity) Validate() error {
|
|
if err := s.ReferenceID.Validate(); err != nil {
|
|
return err
|
|
}
|
|
return s.TransferDecision.Validate()
|
|
}
|
|
|
|
// DecisionData — содержательная часть решения.
|
|
type DecisionData struct {
|
|
ReceivingDepository SettlementRequisites `xml:"http://nsd.ru/schemas/m2m/types ReceivingDepository"`
|
|
Securities []DecisionSecurity `xml:"http://nsd.ru/schemas/m2m/types Security"`
|
|
}
|
|
|
|
// Validate проверяет получателя и каждое решение по бумагам.
|
|
func (d DecisionData) Validate() error {
|
|
if err := d.ReceivingDepository.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if len(d.Securities) == 0 {
|
|
return fmt.Errorf("%w: DecisionData без Security", ErrInvalid)
|
|
}
|
|
for i := range d.Securities {
|
|
if err := d.Securities[i].Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Response — элемент комментария НРД к обработке сообщения.
|
|
type Response struct {
|
|
ReferenceID *ReferenceID `xml:"http://nsd.ru/schemas/m2m/types ReferenceId,omitempty"`
|
|
Code string `xml:"http://nsd.ru/schemas/m2m/types Code"`
|
|
Text string `xml:"http://nsd.ru/schemas/m2m/types Text,omitempty"`
|
|
}
|
|
|
|
// Validate проверяет ссылку (если задана), код и текст.
|
|
func (r Response) Validate() error {
|
|
if r.ReferenceID != nil {
|
|
if err := r.ReferenceID.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if l := len(r.Code); l < 1 || l > 5 {
|
|
return fmt.Errorf("%w: Response.Code длина %d, ожидается 1..5", ErrInvalid, l)
|
|
}
|
|
if l := len(r.Text); l > 1024 {
|
|
return fmt.Errorf("%w: Response.Text длина %d, ожидается не более 1024", ErrInvalid, l)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NSDInfo — обёртка над списком комментариев НРД.
|
|
type NSDInfo struct {
|
|
Info []Response `xml:"http://nsd.ru/schemas/m2m/types Info"`
|
|
}
|
|
|
|
// Validate валидирует каждый Response.
|
|
func (n NSDInfo) Validate() error {
|
|
for i := range n.Info {
|
|
if err := n.Info[i].Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HandbookRusName — наименования участника на русском языке.
|
|
type HandbookRusName struct {
|
|
FullName string `xml:"http://nsd.ru/schemas/m2m/types FullName"`
|
|
ShortName string `xml:"http://nsd.ru/schemas/m2m/types ShortName,omitempty"`
|
|
DisplayName string `xml:"http://nsd.ru/schemas/m2m/types DisplayName"`
|
|
}
|
|
|
|
// HandbookEngName — наименования участника на английском языке.
|
|
type HandbookEngName = HandbookRusName
|
|
|
|
// HandbookNames — пара RUS+ENG наименований.
|
|
type HandbookNames struct {
|
|
Rus HandbookRusName `xml:"http://nsd.ru/schemas/m2m/types Rus"`
|
|
Eng *HandbookEngName `xml:"http://nsd.ru/schemas/m2m/types Eng,omitempty"`
|
|
}
|
|
|
|
// DepositoryPlaces — место хранения (депозитарий).
|
|
type DepositoryPlaces struct {
|
|
ParticipantCode DeponentCode `xml:"http://nsd.ru/schemas/m2m/types ParticipantCode"`
|
|
}
|
|
|
|
// BrokerPlaces — место хранения (брокер).
|
|
type BrokerPlaces struct {
|
|
ParticipantCode DeponentCode `xml:"http://nsd.ru/schemas/m2m/types ParticipantCode"`
|
|
}
|
|
|
|
// HandbookParticipant — запись в справочнике участников.
|
|
type HandbookParticipant struct {
|
|
INN OrganizationINN `xml:"http://nsd.ru/schemas/m2m/types INN"`
|
|
Names HandbookNames `xml:"http://nsd.ru/schemas/m2m/types Names"`
|
|
DepositoryPlace *DepositoryPlaces `xml:"http://nsd.ru/schemas/m2m/types DepositoryPlace,omitempty"`
|
|
BrokerPlace *BrokerPlaces `xml:"http://nsd.ru/schemas/m2m/types BrokerPlace,omitempty"`
|
|
}
|
|
|
|
// Validate валидирует ИНН и коды участников (если заданы).
|
|
func (p HandbookParticipant) Validate() error {
|
|
if err := p.INN.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if p.DepositoryPlace != nil {
|
|
if err := p.DepositoryPlace.ParticipantCode.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if p.BrokerPlace != nil {
|
|
if err := p.BrokerPlace.ParticipantCode.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HandbookParticipants — обёртка над списком участников.
|
|
type HandbookParticipants struct {
|
|
Participants []HandbookParticipant `xml:"http://nsd.ru/schemas/m2m/types Participant"`
|
|
}
|
|
|
|
// Place — место расчётов в справочнике.
|
|
type Place struct {
|
|
INN OrganizationINN `xml:"http://nsd.ru/schemas/m2m/types INN"`
|
|
ShortName string `xml:"http://nsd.ru/schemas/m2m/types ShortName"`
|
|
DisplayName string `xml:"http://nsd.ru/schemas/m2m/types DisplayName"`
|
|
}
|
|
|
|
// SettlementPlaces — обёртка над списком мест расчётов.
|
|
type SettlementPlaces struct {
|
|
Places []Place `xml:"http://nsd.ru/schemas/m2m/types Place"`
|
|
}
|
|
|
|
// M2MTransferRequest — корневой элемент сообщения "Запрос на перевод M2M".
|
|
type M2MTransferRequest struct {
|
|
XMLName xml.Name `xml:"http://nsd.ru/schemas/m2m/request M2MTransferRequest"`
|
|
Header RequestHeader `xml:"http://nsd.ru/schemas/m2m/request Header"`
|
|
Data RequestData `xml:"http://nsd.ru/schemas/m2m/request Data"`
|
|
NSDInfo *NSDInfo `xml:"http://nsd.ru/schemas/m2m/request NSDInfo,omitempty"`
|
|
}
|
|
|
|
// Validate каскадно валидирует заголовок, тело и комментарии НРД.
|
|
func (m M2MTransferRequest) Validate() error {
|
|
if err := m.Header.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := m.Data.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if m.NSDInfo != nil {
|
|
return m.NSDInfo.Validate()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// M2MTransferDecision — корневой элемент сообщения "Решение по запросу M2M".
|
|
type M2MTransferDecision struct {
|
|
XMLName xml.Name `xml:"http://nsd.ru/schemas/m2m/decision M2MTransferDecision"`
|
|
Header DecisionHeader `xml:"http://nsd.ru/schemas/m2m/decision Header"`
|
|
Data DecisionData `xml:"http://nsd.ru/schemas/m2m/decision Data"`
|
|
NSDInfo *NSDInfo `xml:"http://nsd.ru/schemas/m2m/decision NSDInfo,omitempty"`
|
|
}
|
|
|
|
// Validate каскадно валидирует заголовок, тело и комментарии НРД.
|
|
func (m M2MTransferDecision) Validate() error {
|
|
if err := m.Header.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := m.Data.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if m.NSDInfo != nil {
|
|
return m.NSDInfo.Validate()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// M2MTransferResponse — служебный ответ НРД на сообщение M2M.
|
|
// GUID/StatusCode/Response объявлены локально в M2MTransferResponse.xsd
|
|
// (elementFormDefault="qualified") — namespace элементов response, не
|
|
// types, хотя их типы из types.
|
|
type M2MTransferResponse struct {
|
|
XMLName xml.Name `xml:"http://nsd.ru/schemas/m2m/response M2MTransferResponse"`
|
|
GUID UUID `xml:"http://nsd.ru/schemas/m2m/response GUID"`
|
|
StatusCode StatusCode `xml:"http://nsd.ru/schemas/m2m/response StatusCode"`
|
|
Responses []Response `xml:"http://nsd.ru/schemas/m2m/response Response"`
|
|
}
|
|
|
|
// Validate валидирует GUID, статус и каждый Response.
|
|
func (m M2MTransferResponse) Validate() error {
|
|
if err := m.GUID.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := m.StatusCode.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if len(m.Responses) == 0 {
|
|
return fmt.Errorf("%w: M2MTransferResponse без Response", ErrInvalid)
|
|
}
|
|
for i := range m.Responses {
|
|
if err := m.Responses[i].Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// M2MTransferHandbook — справочник участников M2M. CreationTimestamp,
|
|
// SettlementPlaces, Participants — локальные элементы из
|
|
// M2MTransferHandbook.xsd, namespace handbook.
|
|
type M2MTransferHandbook struct {
|
|
XMLName xml.Name `xml:"http://nsd.ru/schemas/m2m/handbook M2MTransferHandbook"`
|
|
CreationTimestamp nsdxml.NSDDateTime `xml:"http://nsd.ru/schemas/m2m/handbook CreationTimestamp"`
|
|
SettlementPlaces SettlementPlaces `xml:"http://nsd.ru/schemas/m2m/handbook SettlementPlaces"`
|
|
Participants HandbookParticipants `xml:"http://nsd.ru/schemas/m2m/handbook Participants"`
|
|
}
|
|
|
|
// Validate валидирует каждого участника и каждое место расчётов.
|
|
func (m M2MTransferHandbook) Validate() error {
|
|
for i := range m.SettlementPlaces.Places {
|
|
if err := m.SettlementPlaces.Places[i].INN.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i := range m.Participants.Participants {
|
|
if err := m.Participants.Participants[i].Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// M2MTransferHandbookRequest — запрос актуального справочника M2M.
|
|
// Тело пустое (пустой complexType в XSD).
|
|
type M2MTransferHandbookRequest struct {
|
|
XMLName xml.Name `xml:"http://nsd.ru/schemas/m2m/handbook/request M2MTransferHandbookRequest"`
|
|
}
|
|
|
|
// Validate всегда возвращает nil — содержимое отсутствует по XSD.
|
|
func (m M2MTransferHandbookRequest) Validate() error { return nil }
|
|
|
|
// M2MTransferParticipantForm — анкета участника M2M. CreationTimestamp
|
|
// и Participant — локальные элементы из M2MTransferParticipantForm.xsd,
|
|
// namespace participant/form.
|
|
type M2MTransferParticipantForm struct {
|
|
XMLName xml.Name `xml:"http://nsd.ru/schemas/m2m/participant/form M2MTransferParticipantForm"`
|
|
CreationTimestamp nsdxml.NSDDateTime `xml:"http://nsd.ru/schemas/m2m/participant/form CreationTimestamp"`
|
|
Participant HandbookParticipant `xml:"http://nsd.ru/schemas/m2m/participant/form Participant"`
|
|
}
|
|
|
|
// Validate валидирует участника.
|
|
func (m M2MTransferParticipantForm) Validate() error {
|
|
return m.Participant.Validate()
|
|
}
|