From cd6e2c424f2c9934d54240093f92ed868eadaa23 Mon Sep 17 00:00:00 2001
From: fontvielle
Date: Tue, 13 Oct 2026 16:02:06 +0300
Subject: [PATCH] =?UTF-8?q?=D0=AD=D0=BC=D1=83=D0=BB=D1=8F=D1=86=D0=B8?=
=?UTF-8?q?=D1=8F=20=D0=9D=D0=A0=D0=94:=20=D0=BE=D0=B4=D0=B8=D0=BD=20?=
=?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D1=82?=
=?UTF-8?q?=D0=B5=D0=BB=D1=8C=20=D0=92=D1=8B=D0=BA=D0=BB=D1=8E=D1=87=D0=B5?=
=?UTF-8?q?=D0=BD=D0=B0/=D0=9F=D0=BE=D0=B4=D0=BC=D0=B5=D0=BD=D0=B0/=D0=9F?=
=?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B0=D1=8F=20=D0=B8=D0=BC=D0=B8=D1=82=D0=B0?=
=?UTF-8?q?=D1=86=D0=B8=D1=8F;=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83=D1=80=20?=
=?UTF-8?q?=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Было: правило «боевой ЛК + тестовый профиль → принудительно имитатор» не давало выключить
эмуляцию, а подмена личности жила отдельным тумблером на другой странице — пользователь не мог
понять состояние и выключить.
Стало — явная модель (контур НРД и режим эмуляции независимы):
- «Выключена» — заявки идут в выбранный контур (NSD.Profile) с реальными данными клиента;
- «Подмена личности» — уходят в контур, но у клиентов из списка блок инвестора заменяется на
тест-инвестора (Бубликов/Баранкин); DEPO и QUIK — по реальному клиенту;
- «Полная имитация» — в НРД ничего не уходит, bj сам отвечает (внутренний имитатор).
Режимы взаимоисключающие (ведутся флагами EmulateNSD/RealNSDTest.Enabled), принудительное
правило убрано. Единый 3-радио на /admin/emulators (как у QUIK), с показом контура, тест-инвестора
и списка паспортов; карточка на /admin/m2m-testing — статус + ссылка на «Эмуляторы».
Тесты обновлены под новую модель.
Co-Authored-By: Claude Opus 4.8
---
internal/lkgateway/emulators.go | 33 ++++++--
internal/lkgateway/emulators_test.go | 4 +-
internal/lkgateway/lkemulate.go | 60 ++++++++++-----
internal/lkgateway/lkemulate_test.go | 34 ++++++---
internal/lkgateway/realtest.go | 3 +
.../web/templates/admin_emulators.html | 75 +++++++++----------
.../web/templates/admin_m2mtest.html | 22 ++----
7 files changed, 140 insertions(+), 91 deletions(-)
diff --git a/internal/lkgateway/emulators.go b/internal/lkgateway/emulators.go
index 575fddf..c78c831 100644
--- a/internal/lkgateway/emulators.go
+++ b/internal/lkgateway/emulators.go
@@ -45,6 +45,9 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
NSDEmuOn bool
NSDEmuWhy string
NSDFlag bool
+ NSDMode string
+ Profile string
+ ProfileTest bool
NSD NSDEmuSettings
RejectCodes []emuRejectOption
RealNSDOn bool
@@ -60,6 +63,9 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
NSDEmuOn: nsdOn,
NSDEmuWhy: nsdWhy,
NSDFlag: s.LK.EmulateNSD,
+ NSDMode: nsdEmuMode(rc),
+ Profile: s.NSD.Profile,
+ ProfileTest: strings.HasPrefix(strings.ToLower(s.NSD.Profile), "test"),
RealNSDOn: s.LK.RealNSDTest.Enabled,
RealNSDDocs: s.LK.RealNSDTest.Docs,
RealNSDAs: realNSDAsText(),
@@ -80,7 +86,18 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
return
}
lk := rc.Snapshot().LK
- lk.EmulateNSD = r.FormValue("emulate") != ""
+ // Режим эмуляции НРД — взаимоисключающий: off | substitute | imitate.
+ switch r.FormValue("mode") {
+ case "imitate":
+ lk.EmulateNSD, lk.RealNSDTest.Enabled = true, false
+ case "substitute":
+ lk.EmulateNSD, lk.RealNSDTest.Enabled = false, true
+ if d := strings.TrimSpace(r.FormValue("docs")); d != "" {
+ lk.RealNSDTest.Docs = d
+ }
+ default: // off
+ lk.EmulateNSD, lk.RealNSDTest.Enabled = false, false
+ }
delay, err := strconv.Atoi(strings.TrimSpace(r.FormValue("delay_sec")))
if err != nil && strings.TrimSpace(r.FormValue("delay_sec")) != "" {
emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор НРД: задержка — целое число секунд")
@@ -100,13 +117,17 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
return
}
applyNSDEmuSettings(rc)
- msg := "выключен — заявки ЛК идут в НРД по обычному пути"
- if on, why := lkEmulateNSD(rc); on {
+ prof := rc.Snapshot().NSD.Profile
+ msg := "выключена — заявки идут в выбранный контур НРД (" + prof + ") с реальными данными клиента"
+ switch nsdEmuMode(rc) {
+ case "imitate":
eff := rc.Snapshot().NSDEmu.Defaults()
- msg = fmt.Sprintf("включён (%s) — в НРД ничего не отправляется; решение контрагента: %s через %d сек",
- why, emuOutcomeRu(eff.Outcome), eff.DelaySec)
+ msg = fmt.Sprintf("полная имитация — в НРД ничего не отправляется; ответ bj: %s через %d сек",
+ emuOutcomeRu(eff.Outcome), eff.DelaySec)
+ case "substitute":
+ msg = "подмена личности — в НРД (" + prof + ") вместо клиента из списка уходит тест-инвестор " + realNSDAsText()
}
- emuAjaxReply(w, r, http.StatusOK, "Эмулятор НРД: "+msg)
+ emuAjaxReply(w, r, http.StatusOK, "Эмуляция НРД: "+msg)
})
mux.HandleFunc("/admin/emulators/quik", func(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/lkgateway/emulators_test.go b/internal/lkgateway/emulators_test.go
index f2df31d..70204dc 100644
--- a/internal/lkgateway/emulators_test.go
+++ b/internal/lkgateway/emulators_test.go
@@ -105,7 +105,7 @@ func TestEmulatorsSave(t *testing.T) {
}
rec := post("/admin/emulators/nsd", url.Values{
- "emulate": {"1"}, "delay_sec": {"45"}, "outcome": {"reject"}, "reject_code": {"18"},
+ "mode": {"imitate"}, "delay_sec": {"45"}, "outcome": {"reject"}, "reject_code": {"18"},
})
if rec.Code != http.StatusOK {
t.Fatalf("nsd: %d %s", rec.Code, rec.Body.String())
@@ -142,7 +142,7 @@ func TestEmulatorsSave(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("GET: %d", rec.Code)
}
- for _, want := range []string{"Эмулятор НРД", "Эмулятор МНП QUIK", "в НРД ничего не отправляется", "M2M18"} {
+ for _, want := range []string{"Эмуляция НРД", "Эмулятор МНП QUIK", "ничего не отправляется", "M2M18"} {
if !strings.Contains(rec.Body.String(), want) {
t.Errorf("на странице нет %q", want)
}
diff --git a/internal/lkgateway/lkemulate.go b/internal/lkgateway/lkemulate.go
index e4071e4..23b7e8f 100644
--- a/internal/lkgateway/lkemulate.go
+++ b/internal/lkgateway/lkemulate.go
@@ -33,14 +33,28 @@ const lkEmuNote = " (тестовый режим: в НРД не отправл
// lkEmuSender — имитатор НРД для заявок ЛК; создаётся в Server.Run.
var lkEmuSender *mock.Sender
-// lkEmulateNSD — идут ли заявки ЛК через имитатор НРД и почему.
-func lkEmulateNSD(rc *RuntimeConfig) (bool, string) {
+// nsdEmuMode — режим эмуляции НРД: "off" | "substitute" | "imitate". Выбор контура НРД
+// (NSD.Profile — test3 / боевой) от режима НЕ зависит: при "off" заявка уходит в выбранный
+// контур как есть. Режимы взаимоисключающие (ведутся флагами EmulateNSD / RealNSDTest.Enabled).
+func nsdEmuMode(rc *RuntimeConfig) string {
s := rc.Snapshot()
- if s.LK.EmulateNSD && devMode(rc) {
- return true, "включена в настройках"
+ switch {
+ case s.LK.EmulateNSD:
+ return "imitate"
+ case s.LK.RealNSDTest.Enabled:
+ return "substitute"
}
- if s.LK.ActiveEnv == "prod" && strings.HasPrefix(strings.ToLower(s.NSD.Profile), "test") {
- return true, "боевой ЛК при тестовом контуре НРД (профиль ИШ " + s.NSD.Profile + ")"
+ return "off"
+}
+
+// lkEmulateNSD — обрабатывается ли заявка ЛК слоем эмуляции (имитация или подмена личности).
+// "off" — заявка идёт в выбранный контур НРД с реальными данными клиента (обычный путь).
+func lkEmulateNSD(rc *RuntimeConfig) (bool, string) {
+ switch nsdEmuMode(rc) {
+ case "imitate":
+ return true, "полная имитация — в НРД ничего не отправляется, ответ формирует bj"
+ case "substitute":
+ return true, "подмена личности — в НРД уходит тест-инвестор вместо клиента"
}
return false, ""
}
@@ -95,17 +109,29 @@ func lkProcessEmulated(ctx context.Context, rc *RuntimeConfig, svc *Service, ep
globalLKFlows.step(guid, id, "crane", "Проверка шлюза bj: можно исполнить",
fmt.Sprintf("Сверка с DEPO: %s (инвестор %s, счёт %s).", res.Summary, pick(res.Investor, "—"), pick(res.Account, "—")))
- // Тест «боевой ЛК → TEST3» (realtest.go): клиент из списка — в настоящий НРД с подменой инвестора.
- sender := m2mcore.NSDSender(lkEmuSender)
- realNSD := realTestMatch(rc, inv.Document.Series, inv.Document.Number) && svc.sender != nil
- if realNSD {
- asInv, asText := realTestInvestor(rc, req.Data.InvestorInformation.IdentityDocument.DocumentType)
- realTestPut(RealTestRec{GUID: guid, ClaimID: id, At: time.Now(), As: asText,
- Client: strings.TrimSpace(fmt.Sprintf("%s %s %s, %s %s", inv.LastName, inv.FirstName, inv.MiddleName, inv.Document.Series, inv.Document.Number))})
- sender = substituteSender{real: svc.sender, inv: asInv}
- globalLKFlows.step(guid, id, "gateway", "Тест: в НРД (TEST3) — с данными тест-инвестора",
- "У нас заявка проходит как клиент ("+inv.LastName+"): DEPO, QUIK МНП (боевая запись), статусы в ЛК. В НРД блок инвестора заменён на "+asText+"; GUID и референсы — настоящие.")
- log.Printf("lk-gateway: ЛК #%d (%s): тест «боевой ЛК → TEST3» — в НРД как %s", id, shortGUID(guid), asText)
+ // Выбор отправителя по режиму эмуляции НРД:
+ // imitate — внутренний имитатор (в НРД ничего не уходит, ответ формирует bj);
+ // substitute — реальная отправка в выбранный контур НРД; клиент из списка идёт с подменой
+ // блока инвестора на тест-инвестора, остальные — с настоящими данными.
+ sender := m2mcore.NSDSender(lkEmuSender) // imitate (по умолчанию в этом пути)
+ realNSD := false
+ if nsdEmuMode(rc) == "substitute" && svc.sender != nil {
+ realNSD = true
+ if realTestMatch(rc, inv.Document.Series, inv.Document.Number) {
+ asInv, asText := realTestInvestor(rc, req.Data.InvestorInformation.IdentityDocument.DocumentType)
+ realTestPut(RealTestRec{GUID: guid, ClaimID: id, At: time.Now(), As: asText,
+ Client: strings.TrimSpace(fmt.Sprintf("%s %s %s, %s %s", inv.LastName, inv.FirstName, inv.MiddleName, inv.Document.Series, inv.Document.Number))})
+ sender = substituteSender{real: svc.sender, inv: asInv}
+ globalLKFlows.step(guid, id, "gateway", "Подмена личности: в НРД ("+rc.Snapshot().NSD.Profile+") с данными тест-инвестора",
+ "У нас заявка проходит как клиент ("+inv.LastName+"): DEPO, QUIK МНП (боевая запись), статусы в ЛК. В НРД блок инвестора заменён на "+asText+"; GUID и референсы — настоящие.")
+ log.Printf("lk-gateway: ЛК #%d (%s): подмена личности — в НРД как %s", id, shortGUID(guid), asText)
+ } else {
+ // подмена включена, но клиент не в списке — отправляем в НРД с настоящими данными
+ sender = svc.sender
+ globalLKFlows.step(guid, id, "gateway", "В НРД ("+rc.Snapshot().NSD.Profile+") с настоящими данными",
+ "Подмена включена, но этот клиент не в списке — запрос уходит в НРД как есть.")
+ log.Printf("lk-gateway: ЛК #%d (%s): отправка в НРД с настоящими данными (вне списка подмены)", id, shortGUID(guid))
+ }
}
if _, err := svc.SendBuiltRequestVia(ctx, sender, req, inv.ID, nil); err != nil {
if strings.Contains(err.Error(), "QUIK") {
diff --git a/internal/lkgateway/lkemulate_test.go b/internal/lkgateway/lkemulate_test.go
index e75bb05..e5089d6 100644
--- a/internal/lkgateway/lkemulate_test.go
+++ b/internal/lkgateway/lkemulate_test.go
@@ -40,6 +40,7 @@ func TestLKEmulateNSD(t *testing.T) {
t.Fatal(err)
}
s := rc.Snapshot()
+ // Контур test3 сам по себе эмуляцию НЕ включает — режим выбирается явно.
s.LK.ActiveEnv, s.NSD.Profile = "prod", "test3-gost"
if err := rc.UpdateLK(s.LK); err != nil {
t.Fatal(err)
@@ -47,23 +48,32 @@ func TestLKEmulateNSD(t *testing.T) {
if err := rc.UpdateNSD(s.NSD); err != nil {
t.Fatal(err)
}
- if on, why := lkEmulateNSD(rc); !on || !strings.Contains(why, "тестовом контуре НРД") {
- t.Errorf("боевой ЛК + тестовый ИШ: ожидали имитацию, получили %v %q", on, why)
- }
- s.LK.ActiveEnv = "test"
- _ = rc.UpdateLK(s.LK)
if on, _ := lkEmulateNSD(rc); on {
- t.Error("тестовый ЛК без флага: имитации быть не должно")
+ t.Error("тестовый контур без режима: эмуляции быть не должно")
}
+ if m := nsdEmuMode(rc); m != "off" {
+ t.Errorf("режим по умолчанию: %q, ожидали off", m)
+ }
+ // Полная имитация.
+ s = rc.Snapshot()
s.LK.EmulateNSD = true
_ = rc.UpdateLK(s.LK)
- if on, _ := lkEmulateNSD(rc); on {
- t.Error("флаг в настройках без режима разработки не действует")
+ if on, why := lkEmulateNSD(rc); !on || !strings.Contains(why, "имитаци") {
+ t.Errorf("полная имитация: %v %q", on, why)
}
- devModeForced = true
- defer func() { devModeForced = false }()
- if on, why := lkEmulateNSD(rc); !on || why != "включена в настройках" {
- t.Errorf("флаг в настройках: %v %q", on, why)
+ if m := nsdEmuMode(rc); m != "imitate" {
+ t.Errorf("режим: %q, ожидали imitate", m)
+ }
+ // Подмена личности (взаимоисключающа с имитацией).
+ s = rc.Snapshot()
+ s.LK.EmulateNSD = false
+ s.LK.RealNSDTest.Enabled = true
+ _ = rc.UpdateLK(s.LK)
+ if on, why := lkEmulateNSD(rc); !on || !strings.Contains(why, "подмена") {
+ t.Errorf("подмена: %v %q", on, why)
+ }
+ if m := nsdEmuMode(rc); m != "substitute" {
+ t.Errorf("режим: %q, ожидали substitute", m)
}
}
diff --git a/internal/lkgateway/realtest.go b/internal/lkgateway/realtest.go
index 0445877..d45838b 100644
--- a/internal/lkgateway/realtest.go
+++ b/internal/lkgateway/realtest.go
@@ -224,6 +224,9 @@ func registerRealNSDTest(mux *http.ServeMux, rc *RuntimeConfig) {
lk := rc.Snapshot().LK
lk.RealNSDTest.Enabled = r.FormValue("enabled") != ""
lk.RealNSDTest.Docs = strings.TrimSpace(r.FormValue("docs"))
+ if lk.RealNSDTest.Enabled {
+ lk.EmulateNSD = false // режимы эмуляции НРД взаимоисключающие (подмена ≠ полная имитация)
+ }
if err := rc.UpdateLK(lk); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
diff --git a/internal/lkgateway/web/templates/admin_emulators.html b/internal/lkgateway/web/templates/admin_emulators.html
index aea37ea..f30430b 100644
--- a/internal/lkgateway/web/templates/admin_emulators.html
+++ b/internal/lkgateway/web/templates/admin_emulators.html
@@ -7,54 +7,51 @@
Эти же флаги доступны в «Настройке» (ЛК — «Имитация НРД», QUIK — «имитация») — состояние общее.
-{{/* ===== Эмулятор НРД ===== */}}
+{{/* ===== Эмуляция НРД ===== */}}
-
Эмулятор НРД
- {{if .NSDEmuOn}}включён{{end}}
-
Заявки из личного кабинета и ручного ввода проходят весь путь (сверка с DEPO, блокировка в QUIK, сделка, статусы),
- но в НРД ничего не отправляется: ответ контрагента формирует внутренний имитатор.
- {{if and .NSDEmuOn (ne .NSDEmuWhy "включена в настройках")}}
-
Сейчас имитация действует принудительно: {{.NSDEmuWhy}} — переключатель её не выключает.
- {{end}}
- {{if .RealNSDOn}}
-
-
⚠ Подмена личности в НРД включена — для этих клиентов имитатор НЕ используется.
- Заявки по паспорту(ам)
{{.RealNSDDocs}} уходят в
реальный тестовый НРД (TEST3) с подменой на тест-инвестора
{{.RealNSDAs}} (запрос отправляется от его имени; GUID, бумаги и счета настоящие, DEPO и QUIK МНП — боевые). Поэтому ответ приходит не от имитатора, а от реального НРД — и если на той стороне никто не отвечает, заявка ждёт.
- {{if can "settings"}}
-
-
Настроить список клиентов — на странице «Тестирование M2M».
- {{end}}
-
+
Эмуляция НРД
+ {{if eq .NSDMode "imitate"}}полная имитация{{end}}
+ {{if eq .NSDMode "substitute"}}подмена личности{{end}}
+
Контур НРД сейчас: {{.Profile}} — заявки уходят туда, когда эмуляция выключена (выбор контура — в «Настройке»). Контур и режим эмуляции независимы. Режимы эмуляции сбрасываются автоматически, когда заканчивается режим разработки.
+ {{if and (eq .NSDMode "off") .ProfileTest}}
+
Контур тестовый ({{.Profile}}), а эмуляция выключена — реальные заявки клиентов уйдут в тестовый НРД с настоящими данными (НРД может их не узнать). Для теста используйте «Подмену личности».
{{end}}