b0de7f2979
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
248 lines
9.3 KiB
Go
248 lines
9.3 KiB
Go
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 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"
|
||
if ref := r.Referer(); strings.Contains(ref, "/admin/m2m-testing") {
|
||
back = "/admin/m2m-testing"
|
||
}
|
||
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)
|
||
}
|