From bf7c7b53bdbab0f0a2584a2397e474960d5d2005 Mon Sep 17 00:00:00 2001 From: zuevav Date: Tue, 6 Oct 2026 23:35:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=87=D0=B5=D0=B5=20?= =?UTF-8?q?=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20=D0=BE=D1=80=D0=B3=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8:=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=81=D1=8B=20M2M=20=D0=B2=D0=BD=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=B3=D0=BE=20=E2=80=94=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=20M2M06=20(=D0=B2=20=D0=BB=D1=8E=D0=B1=D0=BE=D0=BC=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B6=D0=B8=D0=BC=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Настройка в шаге «Время и сеть» и Настройка → Сервер: дни недели, часы (по поясу сервера), включение правила. Запрос, полученный вне рабочего времени (и после включения правила), bj сразу отклоняет по всем бумагам кодом M2M06 «Приостановление операций по счёту депо зачисления» — кода «вне рабочего времени» в перечне НРД (инструкция по обмену M2M, раздел 7.4) нет, решение депозитария. Строка «Рабочее время» в проверке готовности; сквозной тест отказа через ИШ. Файл входящих — переменная (тесты не трогают сервер). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi --- internal/lkgateway/admin.go | 2 + internal/lkgateway/incoming.go | 2 +- internal/lkgateway/m2mauto.go | 1 + internal/lkgateway/readiness.go | 16 ++ internal/lkgateway/runtimeconfig.go | 1 + internal/lkgateway/setup.go | 2 + .../lkgateway/web/templates/admin_setup.html | 6 + .../lkgateway/web/templates/admin_wizard.html | 5 + .../web/templates/part_workhours.html | 20 ++ internal/lkgateway/workhours.go | 210 ++++++++++++++++++ internal/lkgateway/workhours_test.go | 112 ++++++++++ 11 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 internal/lkgateway/web/templates/part_workhours.html create mode 100644 internal/lkgateway/workhours.go create mode 100644 internal/lkgateway/workhours_test.go diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index 31d39da..7cd20c3 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -60,6 +60,8 @@ var templateFuncs = template.FuncMap{ "backupStatus": backupStatus, "backupWhen": backupWhen, "smbWin": smbWin, + "whForm": func(w WorkHoursSettings, ret string) map[string]any { return map[string]any{"W": w, "Ret": ret} }, + "weekdayNames": func() []string { return weekdayShort }, "depoAccPrefix": depoAccPrefix, "certDaysLeft": certDaysLeft, "currentContour": func() string { return contourOf(currentNSDProfile()) }, diff --git a/internal/lkgateway/incoming.go b/internal/lkgateway/incoming.go index 68cddad..00a1c5e 100644 --- a/internal/lkgateway/incoming.go +++ b/internal/lkgateway/incoming.go @@ -18,7 +18,7 @@ import ( // ответить M2MTransferDecision — по каждой бумаге подтвердить приём или // отказать. Здесь — хранилище входящих и их состояние ответа. -const incomingStateFile = "/var/lib/bj/.bj/incoming-transfers.json" +var incomingStateFile = "/var/lib/bj/.bj/incoming-transfers.json" // var — тесты подменяют путь // IncomingSecurity — бумага во входящем переводе (что нам предлагают принять). type IncomingSecurity struct { diff --git a/internal/lkgateway/m2mauto.go b/internal/lkgateway/m2mauto.go index 186e2b2..76a67a5 100644 --- a/internal/lkgateway/m2mauto.go +++ b/internal/lkgateway/m2mauto.go @@ -111,6 +111,7 @@ func (s *Server) m2mAutoLoop(ctx context.Context) { } func m2mAutoTickOnce(ctx context.Context, rc *RuntimeConfig, now time.Time) { + workHoursTick(ctx, rc, now) // вне рабочего времени — отказ M2M06 в любом режиме (workhours.go) cfg := rc.Snapshot().M2MAuto if !cfg.Enabled { return diff --git a/internal/lkgateway/readiness.go b/internal/lkgateway/readiness.go index 8b2a58f..729dbf0 100644 --- a/internal/lkgateway/readiness.go +++ b/internal/lkgateway/readiness.go @@ -265,6 +265,22 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck { } return c }) + run(func() ReadyCheck { + c := ReadyCheck{Order: 11, Title: "Рабочее время", Link: "/admin/wizard?step=12"} + w := s.WorkHours + switch { + case w.SavedAt.IsZero(): + c.State, c.Detail = "warn", "рабочее время организации не задано — запросы на перевод M2M принимаются круглосуточно" + case !w.Enabled: + c.State, c.Detail = "ok", w.Text()+"; ограничение выключено — запросы M2M принимаются круглосуточно" + default: + c.State, c.Detail = "ok", w.Text()+" ("+time.Local.String()+"): запрос на перевод M2M, пришедший вне этого времени, отклоняется кодом "+workHoursRejectCode+" «"+m2mCodeText(workHoursRejectCode)+"»" + if !w.Open(time.Now()) { + c.Detail += "; сейчас нерабочее время" + } + } + return c + }) run(func() ReadyCheck { c := ReadyCheck{Order: 11, Title: "Время сервера", Link: "/admin/setup#server"} cctx, cancel := context.WithTimeout(ctx, 3*time.Second) diff --git a/internal/lkgateway/runtimeconfig.go b/internal/lkgateway/runtimeconfig.go index fb07495..458b5fd 100644 --- a/internal/lkgateway/runtimeconfig.go +++ b/internal/lkgateway/runtimeconfig.go @@ -42,6 +42,7 @@ type Settings struct { Backup BackupSettings `json:"backup,omitempty"` HTTPS HTTPSSettings `json:"https,omitempty"` Time TimeSettings `json:"time,omitempty"` + WorkHours WorkHoursSettings `json:"work_hours,omitempty"` // рабочее время: запросы M2M вне него — отказ M2M06 LastTest *TestRunResult `json:"last_test,omitempty"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 73723b1..2daf669 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -96,6 +96,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service mux.HandleFunc("/admin/setup/lk-check", h.checkLKConn) mux.HandleFunc("/admin/setup/lk-simulate", h.lkSimulate) mux.HandleFunc("/admin/setup/lk-pull", h.lkPull) + mux.HandleFunc("/admin/setup/workhours", h.saveWorkHours) mux.HandleFunc("/admin/setup/alerts-smtp", h.saveAlertsSMTP) mux.HandleFunc("/admin/setup/alerts-smtp-test", h.testAlertsSMTP) mux.HandleFunc("/admin/setup/alerts-zabbix", h.saveAlertsZabbix) @@ -274,6 +275,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) { d.Done.Quik = !d.DepoLicensed || doneQuik(s) d.Modules = wizModuleViews(h.rc, s) d.Done.Server, _ = timeStatus(r.Context()) + d.Done.Server = d.Done.Server && !s.WorkHours.SavedAt.IsZero() // и рабочее время организации задано d.Done.Modules = true for _, m := range d.Modules { if !m.Ready { diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index df2cf40..0711554 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -958,6 +958,12 @@
+
+

Рабочее время организации

+

Когда депозитарий принимает запросы на перевод M2M.

+ {{template "workHoursForm" (whForm .Settings.WorkHours "")}} +
+

Время сервера

Точное время нужно для подписей и обмена с НРД. В закрытом контуре внешние серверы времени недоступны — укажите сервер времени вашей сети (обычно контроллер домена или маршрутизатор).

diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index 0341daa..64141a5 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -624,6 +624,11 @@ +
+
Рабочее время{{if and .Settings.WorkHours.Enabled (not .Settings.WorkHours.SavedAt.IsZero)}}✓ {{.Settings.WorkHours.Text}}{{else if .Settings.WorkHours.SavedAt.IsZero}}⚠ не задано{{else}}без ограничения{{end}}
+ {{template "workHoursForm" (whForm .Settings.WorkHours "/admin/wizard?step=12")}} +
+
Адрес bj https://{{serverIP}}:8443/ diff --git a/internal/lkgateway/web/templates/part_workhours.html b/internal/lkgateway/web/templates/part_workhours.html new file mode 100644 index 0000000..c3aa139 --- /dev/null +++ b/internal/lkgateway/web/templates/part_workhours.html @@ -0,0 +1,20 @@ +{{/* Рабочее время организации (workhours.go): шаг «Время и сеть» мастера и Настройка → Сервер. Данные — whForm. */}} +{{define "workHoursForm"}}{{$w := .W}} +
+ {{if .Ret}}{{end}} + +
{{range $d := intList 1 2 3 4 5 6 7}}{{end}}
+
с до по часовому поясу сервера
+ +
{{if not $w.SavedAt.IsZero}} {{if $w.Enabled}}сейчас: {{$w.Text}}, вне его — отказ M2M06{{else}}сохранено без ограничения — запросы принимаются круглосуточно{{end}}{{end}}
+
+{{end}} diff --git a/internal/lkgateway/workhours.go b/internal/lkgateway/workhours.go new file mode 100644 index 0000000..662c76a --- /dev/null +++ b/internal/lkgateway/workhours.go @@ -0,0 +1,210 @@ +package lkgateway + +// Рабочее время организации (06.10.2026): запросы на перевод M2M принимаются только в рабочее время. Запрос, +// полученный вне его, bj сразу отклоняет по всем бумагам кодом M2M06 «Приостановление операций по счёту депо +// зачисления» — и в автоматическом режиме, и в ручном (оператор такой запрос уже не решает). Так решил +// депозитарий: отдельного кода «вне рабочего времени» в перечне НРД нет (инструкция НРД по обмену M2M, +// раздел 7.4: у зачисляющего только M2M01–M2M09). Время — по часовому поясу сервера (шаг «Время и сеть»). + +import ( + "context" + "fmt" + "log" + "net/http" + "sort" + "strconv" + "strings" + "time" +) + +// workHoursRejectCode — код отказа по запросу, полученному вне рабочего времени. +const workHoursRejectCode = "M2M06" + +// WorkHoursSettings — рабочее время организации. +type WorkHoursSettings struct { + Enabled bool `json:"enabled"` // запросы M2M — только в рабочее время + Days []int `json:"days,omitempty"` // 1 — понедельник … 7 — воскресенье; пусто — пн–пт + From string `json:"from,omitempty"` // «09:00» + To string `json:"to,omitempty"` // «18:00» + SavedAt time.Time `json:"saved_at,omitempty"` +} + +func (w WorkHoursSettings) days() []int { + if len(w.Days) == 0 { + return []int{1, 2, 3, 4, 5} + } + return w.Days +} + +// FromText, ToText — начало и конец рабочего дня (для формы). +func (w WorkHoursSettings) FromText() string { return pickStr(w.From, "09:00") } +func (w WorkHoursSettings) ToText() string { return pickStr(w.To, "18:00") } + +// HasDay — день недели (1 — пн … 7 — вс) рабочий. +func (w WorkHoursSettings) HasDay(d int) bool { + for _, x := range w.days() { + if x == d { + return true + } + } + return false +} + +// hhmm — «09:30» → минуты от полуночи. +func hhmm(s string) (int, bool) { + h, m, ok := strings.Cut(strings.TrimSpace(s), ":") + hi, e1 := strconv.Atoi(h) + mi, e2 := strconv.Atoi(m) + if !ok || e1 != nil || e2 != nil || hi < 0 || hi > 24 || mi < 0 || mi > 59 || (hi == 24 && mi != 0) { + return 0, false + } + return hi*60 + mi, true +} + +// Open — момент t в рабочем времени (по часовому поясу сервера). Правило выключено — всегда да. +func (w WorkHoursSettings) Open(t time.Time) bool { + if !w.Enabled { + return true + } + t = t.In(time.Local) + wd := int(t.Weekday()) + if wd == 0 { + wd = 7 + } + from, _ := hhmm(w.FromText()) + to, _ := hhmm(w.ToText()) + m := t.Hour()*60 + t.Minute() + if from < to { // обычный день + return w.HasDay(wd) && m >= from && m < to + } + if from == to { // круглые сутки в рабочие дни + return w.HasDay(wd) + } + // ночная смена (например 20:00–08:00): начало — в рабочий день, хвост — утром следующего + prev := wd - 1 + if prev == 0 { + prev = 7 + } + return (w.HasDay(wd) && m >= from) || (w.HasDay(prev) && m < to) +} + +var weekdayShort = []string{"", "пн", "вт", "ср", "чт", "пт", "сб", "вс"} + +// Text — «пн–пт 09:00–18:00». +func (w WorkHoursSettings) Text() string { + d := append([]int(nil), w.days()...) + sort.Ints(d) + var parts []string + for i := 0; i < len(d); { + j := i + for j+1 < len(d) && d[j+1] == d[j]+1 { + j++ + } + switch { + case j == i: + parts = append(parts, weekdayShort[d[i]]) + case j == i+1: + parts = append(parts, weekdayShort[d[i]], weekdayShort[d[j]]) + default: + parts = append(parts, weekdayShort[d[i]]+"–"+weekdayShort[d[j]]) + } + i = j + 1 + } + return strings.Join(parts, ", ") + " " + w.FromText() + "–" + w.ToText() +} + +func (r *RuntimeConfig) UpdateWorkHours(w WorkHoursSettings) error { + r.mu.Lock() + r.data.WorkHours = w + r.data.UpdatedAt = time.Now().UTC() + r.mu.Unlock() + return r.save() +} + +// saveWorkHours — POST /admin/setup/workhours (шаг «Время и сеть», Настройка → Сервер). +func (h *setupHandlers) saveWorkHours(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + _ = r.ParseForm() + wh := WorkHoursSettings{Enabled: r.FormValue("enabled") != "", SavedAt: time.Now()} + for _, v := range r.Form["day"] { + if d, err := strconv.Atoi(v); err == nil && d >= 1 && d <= 7 && !wh.HasDayExplicit(d) { + wh.Days = append(wh.Days, d) + } + } + sort.Ints(wh.Days) + if len(wh.Days) == 0 { + setupFlash(w, r, "Рабочее время: отметьте хотя бы один рабочий день") + return + } + from, ok1 := hhmm(r.FormValue("from")) + to, ok2 := hhmm(r.FormValue("to")) + if !ok1 || !ok2 { + setupFlash(w, r, "Рабочее время: время — в виде ЧЧ:ММ, например 09:00 и 18:00") + return + } + wh.From, wh.To = fmt.Sprintf("%02d:%02d", from/60, from%60), fmt.Sprintf("%02d:%02d", to/60, to%60) + if err := h.rc.UpdateWorkHours(wh); err != nil { + setupFlash(w, r, "Рабочее время: "+err.Error()) + return + } + msg := "Рабочее время: " + wh.Text() + " — запросы M2M вне него отклоняются кодом " + workHoursRejectCode + if !wh.Enabled { + msg = "Рабочее время: " + wh.Text() + " — сохранено; запросы M2M принимаются круглосуточно" + } + setupFlash(w, r, msg) +} + +// HasDayExplicit — день есть в явно заданном списке (без подстановки пн–пт по умолчанию). +func (w WorkHoursSettings) HasDayExplicit(d int) bool { + for _, x := range w.Days { + if x == d { + return true + } + } + return false +} + +// workHoursTick — отклонить кодом M2M06 входящие запросы, полученные вне рабочего времени (и ещё без ответа). +// Запросы, пришедшие до включения правила, не трогаются: решение по ним — как раньше. +func workHoursTick(ctx context.Context, rc *RuntimeConfig, now time.Time) { + wh := rc.Snapshot().WorkHours + d := globalIncomingDeps + if !wh.Enabled || d == nil { + return + } + for _, it := range d.store.List() { + if it.Status != "pending" || it.ReceivedAt.Before(wh.SavedAt) || wh.Open(it.ReceivedAt) { + continue + } + key := "workhours:" + it.ID + if !m2mAuto.due(key, now) { + continue + } + var decisions []SecurityDecision + for _, s := range it.Securities { + decisions = append(decisions, SecurityDecision{ReferenceID: s.ReferenceID, RejectCode: workHoursRejectCode}) + } + who := fmt.Sprintf("Перевод %s от %s (%s %s)", shortGUID(it.GUID), it.SenderCode, it.InvestorLast, it.InvestorFirst) + cctx, cancel := context.WithTimeout(ctx, 2*time.Minute) + err := answerIncoming(cctx, *d, it.ID, decisions) + cancel() + if err != nil { + if cur, ok := d.store.Get(it.ID); ok && cur.Status == "answered" { + m2mAuto.done(key) + continue + } + m2mAuto.retry(key, now) + m2mAuto.alert(rc, key, now, "Рабочее время: отказ по запросу M2M не отправлен", + fmt.Sprintf("%s получен вне рабочего времени (%s), отказ %s не отправлен: %v. bj повторит через %d мин.", who, wh.Text(), workHoursRejectCode, err, int(m2mAutoRetry.Minutes()))) + continue + } + if cur, ok := d.store.Get(it.ID); ok { + d.store.MarkAuto(it.ID, "Получен вне рабочего времени организации ("+wh.Text()+", получен "+it.ReceivedAt.In(time.Local).Format("02.01.2006 15:04")+") — отклонён кодом "+workHoursRejectCode+" автоматически. "+cur.AnswerNote) + } + m2mAuto.done(key) + log.Printf("work-hours: %s получен %s вне рабочего времени (%s) — отказ %s отправлен", who, it.ReceivedAt.Format("02.01 15:04"), wh.Text(), workHoursRejectCode) + } +} diff --git a/internal/lkgateway/workhours_test.go b/internal/lkgateway/workhours_test.go new file mode 100644 index 0000000..9071e2e --- /dev/null +++ b/internal/lkgateway/workhours_test.go @@ -0,0 +1,112 @@ +package lkgateway + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + "time" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw" +) + +func TestWorkHoursOpen(t *testing.T) { + at := func(s string) time.Time { // 06.10.2026 — вторник + tm, err := time.ParseInLocation("02.01.2006 15:04", s, time.Local) + if err != nil { + t.Fatal(err) + } + return tm + } + w := WorkHoursSettings{Enabled: true, Days: []int{1, 2, 3, 4, 5}, From: "09:00", To: "18:00"} + cases := map[string]bool{ + "06.10.2026 08:59": false, "06.10.2026 09:00": true, "06.10.2026 17:59": true, "06.10.2026 18:00": false, + "10.10.2026 12:00": false, // суббота + "12.10.2026 10:00": true, // понедельник + } + for s, want := range cases { + if got := w.Open(at(s)); got != want { + t.Errorf("%s: %v, ожидалось %v", s, got, want) + } + } + night := WorkHoursSettings{Enabled: true, Days: []int{5}, From: "20:00", To: "08:00"} // пятница, ночная смена + if !night.Open(at("09.10.2026 21:00")) || !night.Open(at("10.10.2026 07:00")) || night.Open(at("10.10.2026 21:00")) { + t.Error("ночная смена") + } + if !(WorkHoursSettings{}).Open(at("10.10.2026 03:00")) { + t.Error("правило выключено — всегда открыто") + } + if got := w.Text(); got != "пн–пт 09:00–18:00" { + t.Errorf("текст: %q", got) + } + if got := (WorkHoursSettings{Days: []int{1, 3, 4}}).Text(); got != "пн, ср, чт 09:00–18:00" { + t.Errorf("текст: %q", got) + } +} + +// Запрос, полученный вне рабочего времени, получает отказ M2M06 по всем бумагам; полученный в рабочее время — +// ждёт решения как обычно; полученный до включения правила — не трогается. +func TestWorkHoursRejectsOffHours(t *testing.T) { + old := incomingStateFile + incomingStateFile = filepath.Join(t.TempDir(), "incoming.json") + defer func() { incomingStateFile = old }() + var sent []string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + sent = append(sent, r.Method+" "+r.URL.Path+" "+string(b)) + _, _ = io.WriteString(w, `{"id":42}`) + })) + defer srv.Close() + rc, _ := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json")) + _ = rc.UpdateNSD(NSDSettings{DeponentCode: "MC0413600000", OrgINN: "7703807489"}) + enabledAt := time.Date(2026, 10, 1, 0, 0, 0, 0, time.Local) + _ = rc.UpdateWorkHours(WorkHoursSettings{Enabled: true, Days: []int{1, 2, 3, 4, 5}, From: "09:00", To: "18:00", SavedAt: enabledAt}) + store := &IncomingStore{items: map[string]*IncomingTransfer{}} + mk := func(id string, at time.Time) { + store.items[id] = &IncomingTransfer{ID: id, GUID: "6f1c2a7e-0d4b-4c8a-9e2f-3b5d7a9c1e0" + id, ReceivedAt: at, Status: "pending", + SenderCode: "MC0012500000", ReceiverCode: "MC0413600000", InvestorLast: "Бубликов", + Securities: []IncomingSecurity{{ReferenceID: "M2MREF0000000001", SecurityCode: "RU0009029540", Quantity: "1"}, {ReferenceID: "M2MREF0000000002", SecurityCode: "RU000A0JUR61", Quantity: "2"}}} + } + mk("1", time.Date(2026, 10, 10, 20, 0, 0, 0, time.Local)) // суббота вечер + mk("2", time.Date(2026, 10, 6, 11, 0, 0, 0, time.Local)) // вторник, рабочее время + mk("3", time.Date(2026, 9, 27, 20, 0, 0, 0, time.Local)) // до включения правила + saved := globalIncomingDeps + globalIncomingDeps = &incomingDeps{store: store, igwClient: igw.NewClient(srv.URL), igwChan: "TEST3MC0413600000", rc: rc, ourCode: "MC0413600000"} + defer func() { globalIncomingDeps = saved }() + m2mAuto.done("workhours:1") + + workHoursTick(context.Background(), rc, time.Date(2026, 10, 10, 20, 1, 0, 0, time.Local)) + + posts := 0 + for _, x := range sent { // POST пакета; GET — клиент ИШ сам спрашивает статус доставки + if strings.HasPrefix(x, "POST /api/package/TEST3MC0413600000/file") { + posts++ + } + } + if posts != 1 { + for _, n := range rc.Snapshot().News.Items { + t.Log(n.Title, " — ", n.Body) + } + t.Fatalf("в шлюз должен уйти один пакет решения, ушло %d", posts) + } + it, _ := store.Get("1") + if it.Status != "answered" || !strings.Contains(it.AnswerNote, "вне рабочего времени") { + t.Fatalf("запрос вне рабочего времени: %s / %s", it.Status, it.AnswerNote) + } + for _, s := range it.Securities { + if s.Decision != "rejected" || s.RejectCode != workHoursRejectCode { + t.Errorf("бумага %s: %s %s, ожидался отказ %s", s.ReferenceID, s.Decision, s.RejectCode, workHoursRejectCode) + } + } + if !strings.Contains(it.DecisionXML, workHoursRejectCode) { + t.Error("в решении нет кода M2M06") + } + for _, id := range []string{"2", "3"} { + if it, _ := store.Get(id); it.Status != "pending" { + t.Errorf("запрос %s не должен был получить ответ: %s", id, it.Status) + } + } +}