From e51962c5bbd63fe774144d6742f8b9dab15d11f1 Mon Sep 17 00:00:00 2001 From: fontvielle Date: Thu, 22 Oct 2026 17:58:48 +0300 Subject: [PATCH] =?UTF-8?q?QUIK=20=D0=9C=D0=9D=D0=9F:=20=D0=B2=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D1=8F=D1=89=D0=B8=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=B7=D0=B0=D1=87=D0=B8=D1=81=D0=BB=D1=8F=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D1=83?= =?UTF-8?q?=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B2=20DEPO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Если по переводу есть поручение FTV_M2M, зачисление ждёт, пока события BJ_ORDER_ACCEPTED обработаны в DEPO (SUCCESS); до того — состояние «ждёт проведения в DEPO» с причиной. Ошибка в DEPO — ждём, после возврата в очередь и проведения зачисление проходит само. DEPO в переводе не участвует — как раньше, по ответу НРД. 22.10.2026, 895C0A32: бумаги ушли в QUIK, хотя поручение в DEPO не создалось. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn --- internal/lkgateway/depoevents.go | 38 +++++++++++++++++++ internal/lkgateway/fsql/depoevents_q.go | 10 +++++ internal/lkgateway/quikcycle.go | 4 +- internal/lkgateway/quikhooks.go | 13 +++++++ .../lkgateway/web/templates/admin_claim.html | 4 +- .../web/templates/admin_incoming_detail.html | 4 +- .../templates/admin_quik_instructions.html | 2 +- 7 files changed, 69 insertions(+), 6 deletions(-) diff --git a/internal/lkgateway/depoevents.go b/internal/lkgateway/depoevents.go index c758437..ea36141 100644 --- a/internal/lkgateway/depoevents.go +++ b/internal/lkgateway/depoevents.go @@ -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) diff --git a/internal/lkgateway/fsql/depoevents_q.go b/internal/lkgateway/fsql/depoevents_q.go index f31e97c..2b3106d 100644 --- a/internal/lkgateway/fsql/depoevents_q.go +++ b/internal/lkgateway/fsql/depoevents_q.go @@ -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), '')` diff --git a/internal/lkgateway/quikcycle.go b/internal/lkgateway/quikcycle.go index c8c80da..0ffd45e 100644 --- a/internal/lkgateway/quikcycle.go +++ b/internal/lkgateway/quikcycle.go @@ -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 } diff --git a/internal/lkgateway/quikhooks.go b/internal/lkgateway/quikhooks.go index 77b37cf..41781cb 100644 --- a/internal/lkgateway/quikhooks.go +++ b/internal/lkgateway/quikhooks.go @@ -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 { diff --git a/internal/lkgateway/web/templates/admin_claim.html b/internal/lkgateway/web/templates/admin_claim.html index 1b160d6..ca7e7a5 100644 --- a/internal/lkgateway/web/templates/admin_claim.html +++ b/internal/lkgateway/web/templates/admin_claim.html @@ -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 @@ клиент QUIK {{.ClientID}}{{if .Contract}} · {{.Contract}}{{end}}{{if .InstrNo}} · поручение МНП №{{.InstrNo}}{{end}} {{.StateRu}} - {{if .Err}}
{{.Err}}
{{end}} + {{if .Err}}
{{.Err}}
{{end}} {{if .Steps}}
{{range .Steps}}{{msk .At}} · {{.Action}}{{if .ActionID}} #{{.ActionID}}{{end}}: {{.Reply}}{{end}}
{{end}}
{{if eq .State "registered"}} diff --git a/internal/lkgateway/web/templates/admin_incoming_detail.html b/internal/lkgateway/web/templates/admin_incoming_detail.html index 0f611f1..6f4a2b7 100644 --- a/internal/lkgateway/web/templates/admin_incoming_detail.html +++ b/internal/lkgateway/web/templates/admin_incoming_detail.html @@ -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 @@ клиент QUIK {{.ClientID}}{{if .Contract}} · {{.Contract}}{{end}}{{if .InstrNo}} · поручение МНП №{{.InstrNo}}{{end}} {{.StateRu}}
- {{if .Err}}
{{.Err}}
{{end}} + {{if .Err}}
{{.Err}}
{{end}} {{if .Steps}}
{{range .Steps}}{{msk .At}} · {{.Action}}{{if .ActionID}} #{{.ActionID}}{{end}}: {{.Reply}}{{end}}
{{end}}
{{if or (eq .State "error") (eq .State "manual") (not .InstrNo)}} diff --git a/internal/lkgateway/web/templates/admin_quik_instructions.html b/internal/lkgateway/web/templates/admin_quik_instructions.html index 08b4915..d2126b0 100644 --- a/internal/lkgateway/web/templates/admin_quik_instructions.html +++ b/internal/lkgateway/web/templates/admin_quik_instructions.html @@ -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}