adc22ef206
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
188 lines
7.7 KiB
Go
188 lines
7.7 KiB
Go
package lkgateway
|
|
|
|
// Справочник «Депозитарии получателей для переводов M2M» в БД МНП (M2MDepoRecipients) ведёт bj — по
|
|
// справочнику участников M2M от НРД (решение 29.09.2026). QUIK этот справочник сам не получает, а в
|
|
// поручении «Вывод ЦБ (M2M)» депозитарий получателя обязателен и сверяется с ним: без записи МНП
|
|
// отвергает регистрацию («Неизвестный депозитарий получателя»), и бумаги клиента не блокируются.
|
|
// Модуль МНП перечитывает справочник при запуске (ночной перезапуск), поэтому справочник обновляется
|
|
// в течение дня до 20:00 — к ночи в базе МНП полный актуальный список: раз в 30 минут и сразу при
|
|
// изменении справочника НРД. Добавляются недостающие депозитарии и исправляются наименования;
|
|
// записи, которых нет в справочнике НРД, не удаляются.
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
quikRecipientsEvery = 30 * time.Minute
|
|
quikRecipientsLastHour = 20 // после 20:00 не обновляем: модуль уже не перечитает до ночного перезапуска
|
|
)
|
|
|
|
var quikRecipientsState struct {
|
|
sync.Mutex
|
|
At time.Time
|
|
Total int
|
|
Added int
|
|
Renamed int
|
|
Err string
|
|
running bool
|
|
}
|
|
|
|
// quikRecipientsSync — привести M2MDepoRecipients в соответствие справочнику НРД.
|
|
func quikRecipientsSync(ctx context.Context, rc *RuntimeConfig) (total, added, renamed int, err error) {
|
|
s := rc.Snapshot().Quik
|
|
if strings.TrimSpace(s.Server) == "" {
|
|
return 0, 0, 0, nil
|
|
}
|
|
raw, _ := readHandbook()
|
|
var hb struct {
|
|
Participants []HandbookEntry `json:"participants"`
|
|
}
|
|
if err := json.Unmarshal(raw, &hb); err != nil {
|
|
return 0, 0, 0, fmt.Errorf("справочник НРД: %w", err)
|
|
}
|
|
want := map[string]string{}
|
|
for _, p := range hb.Participants {
|
|
inn := strings.TrimSpace(p.INN)
|
|
if inn == "" {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(p.Name)
|
|
if name == "" {
|
|
name = strings.TrimSpace(p.DisplayName)
|
|
}
|
|
if name == "" {
|
|
name = strings.TrimSpace(p.FullName)
|
|
}
|
|
if name == "" {
|
|
name = "Депозитарий ИНН " + inn
|
|
}
|
|
if r := []rune(name); len(r) > 128 {
|
|
name = string(r[:128])
|
|
}
|
|
if _, ok := want[inn]; !ok {
|
|
want[inn] = name
|
|
}
|
|
}
|
|
if len(want) == 0 {
|
|
return 0, 0, 0, fmt.Errorf("справочник НРД пуст")
|
|
}
|
|
conn, err := quikOpen(s)
|
|
if err != nil {
|
|
return 0, 0, 0, err
|
|
}
|
|
defer conn.Close()
|
|
cctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
|
defer cancel()
|
|
have := map[string]string{}
|
|
rows, err := conn.QueryContext(cctx, "SELECT INN, ISNULL(Name,'') FROM M2MDepoRecipients")
|
|
if err != nil {
|
|
return 0, 0, 0, fmt.Errorf("чтение M2MDepoRecipients: %s", quikErrorDetail(err))
|
|
}
|
|
for rows.Next() {
|
|
var inn, name string
|
|
if rows.Scan(&inn, &name) == nil {
|
|
have[strings.TrimSpace(inn)] = strings.TrimSpace(name)
|
|
}
|
|
}
|
|
rows.Close()
|
|
for inn, name := range want {
|
|
cur, ok := have[inn]
|
|
if ok && cur == name {
|
|
continue
|
|
}
|
|
if _, err := conn.ExecContext(cctx, "exec dbo.imp_M2MDepoRecipients @Operation=1, @INN=@inn, @Name=@nm", sql.Named("inn", inn), sql.Named("nm", name)); err != nil {
|
|
return len(want), added, renamed, fmt.Errorf("imp_M2MDepoRecipients %s: %s", inn, quikErrorDetail(err))
|
|
}
|
|
if ok {
|
|
renamed++
|
|
} else {
|
|
added++
|
|
}
|
|
}
|
|
return len(want), added, renamed, nil
|
|
}
|
|
|
|
// quikRecipientsRun — один проход с учётом окна (до 20:00) и записью состояния.
|
|
func quikRecipientsRun(ctx context.Context, rc *RuntimeConfig, why string) {
|
|
if rc == nil || !moduleAllowed(rc, "quik-mnp") || time.Now().Hour() >= quikRecipientsLastHour {
|
|
return
|
|
}
|
|
quikRecipientsState.Lock()
|
|
if quikRecipientsState.running {
|
|
quikRecipientsState.Unlock()
|
|
return
|
|
}
|
|
quikRecipientsState.running = true
|
|
quikRecipientsState.Unlock()
|
|
total, added, renamed, err := quikRecipientsSync(ctx, rc)
|
|
quikRecipientsState.Lock()
|
|
quikRecipientsState.running = false
|
|
quikRecipientsState.At, quikRecipientsState.Total, quikRecipientsState.Added, quikRecipientsState.Renamed = time.Now(), total, added, renamed
|
|
prevErr := quikRecipientsState.Err
|
|
quikRecipientsState.Err = ""
|
|
if err != nil {
|
|
quikRecipientsState.Err = err.Error()
|
|
}
|
|
quikRecipientsState.Unlock()
|
|
switch {
|
|
case err != nil:
|
|
log.Printf("quik: справочник получателей M2M (%s): %v", why, err)
|
|
if prevErr == "" {
|
|
raiseSystemAlert(rc, "QUIK МНП: справочник депозитариев-получателей не обновлён",
|
|
"bj не смог обновить справочник «Депозитарии получателей для переводов M2M» в базе МНП: "+err.Error()+". Без записи о депозитарии получателя МНП отвергает поручения на вывод, и перевод не уйдёт в НРД.")
|
|
}
|
|
case added > 0 || renamed > 0:
|
|
log.Printf("quik: справочник получателей M2M обновлён (%s): добавлено %d, наименований исправлено %d, всего по справочнику НРД %d — модуль МНП применит после перезапуска", why, added, renamed, total)
|
|
}
|
|
}
|
|
|
|
// quikRecipientsSyncAsync — справочник НРД изменился: обновить и в МНП.
|
|
func quikRecipientsSyncAsync(rc *RuntimeConfig) {
|
|
go quikRecipientsRun(context.Background(), rc, "изменился справочник НРД")
|
|
}
|
|
|
|
// quikRecipientsLoop — раз в 30 минут до 20:00.
|
|
func (s *Server) quikRecipientsLoop(ctx context.Context) {
|
|
t := time.NewTimer(time.Minute)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
quikRecipientsRun(ctx, s.rc, "плановое")
|
|
t.Reset(quikRecipientsEvery)
|
|
}
|
|
}
|
|
}
|
|
|
|
// quikRecipientsInfo — строка о справочнике получателей для страницы «Поручения QUIK».
|
|
func quikRecipientsInfo() string {
|
|
quikRecipientsState.Lock()
|
|
defer quikRecipientsState.Unlock()
|
|
st := struct {
|
|
At time.Time
|
|
Total, Added, Renamed int
|
|
Err string
|
|
}{quikRecipientsState.At, quikRecipientsState.Total, quikRecipientsState.Added, quikRecipientsState.Renamed, quikRecipientsState.Err}
|
|
switch {
|
|
case st.At.IsZero():
|
|
return "Справочник депозитариев-получателей M2M в МНП: ещё не сверялся после запуска bj (сверка раз в 30 мин до 20:00)."
|
|
case st.Err != "":
|
|
return "Справочник депозитариев-получателей M2M в МНП: ошибка обновления " + st.At.Format("02.01 15:04") + " — " + st.Err
|
|
}
|
|
s := fmt.Sprintf("Справочник депозитариев-получателей M2M в МНП сверен со справочником НРД %s: %d депозитариев", st.At.Format("02.01 15:04"), st.Total)
|
|
if st.Added > 0 || st.Renamed > 0 {
|
|
s += fmt.Sprintf(" (добавлено %d, исправлено наименований %d — модуль МНП применит после ночного перезапуска)", st.Added, st.Renamed)
|
|
}
|
|
return s + ". Обновляется раз в 30 мин до 20:00."
|
|
}
|