Files
Bridge-and-Join-s/internal/lkgateway/depo_detect.go
T
fontvielle 03918b1e58 Firebird: автоопределение из DEPO как на MSSQL — реквизиты организации и поиск без учёта регистра
Проверено по шагам мастера, что bj берёт из DEPO сам: подготовка базы, префикс счетов (шаг 3), реквизиты
(шаг 4), готовность (шаг 10), ручной ввод, подбор клиента для портфеля ЛК.
- Реквизиты (Det01): счёт НРД ссылается на исходную версию лица, а ИНН НРД бывает только в новых (в исходной —
  ИНН НДЦ) — лицо по любой версии (ID или SELF_ID; ИНН НРД или MNEM «НДЦ»); номер счёта — и «счёт/раздел»,
  не только «счёт/LEI/код/раздел». Счёт НРД — тот, с которым записан выбранный раздел (пара «раздел + счёт»
  — по ней процедуры находят место хранения), а не самый частый. Для MSSQL — только шире: прежнее находится.
- Поиск по ФИО, договору и счёту — без учёта регистра, как в MSSQL (Cyrillic_General_CI_AS): в базе Fancy
  с кодировкой NONE регистр кириллицы различается — Firebird-варианты сравнивают в WIN1251 (подбор клиента
  для портфеля ЛК — запросы вынесены в fsql/lkportfolio.go, поиск и список договоров).
- На копии базы: шаг 4 — счёт, код, разделы, ИНН и наименования организации; поиск «бр-1147», «бубликов»
  находит; FBLivePrepare — 103 из 103.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-27 20:09:59 +03:00

229 lines
9.0 KiB
Go

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 // краткое (OD_FACES.NAME)
OrgShort string // = OrgName (MNEM в DEPO — мнемоника, не наименование)
OrgEng string // английское краткое
OrgFull string // полное (OD_U_FACES.FULL_NAME)
OrgEngFull 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{}
pairs := map[string]map[string]int{} // открытый раздел → счета, с которыми он записан в DEPO
for rows.Next() {
var number, name string
var state int
if rows.Scan(&number, &state, &name) != nil {
continue
}
parts := strings.Split(strings.TrimSpace(number), "/") // счёт/LEI/код/раздел или счёт/раздел
if len(parts) < 2 {
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
if depoAccountRe.MatchString(acc) {
if pairs[sec] == nil {
pairs[sec] = map[string]int{}
}
pairs[sec][acc]++
}
}
}
}
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+" нет среди открытых — проверьте выбранный раздел")
}
// счёт — тот, с которым выбранный раздел записан в DEPO: по паре «раздел + счёт» процедуры обработки
// находят место хранения (у организации бывает несколько счетов в НРД с торговым разделом)
if p := pairs[res.SectionID]; len(p) > 0 {
res.AccountID = mostCommon(p)
if len(p) > 1 {
var other []string
for a := range p {
if a != res.AccountID {
other = append(other, a)
}
}
sort.Strings(other)
res.Notes = append(res.Notes, fmt.Sprintf("раздел %s записан в DEPO с несколькими счетами в НРД — выбран %s, есть ещё %s: проверьте по договору с НРД", res.SectionID, res.AccountID, strings.Join(other, ", ")))
}
} else 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, full, engFull string
err := db.QueryRowContext(ctx, fsql.Det03, sql.Named("code", res.DeponentCode)).Scan(&name, &inn, &mnem, &eng, &full, &engFull)
if err != nil && err != sql.ErrNoRows {
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:
_ = mnem
res.OrgCode, res.OrgINN, res.OrgName, res.OrgShort, res.OrgEng = res.DeponentCode, strings.TrimSpace(inn), strings.TrimSpace(name), strings.TrimSpace(name), strings.TrimSpace(eng)
res.OrgFull, res.OrgEngFull = strings.TrimSpace(full), strings.TrimSpace(engFull)
if res.OrgFull == "" {
res.Notes = append(res.Notes, "полного наименования организации в DEPO нет — для анкеты участника уточните его у НРД")
}
}
}
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)
}