Тест «боевой ЛК → TEST3»: заявки выбранных клиентов ЕСИА Финанс — у нас всё по-настоящему (DEPO, QUIK МНП боевая запись только для них), в НРД с подменой инвестора на тест-клиента; «Исполнена» в ЛК по MS010; повтор отправки тоже с подменой; пометка и шаги ЛК в пути заявки; включатель на «Тестирование M2M»

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-09-29 13:10:06 +03:00
parent 340fca7bd7
commit 91d8584a9c
15 changed files with 485 additions and 21 deletions
+233
View File
@@ -0,0 +1,233 @@
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/m2m-testing/real-nsd: включить/выключить тест и список клиентов.
func registerRealNSDTest(mux *http.ServeMux, rc *RuntimeConfig) {
mux.HandleFunc("/admin/m2m-testing/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 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)
http.Redirect(w, r, "/admin/m2m-testing#real-nsd", http.StatusSeeOther)
})
}