Тест «боевой ЛК → 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:
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user