Анкета участника M2M: сформировать → проверить → отправить в НРД (обычный канал)
Требование НРД при подключении к сервису M2M (МОСТ): зарегистрировать организацию в справочнике участников анкетой M2MTransferParticipantForm, иначе сервис отбивает запросы кодом M2M14. Регистрация идёт ОБЫЧНЫМ каналом (письмом на M2MOST@nsd.ru с вложением), не через M2M-шлюз. - Новая страница /admin/setup/participant-form (participant_form.go): предзаполнение из автоопределения DEPO (ИНН, наименования, код депозитария), правка оператором, валидация (формат ИНН/кодов + m2m.HandbookParticipant.Validate), предпросмотр анкеты, «Скачать» (windows-1251) и «Отправить в НРД» письмом с вложением-анкетой. - Генерация XML строкой-шаблоном по образцу НРД (namespace participant/form, CreationTimestamp по МСК, блоки Eng/BrokerPlace — опциональны), кодировка windows-1251 через nsdxml.EncodeWindows1251; экранирование текста только & < >. - Почта: выделен транспорт smtpDeliver из sendSMTP; добавлены buildMIMEAttach (multipart/mixed + base64-вложение) и sendSMTPAttach. - Тесты: валидация, генерация XML (Eng/BrokerPlace), cp1251 round-trip, MIME-вложение. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XG3zNMMf2LMoDpXo22mwMX
This commit is contained in:
@@ -61,12 +61,18 @@ func sendSMTP(s SMTPSettings, subject, body string) error {
|
|||||||
if len(rcpts) == 0 {
|
if len(rcpts) == 0 {
|
||||||
return fmt.Errorf("не заданы получатели (To)")
|
return fmt.Errorf("не заданы получатели (To)")
|
||||||
}
|
}
|
||||||
|
return smtpDeliver(s, rcpts, buildMIME(s.From, rcpts, subject, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// smtpDeliver — транспорт SMTP: соединение, TLS, аутентификация и отправка
|
||||||
|
// готового MIME-сообщения. Выделено из sendSMTP для переиспользования в
|
||||||
|
// письмах с вложением (sendSMTPAttach, participant_form.go).
|
||||||
|
func smtpDeliver(s SMTPSettings, rcpts []string, msg []byte) error {
|
||||||
port := s.Port
|
port := s.Port
|
||||||
if port == 0 {
|
if port == 0 {
|
||||||
port = 587
|
port = 587
|
||||||
}
|
}
|
||||||
addr := net.JoinHostPort(s.Host, strconv.Itoa(port))
|
addr := net.JoinHostPort(s.Host, strconv.Itoa(port))
|
||||||
msg := buildMIME(s.From, rcpts, subject, body)
|
|
||||||
|
|
||||||
dialer := &net.Dialer{Timeout: 15 * time.Second}
|
dialer := &net.Dialer{Timeout: 15 * time.Second}
|
||||||
var client *smtp.Client
|
var client *smtp.Client
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
package lkgateway
|
||||||
|
|
||||||
|
// participant_form.go — анкета участника сервиса M2M (M2MTransferParticipantForm).
|
||||||
|
// Требование НРД при подключении: зарегистрировать нашу организацию в справочнике
|
||||||
|
// участников M2M, иначе сервис МОСТ отбивает запросы кодом M2M14 «Код ЭДО НРД
|
||||||
|
// отправителя отсутствует в справочнике участников M2M». Здесь bj формирует
|
||||||
|
// заполненную анкету (предзаполнение из автоопределения DEPO), даёт проверить
|
||||||
|
// (валидация + показ XML в windows-1251) и предлагает отправить её в НРД обычным
|
||||||
|
// каналом — письмом на M2MOST@nsd.ru с вложением (не через M2M-шлюз).
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
||||||
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// participantFormRecipient — адрес НРД для регистрации участника M2M (обычный канал).
|
||||||
|
const participantFormRecipient = "M2MOST@nsd.ru"
|
||||||
|
|
||||||
|
var pfINNRe = regexp.MustCompile(`^\d{10}$`)
|
||||||
|
var pfCodeRe = regexp.MustCompile(`^MC\d{10}$`)
|
||||||
|
|
||||||
|
// participantFormData — поля анкеты (юридические реквизиты организации).
|
||||||
|
type participantFormData struct {
|
||||||
|
INN string
|
||||||
|
FullName string
|
||||||
|
ShortName string
|
||||||
|
DisplayName string
|
||||||
|
EngFull string
|
||||||
|
EngShort string
|
||||||
|
EngDisplay string
|
||||||
|
DepoCode string // DepositoryPlace/ParticipantCode (MC+10)
|
||||||
|
BrokerCode string // BrokerPlace/ParticipantCode (MC+10), опционально
|
||||||
|
}
|
||||||
|
|
||||||
|
// participantFormPrefill — предзаполнение из автоопределения DEPO и настроек НРД.
|
||||||
|
func participantFormPrefill(rc *RuntimeConfig) participantFormData {
|
||||||
|
var d participantFormData
|
||||||
|
if r := depoDetectResult(); r != nil {
|
||||||
|
d.INN = strings.TrimSpace(r.OrgINN)
|
||||||
|
d.FullName = strings.TrimSpace(r.OrgName)
|
||||||
|
d.ShortName = strings.TrimSpace(r.OrgShort)
|
||||||
|
d.DisplayName = strings.TrimSpace(r.OrgShort)
|
||||||
|
d.EngFull = strings.TrimSpace(r.OrgEng)
|
||||||
|
if d.DepoCode == "" {
|
||||||
|
d.DepoCode = strings.TrimSpace(r.DeponentCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.DepoCode == "" && rc != nil {
|
||||||
|
d.DepoCode = strings.TrimSpace(rc.Snapshot().NSD.DeponentCode)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// participantFormValidate — проверка обязательных полей и форматов. Пустой срез — ок.
|
||||||
|
func participantFormValidate(d participantFormData) []string {
|
||||||
|
var errs []string
|
||||||
|
if !pfINNRe.MatchString(strings.TrimSpace(d.INN)) {
|
||||||
|
errs = append(errs, "ИНН: нужно 10 цифр (юридическое лицо)")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(d.FullName) == "" {
|
||||||
|
errs = append(errs, "Полное наименование: обязательно")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(d.DisplayName) == "" {
|
||||||
|
errs = append(errs, "Отображаемое наименование (DisplayName): обязательно")
|
||||||
|
}
|
||||||
|
if !pfCodeRe.MatchString(strings.TrimSpace(d.DepoCode)) {
|
||||||
|
errs = append(errs, "Код депозитария (DepositoryPlace): формат MC + 10 цифр")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(d.BrokerCode) != "" && !pfCodeRe.MatchString(strings.TrimSpace(d.BrokerCode)) {
|
||||||
|
errs = append(errs, "Код брокера (BrokerPlace): формат MC + 10 цифр (или оставьте пустым)")
|
||||||
|
}
|
||||||
|
// Структурная проверка типами НРД (ИНН/коды).
|
||||||
|
p := m2m.HandbookParticipant{
|
||||||
|
INN: m2m.OrganizationINN(strings.TrimSpace(d.INN)),
|
||||||
|
Names: m2m.HandbookNames{Rus: m2m.HandbookRusName{FullName: d.FullName, ShortName: d.ShortName, DisplayName: d.DisplayName}},
|
||||||
|
}
|
||||||
|
if c := strings.TrimSpace(d.DepoCode); c != "" {
|
||||||
|
p.DepositoryPlace = &m2m.DepositoryPlaces{ParticipantCode: m2m.DeponentCode(c)}
|
||||||
|
}
|
||||||
|
if c := strings.TrimSpace(d.BrokerCode); c != "" {
|
||||||
|
p.BrokerPlace = &m2m.BrokerPlaces{ParticipantCode: m2m.DeponentCode(c)}
|
||||||
|
}
|
||||||
|
if err := p.Validate(); err != nil && len(errs) == 0 {
|
||||||
|
errs = append(errs, "Проверка НРД: "+err.Error())
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// pfx — экранирование текста XML-элемента (только & < >); кавычки в тексте
|
||||||
|
// оставляем литеральными, как в образце анкеты НРД.
|
||||||
|
var pfXMLEsc = strings.NewReplacer("&", "&", "<", "<", ">", ">")
|
||||||
|
|
||||||
|
func pfx(s string) string { return pfXMLEsc.Replace(strings.TrimSpace(s)) }
|
||||||
|
|
||||||
|
// participantFormXML — анкета в виде XML-строки (UTF-8) по образцу НРД
|
||||||
|
// (M2MTransferParticipantForm.xsd). CreationTimestamp — по МСК.
|
||||||
|
func participantFormXML(d participantFormData) string {
|
||||||
|
msk := time.FixedZone("МСК", 3*3600)
|
||||||
|
ts := time.Now().In(msk).Format("2006-01-02T15:04:05") + "(МСК)"
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(`<?xml version="1.0" encoding="windows-1251"?>` + "\n")
|
||||||
|
b.WriteString(`<pf:M2MTransferParticipantForm xmlns:m2m="http://nsd.ru/schemas/m2m/types" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:pf="http://nsd.ru/schemas/m2m/participant/form" xsi:schemaLocation="http://nsd.ru/schemas/m2m/participant/form M2MTransferParticipantForm.xsd">` + "\n")
|
||||||
|
b.WriteString("\t<pf:CreationTimestamp>" + ts + "</pf:CreationTimestamp>\n")
|
||||||
|
b.WriteString("\t<pf:Participant>\n")
|
||||||
|
b.WriteString("\t\t<m2m:INN>" + pfx(d.INN) + "</m2m:INN>\n")
|
||||||
|
b.WriteString("\t\t<m2m:Names>\n")
|
||||||
|
b.WriteString("\t\t\t<m2m:Rus>\n")
|
||||||
|
b.WriteString("\t\t\t\t<m2m:FullName>" + pfx(d.FullName) + "</m2m:FullName>\n")
|
||||||
|
if strings.TrimSpace(d.ShortName) != "" {
|
||||||
|
b.WriteString("\t\t\t\t<m2m:ShortName>" + pfx(d.ShortName) + "</m2m:ShortName>\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\t\t\t\t<m2m:DisplayName>" + pfx(d.DisplayName) + "</m2m:DisplayName>\n")
|
||||||
|
b.WriteString("\t\t\t</m2m:Rus>\n")
|
||||||
|
if strings.TrimSpace(d.EngFull) != "" || strings.TrimSpace(d.EngDisplay) != "" {
|
||||||
|
b.WriteString("\t\t\t<m2m:Eng>\n")
|
||||||
|
b.WriteString("\t\t\t\t<m2m:FullName>" + pfx(d.EngFull) + "</m2m:FullName>\n")
|
||||||
|
if strings.TrimSpace(d.EngShort) != "" {
|
||||||
|
b.WriteString("\t\t\t\t<m2m:ShortName>" + pfx(d.EngShort) + "</m2m:ShortName>\n")
|
||||||
|
}
|
||||||
|
disp := d.EngDisplay
|
||||||
|
if strings.TrimSpace(disp) == "" {
|
||||||
|
disp = d.EngShort
|
||||||
|
}
|
||||||
|
b.WriteString("\t\t\t\t<m2m:DisplayName>" + pfx(disp) + "</m2m:DisplayName>\n")
|
||||||
|
b.WriteString("\t\t\t</m2m:Eng>\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\t\t</m2m:Names>\n")
|
||||||
|
b.WriteString("\t\t<m2m:DepositoryPlace>\n")
|
||||||
|
b.WriteString("\t\t\t<m2m:ParticipantCode>" + pfx(d.DepoCode) + "</m2m:ParticipantCode>\n")
|
||||||
|
b.WriteString("\t\t</m2m:DepositoryPlace>\n")
|
||||||
|
if strings.TrimSpace(d.BrokerCode) != "" {
|
||||||
|
b.WriteString("\t\t<m2m:BrokerPlace>\n")
|
||||||
|
b.WriteString("\t\t\t<m2m:ParticipantCode>" + pfx(d.BrokerCode) + "</m2m:ParticipantCode>\n")
|
||||||
|
b.WriteString("\t\t</m2m:BrokerPlace>\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\t</pf:Participant>\n")
|
||||||
|
b.WriteString("</pf:M2MTransferParticipantForm>\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// participantFormBytes — анкета в windows-1251 (как требует НРД: кодировка из пролога).
|
||||||
|
func participantFormBytes(d participantFormData) ([]byte, error) {
|
||||||
|
return nsdxml.EncodeWindows1251([]byte(participantFormXML(d)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// participantEmailBody — текст письма на M2MOST@nsd.ru (обычный канал регистрации).
|
||||||
|
func participantEmailBody(d participantFormData) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("Добрый день!\r\n\r\n")
|
||||||
|
b.WriteString("Просим зарегистрировать нашу организацию в справочнике участников сервиса M2M (МОСТ).\r\n")
|
||||||
|
b.WriteString("Заполненная анкета участника (M2MTransferParticipantForm) во вложении.\r\n\r\n")
|
||||||
|
b.WriteString("Реквизиты:\r\n")
|
||||||
|
b.WriteString(" - Депозитарный код участника: " + strings.TrimSpace(d.DepoCode) + "\r\n")
|
||||||
|
if strings.TrimSpace(d.BrokerCode) != "" {
|
||||||
|
b.WriteString(" - Брокерский код участника: " + strings.TrimSpace(d.BrokerCode) + "\r\n")
|
||||||
|
}
|
||||||
|
b.WriteString(" - Полное наименование: " + strings.TrimSpace(d.FullName) + "\r\n")
|
||||||
|
b.WriteString(" - ИНН: " + strings.TrimSpace(d.INN) + "\r\n\r\n")
|
||||||
|
b.WriteString("После регистрации просим подтвердить.\r\n\r\n")
|
||||||
|
b.WriteString("С уважением,\r\n" + strings.TrimSpace(d.ShortName) + "\r\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildMIMEAttach — MIME multipart/mixed: текст письма (UTF-8) + вложение (base64).
|
||||||
|
func buildMIMEAttach(from string, to []string, subject, body, filename string, attach []byte, ctype string) []byte {
|
||||||
|
boundary := fmt.Sprintf("bjpart_%d", time.Now().UnixNano())
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("From: " + from + "\r\n")
|
||||||
|
b.WriteString("To: " + strings.Join(to, ", ") + "\r\n")
|
||||||
|
b.WriteString("Subject: " + encodeHeaderWord(subject) + "\r\n")
|
||||||
|
b.WriteString("MIME-Version: 1.0\r\n")
|
||||||
|
b.WriteString("Content-Type: multipart/mixed; boundary=\"" + boundary + "\"\r\n\r\n")
|
||||||
|
b.WriteString("--" + boundary + "\r\n")
|
||||||
|
b.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
|
||||||
|
b.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n")
|
||||||
|
b.WriteString(body + "\r\n")
|
||||||
|
b.WriteString("--" + boundary + "\r\n")
|
||||||
|
b.WriteString("Content-Type: " + ctype + "; name=\"" + filename + "\"\r\n")
|
||||||
|
b.WriteString("Content-Transfer-Encoding: base64\r\n")
|
||||||
|
b.WriteString("Content-Disposition: attachment; filename=\"" + filename + "\"\r\n\r\n")
|
||||||
|
enc := base64.StdEncoding.EncodeToString(attach)
|
||||||
|
for i := 0; i < len(enc); i += 76 {
|
||||||
|
j := i + 76
|
||||||
|
if j > len(enc) {
|
||||||
|
j = len(enc)
|
||||||
|
}
|
||||||
|
b.WriteString(enc[i:j] + "\r\n")
|
||||||
|
}
|
||||||
|
b.WriteString("--" + boundary + "--\r\n")
|
||||||
|
return []byte(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendSMTPAttach — письмо с вложением (переиспользует транспорт smtpDeliver).
|
||||||
|
func sendSMTPAttach(s SMTPSettings, subject, body, filename string, attach []byte, ctype string) error {
|
||||||
|
if strings.TrimSpace(s.Host) == "" {
|
||||||
|
return fmt.Errorf("не задан хост SMTP")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.From) == "" {
|
||||||
|
return fmt.Errorf("не задан отправитель (From)")
|
||||||
|
}
|
||||||
|
rcpts := splitRecipients(s.To)
|
||||||
|
if len(rcpts) == 0 {
|
||||||
|
return fmt.Errorf("не заданы получатели (To)")
|
||||||
|
}
|
||||||
|
return smtpDeliver(s, rcpts, buildMIMEAttach(s.From, rcpts, subject, body, filename, attach, ctype))
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerParticipantForm — страница «Регистрация участника M2M»: форма, проверка,
|
||||||
|
// скачивание анкеты, отправка письмом на M2MOST@nsd.ru.
|
||||||
|
func registerParticipantForm(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||||
|
mux.HandleFunc("/admin/setup/participant-form", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d := participantFormPrefill(rc)
|
||||||
|
action := ""
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
_ = r.ParseForm()
|
||||||
|
action = r.FormValue("action")
|
||||||
|
d = participantFormData{
|
||||||
|
INN: r.FormValue("inn"), FullName: r.FormValue("full"), ShortName: r.FormValue("short"),
|
||||||
|
DisplayName: r.FormValue("display"), EngFull: r.FormValue("eng_full"), EngShort: r.FormValue("eng_short"),
|
||||||
|
EngDisplay: r.FormValue("eng_display"), DepoCode: r.FormValue("depo"), BrokerCode: r.FormValue("broker"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errs := participantFormValidate(d)
|
||||||
|
|
||||||
|
if action == "download" {
|
||||||
|
if len(errs) > 0 {
|
||||||
|
http.Error(w, "анкета не прошла проверку: "+strings.Join(errs, "; "), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, err := participantFormBytes(d)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "кодировка windows-1251: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/xml; charset=windows-1251")
|
||||||
|
w.Header().Set("Content-Disposition", `attachment; filename="M2MTransferParticipantForm.xml"`)
|
||||||
|
_, _ = w.Write(body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var flash, flashKind string
|
||||||
|
if action == "send" {
|
||||||
|
to := strings.TrimSpace(r.FormValue("to"))
|
||||||
|
if to == "" {
|
||||||
|
to = participantFormRecipient
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(errs) > 0:
|
||||||
|
flash, flashKind = "Анкета не прошла проверку — исправьте поля ниже.", "err"
|
||||||
|
default:
|
||||||
|
cfg := rc.Snapshot()
|
||||||
|
s := cfg.Alerts.SMTP
|
||||||
|
if !s.Enabled || strings.TrimSpace(s.Host) == "" {
|
||||||
|
flash, flashKind = "SMTP не настроен (раздел «Почта») — отправить письмо нельзя. Пока скачайте анкету и отправьте вручную.", "err"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.To = to
|
||||||
|
body, encErr := participantFormBytes(d)
|
||||||
|
if encErr != nil {
|
||||||
|
flash, flashKind = "кодировка windows-1251: "+encErr.Error(), "err"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
subject := "Регистрация участника сервиса M2M (МОСТ), код " + strings.TrimSpace(d.DepoCode)
|
||||||
|
if err := sendSMTPAttach(s, subject, participantEmailBody(d), "M2MTransferParticipantForm.xml", body, "application/xml; charset=windows-1251"); err != nil {
|
||||||
|
flash, flashKind = "письмо не отправлено: "+err.Error(), "err"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
flash, flashKind = "Анкета отправлена письмом на "+to+" (с вложением). Ждём подтверждения регистрации от НРД.", "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlPreview := participantFormXML(d)
|
||||||
|
participantFormRender(w, d, errs, action, flash, flashKind, xmlPreview)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func participantFormRender(w http.ResponseWriter, d participantFormData, errs []string, action, flash, flashKind, xmlPreview string) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
inp := func(name, label, val, ph string) string {
|
||||||
|
return `<label>` + label + `<input name="` + name + `" value="` + html.EscapeString(val) + `" placeholder="` + ph + `"></label>`
|
||||||
|
}
|
||||||
|
var vErr string
|
||||||
|
if (action == "preview" || action == "send") && len(errs) > 0 {
|
||||||
|
vErr = `<div class="bad"><b>Проверка не пройдена:</b><ul><li>` + strings.Join(escapeAll(errs), "</li><li>") + `</li></ul></div>`
|
||||||
|
} else if action == "preview" {
|
||||||
|
vErr = `<div class="ok">Проверка пройдена — анкета корректна.</div>`
|
||||||
|
}
|
||||||
|
var flashHTML string
|
||||||
|
if flash != "" {
|
||||||
|
cls := "ok"
|
||||||
|
if flashKind == "err" {
|
||||||
|
cls = "bad"
|
||||||
|
}
|
||||||
|
flashHTML = `<div class="` + cls + `">` + html.EscapeString(flash) + `</div>`
|
||||||
|
}
|
||||||
|
page := `<!doctype html><html lang="ru"><head><meta charset="utf-8"><title>Регистрация участника M2M</title>
|
||||||
|
<style>body{font:14px/1.5 system-ui,Arial;margin:24px;max-width:900px;color:#111}
|
||||||
|
h1{font-size:20px}label{display:block;margin:8px 0}label input{display:block;width:100%;padding:6px;margin-top:2px;box-sizing:border-box}
|
||||||
|
.row{display:flex;gap:16px}.row>label{flex:1}fieldset{border:1px solid #ccc;border-radius:8px;margin:14px 0;padding:10px 14px}
|
||||||
|
legend{color:#555;padding:0 6px}.btns{margin:16px 0;display:flex;gap:10px;flex-wrap:wrap}
|
||||||
|
button{padding:8px 16px;border:1px solid #888;border-radius:6px;background:#f3f3f3;cursor:pointer}
|
||||||
|
button.primary{background:#1763cc;color:#fff;border-color:#1763cc}
|
||||||
|
pre{background:#f6f8fa;border:1px solid #ddd;border-radius:8px;padding:12px;overflow:auto;white-space:pre-wrap}
|
||||||
|
.ok{background:#e7f6e7;border:1px solid #58a858;padding:8px 12px;border-radius:6px;margin:10px 0}
|
||||||
|
.bad{background:#fdecec;border:1px solid #d66;padding:8px 12px;border-radius:6px;margin:10px 0}
|
||||||
|
.note{color:#555;font-size:13px}</style></head><body>
|
||||||
|
<h1>Регистрация участника сервиса M2M (МОСТ)</h1>
|
||||||
|
<p class="note">Требование НРД при подключении: зарегистрировать организацию в справочнике участников M2M.
|
||||||
|
Без регистрации сервис отбивает запросы кодом <b>M2M14</b>. Отправляется <b>обычным каналом</b> — письмом
|
||||||
|
на <code>` + participantFormRecipient + `</code> с вложенной анкетой (не через M2M-шлюз). Поля предзаполнены
|
||||||
|
из автоопределения DEPO — проверьте юридические реквизиты.</p>
|
||||||
|
` + flashHTML + vErr + `
|
||||||
|
<form method="post">
|
||||||
|
<fieldset><legend>Организация</legend>
|
||||||
|
` + inp("inn", "ИНН (10 цифр)", d.INN, "7700000000") + `
|
||||||
|
` + inp("full", "Полное наименование (по уставу)", d.FullName, "") + `
|
||||||
|
<div class="row">` + inp("short", "Сокращённое", d.ShortName, "") + inp("display", "Отображаемое (DisplayName)", d.DisplayName, "") + `</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset><legend>Английские наименования (опционально)</legend>
|
||||||
|
` + inp("eng_full", "Full name", d.EngFull, "") + `
|
||||||
|
<div class="row">` + inp("eng_short", "Short name", d.EngShort, "") + inp("eng_display", "Display name", d.EngDisplay, "") + `</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset><legend>Коды участника (MC + 10 цифр)</legend>
|
||||||
|
<div class="row">` + inp("depo", "Код депозитария (DepositoryPlace)", d.DepoCode, "MC0000000000") + inp("broker", "Код брокера (BrokerPlace, если брокер)", d.BrokerCode, "оставьте пустым, если не брокер") + `</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset><legend>Отправка</legend>
|
||||||
|
` + inp("to", "Кому (НРД)", participantFormRecipient, participantFormRecipient) + `
|
||||||
|
</fieldset>
|
||||||
|
<div class="btns">
|
||||||
|
<button type="submit" name="action" value="preview">Проверить</button>
|
||||||
|
<button type="submit" name="action" value="download" formtarget="_blank">Скачать анкету (XML)</button>
|
||||||
|
<button type="submit" name="action" value="send" class="primary">Отправить в НРД (письмом)</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<h3>Предпросмотр анкеты (windows-1251)</h3>
|
||||||
|
<pre>` + html.EscapeString(xmlPreview) + `</pre>
|
||||||
|
</body></html>`
|
||||||
|
_, _ = w.Write([]byte(page))
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeAll(ss []string) []string {
|
||||||
|
out := make([]string, len(ss))
|
||||||
|
for i, s := range ss {
|
||||||
|
out[i] = html.EscapeString(s)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package lkgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validPF() participantFormData {
|
||||||
|
return participantFormData{
|
||||||
|
INN: "7703807489", FullName: `ООО "Фонтвьель"`, ShortName: "Фонтвьель",
|
||||||
|
DisplayName: "Фонтвьель", DepoCode: "MC0413600000",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParticipantFormValidate(t *testing.T) {
|
||||||
|
if errs := participantFormValidate(validPF()); len(errs) != 0 {
|
||||||
|
t.Fatalf("корректная анкета должна проходить, получили: %v", errs)
|
||||||
|
}
|
||||||
|
bad := validPF()
|
||||||
|
bad.INN = "770380748" // 9 цифр
|
||||||
|
if errs := participantFormValidate(bad); len(errs) == 0 {
|
||||||
|
t.Fatal("ИНН из 9 цифр должен отклоняться")
|
||||||
|
}
|
||||||
|
bad = validPF()
|
||||||
|
bad.DepoCode = "XX0413600000"
|
||||||
|
if errs := participantFormValidate(bad); len(errs) == 0 {
|
||||||
|
t.Fatal("код не в формате MC+10 должен отклоняться")
|
||||||
|
}
|
||||||
|
bad = validPF()
|
||||||
|
bad.DisplayName = ""
|
||||||
|
if errs := participantFormValidate(bad); len(errs) == 0 {
|
||||||
|
t.Fatal("пустой DisplayName должен отклоняться")
|
||||||
|
}
|
||||||
|
bad = validPF()
|
||||||
|
bad.BrokerCode = "MC12" // неверный формат
|
||||||
|
if errs := participantFormValidate(bad); len(errs) == 0 {
|
||||||
|
t.Fatal("неверный код брокера должен отклоняться")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParticipantFormXML(t *testing.T) {
|
||||||
|
x := participantFormXML(validPF())
|
||||||
|
for _, want := range []string{
|
||||||
|
`encoding="windows-1251"`,
|
||||||
|
`<pf:M2MTransferParticipantForm`,
|
||||||
|
`http://nsd.ru/schemas/m2m/participant/form`,
|
||||||
|
`<m2m:INN>7703807489</m2m:INN>`,
|
||||||
|
`<m2m:DepositoryPlace>`,
|
||||||
|
`<m2m:ParticipantCode>MC0413600000</m2m:ParticipantCode>`,
|
||||||
|
`(МСК)`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(x, want) {
|
||||||
|
t.Errorf("XML должен содержать %q\n---\n%s", want, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(x, "<m2m:Eng>") {
|
||||||
|
t.Error("без англ. полей блок Eng не должен появляться")
|
||||||
|
}
|
||||||
|
if strings.Contains(x, "<m2m:BrokerPlace>") {
|
||||||
|
t.Error("без кода брокера блок BrokerPlace не должен появляться")
|
||||||
|
}
|
||||||
|
// С англ. и брокером — блоки появляются.
|
||||||
|
d := validPF()
|
||||||
|
d.EngFull, d.BrokerCode = "Fontvielle LLC", "MC0413600001"
|
||||||
|
x2 := participantFormXML(d)
|
||||||
|
if !strings.Contains(x2, "<m2m:Eng>") || !strings.Contains(x2, "Fontvielle LLC") {
|
||||||
|
t.Error("с англ. наименованием блок Eng обязателен")
|
||||||
|
}
|
||||||
|
if !strings.Contains(x2, "<m2m:BrokerPlace>") || !strings.Contains(x2, "MC0413600001") {
|
||||||
|
t.Error("с кодом брокера блок BrokerPlace обязателен")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParticipantFormBytesCP1251(t *testing.T) {
|
||||||
|
b, err := participantFormBytes(validPF())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Декод обратно из 1251 — кириллица восстанавливается.
|
||||||
|
got := string(nsdxml.DecodeWindows1251(b))
|
||||||
|
if !strings.Contains(got, `ООО "Фонтвьель"`) {
|
||||||
|
t.Errorf("после cp1251 round-trip потеряна кириллица:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildMIMEAttach(t *testing.T) {
|
||||||
|
msg := string(buildMIMEAttach("a@b.ru", []string{"M2MOST@nsd.ru"}, "Тема", "тело", "form.xml", []byte("<xml/>"), "application/xml"))
|
||||||
|
for _, want := range []string{
|
||||||
|
"multipart/mixed; boundary=",
|
||||||
|
"Content-Disposition: attachment; filename=\"form.xml\"",
|
||||||
|
"Content-Transfer-Encoding: base64",
|
||||||
|
"To: M2MOST@nsd.ru",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(msg, want) {
|
||||||
|
t.Errorf("MIME должен содержать %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -233,6 +233,7 @@ func NewServer(cfg ServerConfig) (*Server, error) {
|
|||||||
registerSeedListing(mux, store)
|
registerSeedListing(mux, store)
|
||||||
registerHandbookRequest(mux, igwClient, igwChannel)
|
registerHandbookRequest(mux, igwClient, igwChannel)
|
||||||
registerChainTest(mux, rc, igwClient != nil) // «Проверить всю цепочку» (мастер, глава «Запуск»)
|
registerChainTest(mux, rc, igwClient != nil) // «Проверить всю цепочку» (мастер, глава «Запуск»)
|
||||||
|
registerParticipantForm(mux, rc) // анкета участника M2M (регистрация в НРД, participant_form.go)
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
|||||||
Reference in New Issue
Block a user