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}}
- -
- - - +
+ + + +
+
+ +

Заявки этих клиентов уйдут в НРД под данными тест-инвестора{{if .RealNSDAs}} {{.RealNSDAs}}{{end}}. Остальные — с настоящими данными.

+
+
+
+ + + +

Изменения сохраняются сразу.