Files
Bridge-and-Join-s/internal/lkgateway/manual_depo.go
fontvielle 312e4f70f1 ЛК на Fancy с Firebird: все запросы к DEPO, курсы, база SPECTRE
ЛК (bj-lk берёт данные у bj-server) сам работает с той СУБД, что выбрана для DEPO.
- Перевод на Firebird: YEAR/MONTH/DAY(x) — EXTRACT, AS float — AS DOUBLE PRECISION (float T-SQL — 8 байт,
  FLOAT Firebird — 4: точность сумм и цен в ЛК); шаблоны со сложением строк (история операций Hist02, поиск
  клиента Man01) — Firebird-тексты до подстановки (depoSQL); UPPER по кириллице — в WIN1251 (база NONE).
- Курсы валют (Fx01): на Firebird запрос от всех курсов шёл больше 200 с (лимит 15 с — курсов нет) — для
  Firebird от 20 валют, 0,2 с.
- База SPECTRE (архив портфеля и операций ЛК): поле в «Трансферные таблицы DEPO»; у Firebird — путь или строка
  подключения Fancy (может быть на другом компьютере); не задана — ЛК пишет, где указать. На данных SPECTRE
  не проверено: базы нет.
- Тест FBLiveLK: функции ЛК, ручного ввода, сверки переводов и QUIK на копии базы по договору К1 Бубликова —
  все работают, 0,07–0,26 с каждая; FBLivePrepare — 101 из 101 запроса.

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

251 lines
8.8 KiB
Go

package lkgateway
// manual_depo.go — подбор клиента и бумаг из DEPO для ручного ввода: поиск по ФИО или номеру
// документа, открытые торговые счета, бумаги на торговом разделе с остатком.
import (
"database/sql"
"encoding/json"
"fmt"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
"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).
// likeEscape: метасимволы LIKE (% _ [) в имени от пользователя означают сами себя,
// иначе поиск «расползается» по всей базе (wildcard-injection).
rows, err := db.QueryContext(ctx, accSQL(depoSQL(rc.Snapshot().Fancy, fsql.Man01)), sql.Named("q", likeEscape(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, accSQL(fsql.Man02), 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, fsql.Man03,
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)
})
}
// manualDepoClientByAccount — OD_CLIENTS.ID открытого торгового счёта по его номеру.
func manualDepoClientByAccount(rc *RuntimeConfig, number string) (int64, string) {
number = strings.TrimSpace(number)
if number == "" || !manualDepoConfigured(rc) {
return 0, ""
}
db, ctx, cancel, errS := depoOpen(rc.Snapshot().Fancy)
if errS != "" {
return 0, errS
}
defer cancel()
defer db.Close()
var id int64
err := db.QueryRowContext(ctx, fsql.Man04,
sql.Named("n", number)).Scan(&id)
if err != nil {
return 0, ""
}
return id, ""
}
// manualQtyOverAvailable — количество в поручении больше остатка на торговом разделе.
// Пусто — всё в порядке или остаток проверить нельзя (DEPO не подключена, счёт не найден):
// тогда решает сверка при сохранении. Смысл: поручение на 111 бумаг при 110 на счёте не
// должно даже сохраниться — дальше по цепочке ему всё равно идти некуда.
func manualQtyOverAvailable(rc *RuntimeConfig, depoClient string, o *ManualOrder) string {
if !manualDepoConfigured(rc) || o == nil {
return ""
}
id, _ := strconv.ParseInt(strings.TrimSpace(depoClient), 10, 64)
if id <= 0 {
id, _ = manualDepoClientByAccount(rc, o.Account)
}
if id <= 0 {
return ""
}
pos, errS := manualDepoPositions(rc, id)
if errS != "" {
return ""
}
for _, sec := range o.Securities {
isin := strings.ToUpper(strings.TrimSpace(sec.ISIN))
if isin == "" {
continue
}
q, err := strconv.ParseFloat(strings.ReplaceAll(strings.TrimSpace(sec.Qty), ",", "."), 64)
if err != nil {
continue
}
for _, p := range pos {
if p.ISIN == isin && q > p.Qty+1e-9 {
return fmt.Sprintf("По бумаге %s на торговом разделе счёта %s доступно %s шт., а в поручении %s — сохранить нельзя: укажите не больше доступного.",
isin, o.Account, strconv.FormatFloat(p.Qty, 'f', -1, 64), strings.TrimSpace(sec.Qty))
}
}
}
return ""
}