Код бумаги в запросе НРД — код НРД из DEPO по ISIN (у акций ≠ ISIN: SBER/03), иначе M2M43
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,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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("исходная заявка не меняется")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -82,6 +82,7 @@ func (s *Service) CreateClaim(ctx context.Context, in CreateClaimRequest) (Creat
|
|||||||
// ReferenceID, реквизиты) БЕЗ отправки и без создания заявки. Нужно, чтобы
|
// ReferenceID, реквизиты) БЕЗ отправки и без создания заявки. Нужно, чтобы
|
||||||
// сначала положить запрос в БД Fancy на проверку, а отправить позже.
|
// сначала положить запрос в БД Fancy на проверку, а отправить позже.
|
||||||
func (s *Service) BuildRequest(ctx context.Context, in CreateClaimRequest) (*m2m.M2MTransferRequest, error) {
|
func (s *Service) BuildRequest(ctx context.Context, in CreateClaimRequest) (*m2m.M2MTransferRequest, error) {
|
||||||
|
in = withNSDSecCodes(in)
|
||||||
domainClaim, err := dtoToClaim(in)
|
domainClaim, err := dtoToClaim(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("lkgateway: dtoToClaim: %w", err)
|
return nil, fmt.Errorf("lkgateway: dtoToClaim: %w", err)
|
||||||
@@ -113,6 +114,7 @@ func (s *Service) receiverFor(in CreateClaimRequest) m2m.DeponentCode {
|
|||||||
// BuildRequestUnchecked — BuildRequest без финальной проверки формата НРД: пакет нужен
|
// BuildRequestUnchecked — BuildRequest без финальной проверки формата НРД: пакет нужен
|
||||||
// для журнала и отказа через Fancy даже при некорректных данных заявки (lkemulate.go).
|
// для журнала и отказа через Fancy даже при некорректных данных заявки (lkemulate.go).
|
||||||
func (s *Service) BuildRequestUnchecked(ctx context.Context, in CreateClaimRequest) (*m2m.M2MTransferRequest, error) {
|
func (s *Service) BuildRequestUnchecked(ctx context.Context, in CreateClaimRequest) (*m2m.M2MTransferRequest, error) {
|
||||||
|
in = withNSDSecCodes(in)
|
||||||
domainClaim, err := dtoToClaim(in)
|
domainClaim, err := dtoToClaim(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("lkgateway: dtoToClaim: %w", err)
|
return nil, fmt.Errorf("lkgateway: dtoToClaim: %w", err)
|
||||||
@@ -693,3 +695,62 @@ func nsdResponseFromDecision(d *m2m.M2MTransferDecision) *NSDResponseView {
|
|||||||
}
|
}
|
||||||
return v
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user