Files
Bridge-and-Join-s/internal/lkgateway/realtest.go
T
fontvielle cd6e2c424f Эмуляция НРД: один переключатель Выключена/Подмена/Полная имитация; контур отдельно
Было: правило «боевой ЛК + тестовый профиль → принудительно имитатор» не давало выключить
эмуляцию, а подмена личности жила отдельным тумблером на другой странице — пользователь не мог
понять состояние и выключить.

Стало — явная модель (контур НРД и режим эмуляции независимы):
- «Выключена» — заявки идут в выбранный контур (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>
2026-10-13 16:02:06 +03:00

258 lines
9.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// Тест «боевой ЛК → тестовый контур НРД» (29.09.2026). Заявка реального клиента из ЛК ЕСИА Финанс
// проходит у нас всё по-настоящему: проверка шлюза и поручение в DEPO, блокировка и списание в QUIK
// МНП (боевая запись — только для таких заявок, остальные остаются в имитации), статусы в ЛК. В НРД
// (TEST3) запрос уходит с подменой блока инвестора на тест-клиента, зарегистрированного в тестовом
// контуре (Бубликов): GUID, референсы, бумаги и счета — настоящие, поэтому все ответы НРД по GUID
// применяются к заявке клиента. «Исполнена» в ЛК — только по отчёту НРД об исполнении (MS010).
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
)
// RealNSDTestSettings — кто идёт в НРД с подменой и под чьими данными.
type RealNSDTestSettings struct {
Enabled bool `json:"enabled"`
// Docs — паспорта клиентов (серия номер), чьи заявки идут в НРД с подменой, через «;».
Docs string `json:"docs,omitempty"`
// As — тест-инвестор, зарегистрированный в тестовом контуре НРД. Пусто — Бубликов (К1).
AsLast string `json:"as_last,omitempty"`
AsFirst string `json:"as_first,omitempty"`
AsMiddle string `json:"as_middle,omitempty"`
AsSeries string `json:"as_series,omitempty"`
AsNumber string `json:"as_number,omitempty"`
}
func (s RealNSDTestSettings) as() (last, first, middle, ser, num string) {
if s.AsLast == "" || s.AsNumber == "" {
k := m2mTestClients["К1"]
return k.Last, k.First, k.Middle, k.DocSeries, k.DocNumber
}
return s.AsLast, s.AsFirst, s.AsMiddle, s.AsSeries, s.AsNumber
}
func normDoc(ser, num string) string {
return strings.ReplaceAll(strings.TrimSpace(ser)+strings.TrimSpace(num), " ", "")
}
// realTestMatch — заявка этого клиента идёт в НРД с подменой.
func realTestMatch(rc *RuntimeConfig, ser, num string) bool {
if rc == nil {
return false
}
t := rc.Snapshot().LK.RealNSDTest
if !t.Enabled || normDoc(ser, num) == "" {
return false
}
for _, d := range strings.Split(t.Docs, ";") {
f := strings.Fields(d)
if len(f) >= 1 && normDoc(strings.Join(f[:len(f)-1], ""), f[len(f)-1]) == normDoc(ser, num) {
return true
}
}
return false
}
// RealTestRec — заявка, ушедшая в НРД с подменой.
type RealTestRec struct {
GUID string `json:"guid"`
ClaimID int `json:"claim_id,omitempty"` // заявка ЛК
Client string `json:"client"` // ФИО и паспорт клиента
As string `json:"as"` // под чьими данными ушло в НРД
At time.Time `json:"at"`
LKDone bool `json:"lk_done,omitempty"` // итог передан в ЛК
}
var realTestFile = "/var/lib/bj/.bj/real-nsd-test.json"
var realTests = struct {
sync.Mutex
loaded bool
m map[string]*RealTestRec
}{}
func realTestLoad() {
if realTests.loaded {
return
}
realTests.loaded, realTests.m = true, map[string]*RealTestRec{}
if raw, err := os.ReadFile(realTestFile); err == nil {
_ = json.Unmarshal(raw, &realTests.m)
}
}
func realTestSave() {
if raw, err := json.MarshalIndent(realTests.m, "", " "); err == nil {
_ = os.WriteFile(realTestFile, raw, 0o600)
}
}
func realTestGet(guid string) *RealTestRec {
if guid == "" {
return nil
}
realTests.Lock()
defer realTests.Unlock()
realTestLoad()
if r, ok := realTests.m[strings.ToLower(guid)]; ok {
cp := *r
return &cp
}
return nil
}
func realTestPut(r RealTestRec) {
realTests.Lock()
defer realTests.Unlock()
realTestLoad()
r.GUID = strings.ToLower(r.GUID)
realTests.m[r.GUID] = &r
realTestSave()
}
// realTestAny — есть тестовые заявки, по которым ещё ждём итог (опросчик QUIK должен работать).
func realTestAny() bool {
realTests.Lock()
defer realTests.Unlock()
realTestLoad()
for _, r := range realTests.m {
if !r.LKDone {
return true
}
}
return false
}
func realTestMarkLKDone(guid string) bool {
realTests.Lock()
defer realTests.Unlock()
realTestLoad()
r, ok := realTests.m[strings.ToLower(guid)]
if !ok || r.LKDone {
return false
}
r.LKDone = true
realTestSave()
return true
}
// substituteSender — отправитель в НРД с подменой блока инвестора; сделка у нас хранит исходный запрос.
type substituteSender struct {
real m2mcore.NSDSender
inv m2m.InvestorInformation
}
func (s substituteSender) Send(ctx context.Context, req *m2m.M2MTransferRequest) (*m2m.M2MTransferResponse, error) {
cp := *req
cp.Data.InvestorInformation = s.inv
return s.real.Send(ctx, &cp)
}
func (s substituteSender) SendDecision(ctx context.Context, d *m2m.M2MTransferDecision) error {
return s.real.SendDecision(ctx, d)
}
// realTestInvestor — блок инвестора тест-клиента в формате запроса НРД; тип документа — как у клиента.
func realTestInvestor(rc *RuntimeConfig, docType m2m.IdentityDocumentCode) (m2m.InvestorInformation, string) {
last, first, middle, ser, num := rc.Snapshot().LK.RealNSDTest.as()
inv := m2m.InvestorInformation{LastName: last, FirstName: first, MiddleName: middle}
inv.IdentityDocument.DocumentType = docType
if ser != "" {
sv := m2m.IdentityDocSerial(ser)
inv.IdentityDocument.DocumentSeries = &sv
}
inv.IdentityDocument.DocumentNumber = m2m.IdentityDocSerial(num)
return inv, strings.TrimSpace(fmt.Sprintf("%s %s %s, %s %s", last, first, middle, ser, num))
}
// realTestSenderFor — отправитель для заявки: с подменой, если заявка из теста; иначе def.
func realTestSenderFor(rc *RuntimeConfig, guid string, req *m2m.M2MTransferRequest, def m2mcore.NSDSender) m2mcore.NSDSender {
if realTestGet(guid) == nil || req == nil || rc == nil {
return def
}
inv, _ := realTestInvestor(rc, req.Data.InvestorInformation.IdentityDocument.DocumentType)
return substituteSender{real: def, inv: inv}
}
// quikSettingsFor — настройки МНП для перевода: у тестовых заявок «боевой ЛК → TEST3» запись в МНП
// настоящая (по решению 29.09.2026), у остальных — как в настройках.
func quikSettingsFor(rc *RuntimeConfig, guid string) QuikMNPSettings {
s := rc.Snapshot().Quik
if realTestGet(guid) != nil {
s.Emulate, s.WritesEnabled = false, true
}
return s
}
// realTestOnExecuted — НРД исполнил (или отверг) наше поручение 16: итог — в ЛК.
func realTestOnExecuted(ctx context.Context, rc *RuntimeConfig, guid string, rejected bool, partial string) {
if realTestGet(guid) == nil || !realTestMarkLKDone(guid) {
return
}
status, reason := "done", "Перевод исполнен: бумаги списаны"
switch {
case rejected && partial == "":
status, reason = "rejected", "НРД не исполнил поручение на списание"
case partial != "":
reason = "Перевод исполнен частично: " + partial
}
globalLKFlows.stepIfExists(guid, pick3(status == "done", "response", "reject"), "Итог НРД (MS010) → статус в ЛК", reason)
OnLKOutcome(ctx, rc, guid, status, reason)
log.Printf("real-test: %s — итог в ЛК: %s (%s)", shortGUID(guid), status, reason)
}
// registerRealNSDTest — POST /admin/setup/lk-real-nsd: включить/выключить тест и список клиентов
// (в «Настройка → ЛК ЕСИА Финанс», доступно без режима разработки — право «настройка»).
func registerRealNSDTest(mux *http.ServeMux, rc *RuntimeConfig) {
mux.HandleFunc("/admin/setup/lk-real-nsd", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
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
}
log.Printf("real-test: %v, клиенты: %s", lk.RealNSDTest.Enabled, lk.RealNSDTest.Docs)
back := "/admin/setup"
switch b := r.FormValue("back"); b {
case "/admin/m2m-testing", "/admin/emulators", "/admin/setup":
back = b
default:
if ref := r.Referer(); strings.Contains(ref, "/admin/m2m-testing") {
back = "/admin/m2m-testing"
} else if strings.Contains(ref, "/admin/emulators") {
back = "/admin/emulators"
}
}
http.Redirect(w, r, back+"#lk-real-nsd", http.StatusSeeOther)
})
}
// realNSDAsText — тест-инвестор для подписи в настройках.
func realNSDAsText() string {
if globalRC == nil {
return ""
}
l, f, m, sr, n := globalRC.Snapshot().LK.RealNSDTest.as()
return strings.TrimSpace(l + " " + f + " " + m + ", " + sr + " " + n)
}