package lkgateway // Готовые письма тем, без кого подключение не закончить (05.10.2026): НРД (доступ к тестовому и боевому // контурам), ARQA (модуль неторговых поручений QUIK), администратору базы DEPO. Реквизиты подставляются // из настроек; то, чего bj не знает (контактное лицо), — в квадратных скобках. У НРД бывают свои формы // заявок — письмо тогда идёт сопроводительным. import ( "encoding/json" "fmt" "html/template" "net/http" "net/url" "strings" ) // Letter — готовое письмо. type Letter struct { Key, Title, To, Subject, Body string } func letterOrg(s Settings) (name, inn, code string) { inn = orgINN(s) name = strings.TrimSpace(s.NSD.OrgName) if name == "" && inn != "" { // наименование не введено — из справочника участников НРД по ИНН raw, _ := readHandbook() var hb struct { Participants []HandbookEntry `json:"participants"` } _ = json.Unmarshal(raw, &hb) for _, p := range hb.Participants { if p.INN == inn { name = pickStr(p.FullName, p.Name) break } } } if name == "" && globalRC != nil { name = licenseStatus(globalRC).Tenant } return pickStr(name, "[наименование организации]"), pickStr(inn, "[ИНН]"), pickStr(orgCode(s), "[код участника]") } func orBracket(v, what string) string { return pickStr(strings.TrimSpace(v), "["+what+"]") } // letterFor — письмо по ключу: nsd-test | nsd-prod | arqa | dba. func letterFor(s Settings, key string) (Letter, bool) { name, inn, code := letterOrg(s) sign := "\n\nКонтактное лицо: [ФИО, должность, телефон, e-mail]\n\nС уважением,\n[ФИО, должность]\n" + name crypto := "ГОСТ" if strings.HasSuffix(s.NSD.Profile, "rsa") { crypto = "RSA" } req := fmt.Sprintf("— депозитарный код: %s\n— депозитарный счёт: %s, раздел: %s\n— программа обмена: Интеграционный шлюз НРД (ИШ), канал WSL, криптография %s\n— роль: депозитарий — участник переводов ценных бумаг M2M", orBracket(s.NSD.DeponentCode, "депозитарный код"), orBracket(s.NSD.AccountID, "депозитарный счёт"), orBracket(s.NSD.SectionID, "раздел счёта"), crypto) switch key { case "nsd-test": return Letter{Key: key, Title: "Письмо в НРД: доступ к тестовому контуру TEST3", To: "НКО АО НРД, служба поддержки электронного документооборота", Subject: "Заявка на подключение к тестовому контуру TEST3 для переводов M2M — " + name, Body: fmt.Sprintf("В НКО АО НРД\n(служба поддержки электронного документооборота)\n\n%s (ИНН %s, код участника %s) просит предоставить доступ к тестовому контуру TEST3 электронного документооборота НРД для обмена сообщениями о переводах ценных бумаг M2M.\n\nСведения для подключения:\n%s\n\nПросим также сообщить порядок получения тестового ключа электронной подписи (УЦ Московской биржи) и включить нас в тестирование сценариев переводов M2M.\n\nЕсли для подключения установлена форма заявки — просим её направить, а это письмо считать сопроводительным.%s", name, inn, code, req, sign)}, true case "nsd-prod": return Letter{Key: key, Title: "Письмо в НРД: подключение к боевому контуру", To: "НКО АО НРД, служба поддержки электронного документооборота", Subject: "Заявка на подключение к промышленному контуру для переводов M2M — " + name, Body: fmt.Sprintf("В НКО АО НРД\n(служба поддержки электронного документооборота)\n\n%s (ИНН %s, код участника %s) завершила тестирование обмена сообщениями о переводах ценных бумаг M2M на контуре TEST3 и просит подключить промышленный контур электронного документооборота НРД.\n\nСведения для подключения:\n%s\n— ключ электронной подписи для промышленного контура: выдан УЦ Московской биржи [номер сертификата, срок действия]\n\nЕсли для подключения установлена форма заявки — просим её направить, а это письмо считать сопроводительным.%s", name, inn, code, req, sign)}, true case "arqa": // коротко и по делу: о внутреннем устройстве (bj, процедуры, классы) поставщику знать не нужно return Letter{Key: key, Title: "Письмо в ARQA Technologies: модуль неторговых поручений", To: "ARQA Technologies, служба поддержки QUIK", Subject: "Подключение модуля неторговых поручений QUIK — " + name, Body: fmt.Sprintf("В ARQA Technologies\n(служба поддержки QUIK)\n\n%s (ИНН %s) просит:\n\n1. Подключить модуль неторговых поручений QUIK (переводы ценных бумаг M2M).\n2. Выдать логин и пароль для подключения к базе данных модуля неторговых поручений.\n3. Разрешить подключение к базе данных модуля с нашего адреса %s и сообщить адрес сервера, порт и имя базы.%s", name, inn, arqaAddr(publicIP()), sign)}, true case "dba": ip := pickStr(outboundIP(s.Fancy.Server, s.Fancy.Port), firstServerIP()) f := s.Fancy what := "— создаёт задание SQL Server Agent «" + depoJobName(f) + "» для обработки обмена;\n" run := "под логином с правами sysadmin (SSMS или sqlcmd)" if f.IsPostgres() { what = "" run = "суперпользователем: psql -h <сервер> -U postgres -f " + depoSetupFileName(f) } else if f.DepoJobMode == "bj" { what = "— даёт логину bj право запускать процедуру обработки обмена;\n" } return Letter{Key: key, Title: "Письмо администратору базы DEPO", To: "Администратор СУБД учёта DEPO", Subject: "Bridge&Join: подготовка базы DEPO на сервере " + orBracket(f.Server, "сервер DEPO"), Body: fmt.Sprintf("Коллеги, добрый день!\n\nДля подключения системы Bridge&Join к учёту DEPO нужно на сервере %s выполнить один скрипт %s %s (файл прилагается — он скачивается в мастере настройки bj). Скрипт:\n— в начале — имя базы DEPO и пароль логина bj: проверьте их; скрипт сначала убедится, что это база DEPO (есть учётные таблицы OD_FACES и OD_RESTS), иначе остановится, ничего не меняя;\n— создаёт логин bj: права — чтение учётных таблиц DEPO и запись только в таблицы обмена FTV_M2M*;\n— создаёт таблицы обмена FTV_M2M*; учётные данные он не меняет;\n%s\nСкрипт можно запускать повторно. В нём пароль логина bj — после выполнения файл удалите.\n\nТакже нужно разрешить подключение к серверу базы с сервера Bridge&Join (адрес %s). Процедуры обработки обмена FTV_M2M_* поставляются отдельно.%s", orBracket(f.Server, "сервер DEPO"), depoSetupFileName(f), run, what, orBracket(ip, "IP-адрес сервера bj"), sign)}, true } return Letter{}, false } // arqaAddr — наш внешний адрес для письма в ARQA с пометкой для отправителя: подключение разрешат только // с этого адреса, поэтому он должен быть постоянным (у провайдера адрес может меняться). Пометку перед // отправкой удаляют — как и всё в квадратных скобках. func arqaAddr(ip string) string { if ip == "" { return "[постоянный внешний IP-адрес вашего сервера — уточните у сетевого администратора]" } return ip + " [убедитесь, что это постоянный внешний адрес вашей организации и он не меняется — уточните у сетевого администратора; пометку удалите]" } func firstServerIP() string { _, ips := localAddrs() for _, ip := range ips { if ip.To4() != nil && !ip.IsLoopback() { return ip.String() } } return "" } // letterView — письмо для шаблона (по текущим настройкам). func letterView(key string) *Letter { if globalRC == nil { return nil } if l, ok := letterFor(globalRC.Snapshot(), key); ok { return &l } return nil } // GET /admin/setup/letters/<ключ>.txt — письмо файлом. func registerLetters(mux *http.ServeMux, rc *RuntimeConfig) { mux.HandleFunc("/admin/setup/letters/", func(w http.ResponseWriter, r *http.Request) { key := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/admin/setup/letters/"), ".txt") l, ok := letterFor(rc.Snapshot(), key) if !ok { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Disposition", `attachment; filename="bj-letter-`+key+`.txt"`) _, _ = fmt.Fprintf(w, "Кому: %s\r\nТема: %s\r\n\r\n%s\r\n", l.To, l.Subject, strings.ReplaceAll(l.Body, "\n", "\r\n")) }) } // mailtoLink — ссылка «Открыть в почте» (тема и текст письма; пробелы и переводы строк — %20 и %0A). func mailtoLink(l *Letter) template.URL { if l == nil { return "" } return template.URL("mailto:?subject=" + url.PathEscape(l.Subject) + "&body=" + url.PathEscape(l.Body)) }