Files
Bridge-and-Join-s/internal/lkgateway/participant_form.go
T
fontvielle 416f9bdcd5 Анкета участника: статус регистрации по справочнику участников M2M
Регистрация подтверждается двумя путями: ответным документом НРД (ловим в
поллере) ИЛИ появлением организации в справочнике участников M2M боевого
контура. На странице анкеты добавлен статус: ищем нашу организацию в справочнике
по MC-коду, затем по ИНН (handbookByCode/handbookByINN); «есть» — регистрация
подтверждена, «нет» — ждём приёма анкеты (справочник обновляется ~10 минут).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-21 17:31:09 +03:00

477 lines
25 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 lkgateway
// participant_form.go — анкета участника сервиса M2M (M2MTransferParticipantForm).
// Требование НРД при подключении: зарегистрировать нашу организацию в справочнике
// участников M2M, иначе сервис МОСТ отбивает запросы кодом M2M14 «Код ЭДО НРД
// отправителя отсутствует в справочнике участников M2M». Здесь bj формирует
// заполненную анкету (предзаполнение из автоопределения DEPO), даёт проверить
// (валидация + показ XML в windows-1251) и отправляет её в НРД: в боевом контуре —
// через ИШ пакетом #M2MPF (businessLine=MOST, подпись боевым контейнером на стороне
// ИШ), либо запасным каналом — письмом на M2MOST@nsd.ru с вложением.
import (
"context"
"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/nsdadapter"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
"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}$`)
// isProdIGW — текущий канал ИШ боевой. Анкету участника в НРД шлём ТОЛЬКО в бою:
// в тестовом/гостевом контуре отправка через шлюз заблокирована.
func isProdIGW(channel string) bool {
return strings.HasPrefix(strings.ToUpper(strings.TrimSpace(channel)), "PROD")
}
// 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 rc != nil {
s := rc.Snapshot()
// Реквизиты нашей организации из настроек НРД и справочника участников —
// заполняем то, чего не дало автоопределение DEPO.
if d.INN == "" {
d.INN = orgINN(s)
}
if d.DepoCode == "" {
d.DepoCode = strings.ToUpper(strings.TrimSpace(pick(s.NSD.DeponentCode, s.NSD.OrgCode)))
}
// Карточка нашей фирмы в справочнике участников НРД: по MC-коду, затем по ИНН.
hb, ok := handbookByCode(orgCode(s))
if !ok && d.INN != "" {
hb, ok = handbookByINN(d.INN)
}
if ok {
if d.FullName == "" {
d.FullName = strings.TrimSpace(pick(hb.FullName, hb.Name))
}
if d.ShortName == "" {
d.ShortName = strings.TrimSpace(hb.Name)
}
if d.DisplayName == "" {
d.DisplayName = strings.TrimSpace(pick(hb.DisplayName, hb.Name))
}
if d.EngFull == "" {
d.EngFull = strings.TrimSpace(hb.EngName)
}
if hb.Kind == "broker" && d.BrokerCode == "" {
d.BrokerCode = strings.ToUpper(strings.TrimSpace(hb.Code))
}
}
if d.FullName == "" {
d.FullName = strings.TrimSpace(s.NSD.OrgName)
}
}
// DisplayName/Short обязательны в анкете — подставим из того, что есть.
if d.DisplayName == "" {
d.DisplayName = strings.TrimSpace(pick(d.ShortName, d.FullName))
}
if d.ShortName == "" {
d.ShortName = strings.TrimSpace(pick(d.DisplayName, d.FullName))
}
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("&", "&amp;", "<", "&lt;", ">", "&gt;")
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, igwClient *igw.Client, igwChannel string) {
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"
}
}
if action == "send_igw" {
switch {
case len(errs) > 0:
flash, flashKind = "Анкета не прошла проверку — исправьте поля ниже.", "err"
case igwClient == nil || igwChannel == "":
flash, flashKind = "Боевой ИШ не настроен (раздел «НРД», профиль prod-*). Отправка через шлюз недоступна — переключитесь в боевой контур.", "err"
case !isProdIGW(igwChannel):
flash, flashKind = "Сейчас выбран тестовый контур. Анкету участника шлём только в боевом контуре — переключитесь на профиль prod-* («НРД») и повторите.", "err"
default:
body, encErr := participantFormBytes(d)
if encErr != nil {
flash, flashKind = "кодировка windows-1251: "+encErr.Error(), "err"
break
}
pkg, _ := nsdadapter.RouteToPackageType(nsdadapter.KindParticipantForm)
zipBody, zErr := igw.PackXML(body, "M2MTransferParticipantForm.xml", string(pkg))
if zErr != nil {
flash, flashKind = "упаковка пакета: "+zErr.Error(), "err"
break
}
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
id, sErr := igwClient.SendPackage(ctx, igwChannel, string(pkg), zipBody)
cancel()
if sErr != nil {
flash, flashKind = "отправка через ИШ не удалась: "+sErr.Error(), "err"
break
}
flash, flashKind = "Анкета отправлена через ИШ ("+string(pkg)+", пакет id="+id+"). Ждём подтверждения регистрации от НРД.", "ok"
}
}
xmlPreview := participantFormXML(d)
participantFormRender(w, d, errs, action, flash, flashKind, xmlPreview, participantRegStatus(rc), igwClient != nil && igwChannel != "", isProdIGW(igwChannel))
})
}
// participantRegStatus — подтверждение регистрации появлением в справочнике участников
// M2M текущего контура: если наша организация (по MC-коду или ИНН) уже в справочнике —
// анкета принята. Это второй сигнал, помимо ответного документа от НРД.
func participantRegStatus(rc *RuntimeConfig) string {
if rc == nil {
return ""
}
s := rc.Snapshot()
contour := map[string]string{"prod": "боевого", "guest": "гостевого", "test3": "тестового"}[contourOf(currentNSDProfile())]
if contour == "" {
contour = "текущего"
}
hb, ok := handbookByCode(orgCode(s))
if !ok {
if inn := orgINN(s); inn != "" {
hb, ok = handbookByINN(inn)
}
}
if ok {
return `<div class="ok">В справочнике участников M2M ` + contour + ` контура: <b>есть</b> — ` +
html.EscapeString(pickStr(hb.Name, hb.FullName)) + `, код ` + html.EscapeString(hb.Code) +
`, ИНН ` + html.EscapeString(hb.INN) + `. Регистрация подтверждена.</div>`
}
return `<div class="note">В справочнике участников M2M ` + contour + ` контура нашей организации пока <b>нет</b>. ` +
`После приёма анкеты НРД организация появится в справочнике (обновляется каждые ~10 минут) — тогда здесь будет «есть». ` +
`Регистрацию подтверждает также ответный документ от НРД.</div>`
}
func participantFormRender(w http.ResponseWriter, d participantFormData, errs []string, action, flash, flashKind, xmlPreview, regHTML string, igwConfigured, igwProd bool) {
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>`
}
igwBtn := `<button type="submit" name="action" value="send_igw" class="primary">Отправить через шлюз (боевой ИШ)</button>`
igwStatus := `<p class="note">Боевой контур: <b style="color:#2a7a2a">да</b> — анкету можно отправить через шлюз (#M2MPF).</p>`
if !igwProd {
igwBtn = `<button type="button" class="primary" disabled title="Доступно только в боевом контуре">Отправить через шлюз (только в бою)</button>`
if igwConfigured {
igwStatus = `<p class="note">Контур <b>тестовый</b> — анкету участника в НРД шлём <b>только в боевом</b> контуре. Переключитесь на профиль <code>prod-*</code> («НРД»). Пока доступны «Скачать» и «Отправить письмом».</p>`
} else {
igwStatus = `<p class="note">Боевой ИШ: <b style="color:#b00">не настроен</b> (раздел «НРД», профиль <code>prod-*</code>). Пока доступны «Скачать» и «Отправить письмом».</p>`
}
}
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>#M2MPF</code> (businessLine=MOST, подпись боевым контейнером на стороне ИШ); письмо на
<code>` + participantFormRecipient + `</code> оставлено запасным каналом. Поля предзаполнены из
автоопределения DEPO — проверьте юридические реквизиты.</p>
` + regHTML + 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>
` + igwStatus + 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>
` + igwBtn + `
<button type="submit" name="action" value="send">Отправить письмом (запасной канал)</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
}