diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index 5caea74..3290840 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -833,13 +833,13 @@ func nowPage(title, active string) page { switch { case s.NSD.IGWBaseURL == "": p.IsMockMode = true - p.MockReason = "ИШ НРД не настроен — обмен с НРД не выполняется" + p.MockReason = "Не подключён интеграционный шлюз НРД." if p.DevMode { p.MockReason = "ИШ НРД не настроен — заявки идут через внутренний mock (Decision эмитируется через 3 сек)" } case s.Crypto.Provider == "" || s.Crypto.Provider == "stub": p.IsMockMode = true - p.MockReason = "Не настроено средство подписи (СКЗИ) — обмен с НРД невозможен" + p.MockReason = "Не настроено средство подписи (СКЗИ)." if p.DevMode { p.MockReason = "Провайдер СКЗИ = stub — подпись не делается, реальный обмен с НРД невозможен" } diff --git a/internal/lkgateway/alerts.go b/internal/lkgateway/alerts.go index e490aa3..1050b1d 100644 --- a/internal/lkgateway/alerts.go +++ b/internal/lkgateway/alerts.go @@ -10,6 +10,7 @@ import ( "mime" "net" "net/smtp" + "strconv" "strings" "time" ) @@ -64,7 +65,7 @@ func sendSMTP(s SMTPSettings, subject, body string) error { if port == 0 { port = 587 } - addr := fmt.Sprintf("%s:%d", s.Host, port) + addr := net.JoinHostPort(s.Host, strconv.Itoa(port)) msg := buildMIME(s.From, rcpts, subject, body) dialer := &net.Dialer{Timeout: 15 * time.Second} diff --git a/internal/lkgateway/onboarding_test.go b/internal/lkgateway/onboarding_test.go index 494aad6..208a735 100644 --- a/internal/lkgateway/onboarding_test.go +++ b/internal/lkgateway/onboarding_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "net/url" "os" "path/filepath" "strings" @@ -236,3 +237,33 @@ func TestLettersFill(t *testing.T) { t.Error("неизвестное письмо") } } + +func TestRuPlural(t *testing.T) { + for n, want := range map[int]string{1: "участник", 2: "участника", 4: "участника", 5: "участников", 11: "участников", 12: "участников", 21: "участник", 162: "участника", 111: "участников", 0: "участников"} { + if got := ruPlural(n, "участник", "участника", "участников"); got != want { + t.Errorf("%d: %q, ожидалось %q", n, got, want) + } + } +} + +// Без лицензии QUIK МНП маршрута «ошибки QUIK» в форме нет, а сохранение формы не стирает его прежнее значение. +func TestNotifyKindsByLicense(t *testing.T) { + rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json") + for _, k := range visibleNotifyKinds(rc) { + if k.Key == "quik" { + t.Fatal("без лицензии QUIK МНП маршрута QUIK быть не должно") + } + } + if len(visibleNotifyKinds(rc)) != len(notifyKinds)-1 { + t.Fatalf("типы: %+v", visibleNotifyKinds(rc)) + } + h := &setupHandlers{rc: rc} + form := url.Values{"enabled": {"on"}, "emails": {"depo@example.ru"}, "route_incoming_depo": {"1"}} + req := httptest.NewRequest(http.MethodPost, "/admin/setup/incoming-notify", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + h.saveIncomingNotify(httptest.NewRecorder(), req) + r := rc.Snapshot().Incoming.Routes + if r["incoming"] != "depo" || r["quik"] != defaultNotifyRoutes["quik"] { + t.Fatalf("маршруты после сохранения: %+v", r) + } +} diff --git a/internal/lkgateway/readiness.go b/internal/lkgateway/readiness.go index 1b3edd0..921b8a3 100644 --- a/internal/lkgateway/readiness.go +++ b/internal/lkgateway/readiness.go @@ -11,6 +11,7 @@ import ( "fmt" "net" "net/http" + "os" "os/exec" "sort" "strings" @@ -18,6 +19,23 @@ import ( "time" ) +// cryptoJarPath — модуль службы пробной подписи bj-crypto (компонент crypto-service.jar). +const cryptoJarPath = "/opt/bj/crypto-service.jar" + +// ruPlural — форма слова для числа: 1 участник, 2 участника, 5 участников. +func ruPlural(n int, one, few, many string) string { + n %= 100 + switch { + case n >= 11 && n <= 14: + return many + case n%10 == 1: + return one + case n%10 >= 2 && n%10 <= 4: + return few + } + return many +} + // ReadyCheck — одна строка итоговой проверки. type ReadyCheck struct { Order int `json:"-"` @@ -107,16 +125,22 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck { cctx, cancel := context.WithTimeout(ctx, 4*time.Second) defer cancel() ver, _, _ := validataVersion(cctx) - out, _ := exec.CommandContext(cctx, "systemctl", "is-active", "bj-crypto.service").Output() - svc := strings.TrimSpace(string(out)) - c := ReadyCheck{Order: 6, Title: "СКЗИ «Валидата» и служба криптографии", Link: "/admin/wizard?step=6", Required: true} - switch { - case ver == "": - c.State, c.Detail = "err", "Валидата на сервере не найдена" - case svc != "active": - c.State, c.Detail = "err", "Валидата "+ver+", но служба bj-crypto не запущена ("+pickStr(svc, "нет")+")" - default: - c.State, c.Detail = "ok", "Валидата "+ver+", служба bj-crypto работает" + c := ReadyCheck{Order: 6, Title: "СКЗИ «Валидата»", Link: "/admin/wizard?step=6", Required: true} + if ver == "" { + c.State, c.Detail = "err", "Валидата на сервере не найдена — без неё шлюз не подпишет пакеты для НРД" + return c + } + c.State, c.Detail = "ok", "Валидата "+ver + // Служба bj-crypto — пробная подпись и проверка ключа из bj; пакеты для НРД подписывает шлюз, поэтому + // её остановка обмену не мешает. Модуль приезжает с обновлениями — до них службы нет, это не ошибка. + if _, err := os.Stat(cryptoJarPath); err == nil { + out, _ := exec.CommandContext(cctx, "systemctl", "is-active", "bj-crypto.service").Output() + if svc := strings.TrimSpace(string(out)); svc != "active" { + c.State, c.Link = "warn", "/admin/setup#sec-crypto" + c.Detail += "; служба пробной подписи bj-crypto не запущена (" + pickStr(svc, "нет") + ") — обмену не мешает: пакеты для НРД подписывает шлюз" + } else { + c.Detail += ", служба пробной подписи bj-crypto работает" + } } return c }) @@ -176,7 +200,11 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck { case s.Alerts.Zabbix.Enabled: c.State, c.Detail = "ok", "Zabbix "+s.Alerts.Zabbix.Server default: - c.State, c.Detail = "warn", "не настроены: о сбоях (НРД не отвечает, недоставленные переводы, ошибки DEPO, срок сертификата) узнают только открыв bj" + depo := "" + if moduleAllowed(rc, "quik-mnp") { + depo = ", ошибки DEPO" + } + c.State, c.Detail = "warn", "не настроены: о сбоях (НРД не отвечает, недоставленные переводы"+depo+", срок сертификата) узнают только открыв bj" } return c }) @@ -241,9 +269,9 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck { case len(hb.Participants) == 0: c.State, c.Detail = "err", "справочника нет — получателя перевода не определить" case !at.IsZero() && time.Since(at) > 14*24*time.Hour: - c.State, c.Detail = "warn", fmt.Sprintf("%d участников, от %s — давно не обновлялся (обновляется по связи с НРД)", len(hb.Participants), at.Format("02.01.2006")) + c.State, c.Detail = "warn", fmt.Sprintf("%d %s, от %s — давно не обновлялся (обновляется по связи с НРД)", len(hb.Participants), ruPlural(len(hb.Participants), "участник", "участника", "участников"), at.Format("02.01.2006")) default: - c.State, c.Detail = "ok", fmt.Sprintf("%d участников, от %s", len(hb.Participants), at.Format("02.01.2006")) + c.State, c.Detail = "ok", fmt.Sprintf("%d %s, от %s", len(hb.Participants), ruPlural(len(hb.Participants), "участник", "участника", "участников"), at.Format("02.01.2006")) } return c }) diff --git a/internal/lkgateway/runtimeconfig.go b/internal/lkgateway/runtimeconfig.go index 6fad29b..49a9d2b 100644 --- a/internal/lkgateway/runtimeconfig.go +++ b/internal/lkgateway/runtimeconfig.go @@ -602,6 +602,18 @@ var notifyKinds = []struct{ Key, Title string }{ {"system", "Прочие системные оповещения"}, } +// visibleNotifyKinds — типы уведомлений этой установки: ошибки QUIK МНП — только при модуле в лицензии. +func visibleNotifyKinds(rc *RuntimeConfig) []struct{ Key, Title string } { + var out []struct{ Key, Title string } + for _, k := range notifyKinds { + if k.Key == "quik" && rc != nil && !moduleAllowed(rc, "quik-mnp") { + continue + } + out = append(out, k) + } + return out +} + // defaultNotifyRoutes — маршруты по умолчанию, пока оператор не настроил свои. var defaultNotifyRoutes = map[string]string{"incoming": "depo,back", "orders": "depo,back", "quik": "back,middle", "system": "back"} diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 8d1e15e..b566158 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -311,6 +311,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) { } if (step >= 3 && step <= 5) || step == 9 || step == 11 { // DEPO, организация, QUIK МНП, оповещения и копии, модули — общие блоки из «Настройки» fd := h.setupData(w, r, "") + fd.InWizard = true d.Fancy = &fd } if step == 3 { @@ -458,6 +459,7 @@ func (h *setupHandlers) testSignCrypto(w http.ResponseWriter, r *http.Request) { type SetupData struct { page FromWizard bool // раздел открыт из мастера (настройка модуля) — показать «Вернуться в мастер» + InWizard bool // общий блок показан внутри шага мастера — заголовок и пояснение даёт сам шаг Settings Settings Suggest *DepoRequisites // предложение реквизитов из DEPO Readiness []Readiness @@ -554,7 +556,7 @@ func (h *setupHandlers) setupData(w http.ResponseWriter, req *http.Request, flas OrgName: orgName(s), OrgCode: orgCode(s), NotifyGroups: notifyGroups, - NotifyKinds: notifyKinds, + NotifyKinds: visibleNotifyKinds(h.rc), DepoJob: depoJobView(s.Fancy), DepoTables: depoTablesView(s.Fancy), QuikExport: quikExportView(h.rc, req.URL.Query().Get("reveal_ro") == "1"), @@ -1103,7 +1105,14 @@ func (h *setupHandlers) saveIncomingNotify(w http.ResponseWriter, r *http.Reques EmailsMiddle: strings.TrimSpace(r.FormValue("emails_middle")), Routes: map[string]string{}, } - for _, k := range notifyKinds { + prev := h.rc.Snapshot().Incoming.Routes + if prev == nil { + prev = defaultNotifyRoutes + } + for _, k := range notifyKinds { // скрытые в форме типы (модуль не в лицензии) — прежний маршрут + s.Routes[k.Key] = prev[k.Key] + } + for _, k := range visibleNotifyKinds(h.rc) { var gs []string for _, g := range notifyGroups { if r.FormValue("route_"+k.Key+"_"+g.Key) != "" { diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index 2ee4e16..d0d9cf4 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -316,8 +316,8 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); } {{if eq .Step 4}}
✓ Определено из DEPO: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}}{{if .Suggest.AccountID}}, счёт НРД {{.Suggest.AccountID}}{{end}}. Поля ниже заполнены — проверьте и нажмите «Сохранить».
{{end}}{{end}}{{end}} {{with .Fancy}}{{template "orgSection" .}}{{end}}Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: ИНН передающего и зачисляющего депозитария в пакетах, код отправителя и код фирмы в QUIK. Выберите себя из справочника участников НРД или введите данные вручную.
+ {{if not .InWizard}}Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: ИНН передающего и зачисляющего депозитария в пакетах{{if mod "quik-mnp"}}, код отправителя и код фирмы в QUIK{{end}}. Выберите себя из справочника участников НРД или введите данные вручную.
{{end}} {{if and .OrgINN .OrgCode}}{{if .OrgName}}{{.OrgName}}{{else}}—{{end}} · ИНН {{.OrgINN}} · код {{.OrgCode}}{{if not .Settings.NSD.OrgINN}} (ИНН взят из справочника по коду){{end}}
Организация не задана — переводы не будут сформированы.
{{end}}