diff --git a/deploy/depo/fancy-m2m-firebird/README.md b/deploy/depo/fancy-m2m-firebird/README.md
index ce8fa2d..ce0df5b 100644
--- a/deploy/depo/fancy-m2m-firebird/README.md
+++ b/deploy/depo/fancy-m2m-firebird/README.md
@@ -27,7 +27,13 @@ bj проверит базу и функции Fancy (UDF), создаст ло
Он же — оператор Fancy (`OD_OPERATORS`, «робот», `U_FACE` = депозитарий): по оператору Fancy нумерует документы.
- Обработку запускает bj (SQL Server Agent нет).
- Запросы bj к DEPO (T-SQL) подключение к Firebird переводит само: `internal/lkgateway/fancyfb.go`, точные
- переводы — `fsql/fb.go`. Проверка — `go test -run FBLive` с `BJ_FB_TEST_DSN` на копии базы клиента.
+ переводы — `fsql/fb.go`. Проверка — `go test -run FBLive` с `BJ_FB_TEST_DSN` на копии базы клиента:
+ `FBLivePrepare` готовит все запросы, `FBLiveLK` выполняет функции ЛК, ручного ввода и сверки переводов по
+ договору (`BJ_FB_TEST_CONTRACT`, `BJ_FB_TEST_PASSPORT`, `BJ_FB_TEST_FIO`, `BJ_FB_TEST_ACC`).
+- Личный кабинет (модуль `bj-lk`) берёт данные у bj-server — с Firebird работает так же, отдельно ничего не
+ настраивается. Архив портфеля и операций читает базу брокерского учёта SPECTRE: у Firebird путь к ней задаётся
+ в «Обмен с DEPO» → «Трансферные таблицы DEPO» (не задан — ЛК пишет, где указать); на данных SPECTRE пока не
+ проверен.
## Установка вручную
diff --git a/internal/lkgateway/fancydb_handlers.go b/internal/lkgateway/fancydb_handlers.go
index 9cbf07d..1b7e854 100644
--- a/internal/lkgateway/fancydb_handlers.go
+++ b/internal/lkgateway/fancydb_handlers.go
@@ -102,6 +102,25 @@ func (h *setupHandlers) saveFancyDepoTables(w http.ResponseWriter, r *http.Reque
if m := strings.TrimSpace(r.FormValue("iis_contract_marker")); len([]rune(m)) <= 100 {
s.IISContractMarker = m
}
+ // база брокерского учёта SPECTRE (архив портфеля и операций в ЛК); у Firebird — путь или строка подключения Fancy
+ if sp := strings.TrimSpace(r.FormValue("spectre_database")); sp == "" {
+ s.SpectreDatabase = ""
+ } else if s.IsFirebird() {
+ if _, _, path, err := fbSplitConn(sp); err != nil || !validFBPath(path) {
+ msg := "путь — без кавычек, ; ? и #"
+ if err != nil {
+ msg = err.Error()
+ }
+ setupFlash(w, r, "База SPECTRE: "+msg)
+ return
+ }
+ s.SpectreDatabase = sp
+ } else if validDBName(sp) {
+ s.SpectreDatabase = sp
+ } else {
+ setupFlash(w, r, "База SPECTRE: имя — только латинские буквы/цифры/_, начинать с буквы")
+ return
+ }
mode := r.FormValue("depo_tables_mode")
if mode != "auto" && mode != "script" {
mode = ""
diff --git a/internal/lkgateway/fancyfb.go b/internal/lkgateway/fancyfb.go
index 632a4b0..3ef8402 100644
--- a/internal/lkgateway/fancyfb.go
+++ b/internal/lkgateway/fancyfb.go
@@ -5,7 +5,9 @@ package lkgateway
// держать второй текст в каждом месте вызова, подключение к Firebird переводит запросы само (fbConn):
// - точный текст Firebird — для запросов, которые механически не переводятся (fsql.FB, ключ — текст T-SQL);
// - остальное механически, вне строк и комментариев: без dbo., TOP n — FIRST n, ISNULL — COALESCE,
-// GETDATE() — CURRENT_TIMESTAMP, LEN — CHAR_LENGTH, N'…' — '…', «exec имя» — EXECUTE PROCEDURE имя;
+// GETDATE() — CURRENT_TIMESTAMP, LEN — CHAR_LENGTH, YEAR(x) — EXTRACT(YEAR FROM x), AS float — AS DOUBLE
+// PRECISION (float T-SQL — 8 байт, FLOAT Firebird — 4), N'…' — '…', «exec имя» — EXECUTE PROCEDURE имя;
+// - шаблоны (fmt, {ACC}) с тем, что механически не переводится, выбираются до подстановки — depoSQL;
// - именованные параметры @имя (sql.Named) — позиционные ? в порядке появления в тексте;
// - GUID в параметрах — в верхнем регистре (в таблицах обмена Firebird GUID — строка в верхнем регистре,
// uniqueidentifier MSSQL сравнивается без учёта регистра); время «2006-01-02T15:04:05» — с пробелом вместо T.
@@ -254,11 +256,24 @@ var (
fbIsNullRe = regexp.MustCompile(`(?i)\bISNULL\s*\(`)
fbGetDateRe = regexp.MustCompile(`(?i)\bGETDATE\s*\(\s*\)`)
fbLenRe = regexp.MustCompile(`(?i)\bLEN\s*\(`)
+ fbDatePartRe = regexp.MustCompile(`(?i)\b(YEAR|MONTH|DAY)\s*\(\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\)`)
+ fbFloatRe = regexp.MustCompile(`(?i)\bAS\s+float\b`)
fbTopRe = regexp.MustCompile(`(?i)\bSELECT(\s+DISTINCT)?\s+TOP\s*(\(\s*@?\w+\s*\)|\d+)`)
fbGUIDRe = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
fbISOTimeRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?$`)
)
+// depoSQL — текст запроса-шаблона для СУБД базы DEPO: для Firebird — перевод из fsql.FB, если он есть (шаблон с
+// fmt-подстановкой или {ACC} подключение к Firebird по тексту уже не узнает). Остальное переводит fbConn.
+func depoSQL(f FancyMSSQLSettings, q string) string {
+ if f.IsFirebird() {
+ if t, ok := fsql.FB[q]; ok {
+ return t
+ }
+ }
+ return q
+}
+
// fbSQL — текст запроса для Firebird: точный перевод из fsql.FB, иначе механический.
func fbSQL(q string) string {
if t, ok := fsql.FB[q]; ok {
@@ -275,6 +290,8 @@ func fbSQL(q string) string {
code = fbIsNullRe.ReplaceAllString(code, "COALESCE(")
code = fbGetDateRe.ReplaceAllString(code, "CURRENT_TIMESTAMP")
code = fbLenRe.ReplaceAllString(code, "CHAR_LENGTH(")
+ code = fbDatePartRe.ReplaceAllString(code, "EXTRACT($1 FROM $2)")
+ code = fbFloatRe.ReplaceAllString(code, "AS DOUBLE PRECISION") // float T-SQL — 8 байт, FLOAT Firebird — 4
return fbTopRe.ReplaceAllStringFunc(code, func(s string) string {
m := fbTopRe.FindStringSubmatch(s)
return "SELECT FIRST " + m[2] + m[1] // в Firebird FIRST — до DISTINCT
diff --git a/internal/lkgateway/fancyfb_live_test.go b/internal/lkgateway/fancyfb_live_test.go
index 6bd26f5..d802b8e 100644
--- a/internal/lkgateway/fancyfb_live_test.go
+++ b/internal/lkgateway/fancyfb_live_test.go
@@ -14,6 +14,7 @@ import (
"go/token"
"os"
"path/filepath"
+ "regexp"
"sort"
"strconv"
"strings"
@@ -25,8 +26,10 @@ import (
func readRepoFile(p string) ([]byte, error) { return os.ReadFile(filepath.Join("..", "..", p)) }
-// fbLiveTodo — личный кабинет и ручной ввод: на Firebird — следующий этап (YEAR(), %, сложение строк).
-var fbLiveTodo = map[string]bool{"Arch02": true, "Arch03": true, "Arch07": true, "Arch08": true, "Arch10": true, "Hist02": true, "Man01": true}
+// fbLiveTodo — запросы, которые на Firebird пока не готовятся (пусто — все).
+var fbLiveTodo = map[string]bool{}
+
+var fbLiveFmtRe = regexp.MustCompile(`%(\[\d\])?s`)
// fbLiveSkip — не к DEPO (PostgreSQL bj и экспорт QUIK, база МНП QUIK, SPECTRE) или куски составных запросов.
var fbLiveSkip = map[string]bool{
@@ -66,9 +69,12 @@ func fbLiveQueries(t *testing.T) map[string]string {
if err != nil {
continue
}
+ if t, ok := fsql.FB[q]; ok { // шаблон: Firebird-вариант выбирается до подстановки (depoSQL)
+ q = t
+ }
q = strings.ReplaceAll(q, "{ACC}", "ВТНКЦ")
q = strings.ReplaceAll(q, "%d", "10")
- q = strings.ReplaceAll(q, "%s", "1")
+ q = fbLiveFmtRe.ReplaceAllString(q, "1")
out[n.Name] = q
}
}
diff --git a/internal/lkgateway/fancyfb_lk_live_test.go b/internal/lkgateway/fancyfb_lk_live_test.go
new file mode 100644
index 0000000..818d998
--- /dev/null
+++ b/internal/lkgateway/fancyfb_lk_live_test.go
@@ -0,0 +1,165 @@
+package lkgateway
+
+// Живая проверка ЛК, ручного ввода и сверки переводов на Fancy на Firebird: функции выполняются на копии базы
+// клиента по договору с бумагами. Запуск:
+// BJ_FB_TEST_DSN='firebird://SYSDBA:пароль@127.0.0.1:3050/путь?charset=WIN1251' BJ_FB_TEST_CONTRACT='БР-1147' \
+// BJ_FB_TEST_ACC='Б' BJ_FB_TEST_PASSPORT='3453 289090' BJ_FB_TEST_FIO='Бубликов Павел Иванович' \
+// go test -run FBLiveLK -v ./internal/lkgateway
+
+import (
+ "context"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+func fbLiveSettings(t *testing.T) FancyMSSQLSettings {
+ dsn := os.Getenv("BJ_FB_TEST_DSN")
+ if dsn == "" || os.Getenv("BJ_FB_TEST_CONTRACT") == "" {
+ t.Skip("BJ_FB_TEST_DSN / BJ_FB_TEST_CONTRACT не заданы")
+ }
+ u, err := url.Parse(dsn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ pass, _ := u.User.Password()
+ port, _ := strconv.Atoi(u.Port())
+ return FancyMSSQLSettings{DBType: "firebird", Server: u.Hostname(), Port: port, User: u.User.Username(), Password: pass,
+ DepoDatabase: strings.TrimPrefix(u.Path, "/"), AccountPrefix: os.Getenv("BJ_FB_TEST_ACC")}
+}
+
+func TestFBLiveLK(t *testing.T) {
+ s := fbLiveSettings(t)
+ rc, err := NewRuntimeConfig(t.TempDir() + "/setup.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := rc.UpdateFancy(s); err != nil {
+ t.Fatal(err)
+ }
+ prev := globalRC
+ globalRC = rc
+ defer func() { globalRC = prev }()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ defer cancel()
+ contract := os.Getenv("BJ_FB_TEST_CONTRACT")
+ last := time.Now()
+ check := func(what string, err error, n int, sample any) {
+ took := time.Since(last)
+ last = time.Now()
+ if took > 5*time.Second { // ЛК ждёт ответа: на MSSQL эти чтения — доли секунды
+ t.Errorf("%s: %v — медленно", what, took.Round(time.Millisecond))
+ }
+ if err != nil {
+ t.Errorf("%s: %v", what, err)
+ return
+ }
+ t.Logf("%s (%v): %d — %+v", what, took.Round(time.Millisecond), n, sample)
+ }
+ first := func(n int, v any) any {
+ if n == 0 {
+ return nil
+ }
+ return v
+ }
+
+ name, ok := depoContractExists(ctx, s, contract)
+ t.Logf("depoContractExists: %v %q", ok, name)
+ if !ok {
+ t.Errorf("договор %s не найден", contract)
+ }
+ pos, err := depoClientPositions(ctx, s, contract)
+ check("depoClientPositions", err, len(pos), pos)
+ lst, err := depoListContracts(ctx, s, "", 5)
+ check("depoListContracts", err, len(lst), first(len(lst), lst))
+ fnd, err := depoFindContracts(ctx, s, contract[:2], 5)
+ check("depoFindContracts", err, len(fnd), fnd)
+ t.Logf("depoSuggestContracts: %v", depoSuggestContracts(ctx, s))
+
+ ids, face, accs, err := depoClientIDs(ctx, s, contract)
+ check("depoClientIDs", err, len(ids), map[string]any{"face": face, "accounts": accs})
+ from, to := time.Now().AddDate(-1, 0, 0).Format("2006-01-02"), time.Now().Format("2006-01-02")
+ ops, err := lkOperations(ctx, s, contract, from, to, 50)
+ check("lkOperations", err, len(ops), first(len(ops), ops))
+ inc, tax, err := lkIncome(ctx, s, contract, from, to)
+ check("lkIncome", err, len(inc), map[string]any{"tax": tax})
+
+ if s.SpectreDatabase == "" { // архив ЛК — из базы брокерского учёта SPECTRE; без неё — понятная ошибка
+ _, err := lkArchivePortfolio(ctx, s, contract, time.Now())
+ t.Logf("lkArchivePortfolio без SPECTRE: %v", err)
+ last = time.Now()
+ } else {
+ ap, err := lkArchivePortfolio(ctx, s, contract, time.Now())
+ check("lkArchivePortfolio", err, 1, ap)
+ aops, err := lkArchiveOperations(ctx, s, contract, time.Now().AddDate(-1, 0, 0), time.Now())
+ check("lkArchiveOperations", err, len(aops), first(len(aops), aops))
+ }
+
+ var isins []string
+ var lkpos []LKPosition
+ for _, p := range pos {
+ isins = append(isins, p.ISIN)
+ lkpos = append(lkpos, LKPosition{ISIN: p.ISIN, Name: p.SecurityName, Quantity: p.Quantity})
+ }
+ ca, err := lkCorpActions(ctx, s, isins)
+ check("lkCorpActions", err, len(ca), first(len(ca), ca))
+ docs, err := lkReadyDocuments(ctx, s, contract)
+ check("lkReadyDocuments", err, len(docs), first(len(docs), docs))
+ up := lkUpcoming(ctx, s, lkpos, 400)
+ check("lkUpcoming", nil, len(up), first(len(up), up))
+ fx := lkFxAccounting(ctx, s)
+ check("lkFxAccounting", nil, len(fx), fx)
+ if len(fx) == 0 {
+ t.Error("lkFxAccounting: нет курсов валют")
+ }
+ for _, isin := range isins {
+ card, err := securityCard(ctx, rc, isin, nil)
+ check("securityCard "+isin, err, 1, card)
+ }
+
+ cur, cands, err := depoAccPrefixInfo(ctx, s)
+ check("depoAccPrefixInfo", err, cur, cands)
+ if req, err := depoDetectRequisites(ctx, s); err != nil {
+ t.Errorf("depoDetectRequisites: %v", err)
+ } else {
+ t.Logf("depoDetectRequisites: %+v", *req)
+ }
+ ver, upd, err := depoVersion(ctx, s)
+ t.Logf("depoVersion: %q %q %v", ver, upd, err)
+
+ fio := strings.Fields(os.Getenv("BJ_FB_TEST_FIO"))
+ pp := strings.Fields(os.Getenv("BJ_FB_TEST_PASSPORT"))
+ if len(fio) == 3 && len(pp) == 2 {
+ faces, msg := manualDepoSearch(rc, fio[0])
+ t.Logf("manualDepoSearch(%s): %d %s — %+v", fio[0], len(faces), msg, first(len(faces), faces))
+ if len(faces) == 0 {
+ t.Errorf("manualDepoSearch: клиент %s не найден (%s)", fio[0], msg)
+ }
+ if len(pos) > 0 {
+ id, msg := manualDepoClientByAccount(rc, pos[0].AccountNum)
+ t.Logf("manualDepoClientByAccount(%s): %d %s", pos[0].AccountNum, id, msg)
+ mp, msg := manualDepoPositions(rc, id)
+ t.Logf("manualDepoPositions: %d %s — %+v", len(mp), msg, first(len(mp), mp))
+ }
+ var secs []IncomingSecurity
+ var outs []depoOutSec
+ for i, p := range pos {
+ ref := "FBREF" + strconv.Itoa(i)
+ secs = append(secs, IncomingSecurity{ReferenceID: ref, ISIN: p.ISIN, Quantity: "1"})
+ outs = append(outs, depoOutSec{ReferenceID: ref, ISIN: p.ISIN, Qty: "1"})
+ }
+ in := runDepoCheck(&IncomingTransfer{InvestorLast: fio[0], InvestorFirst: fio[1], InvestorMiddle: fio[2], DocSeries: pp[0], DocNumber: pp[1], Securities: secs})
+ t.Logf("runDepoCheck (зачисление): ran=%v err=%q investor=%q account=%q %s — %s", in.Ran, in.Err, in.Investor, in.Account, in.Recommend, in.Summary)
+ out := runDepoCheckOut(fio[0], fio[1], fio[2], pp[0], pp[1], outs, "", "")
+ t.Logf("runDepoCheckOut (списание): ran=%v err=%q investor=%q account=%q %s — %s", out.Ran, out.Err, out.Investor, out.Account, out.Recommend, out.Summary)
+ if in.Err != "" || out.Err != "" || in.Investor == "" || out.Investor == "" || in.Account == "" || out.Account == "" {
+ t.Errorf("сверка переводов: зачисление %q / списание %q, инвестор %q / %q", in.Err, out.Err, in.Investor, out.Investor)
+ }
+ code, contr, err := quikResolveClient(ctx, rc, pp[0], pp[1])
+ t.Logf("quikResolveClient: %q %q %v", code, contr, err)
+ }
+}
diff --git a/internal/lkgateway/fsql/fb.go b/internal/lkgateway/fsql/fb.go
index d4952eb..35a957b 100644
--- a/internal/lkgateway/fsql/fb.go
+++ b/internal/lkgateway/fsql/fb.go
@@ -50,6 +50,50 @@ var FB = map[string]string{
Hlth03: `SELECT COALESCE(MAX(ID),0) FROM FTV_M2M_EVENTS WHERE EVENT_TYPE STARTING WITH 'BJ_'`,
+ // сложение строк (+) — ||; UNION строк разной длины — CAST; параметр в DATEADD — CAST
+ Hist02: `SELECT FIRST %d * FROM (
+ SELECT d.R_DATE AS RD, d.PROC_DATE AS PD, COALESCE(d.NUM,'') AS NUM, d.STATE AS ST, CAST('зачисление' AS VARCHAR(32)) AS KIND,
+ z.DEPO AS ACC, COALESCE(v.ISIN,'') AS ISIN, COALESCE(v.NAME,'') AS SEC, CAST(COALESCE(z.AMOUNT,0) AS DOUBLE PRECISION) AS QTY,
+ CAST(TRIM(COALESCE(z.COMMENT,'') || ' ' || COALESCE(z.REJECT_REASON,'')) AS VARCHAR(600)) AS NOTE
+ FROM U_OP_INV_ZACH z JOIN OD_DOCS d ON d.ID = z.DOC LEFT JOIN OD_VALUES v ON v.ID = z.SHARE
+ WHERE z.DEPO IN (%s)
+ UNION ALL
+ SELECT d.R_DATE, d.PROC_DATE, COALESCE(d.NUM,''), d.STATE, 'списание',
+ p.DEPO, COALESCE(v.ISIN,''), COALESCE(v.NAME,''), -CAST(COALESCE(p.AMOUNT,0) AS DOUBLE PRECISION),
+ TRIM(COALESCE(p.COMMENT,'') || ' ' || COALESCE(p.REJECT_REASON,''))
+ FROM U_OP_INV_SPIS p JOIN OD_DOCS d ON d.ID = p.DOC LEFT JOIN OD_VALUES v ON v.ID = p.SHARE
+ WHERE p.DEPO IN (%s)
+ UNION ALL
+ SELECT d.R_DATE, d.PROC_DATE, COALESCE(d.NUM,''), d.STATE, 'перевод: списание',
+ t.D_DEPO, COALESCE(v.ISIN,''), COALESCE(v.NAME,''), -CAST(COALESCE(t.AMOUNT,0) AS DOUBLE PRECISION),
+ TRIM(COALESCE(t.COMMENT,'') || ' ' || COALESCE(t.REJECT_REASON,''))
+ FROM U_OP_INV_PEREVOD t JOIN OD_DOCS d ON d.ID = t.DOC LEFT JOIN OD_VALUES v ON v.ID = t.SHARE
+ WHERE t.D_DEPO IN (%s)
+ UNION ALL
+ SELECT d.R_DATE, d.PROC_DATE, COALESCE(d.NUM,''), d.STATE, 'перевод: зачисление',
+ t.K_DEPO, COALESCE(v.ISIN,''), COALESCE(v.NAME,''), CAST(COALESCE(t.AMOUNT,0) AS DOUBLE PRECISION),
+ TRIM(COALESCE(t.COMMENT,'') || ' ' || COALESCE(t.REJECT_REASON,''))
+ FROM U_OP_INV_PEREVOD t JOIN OD_DOCS d ON d.ID = t.DOC LEFT JOIN OD_VALUES v ON v.ID = t.SHARE
+ WHERE t.K_DEPO IN (%s)) x
+ WHERE x.RD >= @p1 AND x.RD < DATEADD(DAY, 1, CAST(@p2 AS TIMESTAMP))
+ ORDER BY x.RD DESC, x.NUM DESC`,
+
+ // сложение строк — ||; UPPER по кириллице — в WIN1251 (база Fancy — NONE, там UPPER меняет только латиницу)
+ Man01: `SELECT FIRST 20 COALESCE(NULLIF(f.SELF_ID,0),f.ID), COALESCE(f.NAME,''), f.DATE_BIRTH, COALESCE(f.DOC_TYPE,0), COALESCE(f.DOC_SER,''), COALESCE(f.DOC_NUM,''),
+ f.DOC_DAT, COALESCE(f.DOC_PLACE,''), COALESCE(f.DOC_CODE,''), COALESCE(f.PHONE,'')
+ FROM OD_FACES f
+ WHERE COALESCE(f.LAST_FLAG,1)=1 AND (UPPER(CAST(f.NAME AS VARCHAR(255) CHARACTER SET WIN1251)) LIKE UPPER(CAST(@q AS VARCHAR(255) CHARACTER SET WIN1251))
+ OR f.DOC_NUM=@d OR REPLACE(COALESCE(f.DOC_SER,''),' ','') || f.DOC_NUM=@d)
+ AND EXISTS (SELECT 1 FROM OD_CLIENTS c WHERE c.FACE=COALESCE(NULLIF(f.SELF_ID,0),f.ID) AND c.NUMBER LIKE '{ACC}%' AND c.STATE=0 AND c.D_CLOSE IS NULL)
+ ORDER BY f.NAME`,
+
+ // последние курсы валют: от 20 валют, а не от всех курсов — Firebird иначе перебирает миллионы строк
+ // с подзапросом на каждую (на MSSQL — доли секунды, на Firebird — больше 200 с)
+ Fx01: `SELECT UPPER(TRIM(v.SYSNAME)),
+ (SELECT FIRST 1 x.OFICDATE FROM OD_VALUES_RATES x WHERE x.VALUE_ID = v.ID AND x.MARKET = 0 AND x.RATE > 0 ORDER BY x.OFICDATE DESC),
+ (SELECT FIRST 1 CAST(x.RATE AS DOUBLE PRECISION) FROM OD_VALUES_RATES x WHERE x.VALUE_ID = v.ID AND x.MARKET = 0 AND x.RATE > 0 ORDER BY x.OFICDATE DESC)
+ FROM OD_VALUES v WHERE v.V_TYPE = 1`,
+
// SELECT без FROM в Firebird нет
QEvPosted: `SELECT (SELECT COUNT(*) FROM FTV_M2M WHERE GUID=@g),
(SELECT COUNT(*) FROM FTV_M2M_EVENTS WHERE GUID=@g AND EVENT_TYPE='BJ_ORDER_ACCEPTED' AND STATE='SUCCESS'),
diff --git a/internal/lkgateway/lkarchive.go b/internal/lkgateway/lkarchive.go
index c232487..acaeab1 100644
--- a/internal/lkgateway/lkarchive.go
+++ b/internal/lkgateway/lkarchive.go
@@ -42,6 +42,9 @@ type spectreRest struct {
// lkArchivePortfolio — портфель на дату по SPECTRE. date — в местном времени, конец дня включительно.
func lkArchivePortfolio(ctx context.Context, s FancyMSSQLSettings, contract string, date time.Time) (*LKPortfolio, error) {
+ if err := spectreMissing(s); err != nil {
+ return nil, err
+ }
c, ok := spectreContractFor(ctx, s, contract)
if !ok {
return nil, fmt.Errorf("договор %s в брокерском учёте не найден", lkContractNumber(contract))
@@ -329,6 +332,9 @@ func lkArchiveOperations(ctx context.Context, s FancyMSSQLSettings, contract str
}
func lkArchiveOperationsLoad(ctx context.Context, s FancyMSSQLSettings, contract string, from, to time.Time) ([]LKOperationRow, error) {
+ if err := spectreMissing(s); err != nil {
+ return nil, err
+ }
c, ok := spectreContractFor(ctx, s, contract)
if !ok {
return nil, fmt.Errorf("договор %s в брокерском учёте не найден", lkContractNumber(contract))
diff --git a/internal/lkgateway/lkclient.go b/internal/lkgateway/lkclient.go
index 9810537..3b408d7 100644
--- a/internal/lkgateway/lkclient.go
+++ b/internal/lkgateway/lkclient.go
@@ -9,6 +9,7 @@ package lkgateway
import (
"context"
"database/sql"
+ "fmt"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
"strings"
"sync"
@@ -50,12 +51,34 @@ var spectreContractCache = struct {
at map[string]time.Time
}{m: map[string]spectreContract{}, at: map[string]time.Time{}}
-// spectreOpen — подключение к копии SPECTRE тем же логином, что и к DEPO.
+// spectreOpen — подключение к копии SPECTRE тем же логином, что и к DEPO. Firebird: база SPECTRE — свой файл,
+// путь или строка подключения Fancy (может быть на другом компьютере, чем DEPO).
func spectreOpen(s FancyMSSQLSettings) (*sql.DB, error) {
+ if err := spectreMissing(s); err != nil {
+ return nil, err
+ }
db := strings.TrimSpace(s.SpectreDB())
+ if s.IsFirebird() {
+ host, port, path, err := fbSplitConn(db)
+ if err != nil {
+ return nil, err
+ }
+ if host != "" {
+ s.Server, s.Port = host, port
+ }
+ db = path
+ }
return fancyOpen(s, db)
}
+// spectreMissing — у Firebird имени базы SPECTRE по умолчанию нет: путь к ней задают в настройке.
+func spectreMissing(s FancyMSSQLSettings) error {
+ if s.IsFirebird() && strings.TrimSpace(s.SpectreDatabase) == "" {
+ return fmt.Errorf("база брокерского учёта SPECTRE не указана — Настройка → «Обмен с DEPO», карточка «Трансферные таблицы DEPO»")
+ }
+ return nil
+}
+
// SpectreDB — база SPECTRE рядом с DEPO: по умолчанию BJ_SPECTRE_DATA.
func (s FancyMSSQLSettings) SpectreDB() string {
if v := strings.TrimSpace(s.SpectreDatabase); v != "" {
diff --git a/internal/lkgateway/lkfx.go b/internal/lkgateway/lkfx.go
index 2484827..1a5a679 100644
--- a/internal/lkgateway/lkfx.go
+++ b/internal/lkgateway/lkfx.go
@@ -70,8 +70,11 @@ func lkFxAccounting(ctx context.Context, s FancyMSSQLSettings) map[string]LKFxRa
if strings.TrimSpace(s.Server) == "" {
return out
}
- for _, db := range []string{s.DepoDB(), s.SpectreDB()} {
- conn, err := fancyOpen(s, db)
+ for _, open := range []func() (*sql.DB, error){
+ func() (*sql.DB, error) { return fancyOpen(s, s.DepoDB()) },
+ func() (*sql.DB, error) { return spectreOpen(s) },
+ } {
+ conn, err := open()
if err != nil {
continue
}
diff --git a/internal/lkgateway/lkhistory.go b/internal/lkgateway/lkhistory.go
index 8832d6f..d5ade88 100644
--- a/internal/lkgateway/lkhistory.go
+++ b/internal/lkgateway/lkhistory.go
@@ -136,7 +136,7 @@ func lkOperations(ctx context.Context, s FancyMSSQLSettings, contract, from, to
}
fromT, toT := parseRange(from, to)
// Три вида операций в одном списке: зачисление, списание, перевод между счетами.
- q := fmt.Sprintf(fsql.Hist02, limit, inClause(ids), inClause(ids), inClause(ids), inClause(ids))
+ q := fmt.Sprintf(depoSQL(s, fsql.Hist02), limit, inClause(ids), inClause(ids), inClause(ids), inClause(ids))
rows, err := conn.QueryContext(cctx, q, fromT, toT)
if err != nil {
return nil, err
diff --git a/internal/lkgateway/manual_depo.go b/internal/lkgateway/manual_depo.go
index 60f992d..67283fb 100644
--- a/internal/lkgateway/manual_depo.go
+++ b/internal/lkgateway/manual_depo.go
@@ -73,7 +73,7 @@ func manualDepoSearch(rc *RuntimeConfig, q string) ([]manualDepoFace, string) {
// Действующая версия карточки (LAST_FLAG=1); счета привязаны к исходной записи (SELF_ID).
// likeEscape: метасимволы LIKE (% _ [) в имени от пользователя означают сами себя,
// иначе поиск «расползается» по всей базе (wildcard-injection).
- rows, err := db.QueryContext(ctx, accSQL(fsql.Man01), sql.Named("q", likeEscape(q)+"%"), sql.Named("d", digits))
+ 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()
}
diff --git a/internal/lkgateway/web/templates/part_fancy.html b/internal/lkgateway/web/templates/part_fancy.html
index 0d274e5..b3b3bc9 100644
--- a/internal/lkgateway/web/templates/part_fancy.html
+++ b/internal/lkgateway/web/templates/part_fancy.html
@@ -36,6 +36,8 @@
+