Портфель ЛК: номер гос. регистрации из DEPO, пустой портфель не шлём, итог «Залить» на месте

Номер гос. регистрации — из DEPO (OD_SHARES.NUM_REG, выпуск бумаги), экспорт
QUIK — запасной: без подключённого экспорта (модуль НПР) все бумаги
пропускались. Портфель, где отправлять нечего (у всех бумаг нет номера —
иностранные, КСУ), в ЛК не уходит: ЛК отвечал на пустой импорт HTTP 500.
После «Залить сейчас» / «Включить» — назад в тот же раздел к карточке с
сообщением об итоге (раньше #lk-portfolio открывал «Обзор»).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-22 11:55:07 +03:00
parent 6f1df8350a
commit 8afa84a499
+62 -11
View File
@@ -89,18 +89,21 @@ func lkPortfolioLines(ctx context.Context, rc *RuntimeConfig, depoPort string, o
}
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 {
// Номер гос. регистрации — из DEPO (OD_SHARES.NUM_REG — выпуск бумаги, OD_SHARES.ID = OD_VALUES.ID),
// запасной — экспорт QUIK. Проверено на DEPO 28: у Сбербанка 10301481B, у ОФЗ 26222RMFS.
_ = db.QueryRowContext(ctx, `SELECT TOP 1 ISNULL(v.CFI,''), ISNULL(s.NUM_REG,'') FROM OD_VALUES v LEFT JOIN OD_SHARES s ON s.ID=v.ID
WHERE v.ISIN=@i ORDER BY CASE WHEN ISNULL(s.NUM_REG,'')<>'' THEN 0 ELSE 1 END, v.ID`, sql.Named("i", isin)).Scan(&cfi, &reg)
if strings.TrimSpace(reg) == "" && qerr == nil {
_ = qe.QueryRow(ctx, `SELECT "RegNumber" FROM "Securities" WHERE "ISINCode"=$1 AND COALESCE("RegNumber",'')<>'' LIMIT 1`, isin).Scan(&reg)
}
if qerr == nil {
if strings.TrimSpace(reg) != "" || qerr == nil {
lkSecInfoPut(isin, cfi, reg)
}
}
if strings.TrimSpace(reg) == "" {
why := "нет номера гос. регистрации в экспорте QUIK"
why := "нет номера гос. регистрации ни в DEPO, ни в экспорте QUIK (иностранные бумаги, КСУ)"
if qerr != nil {
why = "экспорт QUIK недоступен: " + qerr.Error()
why = "нет номера гос. регистрации в DEPO (экспорт QUIK не подключён)"
}
skipped = append(skipped, fmt.Sprintf("%s %s (%s): %s", isin, p.SecurityName, p.AccountNum, why))
continue
@@ -260,6 +263,12 @@ func lkPortfolioSyncTick(ctx context.Context, rc *RuntimeConfig, force bool) {
if same && !force {
continue
}
if len(lines) == 0 { // ЛК отвечает 500 на пустой импорт — не шлём
lkPortfolioState.Lock()
s.Skipped, s.Err, s.hash = skipped, lkPortfolioNothing(skipped), h
lkPortfolioState.Unlock()
continue
}
code, resp, perr := lkPortfolioPush(ctx, rc, pr.LK, lines)
lkPortfolioState.Lock()
s.Skipped = skipped
@@ -348,17 +357,41 @@ func registerLKPortfolioSync(mux *http.ServeMux, rc *RuntimeConfig) {
}
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
lkPortfolioPushPair(ctx, rc, pr)
n, skipped, perr := lkPortfolioPushPair(ctx, rc, pr)
cancel()
http.Redirect(w, r, "/admin/setup#lk-portfolio", http.StatusSeeOther)
msg := fmt.Sprintf("Портфель залит в ЛК: договор %s ← портфель DEPO %s, бумаг %d", lkc, port, n)
if skipped > 0 {
msg += fmt.Sprintf(" (пропущено %d — без номера гос. регистрации)", skipped)
}
if perr != "" {
msg = "Портфель не залит в ЛК (" + lkc + " ← " + port + "): " + perr
}
if r.FormValue("action") == "client_add" {
msg += ". Клиент добавлен в постоянное заполнение."
}
lkPortfolioDone(w, r, msg)
return
}
if a := r.FormValue("action"); a == "sync" || a == "on" || a == "on_all" { // включили или «Заполнить сейчас» — сразу отправить
msg := "Настройки заполнения портфеля сохранены"
switch a := r.FormValue("action"); a {
case "sync", "on", "on_all": // включили или «Заполнить сейчас» — сразу отправить
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
lkPortfolioSyncTick(ctx, rc, true)
cancel()
ok, bad := 0, 0
for _, st := range lkPortfolioStates() {
if st.Err != "" {
bad++
} else if !st.PushedAt.IsZero() {
ok++
}
}
msg = map[string]string{"on": "Заполнение по списку включено", "on_all": "Заполнение для всех клиентов включено", "sync": "Портфели сверены"}[a] +
fmt.Sprintf(": залито договоров %d, с ошибкой %d — подробности под карточкой", ok, bad)
case "off":
msg = "Заполнение портфеля выключено"
}
http.Redirect(w, r, "/admin/setup#lk-portfolio", http.StatusSeeOther)
lkPortfolioDone(w, r, msg)
})
}
@@ -458,8 +491,9 @@ func lkPortfolioAllPairs(ctx context.Context, rc *RuntimeConfig) ([]lkPortfolioP
return out, rows.Err()
}
// lkPortfolioPushPair — залить портфель одного клиента сейчас (тест / «Залить сейчас»).
func lkPortfolioPushPair(ctx context.Context, rc *RuntimeConfig, pr lkPortfolioPair) {
// lkPortfolioPushPair — залить портфель одного клиента сейчас (тест / «Залить сейчас»): сколько бумаг
// ушло, сколько пропущено, ошибка (пусто — залито).
func lkPortfolioPushPair(ctx context.Context, rc *RuntimeConfig, pr lkPortfolioPair) (int, int, string) {
lines, skipped, err := lkPortfolioLines(ctx, rc, pr.Depo, nil)
lkPortfolioState.Lock()
s := lkPortfolioState.m[pr.LK]
@@ -472,6 +506,8 @@ func lkPortfolioPushPair(ctx context.Context, rc *RuntimeConfig, pr lkPortfolioP
msg := ""
if err != nil {
msg = err.Error()
} else if len(lines) == 0 {
msg = lkPortfolioNothing(skipped)
} else if code, resp, perr := lkPortfolioPush(ctx, rc, pr.LK, lines); perr != nil {
msg = perr.Error()
} else if code != 200 || !strings.Contains(resp, `"success":true`) {
@@ -490,4 +526,19 @@ func lkPortfolioPushPair(ctx context.Context, rc *RuntimeConfig, pr lkPortfolioP
} else {
log.Printf("lk-portfolio: %s ← %s: портфель ДЕПО залит в ЛК (%d бумаг, пропущено %d)", pr.LK, pr.Depo, len(lines), len(skipped))
}
return len(lines), len(skipped), msg
}
// lkPortfolioNothing — почему портфель не отправлен: отправлять нечего.
func lkPortfolioNothing(skipped []string) string {
if len(skipped) == 0 {
return "отправлять нечего: в DEPO у клиента нет бумаг на остатке"
}
return fmt.Sprintf("отправлять нечего: у всех бумаг (%d) нет номера гос. регистрации, без него ЛК не принимает портфель", len(skipped))
}
// lkPortfolioDone — вернуться к карточке «Заполнение портфеля» в том же разделе настроек с сообщением.
func lkPortfolioDone(w http.ResponseWriter, r *http.Request, msg string) {
r.Form.Set("section", "lk-portfolio")
setupFlash(w, r, msg)
}