feat(admin): dev-страница «Эмуляторы» — переключатели и настройки эмуляции НРД и МНП QUIK

В режиме разработки новая страница /admin/emulators с двумя клавишами:

- «Эмулятор НРД» (флаг LK.EmulateNSD): заявки ЛК и ручного ввода идут через
  внутренний имитатор, в НРД ничего не отправляется. Настройки: задержка
  решения (0–300 с), исход (подтвердить/отклонить/не отвечать), код отказа
  M2M01–M2M09/18. Сендер перенастраивается в рантайме через потокобезопасный
  mock.Sender.Reconfigure — без перезапуска.
- «Эмулятор МНП QUIK» (флаг QuikMNP.Emulate): шаги цикла без записи в боевую
  БД МНП. Настройки: задержка шага (0–60 с), точечные отказы по шагам
  (регистрация/подпись/отказ/зачисление) — эмулированный отказ ведёт себя как
  боевой: шаг OK=false, состояние не продвигается, повтор на следующем проходе.

Настройки хранятся в setup.json (nsd_emu/mnp_emu), обратная совместимость —
дефолты. Баннер «Режим эмуляции» показывается и при ручном включении
эмуляторов (раньше зависел только от отсутствия ИШ). Пункт меню «Эмуляторы»
и две плитки на дашборде — только в dev. Пути занесены в devOnlyPath.

Попутно: исправлен race в TestLicenseActivateAndRenew (cleanup восстанавливал
DefaultLicensePublicKey без moduleEnforceMu — падал под -race и на чистом HEAD).
This commit is contained in:
zuevav
2026-10-07 21:34:39 +03:00
parent 773c98d81e
commit fd238c1078
16 changed files with 775 additions and 17 deletions
+28 -2
View File
@@ -130,7 +130,7 @@ func lkProcessEmulated(ctx context.Context, rc *RuntimeConfig, svc *Service, ep
if devMode(rc) {
sentTitle = "Бумаги заблокированы, заявка передана имитатору НРД"
}
sentDetail := lkQuikSummary(guid) + fmt.Sprintf(" В НРД не отправлялась; ответ контрагента — через %s.", lkEmuDecisionDelay)
sentDetail := lkQuikSummary(guid) + fmt.Sprintf(" В НРД не отправлялась; ответ контрагента — через %s.", lkEmuDecisionDelayNow(rc))
if realNSD {
sentTitle, sentDetail = "Бумаги заблокированы, запрос перевода отправлен в НРД (TEST3)", lkQuikSummary(guid)+" Ждём решения контрагента от НРД."
}
@@ -142,7 +142,7 @@ func lkProcessEmulated(ctx context.Context, rc *RuntimeConfig, svc *Service, ep
log.Printf("lk-gateway: ЛК #%d (%s): сверка пройдена, бумаги заблокированы, отправлено в НРД TEST3 с подменой инвестора", id, shortGUID(guid))
return "sent", nil
}
log.Printf("lk-gateway: ЛК #%d (%s): сверка пройдена, бумаги заблокированы, заявка передана имитатору НРД — решение контрагента через %s", id, shortGUID(guid), lkEmuDecisionDelay)
log.Printf("lk-gateway: ЛК #%d (%s): сверка пройдена, бумаги заблокированы, заявка передана имитатору НРД — решение контрагента через %s", id, shortGUID(guid), lkEmuDecisionDelayNow(rc))
return "sent", nil
}
@@ -328,3 +328,29 @@ func newLKEmuSender() *mock.Sender {
cfg.NSDSenderCode = "MC0010300000"
return mock.NewSender(cfg)
}
// applyNSDEmuSettings переносит настройки эмулятора НРД (страница «Эмуляторы»)
// в конфиг работающего сендера — без пересоздания: канал Decisions() привязан
// к экземпляру. Вызывается при старте и после сохранения настроек.
func applyNSDEmuSettings(rc *RuntimeConfig) {
if lkEmuSender == nil {
return
}
s := rc.Snapshot().NSDEmu.Defaults()
cfg := mock.DefaultConfig()
cfg.NSDSenderCode = "MC0010300000"
cfg.DecisionDelay = time.Duration(s.DelaySec) * time.Second
switch s.Outcome {
case "reject":
cfg.DefaultOutcome = mock.OutcomeReject
case "timeout":
cfg.DefaultOutcome = mock.OutcomeTimeout
}
cfg.RejectionCode = s.RejectCode
lkEmuSender.Reconfigure(cfg)
}
// lkEmuDecisionDelayNow — текущая задержка решения имитатора (для текстов в ЛК).
func lkEmuDecisionDelayNow(rc *RuntimeConfig) time.Duration {
return time.Duration(rc.Snapshot().NSDEmu.Defaults().DelaySec) * time.Second
}