Files
Bridge-and-Join-s/internal/lkgateway/participant_form.go
T
fontvielle 60b4d46277 Списки: период, сортировка, подгрузка; анкета участника — строкой исходящих
«Исходящие», «Входящие», «Журнал обмена»: быстрые периоды (сегодня, 7, 30
дней, всё время), даты с–по, сортировка (время, инвестор/контрагент,
направление, статус), первые 50 строк и подгрузка при прокрутке
(part=rows&offset=N). Раньше показывались только последние 200/100.

Анкета участника — исходящий пакет в НРД: строка в «Исходящих» и
«Журнале» с ответом НРД (принята / отклонена с кодом / ждёт), вместо
плашки над списком. Отправки копятся в participant-regs.json (прежний
participant-reg.json подхватывается). userTemplate — функции текущего
пользователя и для порций строк.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-22 10:50:43 +03:00

592 lines
32 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
r := depoDetectResult()
if r == nil && rc != nil && strings.TrimSpace(rc.Snapshot().Fancy.Server) != "" {
// после перезапуска bj автоопределения в памяти нет — прочитать DEPO сейчас
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
if res, err := depoDetectRequisites(ctx, rc.Snapshot().Fancy); err == nil {
lastDepoDetect.mu.Lock()
lastDepoDetect.r = res
lastDepoDetect.mu.Unlock()
r = res
}
cancel()
}
if r != nil {
// DEPO: полное — OD_U_FACES, краткое — OD_FACES.NAME, отображаемое = краткое (так советует НРД);
// английские — только парой (полное + краткое), иначе их не указываем
d.INN = strings.TrimSpace(r.OrgINN)
d.FullName = strings.TrimSpace(pick(r.OrgFull, r.OrgName))
d.ShortName = strings.TrimSpace(r.OrgName)
d.DisplayName = strings.TrimSpace(r.OrgName)
if strings.TrimSpace(r.OrgEngFull) != "" {
d.EngFull = strings.TrimSpace(r.OrgEngFull)
d.EngShort = strings.TrimSpace(r.OrgEng)
d.EngDisplay = d.EngShort
}
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 {
// НРД сверяет наименования анкеты со своей клиентской базой (M2M37) — карточка в справочнике
// НРД главнее DEPO: в DEPO «наименование» бывает кратким (21.10.2026 краткое ушло в FullName).
// Английские — из DEPO: в справочнике НРД есть лишь краткое.
if n := strings.TrimSpace(hb.FullName); n != "" {
d.FullName = n
}
if n := strings.TrimSpace(hb.Name); n != "" && hb.FullName != "" {
d.ShortName, d.DisplayName = n, n // отображаемое НРД советует как краткое
}
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
}
// pfAbbrRe — наименование начинается с сокращённой формы (ООО, АО…) — для полного это ошибка.
var pfAbbrRe = regexp.MustCompile(`^(ООО|ОАО|ЗАО|ПАО|НАО|АО|АКБ|КБ|НКО|ИК)[\s"«]`)
// pfAskNSD — текст запроса в НРД о точных наименованиях участника (ответ НРД — источник истины).
func pfAskNSD(code string) string {
if code == "" {
code = "наш код участника"
}
return "Просим сообщить точное наименование, под которым участник " + code +
" зарегистрирован в клиентской базе НРД: полное (FullName), сокращённое (ShortName), " +
"а также английские полное и сокращённое, если они есть."
}
// pfNameCheck — что в наименованиях надо уточнить у НРД. НРД сверяет их со своей клиентской
// базой (M2M37): точный источник — карточка нашей фирмы в справочнике НРД или ответ НРД;
// данные DEPO — только черновик.
func pfNameCheck(rc *RuntimeConfig, d participantFormData) []string {
var w []string
hb, ok := participantCard(rc, d.INN)
switch {
case ok && strings.TrimSpace(hb.FullName) != "":
x := participantFormData{FullName: hb.FullName}
pfNames(&x)
if x.FullName != d.FullName {
w = append(w, "Полное наименование отличается от карточки нашей фирмы в справочнике НРД: «"+x.FullName+"». НРД сверяет с ней — без причины не меняйте.")
}
case strings.TrimSpace(d.FullName) == "":
w = append(w, "Полного наименования нет ни в DEPO, ни в справочнике НРД — уточните его у НРД и впишите как есть.")
}
if pfAbbrRe.MatchString(d.FullName) || (d.FullName != "" && d.FullName == d.ShortName) {
w = append(w, "Полное наименование похоже на сокращённое — нужно полное («Общество с ограниченной ответственностью …»), как в клиентской базе НРД.")
}
if (d.EngShort != "" || d.EngDisplay != "") && d.EngFull == "" {
w = append(w, "Английские: указано краткое без полного — уточните полное у НРД или оставьте английские пустыми.")
}
return w
}
// participantCard — карточка нашей фирмы в справочнике НРД: по MC-коду, затем по ИНН.
func participantCard(rc *RuntimeConfig, inn string) (HandbookEntry, bool) {
if rc == nil {
return HandbookEntry{}, false
}
s := rc.Snapshot()
if hb, ok := handbookByCode(orgCode(s)); ok {
return hb, true
}
if inn = strings.TrimSpace(pick(inn, orgINN(s))); inn != "" {
return handbookByINN(inn)
}
return HandbookEntry{}, false
}
// pfQuotes — кавычки как в клиентской базе НРД: прямые ".
var pfQuotes = strings.NewReplacer("«", `"`, "»", `"`, "“", `"`, "”", `"`, "„", `"`)
// pfNames — наименования к виду базы НРД (кавычки, лишние пробелы).
func pfNames(d *participantFormData) {
for _, p := range []*string{&d.FullName, &d.ShortName, &d.DisplayName, &d.EngFull, &d.EngShort, &d.EngDisplay} {
*p = strings.Join(strings.Fields(pfQuotes.Replace(*p)), " ")
}
}
// 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 {
pfNames(&d)
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"),
}
}
pfNames(&d) // кавычки и пробелы — как в базе НРД, и в анкете, и в форме, и в статусе
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"
addParticipantReg(participantReg{SentAt: time.Now(), PkgID: id, Name: d.FullName, Channel: igwChannel})
}
}
xmlPreview := participantFormXML(d)
reg := participantRegStatus(rc)
if ws := pfNameCheck(rc, d); len(ws) > 0 {
code := ""
if rc != nil {
code = orgCode(rc.Snapshot())
}
reg += `<div class="warn"><b>Уточните у НРД:</b><ul><li>` + strings.Join(escapeAll(ws), "</li><li>") +
`</li></ul>Запрос в НРД (можно скопировать):<pre>` + html.EscapeString(pfAskNSD(code)) + `</pre></div>`
}
participantFormRender(w, d, errs, action, flash, flashKind, xmlPreview, reg, igwClient != nil && igwChannel != "", isProdIGW(igwChannel))
})
}
// participantRegStatus — подтверждение регистрации появлением в справочнике участников
// M2M текущего контура: если наша организация (по MC-коду или ИНН) уже в справочнике —
// анкета принята. Это второй сигнал, помимо ответного документа от НРД.
func participantRegStatus(rc *RuntimeConfig) string {
if rc == nil {
return ""
}
var out strings.Builder
// Последняя отправка анкеты и ответ НРД на неё (учёт в participant-reg.json).
if reg := loadParticipantReg(); !reg.SentAt.IsZero() {
out.WriteString(`<div class="note">Анкета отправлена через ИШ ` + reg.SentAt.Format("02.01.2006 15:04") + `, пакет ` + html.EscapeString(reg.PkgID))
if reg.Name != "" {
out.WriteString(`, наименование «` + html.EscapeString(reg.Name) + `»`)
}
out.WriteString(`. `)
switch {
case reg.RespAt.IsZero():
out.WriteString(`Ответ НРД пока не получен.`)
case strings.EqualFold(reg.Status, "ERROR"):
out.WriteString(`<b style="color:#b00">НРД отклонил</b> ` + reg.RespAt.Format("02.01 15:04") + `: ` + html.EscapeString(reg.RespText) + `. Исправьте указанное и отправьте снова.`)
if containsStr(reg.Codes, "M2M37") {
out.WriteString(` Наименование не совпало с клиентской базой НРД — запросите у НРД точные наименования (текст запроса ниже) и впишите их без изменений.`)
}
default:
out.WriteString(`<b style="color:#2a7a2a">НРД принял</b> ` + reg.RespAt.Format("02.01 15:04") + `. ` + html.EscapeString(reg.RespText))
}
out.WriteString(`</div>`)
}
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 {
out.WriteString(`<div class="ok">В справочнике участников M2M ` + contour + ` контура: <b>есть</b> — ` +
html.EscapeString(pickStr(hb.Name, hb.FullName)) + `, код ` + html.EscapeString(hb.Code) +
`, ИНН ` + html.EscapeString(hb.INN) + `. Регистрация подтверждена.</div>`)
return out.String()
}
out.WriteString(`<div class="note">В справочнике участников M2M ` + contour + ` контура нашей организации пока <b>нет</b>. ` +
`После приёма анкеты НРД организация появится в справочнике (обновляется каждые ~10 минут) — тогда здесь будет «есть». ` +
`Регистрацию подтверждает также ответный документ от НРД.</div>`)
return out.String()
}
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}
.warn{background:#fff7e0;border:1px solid #e0b040;padding:8px 12px;border-radius:6px;margin:10px 0}.warn pre{margin:6px 0 0}</style></head><body>
<h1>Регистрация участника сервиса M2M (МОСТ)</h1>
<p class="note">Требование НРД при подключении: зарегистрировать организацию в справочнике участников M2M.
Без регистрации сервис отбивает запросы кодом <b>M2M14</b>. В боевом контуре анкета подаётся <b>через ИШ</b>
пакетом <code>#M2MPF</code> (businessLine=MOST, подпись боевым контейнером на стороне ИШ); письмо на
<code>` + participantFormRecipient + `</code> оставлено запасным каналом. Наименования должны точно совпадать с
клиентской базой НРД (иначе <b>M2M37</b>): bj берёт их из карточки нашей фирмы в справочнике НРД и из DEPO (полное — карточка юрлица).
Чего нет или в чём bj не уверен — уточните у НРД. Кавычки — прямые <code>"</code>.</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
}