From 059206348f502f8485afdaf7cd63190633a545cd Mon Sep 17 00:00:00 2001 From: zuevav Date: Thu, 17 Sep 2026 12:54:50 +0300 Subject: [PATCH] =?UTF-8?q?DEPO:=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=83=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC=D0=BC=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=B0=20bj2depo=20=E2=80=94=20=D0=B1=D0=B5=D0=B7=20COMME?= =?UTF-8?q?NT/STATE,=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80?= =?UTF-8?q?=D1=8B=20=D0=B2=20PARAMETERS,=20=D0=B2=D1=80=D0=B5=D0=BC=D1=8F?= =?UTF-8?q?=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20=D0=B8=D0=B7=20?= =?UTF-8?q?bj?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit COMMENT и STATE в трансферных таблицах заполняет только bj2depo: bj их больше не пишет (шапка, события, BJ_PING). Параметры событий — в PARAMETERS (CONTR_D_ID поручения НРД, номер поручения МНП, текст причины отказа), EVENT_DATE — фактическое время события в bj. Событий от bj2depo нет: bj не читает и не помечает DEPO_*, номер документа берёт из FTV_M2M.DOC. Страница «bj ↔ bj2depo» обновлена: поля, правила, FTV_M2M_PROCESS_WIRING по BJ_ORDER_ACCEPTED/BJ_EXECUTED, примеры SQL, закрытые и оставшиеся вопросы. Co-Authored-By: Claude Opus 5 (1M context) --- internal/lkgateway/depoevents.go | 173 +++++++----------- .../templates/admin_help_fancy_events.html | 102 ++++++----- 2 files changed, 120 insertions(+), 155 deletions(-) diff --git a/internal/lkgateway/depoevents.go b/internal/lkgateway/depoevents.go index 4119d67..e66d39d 100644 --- a/internal/lkgateway/depoevents.go +++ b/internal/lkgateway/depoevents.go @@ -2,11 +2,12 @@ package lkgateway // Обмен с DEPO (Fancy) через трансферные таблицы в нашей копии базы // (BJ_DEPO_DATA): FTV_M2M / FTV_M2M_LINES / FTV_M2M_EVENTS / FTV_M2M_HANDBOOK. -// Согласовано с программистом Fancy 09–10.09.2026 (страница /admin/help/fancy-events): -// - bj пишет поручения и события BJ_*; джоба bj2depo создаёт сущности DEPO и -// пишет в ответ только DEPO_ORDER_CREATED / DEPO_ERROR; -// - справочник участников — TRUNCATE + полная перезапись при каждом получении; -// - STATE: TODO при записи → SUCCESS/ERROR после обработки получателем. +// Согласовано с программистом Fancy (страница /admin/help/fancy-events), уточнено 17.09.2026: +// - bj — мастер: пишет поручения и события BJ_*; джоба bj2depo создаёт сущности DEPO, событий от +// себя не пишет — результат bj читает в FTV_M2M.DOC и в STATE/COMMENT своих событий; +// - COMMENT и STATE заполняет только bj2depo (STATE по умолчанию TODO); параметры события bj пишет +// в PARAMETERS; EVENT_DATE — фактическое время события в bj; +// - справочник участников — TRUNCATE + полная перезапись при каждом получении. import ( "context" @@ -124,13 +125,15 @@ func depoTableExists(ctx context.Context, db *sql.DB, name string) bool { var depoTablesWarned time.Time -// depoEvent — одна строка FTV_M2M_EVENTS (идемпотентно по GUID+REFERENCE+EVENT_TYPE), дата = сейчас. -func depoEvent(rc *RuntimeConfig, guid, ref, typ, codes, comment string) { - depoEventAt(rc, time.Time{}, guid, ref, typ, codes, comment) +// depoEvent — одна строка FTV_M2M_EVENTS (идемпотентно по GUID+REFERENCE+EVENT_TYPE), время события — сейчас. +// params — параметры события для bj2depo (PARAMETERS): номер поручения НРД, номер поручения МНП, текст причины отказа. +func depoEvent(rc *RuntimeConfig, guid, ref, typ, codes, params string) { + depoEventAt(rc, time.Now(), guid, ref, typ, codes, params) } -// depoEventAt — то же с явной датой события (для выгрузки истории). -func depoEventAt(rc *RuntimeConfig, at time.Time, guid, ref, typ, codes, comment string) { +// depoEventAt — то же с явным временем события (выгрузка истории). COMMENT и STATE не пишем: +// их заполняет bj2depo (STATE по умолчанию TODO). +func depoEventAt(rc *RuntimeConfig, at time.Time, guid, ref, typ, codes, params string) { guid = strings.ToUpper(strings.TrimSpace(guid)) if !uuidRe.MatchString(guid) { return // тестовые/служебные идентификаторы в DEPO не отдаём @@ -149,8 +152,11 @@ func depoEventAt(rc *RuntimeConfig, at time.Time, guid, ref, typ, codes, comment } return } - if len([]rune(comment)) > 4000 { - comment = string([]rune(comment)[:4000]) + if at.IsZero() { + at = time.Now() + } + if len([]rune(params)) > 4000 { + params = string([]rune(params)[:4000]) } var n int _ = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g AND ISNULL(REFERENCE,'')=@r AND EVENT_TYPE=@t", @@ -158,17 +164,18 @@ func depoEventAt(rc *RuntimeConfig, at time.Time, guid, ref, typ, codes, comment if n > 0 { return } - // Параметр даты — типизированный NULL: при нетипизированном nil драйвер шлёт nvarchar(1), - // ISNULL берёт тип первого аргумента и GETDATE() обрезается до одного символа. - evDate := sql.NullTime{Time: at, Valid: !at.IsZero()} - _, err = db.ExecContext(ctx, `INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, STATE, EVENT_TYPE, EVENT_DATE, M2M_REJECT_CODE, COMMENT) - VALUES (@g, @r, 'TODO', @t, COALESCE(@at, GETDATE()), @c, @cm)`, - sql.Named("g", guid), sql.Named("r", nullStr(ref)), sql.Named("t", typ), sql.Named("at", evDate), sql.Named("c", nullStr(codes)), sql.Named("cm", nullStr(comment))) + q := `INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, EVENT_TYPE, EVENT_DATE, M2M_REJECT_CODE) VALUES (@g, @r, @t, @at, @c)` + if depoHasColumn(ctx, db, "FTV_M2M_EVENTS", "PARAMETERS") { + q = `INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, EVENT_TYPE, EVENT_DATE, M2M_REJECT_CODE, PARAMETERS) VALUES (@g, @r, @t, @at, @c, @p)` + } + _, err = db.ExecContext(ctx, q, + sql.Named("g", guid), sql.Named("r", nullStr(ref)), sql.Named("t", typ), sql.Named("at", at.In(time.FixedZone("МСК", 3*3600)).Format("2006-01-02T15:04:05.000")), + sql.Named("c", nullStr(codes)), sql.Named("p", nullStr(params))) if err != nil { log.Printf("depo-events: %s %s/%s: %v", typ, shortGUID(guid), ref, err) return } - log.Printf("depo-events: %s %s %s %s", typ, shortGUID(guid), ref, codes) + log.Printf("depo-events: %s %s %s %s %s", typ, shortGUID(guid), ref, codes, params) } type depoLine struct { @@ -223,9 +230,10 @@ func depoUpsertOrder(rc *RuntimeConfig, guid, direction, cpCode, deponentName, d log.Printf("depo-events: FTV_M2M %s: %v", shortGUID(guid), err) return false } - head := `INSERT INTO dbo.FTV_M2M (GUID, DIRECTION, CONTRAGENT_M2M_CODE, DEPONENT_NAME, DEPONENT_DOC, COMMENT) VALUES (@g, @d, @cp, @nm, @doc, @cm)` + // COMMENT не пишем — его заполняет bj2depo (17.09.2026); comment остаётся только для журнала bj. + head := `INSERT INTO dbo.FTV_M2M (GUID, DIRECTION, CONTRAGENT_M2M_CODE, DEPONENT_NAME, DEPONENT_DOC) VALUES (@g, @d, @cp, @nm, @doc)` if hasDeponentID { - head = `INSERT INTO dbo.FTV_M2M (GUID, DIRECTION, CONTRAGENT_M2M_CODE, DEPONENT_NAME, DEPONENT_DOC, DEPONENT_ID, COMMENT) VALUES (@g, @d, @cp, @nm, @doc, @did, @cm)` + head = `INSERT INTO dbo.FTV_M2M (GUID, DIRECTION, CONTRAGENT_M2M_CODE, DEPONENT_NAME, DEPONENT_DOC, DEPONENT_ID) VALUES (@g, @d, @cp, @nm, @doc, @did)` } if _, err := tx.ExecContext(ctx, head, sql.Named("g", guid), sql.Named("d", direction), sql.Named("cp", nullStr(cpCode)), sql.Named("nm", nullStr(deponentName)), sql.Named("doc", nullStr(deponentDoc)), @@ -310,7 +318,7 @@ func depoOnIncomingReceived(rc *RuntimeConfig, it *IncomingTransfer) { name := strings.TrimSpace(strings.Join([]string{it.InvestorLast, it.InvestorFirst, it.InvestorMiddle}, " ")) doc := strings.TrimSpace(it.DocSeries + " " + it.DocNumber) if depoUpsertOrder(rc, it.GUID, "IN", it.SenderCode, name, doc, it.DocSeries, it.DocNumber, lines, "входящий перевод M2M от "+it.SenderCode) { - depoEvent(rc, it.GUID, "", "BJ_RECEIVED", "", "входящий перевод получен, ждёт решения") + depoEvent(rc, it.GUID, "", "BJ_RECEIVED", "", "") } } @@ -335,7 +343,7 @@ func depoOnOutgoingSent(rc *RuntimeConfig, req *m2m.M2MTransferRequest) { name := strings.TrimSpace(strings.Join([]string{inv.LastName, inv.FirstName, inv.MiddleName}, " ")) guid := string(req.Header.GUID) if depoUpsertOrder(rc, guid, "OUT", cp, name, strings.TrimSpace(ser+" "+num), ser, num, lines, "исходящий перевод M2M, получатель ИНН "+inn) { - depoEvent(rc, guid, "", "BJ_SENT", "", "запрос на перевод отправлен в НРД") + depoEvent(rc, guid, "", "BJ_SENT", "", "") } } @@ -346,9 +354,9 @@ func depoOnIncomingDecision(rc *RuntimeConfig, it *IncomingTransfer, decisions [ } for _, d := range decisions { if d.Confirm { - depoEvent(rc, it.GUID, d.ReferenceID, "BJ_DECISION_CONFIRM", "", "приём бумаги подтверждён контрагенту") + depoEvent(rc, it.GUID, d.ReferenceID, "BJ_DECISION_CONFIRM", "", "") } else { - depoEvent(rc, it.GUID, d.ReferenceID, "BJ_DECISION_REJECT", d.RejectCode, "отказ контрагенту: "+m2mCodeLabel(d.RejectCode)) + depoEvent(rc, it.GUID, d.ReferenceID, "BJ_DECISION_REJECT", d.RejectCode, m2mCodeLabel(d.RejectCode)) } } } @@ -363,33 +371,33 @@ func depoOnCounterpartyDecision(rc *RuntimeConfig, decision *m2m.M2MTransferDeci ref := string(sec.ReferenceID) if sec.TransferDecision.Rejection != nil { codes := strings.Join(sec.TransferDecision.Rejection.Codes, ";") - depoEvent(rc, guid, ref, "BJ_CP_REJECT", codes, "отказ принимающего депозитария: "+rejectCodesLabel(sec.TransferDecision.Rejection.Codes)) + depoEvent(rc, guid, ref, "BJ_CP_REJECT", codes, rejectCodesLabel(sec.TransferDecision.Rejection.Codes)) } else if sec.TransferDecision.Confirmation != nil { - depoEvent(rc, guid, ref, "BJ_CP_CONFIRM", "", "принимающий депозитарий подтвердил") + depoEvent(rc, guid, ref, "BJ_CP_CONFIRM", "", "") } } } func depoOnServiceError(rc *RuntimeConfig, guid, codes, reason string) { - depoEvent(rc, guid, "", "BJ_SERVICE_ERROR", strings.ReplaceAll(strings.TrimSpace(codes), " ", ";"), "отклонено сервисом MOEX МОСТ: "+reason) + depoEvent(rc, guid, "", "BJ_SERVICE_ERROR", strings.ReplaceAll(strings.TrimSpace(codes), " ", ";"), reason) } func depoOnOrderSent(rc *RuntimeConfig, guid string, contrDIDs []string, form string) { - depoEvent(rc, guid, "", "BJ_ORDER_SENT", "", "подано депо-поручение "+form+" в НРД, CONTR_D_ID "+strings.Join(contrDIDs, ", ")) + depoEvent(rc, guid, "", "BJ_ORDER_SENT", "", strings.Join(contrDIDs, ";")) } // результат по депо-поручению: accepted | rejected | late | expired func depoOnOrderResult(rc *RuntimeConfig, guid, kind, text string) { switch kind { case "accepted": - depoEvent(rc, guid, "", "BJ_ORDER_ACCEPTED", "", "НРД принял депо-поручение (отчёт MS010)") - depoEvent(rc, guid, "", "BJ_EXECUTED", "", "перевод исполнен (MS010)") + depoEvent(rc, guid, "", "BJ_ORDER_ACCEPTED", "", "") + depoEvent(rc, guid, "", "BJ_EXECUTED", "", "") case "rejected": - depoEvent(rc, guid, "", "BJ_ORDER_REJECTED", "", "НРД отверг депо-поручение: "+text) + depoEvent(rc, guid, "", "BJ_ORDER_REJECTED", "", text) case "late": - depoEvent(rc, guid, "", "BJ_ORDER_LATE", "", "отчёт MS010 не получен за 5 минут") + depoEvent(rc, guid, "", "BJ_ORDER_LATE", "", "") case "expired": - depoEvent(rc, guid, "", "BJ_CANCELLED", "", "истёк срок 30 дней без исполнения — перевод считается отменённым") + depoEvent(rc, guid, "", "BJ_CANCELLED", "", "") } } @@ -398,7 +406,7 @@ func depoOnQuik(rc *RuntimeConfig, guid, isin, evt, instrNo string) { if typ == "" { return } - depoEvent(rc, guid, "", typ, "", "QUIK МНП: поручение №"+instrNo+", "+isin) + depoEvent(rc, guid, "", typ, "", instrNo) } // ---- чтение событий DEPO ---- @@ -414,55 +422,10 @@ func depoPollEvents(ctx context.Context, rc *RuntimeConfig) { if !depoTableExists(cctx, db, "FTV_M2M_EVENTS") { return } - docCol := "NULL" - if depoHasColumn(cctx, db, "FTV_M2M", "DOC") { - docCol = "(SELECT TOP 1 CAST(m.DOC AS varchar(32)) FROM dbo.FTV_M2M m WHERE m.GUID=e.GUID)" - } else if depoHasColumn(cctx, db, "FTV_M2M", "FANSY_DOC") { - docCol = "(SELECT TOP 1 CAST(m.FANSY_DOC AS varchar(32)) FROM dbo.FTV_M2M m WHERE m.GUID=e.GUID)" - } + // Событий от bj2depo нет (17.09.2026: мастер — bj): номер документа — FTV_M2M.DOC, результат + // обработки — STATE/COMMENT наших событий и шапки. depoPollOrders(cctx, db, rc) depoAlertEventFailures(cctx, db, rc) - rows, err := db.QueryContext(cctx, `SELECT TOP 100 e.ID, CAST(e.GUID AS varchar(36)), ISNULL(e.REFERENCE,''), e.EVENT_TYPE, ISNULL(e.COMMENT,''), ISNULL(e.M2M_REJECT_CODE,''), - ISNULL(`+docCol+`,'') - FROM dbo.FTV_M2M_EVENTS e WHERE e.EVENT_TYPE LIKE 'DEPO[_]%' AND ISNULL(e.STATE,'TODO')='TODO' ORDER BY e.ID`) - if err != nil { - log.Printf("depo-events: чтение: %v", err) - return - } - type ev struct { - id int64 - guid, ref, typ, comment, codes, doc string - } - var list []ev - for rows.Next() { - var e ev - if rows.Scan(&e.id, &e.guid, &e.ref, &e.typ, &e.comment, &e.codes, &e.doc) == nil { - list = append(list, e) - } - } - rows.Close() - for _, e := range list { - state, note := "SUCCESS", "применено в bj" - switch e.typ { - case "DEPO_ORDER_CREATED": - doc := strings.TrimSpace(e.comment) - if e.doc != "" && e.doc != "0" { - doc = e.doc - } - globalDepoLinks.update(e.guid, func(l *DepoLink) { l.DocNo, l.DocAt, l.LastEvent = doc, time.Now(), e.typ }) - log.Printf("depo-events: DEPO создала поручение по %s: %s", shortGUID(e.guid), doc) - case "DEPO_ERROR": - globalDepoLinks.update(e.guid, func(l *DepoLink) { - l.Errors = append(l.Errors, time.Now().Format("02.01 15:04")+" "+e.comment) - l.LastEvent = e.typ - }) - raiseSystemAlert(rc, "DEPO: ошибка обработки перевода "+shortGUID(e.guid), e.comment) - default: - state, note = "ERROR", "тип события не поддерживается bj" - } - _, _ = db.ExecContext(cctx, "UPDATE dbo.FTV_M2M_EVENTS SET STATE=@s, TimeBeg=ISNULL(TimeBeg,GETDATE()), TimeEnd=GETDATE(), COMMENT=ISNULL(COMMENT,'')+@n WHERE ID=@id", - sql.Named("s", state), sql.Named("n", " | bj: "+note), sql.Named("id", e.id)) - } } // depoPollOrders — джоба bj2depo ставит номер поручения DEPO прямо в FTV_M2M.DOC (и DOL по строкам), @@ -745,7 +708,7 @@ ALTER ROLE SQLAgentOperatorRole ADD MEMBER %[1]s; func depoJobPing(ctx context.Context, db *sql.DB, wait time.Duration) (bool, string) { guid := strings.ToUpper(fmt.Sprintf("%08x-0000-4000-8000-%012x", time.Now().Unix()&0xFFFFFFFF, time.Now().UnixNano()&0xFFFFFFFFFFFF)) - if _, err := db.ExecContext(ctx, "INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, STATE, EVENT_TYPE, EVENT_DATE, COMMENT) VALUES (@g, NULL, 'TODO', 'BJ_PING', GETDATE(), N'проверка обработчика из bj')", sql.Named("g", guid)); err != nil { + if _, err := db.ExecContext(ctx, "INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, EVENT_TYPE, EVENT_DATE) VALUES (@g, NULL, 'BJ_PING', GETDATE())", sql.Named("g", guid)); err != nil { return false, "не удалось записать проверочное событие: " + quikErrorDetail(err) } defer db.ExecContext(context.Background(), "DELETE FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g", sql.Named("g", guid)) @@ -898,27 +861,27 @@ func depoReplayHistory(ctx context.Context, rc *RuntimeConfig, svc *Service, onl cp := handbookNameByCode(it.SenderCode) depoUpsertOrder(rc, it.GUID, "IN", it.SenderCode, name, strings.TrimSpace(it.DocSeries+" "+it.DocNumber), it.DocSeries, it.DocNumber, lines, "TEST3: входящий от "+it.SenderCode+" "+cp) nIn++ - ev(it.ReceivedAt, it.GUID, "", "BJ_RECEIVED", "", "входящий перевод получен от "+it.SenderCode+" "+cp) + ev(it.ReceivedAt, it.GUID, "", "BJ_RECEIVED", "", "") if it.Status == "answered" { for _, s := range it.Securities { switch s.Decision { case "confirmed": - ev(it.AnsweredAt, it.GUID, s.ReferenceID, "BJ_DECISION_CONFIRM", "", "приём подтверждён контрагенту") + ev(it.AnsweredAt, it.GUID, s.ReferenceID, "BJ_DECISION_CONFIRM", "", "") case "rejected": - ev(it.AnsweredAt, it.GUID, s.ReferenceID, "BJ_DECISION_REJECT", s.RejectCode, "отказ контрагенту: "+m2mCodeLabel(s.RejectCode)) + ev(it.AnsweredAt, it.GUID, s.ReferenceID, "BJ_DECISION_REJECT", s.RejectCode, m2mCodeLabel(s.RejectCode)) } } } if it.OrderSubmitted { - ev(it.OrderAt, it.GUID, "", "BJ_ORDER_SENT", "", "подано депо-поручение 16/1 (зачисление), CONTR_D_ID "+it.OrderContrDID) + ev(it.OrderAt, it.GUID, "", "BJ_ORDER_SENT", "", it.OrderContrDID) resps := findOrderResponses(it.OrderContrDID) if done, rejected, text := orderRespVerdict(resps); done { at := orderRespAt(resps) if rejected { - ev(at, it.GUID, "", "BJ_ORDER_REJECTED", "", "НРД отверг депо-поручение: "+text) + ev(at, it.GUID, "", "BJ_ORDER_REJECTED", "", text) } else { - ev(at, it.GUID, "", "BJ_ORDER_ACCEPTED", "", "НРД принял депо-поручение (отчёт MS010)") - ev(at, it.GUID, "", "BJ_EXECUTED", "", "перевод исполнен (MS010)") + ev(at, it.GUID, "", "BJ_ORDER_ACCEPTED", "", "") + ev(at, it.GUID, "", "BJ_EXECUTED", "", "") } } } @@ -957,14 +920,14 @@ func depoReplayHistory(ctx context.Context, rc *RuntimeConfig, svc *Service, onl name := strings.TrimSpace(strings.Join([]string{inv.LastName, inv.FirstName, inv.MiddleName}, " ")) depoUpsertOrder(rc, guid, "OUT", cp, name, strings.TrimSpace(ser+" "+num), ser, num, lines, "TEST3: исходящий, получатель "+cp+" "+handbookNameByCode(cp)) nOut++ - ev(d.CreatedAt, guid, "", "BJ_SENT", "", "запрос на перевод отправлен в НРД") + ev(d.CreatedAt, guid, "", "BJ_SENT", "", "") if d.Decision != nil { for _, sec := range d.Decision.Data.Securities { ref := string(sec.ReferenceID) if sec.TransferDecision.Rejection != nil { - ev(d.UpdatedAt, guid, ref, "BJ_CP_REJECT", strings.Join(sec.TransferDecision.Rejection.Codes, ";"), "отказ принимающего депозитария: "+rejectCodesLabel(sec.TransferDecision.Rejection.Codes)) + ev(d.UpdatedAt, guid, ref, "BJ_CP_REJECT", strings.Join(sec.TransferDecision.Rejection.Codes, ";"), rejectCodesLabel(sec.TransferDecision.Rejection.Codes)) } else if sec.TransferDecision.Confirmation != nil { - ev(d.UpdatedAt, guid, ref, "BJ_CP_CONFIRM", "", "принимающий депозитарий подтвердил") + ev(d.UpdatedAt, guid, ref, "BJ_CP_CONFIRM", "", "") } } } @@ -973,19 +936,19 @@ func depoReplayHistory(ctx context.Context, rc *RuntimeConfig, svc *Service, onl for _, rr := range d.Response.Responses { codes = append(codes, string(rr.Code)) } - ev(d.UpdatedAt, guid, "", "BJ_SERVICE_ERROR", strings.Join(codes, ";"), "отклонено сервисом MOEX МОСТ: "+rejectCodesLabel(codes)) + ev(d.UpdatedAt, guid, "", "BJ_SERVICE_ERROR", strings.Join(codes, ";"), rejectCodesLabel(codes)) } if globalOutgoingStore != nil { if od, ok := scanForm16Deal(globalOutgoingStore, guid); ok && od.Submitted { - ev(od.OrderAt, guid, "", "BJ_ORDER_SENT", "", "подано депо-поручение 16 (списание), CONTR_D_ID "+strings.Join(od.ContrDIDs, ", ")) + ev(od.OrderAt, guid, "", "BJ_ORDER_SENT", "", strings.Join(od.ContrDIDs, ";")) resps, _ := outgoingResponses(od.ContrDIDs) if done, rejected, text := orderRespVerdict(resps); done { at := orderRespAt(resps) if rejected { - ev(at, guid, "", "BJ_ORDER_REJECTED", "", "НРД отверг депо-поручение: "+text) + ev(at, guid, "", "BJ_ORDER_REJECTED", "", text) } else { - ev(at, guid, "", "BJ_ORDER_ACCEPTED", "", "НРД принял депо-поручение (отчёт MS010)") - ev(at, guid, "", "BJ_EXECUTED", "", "перевод исполнен (MS010)") + ev(at, guid, "", "BJ_ORDER_ACCEPTED", "", "") + ev(at, guid, "", "BJ_EXECUTED", "", "") } } } @@ -1028,24 +991,24 @@ func depoSimulateIncoming(ctx context.Context, rc *RuntimeConfig, it *IncomingTr return "", fmt.Errorf("не удалось записать шапку в FTV_M2M") } now := time.Now() - depoEventAt(rc, now, guid, "", "BJ_RECEIVED", "", "входящий перевод получен от "+it.SenderCode+" "+cp+" (тест по образцу "+shortGUID(it.GUID)+")") + depoEventAt(rc, now, guid, "", "BJ_RECEIVED", "", "") anyConfirmed := false for _, s := range it.Securities { switch s.Decision { case "confirmed": anyConfirmed = true - depoEventAt(rc, now.Add(time.Minute), guid, s.ReferenceID, "BJ_DECISION_CONFIRM", "", "приём подтверждён контрагенту") + depoEventAt(rc, now.Add(time.Minute), guid, s.ReferenceID, "BJ_DECISION_CONFIRM", "", "") case "rejected": - depoEventAt(rc, now.Add(time.Minute), guid, s.ReferenceID, "BJ_DECISION_REJECT", s.RejectCode, "отказ контрагенту: "+m2mCodeLabel(s.RejectCode)) + depoEventAt(rc, now.Add(time.Minute), guid, s.ReferenceID, "BJ_DECISION_REJECT", s.RejectCode, m2mCodeLabel(s.RejectCode)) default: anyConfirmed = true - depoEventAt(rc, now.Add(time.Minute), guid, s.ReferenceID, "BJ_DECISION_CONFIRM", "", "приём подтверждён контрагенту (тест)") + depoEventAt(rc, now.Add(time.Minute), guid, s.ReferenceID, "BJ_DECISION_CONFIRM", "", "") } } if anyConfirmed { - depoEventAt(rc, now.Add(2*time.Minute), guid, "", "BJ_ORDER_SENT", "", "подано депо-поручение 16/1 (зачисление) в НРД (тест)") - depoEventAt(rc, now.Add(3*time.Minute), guid, "", "BJ_ORDER_ACCEPTED", "", "НРД принял депо-поручение (отчёт MS010) (тест)") - depoEventAt(rc, now.Add(3*time.Minute), guid, "", "BJ_EXECUTED", "", "перевод исполнен (MS010) (тест)") + depoEventAt(rc, now.Add(2*time.Minute), guid, "", "BJ_ORDER_SENT", "", "") + depoEventAt(rc, now.Add(3*time.Minute), guid, "", "BJ_ORDER_ACCEPTED", "", "") + depoEventAt(rc, now.Add(3*time.Minute), guid, "", "BJ_EXECUTED", "", "") } return guid, nil } diff --git a/internal/lkgateway/web/templates/admin_help_fancy_events.html b/internal/lkgateway/web/templates/admin_help_fancy_events.html index f712d3b..23cd703 100644 --- a/internal/lkgateway/web/templates/admin_help_fancy_events.html +++ b/internal/lkgateway/web/templates/admin_help_fancy_events.html @@ -45,7 +45,7 @@ БумагаISIN + код НРД (напр. SBER/03); bj находит OD_VALUES.ID по ISIN (IS_IN=1 = принята на обслуживание).ISIN, SEC_CODE и SHARE = OD_VALUES.ID. Счёт клиентаbj находит открытый торговый счёт ВТНКЦ… (OD_CLIENTS, STATE=0). При нескольких договорах берётся самый ранний (правило депозитария).OD_CLIENTS.ID и NUMBER. Счёт депозитария (место хранения)наш счёт в НРД HL171004001C + раздел 36MC04136…. В DEPO это запись OD_CLIENTS с NUMBER HL171004001C/253400M18U5TB02TW421/253400_X1A47/<раздел> (например ID 79 для раздела 36MC0413600000F00). OD_CLIENTS_REC.LINK_NUMBER у нас пустой везде.PLACE_NUMBER = раздел (или ID записи OD_CLIENTS, если удобнее). - Причины отказакоды M2M01…M2M10 по каждой бумаге (может быть несколько), при отказе НРД по поручению 16/16-1 — код и текст из MS010.M2M_REJECT_CODE через ;, текст — в COMMENT. + Причины отказакоды M2M01…M2M10 по каждой бумаге (может быть несколько), при отказе НРД по поручению 16/16-1 — код и текст из MS010.M2M_REJECT_CODE через ;, текст — в PARAMETERS. КлючиGUID перевода (из M2MTR) и REFERENCE строки (референс бумаги из M2MTR, M2M…).как ключи FTV_M2M / FTV_M2M_LINES / FTV_M2M_EVENTS. @@ -54,20 +54,23 @@ - + - +
ТаблицаПоля от bj
FTV_M2MGUID, DIRECTION (IN — к нам зачисляют, OUT — мы списываем), CONTRAGENT_M2M_CODE, DEPONENT_NAME, DEPONENT_DOC, DEPONENT_ID (если нашли), COMMENT. Вставка вместе со строками в одной транзакции.
FTV_M2MGUID, DIRECTION (IN — к нам зачисляют, OUT — мы списываем), CONTRAGENT_M2M_CODE, DEPONENT_NAME, DEPONENT_DOC, DEPONENT_ID (если нашли). Вставка вместе со строками в одной транзакции.
FTV_M2M_LINESGUID, REFERENCE, ISIN, SEC_CODE, SHARE (если нашли), AMOUNT (штуки, дробные для паёв), PLACE_NUMBER, DEPO_NUMBER — номер торгового счёта клиента ВТНКЦ… (джоба находит по нему DEPO).
FTV_M2M_EVENTSGUID, REFERENCE (пусто = по поручению целиком), EVENT_TYPE, EVENT_DATE, M2M_REJECT_CODE, COMMENT, STATE (см. правила).
FTV_M2M_EVENTSGUID, REFERENCE (пусто = по поручению целиком), EVENT_TYPE, EVENT_DATE (фактическое время события в bj), M2M_REJECT_CODE, PARAMETERS (параметры события, см. таблицу событий).
+

COMMENT и STATE во всех таблицах заполняет только bj2depo (17.09.2026): bj их не пишет, STATE при вставке получает значение по умолчанию TODO. Номер документа DEPO bj2depo ставит в FTV_M2M.DOC (строки — DOL).

Поручение создаётся в DEPO на самом раннем этапе: сразу после записи в FTV_M2M (для входящих — как только пришёл M2MTR; для исходящих — как только наш M2MTR ушёл в НРД).

Правила для событий

События от bj (обрабатывает bj2depo)

@@ -79,38 +82,34 @@ BJ_DECISION_CONFIRM / BJ_DECISION_REJECTмы ответили контрагенту по бумаге (M2MTD); при отказе — кодыстрокапроставить статус строки: принято / отказано с кодами BJ_CP_CONFIRM / BJ_CP_REJECTконтрагент ответил по нашему исходящему (M2MTD)строкато же для списания BJ_SERVICE_ERRORсервис МОСТ отверг перевод (M2MER ERROR), код M2Mxxпоручениезакрыть поручение как отклонённое - BJ_ORDER_SENTподано депо-поручение 16 (списание) или 16/1 (зачисление) в НРД; в COMMENT — CONTR_D_ID и датапоручениепривязать номер поручения НРД + BJ_ORDER_SENTподано депо-поручение 16 (списание) или 16/1 (зачисление) в НРД; в PARAMETERS — CONTR_D_ID (несколько — через ;)поручениепривязать номер поручения НРД BJ_ORDER_ACCEPTED / BJ_ORDER_REJECTEDНРД принял (отчёт MS010) или отверг наше депо-поручение; при отказе — код и текстпоручениепроставить результат; при принятии — провести движение по счёту, если DEPO делает это по нашему сигналу (см. вопросы) BJ_ORDER_LATEMS010 не получен за 5 минут после подачи (регламент)поручениедля информации / контроля BJ_EXECUTEDперевод исполнен (по отчёту НРД)поручениезакрыть как исполненное BJ_CANCELLEDперевод отозван / аннулирован / истёк срок 30 дней без исполненияпоручениезакрыть как отменённое - BJ_GATEWAY_REJECTшлюз bj отказал в исходящем переводе из ЛК до отправки в НРД (сверка: лицо, счёт, бумага, остаток, место хранения, блокировки; либо данные заявки некорректны). Код в M2M_REJECT_CODE (M2Mxx или QTY/PLACE/QUIK), причина в COMMENT; одновременно вставлены FTV_M2M (OUT) + LINESстроказакрыть строку и документ как отклонённые (см. «Действия джобы»); пометить событие SUCCESS или ERROR — только после этого bj ставит заявке в ЛК «Отклонена» + BJ_GATEWAY_REJECTшлюз bj отказал в исходящем переводе из ЛК до отправки в НРД (сверка: лицо, счёт, бумага, остаток, место хранения, блокировки; либо данные заявки некорректны). Код в M2M_REJECT_CODE (M2Mxx или QTY/PLACE/QUIK), причина в PARAMETERS; одновременно вставлены FTV_M2M (OUT) + LINESстроказакрыть строку и документ как отклонённые (см. «Действия джобы»); пометить событие SUCCESS или ERROR — только после этого bj ставит заявке в ЛК «Отклонена» BJ_PINGпроверка готовности обработчика из настроек bj (GUID случайный, без шапки в FTV_M2M)служебноеничего не делать, только пометить SUCCESS — bj ждёт отметку до 40 с и удаляет строку - BJ_QUIK_BLOCKED / BJ_QUIK_RELEASED / BJ_QUIK_SIGNED / BJ_QUIK_DEPOSITEDблокировка / снятие / списание / зачисление в QUIK МНП; номер поручения МНП в COMMENTстрокатолько для сверки QUIK ↔ DEPO, в DEPO ничего не менять + BJ_QUIK_BLOCKED / BJ_QUIK_RELEASED / BJ_QUIK_SIGNED / BJ_QUIK_DEPOSITEDблокировка / снятие / списание / зачисление в QUIK МНП; номер поручения МНП в PARAMETERSстрокатолько для сверки QUIK ↔ DEPO, в DEPO ничего не менять -

События от bj2depo (обрабатывает bj)

-

Согласовано: bj2depo пишет только DEPO_ORDER_CREATED и DEPO_ERROR. Остальные типы из таблицы ниже (проверка, исполнение, отмена) остаются в резерве на случай, если понадобятся позже; bj их пока не ждёт.

- - - - - - -
EVENT_TYPEКогда bj2depo пишетУровеньЧто делает bj
DEPO_ORDER_CREATEDпоручение в DEPO создано; номер документа в COMMENT (и/или FANSY_DOC в FTV_M2M)поручениепоказывает номер в карточке перевода, далее ссылается на него
DEPO_ERRORджоба не смогла обработать событие или поручение (текст)любойоповещение оператору bj
-

Согласовано с программистом Fancy: от bj2depo достаточно двух событий, DEPO_ORDER_CREATED и DEPO_ERROR. Проверку строк по регламенту делает bj (сверка с DEPO), состояние документа bj читает сам (раздел «Обратная связь в bj»), поэтому отдельные события о проверке, исполнении и отмене не нужны.

+

События от bj2depo

+

Не используются (17.09.2026, программист Fancy: «мастером является bj»). Прежние DEPO_ORDER_CREATED / DEPO_ERROR отменены. bj узнаёт результат так:

+

Порядок событий по этапам

- + - +
Входящий перевод (мы зачисляем)Исходящий перевод (мы списываем)
1. bj: FTV_M2M + LINES, BJ_RECEIVED1. bj: QUIK блокирует бумаги (BJ_QUIK_BLOCKED), M2MTR в НРД, FTV_M2M + LINES, BJ_SENT
2. bj2depo: DEPO_ORDER_CREATED; bj сверяет строки с DEPO2. bj2depo: DEPO_ORDER_CREATED
2. bj2depo: документ DEPO (FTV_M2M.DOC); bj сверяет строки с DEPO2. bj2depo: документ DEPO (FTV_M2M.DOC)
3. bj: ответ контрагенту — BJ_DECISION_CONFIRM / BJ_DECISION_REJECT3. bj: ответ контрагента — BJ_CP_CONFIRM / BJ_CP_REJECT (при отказе QUIK снимает блокировку: BJ_QUIK_RELEASED)
4. bj: подача 16/1 — BJ_ORDER_SENT4. bj: подача 16 — BJ_ORDER_SENT
5. bj: BJ_ORDER_ACCEPTED (MS010) или BJ_ORDER_REJECTED; QUIK зачисляет — BJ_QUIK_DEPOSITED5. bj: BJ_ORDER_ACCEPTED / BJ_ORDER_REJECTED; QUIK списывает — BJ_QUIK_SIGNED
6. bj2depo: движение по счёту депо по BJ_ORDER_ACCEPTED / BJ_EXECUTED, документ в «Конец обработки»6. bj2depo: то же, списание
6. bj2depo: по BJ_ORDER_ACCEPTED / BJ_EXECUTED — FTV_M2M_PROCESS_WIRING, документ «Обработано (ожидание формирования отчёта)»; 7. «Формирование отчёта» — сотрудник депозитария (по рассылке) или автомат документооборота6–7. то же для списания
Отклонения: BJ_GATEWAY_REJECT (шлюз bj отказал до отправки в НРД; статус в ЛК — после обработки джобой), BJ_SERVICE_ERROR, BJ_ORDER_LATE (5 мин без MS010), BJ_CANCELLED (30 дней без исполнения), DEPO_ERROR.
@@ -123,26 +122,26 @@ СобытиеУровеньЧто сделать в DEPOКонечное состояние BJ_RECEIVED, BJ_SENTдокументдокумент уже создан по шапке; ничего не делать, пометить SUCCESS«Начальный ввод» (0) → можно «Начало обработки» (1) - BJ_DECISION_REJECT (входящий: мы отказали), BJ_CP_REJECT (исходящий: отказал контрагент)строкав D_CLISER_M2M_DOL строки записать код из M2M_REJECT_CODE в M2M_STATUS/M2M_TEXT (текст из COMMENT). Если отказаны все строки документа — выполнить шаг «Отмена» (тип шага 0→REJ), как это делает оператор вручнуюстрока «отказ, M2Mxx»; документ «Отмена обработки» (3) + BJ_DECISION_REJECT (входящий: мы отказали), BJ_CP_REJECT (исходящий: отказал контрагент)строкав D_CLISER_M2M_DOL строки записать код из M2M_REJECT_CODE в M2M_STATUS/M2M_TEXT (текст из PARAMETERS). Если отказаны все строки документа — выполнить шаг «Отмена» (тип шага 0→REJ), как это делает оператор вручнуюстрока «отказ, M2Mxx»; документ «Отмена обработки» (3) BJ_DECISION_CONFIRM, BJ_CP_CONFIRMстрокаотметить строку как принятую (M2M_STATUS «принято»); движения по счёту пока нет — ждём поручение НРДстрока «принято»; документ «Начало обработки» (1) - BJ_ORDER_SENTдокументзаписать номер депо-поручения НРД (CONTR_D_ID из COMMENT); состояние — ожидание«Ожидание доставки (ответа)» (10) или «Ожидание исполнения операции» (127) - BJ_ORDER_ACCEPTED + BJ_EXECUTEDдокументпровести движение по счёту депо по всем принятым строкам: входящий — зачисление на торговый счёт клиента (то, что оператор сделал вручную 14.09 для 75070271: появился остаток 10 SBER на 836), исходящий — списание; затем закрыть документ«Конец обработки (в архиве)» (2) - BJ_GATEWAY_REJECTстрокав D_CLISER_M2M_DOL строки записать код из M2M_REJECT_CODE и причину из COMMENT; если отказаны все строки — шаг «Отмена». Документ мог не создаться (например, бумаги с таким ISIN нет в OD_VALUES) — тогда достаточно пометить событие ERROR с текстом причиныстрока «отказ шлюза»; документ «Отмена обработки» (3) - BJ_ORDER_REJECTED, BJ_SERVICE_ERROR, BJ_CANCELLEDдокументзаписать причину (COMMENT / код) в документ, выполнить шаг «Отмена»«Отмена обработки» (3) + BJ_ORDER_SENTдокументзаписать номер депо-поручения НРД (CONTR_D_ID из PARAMETERS); состояние — ожидание«Ожидание доставки (ответа)» (10) или «Ожидание исполнения операции» (127) + BJ_ORDER_ACCEPTED + BJ_EXECUTEDдокументреализовано 17.09.2026: bj2depo вызывает [dbo].[FTV_M2M_PROCESS_WIRING] @OPER_ID (ID документа) — директива «Обработать» для списания / зачисления; bj процедуру не вызывает. Дальше директива «Формирование отчёта»: сотрудники депозитария по почтовой рассылке или автомат документооборота (нужна лицензия) — средствами SQL отчёт не формируется«Обработано (ожидание формирования отчёта)» → после отчёта — конечное + BJ_GATEWAY_REJECTстрокав D_CLISER_M2M_DOL строки записать код из M2M_REJECT_CODE и причину из PARAMETERS; если отказаны все строки — шаг «Отмена». Документ мог не создаться (например, бумаги с таким ISIN нет в OD_VALUES) — тогда достаточно пометить событие ERROR с текстом причиныстрока «отказ шлюза»; документ «Отмена обработки» (3) + BJ_ORDER_REJECTED, BJ_SERVICE_ERROR, BJ_CANCELLEDдокументзаписать причину (PARAMETERS / код) в документ, выполнить шаг «Отмена»«Отмена обработки» (3) BJ_ORDER_LATEдокументтолько пометка/комментарий («отчёт НРД не получен за 5 минут»); состояние не менятьбез изменений BJ_QUIK_*, BJ_PINGслужебныеничего не делать, пометить SUCCESSбез изменений

Названия и коды состояний — из справочника OD_DOC_STATES (D_CAT=0): 0 «Начальный ввод», 1 «Начало обработки (зарегистрирован)», 2 «Конец обработки (в архиве)», 3 «Отмена обработки», 10 «Ожидание доставки (ответа)», 127 «Ожидание исполнения операции». Шаг отмены, который выполнял оператор: OD_STEP_TYPES ID 75069472 «Отмена» (SYS_NAME 0->REJ, OUT_STATE 3).

Запрос, которым джоба находит отказы вместе с документом и строкой:

-
SELECT e.ID, e.GUID, e.REFERENCE, e.M2M_REJECT_CODE, e.COMMENT, m.DOC, l.DOL
+  
SELECT e.ID, e.GUID, e.REFERENCE, e.M2M_REJECT_CODE, e.PARAMETERS, m.DOC, l.DOL
 FROM dbo.FTV_M2M_EVENTS e
 JOIN dbo.FTV_M2M m ON m.GUID = e.GUID
 JOIN dbo.FTV_M2M_LINES l ON l.GUID = e.GUID AND l.REFERENCE = e.REFERENCE
 WHERE e.STATE = 'TODO' AND e.EVENT_TYPE IN ('BJ_DECISION_REJECT', 'BJ_CP_REJECT')
 ORDER BY e.ID;

Для событий по документу целиком (REFERENCE пуст) джойн со строками не нужен:

-
SELECT e.ID, e.GUID, e.EVENT_TYPE, e.M2M_REJECT_CODE, e.COMMENT, m.DOC
+  
SELECT e.ID, e.GUID, e.EVENT_TYPE, e.M2M_REJECT_CODE, e.PARAMETERS, m.DOC
 FROM dbo.FTV_M2M_EVENTS e
 JOIN dbo.FTV_M2M m ON m.GUID = e.GUID
 WHERE e.STATE = 'TODO' AND e.REFERENCE IS NULL
@@ -197,20 +196,19 @@ ORDER BY e.ID;

Примеры SQL

Так bj заведёт входящий перевод (одна транзакция):

BEGIN TRAN;
-INSERT INTO dbo.FTV_M2M (GUID, DIRECTION, CONTRAGENT_M2M_CODE, DEPONENT_NAME, DEPONENT_DOC, DEPONENT_ID, COMMENT)
-VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', 'IN', 'MC0034900000', N'Погонщикова Виктория Олеговна', '4500 123456', 11773497, N'M2MTR от MC0034900000');
-INSERT INTO dbo.FTV_M2M_LINES (GUID, REFERENCE, ISIN, SEC_CODE, SHARE, AMOUNT, PLACE_NUMBER)
-VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', 'M2M0000002675550', 'RU000A107UL4', 'RU000A107UL4', 18824, 1, '36MC0413600000F00');
-INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, STATE, EVENT_TYPE, EVENT_DATE, COMMENT)
-VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', NULL, 'TODO', 'BJ_RECEIVED', GETDATE(), N'входящий перевод получен');
-COMMIT;
-

Так bj2depo сообщает, что документ в DEPO создан:

-
INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, STATE, EVENT_TYPE, EVENT_DATE, COMMENT)
-VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', NULL, 'TODO', 'DEPO_ORDER_CREATED', GETDATE(), N'75070271');
-

Так bj забирает свои события и подтверждает обработку:

-
SELECT ID, GUID, REFERENCE, EVENT_TYPE, M2M_REJECT_CODE, COMMENT
-FROM dbo.FTV_M2M_EVENTS WHERE EVENT_TYPE LIKE 'DEPO_%' AND STATE = 'TODO' ORDER BY ID;
-UPDATE dbo.FTV_M2M_EVENTS SET STATE = 'SUCCESS', TimeBeg = GETDATE(), TimeEnd = GETDATE(), COMMENT = N'применено в bj' WHERE ID = @id;
+INSERT INTO dbo.FTV_M2M (GUID, DIRECTION, CONTRAGENT_M2M_CODE, DEPONENT_NAME, DEPONENT_DOC) +VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', 'IN', 'MC0034900000', N'Погонщикова Виктория Олеговна', '4500 123456'); +INSERT INTO dbo.FTV_M2M_LINES (GUID, REFERENCE, ISIN, SEC_CODE, SHARE, AMOUNT, PLACE_NUMBER, DEPO_NUMBER) +VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', 'M2M0000002675550', 'RU000A107UL4', 'RU000A107UL4', 18824, 1, '36MC0413600000F00', N'ВТНКЦ00000816'); +INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, EVENT_TYPE, EVENT_DATE) +VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', NULL, 'BJ_RECEIVED', '2026-09-17T09:21:26.000'); -- время получения M2MTR в bj +COMMIT; +-- параметры события — в PARAMETERS, например подача поручения 16/1: +INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, EVENT_TYPE, EVENT_DATE, PARAMETERS) +VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', NULL, 'BJ_ORDER_SENT', '2026-09-17T09:22:24.000', N'20608');
+

Так bj читает результат обработки (событий от bj2depo нет):

+
SELECT CAST(GUID AS varchar(36)) GUID, STATE, COMMENT, DOC FROM dbo.FTV_M2M WHERE GUID = @guid;
+SELECT ID, EVENT_TYPE, REFERENCE, STATE, COMMENT, TimeBeg, TimeEnd FROM dbo.FTV_M2M_EVENTS WHERE GUID = @guid ORDER BY ID;

Структура таблицы справочника и то, как bj её перезаписывает:

CREATE TABLE dbo.FTV_M2M_HANDBOOK (
@@ -225,18 +223,22 @@ VALUES ('7707025725', 'MC0005600000', 'DEPO', N'…', N'АО АКБ "Центр
 COMMIT;

Открытые вопросы

-

Закрыто 15.09.2026:

+

Закрыто 17.09.2026 (ответ программиста Fancy):

    -
  • от bj2depo достаточно DEPO_ORDER_CREATED и DEPO_ERROR; DEPO_EXECUTED не нужен, bj его не ждёт;
  • -
  • справочник: поля как в описании, джоба использует INN / PARTICIPANT_CODE / NAME_SHORT, история не нужна, перезапись целиком; таблицу создаёт наша сторона;
  • -
  • REFERENCE для всех исходящих переводов, включая созданные из терминала QUIK, присваивает bj при формировании M2MTR (формат M2M…);
  • -
  • джоба bj2depo на SQL002 / BJ_DEPO_DATA работает (EXECUTE AS SYSDBA), логин bj — читатель базы и запись в трансферные таблицы;
  • -
  • события BJ_QUIK_* bj пишет для истории; джоба помечает их SUCCESS без действий.
  • +
  • таблица FTV_M2M_HANDBOOK создана и заполняется bj; обработка справочника джобой — на последнем этапе проекта (директива вендора переносится с Fancy-скриптов на SQL);
  • +
  • BJ_ORDER_ACCEPTED / BJ_EXECUTED — процедура FTV_M2M_PROCESS_WIRING (вызывает bj2depo), документ «Обработано (ожидание формирования отчёта)»;
  • +
  • COMMENT и STATE заполняет только bj2depo; параметры — в PARAMETERS; EVENT_DATE — фактическое время события (в bj сделано 17.09);
  • +
  • событий от bj2depo нет, мастер — bj (DEPO_ORDER_CREATED / DEPO_ERROR отменены);
  • +
  • счёт клиента — FTV_M2M_LINES.DEPO_NUMBER (bj пишет номер ВТНКЦ… с 16.09).
+

Закрыто 15.09.2026: REFERENCE для всех исходящих присваивает bj (M2M…); джоба bj2depo на SQL002 / BJ_DEPO_DATA работает; BJ_QUIK_* — для истории, без действий.

Остаётся:

    -
  1. Создать таблицу FTV_M2M_HANDBOOK в BJ_DEPO_DATA по скрипту из раздела «Справочник участников».
  2. -
  3. Действия джобы по событиям (раздел «Действия джобы»): движение по счёту депо по BJ_ORDER_ACCEPTED / BJ_EXECUTED, запись отказов в строки и шаг «Отмена».
  4. +
  5. События без REFERENCE (по документу целиком: BJ_RECEIVED, BJ_SENT, BJ_ORDER_SENT, BJ_ORDER_ACCEPTED, BJ_EXECUTED, BJ_QUIK_*) джоба помечает ERROR «Не найдена строка M2M-поручение по GUID + REFERENCE // FTV_M2M_PROCESS_EVENTS // 69» — на 17.09 (перевод D744A410) так же. Для них строку искать не нужно: документ — по GUID.
  6. +
  7. Запись отказов в строки и шаг «Отмена» — в работе у программиста.
  8. +
  9. Создание документа «Списание / Зачисление» — доработка у программиста.
  10. +
  11. «Формирование отчёта» после «Обработано»: вручную по рассылке или лицензия на автомат документооборота — решение депозитария.
  12. +
  13. Место хранения: PLACE по PLACE_NUMBER = 36MC0413600000F00 не проставляется; в DEPO запись называется HL171004001C/…/36MC0413600000F00 — в каком виде передавать?