Отказ сервиса 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:
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,7 @@ var m2mCodeTexts = map[string]string{
|
||||
"M2M18": "Неверно указан ИНН зачисляющего депозитария", // предположительно (по соседним кодам); уточнить по Спецификации ЭД
|
||||
"M2M19": "Недопустимое место расчётов",
|
||||
"M2M30": "Неверно указан код НРД ценной бумаги",
|
||||
"M2M43": "Ценная бумага не учитывается в НРД", // текст НРД из ответа сервиса (29.09.2026)
|
||||
// Внутренние причины отказа сверки списания (не коды НРД): недостаточно бумаг
|
||||
// на торговом счёте и неверное место хранения в запросе.
|
||||
"QTY": "Недостаточно бумаг на торговом счёте депо",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user