Рабочее время организации: запросы M2M вне него — отказ M2M06 (в любом режиме)
Настройка в шаге «Время и сеть» и Настройка → Сервер: дни недели, часы (по поясу сервера), включение правила. Запрос, полученный вне рабочего времени (и после включения правила), bj сразу отклоняет по всем бумагам кодом M2M06 «Приостановление операций по счёту депо зачисления» — кода «вне рабочего времени» в перечне НРД (инструкция по обмену M2M, раздел 7.4) нет, решение депозитария. Строка «Рабочее время» в проверке готовности; сквозной тест отказа через ИШ. Файл входящих — переменная (тесты не трогают сервер). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -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()) },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -958,6 +958,12 @@
|
||||
<form method="post" action="/admin/setup/https/selfsigned" style="margin-top:8px"><button type="submit" class="btn btn-secondary" onclick="return confirm('Создать новый самоподписанный сертификат? Сертификат организации будет заменён.')">Вернуть самоподписанный</button></form>
|
||||
</details>
|
||||
</div>
|
||||
<div class="card" id="sec-workhours">
|
||||
<h2><span class="dot {{if and .Settings.WorkHours.Enabled (not .Settings.WorkHours.SavedAt.IsZero)}}ok{{else}}warn{{end}}"></span>Рабочее время организации</h2>
|
||||
<p class="muted">Когда депозитарий принимает запросы на перевод M2M.</p>
|
||||
{{template "workHoursForm" (whForm .Settings.WorkHours "")}}
|
||||
</div>
|
||||
|
||||
<div class="card" id="time">
|
||||
<h2><span class="dot {{if timeSynced}}ok{{else}}warn{{end}}"></span>Время сервера</h2>
|
||||
<p class="muted">Точное время нужно для подписей и обмена с НРД. В закрытом контуре внешние серверы времени недоступны — укажите сервер времени вашей сети (обычно контроллер домена или маршрутизатор).</p>
|
||||
|
||||
@@ -624,6 +624,11 @@
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<div style="margin-top:18px">
|
||||
<div class="wiz-row" style="margin-bottom:8px"><span class="wiz-row-l">Рабочее время</span>{{if and .Settings.WorkHours.Enabled (not .Settings.WorkHours.SavedAt.IsZero)}}<span style="color:var(--ok)">✓ {{.Settings.WorkHours.Text}}</span>{{else if .Settings.WorkHours.SavedAt.IsZero}}<span style="color:var(--warn)">⚠ не задано</span>{{else}}<span class="muted">без ограничения</span>{{end}}</div>
|
||||
{{template "workHoursForm" (whForm .Settings.WorkHours "/admin/wizard?step=12")}}
|
||||
</div>
|
||||
|
||||
<div class="wiz-row" style="margin-top:18px">
|
||||
<span class="wiz-row-l">Адрес bj</span>
|
||||
<a href="https://{{serverIP}}:8443/admin/">https://{{serverIP}}:8443/</a>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{{/* Рабочее время организации (workhours.go): шаг «Время и сеть» мастера и Настройка → Сервер. Данные — whForm. */}}
|
||||
{{define "workHoursForm"}}{{$w := .W}}
|
||||
<form method="post" action="/admin/setup/workhours" class="wh-form">
|
||||
{{if .Ret}}<input type="hidden" name="return_to" value="{{.Ret}}">{{end}}
|
||||
<style>
|
||||
.wh-form{display:grid;gap:10px;max-width:640px}
|
||||
.wh-days{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.wh-days label{display:inline-flex;align-items:center;gap:5px;padding:6px 10px;border:1px solid var(--border);border-radius:8px;font-weight:400;margin:0;cursor:pointer}
|
||||
.wh-days label:has(input:checked){border-color:var(--accent);background:var(--accent-weak)}
|
||||
.wh-days input{width:auto;margin:0}
|
||||
.wh-time{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.wh-time input{width:auto}
|
||||
</style>
|
||||
<div class="wh-days">{{range $d := intList 1 2 3 4 5 6 7}}<label><input type="checkbox" name="day" value="{{$d}}" {{if $w.HasDay $d}}checked{{end}}>{{index (weekdayNames) $d}}</label>{{end}}</div>
|
||||
<div class="wh-time">с <input type="time" name="from" value="{{$w.FromText}}" required aria-label="Начало рабочего дня"> до <input type="time" name="to" value="{{$w.ToText}}" required aria-label="Конец рабочего дня"> <span class="muted" style="font-size:13px">по часовому поясу сервера</span></div>
|
||||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin:0"><input type="checkbox" name="enabled" style="width:auto;margin-top:3px" {{if or $w.Enabled $w.SavedAt.IsZero}}checked{{end}}>
|
||||
<span><b>Принимать запросы на перевод M2M только в рабочее время</b><br><span class="muted" style="font-size:13px">Запрос, пришедший вне рабочего времени, bj сразу отклоняет по всем бумагам кодом M2M06 «Приостановление операций по счёту депо зачисления» — и в автоматическом режиме, и в ручном.</span></span></label>
|
||||
<div><button type="submit" class="btn">Сохранить</button>{{if not $w.SavedAt.IsZero}} <span class="muted" style="font-size:13px">{{if $w.Enabled}}сейчас: {{$w.Text}}, вне его — отказ M2M06{{else}}сохранено без ограничения — запросы принимаются круглосуточно{{end}}</span>{{end}}</div>
|
||||
</form>
|
||||
{{end}}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user