diff --git a/internal/lkgateway/lkportfolio.go b/internal/lkgateway/lkportfolio.go index 9bdf80f..9f7fb47 100644 --- a/internal/lkgateway/lkportfolio.go +++ b/internal/lkgateway/lkportfolio.go @@ -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, ®) + if strings.TrimSpace(reg) == "" && qerr == nil { _ = qe.QueryRow(ctx, `SELECT "RegNumber" FROM "Securities" WHERE "ISINCode"=$1 AND COALESCE("RegNumber",'')<>'' LIMIT 1`, isin).Scan(®) } - 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) }