diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index bad1050..5946d93 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -27,7 +27,7 @@ type admin struct { incoming, incomingDetail, fancyQueue, quikInstr *template.Template wizard, news, keyWizard *template.Template handbook *template.Template - m2mtest, m2mpreview, depoSim *template.Template + m2mtest, m2mpreview, depoSim, depoTest *template.Template journal *template.Template } @@ -325,6 +325,10 @@ func newAdmin() (*admin, error) { if err != nil { return nil, fmt.Errorf("parse admin_depo_sim: %w", err) } + depoTest, err := parse("admin_depo_test.html") + if err != nil { + return nil, fmt.Errorf("parse admin_depo_test: %w", err) + } journal, err := parse("admin_journal.html") if err != nil { return nil, fmt.Errorf("parse admin_journal: %w", err) @@ -339,6 +343,7 @@ func newAdmin() (*admin, error) { m2mtest: m2mtest, m2mpreview: m2mpreview, depoSim: depoSim, + depoTest: depoTest, journal: journal, }, nil } diff --git a/internal/lkgateway/auth.go b/internal/lkgateway/auth.go index 82d1923..61c71e3 100644 --- a/internal/lkgateway/auth.go +++ b/internal/lkgateway/auth.go @@ -889,7 +889,7 @@ func requiredPerm(p, method string) string { return "dev" } switch { - case strings.HasPrefix(p, "/admin/setup") || p == "/admin/wizard": + case strings.HasPrefix(p, "/admin/setup") || p == "/admin/wizard" || strings.HasPrefix(p, "/admin/depo-test"): return "settings" case strings.HasPrefix(p, "/admin/npr"): return "npr" diff --git a/internal/lkgateway/depoevents.go b/internal/lkgateway/depoevents.go index f7885bd..83f972b 100644 --- a/internal/lkgateway/depoevents.go +++ b/internal/lkgateway/depoevents.go @@ -512,7 +512,9 @@ func depoPollOrders(ctx context.Context, db *sql.DB, rc *RuntimeConfig) { if l != nil && l.DocState != "" && docState != l.DocState { log.Printf("depo-events: документ DEPO %s по %s: состояние «%s»", doc, shortGUID(guid), docState) if strings.Contains(strings.ToLower(docState), "отмен") { - raiseSystemAlert(rc, "DEPO: депозитарий отменил документ по переводу "+shortGUID(guid), "Документ №"+doc+": «"+docState+"». Проверьте перевод и позицию клиента в QUIK.") + if !isDepoTestGUID(guid) { + raiseSystemAlert(rc, "DEPO: депозитарий отменил документ по переводу "+shortGUID(guid), "Документ №"+doc+": «"+docState+"». Проверьте перевод и позицию клиента в QUIK.") + } } } } @@ -527,7 +529,9 @@ func depoPollOrders(ctx context.Context, db *sql.DB, rc *RuntimeConfig) { x.Errors = append(x.Errors, time.Now().Format("02.01 15:04")+" "+msg) x.LastEvent = "FTV_M2M.STATE=ERROR" }) - raiseSystemAlert(rc, "DEPO: джоба не обработала перевод "+shortGUID(guid), msg) + if !isDepoTestGUID(guid) { + raiseSystemAlert(rc, "DEPO: джоба не обработала перевод "+shortGUID(guid), msg) + } } } } diff --git a/internal/lkgateway/depohealth.go b/internal/lkgateway/depohealth.go index 91111ed..5280cfb 100644 --- a/internal/lkgateway/depohealth.go +++ b/internal/lkgateway/depohealth.go @@ -133,6 +133,9 @@ func depoAlertEventFailures(ctx context.Context, db *sql.DB, rc *RuntimeConfig) return } depoFailAlertedID = id.Int64 + if isDepoTestGUID(guid) { + return // тестовый прогон DEPO: результат виден на странице теста, сотрудников не оповещаем + } log.Printf("depo-events: обработчик пометил ERROR событий: %d, последнее %s по %s: %s", n, typ, shortGUID(guid), comment) raiseSystemAlert(rc, fmt.Sprintf("DEPO: обработчик не смог обработать события (%d)", n), fmt.Sprintf("Последнее: %s по переводу %s — %s. Устраните причину и верните записи в очередь: Обмен с DEPO → «Вернуть в очередь».", typ, shortGUID(guid), comment)) diff --git a/internal/lkgateway/depotest.go b/internal/lkgateway/depotest.go new file mode 100644 index 0000000..5b60e3f --- /dev/null +++ b/internal/lkgateway/depotest.go @@ -0,0 +1,418 @@ +package lkgateway + +// depotest.go — режим тестирования DEPO: оператор собирает перевод (клиент и бумаги из DEPO, +// контрагент, исход по каждой бумаге), bj пишет его в трансферные таблицы FTV_M2M* и +// проигрывает события по шагам так, как они шли бы в бою, имитируя ответы НРД (решение +// контрагента, приём и исполнение поручения 16/16.1, отказ НРД, истечение срока). После +// каждого шага ждёт, пока джоба bj2depo обработает запись, и показывает STATE/COMMENT. +// В НРД, контрагенту, QUIK и ЛК ничего не отправляется — только база DEPO. + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" +) + +const depoTestFile = "/var/lib/bj/.bj/depo-tests.json" + +// Исходы строки. +const ( + dtExec = "exec" // принято и исполнено + dtDecline = "decline" // отказ по M2M: наш (зачисление) или контрагента (списание) + dtNSD = "nsd" // поручение отклонено НРД (не сквитовано) + dtExpire = "expire" // поручение не исполнено в срок (30 дней) +) + +var depoTestOutcomes = []struct{ Code, Title string }{ + {dtExec, "исполнено (MS010: исполнено)"}, + {dtDecline, "отказ по M2M с кодом"}, + {dtNSD, "поручение отклонено НРД (MS010: не сквитовано)"}, + {dtExpire, "не исполнено за 30 дней (отмена)"}, +} + +type DepoTestLine struct { + Ref, ISIN, Qty, Outcome, Code string + State, DOL, Comment string // результат джобы по строке FTV_M2M_LINES +} + +type DepoTestStep struct { + At time.Time + Title, Event, Ref string + State, Comment string // STATE/COMMENT, которые поставила джоба + Emulated string // что «ответил» имитатор НРД (для наглядности) +} + +type DepoTestRun struct { + ID, GUID, Direction, CP, CPName string + Client, Ser, Num string + Lines []DepoTestLine + Steps []DepoTestStep + Created time.Time + Status string // running | done | error + Doc, HeaderState, HeaderComment string + Errors int +} + +type depoTestStore struct { + mu sync.Mutex + runs []*DepoTestRun +} + +var globalDepoTests = loadDepoTests() + +func loadDepoTests() *depoTestStore { + s := &depoTestStore{} + if b, err := os.ReadFile(depoTestFile); err == nil { + _ = json.Unmarshal(b, &s.runs) + } + for _, r := range s.runs { + if r.Status == "running" { + r.Status = "error" + r.Steps = append(r.Steps, DepoTestStep{At: time.Now(), Title: "прогон прерван перезапуском bj"}) + } + } + return s +} + +func (s *depoTestStore) saveLocked() { + if len(s.runs) > 200 { + s.runs = s.runs[len(s.runs)-200:] + } + b, _ := json.MarshalIndent(s.runs, "", " ") + tmp := depoTestFile + ".tmp" + if os.WriteFile(tmp, b, 0o600) == nil { + _ = os.Rename(tmp, depoTestFile) + } +} + +func (s *depoTestStore) update(id string, fn func(r *DepoTestRun)) { + s.mu.Lock() + defer s.mu.Unlock() + for _, r := range s.runs { + if r.ID == id { + fn(r) + } + } + s.saveLocked() +} + +func (s *depoTestStore) list() []DepoTestRun { + s.mu.Lock() + defer s.mu.Unlock() + out := make([]DepoTestRun, 0, len(s.runs)) + for i := len(s.runs) - 1; i >= 0; i-- { + r := *s.runs[i] + r.Steps = append([]DepoTestStep(nil), r.Steps...) + r.Lines = append([]DepoTestLine(nil), r.Lines...) + out = append(out, r) + } + return out +} + +// isDepoTestGUID — перевод создан тестовым режимом: по нему не шлём оповещений сотрудникам. +func isDepoTestGUID(guid string) bool { + g := strings.ToUpper(strings.TrimSpace(guid)) + globalDepoTests.mu.Lock() + defer globalDepoTests.mu.Unlock() + for _, r := range globalDepoTests.runs { + if r.GUID == g { + return true + } + } + return false +} + +func newDepoGUID() string { + n := time.Now() + return strings.ToUpper(fmt.Sprintf("%08x-%04x-4000-8000-%012x", n.Unix()&0xFFFFFFFF, n.UnixNano()&0xFFFF, n.UnixNano()&0xFFFFFFFFFFFF)) +} + +// depoTestExec — проигрывание сценария. pace — пауза между шагами (имитация задержек НРД). +func depoTestExec(rc *RuntimeConfig, run *DepoTestRun, pace time.Duration, stopOnError bool) { + id := run.ID + fail := func(msg string) { + globalDepoTests.update(id, func(r *DepoTestRun) { + r.Status = "error" + r.Steps = append(r.Steps, DepoTestStep{At: time.Now(), Title: msg, State: "ERROR"}) + }) + } + db, err := depoEventsDB(rc) + if err != nil { + fail("нет подключения к DEPO: " + err.Error()) + return + } + defer db.Close() + + // 1. шапка и строки + place := rc.Snapshot().NSD.SectionID + var lines []depoLine + for _, l := range run.Lines { + lines = append(lines, depoLine{Ref: l.Ref, ISIN: l.ISIN, Code: l.ISIN, Amount: parseQty(l.Qty), Place: place}) + } + dir := "IN" + if run.Direction == "out" { + dir = "OUT" + } + if !depoUpsertOrder(rc, run.GUID, dir, run.CP, run.Client, strings.TrimSpace(run.Ser+" "+run.Num), run.Ser, run.Num, lines, "ТЕСТ DEPO "+run.ID) { + fail("не удалось записать шапку и строки в FTV_M2M (журнал bj-server)") + return + } + state, doc, comment := depoTestWaitHeader(db, run.GUID) + globalDepoTests.update(id, func(r *DepoTestRun) { + r.Doc, r.HeaderState, r.HeaderComment = doc, state, comment + r.Steps = append(r.Steps, DepoTestStep{At: time.Now(), Title: "Перевод записан в FTV_M2M / FTV_M2M_LINES → документ DEPO", Event: "FTV_M2M", State: state, Comment: pick(comment, "документ № "+doc)}) + if state == "ERROR" { + r.Errors++ + } + }) + if state == "ERROR" && stopOnError { + depoTestFinish(db, id, run.GUID) + return + } + + // 2. события по шагам + type ev struct{ title, typ, codes, params, emu string } + stepsFor := func(l DepoTestLine) []ev { + var s []ev + if run.Direction == "out" { + s = append(s, ev{"Отправили M2MTR в НРД", "BJ_SENT", "", "", ""}) + if l.Outcome == dtDecline { + return append(s, ev{"Контрагент отказал (M2MTD)", "BJ_CP_REJECT", l.Code, m2mCodeLabel(l.Code), "НРД: решение контрагента — отказ " + l.Code}) + } + s = append(s, ev{"Контрагент подтвердил (M2MTD)", "BJ_CP_CONFIRM", "", "", "НРД: решение контрагента — подтверждение"}) + } else { + s = append(s, ev{"Получили M2MTR от контрагента", "BJ_RECEIVED", "", "", "НРД: входящий M2MTR"}) + if l.Outcome == dtDecline { + return append(s, ev{"Наше решение: отказ (M2MTD)", "BJ_DECISION_REJECT", l.Code, m2mCodeLabel(l.Code), ""}) + } + s = append(s, ev{"Наше решение: подтверждение (M2MTD)", "BJ_DECISION_CONFIRM", "", "", ""}) + } + form := "16/1" + if run.Direction == "out" { + form = "16" + } + s = append(s, ev{"Подали поручение " + form + " в НРД", "BJ_ORDER_SENT", "", "EMU-" + l.Ref, "НРД: поручение принято к обработке"}) + switch l.Outcome { + case dtNSD: + s = append(s, ev{"НРД отклонил поручение (MS010)", "BJ_ORDER_REJECTED", "", "GS116 — поручение не сквитовано со встречным поручением контрагента (имитация НРД)", "НРД: MS010 — отказ, GS116"}) + case dtExpire: + s = append(s, ev{"Нет ответа НРД в срок (просрочка)", "BJ_ORDER_LATE", "", "", "НРД: MS010 не пришёл"}, + ev{"30 дней истекли — поручение отменено", "BJ_CANCELLED", "", "", "НРД: срок исполнения истёк"}) + default: + s = append(s, ev{"НРД принял поручение (MS010)", "BJ_ORDER_ACCEPTED", "", "", "НРД: MS010 — принято"}, + ev{"НРД исполнил поручение (MS010)", "BJ_EXECUTED", "", "", "НРД: MS010 — исполнено"}) + } + return s + } + // порядок как в бою: сначала общий этап по всем строкам, затем исходы по строкам + var queue [][]ev + maxLen := 0 + for _, l := range run.Lines { + st := stepsFor(l) + queue = append(queue, st) + if len(st) > maxLen { + maxLen = len(st) + } + } + for k := 0; k < maxLen; k++ { + for i, l := range run.Lines { + if k >= len(queue[i]) { + continue + } + e := queue[i][k] + if pace > 0 { + time.Sleep(pace) + } + depoEventAt(rc, time.Now(), run.GUID, l.Ref, e.typ, e.codes, e.params) + st, cm := depoTestWaitEvent(db, run.GUID, l.Ref, e.typ) + globalDepoTests.update(id, func(r *DepoTestRun) { + r.Steps = append(r.Steps, DepoTestStep{At: time.Now(), Title: e.title, Event: e.typ, Ref: l.Ref + " " + l.ISIN, State: st, Comment: cm, Emulated: e.emu}) + if st == "ERROR" || st == "" { + r.Errors++ + } + }) + if st == "ERROR" && stopOnError { + depoTestFinish(db, id, run.GUID) + return + } + } + } + depoTestFinish(db, id, run.GUID) +} + +func depoTestFinish(db *sql.DB, id, guid string) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + res := map[string]DepoTestLine{} + if rows, err := db.QueryContext(ctx, fsql.DT03, sql.Named("g", guid)); err == nil { + for rows.Next() { + var l DepoTestLine + if rows.Scan(&l.Ref, &l.ISIN, &l.State, &l.DOL, &l.Comment) == nil { + res[l.Ref] = l + } + } + rows.Close() + } + var st, doc, cm string + _ = db.QueryRowContext(ctx, fsql.DT01, sql.Named("g", guid)).Scan(&st, &doc, &cm) + globalDepoTests.update(id, func(r *DepoTestRun) { + for i := range r.Lines { + if x, ok := res[r.Lines[i].Ref]; ok { + r.Lines[i].State, r.Lines[i].DOL, r.Lines[i].Comment = x.State, x.DOL, x.Comment + } + } + r.HeaderState, r.Doc, r.HeaderComment = st, doc, cm + if r.Status == "running" { + r.Status = "done" + } + }) + log.Printf("depo-test: прогон %s завершён (GUID %s)", id, shortGUID(guid)) +} + +func depoTestWaitHeader(db *sql.DB, guid string) (state, doc, comment string) { + deadline := time.Now().Add(90 * time.Second) + for time.Now().Before(deadline) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + err := db.QueryRowContext(ctx, fsql.DT01, sql.Named("g", guid)).Scan(&state, &doc, &comment) + cancel() + if err == nil && state != "" && state != "TODO" && state != "WORK" { + return + } + time.Sleep(2 * time.Second) + } + return pick(state, "TODO"), doc, "джоба не обработала шапку за 90 с — проверьте, что обработка DEPO запущена" +} + +func depoTestWaitEvent(db *sql.DB, guid, ref, typ string) (state, comment string) { + deadline := time.Now().Add(90 * time.Second) + for time.Now().Before(deadline) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + err := db.QueryRowContext(ctx, fsql.DT02, sql.Named("g", guid), sql.Named("r", ref), sql.Named("t", typ)).Scan(&state, &comment) + cancel() + if err == nil && state != "" && state != "TODO" && state != "WORK" { + return + } + time.Sleep(2 * time.Second) + } + return pick(state, "TODO"), "джоба не обработала событие за 90 с" +} + +// registerDepoTest — /admin/depo-test: форма, список прогонов; POST /admin/depo-test/run — запуск. +func registerDepoTest(mux *http.ServeMux, a *admin) { + mux.HandleFunc("/admin/depo-test", func(w http.ResponseWriter, r *http.Request) { + clients, secs := depoSimLists() + var parts []HandbookEntry + for _, p := range handbookParticipants() { + if p.Code != "" { + parts = append(parts, p) + } + } + sort.Slice(parts, func(i, j int) bool { return parts[i].Name < parts[j].Name }) + var codes []struct{ Code, Title string } + for i := 1; i <= 9; i++ { + c := fmt.Sprintf("M2M%02d", i) + codes = append(codes, struct{ Code, Title string }{c, c + " — " + m2mCodeLabel(c)}) + } + runs := globalDepoTests.list() + running := false + for _, x := range runs { + if x.Status == "running" { + running = true + } + } + fs := globalRC.Snapshot().Fancy + data := struct { + page + Clients []depoSimClient + Secs []depoSimSec + Parts []HandbookEntry + Codes []struct{ Code, Title string } + Outcomes []struct{ Code, Title string } + Runs []DepoTestRun + Running bool + DB string + JobMode string + Flash string + }{nowPage("Тестирование DEPO", "depotest"), clients, secs, parts, codes, + append([]struct{ Code, Title string }(nil), depoTestOutcomes...), runs, running, + fs.Server + " / " + fs.DepoDB(), fs.DepoJobMode, r.URL.Query().Get("flash")} + render(w, a.depoTest, data) + }) + mux.HandleFunc("/admin/depo-test/run", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + back := func(msg string) { + http.Redirect(w, r, "/admin/depo-test?flash="+urlQ(msg), http.StatusSeeOther) + } + dir := r.FormValue("direction") + if dir != "out" { + dir = "in" + } + cp := strings.TrimSpace(r.FormValue("cp")) + client := strings.TrimSpace(r.FormValue("client")) // «ФИО|серия|номер» + p := strings.Split(client, "|") + if len(p) != 3 || p[0] == "" || p[2] == "" { + back("Выберите клиента из DEPO") + return + } + if cp == "" { + back("Выберите контрагента") + return + } + run := &DepoTestRun{ID: time.Now().Format("0102-150405"), GUID: newDepoGUID(), Direction: dir, CP: cp, CPName: handbookNameByCode(cp), + Client: p[0], Ser: p[1], Num: p[2], Created: time.Now(), Status: "running"} + base := time.Now().UnixNano() / 1e6 % 1e10 + for i := 1; i <= 5; i++ { + isin := strings.ToUpper(strings.TrimSpace(r.FormValue("isin" + strconv.Itoa(i)))) + if isin == "" { + continue + } + l := DepoTestLine{Ref: fmt.Sprintf("M2MT%010d%02d", base, i), ISIN: isin, Qty: pick(strings.TrimSpace(r.FormValue("qty"+strconv.Itoa(i))), "1"), + Outcome: r.FormValue("out" + strconv.Itoa(i)), Code: r.FormValue("code" + strconv.Itoa(i))} + switch l.Outcome { + case dtExec, dtNSD, dtExpire: + l.Code = "" + case dtDecline: + if l.Code == "" { + l.Code = "M2M05" + } + default: + l.Outcome = dtExec + } + run.Lines = append(run.Lines, l) + } + if len(run.Lines) == 0 { + back("Добавьте хотя бы одну бумагу") + return + } + pace, _ := strconv.Atoi(r.FormValue("pace")) + stop := r.FormValue("stop") == "1" + globalDepoTests.mu.Lock() + globalDepoTests.runs = append(globalDepoTests.runs, run) + globalDepoTests.saveLocked() + globalDepoTests.mu.Unlock() + cpy := *run + cpy.Lines = append([]DepoTestLine(nil), run.Lines...) + go depoTestExec(globalRC, &cpy, time.Duration(pace)*time.Second, stop) + log.Printf("depo-test: запущен прогон %s (%s, %s, %d бумаг) пользователем %s", run.ID, dir, cp, len(run.Lines), func() string { + if u := currentUser(w); u != nil { + return u.Login + } + return "-" + }()) + http.Redirect(w, r, "/admin/depo-test#run-"+run.ID, http.StatusSeeOther) + }) +} diff --git a/internal/lkgateway/fsql/depotest.go b/internal/lkgateway/fsql/depotest.go new file mode 100644 index 0000000..489a4d4 --- /dev/null +++ b/internal/lkgateway/fsql/depotest.go @@ -0,0 +1,11 @@ +package fsql + +// Тестовый режим DEPO: чтение результата обработки трансферных таблиц джобой bj2depo. + +var DT01 = `SELECT TOP 1 ISNULL(STATE,''), ISNULL(CAST(DOC AS varchar(32)),''), ISNULL(COMMENT,'') FROM dbo.FTV_M2M WHERE GUID=@g` + +var DT02 = `SELECT TOP 1 ISNULL(STATE,''), ISNULL(COMMENT,'') FROM dbo.FTV_M2M_EVENTS + WHERE GUID=@g AND ISNULL(REFERENCE,'')=@r AND EVENT_TYPE=@t ORDER BY ID DESC` + +var DT03 = `SELECT ISNULL(REFERENCE,''), ISNULL(ISIN,''), ISNULL(STATE,''), ISNULL(CAST(DOL AS varchar(32)),''), ISNULL(COMMENT,'') + FROM dbo.FTV_M2M_LINES WHERE GUID=@g ORDER BY ID` diff --git a/internal/lkgateway/server.go b/internal/lkgateway/server.go index 804ffe1..933daed 100644 --- a/internal/lkgateway/server.go +++ b/internal/lkgateway/server.go @@ -190,7 +190,8 @@ func NewServer(cfg ServerConfig) (*Server, error) { globalIncomingDeps = &inDeps // автоматический режим M2M registerIncoming(mux, adminTpl, inDeps) globalOutgoingStore = NewOutgoingStore() - registerDepoSim(mux, adminTpl) // песочница сверки с DEPO (имитация ответа Fancy) + registerDepoSim(mux, adminTpl) + registerDepoTest(mux, adminTpl) // тестирование DEPO: сценарии с имитацией НРД, только база депозитария // песочница сверки с DEPO (имитация ответа Fancy) outDeps := outgoingDeps{ store: globalOutgoingStore, igwClient: igwClient, diff --git a/internal/lkgateway/web/templates/admin_depo_test.html b/internal/lkgateway/web/templates/admin_depo_test.html new file mode 100644 index 0000000..21ec4d7 --- /dev/null +++ b/internal/lkgateway/web/templates/admin_depo_test.html @@ -0,0 +1,78 @@ +{{define "content"}} + + +{{if .Flash}}
{{.Flash}}
{{end}} + +
+

Тестирование DEPO — сценарии с имитацией НРД

+

Перевод собирается из реальных данных DEPO и проигрывается по шагам так, как шёл бы в бою: bj пишет записи в трансферные таблицы + FTV_M2M / FTV_M2M_LINES / FTV_M2M_EVENTS, ответы НРД (решение контрагента, приём и исполнение поручения 16/16.1, отказ, истечение срока) имитируются. + После каждого шага bj ждёт, пока джоба обработки DEPO пометит запись, и показывает её STATE и комментарий. + В НРД, контрагенту, QUIK МНП и ЛК ничего не уходит. Документы в DEPO создаются настоящие — используйте тестового клиента.

+

База: {{.DB}} · обработка DEPO: {{if .JobMode}}{{.JobMode}}{{else}}не настроена{{end}}

+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+ + {{range .Secs}}{{end}} +
+ + {{$o := .Outcomes}}{{$c := .Codes}} + {{range $i, $n := (slice "1" "2" "3" "4" "5")}} + + + + + {{end}} +
+

Разные исходы у разных бумаг = частичное исполнение. «Отказ по M2M» при зачислении — наш отказ (BJ_DECISION_REJECT), при списании — отказ контрагента (BJ_CP_REJECT).

+ +
+
+ +{{range .Runs}} +
+

{{if eq .Status "running"}}идёт{{else if eq .Status "done"}}завершён{{else}}ошибка{{end}} + {{if eq .Direction "out"}}↗ Списание{{else}}↘ Зачисление{{end}} · {{.Client}} · {{.CPName}} ({{.CP}})

+

Прогон {{.ID}} · {{.Created.Format "02.01.2006 15:04:05"}} · GUID {{.GUID}} · документ DEPO {{if .Doc}}№ {{.Doc}}{{else}}—{{end}} · шапка {{.HeaderState}}{{if .Errors}} · ошибок: {{.Errors}}{{end}}

+ + {{range .Lines}}{{end}} +
БумагаКол-воСценарийСтрока DEPO
{{.ISIN}}
{{.Ref}}
{{.Qty}}{{.Outcome}}{{if .Code}} {{.Code}}{{end}}{{if .State}}{{.State}} {{if .DOL}}№ {{.DOL}}{{end}} {{.Comment}}{{else}}—{{end}}
+
Шаги ({{len .Steps}}) + + {{range .Steps}}{{end}} +
ВремяШагСобытие / строкаДжоба DEPO
{{.At.Format "15:04:05"}}{{.Title}}{{if .Emulated}}
имитация — {{.Emulated}}{{end}}
{{.Event}}
{{.Ref}}
{{if .State}}{{.State}}{{end}} {{.Comment}}
+
+{{end}} +{{if .Running}}{{end}} +{{end}} diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index b68c572..3760caa 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -329,7 +329,7 @@ label { font-size:13px; font-weight:500; } Входящие - {{end}}{{if can "view"}}Ручной ввод M2M{{end}}{{if and devOn (can "operate")}}Тестирование M2M{{end}} + {{end}}{{if can "view"}}Ручной ввод M2M{{end}}{{if can "settings"}}Тест DEPO{{end}}{{if and devOn (can "operate")}}Тестирование M2M{{end}} {{if and .NPR (can "npr")}}НПР / УДС{{end}} {{if or (can "view") (can "quik")}} Обмен ▾