From f35c72c50abd451da60c2577201f33a456109fd3 Mon Sep 17 00:00:00 2001 From: zuevav Date: Wed, 16 Sep 2026 15:56:03 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20M2M:=20=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D1=82=D0=B2=D0=BE=20=D0=B1=D1=83=D0=BC?= =?UTF-8?q?=D0=B0=D0=B3=20=D0=B2=D1=8B=D0=B1=D0=B8=D1=80=D0=B0=D0=B5=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=B2=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80=D0=B5=20=E2=80=94=20=D1=81?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20DEPO=20=D0=B8=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20=D0=B1=D0=B5=D1=80=D1=83?= =?UTF-8?q?=D1=82=20=D0=B5=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- internal/lkgateway/m2mqty_test.go | 26 +++++++++ internal/lkgateway/m2mtest.go | 53 ++++++++++++++++--- .../web/templates/admin_m2mpreview.html | 10 +++- 3 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 internal/lkgateway/m2mqty_test.go diff --git a/internal/lkgateway/m2mqty_test.go b/internal/lkgateway/m2mqty_test.go new file mode 100644 index 0000000..084c20b --- /dev/null +++ b/internal/lkgateway/m2mqty_test.go @@ -0,0 +1,26 @@ +package lkgateway + +import ( + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +func TestM2MQtyOverrides(t *testing.T) { + c := m2mCase{ID: "2", Secs: []m2mSec{sSBER("10"), {ISIN: "RU000A0JUR61", Code: "PAI", Qty: "5", Frac: true}}} + form := url.Values{"qty_RU0009029540": {"1"}, "qty_RU000A0JUR61": {"2,5"}} + r := httptest.NewRequest("POST", "/admin/m2m-testing/initiate", strings.NewReader(form.Encode())) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + got := m2mQtyOverrides(r, c) + if got["RU0009029540"] != "1" || got["RU000A0JUR61"] != "2.5" { + t.Fatalf("количество: %v", got) + } + if note := m2mQtyNote(c, got); !strings.Contains(note, "SBER/03 × 1 (по плану 10)") { + t.Fatalf("отметка: %q", note) + } + bad := httptest.NewRequest("GET", "/?qty_RU0009029540=1.5&qty_RU000A0JUR61=-1", nil) + if got := m2mQtyOverrides(bad, c); len(got) != 0 { + t.Fatalf("дробное для акций и отрицательное — игнорировать: %v", got) + } +} diff --git a/internal/lkgateway/m2mtest.go b/internal/lkgateway/m2mtest.go index 856b670..9c57c0b 100644 --- a/internal/lkgateway/m2mtest.go +++ b/internal/lkgateway/m2mtest.go @@ -18,6 +18,7 @@ import ( "net/http" "net/url" "os" + "strconv" "strings" "sync" "time" @@ -373,10 +374,11 @@ func registerM2MTest(mux *http.ServeMux, a *admin, h *setupHandlers) { ISIN, Code, Name, Qty string Bad bool } + qtys := m2mQtyOverrides(r, *c) var secs []pvSec for _, s := range c.Secs { secs = append(secs, pvSec{ - ISIN: s.ISIN, Code: s.Code, Name: m2mSecName[s.ISIN], Qty: s.Qty, + ISIN: s.ISIN, Code: s.Code, Name: m2mSecName[s.ISIN], Qty: pickStr(qtys[s.ISIN], s.Qty), Bad: m2mSecBad(*c, s), }) } @@ -420,7 +422,7 @@ func registerM2MTest(mux *http.ServeMux, a *admin, h *setupHandlers) { } var ss []depoOutSec for _, s := range c.Secs { - ss = append(ss, depoOutSec{ReferenceID: s.Code, ISIN: s.ISIN, Code: s.Code, Qty: s.Qty}) + ss = append(ss, depoOutSec{ReferenceID: s.Code, ISIN: s.ISIN, Code: s.Code, Qty: pickStr(qtys[s.ISIN], s.Qty)}) } return runDepoCheckOut(cl.Last, cl.First, cl.Middle, cl.DocSeries, cl.DocNumber, ss, nsd.AccountID, nsd.SectionID) }(), @@ -562,7 +564,8 @@ func registerM2MTest(mux *http.ServeMux, a *admin, h *setupHandlers) { back("Сначала укажите код участника контрагента (кнопка «Контрагент»)") return } - guid, err := h.m2mInitiate(*cs, plan) + qtys := m2mQtyOverrides(r, *cs) + guid, err := h.m2mInitiate(*cs, plan, qtys) m2mMu.Lock() plan = m2mLoadPlan() st := plan.States[id] @@ -574,7 +577,7 @@ func registerM2MTest(mux *http.ServeMux, a *admin, h *setupHandlers) { } else { st.Status = "in_progress" st.GUID = guid - st.Note = "Отправлено контрагенту " + plan.CounterpartyCode + " (GUID " + guid + "). Ждём ответ." + st.Note = "Отправлено контрагенту " + plan.CounterpartyCode + " (GUID " + guid + ")" + m2mQtyNote(*cs, qtys) + ". Ждём ответ." msg = "Кейс " + id + ": исходящий M2M отправлен (GUID " + guid + "). Ответ придёт во «Входящие»." } st.Updated = time.Now() @@ -585,8 +588,41 @@ func registerM2MTest(mux *http.ServeMux, a *admin, h *setupHandlers) { }) } -// m2mInitiate строит исходящий M2M по кейсу и отправляет через НРД контрагенту. -func (h *setupHandlers) m2mInitiate(c m2mCase, plan m2mPlan) (string, error) { +// m2mQtyOverrides — количество по бумагам, выбранное в предпросмотре (qty_); неверное значение +// игнорируется (остаётся количество из плана). +func m2mQtyOverrides(r *http.Request, c m2mCase) map[string]string { + out := map[string]string{} + for _, s := range c.Secs { + v := strings.ReplaceAll(strings.TrimSpace(r.FormValue("qty_"+s.ISIN)), ",", ".") + if v == "" { + continue + } + f, err := strconv.ParseFloat(v, 64) + if err != nil || f <= 0 || (!s.Frac && f != float64(int64(f))) { + continue + } + out[s.ISIN] = v + } + return out +} + +// m2mQtyNote — «, количество: SBER/03 × 1» для отметки кейса, если количество меняли. +func m2mQtyNote(c m2mCase, qtys map[string]string) string { + var parts []string + for _, s := range c.Secs { + if q, ok := qtys[s.ISIN]; ok && q != s.Qty { + parts = append(parts, s.Code+" × "+q+" (по плану "+s.Qty+")") + } + } + if len(parts) == 0 { + return "" + } + return ", количество: " + strings.Join(parts, "; ") +} + +// m2mInitiate строит исходящий M2M по кейсу и отправляет через НРД контрагенту. qtys — количество, +// выбранное в предпросмотре (ISIN → количество), иначе — из плана. +func (h *setupHandlers) m2mInitiate(c m2mCase, plan m2mPlan, qtys map[string]string) (string, error) { if len(c.Secs) == 0 { return "", fmt.Errorf("в кейсе %s нет бумаг для отправки (это кейс сценария 2 — формируется вручную)", c.ID) } @@ -596,9 +632,10 @@ func (h *setupHandlers) m2mInitiate(c m2mCase, plan m2mPlan) (string, error) { secs := make([]ClaimSec, 0, len(c.Secs)) for _, s := range c.Secs { - qty := Quantity{Whole: uint64(atoiSafe(s.Qty))} + q := pickStr(qtys[s.ISIN], s.Qty) + qty := Quantity{Whole: uint64(atoiSafe(q))} if s.Frac { - qty = Quantity{Fractional: s.Qty} + qty = Quantity{Fractional: q} } secs = append(secs, ClaimSec{ SecurityCode: s.Code, diff --git a/internal/lkgateway/web/templates/admin_m2mpreview.html b/internal/lkgateway/web/templates/admin_m2mpreview.html index 29527c1..e307ce6 100644 --- a/internal/lkgateway/web/templates/admin_m2mpreview.html +++ b/internal/lkgateway/web/templates/admin_m2mpreview.html @@ -74,11 +74,18 @@ {{if .Bad}}🔴{{else}}🟢{{end}} {{.ISIN}}{{if ne .Code .ISIN}} · {{.Code}}{{end}} {{.Name}}{{if .Bad}} — неверная для этого кейса{{end}} - {{.Qty}} + {{if $.WeInitiate}}{{else}}{{.Qty}}{{end}} {{end}} + {{if .WeInitiate}} +
+ + + Количество можно изменить (по плану — как в таблице кейсов); сверка DEPO и отправка берут то, что указано здесь после пересчёта. +
+ {{end}} {{end}} {{if .WeInitiate}} @@ -117,6 +124,7 @@ {{if .WeInitiate}}
+ {{range .Secs}}{{end}}
Отмена