From 44d6d7a74dfa7066662dd9c818d2529383d77589 Mon Sep 17 00:00:00 2001 From: zuevav Date: Tue, 29 Sep 2026 15:37:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=82=D0=BA=D0=B0=D0=B7=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B8=D1=81=D0=B0=20M2M=20=D0=B2=20=D0=9B=D0=9A=20?= =?UTF-8?q?=E2=80=94=20=D1=81=D0=BB=D0=BE=D0=B2=D0=B0=D0=BC=D0=B8=20(?= =?UTF-8?q?=D0=BD=D0=B0=D1=88=D0=B0=20=D1=80=D0=B0=D1=81=D1=88=D0=B8=D1=84?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B8=D0=BB=D0=B8=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=20=D0=9D=D0=A0=D0=94),=20M2M43;=20?= =?UTF-8?q?=C2=AB=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=BE=20=D1=80=D0=B0=D1=81=D1=85=D0=BE=D0=B4=D0=B0?= =?UTF-8?q?=D1=85=C2=BB=20=D0=B2=20QUIK=20=E2=80=94=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B5=20=D0=BA=D0=BB=D0=B8=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0?= 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/lkclaim_live_test.go | 32 +++++++++++++++++++++++++ internal/lkgateway/m2mcodes.go | 1 + internal/lkgateway/nsdpoller.go | 30 +++++++++++++++++++---- internal/lkgateway/quikhooks.go | 5 ++-- internal/lkgateway/serviceerr_test.go | 23 ++++++++++++++++++ 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 internal/lkgateway/lkclaim_live_test.go create mode 100644 internal/lkgateway/serviceerr_test.go diff --git a/internal/lkgateway/lkclaim_live_test.go b/internal/lkgateway/lkclaim_live_test.go new file mode 100644 index 0000000..6afcb7c --- /dev/null +++ b/internal/lkgateway/lkclaim_live_test.go @@ -0,0 +1,32 @@ +//go:build live + +package lkgateway + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "testing" +) + +// Только чтение: заявка из ЛК ЕСИА Финанс как её получает bj. LK_ID=70038 +func TestLKClaimLive(t *testing.T) { + rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json") + if err != nil { + t.Fatal(err) + } + ep, ok := lkActiveEndpoint(rc) + if !ok { + t.Fatal("ЛК не настроен") + } + code, body, err := lkGet(context.Background(), ep, "/claims/"+os.Getenv("LK_ID")) + fmt.Println("HTTP", code, err) + var out bytes.Buffer + if json.Indent(&out, body, "", " ") == nil { + fmt.Println(out.String()) + } else { + fmt.Println(string(body)) + } +} diff --git a/internal/lkgateway/m2mcodes.go b/internal/lkgateway/m2mcodes.go index fc8df41..f0a9662 100644 --- a/internal/lkgateway/m2mcodes.go +++ b/internal/lkgateway/m2mcodes.go @@ -117,6 +117,7 @@ var m2mCodeTexts = map[string]string{ "M2M18": "Неверно указан ИНН зачисляющего депозитария", // предположительно (по соседним кодам); уточнить по Спецификации ЭД "M2M19": "Недопустимое место расчётов", "M2M30": "Неверно указан код НРД ценной бумаги", + "M2M43": "Ценная бумага не учитывается в НРД", // текст НРД из ответа сервиса (29.09.2026) // Внутренние причины отказа сверки списания (не коды НРД): недостаточно бумаг // на торговом счёте и неверное место хранения в запросе. "QTY": "Недостаточно бумаг на торговом счёте депо", diff --git a/internal/lkgateway/nsdpoller.go b/internal/lkgateway/nsdpoller.go index 496e840..4c11bc1 100644 --- a/internal/lkgateway/nsdpoller.go +++ b/internal/lkgateway/nsdpoller.go @@ -184,11 +184,7 @@ func (s *Server) applyIncoming(ctx context.Context, p igw.Package) error { // ERROR сервиса МОСТ — отказ: замыкаем на ЛК с человекочитаемой причиной. if strings.EqualFold(string(resp.StatusCode), "ERROR") { reason := "Отклонено сервисом MOEX МОСТ" - if c := strings.TrimSpace(codes); c != "" { - parts := make([]string, 0) - for _, code := range strings.Fields(c) { - parts = append(parts, m2mCodeLabel(code)) - } + if parts := serviceErrorTexts(resp); len(parts) > 0 { reason = strings.Join(parts, "; ") } // Итог для заявки модуля ЛК (lk.transfer_requests): 'declined' — отказ после отправки в НРД. @@ -358,3 +354,27 @@ func saveHandbook(hb *m2m.M2MTransferHandbook) (changed bool) { } return changed } + +// serviceErrorTexts — причины отказа сервиса M2M понятными словами: наша расшифровка кода, иначе текст +// из ответа НРД (он есть в каждом Response), иначе сам код. Раньше неизвестный bj код уходил клиенту +// в ЛК голым («M2M43»). +func serviceErrorTexts(resp *m2m.M2MTransferResponse) []string { + var parts []string + seen := map[string]bool{} + for _, rr := range resp.Responses { + code := strings.TrimSpace(rr.Code) + if code == "" || seen[code] { + continue + } + seen[code] = true + switch { + case m2mCodeText(code) != "": + parts = append(parts, m2mCodeLabel(code)) + case strings.TrimSpace(rr.Text) != "": + parts = append(parts, strings.TrimSpace(rr.Text)+" ("+code+")") + default: + parts = append(parts, code) + } + } + return parts +} diff --git a/internal/lkgateway/quikhooks.go b/internal/lkgateway/quikhooks.go index 12cdaee..b15f7b5 100644 --- a/internal/lkgateway/quikhooks.go +++ b/internal/lkgateway/quikhooks.go @@ -72,13 +72,14 @@ func quikOutgoingBeforeSend(ctx context.Context, rc *RuntimeConfig, req *m2m.M2M return nil } inn := string(req.Data.ReceivingDepository.INN) + passExpenses := req.Header.CostInfo.Yes != nil // «передавать информацию о расходах» — как в заявке клиента var done []string for _, sec := range req.Data.TransferredSecurities.Securities { isin, qty := quikSecISIN(sec), quikSecQty(sec.Quantity) if isin == "" || qty <= 0 { continue } - if _, err := QuikOutRegister(ctx, rc, guid, code, contract, isin, qty, inn, "", true); err != nil { + if _, err := QuikOutRegister(ctx, rc, guid, code, contract, isin, qty, inn, "", passExpenses); err != nil { for _, d := range done { _, _ = QuikOutDeny(ctx, rc, guid, d, "Отмена: не удалось заблокировать другую бумагу перевода") } @@ -354,7 +355,7 @@ func registerQuikOps(mux *http.ServeMux, rc *RuntimeConfig, svc *Service) { flash("QUIK МНП: бумага " + isin + " не найдена в заявке") return } - if _, err := QuikOutRegister(ctx, rc, guid, code, contract, isin, qty, string(deal.Request.Data.ReceivingDepository.INN), "", true); err != nil { + if _, err := QuikOutRegister(ctx, rc, guid, code, contract, isin, qty, string(deal.Request.Data.ReceivingDepository.INN), "", deal.Request.Header.CostInfo.Yes != nil); err != nil { flash("QUIK МНП: " + err.Error()) return } diff --git a/internal/lkgateway/serviceerr_test.go b/internal/lkgateway/serviceerr_test.go new file mode 100644 index 0000000..df59491 --- /dev/null +++ b/internal/lkgateway/serviceerr_test.go @@ -0,0 +1,23 @@ +package lkgateway + +import ( + "strings" + "testing" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" +) + +// Клиенту — причина словами: наша расшифровка или текст НРД, а не голый код. +func TestServiceErrorTexts(t *testing.T) { + resp := &m2m.M2MTransferResponse{Responses: []m2m.Response{ + {Code: "M2M43", Text: "Ценная бумага не учитывается в НРД"}, + {Code: "M2M99", Text: "Новая причина НРД"}, + {Code: "M2M98"}, + }} + got := strings.Join(serviceErrorTexts(resp), "; ") + for _, want := range []string{"Ценная бумага не учитывается в НРД (M2M43)", "Новая причина НРД (M2M99)", "M2M98"} { + if !strings.Contains(got, want) { + t.Fatalf("нет %q в %q", want, got) + } + } +}