package lkgateway // depo_detect.go — автоопределение реквизитов депозитария из базы DEPO (Fancy) при первичной // настройке: депозитарный счёт и разделы в НРД, код участника, карточка организации. // Человек проверяет предложенные значения в форме и сохраняет сам — в настройки ничего не // пишется автоматически. import ( "context" "database/sql" "fmt" "net/http" "regexp" "sort" "strings" "sync" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" ) // DepoRequisites — что удалось определить в DEPO. type DepoRequisites struct { AccountID string // депозитарный счёт в НРД (HL…) DeponentCode string // код участника/депонента (MC…) SectionID string // раздел для M2M (36<код>F00) Sections []string // все открытые разделы счёта OrgCode string // = DeponentCode OrgINN string OrgName string OrgShort string OrgEng string Notes []string // что не нашли / на что обратить внимание At time.Time } var ( depoSectionRe = regexp.MustCompile(`^(\d{2})([A-Z]{2}[0-9A-Z]{10})(F\d{2})$`) depoAccountRe = regexp.MustCompile(`^[A-Z]{2}[0-9A-Z]{10}$`) ) // lastDepoDetect — последний результат автоопределения (в памяти процесса): его показывают // карточки настроек как предложение, пока поля не сохранены. var lastDepoDetect struct { mu sync.Mutex r *DepoRequisites } func depoDetectResult() *DepoRequisites { lastDepoDetect.mu.Lock() defer lastDepoDetect.mu.Unlock() return lastDepoDetect.r } // depoDetectRequisites — читает DEPO и собирает реквизиты. Ничего не пишет. func depoDetectRequisites(ctx context.Context, fs FancyMSSQLSettings) (*DepoRequisites, error) { if strings.TrimSpace(fs.Server) == "" { return nil, fmt.Errorf("подключение к DEPO не настроено") } db, err := fancyOpen(fs, fs.DepoDB()) if err != nil { return nil, err } defer db.Close() ctx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() res := &DepoRequisites{At: time.Now()} rows, err := db.QueryContext(ctx, fsql.Det01) if err != nil { return nil, fmt.Errorf("счета в НРД (OD_CLIENTS): %w", err) } accounts := map[string]int{} codes := map[string]int{} sections := map[string]bool{} for rows.Next() { var number, name string var state int if rows.Scan(&number, &state, &name) != nil { continue } parts := strings.Split(strings.TrimSpace(number), "/") if len(parts) < 4 { continue } acc, sec := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[len(parts)-1]) if depoAccountRe.MatchString(acc) { accounts[acc]++ } if m := depoSectionRe.FindStringSubmatch(sec); m != nil { codes[m[2]]++ if state == 0 { sections[sec] = true } } } rows.Close() if len(accounts) == 0 { res.Notes = append(res.Notes, "счета депозитария в НРД в DEPO не найдены (нет счетов у лица «НКО АО НРД», ИНН 7702165310) — реквизиты вводятся вручную из договора с НРД") return res, nil } res.AccountID = mostCommon(accounts) res.DeponentCode = mostCommon(codes) for s := range sections { res.Sections = append(res.Sections, s) } sort.Strings(res.Sections) // раздел для M2M — торговый «36<код>F00» (по договорённости с Fancy); иначе первый 36…, иначе первый открытый want := "36" + res.DeponentCode + "F00" switch { case sections[want]: res.SectionID = want default: for _, s := range res.Sections { if strings.HasPrefix(s, "36") { res.SectionID = s break } } if res.SectionID == "" && len(res.Sections) > 0 { res.SectionID = res.Sections[0] } res.Notes = append(res.Notes, "раздела "+want+" нет среди открытых — проверьте выбранный раздел") } if len(accounts) > 1 { res.Notes = append(res.Notes, fmt.Sprintf("в DEPO несколько депозитарных счетов в НРД (%d) — выбран самый частый %s", len(accounts), res.AccountID)) } // организация по коду участника if res.DeponentCode != "" { var name, inn, mnem, eng string err := db.QueryRowContext(ctx, fsql.Det02, sql.Named("code", res.DeponentCode)).Scan(&name, &inn, &mnem, &eng) switch { case err == sql.ErrNoRows: res.Notes = append(res.Notes, "лицо с кодом "+res.DeponentCode+" в DEPO не найдено — ИНН и наименование организации введите вручную") case err != nil: res.Notes = append(res.Notes, "карточка организации: "+err.Error()) default: res.OrgCode, res.OrgINN, res.OrgName, res.OrgShort, res.OrgEng = res.DeponentCode, strings.TrimSpace(inn), strings.TrimSpace(name), strings.TrimSpace(mnem), strings.TrimSpace(eng) } } return res, nil } func mostCommon(m map[string]int) string { best, n := "", -1 for k, v := range m { if v > n || (v == n && k < best) { best, n = k, v } } return best } // Summary — короткая строка для сообщения. func (r *DepoRequisites) Summary() string { if r == nil { return "" } var b []string if r.AccountID != "" { b = append(b, "счёт "+r.AccountID) } if r.DeponentCode != "" { b = append(b, "код "+r.DeponentCode) } if r.SectionID != "" { b = append(b, "раздел "+r.SectionID) } if r.OrgName != "" { b = append(b, r.OrgName+" (ИНН "+r.OrgINN+")") } return strings.Join(b, ", ") } // detectFancyRequisites — POST /admin/setup/fancy/detect: определить реквизиты в DEPO и показать // их как предложение в карточках «ИШ НРД» и «Организация» (и в мастере). func (h *setupHandlers) detectFancyRequisites(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method", http.StatusMethodNotAllowed) return } res, err := depoDetectRequisites(r.Context(), h.rc.Snapshot().Fancy) if err != nil { setupFlash(w, r, "DEPO: реквизиты не определены — "+err.Error()) return } lastDepoDetect.mu.Lock() lastDepoDetect.r = res lastDepoDetect.mu.Unlock() msg := "Из DEPO определено: " + res.Summary() + ". Значения подставлены в пустые поля — проверьте и нажмите «Сохранить»." if res.AccountID == "" { msg = "DEPO: " + strings.Join(res.Notes, "; ") } else if len(res.Notes) > 0 { msg += " Замечания: " + strings.Join(res.Notes, "; ") } setupFlash(w, r, msg) }