ЛК ЕСИА Финанс: портфель ДЕПО из DEPO — сверка каждые 10 с в проходе опроса, отправка только при изменении; настройки и «Синхронизировать сейчас»
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -55,6 +55,7 @@ var templateFuncs = template.FuncMap{
|
||||
"realTest": realTestGet,
|
||||
"realNSDAs": realNSDAsText,
|
||||
"quikRecipientsInfo": quikRecipientsInfo,
|
||||
"lkPortfolioStates": lkPortfolioStates,
|
||||
"nsdSegTip": nsdSegTip,
|
||||
"devOn": authTemplateFuncs["devOn"],
|
||||
"devLicensed": authTemplateFuncs["devLicensed"],
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//go:build live
|
||||
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Дописать PARAMETERS событиям перевода, где оно пустое. EV_GUID, EV_TYPES (через запятую не нужно — два типа), EV_PARAMS, EV_GO=1.
|
||||
func TestEventParamsLive(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := depoEventsDB(rc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
ctx := context.Background()
|
||||
g, p := os.Getenv("EV_GUID"), os.Getenv("EV_PARAMS")
|
||||
if os.Getenv("EV_GO") == "1" {
|
||||
res, err := db.ExecContext(ctx, `UPDATE dbo.FTV_M2M_EVENTS SET PARAMETERS=@p
|
||||
WHERE GUID=@g AND EVENT_TYPE IN ('BJ_ORDER_ACCEPTED','BJ_EXECUTED') AND (PARAMETERS IS NULL OR PARAMETERS='')`, sql.Named("p", p), sql.Named("g", g))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
fmt.Println("обновлено строк:", n)
|
||||
}
|
||||
rows, err := db.QueryContext(ctx, `SELECT EVENT_TYPE, ISNULL(REFERENCE,''), ISNULL(STATE,''), ISNULL(PARAMETERS,''), EVENT_DATE FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g ORDER BY EVENT_DATE`, sql.Named("g", g))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var ty, ref, st, pr string
|
||||
var at interface{}
|
||||
_ = rows.Scan(&ty, &ref, &st, &pr, &at)
|
||||
fmt.Printf("%-18s %-18s %-8s %-24q %v\n", ty, ref, st, pr, at)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//go:build live
|
||||
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Только чтение: снимок данных клиента в ЛК ЕСИА Финанс. LK_PATHS=/contracts/1557,/contracts/1557/depo_accounts,/users/891 LK_OUT=файл
|
||||
func TestLKBackupLive(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ep, ok := lkActiveEndpoint(rc)
|
||||
if !ok {
|
||||
t.Fatal("ЛК не настроен")
|
||||
}
|
||||
out := map[string]any{"at": time.Now().Format(time.RFC3339), "env": rc.Snapshot().LK.ActiveEnv}
|
||||
for _, p := range strings.Split(os.Getenv("LK_PATHS"), ",") {
|
||||
code, body, err := lkGet(context.Background(), ep, p)
|
||||
var v any
|
||||
if json.Unmarshal(body, &v) != nil {
|
||||
v = string(body)
|
||||
}
|
||||
out[p] = map[string]any{"http": code, "err": fmt.Sprint(err), "body": v}
|
||||
fmt.Println(p, "→ HTTP", code, err, len(body), "байт")
|
||||
}
|
||||
raw, _ := json.MarshalIndent(out, "", " ")
|
||||
if err := os.WriteFile(os.Getenv("LK_OUT"), raw, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -294,6 +294,10 @@ func (s *Server) pollLKClaims(ctx context.Context) {
|
||||
log.Printf("lk-gateway: поллер ЛК: связь с ЛК восстановлена")
|
||||
lastErr = ""
|
||||
}
|
||||
// тот же проход: портфели ДЕПО из DEPO в ЛК (только изменившиеся, lkportfolio.go)
|
||||
pctx, pcancel := context.WithTimeout(ctx, time.Minute)
|
||||
lkPortfolioSyncTick(pctx, s.rc, false)
|
||||
pcancel()
|
||||
if res.Fetched > 0 || len(res.Errors) > 0 {
|
||||
log.Printf("lk-gateway: поллер ЛК: получено %d, отправлено %d, отклонено %d, ошибок %d (имитация НРД: %v)",
|
||||
res.Fetched, res.Sent, res.Rejected, len(res.Errors), res.Emulated)
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
package lkgateway
|
||||
|
||||
// Синхронизация депозитарного портфеля клиента из DEPO в ЛК ЕСИА Финанс (29.09.2026): метод back_office
|
||||
// POST /portfolio/import?import_type=depo_portfolio. Форма заявки M2M в ЛК берёт бумаги клиента из этого
|
||||
// портфеля (по счёту депо и разделу). Источники полей:
|
||||
// - остатки, счёт депо, ISIN, наименование — DEPO (OD_RESTS / OD_VALUES);
|
||||
// - вид бумаги (paper_type) — код CFI в DEPO: ED — депозитарная расписка, E — акция, D — облигация, C — фонд;
|
||||
// - номер гос. регистрации — экспорт QUIK (Securities.RegNumber); в DEPO его нет, а ЛК без него не принимает;
|
||||
// - раздел счёта — «Основной» (решение 29.09.2026: в DEPO и в ЛК разделов нет, ЛК требует непустой),
|
||||
// место хранения — «НРД».
|
||||
// Бумаги без номера гос. регистрации не отправляются (ЛК отвергает весь импорт) — они в отчёте.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
lkPortfolioSection = "Основной"
|
||||
lkPortfolioStorage = "НРД"
|
||||
)
|
||||
|
||||
// LKPortfolioLine — строка портфеля ДЕПО для ЛК.
|
||||
type LKPortfolioLine struct {
|
||||
ISIN string `json:"isin"`
|
||||
Name string `json:"name"`
|
||||
RegNumber string `json:"registration_number"`
|
||||
Count float64 `json:"count"`
|
||||
DepoAccount string `json:"depo_account"`
|
||||
Section string `json:"depo_account_section"`
|
||||
Storage string `json:"storage"`
|
||||
PaperType string `json:"paper_type"`
|
||||
}
|
||||
|
||||
// lkPaperType — вид бумаги по коду CFI (ISO 10962).
|
||||
func lkPaperType(cfi, name string) string {
|
||||
cfi = strings.ToUpper(strings.TrimSpace(cfi))
|
||||
switch {
|
||||
case strings.HasPrefix(cfi, "ED"):
|
||||
return "depositary_receipt"
|
||||
case strings.HasPrefix(cfi, "E"):
|
||||
return "share"
|
||||
case strings.HasPrefix(cfi, "D"):
|
||||
return "bond"
|
||||
case strings.HasPrefix(cfi, "C"):
|
||||
return "fund"
|
||||
}
|
||||
low := strings.ToLower(name)
|
||||
switch {
|
||||
case strings.Contains(low, "облиг") || strings.Contains(low, "офз") || strings.Contains(low, " обл"):
|
||||
return "bond"
|
||||
case strings.Contains(low, "пиф") || strings.Contains(low, "пай") || strings.Contains(low, "фонд"):
|
||||
return "fund"
|
||||
case strings.Contains(low, "расписк"):
|
||||
return "depositary_receipt"
|
||||
}
|
||||
return "share"
|
||||
}
|
||||
|
||||
// lkPortfolioLines — портфель клиента (портфель DEPO, напр. «БР-859») для ЛК; onlyISIN — только эти бумаги
|
||||
// (пусто — все). skipped — бумаги, которые не отправляются, с причиной.
|
||||
func lkPortfolioLines(ctx context.Context, rc *RuntimeConfig, depoPort string, onlyISIN map[string]bool) (lines []LKPortfolioLine, skipped []string, err error) {
|
||||
fs := rc.Snapshot().Fancy
|
||||
pos, err := depoClientPositions(ctx, fs, depoPort)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("DEPO: %w", err)
|
||||
}
|
||||
db, err := fancyOpen(fs, fs.DepoDB())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("DEPO: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
qe, qerr := nprDB(rc) // экспорт QUIK (только чтение) — номер гос. регистрации
|
||||
for _, p := range pos {
|
||||
isin := strings.ToUpper(strings.TrimSpace(p.ISIN))
|
||||
if isin == "" || (len(onlyISIN) > 0 && !onlyISIN[isin]) || p.Quantity <= 0 {
|
||||
continue
|
||||
}
|
||||
cfi, reg, cached := lkSecInfoCached(isin)
|
||||
if !cached {
|
||||
_ = db.QueryRowContext(ctx, "SELECT TOP 1 ISNULL(CFI,'') FROM OD_VALUES WHERE ISIN=@i ORDER BY ID", sql.Named("i", isin)).Scan(&cfi)
|
||||
if qerr == nil {
|
||||
_ = qe.QueryRow(ctx, `SELECT "RegNumber" FROM "Securities" WHERE "ISINCode"=$1 AND COALESCE("RegNumber",'')<>'' LIMIT 1`, isin).Scan(®)
|
||||
}
|
||||
if qerr == nil {
|
||||
lkSecInfoPut(isin, cfi, reg)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(reg) == "" {
|
||||
why := "нет номера гос. регистрации в экспорте QUIK"
|
||||
if qerr != nil {
|
||||
why = "экспорт QUIK недоступен: " + qerr.Error()
|
||||
}
|
||||
skipped = append(skipped, fmt.Sprintf("%s %s (%s): %s", isin, p.SecurityName, p.AccountNum, why))
|
||||
continue
|
||||
}
|
||||
lines = append(lines, LKPortfolioLine{
|
||||
ISIN: isin, Name: strings.TrimSpace(p.SecurityName), RegNumber: strings.TrimSpace(reg), Count: p.Quantity,
|
||||
DepoAccount: p.AccountNum, Section: lkPortfolioSection, Storage: lkPortfolioStorage, PaperType: lkPaperType(cfi, p.SecurityName),
|
||||
})
|
||||
}
|
||||
return lines, skipped, nil
|
||||
}
|
||||
|
||||
// lkPortfolioPush — отправить портфель ДЕПО договора ЛК (номер договора депо в ЛК, напр. «ДД-816»).
|
||||
func lkPortfolioPush(ctx context.Context, rc *RuntimeConfig, lkContract string, lines []LKPortfolioLine) (int, string, error) {
|
||||
ep, ok := lkActiveEndpoint(rc)
|
||||
if !ok {
|
||||
return 0, "", fmt.Errorf("ЛК не настроен")
|
||||
}
|
||||
body, _ := json.Marshal(map[string]any{"data": map[string]any{lkContract: lines}})
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, lkAPIBase(ep.BaseURL)+"/portfolio/import?import_type=depo_portfolio", strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.SetBasicAuth(ep.Login, ep.Password)
|
||||
resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(req)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||||
return resp.StatusCode, strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
|
||||
// Кэш вида бумаги (CFI) и номера гос. регистрации на час: портфель сверяется каждые 10 секунд.
|
||||
var lkSecInfo = struct {
|
||||
sync.Mutex
|
||||
m map[string]lkSecInfoEntry
|
||||
}{m: map[string]lkSecInfoEntry{}}
|
||||
|
||||
type lkSecInfoEntry struct {
|
||||
cfi, reg string
|
||||
at time.Time
|
||||
}
|
||||
|
||||
func lkSecInfoCached(isin string) (cfi, reg string, ok bool) {
|
||||
lkSecInfo.Lock()
|
||||
defer lkSecInfo.Unlock()
|
||||
e, ok := lkSecInfo.m[isin]
|
||||
if !ok || time.Since(e.at) > time.Hour {
|
||||
return "", "", false
|
||||
}
|
||||
return e.cfi, e.reg, true
|
||||
}
|
||||
|
||||
func lkSecInfoPut(isin, cfi, reg string) {
|
||||
lkSecInfo.Lock()
|
||||
lkSecInfo.m[isin] = lkSecInfoEntry{cfi: cfi, reg: reg, at: time.Now()}
|
||||
lkSecInfo.Unlock()
|
||||
}
|
||||
|
||||
// LKPortfolioSyncSettings — какие договоры ЛК синхронизировать и с какими портфелями DEPO.
|
||||
type LKPortfolioSyncSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
// Pairs — «договор депо в ЛК = портфель DEPO» через «;», напр. «ДД-816=БР-859».
|
||||
Pairs string `json:"pairs,omitempty"`
|
||||
}
|
||||
|
||||
type lkPortfolioPair struct{ LK, Depo string }
|
||||
|
||||
func (s LKPortfolioSyncSettings) pairs() []lkPortfolioPair {
|
||||
var out []lkPortfolioPair
|
||||
for _, p := range strings.Split(s.Pairs, ";") {
|
||||
a, b, ok := strings.Cut(p, "=")
|
||||
if a, b = strings.TrimSpace(a), strings.TrimSpace(b); ok && a != "" && b != "" {
|
||||
out = append(out, lkPortfolioPair{LK: a, Depo: b})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// LKPortfolioSyncState — итог по договору (для настроек).
|
||||
type LKPortfolioSyncState struct {
|
||||
LK, Depo string
|
||||
CheckedAt time.Time // последняя сверка с DEPO
|
||||
PushedAt time.Time // последняя отправка в ЛК
|
||||
Lines int
|
||||
Skipped []string
|
||||
Err string
|
||||
hash string
|
||||
}
|
||||
|
||||
var lkPortfolioState = struct {
|
||||
sync.Mutex
|
||||
m map[string]*LKPortfolioSyncState
|
||||
}{m: map[string]*LKPortfolioSyncState{}}
|
||||
|
||||
// lkPortfolioSyncTick — сверка портфелей с DEPO; в ЛК уходит только изменившийся (или при force).
|
||||
func lkPortfolioSyncTick(ctx context.Context, rc *RuntimeConfig, force bool) {
|
||||
st := rc.Snapshot().LK.PortfolioSync
|
||||
if !st.Enabled {
|
||||
return
|
||||
}
|
||||
for _, pr := range st.pairs() {
|
||||
lines, skipped, err := lkPortfolioLines(ctx, rc, pr.Depo, nil)
|
||||
lkPortfolioState.Lock()
|
||||
s := lkPortfolioState.m[pr.LK]
|
||||
if s == nil {
|
||||
s = &LKPortfolioSyncState{LK: pr.LK, Depo: pr.Depo}
|
||||
lkPortfolioState.m[pr.LK] = s
|
||||
}
|
||||
s.Depo, s.CheckedAt = pr.Depo, time.Now()
|
||||
if err != nil {
|
||||
if s.Err != err.Error() {
|
||||
log.Printf("lk-portfolio: %s ← %s: %v", pr.LK, pr.Depo, err)
|
||||
}
|
||||
s.Err = err.Error()
|
||||
lkPortfolioState.Unlock()
|
||||
continue
|
||||
}
|
||||
raw, _ := json.Marshal(lines)
|
||||
sum := sha256.Sum256(raw)
|
||||
h := hex.EncodeToString(sum[:])
|
||||
same := h == s.hash
|
||||
lkPortfolioState.Unlock()
|
||||
if same && !force {
|
||||
continue
|
||||
}
|
||||
code, resp, perr := lkPortfolioPush(ctx, rc, pr.LK, lines)
|
||||
lkPortfolioState.Lock()
|
||||
s.Skipped = skipped
|
||||
switch {
|
||||
case perr != nil:
|
||||
s.Err = perr.Error()
|
||||
case code != 200 || !strings.Contains(resp, `"success":true`):
|
||||
s.Err = fmt.Sprintf("HTTP %d: %s", code, resp)
|
||||
default:
|
||||
s.Err, s.hash, s.PushedAt, s.Lines = "", h, time.Now(), len(lines)
|
||||
}
|
||||
msg := s.Err
|
||||
lkPortfolioState.Unlock()
|
||||
if msg != "" {
|
||||
log.Printf("lk-portfolio: %s ← %s: отправка не прошла: %s", pr.LK, pr.Depo, msg)
|
||||
} else {
|
||||
log.Printf("lk-portfolio: %s ← %s: портфель ДЕПО обновлён в ЛК (%d бумаг, пропущено %d)", pr.LK, pr.Depo, len(lines), len(skipped))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lkPortfolioStates — состояние синхронизации для страницы настроек.
|
||||
func lkPortfolioStates() []LKPortfolioSyncState {
|
||||
lkPortfolioState.Lock()
|
||||
defer lkPortfolioState.Unlock()
|
||||
var out []LKPortfolioSyncState
|
||||
for _, s := range lkPortfolioState.m {
|
||||
out = append(out, *s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// registerLKPortfolioSync — POST /admin/setup/lk-portfolio: настройки и «синхронизировать сейчас».
|
||||
func registerLKPortfolioSync(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
mux.HandleFunc("/admin/setup/lk-portfolio", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if r.FormValue("action") == "sync" {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||
lkPortfolioSyncTick(ctx, rc, true)
|
||||
cancel()
|
||||
} else {
|
||||
lk := rc.Snapshot().LK
|
||||
lk.PortfolioSync.Enabled = r.FormValue("enabled") != ""
|
||||
lk.PortfolioSync.Pairs = strings.TrimSpace(r.FormValue("pairs"))
|
||||
if err := rc.UpdateLK(lk); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, "/admin/setup#lk-portfolio", http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//go:build live
|
||||
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Портфель ДЕПО в ЛК ЕСИА Финанс. PF_CONTRACT=ДД-816 PF_PORT=БР-859 PF_ISIN=RU0009029540[,…] [PF_GO=1 — отправить]
|
||||
func TestLKPortfolioLive(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
only := map[string]bool{}
|
||||
for _, i := range strings.Split(os.Getenv("PF_ISIN"), ",") {
|
||||
if i = strings.ToUpper(strings.TrimSpace(i)); i != "" {
|
||||
only[i] = true
|
||||
}
|
||||
}
|
||||
lines, skipped, err := lkPortfolioLines(context.Background(), rc, os.Getenv("PF_PORT"), only)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, _ := json.MarshalIndent(map[string]any{"data": map[string]any{os.Getenv("PF_CONTRACT"): lines}}, "", " ")
|
||||
fmt.Println("запрос: POST /portfolio/import?import_type=depo_portfolio")
|
||||
fmt.Println(string(body))
|
||||
for _, s := range skipped {
|
||||
fmt.Println("пропущено:", s)
|
||||
}
|
||||
if os.Getenv("PF_GO") != "1" {
|
||||
return
|
||||
}
|
||||
code, resp, err := lkPortfolioPush(context.Background(), rc, os.Getenv("PF_CONTRACT"), lines)
|
||||
fmt.Println("ответ HTTP", code, resp, err)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package lkgateway
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLKPaperTypeAndPairs(t *testing.T) {
|
||||
for cfi, want := range map[string]string{"ESVXFR": "share", "EDSXFR": "depositary_receipt", "DBFXFR": "bond", "CEOGMR": "fund"} {
|
||||
if got := lkPaperType(cfi, ""); got != want {
|
||||
t.Fatalf("%s: %s", cfi, got)
|
||||
}
|
||||
}
|
||||
if lkPaperType("", "ОФЗ 26238 ПД обл") != "bond" || lkPaperType("", "БПИФ «Ликвидность» фонд") != "fund" {
|
||||
t.Fatal("по названию")
|
||||
}
|
||||
p := LKPortfolioSyncSettings{Pairs: "ДД-816=БР-859; мусор ;ДД-1 = БР-1"}.pairs()
|
||||
if len(p) != 2 || p[0].LK != "ДД-816" || p[0].Depo != "БР-859" || p[1].Depo != "БР-1" {
|
||||
t.Fatalf("%+v", p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//go:build live
|
||||
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Только чтение: строка остатка SBER у ВТНКЦ00000816 целиком + таблицы с рег. номером.
|
||||
func TestODValuesLive(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fs := rc.Snapshot().Fancy
|
||||
db, err := fancyOpen(fs, fs.DepoDB())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
ctx := context.Background()
|
||||
dump := func(q string, args ...any) {
|
||||
rows, err := db.QueryContext(ctx, q, args...)
|
||||
if err != nil {
|
||||
fmt.Println("ошибка:", err)
|
||||
return
|
||||
}
|
||||
cols, _ := rows.Columns()
|
||||
for rows.Next() {
|
||||
vals := make([]sql.NullString, len(cols))
|
||||
ptrs := make([]any, len(cols))
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
_ = rows.Scan(ptrs...)
|
||||
for i, c := range cols {
|
||||
if vals[i].Valid && vals[i].String != "" && vals[i].String != "0" {
|
||||
fmt.Printf("%s=%s | ", c, vals[i].String)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
fmt.Println("== OD_ACC_PLANS план 4")
|
||||
dump("SELECT * FROM OD_ACC_PLANS WHERE ID=4")
|
||||
fmt.Println("== OD_PLAN_DIRS план 4")
|
||||
dump("SELECT * FROM OD_PLAN_DIRS WHERE PLAN_ID=4")
|
||||
}
|
||||
@@ -277,6 +277,8 @@ type LKSettings struct {
|
||||
// RealNSDTest — тест «боевой ЛК → тестовый контур НРД»: заявки указанных клиентов проходят всё
|
||||
// по-настоящему (DEPO, QUIK МНП, статусы в ЛК), а в НРД уходят с данными тест-инвестора (realtest.go).
|
||||
RealNSDTest RealNSDTestSettings `json:"real_nsd_test,omitempty"`
|
||||
// PortfolioSync — портфель ДЕПО из DEPO в ЛК ЕСИА Финанс (lkportfolio.go): каждый проход опроса ЛК.
|
||||
PortfolioSync LKPortfolioSyncSettings `json:"portfolio_sync,omitempty"`
|
||||
|
||||
// Интеграция с ЛК (потребление back_office API платформы): два окружения.
|
||||
ActiveEnv string `json:"lk_active_env"` // "test" | "prod"
|
||||
|
||||
@@ -636,6 +636,18 @@
|
||||
{{if devOn}} <input type="hidden" name="emulate_nsd_form" value="1">
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;font-weight:400;flex-basis:100%;margin-top:6px"><input type="checkbox" name="emulate_nsd" style="width:auto;margin-top:3px" {{if .Settings.LK.EmulateNSD}}checked{{end}} onchange="this.form.submit()"> <span><b>Имитация НРД</b> — заявки ЛК проходят сверку с DEPO, блокировку в QUIK и получают статус в ЛК, но в НРД не отправляются: ответ контрагента имитирует bj.{{if and (eq .Settings.LK.ActiveEnv "prod") (not .Settings.LK.EmulateNSD)}} <span style="color:var(--warn)">Сейчас включена принудительно: боевой ЛК при тестовом профиле ИШ ({{.Settings.NSD.Profile}}).</span>{{end}}</span></label>{{end}}
|
||||
</form>
|
||||
<div id="lk-portfolio" style="margin-top:14px;padding:12px 14px;border:1px solid var(--border);border-radius:10px">
|
||||
<b>Портфель ДЕПО → ЛК</b> {{if .Settings.LK.PortfolioSync.Enabled}}<span class="badge confirmed">включено</span>{{else}}<span class="badge draft">выключено</span>{{end}}
|
||||
<p class="muted" style="margin:6px 0 8px;font-size:13px">Каждые 10 секунд, в том же проходе, что и приём заявок, bj сверяет портфель клиента в DEPO и отправляет его в ЛК (<code>/portfolio/import</code>, портфель ДЕПО) — только если остатки изменились. Номер гос. регистрации — из экспорта QUIK, раздел счёта — «Основной», хранение — НРД. Бумаги без номера гос. регистрации не отправляются (ЛК отвергает весь импорт).</p>
|
||||
<form method="post" action="/admin/setup/lk-portfolio" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-weight:400"><input type="checkbox" name="enabled" style="width:auto"{{if .Settings.LK.PortfolioSync.Enabled}} checked{{end}}> включить</label>
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px;min-width:260px">Договор ЛК = портфель DEPO (через «;»)
|
||||
<input name="pairs" value="{{.Settings.LK.PortfolioSync.Pairs}}" placeholder="ДД-816=БР-859"></label>
|
||||
<button class="btn" type="submit">Сохранить</button>
|
||||
<button class="btn" type="submit" name="action" value="sync">Синхронизировать сейчас</button>
|
||||
</form>
|
||||
{{range lkPortfolioStates}}<div class="muted" style="font-size:12.5px;margin-top:8px"><b>{{.LK}}</b> ← {{.Depo}}: {{if .Err}}<span style="color:var(--err)">ошибка — {{.Err}}</span>{{else if .PushedAt.IsZero}}сверено {{.CheckedAt.Format "15:04:05"}}, отправки ещё не было{{else}}отправлено {{.PushedAt.Format "02.01 15:04:05"}} ({{.Lines}} бумаг), сверено {{.CheckedAt.Format "15:04:05"}}{{end}}{{range .Skipped}}<br>пропущено: {{.}}{{end}}</div>{{end}}
|
||||
</div>
|
||||
<div id="lk-real-nsd" style="margin-top:14px;padding:12px 14px;border:1px solid var(--border);border-radius:10px;{{if .Settings.LK.RealNSDTest.Enabled}}background:var(--warn-weak){{end}}">
|
||||
<b>Тест «боевой ЛК → тестовый контур НРД»</b> {{if .Settings.LK.RealNSDTest.Enabled}}<span class="badge awaiting_decision">включён</span>{{else}}<span class="badge draft">выключен</span>{{end}}
|
||||
<p class="muted" style="margin:6px 0 8px;font-size:13px">Заявки клиентов из списка проходят у нас всё по-настоящему: сверка и поручение в DEPO, блокировка и списание в QUIK МНП (<b>боевая запись</b>), статусы в ЛК. В НРД (тестовый контур) запрос уходит с данными тест-инвестора <b>{{realNSDAs}}</b>; GUID, бумаги и счета — настоящие, ответы НРД применяются к заявке клиента. «Исполнена» в ЛК — по отчёту НРД об исполнении (MS010). Остальные заявки ЛК — как раньше. Включайте до того, как клиент подпишет заявку.</p>
|
||||
|
||||
Reference in New Issue
Block a user