Files
Bridge-and-Join-s/internal/lkgateway/quikrecipients.go
T

194 lines
8.1 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
}
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."
}