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:
@@ -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, ¬OK, &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)
|
||||
|
||||
@@ -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), '')`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user