From 7cd358384490456726b320f25f419447bfa2d130 Mon Sep 17 00:00:00 2001 From: zuevav Date: Tue, 29 Sep 2026 15:09:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D0=B4=20=D0=B1=D1=83=D0=BC=D0=B0?= =?UTF-8?q?=D0=B3=D0=B8=20=D0=B2=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=B5=20=D0=9D=D0=A0=D0=94=20=E2=80=94=20=D0=BA=D0=BE=D0=B4=20?= =?UTF-8?q?=D0=9D=D0=A0=D0=94=20=D0=B8=D0=B7=20DEPO=20=D0=BF=D0=BE=20ISIN?= =?UTF-8?q?=20(=D1=83=20=D0=B0=D0=BA=D1=86=D0=B8=D0=B9=20=E2=89=A0=20ISIN:?= =?UTF-8?q?=20SBER/03),=20=D0=B8=D0=BD=D0=B0=D1=87=D0=B5=20M2M43?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi --- internal/lkgateway/nsdcode_live_test.go | 25 ++++++++++ internal/lkgateway/nsdseccode_test.go | 35 +++++++++++++ internal/lkgateway/quikblock_live_test.go | 56 +++++++++++++++++++++ internal/lkgateway/service.go | 61 +++++++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 internal/lkgateway/nsdcode_live_test.go create mode 100644 internal/lkgateway/nsdseccode_test.go create mode 100644 internal/lkgateway/quikblock_live_test.go diff --git a/internal/lkgateway/nsdcode_live_test.go b/internal/lkgateway/nsdcode_live_test.go new file mode 100644 index 0000000..80f6369 --- /dev/null +++ b/internal/lkgateway/nsdcode_live_test.go @@ -0,0 +1,25 @@ +//go:build live + +package lkgateway + +import ( + "fmt" + "testing" +) + +// Только чтение: код НРД бумаг из DEPO. +func TestNSDCodeLive(t *testing.T) { + rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json") + if err != nil { + t.Fatal(err) + } + fs := rc.Snapshot().Fancy + db, err := fancyOpen(fs, fs.DepoDB()) + if err != nil { + t.Fatal(err) + } + defer db.Close() + for _, i := range []string{"RU0009029540", "RU000A0JUR61", "RU000A102QL3", "RU000A1038V6", "RU000A107UL4"} { + fmt.Println(i, "→", depoNSDCode(db, i)) + } +} diff --git a/internal/lkgateway/nsdseccode_test.go b/internal/lkgateway/nsdseccode_test.go new file mode 100644 index 0000000..15b1f97 --- /dev/null +++ b/internal/lkgateway/nsdseccode_test.go @@ -0,0 +1,35 @@ +package lkgateway + +import ( + "testing" + "time" +) + +// Код бумаги НРД вместо ISIN (M2M43 29.09.2026): из DEPO; явный код НРД не трогаем. +func TestWithNSDSecCodes(t *testing.T) { + nsdSecCodes.Lock() + nsdSecCodes.m["RU0009029540"] = nsdSecCodeEntry{code: "SBER/03", at: time.Now()} + nsdSecCodes.m["RU000A0JUR61"] = nsdSecCodeEntry{code: "", at: time.Now()} + nsdSecCodes.Unlock() + old := globalRC + if globalRC == nil { + globalRC = &RuntimeConfig{} + } + defer func() { globalRC = old }() + in := CreateClaimRequest{Securities: []ClaimSec{ + {SecurityCode: "RU0009029540", SecurityDetails: SecurityDetails{ISIN: "RU0009029540"}}, + {SecurityCode: "", SecurityDetails: SecurityDetails{ISIN: "RU0009029540"}}, + {SecurityCode: "RU000A0JUR61", SecurityDetails: SecurityDetails{ISIN: "RU000A0JUR61"}}, + {SecurityCode: "SU26238RMFS4", SecurityDetails: SecurityDetails{ISIN: "RU000A1038V6"}}, + }} + out := withNSDSecCodes(in) + want := []string{"SBER/03", "SBER/03", "RU000A0JUR61", "SU26238RMFS4"} + for i, w := range want { + if out.Securities[i].SecurityCode != w { + t.Fatalf("бумага %d: %q, ожидалось %q", i, out.Securities[i].SecurityCode, w) + } + } + if in.Securities[0].SecurityCode != "RU0009029540" { + t.Fatal("исходная заявка не меняется") + } +} diff --git a/internal/lkgateway/quikblock_live_test.go b/internal/lkgateway/quikblock_live_test.go new file mode 100644 index 0000000..a2de646 --- /dev/null +++ b/internal/lkgateway/quikblock_live_test.go @@ -0,0 +1,56 @@ +//go:build live + +package lkgateway + +import ( + "context" + "fmt" + "os" + "testing" + "time" +) + +// Проверка боевой блокировки и снятия в МНП: «Вывод ЦБ (M2M)» 1 шт. → снятие. BLK_GO=1 +func TestQuikBlockUnblockLive(t *testing.T) { + if os.Getenv("BLK_GO") != "1" { + t.Skip() + } + rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json") + if err != nil { + t.Fatal(err) + } + globalRC = rc + ctx := context.Background() + s := rc.Snapshot().Quik + s.Emulate, s.WritesEnabled = false, true // только для этой проверки + if no := os.Getenv("BLK_DENY"); no != "" { // только снять поручение №BLK_DENY + var n float64 + fmt.Sscan(no, &n) + aid2, err := quikExecDeny(ctx, s, n, "Проверка bj: снятие тестовой блокировки") + fmt.Println("снятие ActionID:", aid2, err) + if err == nil { + r2 := quikWaitAction(ctx, s, aid2, 90*time.Second) + fmt.Printf("снятие: статус %d — %s\n", r2.QueryStatus, r2.ReplyText) + } + return + } + aid, err := quikExecNewInstr(ctx, s, true, "BJ-BLK-TEST", "BR859", "RU0009029540", 1, "7702015515", true, "bj: проверка блокировки/снятия") + fmt.Println("регистрация ActionID:", aid, err) + if err != nil { + return + } + r := quikWaitAction(ctx, s, aid, 90*time.Second) + fmt.Printf("блокировка: статус %d, поручение %s — %s\n", r.QueryStatus, r.InstrNo, r.ReplyText) + if r.QueryStatus != 2 || r.InstrNo == "" || r.InstrNo == "0" || os.Getenv("BLK_KEEP") == "1" { + return + } + var no float64 + fmt.Sscan(r.InstrNo, &no) + aid2, err := quikExecDeny(ctx, s, no, "Проверка bj: снятие тестовой блокировки") + fmt.Println("снятие ActionID:", aid2, err) + if err != nil { + return + } + r2 := quikWaitAction(ctx, s, aid2, 90*time.Second) + fmt.Printf("снятие: статус %d — %s\n", r2.QueryStatus, r2.ReplyText) +} diff --git a/internal/lkgateway/service.go b/internal/lkgateway/service.go index c0d2234..8e2741b 100644 --- a/internal/lkgateway/service.go +++ b/internal/lkgateway/service.go @@ -82,6 +82,7 @@ func (s *Service) CreateClaim(ctx context.Context, in CreateClaimRequest) (Creat // ReferenceID, реквизиты) БЕЗ отправки и без создания заявки. Нужно, чтобы // сначала положить запрос в БД Fancy на проверку, а отправить позже. func (s *Service) BuildRequest(ctx context.Context, in CreateClaimRequest) (*m2m.M2MTransferRequest, error) { + in = withNSDSecCodes(in) domainClaim, err := dtoToClaim(in) if err != nil { return nil, fmt.Errorf("lkgateway: dtoToClaim: %w", err) @@ -113,6 +114,7 @@ func (s *Service) receiverFor(in CreateClaimRequest) m2m.DeponentCode { // BuildRequestUnchecked — BuildRequest без финальной проверки формата НРД: пакет нужен // для журнала и отказа через Fancy даже при некорректных данных заявки (lkemulate.go). func (s *Service) BuildRequestUnchecked(ctx context.Context, in CreateClaimRequest) (*m2m.M2MTransferRequest, error) { + in = withNSDSecCodes(in) domainClaim, err := dtoToClaim(in) if err != nil { return nil, fmt.Errorf("lkgateway: dtoToClaim: %w", err) @@ -693,3 +695,62 @@ func nsdResponseFromDecision(d *m2m.M2MTransferDecision) *NSDResponseView { } return v } + +// nsdSecCodes — код бумаги в НРД (SecurityCode) по ISIN из DEPO (кодировщик «НДЦ»), с кэшем на час. +// У акций код НРД отличается от ISIN (Сбербанк — SBER/03), и с ISIN вместо кода НРД сервис отвечает +// M2M43 «Ценная бумага не учитывается в НРД» (29.09.2026). +var nsdSecCodes = struct { + sync.Mutex + m map[string]nsdSecCodeEntry +}{m: map[string]nsdSecCodeEntry{}} + +type nsdSecCodeEntry struct { + code string + at time.Time +} + +func nsdSecCodeByISIN(isin string) string { + isin = strings.ToUpper(strings.TrimSpace(isin)) + if isin == "" || globalRC == nil { + return "" + } + nsdSecCodes.Lock() + if e, ok := nsdSecCodes.m[isin]; ok && time.Since(e.at) < time.Hour { + nsdSecCodes.Unlock() + return e.code + } + nsdSecCodes.Unlock() + fs := globalRC.Snapshot().Fancy + if strings.TrimSpace(fs.Server) == "" { + return "" + } + db, err := fancyOpen(fs, fs.DepoDB()) + if err != nil { + return "" + } + defer db.Close() + code := depoNSDCode(db, isin) + nsdSecCodes.Lock() + nsdSecCodes.m[isin] = nsdSecCodeEntry{code: code, at: time.Now()} + nsdSecCodes.Unlock() + return code +} + +// withNSDSecCodes — бумаги, у которых код НРД не задан или равен ISIN, получают код НРД из DEPO. +func withNSDSecCodes(in CreateClaimRequest) CreateClaimRequest { + if len(in.Securities) == 0 { + return in + } + secs := append([]ClaimSec(nil), in.Securities...) + for i := range secs { + isin := strings.ToUpper(strings.TrimSpace(secs[i].SecurityDetails.ISIN)) + if isin == "" || (secs[i].SecurityCode != "" && !strings.EqualFold(secs[i].SecurityCode, isin)) { + continue + } + if code := nsdSecCodeByISIN(isin); code != "" { + secs[i].SecurityCode = code + } + } + in.Securities = secs + return in +}