diff --git a/internal/lkgateway/depo_detect.go b/internal/lkgateway/depo_detect.go new file mode 100644 index 0000000..7b7739d --- /dev/null +++ b/internal/lkgateway/depo_detect.go @@ -0,0 +1,197 @@ +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) +} diff --git a/internal/lkgateway/fancydb_handlers.go b/internal/lkgateway/fancydb_handlers.go index 0a94b51..5e6c67a 100644 --- a/internal/lkgateway/fancydb_handlers.go +++ b/internal/lkgateway/fancydb_handlers.go @@ -16,6 +16,7 @@ import ( func (h *setupHandlers) registerFancy(mux *http.ServeMux) { mux.HandleFunc("/admin/setup/fancy", h.saveFancy) mux.HandleFunc("/admin/setup/fancy/test", h.testFancy) + mux.HandleFunc("/admin/setup/fancy/detect", h.detectFancyRequisites) // реквизиты НРД/организации из DEPO mux.HandleFunc("/admin/setup/fancy/depo-job", h.saveFancyDepoJob) mux.HandleFunc("/admin/setup/fancy/depo-job/check", h.checkFancyDepoJob) mux.HandleFunc("/admin/setup/fancy/depo-job/create", h.createFancyDepoJob) diff --git a/internal/lkgateway/fsql/depodetect.go b/internal/lkgateway/fsql/depodetect.go new file mode 100644 index 0000000..98e2023 --- /dev/null +++ b/internal/lkgateway/fsql/depodetect.go @@ -0,0 +1,12 @@ +package fsql + +// Автоопределение реквизитов депозитария в DEPO (Fancy) для первичной настройки. + +// Det01 — счета депозитария в НРД: в DEPO это счета «лица НКО АО НРД» (ИНН 7702165310 одинаков у всех +// клиентов Fancy). NUMBER = <депозитарный счёт>//<код>/<раздел>. +var Det01 = `SELECT c.NUMBER, ISNULL(c.STATE,0), ISNULL(c.NAME,'') FROM OD_CLIENTS c JOIN OD_FACES f ON f.ID=c.FACE + WHERE f.INN='7702165310' AND c.NUMBER LIKE '%/%/%/%' ORDER BY c.STATE, c.NUMBER` + +// Det02 — карточка организации по коду участника НРД (кодер лица). +var Det02 = `SELECT TOP 1 ISNULL(f.NAME,''), ISNULL(f.INN,''), ISNULL(f.MNEM,''), ISNULL(f.ENG_NAME,'') + FROM OD_FACE_CODERS fc JOIN OD_FACES f ON f.ID=fc.FACE WHERE fc.CODE=@code ORDER BY f.E_DATE DESC, f.ID DESC` diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 30a2665..f11824f 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -165,6 +165,7 @@ type WizardData struct { page Step int Settings Settings + Suggest *DepoRequisites // предложение реквизитов из DEPO (после «Определить из DEPO») Certs []cryptocli.Certificate Flash string Done struct { @@ -188,6 +189,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) { d := WizardData{ page: nowPage("Мастер настройки", "wizard"), Settings: s, + Suggest: depoDetectResult(), Certs: h.listCertsForUI(), Flash: r.URL.Query().Get("flash"), } @@ -300,6 +302,7 @@ func (h *setupHandlers) testSignCrypto(w http.ResponseWriter, r *http.Request) { type SetupData struct { page Settings Settings + Suggest *DepoRequisites // предложение реквизитов из DEPO Readiness []Readiness ReadyCount int TotalCount int @@ -373,6 +376,7 @@ func (h *setupHandlers) renderSetup(w http.ResponseWriter, req *http.Request, fl Components: comps, UpdatesAvailable: anyUpdate, Settings: s, + Suggest: depoDetectResult(), Readiness: r, ReadyCount: ready, TotalCount: len(r), diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index eb5ee5e..a5a8d3e 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -542,11 +542,12 @@ function hbBind(inp,box,list,onPick){ {{if not .Participants}}Справочник участников ещё не получен — запросите его в «Диагностике» или введите данные вручную.{{end}} - + - + - + + {{if .Suggest}}{{if .Suggest.OrgName}}Из DEPO: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}} — подставлено в пустые поля.{{end}}{{end}}
@@ -581,12 +582,18 @@ function hbBind(inp,box,list,onPick){

Депозитарные реквизиты (откуда списываются бумаги) — из договора/письма НРД. Нужны для формирования заявки на перевод.

+ {{if .Settings.Fancy.Server}}
+ + Счёт, код и раздел в НРД и карточка организации читаются из базы DEPO (Fancy) и подставляются в пустые поля — проверьте и сохраните. +
{{end}} + {{if .Suggest}}{{if .Suggest.AccountID}}
Найдено в DEPO: счёт {{.Suggest.AccountID}}, код {{.Suggest.DeponentCode}}, раздел {{.Suggest.SectionID}}{{if gt (len .Suggest.Sections) 1}} (открытых разделов: {{len .Suggest.Sections}}){{end}}{{if .Suggest.OrgName}}; организация {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}{{end}}.{{range .Suggest.Notes}}
⚠ {{.}}{{end}}
{{end}}{{end}} - + - + - + + {{if .Suggest}}{{if gt (len .Suggest.Sections) 1}}{{range .Suggest.Sections}}{{end}}{{end}}