Отказ сервиса M2M в ЛК — словами (наша расшифровка или текст НРД), M2M43; «передавать информацию о расходах» в QUIK — по заявке клиента

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-09-29 15:37:34 +03:00
parent 7cd3583844
commit 44d6d7a74d
5 changed files with 84 additions and 7 deletions
+32
View File
@@ -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))
}
}
+1
View File
@@ -117,6 +117,7 @@ var m2mCodeTexts = map[string]string{
"M2M18": "Неверно указан ИНН зачисляющего депозитария", // предположительно (по соседним кодам); уточнить по Спецификации ЭД
"M2M19": "Недопустимое место расчётов",
"M2M30": "Неверно указан код НРД ценной бумаги",
"M2M43": "Ценная бумага не учитывается в НРД", // текст НРД из ответа сервиса (29.09.2026)
// Внутренние причины отказа сверки списания (не коды НРД): недостаточно бумаг
// на торговом счёте и неверное место хранения в запросе.
"QTY": "Недостаточно бумаг на торговом счёте депо",
+25 -5
View File
@@ -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
}
+3 -2
View File
@@ -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
}
+23
View File
@@ -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)
}
}
}