Тест DEPO: сверка шлюза с DEPO как в бою (иностранные ISIN, остаток, счёт…): зачисление — отказ с кодом, списание — перевод не отправляется; режим «без проверки» для отладки джобы; статус «остановлен»

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-24 13:42:30 +03:00
parent 8dbd97238a
commit 54404a8f6d
2 changed files with 85 additions and 4 deletions
+79
View File
@@ -44,6 +44,7 @@ var depoTestOutcomes = []struct{ Code, Title string }{
type DepoTestLine struct {
Ref, ISIN, Qty, Outcome, Code string
Code2 string // код бумаги в НРД, ушедший в SEC_CODE
Gate string // проверка шлюза bj (как в бою): пусто — пройдена, иначе причина
State, DOL, Comment string // результат джобы по строке FTV_M2M_LINES
}
@@ -63,6 +64,7 @@ type DepoTestRun struct {
Status string // running | done | error
Doc, HeaderState, HeaderComment string
Errors int
GateMode string // strict — как в бою | off — без проверки
}
type depoTestStore struct {
@@ -187,6 +189,7 @@ func depoTestExec(rc *RuntimeConfig, run *DepoTestRun, pace time.Duration, stopO
}
})
if state == "ERROR" && stopOnError {
globalDepoTests.update(id, func(r *DepoTestRun) { r.Status = "stopped" })
depoTestFinish(db, id, run.GUID)
return
}
@@ -253,6 +256,10 @@ func depoTestExec(rc *RuntimeConfig, run *DepoTestRun, pace time.Duration, stopO
}
})
if st == "ERROR" && stopOnError {
globalDepoTests.update(id, func(r *DepoTestRun) {
r.Status = "stopped"
r.Steps = append(r.Steps, DepoTestStep{At: time.Now(), Title: "Сценарий остановлен на ошибке джобы (галочка «остановиться на первой ошибке»): оставшиеся шаги не записаны", State: "WARN"})
})
depoTestFinish(db, id, run.GUID)
return
}
@@ -418,6 +425,35 @@ func registerDepoTest(mux *http.ServeMux, a *admin) {
back("Добавьте хотя бы одну бумагу")
return
}
// Проверка шлюза bj — та же сверка с DEPO, что перед настоящей отправкой/решением.
run.GateMode = "strict"
if r.FormValue("gate") == "off" {
run.GateMode = "off"
}
gate := depoTestGate(run)
var blocked []string
for i := range run.Lines {
l := &run.Lines[i]
g, ok := gate[l.Ref]
if !ok || g.ok {
continue
}
l.Gate = g.text
if run.GateMode != "strict" || l.Outcome == dtDecline {
continue
}
if dir == "in" {
// как автоматический режим: отказ с кодом, который дала сверка
l.Outcome, l.Code = dtDecline, pick(g.code, "M2M05")
} else {
blocked = append(blocked, l.ISIN+": "+g.text)
}
}
if len(blocked) > 0 {
back("В бою bj не отправил бы этот перевод — сверка списания с DEPO не пройдена: " + strings.Join(blocked, "; ") +
". Уберите бумагу или выберите «без проверки шлюза», если нужно проверить именно джобу DEPO.")
return
}
pace, _ := strconv.Atoi(r.FormValue("pace"))
stop := r.FormValue("stop") == "1"
globalDepoTests.mu.Lock()
@@ -436,3 +472,46 @@ func registerDepoTest(mux *http.ServeMux, a *admin) {
http.Redirect(w, r, "/admin/depo-test#run-"+run.ID, http.StatusSeeOther)
})
}
type depoGateLine struct {
ok bool
code, text string
}
// depoTestGate — сверка с DEPO для строк сценария (runDepoCheck / runDepoCheckOut, как в бою).
func depoTestGate(run *DepoTestRun) map[string]depoGateLine {
out := map[string]depoGateLine{}
fio := strings.Fields(run.Client)
for len(fio) < 3 {
fio = append(fio, "")
}
var res depoResult
if run.Direction == "out" {
var secs []depoOutSec
for _, l := range run.Lines {
secs = append(secs, depoOutSec{ReferenceID: l.Ref, ISIN: l.ISIN, Code: l.ISIN, Qty: l.Qty})
}
nsd := globalRC.Snapshot().NSD
res = runDepoCheckOut(fio[0], fio[1], strings.Join(fio[2:], " "), run.Ser, run.Num, secs, nsd.AccountID, nsd.SectionID)
} else {
it := &IncomingTransfer{GUID: run.GUID, SenderCode: run.CP, ReceivedAt: time.Now(),
InvestorLast: fio[0], InvestorFirst: fio[1], InvestorMiddle: strings.Join(fio[2:], " "),
DocType: "21", DocSeries: run.Ser, DocNumber: run.Num}
for _, l := range run.Lines {
it.Securities = append(it.Securities, IncomingSecurity{ReferenceID: l.Ref, ISIN: l.ISIN, SecurityCode: l.ISIN, Quantity: l.Qty})
}
res = runDepoCheck(it)
}
for _, s := range res.Secs {
g := depoGateLine{ok: s.Recommend != "reject", code: s.RejectCode}
var bad []string
for _, c := range s.Checks {
if !c.OK {
bad = append(bad, strings.TrimSpace(c.Name+" "+c.Detail))
}
}
g.text = strings.Join(bad, "; ")
out[s.ReferenceID] = g
}
return out
}
@@ -12,7 +12,7 @@
.st.ERROR{background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)}
.st.TODO,.st.WARN,.st.running{background:var(--warn-weak,#fff4e0);color:var(--warn,#9a6b12)}
.st.done{background:var(--ok-weak,#e6f4ea);color:var(--ok,#12813f)}
.st.error{background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)}
.st.error,.st.stopped{background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)}
table.dt{width:100%;border-collapse:collapse;font-size:13px}
table.dt td,table.dt th{padding:5px 8px;border-bottom:1px solid var(--border);text-align:left;vertical-align:top}
.emu{color:var(--muted);font-size:12px}
@@ -41,6 +41,8 @@
<select name="client" required><option value="">— выбрать —</option>{{range .Clients}}<option value="{{.Name}}|{{.Ser}}|{{.Num}}" {{if eq .Num "289090"}}selected{{end}}>{{.Name}} · {{.Ser}} {{.Num}} · {{.Account}}</option>{{end}}</select></div>
<div><label>Пауза между шагами (имитация задержек НРД)</label>
<select name="pace"><option value="0">без пауз — по готовности джобы</option><option value="5">5 с</option><option value="15">15 с</option><option value="60">1 мин</option></select></div>
<div><label>Проверка шлюза bj (сверка с DEPO перед отправкой)</label>
<select name="gate"><option value="strict">как в бою — непроходные бумаги: отказ (зачисление) / перевод не отправляется (списание)</option><option value="off">без проверки — только отладка джобы DEPO</option></select></div>
<div><label>&nbsp;</label><label style="display:flex;gap:8px;align-items:center;color:var(--text);font-size:13px"><input type="checkbox" name="stop" value="1" checked style="width:auto"> остановиться на первой ошибке джобы</label></div>
</div>
@@ -62,11 +64,11 @@
{{range .Runs}}
<div class="card" id="run-{{.ID}}">
<h2 style="margin-bottom:4px"><span class="st {{.Status}}">{{if eq .Status "running"}}идёт{{else if eq .Status "done"}}завершён{{else}}ошибка{{end}}</span>
<h2 style="margin-bottom:4px"><span class="st {{.Status}}">{{if eq .Status "running"}}идёт{{else if eq .Status "done"}}завершён{{else if eq .Status "stopped"}}остановлен{{else}}ошибка{{end}}</span>
{{if eq .Direction "out"}}↗ Списание{{else}}↘ Зачисление{{end}} · {{.Client}} · {{.CPName}} ({{.CP}})</h2>
<p class="muted" style="font-size:12px;margin-top:0">Прогон {{.ID}} · {{.Created.Format "02.01.2006 15:04:05"}} · GUID <code>{{.GUID}}</code> · документ DEPO {{if .Doc}}№ {{.Doc}}{{else}}—{{end}} · шапка <span class="st {{.HeaderState}}">{{.HeaderState}}</span>{{if .Errors}} · <span class="st ERROR">ошибок: {{.Errors}}</span>{{end}}</p>
<p class="muted" style="font-size:12px;margin-top:0">Прогон {{.ID}} · {{.Created.Format "02.01.2006 15:04:05"}} · GUID <code>{{.GUID}}</code> · документ DEPO {{if .Doc}}№ {{.Doc}}{{else}}—{{end}} · шапка <span class="st {{.HeaderState}}">{{.HeaderState}}</span> · проверка шлюза: {{if eq .GateMode "off"}}<b>выключена</b>{{else}}как в бою{{end}}{{if .Errors}} · <span class="st ERROR">ошибок: {{.Errors}}</span>{{end}}</p>
<table class="dt"><tr><th>Бумага</th><th>Кол-во</th><th>Сценарий</th><th>Строка DEPO</th></tr>
{{range .Lines}}<tr><td>{{.ISIN}}{{if .Code2}} · {{.Code2}}{{end}}<br><span class="emu">{{.Ref}}</span></td><td>{{.Qty}}</td><td>{{.Outcome}}{{if .Code}} {{.Code}}{{end}}</td><td>{{if .State}}<span class="st {{.State}}">{{.State}}</span> {{if .DOL}}№ {{.DOL}}{{end}} {{.Comment}}{{else}}—{{end}}</td></tr>{{end}}
{{range .Lines}}<tr><td>{{.ISIN}}{{if .Code2}} · {{.Code2}}{{end}}<br><span class="emu">{{.Ref}}</span></td><td>{{.Qty}}</td><td>{{.Outcome}}{{if .Code}} {{.Code}}{{end}}{{if .Gate}}<br><span class="emu" style="color:var(--err,#b3261e)">шлюз: {{.Gate}}</span>{{end}}</td><td>{{if .State}}<span class="st {{.State}}">{{.State}}</span> {{if .DOL}}№ {{.DOL}}{{end}} {{.Comment}}{{else}}—{{end}}</td></tr>{{end}}
</table>
<details {{if eq .Status "running"}}open{{end}} style="margin-top:8px"><summary>Шаги ({{len .Steps}})</summary>
<table class="dt"><tr><th>Время</th><th>Шаг</th><th>Событие / строка</th><th>Джоба DEPO</th></tr>