7ce4c2c79f
Реализация варианта B из аудита защиты знаний. Прямой garble всего bj-server ломает html/template (он обращается к полям структур по имени, garble их переименовывает). Решение — селективная обфускация: GOGARBLE обфусцирует ТОЛЬКО пакет с секретами, шаблоны и обработчики не затрагиваются. 95 SQL-запросов к DEPO/SPECTRE/QUIK и трансферным таблицам FTV_M2M вынесены из lkgateway в отдельный пакет internal/lkgateway/fsql как переменные (именно var, не const: const компилятор вставил бы обратно в необфусцируемый пакет, и шифрование бы не сработало — проверено). Сами обработчики, типы и параметры sql.Named остались на месте; пакет держит только строки запросов. build-release.sh собирает bj-server с `GOGARBLE=.../fsql garble -literals` (нужен отдельный SDK go1.26 — модуль-кэшный тулчейн garble не подходит); если garble/go1.26 не найдены — обычная сборка с предупреждением. Проверено на реальном garble-бинаре (запускается, /admin и страницы на структурах работают): рецепт чтения чужих систем из бинаря исчез — OD_RESTS 13→0, OD_WIRING 5→0, OD_TURNS 1→0, OD_BALANCES 3→0, D_B_CONTRACTS 1→0, OD_CLIENTS_PORTS 10→0, DepoLimits/MoneyLimits →0. Остаток FTV_M2M — упоминания имён таблиц в операторских embed-шаблонах и логах, не сам рецепт. Стенд собран обычной сборкой (без garble) — трассировки остаются читаемыми. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
256 lines
9.4 KiB
Go
256 lines
9.4 KiB
Go
package lkgateway
|
|
|
|
// lkdocs.go — корпоративные действия по бумагам клиента и документы депозитария:
|
|
// готовые выписки из учётной системы (U_OP_INF_EXTRACT) плюс заявки клиента на
|
|
// документ, которые кабинет кладёт в схему lk и показывает сотрудникам.
|
|
//
|
|
// Кабинет не создаёт документы в учётной системе: заявка — это поручение сотруднику,
|
|
// он готовит документ у себя и выкладывает клиенту.
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// LKCorpAction — корпоративное действие по бумаге клиента.
|
|
type LKCorpAction struct {
|
|
FixDate string `json:"fix_date,omitempty"` // дата фиксации
|
|
OperDate string `json:"oper_date,omitempty"` // дата операции
|
|
Kind string `json:"kind"` // расшифровка вида
|
|
Code string `json:"code,omitempty"` // код вида в учёте
|
|
ISIN string `json:"isin,omitempty"`
|
|
Security string `json:"security,omitempty"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
// caKindRu — вид корпоративного действия по коду учёта (коды ISO 20022).
|
|
func caKindRu(code string) string {
|
|
switch strings.ToUpper(strings.TrimSpace(code)) {
|
|
case "DVCA":
|
|
return "дивиденды"
|
|
case "INTR":
|
|
return "выплата купона"
|
|
case "REDM":
|
|
return "погашение"
|
|
case "MCAL":
|
|
return "досрочное погашение"
|
|
case "PCAL":
|
|
return "частичное погашение"
|
|
case "BPUT":
|
|
return "оферта: предъявление к выкупу"
|
|
case "TEND":
|
|
return "добровольное предложение о выкупе"
|
|
case "MEET", "XMET", "OMET":
|
|
return "собрание владельцев"
|
|
case "CONV":
|
|
return "конвертация"
|
|
case "SPLF", "SPLR":
|
|
return "дробление или консолидация"
|
|
case "MRGR":
|
|
return "реорганизация"
|
|
case "EXOF":
|
|
return "обмен бумаг"
|
|
case "DRCA":
|
|
return "выплата по депозитарной расписке"
|
|
}
|
|
return "корпоративное действие"
|
|
}
|
|
|
|
// lkCorpActions — ближайшие корпоративные действия по бумагам клиента.
|
|
func lkCorpActions(ctx context.Context, s FancyMSSQLSettings, isins []string) ([]LKCorpAction, error) {
|
|
if strings.TrimSpace(s.Server) == "" || len(isins) == 0 {
|
|
return nil, nil
|
|
}
|
|
conn, err := fancyOpen(s, s.DepoDB())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
cctx, cancel := context.WithTimeout(ctx, 25*time.Second)
|
|
defer cancel()
|
|
// Список ISIN подставляем параметрами: значения приходят из портфеля клиента.
|
|
names := make([]string, 0, len(isins))
|
|
args := make([]any, 0, len(isins))
|
|
for i, isin := range isins {
|
|
names = append(names, fmt.Sprintf("@p%d", i+1))
|
|
args = append(args, sql.Named(fmt.Sprintf("p%d", i+1), strings.ToUpper(strings.TrimSpace(isin))))
|
|
}
|
|
q := fmt.Sprintf(fsql.Doc01, strings.Join(names, ","))
|
|
rows, err := conn.QueryContext(cctx, q, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []LKCorpAction
|
|
for rows.Next() {
|
|
var code, isin, name, offer string
|
|
var fix, oper sql.NullTime
|
|
if rows.Scan(&code, &isin, &name, &fix, &oper, &offer) != nil {
|
|
continue
|
|
}
|
|
a := LKCorpAction{Code: strings.TrimSpace(code), Kind: caKindRu(code), ISIN: isin, Security: name, Note: strings.TrimSpace(offer)}
|
|
if fix.Valid && fix.Time.Year() < 9000 {
|
|
a.FixDate = fix.Time.Format("02.01.2006")
|
|
}
|
|
if oper.Valid && oper.Time.Year() > 2000 && oper.Time.Year() < 9000 {
|
|
a.OperDate = oper.Time.Format("02.01.2006")
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// LKDocument — документ депозитария, готовый или заказанный клиентом.
|
|
type LKDocument struct {
|
|
ID int64 `json:"id,omitempty"`
|
|
Date string `json:"date"` // дата готовности или заказа
|
|
Title string `json:"title"` // что за документ
|
|
Period string `json:"period,omitempty"` // за какой период
|
|
Account string `json:"account,omitempty"`
|
|
State string `json:"state"` // готов | в работе | отклонён
|
|
Note string `json:"note,omitempty"` // комментарий депозитария
|
|
Ordered bool `json:"ordered"` // заявка клиента (а не готовый документ учёта)
|
|
Kind string `json:"kind,omitempty"` // вид заявки
|
|
}
|
|
|
|
// lkDocSchema — таблица заявок на документы (создаётся вместе со схемой lk).
|
|
var lkDocSchema = fsql.Doc02
|
|
|
|
// lkDocKinds — виды документов, которые клиент может заказать в кабинете.
|
|
var lkDocKinds = []string{
|
|
"Выписка по счёту депо",
|
|
"Справка об остатках на дату",
|
|
"Отчёт об операциях за период",
|
|
"Справка о доходах",
|
|
}
|
|
|
|
// lkDocKindAllowed — вид заявки из списка (чужие значения не принимаем).
|
|
func lkDocKindAllowed(kind string) bool {
|
|
for _, k := range lkDocKinds {
|
|
if k == kind {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// lkDocRequestAdd — заявка клиента на документ.
|
|
func lkDocRequestAdd(ctx context.Context, rc *RuntimeConfig, contract, clientName, kind, from, to, account, comment string) error {
|
|
if !lkDocKindAllowed(kind) {
|
|
return fmt.Errorf("неизвестный вид документа")
|
|
}
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var fromP, toP any
|
|
if t, err := time.Parse("2006-01-02", strings.TrimSpace(from)); err == nil {
|
|
fromP = t
|
|
}
|
|
if t, err := time.Parse("2006-01-02", strings.TrimSpace(to)); err == nil {
|
|
toP = t
|
|
}
|
|
_, err = db.ExecContext(ctx, fsql.Doc03, contract, clientName, kind, fromP, toP, strings.TrimSpace(account), strings.TrimSpace(comment))
|
|
return err
|
|
}
|
|
|
|
// lkDocRequests — заявки клиента на документы.
|
|
func lkDocRequests(ctx context.Context, rc *RuntimeConfig, contract string) ([]LKDocument, error) {
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := db.QueryContext(ctx, fsql.Doc04, contract)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []LKDocument
|
|
for rows.Next() {
|
|
var d LKDocument
|
|
var created time.Time
|
|
var from, to *time.Time
|
|
var status string
|
|
if err := rows.Scan(&d.ID, &created, &d.Kind, &from, &to, &d.Account, &status, &d.Note); err != nil {
|
|
continue
|
|
}
|
|
d.Ordered, d.Title, d.Date = true, d.Kind, created.Format("02.01.2006")
|
|
switch status {
|
|
case "done":
|
|
d.State = "готов"
|
|
case "rejected":
|
|
d.State = "отклонён"
|
|
default:
|
|
d.State = "в работе"
|
|
}
|
|
if from != nil && to != nil {
|
|
d.Period = from.Format("02.01.2006") + " — " + to.Format("02.01.2006")
|
|
}
|
|
out = append(out, d)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// lkReadyDocuments — готовые выписки по счетам клиента из учётной системы.
|
|
func lkReadyDocuments(ctx context.Context, s FancyMSSQLSettings, contract string) ([]LKDocument, error) {
|
|
if strings.TrimSpace(s.Server) == "" {
|
|
return nil, nil
|
|
}
|
|
ids, _, accounts, err := depoClientIDs(ctx, s, contract)
|
|
if err != nil || len(ids) == 0 {
|
|
return nil, err
|
|
}
|
|
conn, err := fancyOpen(s, s.DepoDB())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
cctx, cancel := context.WithTimeout(ctx, 25*time.Second)
|
|
defer cancel()
|
|
q := fmt.Sprintf(fsql.Doc05, inClause(ids))
|
|
rows, err := conn.QueryContext(cctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []LKDocument
|
|
for rows.Next() {
|
|
var rd, bd, ed sql.NullTime
|
|
var num string
|
|
var acc int64
|
|
var state, turns sql.NullInt64
|
|
if rows.Scan(&rd, &num, &bd, &ed, &acc, &state, &turns) != nil {
|
|
continue
|
|
}
|
|
d := LKDocument{Title: "Выписка по счёту депо", Account: accounts[acc], State: docStateRu(state, sql.NullTime{}), Note: num}
|
|
if turns.Valid && turns.Int64 == 1 {
|
|
d.Title = "Выписка по счёту депо с оборотами"
|
|
}
|
|
if rd.Valid {
|
|
d.Date = rd.Time.Format("02.01.2006")
|
|
}
|
|
if bd.Valid && ed.Valid {
|
|
d.Period = bd.Time.Format("02.01.2006") + " — " + ed.Time.Format("02.01.2006")
|
|
} else if ed.Valid {
|
|
d.Period = "на " + ed.Time.Format("02.01.2006")
|
|
}
|
|
out = append(out, d)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// notifyDocRequest — сообщить сотрудникам депозитария, что клиент заказал документ.
|
|
func notifyDocRequest(rc *RuntimeConfig, u LKUser, kind, from, to string) {
|
|
period := ""
|
|
if from != "" || to != "" {
|
|
period = fmt.Sprintf(" за период %s — %s", pick(from, "…"), pick(to, "…"))
|
|
}
|
|
go raiseSystemAlert(rc, "Кабинет: заказан документ",
|
|
fmt.Sprintf("Клиент %s (договор %s) заказал документ «%s»%s. Заявка видна в кабинете клиента и в журнале lk.doc_requests.",
|
|
pick(u.FullName, u.Login), u.Contract, kind, period))
|
|
}
|