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
238 lines
8.6 KiB
Go
238 lines
8.6 KiB
Go
package lkgateway
|
|
|
|
// lkhistory.go — история для кабинета клиента: движения по счетам депо и выплаты дохода
|
|
// с удержанным налогом. Источник — учётная система депозитария (DEPO):
|
|
//
|
|
// операции: OD_DOCS + U_OP_INV_ZACH / U_OP_INV_SPIS / U_OP_INV_PEREVOD (связь по DEPO = OD_CLIENTS.ID);
|
|
// доходы: U_OP_INF_INCOME + U_OP_INF_INCOME_DOL (связь по DEPO или INCOME_FACE);
|
|
// налог: OD_NDFL по лицу.
|
|
//
|
|
// Кабинет ничего не считает сам: суммы и статусы показываются как в учёте.
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// LKOperation — движение по счёту депо.
|
|
type LKOperation struct {
|
|
Date string `json:"date"` // дата регистрации, ДД.ММ.ГГГГ
|
|
DoneAt string `json:"done_at,omitempty"` // дата проведения
|
|
Kind string `json:"kind"` // зачисление | списание | перевод между счетами
|
|
Number string `json:"number,omitempty"` // номер документа депозитария
|
|
Account string `json:"account,omitempty"` // счёт депо
|
|
ISIN string `json:"isin,omitempty"`
|
|
Security string `json:"security,omitempty"`
|
|
Quantity float64 `json:"quantity"` // со знаком: списание отрицательное
|
|
State string `json:"state"` // исполнено | в обработке | отменено
|
|
Note string `json:"note,omitempty"` // комментарий или причина отказа
|
|
}
|
|
|
|
// LKIncome — выплата дохода клиенту.
|
|
type LKIncome struct {
|
|
Date string `json:"date"`
|
|
Kind string `json:"kind"` // купон | дивиденды | доход
|
|
ISIN string `json:"isin,omitempty"`
|
|
Security string `json:"security,omitempty"`
|
|
Quantity float64 `json:"quantity,omitempty"` // бумаг в расчёте
|
|
Accrued float64 `json:"accrued"` // начислено
|
|
Tax float64 `json:"tax"` // удержан налог
|
|
Paid float64 `json:"paid"` // к выплате
|
|
State string `json:"state,omitempty"`
|
|
Account string `json:"account,omitempty"`
|
|
}
|
|
|
|
// depoClientIDs — внутренние идентификаторы счетов депо клиента и его лицо в DEPO.
|
|
func depoClientIDs(ctx context.Context, s FancyMSSQLSettings, contract string) (ids []int64, face int64, accounts map[int64]string, err error) {
|
|
conn, err := fancyOpen(s, s.DepoDB())
|
|
if err != nil {
|
|
return nil, 0, nil, err
|
|
}
|
|
defer conn.Close()
|
|
cctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
|
defer cancel()
|
|
rows, err := conn.QueryContext(cctx, fsql.Hist01, contract, likeEscape(contract)+" %")
|
|
if err != nil {
|
|
return nil, 0, nil, err
|
|
}
|
|
defer rows.Close()
|
|
accounts = map[int64]string{}
|
|
for rows.Next() {
|
|
var id, f int64
|
|
var num string
|
|
if rows.Scan(&id, &num, &f) != nil {
|
|
continue
|
|
}
|
|
ids = append(ids, id)
|
|
accounts[id] = num
|
|
if face == 0 {
|
|
face = f
|
|
}
|
|
}
|
|
return ids, face, accounts, rows.Err()
|
|
}
|
|
|
|
// docStateRu — состояние документа депозитария словами.
|
|
func docStateRu(state sql.NullInt64, proc sql.NullTime) string {
|
|
switch {
|
|
case state.Valid && state.Int64 == 2:
|
|
return "исполнено"
|
|
case state.Valid && state.Int64 == 3:
|
|
return "отменено"
|
|
case proc.Valid:
|
|
return "исполнено"
|
|
}
|
|
return "в обработке"
|
|
}
|
|
|
|
// inClause — список идентификаторов для IN (...) — значения свои, не от пользователя.
|
|
func inClause(ids []int64) string {
|
|
parts := make([]string, 0, len(ids))
|
|
for _, id := range ids {
|
|
parts = append(parts, fmt.Sprint(id))
|
|
}
|
|
return strings.Join(parts, ",")
|
|
}
|
|
|
|
// parseRange — период запроса: пустые значения — последний год по сегодня.
|
|
func parseRange(from, to string) (time.Time, time.Time) {
|
|
fromT, err := time.Parse("2006-01-02", strings.TrimSpace(from))
|
|
if err != nil {
|
|
fromT = time.Now().AddDate(-1, 0, 0) // по умолчанию — последний год
|
|
}
|
|
toT, err := time.Parse("2006-01-02", strings.TrimSpace(to))
|
|
if err != nil {
|
|
toT = time.Now()
|
|
}
|
|
return time.Date(fromT.Year(), fromT.Month(), fromT.Day(), 0, 0, 0, 0, time.UTC),
|
|
time.Date(toT.Year(), toT.Month(), toT.Day(), 0, 0, 0, 0, time.UTC)
|
|
}
|
|
|
|
// lkOperations — движения по счетам депо клиента за период (from/to — ГГГГ-ММ-ДД, можно пустыми).
|
|
func lkOperations(ctx context.Context, s FancyMSSQLSettings, contract, from, to string, limit int) ([]LKOperation, error) {
|
|
if strings.TrimSpace(s.Server) == "" {
|
|
return nil, fmt.Errorf("учётная система депозитария не подключена")
|
|
}
|
|
ids, _, accounts, err := depoClientIDs(ctx, s, contract)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ids) == 0 {
|
|
return nil, nil
|
|
}
|
|
conn, err := fancyOpen(s, s.DepoDB())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
if limit <= 0 || limit > 500 {
|
|
limit = 200
|
|
}
|
|
fromT, toT := parseRange(from, to)
|
|
// Три вида операций в одном списке: зачисление, списание, перевод между счетами.
|
|
q := fmt.Sprintf(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
|
|
}
|
|
defer rows.Close()
|
|
var out []LKOperation
|
|
for rows.Next() {
|
|
var rd, pd sql.NullTime
|
|
var num, kind, isin, sec, note string
|
|
var st sql.NullInt64
|
|
var acc int64
|
|
var qty float64
|
|
if rows.Scan(&rd, &pd, &num, &st, &kind, &acc, &isin, &sec, &qty, ¬e) != nil {
|
|
continue
|
|
}
|
|
op := LKOperation{Kind: kind, Number: num, ISIN: isin, Security: sec, Quantity: qty,
|
|
Account: accounts[acc], State: docStateRu(st, pd), Note: strings.TrimSpace(note)}
|
|
if rd.Valid {
|
|
op.Date = rd.Time.Format("02.01.2006")
|
|
}
|
|
if pd.Valid && pd.Time.Year() < 9000 {
|
|
op.DoneAt = pd.Time.Format("02.01.2006")
|
|
}
|
|
out = append(out, op)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// incomeKindRu — вид дохода из учёта.
|
|
func incomeKindRu(kind sql.NullInt64) string {
|
|
switch {
|
|
case !kind.Valid:
|
|
return "доход"
|
|
case kind.Int64 == 1:
|
|
return "дивиденды"
|
|
case kind.Int64 == 2:
|
|
return "купон"
|
|
case kind.Int64 == 3:
|
|
return "погашение"
|
|
}
|
|
return "доход"
|
|
}
|
|
|
|
// lkIncome — выплаты дохода клиенту за период и удержанный налог по данным учёта.
|
|
func lkIncome(ctx context.Context, s FancyMSSQLSettings, contract, from, to string) (rowsOut []LKIncome, taxTotal float64, err error) {
|
|
if strings.TrimSpace(s.Server) == "" {
|
|
return nil, 0, fmt.Errorf("учётная система депозитария не подключена")
|
|
}
|
|
ids, face, accounts, err := depoClientIDs(ctx, s, contract)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if len(ids) == 0 {
|
|
return nil, 0, nil
|
|
}
|
|
conn, err := fancyOpen(s, s.DepoDB())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer conn.Close()
|
|
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
fromT, toT := parseRange(from, to)
|
|
q := fmt.Sprintf(fsql.Hist03, inClause(ids))
|
|
rows, err := conn.QueryContext(cctx, q, fromT, toT, face)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var pd sql.NullTime
|
|
var kind sql.NullInt64
|
|
var isin, sec, status string
|
|
var qty, accrued, tax, paid float64
|
|
var acc int64
|
|
if rows.Scan(&pd, &kind, &isin, &sec, &qty, &accrued, &tax, &paid, &status, &acc) != nil {
|
|
continue
|
|
}
|
|
in := LKIncome{Kind: incomeKindRu(kind), ISIN: isin, Security: sec, Quantity: qty,
|
|
Accrued: accrued, Tax: tax, Paid: paid, Account: accounts[acc]}
|
|
if st := strings.TrimSpace(status); st != "" && st != "0" { // «0» — служебное значение учёта
|
|
in.State = st
|
|
}
|
|
if pd.Valid {
|
|
in.Date = pd.Time.Format("02.01.2006")
|
|
}
|
|
rowsOut = append(rowsOut, in)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return rowsOut, 0, err
|
|
}
|
|
// Удержанный налог по лицу за период — по журналу НДФЛ депозитария.
|
|
if face > 0 {
|
|
_ = conn.QueryRowContext(cctx, fsql.Hist04,
|
|
sql.Named("p1", face), sql.Named("p2", fromT), sql.Named("p3", toT)).Scan(&taxTotal)
|
|
}
|
|
return rowsOut, taxTotal, nil
|
|
}
|