feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР

- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk)
- трансферные таблицы FTV_M2M: создание при установке, события, справочник участников
- сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO
- боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК
- онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML)
- модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-15 19:41:12 +03:00
parent 59fc774f08
commit da12a9ec1c
132 changed files with 29969 additions and 1089 deletions
+147
View File
@@ -0,0 +1,147 @@
//go:build live
package lkgateway
// Разбор, какие данные пришли из боевого ЛК по заявке и как клиент сопоставлен с DEPO.
// Только чтение: GET из ЛК, SELECT из DEPO. Паспорт и ФИО в выводе маскируются.
// go test -c -tags live -o lkmatch.test ./internal/lkgateway/ && sudo ./lkmatch.test -test.run TestLKMatchLive -test.v
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
"sort"
"strconv"
"strings"
"testing"
)
func mask(s string) string {
r := []rune(strings.TrimSpace(s))
switch {
case len(r) == 0:
return "(пусто)"
case len(r) <= 2:
return strings.Repeat("•", len(r))
}
return string(r[:1]) + strings.Repeat("•", len(r)-2) + string(r[len(r)-1:])
}
func TestLKMatchLive(t *testing.T) {
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
if err != nil {
t.Fatal(err)
}
globalRC = rc
ep, ok := lkActiveEndpoint(rc)
if !ok {
t.Skip("ЛК не настроен")
}
id := 69753
if v := os.Getenv("CLAIM"); v != "" {
id, _ = strconv.Atoi(v)
}
ctx := context.Background()
_, body, err := lkGet(ctx, ep, "/claims/"+strconv.Itoa(id))
if err != nil {
t.Fatal(err)
}
var wrap struct {
Claim map[string]any `json:"claim"`
}
_ = json.Unmarshal(body, &wrap)
cl := wrap.Claim
data, _ := cl["data"].(map[string]any)
fmt.Printf("== Заявка ЛК #%d: kind=%v, user_id=%v, owner=%v/%v\n", id, cl["kind"], cl["user_id"], cl["owner_type"], cl["owner_id"])
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
b, _ := json.Marshal(data[k])
fmt.Printf(" data.%s = %s\n", k, b)
}
uid := toInt(cl["user_id"])
_, ub, _ := lkGet(ctx, ep, "/users/"+strconv.Itoa(uid))
var uw map[string]any
_ = json.Unmarshal(ub, &uw)
u := pickMap(uw, "user", "data")
if u == nil {
u = uw
}
fmt.Printf("== Пользователь ЛК #%d: поля карточки (значения ФИО и документа замаскированы)\n", uid)
ukeys := make([]string, 0, len(u))
for k := range u {
ukeys = append(ukeys, k)
}
sort.Strings(ukeys)
var present []string
for _, k := range ukeys {
if s := str(u[k]); s != "" && s != "<nil>" {
present = append(present, k)
}
}
fmt.Printf(" заполненные поля: %s\n", strings.Join(present, ", "))
for _, k := range []string{"surname", "name", "patronymic", "document_type", "pass_series", "pass_number", "document_series", "document_number", "birth_date", "inn", "snils"} {
if v, ok := u[k]; ok {
val := str(v)
if k != "document_type" {
val = mask(val)
}
fmt.Printf(" %s = %s\n", k, val)
}
}
req, err := lkClaimToRequest(ctx, rc, ep, cl, data)
if err != nil {
t.Fatal(err)
}
inv := req.Investor
fmt.Printf("== В сверку с DEPO передано: ФИО %s %s %s, документ тип %s серия %s номер %s\n",
mask(inv.LastName), mask(inv.FirstName), mask(inv.MiddleName), inv.Document.DocumentType, mask(inv.Document.Series), mask(inv.Document.Number))
var secs []depoOutSec
acc, section := "", ""
for _, s := range req.Securities {
secs = append(secs, depoOutSec{ISIN: s.SecurityDetails.ISIN, Code: s.SecurityCode, Qty: strconv.FormatUint(s.Quantity.Whole, 10)})
if len(s.SettlementAccounts) > 0 {
acc, section = s.SettlementAccounts[0].SettlementLocation.AccountID, s.SettlementAccounts[0].SettlementLocation.SectionID
}
}
if v := os.Getenv("ACCOUNT"); v != "" {
req.ClientAccount = v // проверка несовпадения: подставить чужой счёт
}
fmt.Printf("== Счёт из заявки: %s\n", req.ClientAccount)
if v := os.Getenv("IIS_ACCOUNT"); v != "" { // признак ИИС по произвольному счёту
fs := rc.Snapshot().Fancy
if db, err := fancyOpen(fs, fs.DepoDB()); err == nil {
var cid int64
_ = db.QueryRowContext(ctx, "SELECT TOP 1 ID FROM OD_CLIENTS WHERE NUMBER=@p1", sql.Named("p1", v)).Scan(&cid)
ok, det := depoAccountIIS(ctx, db, fs, cid)
fmt.Printf("== ИИС по счёту %s: не ИИС=%v — %s\n", v, ok, det)
db.Close()
}
}
if v := os.Getenv("FIO_LAST"); v != "" {
inv.LastName = v // проверка расхождения ФИО
}
if v := os.Getenv("BIRTH"); v != "" {
inv.BirthDate = v // проверка расхождения даты рождения
}
res := runDepoCheckOutAccount(inv.LastName, inv.FirstName, inv.MiddleName, inv.Document.Series, inv.Document.Number, inv.BirthDate, secs, acc, section, req.ClientAccount)
fmt.Printf("== Сверка с DEPO: итог %s; лицо в DEPO: %s; счёт: %s\n", res.Summary, mask(res.Investor), res.Account)
for _, s := range res.Secs {
for _, c := range s.Checks {
mark := "✓"
if !c.OK {
mark = "✗"
} else if c.Warn {
mark = "!"
}
fmt.Printf(" %s %s [%s]: %s\n", mark, c.Name, c.Code, c.Detail)
}
}
}