package lkgateway // manual_depo.go — подбор клиента и бумаг из DEPO для ручного ввода: поиск по ФИО или номеру // документа, открытые торговые счета, бумаги на торговом разделе с остатком. import ( "database/sql" "encoding/json" "net/http" "strconv" "strings" ) // manualDepoAcc — открытый торговый счёт клиента. type manualDepoAcc struct { ClientID int64 `json:"client_id"` Number string `json:"number"` Contract string `json:"contract,omitempty"` IIS bool `json:"iis,omitempty"` } // manualDepoFace — клиент из картотеки DEPO. type manualDepoFace struct { LastName string `json:"last_name"` FirstName string `json:"first_name"` MiddleName string `json:"middle_name,omitempty"` Name string `json:"name"` Birth string `json:"birth,omitempty"` // ГГГГ-ММ-ДД DocType string `json:"doc_type,omitempty"` DocSer string `json:"doc_ser,omitempty"` DocNum string `json:"doc_num"` DocDate string `json:"doc_date,omitempty"` DocIssuer string `json:"doc_issuer,omitempty"` Phone string `json:"phone,omitempty"` Accounts []manualDepoAcc `json:"accounts"` } // manualDepoPos — бумага на торговом разделе клиента. type manualDepoPos struct { ISIN string `json:"isin"` Name string `json:"name"` Qty float64 `json:"qty"` Russian bool `json:"russian"` } // depoDocTypeNSD — вид документа DEPO → код справочника НРД (паспорт РФ в DEPO — 20). func depoDocTypeNSD(t int64) string { if t == 20 { return "21" } return "" } // manualDepoSearch — клиенты с открытым торговым счётом по ФИО (часть, без учёта регистра) или номеру документа. func manualDepoSearch(rc *RuntimeConfig, q string) ([]manualDepoFace, string) { q = strings.Join(strings.Fields(q), " ") if len([]rune(q)) < 3 { return nil, "введите не меньше 3 символов фамилии или номер документа" } if !manualDepoConfigured(rc) { return nil, "учётная система (DEPO) не подключена" } fs := rc.Snapshot().Fancy db, ctx, cancel, errS := depoOpen(fs) if errS != "" { return nil, errS } defer cancel() defer db.Close() digits := strings.ReplaceAll(q, " ", "") // Действующая версия карточки (LAST_FLAG=1); счета привязаны к исходной записи (SELF_ID). rows, err := db.QueryContext(ctx, `SELECT TOP 20 ISNULL(NULLIF(f.SELF_ID,0),f.ID), ISNULL(f.NAME,''), f.DATE_BIRTH, ISNULL(f.DOC_TYPE,0), ISNULL(f.DOC_SER,''), ISNULL(f.DOC_NUM,''), f.DOC_DAT, ISNULL(f.DOC_PLACE,''), ISNULL(f.DOC_CODE,''), ISNULL(f.PHONE,'') FROM OD_FACES f WHERE ISNULL(f.LAST_FLAG,1)=1 AND (UPPER(f.NAME) LIKE UPPER(@q) OR f.DOC_NUM=@d OR REPLACE(ISNULL(f.DOC_SER,''),' ','')+f.DOC_NUM=@d) AND EXISTS (SELECT 1 FROM OD_CLIENTS c WHERE c.FACE=ISNULL(NULLIF(f.SELF_ID,0),f.ID) AND c.NUMBER LIKE N'ВТНКЦ%' AND c.STATE=0 AND c.D_CLOSE IS NULL) ORDER BY f.NAME`, sql.Named("q", q+"%"), sql.Named("d", digits)) if err != nil { return nil, "поиск в DEPO: " + err.Error() } type faceRow struct { id int64 f manualDepoFace } var found []faceRow for rows.Next() { var r faceRow var birth, docDat sql.NullTime var docType int64 var place, code string if rows.Scan(&r.id, &r.f.Name, &birth, &docType, &r.f.DocSer, &r.f.DocNum, &docDat, &place, &code, &r.f.Phone) != nil { continue } p := strings.Fields(r.f.Name) if len(p) > 0 { r.f.LastName = p[0] } if len(p) > 1 { r.f.FirstName = p[1] } if len(p) > 2 { r.f.MiddleName = strings.Join(p[2:], " ") } if birth.Valid { r.f.Birth = birth.Time.Format("2006-01-02") } if docDat.Valid { r.f.DocDate = docDat.Time.Format("2006-01-02") } r.f.DocType = depoDocTypeNSD(docType) r.f.DocSer = strings.ReplaceAll(r.f.DocSer, " ", "") r.f.DocIssuer = strings.TrimSpace(place) if code = strings.TrimSpace(code); code != "" { r.f.DocIssuer += ", код подразделения " + code } found = append(found, r) } rows.Close() out := make([]manualDepoFace, 0, len(found)) for _, r := range found { ar, err := db.QueryContext(ctx, `SELECT c.ID, ISNULL(c.NUMBER,''), ISNULL((SELECT TOP 1 p.PORT_NUM FROM OD_CLIENTS_PORTS p WHERE p.CLIENT=c.ID ORDER BY p.PORT_DATE, p.PORT_NUM),'') FROM OD_CLIENTS c WHERE c.FACE=@f AND c.NUMBER LIKE N'ВТНКЦ%' AND c.STATE=0 AND c.D_CLOSE IS NULL ORDER BY c.ID`, sql.Named("f", r.id)) if err == nil { for ar.Next() { var a manualDepoAcc if ar.Scan(&a.ClientID, &a.Number, &a.Contract) == nil { r.f.Accounts = append(r.f.Accounts, a) } } ar.Close() } for i := range r.f.Accounts { ok, _ := depoAccountIIS(ctx, db, fs, r.f.Accounts[i].ClientID) r.f.Accounts[i].IIS = !ok } out = append(out, r.f) } if len(out) == 0 { return nil, "клиентов с открытым торговым счётом не найдено" } return out, "" } // manualDepoPositions — бумаги с положительным остатком на торговом разделе счёта (98070.04). func manualDepoPositions(rc *RuntimeConfig, clientID int64) ([]manualDepoPos, string) { if !manualDepoConfigured(rc) { return nil, "учётная система (DEPO) не подключена" } db, ctx, cancel, errS := depoOpen(rc.Snapshot().Fancy) if errS != "" { return nil, errS } defer cancel() defer db.Close() rows, err := db.QueryContext(ctx, `SELECT ISNULL(v.ISIN,''), ISNULL(v.NAME,''), SUM(CAST(r.K AS float)) FROM OD_RESTS r JOIN OD_VALUES v ON v.ID=r.VALUE_ID WHERE r.REG_1=@c AND r.BAL_ACC=@b GROUP BY v.ISIN, v.NAME HAVING SUM(CAST(r.K AS float)) > 0 ORDER BY v.NAME`, sql.Named("c", clientID), sql.Named("b", depoTradeBalAcc)) if err != nil { return nil, "остатки в DEPO: " + err.Error() } defer rows.Close() var out []manualDepoPos for rows.Next() { var p manualDepoPos if rows.Scan(&p.ISIN, &p.Name, &p.Qty) == nil { p.ISIN = strings.ToUpper(strings.TrimSpace(p.ISIN)) p.Russian = isRussianISIN(p.ISIN) out = append(out, p) } } return out, "" } // registerManualDepo — GET /admin/manual/depo/search?q= и /admin/manual/depo/positions?client=ID (JSON). func registerManualDepo(mux *http.ServeMux, rc *RuntimeConfig) { reply := func(w http.ResponseWriter, v any, errS string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(map[string]any{"items": v, "error": errS}) } mux.HandleFunc("/admin/manual/depo/search", func(w http.ResponseWriter, r *http.Request) { items, errS := manualDepoSearch(rc, r.URL.Query().Get("q")) reply(w, items, errS) }) mux.HandleFunc("/admin/manual/depo/positions", func(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(r.URL.Query().Get("client"), 10, 64) if err != nil || id <= 0 { reply(w, nil, "не указан счёт") return } items, errS := manualDepoPositions(rc, id) reply(w, items, errS) }) }