Тест DEPO: сверка шлюза с DEPO как в бою (иностранные ISIN, остаток, счёт…): зачисление — отказ с кодом, списание — перевод не отправляется; режим «без проверки» для отладки джобы; статус «остановлен»
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,7 @@ var depoTestOutcomes = []struct{ Code, Title string }{
|
|||||||
type DepoTestLine struct {
|
type DepoTestLine struct {
|
||||||
Ref, ISIN, Qty, Outcome, Code string
|
Ref, ISIN, Qty, Outcome, Code string
|
||||||
Code2 string // код бумаги в НРД, ушедший в SEC_CODE
|
Code2 string // код бумаги в НРД, ушедший в SEC_CODE
|
||||||
|
Gate string // проверка шлюза bj (как в бою): пусто — пройдена, иначе причина
|
||||||
State, DOL, Comment string // результат джобы по строке FTV_M2M_LINES
|
State, DOL, Comment string // результат джобы по строке FTV_M2M_LINES
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ type DepoTestRun struct {
|
|||||||
Status string // running | done | error
|
Status string // running | done | error
|
||||||
Doc, HeaderState, HeaderComment string
|
Doc, HeaderState, HeaderComment string
|
||||||
Errors int
|
Errors int
|
||||||
|
GateMode string // strict — как в бою | off — без проверки
|
||||||
}
|
}
|
||||||
|
|
||||||
type depoTestStore struct {
|
type depoTestStore struct {
|
||||||
@@ -187,6 +189,7 @@ func depoTestExec(rc *RuntimeConfig, run *DepoTestRun, pace time.Duration, stopO
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if state == "ERROR" && stopOnError {
|
if state == "ERROR" && stopOnError {
|
||||||
|
globalDepoTests.update(id, func(r *DepoTestRun) { r.Status = "stopped" })
|
||||||
depoTestFinish(db, id, run.GUID)
|
depoTestFinish(db, id, run.GUID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -253,6 +256,10 @@ func depoTestExec(rc *RuntimeConfig, run *DepoTestRun, pace time.Duration, stopO
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if st == "ERROR" && stopOnError {
|
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)
|
depoTestFinish(db, id, run.GUID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -418,6 +425,35 @@ func registerDepoTest(mux *http.ServeMux, a *admin) {
|
|||||||
back("Добавьте хотя бы одну бумагу")
|
back("Добавьте хотя бы одну бумагу")
|
||||||
return
|
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"))
|
pace, _ := strconv.Atoi(r.FormValue("pace"))
|
||||||
stop := r.FormValue("stop") == "1"
|
stop := r.FormValue("stop") == "1"
|
||||||
globalDepoTests.mu.Lock()
|
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)
|
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.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.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.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{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}
|
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}
|
.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>
|
<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>
|
<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>
|
<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> </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><label> </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>
|
</div>
|
||||||
|
|
||||||
@@ -62,11 +64,11 @@
|
|||||||
|
|
||||||
{{range .Runs}}
|
{{range .Runs}}
|
||||||
<div class="card" id="run-{{.ID}}">
|
<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>
|
{{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>
|
<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>
|
</table>
|
||||||
<details {{if eq .Status "running"}}open{{end}} style="margin-top:8px"><summary>Шаги ({{len .Steps}})</summary>
|
<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>
|
<table class="dt"><tr><th>Время</th><th>Шаг</th><th>Событие / строка</th><th>Джоба DEPO</th></tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user