package lkgateway import ( "context" "fmt" "strings" "time" ) // Чтение РЕАЛЬНЫХ портфелей клиентов из депозитарной базы Fancy (BJ_DEPO_DATA). // Только SELECT — боевая база учёта, ничего не меняем. // // Найденная цепочка связи: // // OD_CLIENTS_PORTS.PORT_NUM договор клиента («БР-1001 Гравс Геннадий») // ↓ CLIENT // OD_CLIENTS.ID счёт депо клиента (В00000957 / ВТНКЦ00000957) // ↓ = OD_RESTS.REG_1 ← ключ связи остатков со счётом // OD_RESTS K = количество, BAL_ACC (836 рос. / 318 иностр.), // BLOCK_DATE/BLOCK_REASON = блокировка // ↓ VALUE_ID // OD_VALUES бумага: NAME + ISIN (V_TYPE=2 — ценные бумаги) // // Важно: numeric-колонки читаем через CAST(... AS float) — иначе драйвер отдаёт 0. // DepoPosition — позиция клиента по данным депозитарного учёта. type DepoPosition struct { Contract string // номер договора (PORT_NUM) ClientName string // ФИО из договора AccountNum string // счёт депо (OD_CLIENTS.NUMBER) ISIN string // SecurityName string // Quantity float64 // остаток (OD_RESTS.K) Blocked float64 // из них заблокировано (по BLOCK_REASON) BlockReason string // BalAcc int // балансовый счёт (836 — рос., 318 — иностр.) } // depoContractExists проверяет, есть ли такой договор в депозитарии. func depoContractExists(ctx context.Context, s FancyMSSQLSettings, contract string) (string, bool) { conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return "", false } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 12*time.Second) defer cancel() var full string err = conn.QueryRowContext(cctx, "SELECT TOP 1 PORT_NUM FROM OD_CLIENTS_PORTS WHERE PORT_NUM=@p1 OR PORT_NUM LIKE @p2", contract, contract+" %").Scan(&full) if err != nil { return "", false } return full, true } // depoClientPositions читает реальный портфель клиента по номеру договора. // contract — «БР-1001» либо полная строка «БР-1001 Гравс Геннадий». func depoClientPositions(ctx context.Context, s FancyMSSQLSettings, contract string) ([]DepoPosition, error) { 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 := `SELECT p.PORT_NUM, ISNULL(c.NUMBER,''), ISNULL(v.ISIN,''), ISNULL(v.NAME,''), CAST(rst.K AS float), ISNULL(rst.BLOCK_REASON,''), ISNULL(rst.BAL_ACC,0) FROM OD_RESTS rst JOIN OD_CLIENTS c ON c.ID = rst.REG_1 JOIN OD_CLIENTS_PORTS p ON p.CLIENT = c.ID JOIN OD_VALUES v ON v.ID = rst.VALUE_ID AND v.V_TYPE = 2 WHERE (p.PORT_NUM = @p1 OR p.PORT_NUM LIKE @p2) AND CAST(rst.K AS float) <> 0 ORDER BY v.NAME` rows, err := conn.QueryContext(cctx, q, contract, contract+" %") if err != nil { return nil, err } defer rows.Close() var out []DepoPosition for rows.Next() { var p DepoPosition if err := rows.Scan(&p.Contract, &p.AccountNum, &p.ISIN, &p.SecurityName, &p.Quantity, &p.BlockReason, &p.BalAcc); err != nil { return out, err } // ФИО зашито в номер договора: «БР-1001 Гравс Геннадий» if i := strings.Index(p.Contract, " "); i > 0 { p.ClientName = strings.TrimSpace(p.Contract[i:]) } if p.BlockReason != "" { p.Blocked = p.Quantity } out = append(out, p) } return out, rows.Err() } // depoPositionsAsClient переводит позиции депозитария в вид, который отдаётся в // Личный кабинет (агрегируя одинаковые бумаги по разным счетам клиента). func depoPositionsAsClient(pos []DepoPosition) []ClientPosition { type agg struct { p ClientPosition accts map[string]bool } byISIN := map[string]*agg{} var order []string for _, d := range pos { key := strings.ToUpper(d.ISIN) a, ok := byISIN[key] if !ok { a = &agg{p: ClientPosition{ ISIN: d.ISIN, SecurityCode: d.ISIN, SecurityName: d.SecurityName, ClientName: d.ClientName, }, accts: map[string]bool{}} byISIN[key] = a order = append(order, key) } a.p.Quantity += d.Quantity a.p.Blocked += d.Blocked if d.AccountNum != "" && !a.accts[d.AccountNum] { a.accts[d.AccountNum] = true if a.p.AccountID == "" { a.p.AccountID = d.AccountNum } else { a.p.AccountID += ", " + d.AccountNum } } } out := make([]ClientPosition, 0, len(order)) for _, k := range order { a := byISIN[k] a.p.Available = a.p.Quantity - a.p.Blocked a.p.UpdatedAt = time.Now().Format("02.01.2006 15:04") out = append(out, a.p) } // Иностранные бумаги видны в портфеле, но к переводу недоступны. return markTransferable(out) } // DepoContract — реальный клиент депозитария для списка в админке ЛК. type DepoContract struct { Contract string `json:"contract"` // «БР-1001 Гравс Геннадий» ClientName string `json:"name"` // ФИО (если есть в номере договора) Securities int `json:"securities"` // сколько бумаг с ненулевым остатком } // depoListContracts — реальные клиенты депозитария с ненулевыми остатками. // search — часть номера договора или ФИО (пусто = первые limit по алфавиту). func depoListContracts(ctx context.Context, s FancyMSSQLSettings, search string, limit int) ([]DepoContract, error) { 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 = 100 } q := fmt.Sprintf(`SELECT TOP (%d) p.PORT_NUM, COUNT(DISTINCT rst.VALUE_ID) FROM OD_CLIENTS_PORTS p JOIN OD_CLIENTS c ON c.ID = p.CLIENT JOIN OD_RESTS rst ON rst.REG_1 = c.ID JOIN OD_VALUES v ON v.ID = rst.VALUE_ID AND v.V_TYPE = 2 WHERE CAST(rst.K AS float) <> 0 AND (@p1 = '' OR p.PORT_NUM LIKE @p2) GROUP BY p.PORT_NUM ORDER BY p.PORT_NUM`, limit) rows, err := conn.QueryContext(cctx, q, search, "%"+search+"%") if err != nil { return nil, err } defer rows.Close() var out []DepoContract for rows.Next() { var c DepoContract if err := rows.Scan(&c.Contract, &c.Securities); err != nil { return out, err } if i := strings.Index(c.Contract, " "); i > 0 { c.ClientName = strings.TrimSpace(c.Contract[i:]) } out = append(out, c) } return out, rows.Err() } // depoFindContracts ищет договоры клиентов по части номера/ФИО (для админки). func depoFindContracts(ctx context.Context, s FancyMSSQLSettings, like string, limit int) ([]string, error) { conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() if limit <= 0 || limit > 200 { limit = 50 } rows, err := conn.QueryContext(cctx, fmt.Sprintf( "SELECT DISTINCT TOP (%d) PORT_NUM FROM OD_CLIENTS_PORTS WHERE PORT_NUM LIKE @p1 ORDER BY PORT_NUM", limit), "%"+like+"%") if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var s string if err := rows.Scan(&s); err != nil { return out, err } out = append(out, s) } return out, rows.Err() }