Кабинет: курсы ЦБ, история стоимости портфеля, быстрые операции, контакты в профиле
Курсы валют. Оценивать замещающие и еврооблигации по курсу из копии учётной системы было нельзя: в DEPO он от 31.05.2026, в SPECTRE — от 18.07.2026. Берём официальный курс Банка России (открытый XML, без ключа — тем же каналом пользуется Инвест-хаб) и складываем по дням в lk.fx_rates: без интернета кабинет считает по последнему известному курсу, а не отказывается оценивать. Источник переключается в настройках («Данные кабинета»), там же кнопка «Обновить курсы сейчас». Портфель БР-719: 560,3 млн ₽ против 522 млн по старому курсу. История стоимости. Ни QUIK, ни SPECTRE её не хранят, поэтому раз в день после закрытия торгов считаем стоимость каждого портфеля по экспорту QUIK и пишем строку в lk.portfolio_daily (257 портфелей за 14 секунд). Кабинет рисует по ним график на странице портфеля — SVG собирается на сервере, скриптов в кабинете по-прежнему нет. Режим по умолчанию — «автоматически»: ведём, если настроен экспорт QUIK, потому что задним числом историю не восстановить. Операции. Связка «JOIN OD_RESTS ON sr.ID = CASE WHEN w.D_REST IN (…)» заставляла SQL Server разбирать список счетов на каждой строке журнала проводок: год данных читался 6–7 секунд. Обе стороны проводки выбираются заранее двумя ветвями UNION ALL, индексы IX_WIRING_D_REST_WIRDATE и IX_WIRING_K_REST_WIRDATE отрабатывают поиском — 89 мс на тех же 64 строках. Период по умолчанию вернули к году, ответ кэшируется на пять минут. Профиль: почта, телефон и прошлый вход из анкеты — клиент видит, куда с ним свяжется депозитарий, и заметит устаревшие данные. Правка только через менеджера. Заказ документов из кабинета убран совсем (форма была убрана раньше, теперь нет и обработчика). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
+54
-1
@@ -33,7 +33,7 @@ func fakeBJ(t *testing.T) *httptest.Server {
|
||||
})
|
||||
mux.HandleFunc("/api/lk/portfolio", func(w http.ResponseWriter, r *http.Request) {
|
||||
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"full_name": "Мостовиков Тест", "contract": "4519", "source": "депозитарный учёт", "total_value": 110268.0, "priced": 1, "positions": []any{
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"full_name": "Мостовиков Тест", "contract": "4519", "source": "депозитарный учёт", "total_value": 110268.0, "priced": 1, "email": "client@example.org", "phone": "+7 900 000-00-00", "last_login": "17.09.2026 21:58", "positions": []any{
|
||||
map[string]any{"isin": "RU0009029540", "security_name": "Сбербанк, ао", "account_id": "ВТНКЦ00000001", "quantity": 400, "blocked": 100, "available": 300, "transferable": true, "updated_at": "17.09.2026 21:58", "price": 275.67, "value": 110268, "change_pct": -1.53},
|
||||
map[string]any{"isin": "US0378331005", "security_name": "Apple Inc.", "account_id": "В000000044", "quantity": 12, "blocked": 0, "available": 0, "transferable": false, "note": "иностранная бумага — перевод через M2M недоступен"},
|
||||
}})
|
||||
@@ -47,6 +47,15 @@ func fakeBJ(t *testing.T) *httptest.Server {
|
||||
}})
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/api/lk/history", func(w http.ResponseWriter, r *http.Request) {
|
||||
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"on": true, "at": "20:30", "points": []any{
|
||||
map[string]any{"day": "2026-09-16", "value": 100000.0, "cash": 200.0, "total": 100200.0},
|
||||
map[string]any{"day": "2026-09-17", "value": 104000.0, "cash": 200.0, "total": 104200.0},
|
||||
map[string]any{"day": "2026-09-18", "value": 110068.0, "cash": 200.0, "total": 110268.0},
|
||||
}})
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/api/lk/admin/clients", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !keyOK(w, r) || r.Header.Get("X-BJ-Session") != "sess-1" {
|
||||
return
|
||||
@@ -561,3 +570,47 @@ func TestClientsSearch(t *testing.T) {
|
||||
t.Errorf("пустой результат не объяснён: %s", empty)
|
||||
}
|
||||
}
|
||||
|
||||
// График стоимости и контакты в профиле.
|
||||
func TestChartAndProfile(t *testing.T) {
|
||||
bj := fakeBJ(t)
|
||||
s, err := New(Config{BJURL: bj.URL, BJKey: "lk-key", Title: "Кабинет"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := s.Handler()
|
||||
get := func(path string) string {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
req.AddCookie(&http.Cookie{Name: "bj_lk", Value: "sess-1"})
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("%s: код %d", path, rec.Code)
|
||||
}
|
||||
return rec.Body.String()
|
||||
}
|
||||
board := get("/")
|
||||
for _, want := range []string{"Стоимость портфеля", "<svg viewBox=", "срезов: 3", "16.09"} {
|
||||
if !strings.Contains(board, want) {
|
||||
t.Errorf("на графике нет %q", want)
|
||||
}
|
||||
}
|
||||
// В архиве график не показываем: там стоимость на выбранную дату.
|
||||
if strings.Contains(get("/?view=archive&date=2025-12-31"), `class="sec chart"`) {
|
||||
t.Error("в архиве график лишний")
|
||||
}
|
||||
prof := get("/profile")
|
||||
for _, want := range []string{"Контакты в анкете", "client@example.org", "900 000-00-00", "Прошлый вход"} {
|
||||
if !strings.Contains(prof, want) {
|
||||
t.Errorf("в профиле нет %q", want)
|
||||
}
|
||||
}
|
||||
// Заказ документов убран: формы и обработчика больше нет.
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/documents", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "bj_lk", Value: "sess-1"})
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code == http.StatusSeeOther {
|
||||
t.Error("POST /documents всё ещё принимает заказ документа")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user