Эмуляция НРД: один переключатель Выключена/Подмена/Полная имитация; контур отдельно

Было: правило «боевой ЛК + тестовый профиль → принудительно имитатор» не давало выключить
эмуляцию, а подмена личности жила отдельным тумблером на другой странице — пользователь не мог
понять состояние и выключить.

Стало — явная модель (контур НРД и режим эмуляции независимы):
- «Выключена» — заявки идут в выбранный контур (NSD.Profile) с реальными данными клиента;
- «Подмена личности» — уходят в контур, но у клиентов из списка блок инвестора заменяется на
  тест-инвестора (Бубликов/Баранкин); DEPO и QUIK — по реальному клиенту;
- «Полная имитация» — в НРД ничего не уходит, bj сам отвечает (внутренний имитатор).
Режимы взаимоисключающие (ведутся флагами EmulateNSD/RealNSDTest.Enabled), принудительное
правило убрано. Единый 3-радио на /admin/emulators (как у QUIK), с показом контура, тест-инвестора
и списка паспортов; карточка на /admin/m2m-testing — статус + ссылка на «Эмуляторы».
Тесты обновлены под новую модель.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fontvielle
2026-10-13 16:02:06 +03:00
parent 6ca556c2fc
commit cd6e2c424f
7 changed files with 140 additions and 91 deletions
+27 -6
View File
@@ -45,6 +45,9 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
NSDEmuOn bool NSDEmuOn bool
NSDEmuWhy string NSDEmuWhy string
NSDFlag bool NSDFlag bool
NSDMode string
Profile string
ProfileTest bool
NSD NSDEmuSettings NSD NSDEmuSettings
RejectCodes []emuRejectOption RejectCodes []emuRejectOption
RealNSDOn bool RealNSDOn bool
@@ -60,6 +63,9 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
NSDEmuOn: nsdOn, NSDEmuOn: nsdOn,
NSDEmuWhy: nsdWhy, NSDEmuWhy: nsdWhy,
NSDFlag: s.LK.EmulateNSD, 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, RealNSDOn: s.LK.RealNSDTest.Enabled,
RealNSDDocs: s.LK.RealNSDTest.Docs, RealNSDDocs: s.LK.RealNSDTest.Docs,
RealNSDAs: realNSDAsText(), RealNSDAs: realNSDAsText(),
@@ -80,7 +86,18 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
return return
} }
lk := rc.Snapshot().LK 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"))) delay, err := strconv.Atoi(strings.TrimSpace(r.FormValue("delay_sec")))
if err != nil && strings.TrimSpace(r.FormValue("delay_sec")) != "" { if err != nil && strings.TrimSpace(r.FormValue("delay_sec")) != "" {
emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор НРД: задержка — целое число секунд") emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор НРД: задержка — целое число секунд")
@@ -100,13 +117,17 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
return return
} }
applyNSDEmuSettings(rc) applyNSDEmuSettings(rc)
msg := "выключен — заявки ЛК идут в НРД по обычному пути" prof := rc.Snapshot().NSD.Profile
if on, why := lkEmulateNSD(rc); on { msg := "выключена — заявки идут в выбранный контур НРД (" + prof + ") с реальными данными клиента"
switch nsdEmuMode(rc) {
case "imitate":
eff := rc.Snapshot().NSDEmu.Defaults() eff := rc.Snapshot().NSDEmu.Defaults()
msg = fmt.Sprintf("включён (%s) — в НРД ничего не отправляется; решение контрагента: %s через %d сек", msg = fmt.Sprintf("полная имитация — в НРД ничего не отправляется; ответ bj: %s через %d сек",
why, emuOutcomeRu(eff.Outcome), eff.DelaySec) 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) { mux.HandleFunc("/admin/emulators/quik", func(w http.ResponseWriter, r *http.Request) {
+2 -2
View File
@@ -105,7 +105,7 @@ func TestEmulatorsSave(t *testing.T) {
} }
rec := post("/admin/emulators/nsd", url.Values{ 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 { if rec.Code != http.StatusOK {
t.Fatalf("nsd: %d %s", rec.Code, rec.Body.String()) t.Fatalf("nsd: %d %s", rec.Code, rec.Body.String())
@@ -142,7 +142,7 @@ func TestEmulatorsSave(t *testing.T) {
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("GET: %d", rec.Code) 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) { if !strings.Contains(rec.Body.String(), want) {
t.Errorf("на странице нет %q", want) t.Errorf("на странице нет %q", want)
} }
+38 -12
View File
@@ -33,14 +33,28 @@ const lkEmuNote = " (тестовый режим: в НРД не отправл
// lkEmuSender — имитатор НРД для заявок ЛК; создаётся в Server.Run. // lkEmuSender — имитатор НРД для заявок ЛК; создаётся в Server.Run.
var lkEmuSender *mock.Sender var lkEmuSender *mock.Sender
// lkEmulateNSD — идут ли заявки ЛК через имитатор НРД и почему. // nsdEmuMode — режим эмуляции НРД: "off" | "substitute" | "imitate". Выбор контура НРД
func lkEmulateNSD(rc *RuntimeConfig) (bool, string) { // (NSD.Profile — test3 / боевой) от режима НЕ зависит: при "off" заявка уходит в выбранный
// контур как есть. Режимы взаимоисключающие (ведутся флагами EmulateNSD / RealNSDTest.Enabled).
func nsdEmuMode(rc *RuntimeConfig) string {
s := rc.Snapshot() s := rc.Snapshot()
if s.LK.EmulateNSD && devMode(rc) { switch {
return true, "включена в настройках" 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 "off"
return true, "боевой ЛК при тестовом контуре НРД (профиль ИШ " + s.NSD.Profile + ")" }
// lkEmulateNSD — обрабатывается ли заявка ЛК слоем эмуляции (имитация или подмена личности).
// "off" — заявка идёт в выбранный контур НРД с реальными данными клиента (обычный путь).
func lkEmulateNSD(rc *RuntimeConfig) (bool, string) {
switch nsdEmuMode(rc) {
case "imitate":
return true, "полная имитация — в НРД ничего не отправляется, ответ формирует bj"
case "substitute":
return true, "подмена личности — в НРД уходит тест-инвестор вместо клиента"
} }
return false, "" return false, ""
} }
@@ -95,17 +109,29 @@ func lkProcessEmulated(ctx context.Context, rc *RuntimeConfig, svc *Service, ep
globalLKFlows.step(guid, id, "crane", "Проверка шлюза bj: можно исполнить", globalLKFlows.step(guid, id, "crane", "Проверка шлюза bj: можно исполнить",
fmt.Sprintf("Сверка с DEPO: %s (инвестор %s, счёт %s).", res.Summary, pick(res.Investor, "—"), pick(res.Account, "—"))) fmt.Sprintf("Сверка с DEPO: %s (инвестор %s, счёт %s).", res.Summary, pick(res.Investor, "—"), pick(res.Account, "—")))
// Тест «боевой ЛК → TEST3» (realtest.go): клиент из списка — в настоящий НРД с подменой инвестора. // Выбор отправителя по режиму эмуляции НРД:
sender := m2mcore.NSDSender(lkEmuSender) // imitate — внутренний имитатор (в НРД ничего не уходит, ответ формирует bj);
realNSD := realTestMatch(rc, inv.Document.Series, inv.Document.Number) && svc.sender != nil // substitute — реальная отправка в выбранный контур НРД; клиент из списка идёт с подменой
if realNSD { // блока инвестора на тест-инвестора, остальные — с настоящими данными.
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) asInv, asText := realTestInvestor(rc, req.Data.InvestorInformation.IdentityDocument.DocumentType)
realTestPut(RealTestRec{GUID: guid, ClaimID: id, At: time.Now(), As: asText, 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))}) 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} sender = substituteSender{real: svc.sender, inv: asInv}
globalLKFlows.step(guid, id, "gateway", "Тест: в НРД (TEST3) — с данными тест-инвестора", globalLKFlows.step(guid, id, "gateway", "Подмена личности: в НРД ("+rc.Snapshot().NSD.Profile+") с данными тест-инвестора",
"У нас заявка проходит как клиент ("+inv.LastName+"): DEPO, QUIK МНП (боевая запись), статусы в ЛК. В НРД блок инвестора заменён на "+asText+"; GUID и референсы — настоящие.") "У нас заявка проходит как клиент ("+inv.LastName+"): DEPO, QUIK МНП (боевая запись), статусы в ЛК. В НРД блок инвестора заменён на "+asText+"; GUID и референсы — настоящие.")
log.Printf("lk-gateway: ЛК #%d (%s): тест «боевой ЛК → TEST3» — в НРД как %s", id, shortGUID(guid), asText) 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 _, err := svc.SendBuiltRequestVia(ctx, sender, req, inv.ID, nil); err != nil {
if strings.Contains(err.Error(), "QUIK") { if strings.Contains(err.Error(), "QUIK") {
+22 -12
View File
@@ -40,6 +40,7 @@ func TestLKEmulateNSD(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
s := rc.Snapshot() s := rc.Snapshot()
// Контур test3 сам по себе эмуляцию НЕ включает — режим выбирается явно.
s.LK.ActiveEnv, s.NSD.Profile = "prod", "test3-gost" s.LK.ActiveEnv, s.NSD.Profile = "prod", "test3-gost"
if err := rc.UpdateLK(s.LK); err != nil { if err := rc.UpdateLK(s.LK); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -47,23 +48,32 @@ func TestLKEmulateNSD(t *testing.T) {
if err := rc.UpdateNSD(s.NSD); err != nil { if err := rc.UpdateNSD(s.NSD); err != nil {
t.Fatal(err) 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 { 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 s.LK.EmulateNSD = true
_ = rc.UpdateLK(s.LK) _ = rc.UpdateLK(s.LK)
if on, _ := lkEmulateNSD(rc); on { if on, why := lkEmulateNSD(rc); !on || !strings.Contains(why, "имитаци") {
t.Error("флаг в настройках без режима разработки не действует") t.Errorf("полная имитация: %v %q", on, why)
} }
devModeForced = true if m := nsdEmuMode(rc); m != "imitate" {
defer func() { devModeForced = false }() t.Errorf("режим: %q, ожидали imitate", m)
if on, why := lkEmulateNSD(rc); !on || why != "включена в настройках" { }
t.Errorf("флаг в настройках: %v %q", on, why) // Подмена личности (взаимоисключающа с имитацией).
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)
} }
} }
+3
View File
@@ -224,6 +224,9 @@ func registerRealNSDTest(mux *http.ServeMux, rc *RuntimeConfig) {
lk := rc.Snapshot().LK lk := rc.Snapshot().LK
lk.RealNSDTest.Enabled = r.FormValue("enabled") != "" lk.RealNSDTest.Enabled = r.FormValue("enabled") != ""
lk.RealNSDTest.Docs = strings.TrimSpace(r.FormValue("docs")) lk.RealNSDTest.Docs = strings.TrimSpace(r.FormValue("docs"))
if lk.RealNSDTest.Enabled {
lk.EmulateNSD = false // режимы эмуляции НРД взаимоисключающие (подмена ≠ полная имитация)
}
if err := rc.UpdateLK(lk); err != nil { if err := rc.UpdateLK(lk); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -7,37 +7,31 @@
Эти же флаги доступны в «Настройке» (ЛК — «Имитация НРД», QUIK — «имитация») — состояние общее.</p> Эти же флаги доступны в «Настройке» (ЛК — «Имитация НРД», QUIK — «имитация») — состояние общее.</p>
</div> </div>
{{/* ===== Эмулятор НРД ===== */}} {{/* ===== Эмуляция НРД ===== */}}
<div class="card" id="emu-nsd-card"> <div class="card" id="emu-nsd-card">
<h2><span class="dot {{if .NSDEmuOn}}warn{{else}}ok{{end}}"></span>Эмулятор НРД <h2><span class="dot {{if ne .NSDMode "off"}}warn{{else}}ok{{end}}"></span>Эмуляция НРД
{{if .NSDEmuOn}}<span class="hero-status warn">включён</span>{{end}}</h2> {{if eq .NSDMode "imitate"}}<span class="hero-status warn">полная имитация</span>{{end}}
<p class="muted">Заявки из личного кабинета и ручного ввода проходят весь путь (сверка с DEPO, блокировка в QUIK, сделка, статусы), {{if eq .NSDMode "substitute"}}<span class="hero-status warn">подмена личности</span>{{end}}</h2>
но <b>в НРД ничего не отправляется</b>: ответ контрагента формирует внутренний имитатор.</p> <p class="muted">Контур НРД сейчас: <b>{{.Profile}}</b> — заявки уходят туда, когда эмуляция выключена (выбор контура — в «Настройке»). Контур и режим эмуляции независимы. Режимы эмуляции сбрасываются автоматически, когда заканчивается режим разработки.</p>
{{if and .NSDEmuOn (ne .NSDEmuWhy "включена в настройках")}} {{if and (eq .NSDMode "off") .ProfileTest}}
<p class="dq-warn">Сейчас имитация действует принудительно: {{.NSDEmuWhy}} — переключатель её не выключает.</p> <p class="dq-warn">Контур тестовый ({{.Profile}}), а эмуляция выключена — реальные заявки клиентов уйдут в тестовый НРД с настоящими данными (НРД может их не узнать). Для теста используйте «Подмену личности».</p>
{{end}}
{{if .RealNSDOn}}
<div class="dq-warn" style="margin:8px 0;padding:12px 14px;border-radius:8px;background:var(--warn-weak,#fbf3d9)">
<b>⚠ Подмена личности в НРД включена</b> — для этих клиентов имитатор НЕ используется.<br>
Заявки по паспорту(ам) <code>{{.RealNSDDocs}}</code> уходят в <b>реальный тестовый НРД (TEST3)</b> с подменой на тест-инвестора <b>{{.RealNSDAs}}</b> (запрос отправляется от его имени; GUID, бумаги и счета настоящие, DEPO и QUIK МНП — боевые). Поэтому ответ приходит не от имитатора, а от реального НРД — и если на той стороне никто не отвечает, заявка ждёт.
{{if can "settings"}}
<form method="post" action="/admin/setup/lk-real-nsd" style="margin-top:10px" onsubmit="return confirm('Выключить подмену личности в НРД? Новые заявки этих клиентов снова пойдут через имитатор (а не в реальный TEST3).');">
<input type="hidden" name="back" value="/admin/emulators">
<input type="hidden" name="docs" value="{{.RealNSDDocs}}">
<button class="btn" type="submit">Выключить подмену личности</button>
</form>
<p class="muted" style="margin:6px 0 0;font-size:12px">Настроить список клиентов — на странице «Тестирование M2M».</p>
{{end}}
</div>
{{end}} {{end}}
<form method="post" action="/admin/emulators/nsd" id="emu-nsd-form" class="dq"> <form method="post" action="/admin/emulators/nsd" id="emu-nsd-form" class="dq">
<label style="display:flex;gap:8px;align-items:center;font-weight:600"> <div class="dq-tiles">
<input type="checkbox" name="emulate" value="1" {{if .NSDFlag}}checked{{end}}> <label class="dq-tile"><input type="radio" name="mode" value="off" {{if eq .NSDMode "off"}}checked{{end}}><span><b>Выключена</b><small>Заявки клиентов уходят в выбранный контур НРД ({{.Profile}}) с реальными данными — как в боевой работе.</small></span></label>
Эмулировать НРД (заявки в настоящий НРД не уходят)</label> <label class="dq-tile"><input type="radio" name="mode" value="substitute" {{if eq .NSDMode "substitute"}}checked{{end}}><span><b>Подмена личности</b><small>Заявка уходит в контур НРД ({{.Profile}}), но у клиентов из списка блок инвестора заменяется на тест-инвестора{{if .RealNSDAs}} ({{.RealNSDAs}}){{end}}. DEPO и QUIK МНП — по реальному клиенту.</small></span></label>
<label class="dq-tile"><input type="radio" name="mode" value="imitate" {{if eq .NSDMode "imitate"}}checked{{end}}><span><b>Полная имитация</b><small>В НРД ничего не отправляется — bj сам отвечает корректным файлом (ответ контрагента формирует внутренний имитатор).</small></span></label>
</div>
<div id="emu-nsd-sub" style="margin-top:10px;max-width:560px">
<label>Паспорта клиентов для подмены <span class="muted">(серия номер, через «;»)</span>
<input type="text" name="docs" value="{{.RealNSDDocs}}" placeholder="1110 754503" spellcheck="false" class="dq-mono" style="width:100%"></label>
<p class="muted" style="font-size:12px;margin:4px 0 0">Заявки этих клиентов уйдут в НРД под данными тест-инвестора{{if .RealNSDAs}} <b>{{.RealNSDAs}}</b>{{end}}. Остальные — с настоящими данными.</p>
</div>
<div id="emu-nsd-imi">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin-top:10px"> <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin-top:10px">
<label>Задержка решения, сек <span class="muted">(0..300)</span> <label>Задержка ответа, сек <span class="muted">(0..300)</span>
<input type="number" name="delay_sec" min="0" max="300" value="{{.NSD.DelaySec}}" style="width:100%"></label> <input type="number" name="delay_sec" min="0" max="300" value="{{.NSD.DelaySec}}" style="width:100%"></label>
<label>Исход по заявке <label>Ответ имитатора
<select name="outcome" style="width:100%"> <select name="outcome" style="width:100%">
<option value="confirm" {{if eq .NSD.Outcome "confirm"}}selected{{end}}>Подтвердить перевод</option> <option value="confirm" {{if eq .NSD.Outcome "confirm"}}selected{{end}}>Подтвердить перевод</option>
<option value="reject" {{if eq .NSD.Outcome "reject"}}selected{{end}}>Отклонить (код ниже)</option> <option value="reject" {{if eq .NSD.Outcome "reject"}}selected{{end}}>Отклонить (код ниже)</option>
@@ -48,13 +42,16 @@
{{range .RejectCodes}}<option value="{{.Code}}" {{if eq $.NSD.RejectCode .Code}}selected{{end}}>{{.Label}}</option>{{end}} {{range .RejectCodes}}<option value="{{.Code}}" {{if eq $.NSD.RejectCode .Code}}selected{{end}}>{{.Label}}</option>{{end}}
</select></label> </select></label>
</div> </div>
</div>
<p class="dq-note" id="emu-nsd-st" aria-live="polite">Изменения сохраняются сразу.</p> <p class="dq-note" id="emu-nsd-st" aria-live="polite">Изменения сохраняются сразу.</p>
<noscript><button type="submit" class="btn">Сохранить</button></noscript> <noscript><button type="submit" class="btn">Сохранить</button></noscript>
</form> </form>
</div> </div>
<script>(function(){ <script>(function(){
var f=document.getElementById('emu-nsd-form'), st=document.getElementById('emu-nsd-st'), code=document.getElementById('emu-nsd-code'); var f=document.getElementById('emu-nsd-form'), st=document.getElementById('emu-nsd-st');
function sync(){ code.style.opacity=(f.outcome.value==='reject')?'1':'.45'; } var sub=document.getElementById('emu-nsd-sub'), imi=document.getElementById('emu-nsd-imi'), code=document.getElementById('emu-nsd-code');
function mode(){ var r=f.querySelector('input[name=mode]:checked'); return r?r.value:'off'; }
function sync(){ var m=mode(); sub.hidden=(m!=='substitute'); imi.hidden=(m!=='imitate'); if(code) code.style.opacity=(f.outcome.value==='reject')?'1':'.45'; }
f.addEventListener('change', function(){ sync(); f.addEventListener('change', function(){ sync();
fetch(f.action,{method:'POST',body:new URLSearchParams(new FormData(f)),headers:{'Content-Type':'application/x-www-form-urlencoded','X-BJ-Ajax':'1'},cache:'no-store'}) fetch(f.action,{method:'POST',body:new URLSearchParams(new FormData(f)),headers:{'Content-Type':'application/x-www-form-urlencoded','X-BJ-Ajax':'1'},cache:'no-store'})
.then(function(r){ return r.text().then(function(t){ st.textContent=(r.ok?'✓ Сохранено: ':'✗ ')+t; }); }) .then(function(r){ return r.text().then(function(t){ st.textContent=(r.ok?'✓ Сохранено: ':'✗ ')+t; }); })
@@ -63,21 +63,13 @@
{{end}} {{end}}
<div class="card" id="real-nsd" style="margin:10px 0;{{if .RealNSD.Enabled}}background:var(--warn-weak,#fbf3d9){{end}}"> <div class="card" id="real-nsd" style="margin:10px 0;{{if .RealNSD.Enabled}}background:var(--warn-weak,#fbf3d9){{end}}">
<h3 style="margin:0 0 6px;font-size:15px">Тест «боевой ЛК → тестовый контур НРД» {{if .RealNSD.Enabled}}<span class="badge awaiting_decision">включён</span>{{else}}<span class="badge draft">выключен</span>{{end}}</h3> <h3 style="margin:0 0 6px;font-size:15px">Эмуляция НРД: подмена личности {{if .RealNSD.Enabled}}<span class="badge awaiting_decision">включена</span>{{else}}<span class="badge draft">выключена</span>{{end}}</h3>
<p class="muted" style="margin:0 0 8px;font-size:13px">Заявки клиентов из списка (из ЛК ЕСИА Финанс) проходят у нас всё по-настоящему: проверка шлюза и поручение в DEPO, блокировка и списание в QUIK МНП (<b>боевая запись</b>), статусы в ЛК. В НРД (TEST3) запрос уходит с данными тест-инвестора <b>{{.RealNSDAs}}</b> — GUID, референсы, бумаги и счета настоящие, ответы НРД применяются к заявке клиента. «Исполнена» в ЛК — по отчёту НРД об исполнении (MS010). Остальные заявки ЛК — как раньше (без отправки в НРД).</p> {{if .RealNSD.Enabled}}
{{if .RealNSD.Enabled}}<p style="margin:0 0 10px;font-size:13.5px;color:var(--text)"><b>Сейчас подменяет:</b> заявки по паспорту(ам) <code>{{.RealNSD.Docs}}</code> → в НРД (TEST3) уходят от имени <b>{{.RealNSDAs}}</b>. Запрос отправляется от тест-инвестора; у клиента всё боевое (DEPO, QUIK МНП).</p> <p style="margin:0 0 6px;font-size:13.5px;color:var(--text)"><b>Сейчас подменяет:</b> заявки по паспорту(ам) <code>{{.RealNSD.Docs}}</code> → в НРД уходят от имени тест-инвестора <b>{{.RealNSDAs}}</b>. У клиента всё боевое (DEPO, QUIK МНП), в НРД — подменённый блок инвестора; ответы НРД применяются к заявке клиента, «Исполнена» в ЛК — по отчёту MS010.</p>
{{if can "settings"}}<form method="post" action="/admin/setup/lk-real-nsd" style="margin:0 0 10px" onsubmit="return confirm('Выключить подмену личности в НРД? Новые заявки этих клиентов снова пойдут через имитатор (а не в реальный TEST3).');"> {{else}}
<input type="hidden" name="back" value="/admin/m2m-testing"> <p class="muted" style="margin:0 0 6px;font-size:13px">Выключена — заявки клиентов идут в выбранный контур НРД с реальными данными. При включении заявки клиентов из списка уходят в НРД под данными тест-инвестора (остальное — боевое).</p>
<input type="hidden" name="docs" value="{{.RealNSD.Docs}}"> {{end}}
<button class="btn" type="submit" style="background:var(--err-weak,#fdecea);border-color:var(--err,#b3261e);color:var(--err,#b3261e)">Выключить подмену</button> <p class="muted" style="margin:0;font-size:13px">Управление режимом эмуляции НРД (выключена / подмена / полная имитация) и список клиентов — на странице <a href="/admin/emulators#emu-nsd-card">«Эмуляторы»</a>.</p>
</form>{{end}}{{end}}
{{if can "settings"}}<form method="post" action="/admin/setup/lk-real-nsd" class="cpf" onsubmit="return !this.enabled.checked || confirm('Включить? Для клиентов из списка QUIK МНП будет записывать по-настоящему (блокировка и списание бумаг).');">
<input type="hidden" name="back" value="/admin/m2m-testing">
<label style="flex-direction:row;align-items:center;gap:6px;font-size:13px;color:var(--text)"><input type="checkbox" name="enabled" style="min-width:0"{{if .RealNSD.Enabled}} checked{{end}}> включить</label>
<label style="min-width:280px">Паспорта клиентов (серия номер, через «;»)
<input name="docs" value="{{.RealNSD.Docs}}" placeholder="1110 754503"></label>
<button class="b-init" style="background:var(--card);border-color:var(--border);color:var(--text)">Сохранить</button>
</form>{{end}}
</div> </div>
<form method="post" action="/admin/m2m-testing/counterparty" class="cpf" id="cpForm"> <form method="post" action="/admin/m2m-testing/counterparty" class="cpf" id="cpForm">