9a227228e5
НРД ответил M2M37 на анкету 21.10: в FullName ушло краткое наименование из DEPO. Теперь полное, краткое и отображаемое берутся из карточки нашей фирмы в справочнике НРД (НРД сверяет именно с клиентской базой); отображаемое = краткое, как советует НРД. Английские — только введённые. Кавычки «» “” приводятся к прямым ", как в базе НРД. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
211 lines
8.3 KiB
Go
211 lines
8.3 KiB
Go
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)
|
||
}
|
||
}
|