@@ -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
}