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}}
{{.ClientID}}{{if .Contract}} · {{.Contract}}{{end}}{{if .InstrNo}} · поручение МНП №{{.InstrNo}}{{end}}
{{.StateRu}}