QUIK МНП: входящий перевод зачисляется клиенту только после проведения в DEPO

Если по переводу есть поручение FTV_M2M, зачисление ждёт, пока события BJ_ORDER_ACCEPTED обработаны в DEPO
(SUCCESS); до того — состояние «ждёт проведения в DEPO» с причиной. Ошибка в DEPO — ждём, после возврата
в очередь и проведения зачисление проходит само. DEPO в переводе не участвует — как раньше, по ответу НРД.
22.10.2026, 895C0A32: бумаги ушли в QUIK, хотя поручение в DEPO не создалось.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-22 17:58:48 +03:00
parent 56a5a50174
commit e51962c5bb
7 changed files with 69 additions and 6 deletions
+38
View File
@@ -118,6 +118,44 @@ func depoEventsDB(rc *RuntimeConfig) (*sql.DB, error) {
return fancyOpen(fs, fs.DepoDB())
}
// depoInPosted — можно ли зачислять входящий перевод в QUIK: бумаги появляются у клиента в QUIK только после
// проведения в DEPO (операция зачисления по BJ_ORDER_ACCEPTED обработана без ошибки). gate=false — DEPO в
// переводе не участвует (обмен не настроен или поручения FTV_M2M нет): зачисление как раньше, по ответу НРД.
// DEPO недоступна — ждём (gate=true, posted=false): зачислять, не зная состояния учёта, нельзя.
func depoInPosted(ctx context.Context, rc *RuntimeConfig, guid string) (gate, posted bool, why string) {
if rc == nil || strings.TrimSpace(rc.Snapshot().Fancy.Server) == "" {
return false, false, ""
}
guid = strings.ToUpper(strings.TrimSpace(guid))
if !uuidRe.MatchString(guid) {
return false, false, ""
}
db, err := depoEventsDB(rc)
if err != nil {
return true, false, "DEPO недоступна: " + err.Error()
}
defer db.Close()
if !depoTableExists(ctx, db, "FTV_M2M_EVENTS") {
return false, false, ""
}
var m2m, ok, notOK int
var state, lastErr string
if err := db.QueryRowContext(ctx, fsql.QEvPosted, sql.Named("g", guid)).Scan(&m2m, &ok, &notOK, &state, &lastErr); err != nil {
return true, false, "DEPO: " + quikErrorDetail(err)
}
switch {
case m2m == 0:
return false, false, ""
case ok > 0 && notOK == 0:
return true, true, ""
case lastErr != "" || state == "ERROR":
return true, false, "ошибка в DEPO: " + strings.TrimSpace(lastErr) + " — устраните и верните запись в очередь («Обмен с DEPO»), зачисление в QUIK пройдёт само"
case ok+notOK == 0:
return true, false, "ждём отчёт НРД об исполнении (событие BJ_ORDER_ACCEPTED в DEPO)"
}
return true, false, "ждём проведения операции зачисления в DEPO"
}
func depoHasColumn(ctx context.Context, db *sql.DB, table, col string) bool {
var n int
_ = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME=@t AND COLUMN_NAME=@c", sql.Named("t", table), sql.Named("c", col)).Scan(&n)
+10
View File
@@ -27,3 +27,13 @@ var QEv10 = "INSERT INTO dbo.FTV_M2M_EVENTS (EVENT_TYPE, EVENT_DATE) VALUES ('HA
// QEv11 — результат последней обработки справочника участников джобой bj2depo: STATE/COMMENT
// последнего события HANDBOOK. bj только читает (COMMENT/STATE пишет Fancy).
var QEv11 = "SELECT TOP 1 EVENT_DATE, ISNULL(STATE,''), ISNULL(COMMENT,'') FROM dbo.FTV_M2M_EVENTS WHERE EVENT_TYPE='HANDBOOK' AND (NULLIF(LTRIM(RTRIM(ISNULL(COMMENT,''))),'') IS NOT NULL OR NULLIF(LTRIM(RTRIM(ISNULL(STATE,''))),'') IS NOT NULL) ORDER BY EVENT_DATE DESC"
// QEvPosted — проведён ли входящий перевод в DEPO: есть ли поручение FTV_M2M, сколько событий BJ_ORDER_ACCEPTED
// обработано (SUCCESS) и сколько нет (TODO/ERROR), ошибка поручения или этих событий (другие события перевода
// зачисление не задерживают).
var QEvPosted = `SELECT (SELECT COUNT(*) FROM dbo.FTV_M2M WHERE GUID=@g),
(SELECT COUNT(*) FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g AND EVENT_TYPE='BJ_ORDER_ACCEPTED' AND STATE='SUCCESS'),
(SELECT COUNT(*) FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g AND EVENT_TYPE='BJ_ORDER_ACCEPTED' AND STATE<>'SUCCESS'),
ISNULL((SELECT TOP 1 STATE FROM dbo.FTV_M2M WHERE GUID=@g ORDER BY ID DESC),''),
COALESCE((SELECT TOP 1 COMMENT FROM dbo.FTV_M2M WHERE GUID=@g AND STATE='ERROR' ORDER BY ID DESC),
(SELECT TOP 1 COMMENT FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g AND EVENT_TYPE='BJ_ORDER_ACCEPTED' AND STATE='ERROR' ORDER BY ID DESC), '')`
+3 -1
View File
@@ -52,7 +52,7 @@ type QuikLink struct {
ISIN string `json:"isin"`
Qty float64 `json:"qty"`
InstrNo string `json:"instr_no,omitempty"` // номер поручения в МНП
State string `json:"state"` // registered | signed | denied | deposited | error | manual
State string `json:"state"` // registered | signed | denied | deposited | error | manual | wait_depo
Err string `json:"err,omitempty"`
Steps []QuikStep `json:"steps"`
UpdatedAt time.Time `json:"updated_at"`
@@ -76,6 +76,8 @@ func (l *QuikLink) StateRu() string {
return "ошибка"
case "manual":
return "автоматика выключена"
case "wait_depo":
return "ждёт проведения в DEPO"
}
return l.State
}
+13
View File
@@ -212,6 +212,19 @@ func quikCycleTick(ctx context.Context, rc *RuntimeConfig) {
if !done || rejected {
continue
}
// бумаги в QUIK — только после проведения в DEPO (если DEPO участвует в переводе)
if gate, posted, why := depoInPosted(ctx, rc, it.GUID); gate && !posted {
for _, sec := range it.Securities {
if sec.Decision == "confirmed" {
globalQuikLinks.update(it.GUID, sec.ISIN, func(l *QuikLink) {
if l.State == "" || l.State == "wait_depo" {
l.Direction, l.State, l.Err = "in", "wait_depo", why
}
})
}
}
continue
}
code, contract, err := quikResolveClient(ctx, rc, it.DocSeries, it.DocNumber)
if err != nil {
for _, sec := range it.Securities {
@@ -121,7 +121,7 @@
.qk-step.ok{border-color:color-mix(in srgb,#16a34a 45%,transparent);background:color-mix(in srgb,#16a34a 12%,var(--card));color:#0f7a37;font-weight:600}
.qk-step.bad{border-color:color-mix(in srgb,#dc2626 45%,transparent);background:color-mix(in srgb,#dc2626 10%,var(--card));color:#b3261e;font-weight:600}
.qk-state{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:700}
.qk-state.registered{background:color-mix(in srgb,#d97706 15%,var(--card));color:#9a6b12}
.qk-state.registered,.qk-state.wait_depo{background:color-mix(in srgb,#d97706 15%,var(--card));color:#9a6b12}
.qk-state.signed,.qk-state.deposited{background:color-mix(in srgb,#16a34a 15%,var(--card));color:#0f7a37}
.qk-state.denied{background:var(--card-2);color:var(--muted)}
.qk-state.error{background:color-mix(in srgb,#dc2626 15%,var(--card));color:#b3261e}
@@ -162,7 +162,7 @@
<span class="muted" style="font-size:12px">клиент QUIK <code>{{.ClientID}}</code>{{if .Contract}} · {{.Contract}}{{end}}{{if .InstrNo}} · поручение МНП <b>№{{.InstrNo}}</b>{{end}}</span>
<span class="qk-state {{.State}}">{{.StateRu}}</span>
</div>
{{if .Err}}<div style="color:#b3261e;font-size:12.5px;margin-top:4px">{{.Err}}</div>{{end}}
{{if .Err}}<div style="{{if eq .State "wait_depo"}}color:var(--muted){{else}}color:#b3261e{{end}};font-size:12.5px;margin-top:4px">{{.Err}}</div>{{end}}
{{if .Steps}}<div class="qk-steps">{{range .Steps}}<span class="qk-step {{if .OK}}ok{{else}}bad{{end}}" title="{{.Reply}}">{{msk .At}} · {{.Action}}{{if .ActionID}} #{{.ActionID}}{{end}}: {{.Reply}}</span>{{end}}</div>{{end}}
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:6px">
{{if eq .State "registered"}}
@@ -371,7 +371,7 @@
.qk-step.ok{border-color:color-mix(in srgb,#16a34a 45%,transparent);background:color-mix(in srgb,#16a34a 12%,var(--card));color:#0f7a37;font-weight:600}
.qk-step.bad{border-color:color-mix(in srgb,#dc2626 45%,transparent);background:color-mix(in srgb,#dc2626 10%,var(--card));color:#b3261e;font-weight:600}
.qk-state{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:700}
.qk-state.registered{background:color-mix(in srgb,#d97706 15%,var(--card));color:#9a6b12}
.qk-state.registered,.qk-state.wait_depo{background:color-mix(in srgb,#d97706 15%,var(--card));color:#9a6b12}
.qk-state.signed,.qk-state.deposited{background:color-mix(in srgb,#16a34a 15%,var(--card));color:#0f7a37}
.qk-state.denied{background:var(--card-2);color:var(--muted)}
.qk-state.error{background:color-mix(in srgb,#dc2626 15%,var(--card));color:#b3261e}
@@ -399,7 +399,7 @@
<span class="muted" style="font-size:12px">клиент QUIK <code>{{.ClientID}}</code>{{if .Contract}} · {{.Contract}}{{end}}{{if .InstrNo}} · поручение МНП <b>№{{.InstrNo}}</b>{{end}}</span>
<span class="qk-state {{.State}}">{{.StateRu}}</span>
</div>
{{if .Err}}<div style="color:#b3261e;font-size:12.5px;margin-top:4px">{{.Err}}</div>{{end}}
{{if .Err}}<div style="{{if eq .State "wait_depo"}}color:var(--muted){{else}}color:#b3261e{{end}};font-size:12.5px;margin-top:4px">{{.Err}}</div>{{end}}
{{if .Steps}}<div class="qk-steps">{{range .Steps}}<span class="qk-step {{if .OK}}ok{{else}}bad{{end}}" title="{{.Reply}}">{{msk .At}} · {{.Action}}{{if .ActionID}} #{{.ActionID}}{{end}}: {{.Reply}}</span>{{end}}</div>{{end}}
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:6px">
{{if or (eq .State "error") (eq .State "manual") (not .InstrNo)}}
@@ -8,7 +8,7 @@
.qk-step.ok{border-color:color-mix(in srgb,#16a34a 45%,transparent);background:color-mix(in srgb,#16a34a 12%,var(--card));color:#0f7a37;font-weight:600}
.qk-step.bad{border-color:color-mix(in srgb,#dc2626 45%,transparent);background:color-mix(in srgb,#dc2626 10%,var(--card));color:#b3261e;font-weight:600}
.qk-state{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:700}
.qk-state.registered{background:color-mix(in srgb,#d97706 15%,var(--card));color:#9a6b12}
.qk-state.registered,.qk-state.wait_depo{background:color-mix(in srgb,#d97706 15%,var(--card));color:#9a6b12}
.qk-state.signed,.qk-state.deposited{background:color-mix(in srgb,#16a34a 15%,var(--card));color:#0f7a37}
.qk-state.denied{background:var(--card-2);color:var(--muted)}
.qk-state.error{background:color-mix(in srgb,#dc2626 15%,var(--card));color:#b3261e}