Кабинет клиента: портфель как у брокера — «Онлайн» по QUIK, «Архив» на дату и операции по SPECTRE
- lkclient.go: договор → код QUIK (БР-1545 → BR1545, +OUT), договор → D_B_CONTRACTS SPECTRE - lkonline.go: позиции DepoLimits (T2, свежая строка на счёт), средняя цена покупки, деньги MoneyLimits по валютам, цены и P&L; стоимость, доли - lkarchive.go: портфель на дату по остаткам OD_RESTS (REG_3 = договор, раздел 39, счета 50/51/52/55 и 58.01/58.02; 58.05 НКД не показываем) с откатом по OD_TURNS после даты (индекс REST+WIRDATE, знак TYPE_ проверен: сумма оборотов = остаток); оценка по котировке OD_VALUES_RATES ≤ 90 дней (в рублях за бумагу), иначе балансовая; операции из OD_WIRING (покупка/продажа/ввод/вывод/погашение/конвертация, продажа — по счёту ВЫРУЧКА) и пополнения/выводы денег; рецепт — InvestControl Hub (build_portfolio_base, deals_report) - API: /api/lk/portfolio2?view=online|archive&date=, /api/lk/ops (сделки + выплаты дохода); без клиента в QUIK онлайн падает на брокерский учёт за сегодня - кабинет: разделы Портфель (вкладки Онлайн/Архив с датой), Операции, Поручения, Документы, Профиль; депозитарные реестр и доходы из клиентского вида убраны (/portfolio → /, /income → /operations); проверено на БР-704 и БР-1545 - настройка Fancy.SpectreDatabase (по умолчанию BJ_SPECTRE_DATA) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
+47
-15
@@ -83,6 +83,32 @@ func fakeBJ(t *testing.T) *httptest.Server {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/api/lk/portfolio2", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !keyOK(w, r) {
|
||||
return
|
||||
}
|
||||
view := r.URL.Query().Get("view")
|
||||
asOf := "18.09.2026"
|
||||
if view == "archive" {
|
||||
asOf = "31.12.2025"
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"contract": "BR-1", "full_name": "Клиент Тестовый", "portfolio": map[string]any{
|
||||
"source": map[bool]string{true: "spectre", false: "quik"}[view == "archive"], "as_of": asOf,
|
||||
"total": 110268.0, "securities_value": 92698.28, "cash_rub": 17569.72, "cost": 80000.0, "pnl": 12698.28, "pnl_pct": 15.87, "priced": 1,
|
||||
"cash": []map[string]any{{"currency": "SUR", "amount": 17569.72}},
|
||||
"positions": []map[string]any{{"isin": "RU0009029540", "name": "Сбербанк, ао", "kind": "акция", "quantity": 336, "avg_price": 238.1, "price": 275.887, "value": 92698.28, "cost": 80000, "pnl": 12698.28, "pnl_pct": 15.87, "day_pct": 1.2, "share": 84.1}},
|
||||
}})
|
||||
})
|
||||
mux.HandleFunc("/api/lk/ops", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !keyOK(w, r) {
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"from": "2025-09-18", "to": "2026-09-18", "operations": []map[string]any{
|
||||
{"date": "10.07.2026", "kind": "продажа", "side": "out", "name": "НОВАТЭК 001P-03", "isin": "RU000A10AUX8", "quantity": 1700, "price": 7793.07, "amount": 13248214.78, "currency": "SUR", "amount_rub": 13248214.78},
|
||||
{"date": "06.03.2026", "kind": "покупка", "side": "in", "name": "Сбербанк, ао", "isin": "RU0009029540", "quantity": 100, "price": 238.1, "amount": 23810, "currency": "SUR", "amount_rub": 23810},
|
||||
{"date": "12.05.2026", "kind": "дивиденды", "side": "in", "name": "Сбербанк, ао", "isin": "RU0009029540", "quantity": 336, "amount": 494.6, "currency": "SUR", "amount_rub": 494.6},
|
||||
}})
|
||||
})
|
||||
mux.HandleFunc("/api/lk/admin/bootstrap", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !keyOK(w, r) {
|
||||
return
|
||||
@@ -280,19 +306,31 @@ func TestBoardPages(t *testing.T) {
|
||||
return rec.Body.String()
|
||||
}
|
||||
board := get("/")
|
||||
for _, want := range []string{"Стоимость бумаг", "Новое поручение", "Сбербанк, ао", "Отправлено в НРД", "Поручений в работе"} {
|
||||
for _, want := range []string{"Стоимость портфеля", "Новое поручение", "Сбербанк, ао", "Онлайн", "Архив", "Денежные средства", "12 698,28 ₽", "15,87 %", "84,1%"} {
|
||||
if !strings.Contains(board, want) {
|
||||
t.Errorf("на табло нет %q", want)
|
||||
t.Errorf("в портфеле нет %q", want)
|
||||
if i := strings.Index(board, "Результат"); i > 0 {
|
||||
t.Logf("фрагмент: %s", board[i:min(i+400, len(board))])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !strings.Contains(board, "110 268 ₽") {
|
||||
t.Errorf("на табло нет оценки портфеля")
|
||||
t.Errorf("стоимость портфеля не показана: %s", board)
|
||||
}
|
||||
reg := get("/portfolio")
|
||||
for _, want := range []string{"ВТНКЦ00000001", "иностранный выпуск", "депозитарный учёт", "перевести"} {
|
||||
if !strings.Contains(reg, want) {
|
||||
t.Errorf("в реестре нет %q", want)
|
||||
}
|
||||
arch := get("/?view=archive&date=2025-12-31")
|
||||
if !strings.Contains(arch, "на 31.12.2025") || !strings.Contains(arch, `value="2025-12-31"`) {
|
||||
t.Errorf("архив на дату не показан")
|
||||
}
|
||||
// старые адреса ведут на новые разделы
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/portfolio", nil))
|
||||
if rec.Code != http.StatusMovedPermanently || rec.Header().Get("Location") != "/" {
|
||||
t.Errorf("/portfolio должен вести на портфель: %d %s", rec.Code, rec.Header().Get("Location"))
|
||||
}
|
||||
rec = httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/income", nil))
|
||||
if rec.Code != http.StatusMovedPermanently || rec.Header().Get("Location") != "/operations" {
|
||||
t.Errorf("/income должен вести на операции: %d %s", rec.Code, rec.Header().Get("Location"))
|
||||
}
|
||||
orders := get("/orders")
|
||||
if !strings.Contains(orders, "Поручения депозитарию") || !strings.Contains(orders, "Перевод в другой депозитарий") {
|
||||
@@ -311,17 +349,11 @@ func TestBoardPages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
ops := get("/operations")
|
||||
for _, want := range []string{"Операции по счетам депо", "зачисление", "Ликвидность, пай", "Отчёт клиринговой организации"} {
|
||||
for _, want := range []string{"Операции", "продажа", "покупка", "дивиденды", "НОВАТЭК 001P-03", "−13 248 214,78 ₽", "+494,60 ₽", "Продажи"} {
|
||||
if !strings.Contains(ops, want) {
|
||||
t.Errorf("в операциях нет %q", want)
|
||||
}
|
||||
}
|
||||
inc := get("/income")
|
||||
for _, want := range []string{"Выплачено дохода", "494,60 ₽", "Удержан налог", "дивиденды"} {
|
||||
if !strings.Contains(inc, want) {
|
||||
t.Errorf("в доходах нет %q", want)
|
||||
}
|
||||
}
|
||||
prof := get("/profile")
|
||||
if !strings.Contains(prof, "Счета депо") || !strings.Contains(prof, "ВТНКЦ00000001") {
|
||||
t.Errorf("профиль: %s", prof)
|
||||
|
||||
Reference in New Issue
Block a user