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 } if strings.TrimSpace(rc.Snapshot().Quik.Server) == "" { // МНП не подключён — сверять не с чем 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 { if globalRC != nil && strings.TrimSpace(globalRC.Snapshot().Quik.Server) == "" { return "Справочник депозитариев-получателей M2M в МНП bj заполнит сам, как только будет подключена база МНП (выше)." } 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." }