DEPO: события по ответу программиста bj2depo — без COMMENT/STATE, параметры в PARAMETERS, время события из bj

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) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-17 12:54:50 +03:00
parent 28c0ed7349
commit 059206348f
2 changed files with 120 additions and 155 deletions
+68 -105
View File
@@ -2,11 +2,12 @@ package lkgateway
// Обмен с DEPO (Fancy) через трансферные таблицы в нашей копии базы // Обмен с DEPO (Fancy) через трансферные таблицы в нашей копии базы
// (BJ_DEPO_DATA): FTV_M2M / FTV_M2M_LINES / FTV_M2M_EVENTS / FTV_M2M_HANDBOOK. // (BJ_DEPO_DATA): FTV_M2M / FTV_M2M_LINES / FTV_M2M_EVENTS / FTV_M2M_HANDBOOK.
// Согласовано с программистом Fancy 09–10.09.2026 (страница /admin/help/fancy-events): // Согласовано с программистом Fancy (страница /admin/help/fancy-events), уточнено 17.09.2026:
// - bj пишет поручения и события BJ_*; джоба bj2depo создаёт сущности DEPO и // - bj — мастер: пишет поручения и события BJ_*; джоба bj2depo создаёт сущности DEPO, событий от
// пишет в ответ только DEPO_ORDER_CREATED / DEPO_ERROR; // себя не пишет — результат bj читает в FTV_M2M.DOC и в STATE/COMMENT своих событий;
// - справочник участников — TRUNCATE + полная перезапись при каждом получении; // - COMMENT и STATE заполняет только bj2depo (STATE по умолчанию TODO); параметры события bj пишет
// - STATE: TODO при записи → SUCCESS/ERROR после обработки получателем. // в PARAMETERS; EVENT_DATE — фактическое время события в bj;
// - справочник участников — TRUNCATE + полная перезапись при каждом получении.
import ( import (
"context" "context"
@@ -124,13 +125,15 @@ func depoTableExists(ctx context.Context, db *sql.DB, name string) bool {
var depoTablesWarned time.Time var depoTablesWarned time.Time
// depoEvent — одна строка FTV_M2M_EVENTS (идемпотентно по GUID+REFERENCE+EVENT_TYPE), дата = сейчас. // depoEvent — одна строка FTV_M2M_EVENTS (идемпотентно по GUID+REFERENCE+EVENT_TYPE), время события — сейчас.
func depoEvent(rc *RuntimeConfig, guid, ref, typ, codes, comment string) { // params — параметры события для bj2depo (PARAMETERS): номер поручения НРД, номер поручения МНП, текст причины отказа.
depoEventAt(rc, time.Time{}, guid, ref, typ, codes, comment) func depoEvent(rc *RuntimeConfig, guid, ref, typ, codes, params string) {
depoEventAt(rc, time.Now(), guid, ref, typ, codes, params)
} }
// depoEventAt — то же с явной датой события (для выгрузки истории). // depoEventAt — то же с явным временем события (выгрузка истории). COMMENT и STATE не пишем:
func depoEventAt(rc *RuntimeConfig, at time.Time, guid, ref, typ, codes, comment string) { // их заполняет bj2depo (STATE по умолчанию TODO).
func depoEventAt(rc *RuntimeConfig, at time.Time, guid, ref, typ, codes, params string) {
guid = strings.ToUpper(strings.TrimSpace(guid)) guid = strings.ToUpper(strings.TrimSpace(guid))
if !uuidRe.MatchString(guid) { if !uuidRe.MatchString(guid) {
return // тестовые/служебные идентификаторы в DEPO не отдаём return // тестовые/служебные идентификаторы в DEPO не отдаём
@@ -149,8 +152,11 @@ func depoEventAt(rc *RuntimeConfig, at time.Time, guid, ref, typ, codes, comment
} }
return return
} }
if len([]rune(comment)) > 4000 { if at.IsZero() {
comment = string([]rune(comment)[:4000]) at = time.Now()
}
if len([]rune(params)) > 4000 {
params = string([]rune(params)[:4000])
} }
var n int var n int
_ = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g AND ISNULL(REFERENCE,'')=@r AND EVENT_TYPE=@t", _ = 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 { if n > 0 {
return return
} }
// Параметр даты — типизированный NULL: при нетипизированном nil драйвер шлёт nvarchar(1), q := `INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, EVENT_TYPE, EVENT_DATE, M2M_REJECT_CODE) VALUES (@g, @r, @t, @at, @c)`
// ISNULL берёт тип первого аргумента и GETDATE() обрезается до одного символа. if depoHasColumn(ctx, db, "FTV_M2M_EVENTS", "PARAMETERS") {
evDate := sql.NullTime{Time: at, Valid: !at.IsZero()} 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, `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)`, _, err = db.ExecContext(ctx, q,
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))) 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 { if err != nil {
log.Printf("depo-events: %s %s/%s: %v", typ, shortGUID(guid), ref, err) log.Printf("depo-events: %s %s/%s: %v", typ, shortGUID(guid), ref, err)
return 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 { 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) log.Printf("depo-events: FTV_M2M %s: %v", shortGUID(guid), err)
return false 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 { 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, 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)), 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}, " ")) name := strings.TrimSpace(strings.Join([]string{it.InvestorLast, it.InvestorFirst, it.InvestorMiddle}, " "))
doc := strings.TrimSpace(it.DocSeries + " " + it.DocNumber) 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) { 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}, " ")) name := strings.TrimSpace(strings.Join([]string{inv.LastName, inv.FirstName, inv.MiddleName}, " "))
guid := string(req.Header.GUID) guid := string(req.Header.GUID)
if depoUpsertOrder(rc, guid, "OUT", cp, name, strings.TrimSpace(ser+" "+num), ser, num, lines, "исходящий перевод M2M, получатель ИНН "+inn) { 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 { for _, d := range decisions {
if d.Confirm { if d.Confirm {
depoEvent(rc, it.GUID, d.ReferenceID, "BJ_DECISION_CONFIRM", "", "приём бумаги подтверждён контрагенту") depoEvent(rc, it.GUID, d.ReferenceID, "BJ_DECISION_CONFIRM", "", "")
} else { } 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) ref := string(sec.ReferenceID)
if sec.TransferDecision.Rejection != nil { if sec.TransferDecision.Rejection != nil {
codes := strings.Join(sec.TransferDecision.Rejection.Codes, ";") 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 { } 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) { 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) { 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 // результат по депо-поручению: accepted | rejected | late | expired
func depoOnOrderResult(rc *RuntimeConfig, guid, kind, text string) { func depoOnOrderResult(rc *RuntimeConfig, guid, kind, text string) {
switch kind { switch kind {
case "accepted": case "accepted":
depoEvent(rc, guid, "", "BJ_ORDER_ACCEPTED", "", "НРД принял депо-поручение (отчёт MS010)") depoEvent(rc, guid, "", "BJ_ORDER_ACCEPTED", "", "")
depoEvent(rc, guid, "", "BJ_EXECUTED", "", "перевод исполнен (MS010)") depoEvent(rc, guid, "", "BJ_EXECUTED", "", "")
case "rejected": case "rejected":
depoEvent(rc, guid, "", "BJ_ORDER_REJECTED", "", "НРД отверг депо-поручение: "+text) depoEvent(rc, guid, "", "BJ_ORDER_REJECTED", "", text)
case "late": case "late":
depoEvent(rc, guid, "", "BJ_ORDER_LATE", "", "отчёт MS010 не получен за 5 минут") depoEvent(rc, guid, "", "BJ_ORDER_LATE", "", "")
case "expired": 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 == "" { if typ == "" {
return return
} }
depoEvent(rc, guid, "", typ, "", "QUIK МНП: поручение №"+instrNo+", "+isin) depoEvent(rc, guid, "", typ, "", instrNo)
} }
// ---- чтение событий DEPO ---- // ---- чтение событий DEPO ----
@@ -414,55 +422,10 @@ func depoPollEvents(ctx context.Context, rc *RuntimeConfig) {
if !depoTableExists(cctx, db, "FTV_M2M_EVENTS") { if !depoTableExists(cctx, db, "FTV_M2M_EVENTS") {
return return
} }
docCol := "NULL" // Событий от bj2depo нет (17.09.2026: мастер — bj): номер документа — FTV_M2M.DOC, результат
if depoHasColumn(cctx, db, "FTV_M2M", "DOC") { // обработки — STATE/COMMENT наших событий и шапки.
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)"
}
depoPollOrders(cctx, db, rc) depoPollOrders(cctx, db, rc)
depoAlertEventFailures(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 по строкам), // 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) { 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)) 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) return false, "не удалось записать проверочное событие: " + quikErrorDetail(err)
} }
defer db.ExecContext(context.Background(), "DELETE FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g", sql.Named("g", guid)) 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) 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) depoUpsertOrder(rc, it.GUID, "IN", it.SenderCode, name, strings.TrimSpace(it.DocSeries+" "+it.DocNumber), it.DocSeries, it.DocNumber, lines, "TEST3: входящий от "+it.SenderCode+" "+cp)
nIn++ nIn++
ev(it.ReceivedAt, it.GUID, "", "BJ_RECEIVED", "", "входящий перевод получен от "+it.SenderCode+" "+cp) ev(it.ReceivedAt, it.GUID, "", "BJ_RECEIVED", "", "")
if it.Status == "answered" { if it.Status == "answered" {
for _, s := range it.Securities { for _, s := range it.Securities {
switch s.Decision { switch s.Decision {
case "confirmed": case "confirmed":
ev(it.AnsweredAt, it.GUID, s.ReferenceID, "BJ_DECISION_CONFIRM", "", "приём подтверждён контрагенту") ev(it.AnsweredAt, it.GUID, s.ReferenceID, "BJ_DECISION_CONFIRM", "", "")
case "rejected": 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 { 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) resps := findOrderResponses(it.OrderContrDID)
if done, rejected, text := orderRespVerdict(resps); done { if done, rejected, text := orderRespVerdict(resps); done {
at := orderRespAt(resps) at := orderRespAt(resps)
if rejected { if rejected {
ev(at, it.GUID, "", "BJ_ORDER_REJECTED", "", "НРД отверг депо-поручение: "+text) ev(at, it.GUID, "", "BJ_ORDER_REJECTED", "", text)
} else { } else {
ev(at, it.GUID, "", "BJ_ORDER_ACCEPTED", "", "НРД принял депо-поручение (отчёт MS010)") ev(at, it.GUID, "", "BJ_ORDER_ACCEPTED", "", "")
ev(at, it.GUID, "", "BJ_EXECUTED", "", "перевод исполнен (MS010)") 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}, " ")) 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)) depoUpsertOrder(rc, guid, "OUT", cp, name, strings.TrimSpace(ser+" "+num), ser, num, lines, "TEST3: исходящий, получатель "+cp+" "+handbookNameByCode(cp))
nOut++ nOut++
ev(d.CreatedAt, guid, "", "BJ_SENT", "", "запрос на перевод отправлен в НРД") ev(d.CreatedAt, guid, "", "BJ_SENT", "", "")
if d.Decision != nil { if d.Decision != nil {
for _, sec := range d.Decision.Data.Securities { for _, sec := range d.Decision.Data.Securities {
ref := string(sec.ReferenceID) ref := string(sec.ReferenceID)
if sec.TransferDecision.Rejection != nil { 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 { } 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 { for _, rr := range d.Response.Responses {
codes = append(codes, string(rr.Code)) 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 globalOutgoingStore != nil {
if od, ok := scanForm16Deal(globalOutgoingStore, guid); ok && od.Submitted { 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) resps, _ := outgoingResponses(od.ContrDIDs)
if done, rejected, text := orderRespVerdict(resps); done { if done, rejected, text := orderRespVerdict(resps); done {
at := orderRespAt(resps) at := orderRespAt(resps)
if rejected { if rejected {
ev(at, guid, "", "BJ_ORDER_REJECTED", "", "НРД отверг депо-поручение: "+text) ev(at, guid, "", "BJ_ORDER_REJECTED", "", text)
} else { } else {
ev(at, guid, "", "BJ_ORDER_ACCEPTED", "", "НРД принял депо-поручение (отчёт MS010)") ev(at, guid, "", "BJ_ORDER_ACCEPTED", "", "")
ev(at, guid, "", "BJ_EXECUTED", "", "перевод исполнен (MS010)") ev(at, guid, "", "BJ_EXECUTED", "", "")
} }
} }
} }
@@ -1028,24 +991,24 @@ func depoSimulateIncoming(ctx context.Context, rc *RuntimeConfig, it *IncomingTr
return "", fmt.Errorf("не удалось записать шапку в FTV_M2M") return "", fmt.Errorf("не удалось записать шапку в FTV_M2M")
} }
now := time.Now() now := time.Now()
depoEventAt(rc, now, guid, "", "BJ_RECEIVED", "", "входящий перевод получен от "+it.SenderCode+" "+cp+" (тест по образцу "+shortGUID(it.GUID)+")") depoEventAt(rc, now, guid, "", "BJ_RECEIVED", "", "")
anyConfirmed := false anyConfirmed := false
for _, s := range it.Securities { for _, s := range it.Securities {
switch s.Decision { switch s.Decision {
case "confirmed": case "confirmed":
anyConfirmed = true 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": 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: default:
anyConfirmed = true 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 { if anyConfirmed {
depoEventAt(rc, now.Add(2*time.Minute), guid, "", "BJ_ORDER_SENT", "", "подано депо-поручение 16/1 (зачисление) в НРД (тест)") depoEventAt(rc, now.Add(2*time.Minute), guid, "", "BJ_ORDER_SENT", "", "")
depoEventAt(rc, now.Add(3*time.Minute), guid, "", "BJ_ORDER_ACCEPTED", "", "НРД принял депо-поручение (отчёт MS010) (тест)") depoEventAt(rc, now.Add(3*time.Minute), guid, "", "BJ_ORDER_ACCEPTED", "", "")
depoEventAt(rc, now.Add(3*time.Minute), guid, "", "BJ_EXECUTED", "", "перевод исполнен (MS010) (тест)") depoEventAt(rc, now.Add(3*time.Minute), guid, "", "BJ_EXECUTED", "", "")
} }
return guid, nil return guid, nil
} }
@@ -45,7 +45,7 @@
<tr><td>Бумага</td><td>ISIN + код НРД (напр. <code>SBER/03</code>); bj находит <code>OD_VALUES.ID</code> по ISIN (<code>IS_IN=1</code> = принята на обслуживание).</td><td><code>ISIN</code>, <code>SEC_CODE</code> и <code>SHARE = OD_VALUES.ID</code>.</td></tr> <tr><td>Бумага</td><td>ISIN + код НРД (напр. <code>SBER/03</code>); bj находит <code>OD_VALUES.ID</code> по ISIN (<code>IS_IN=1</code> = принята на обслуживание).</td><td><code>ISIN</code>, <code>SEC_CODE</code> и <code>SHARE = OD_VALUES.ID</code>.</td></tr>
<tr><td>Счёт клиента</td><td>bj находит открытый торговый счёт <code>ВТНКЦ…</code> (<code>OD_CLIENTS</code>, <code>STATE=0</code>). При нескольких договорах берётся самый ранний (правило депозитария).</td><td><code>OD_CLIENTS.ID</code> и <code>NUMBER</code>.</td></tr> <tr><td>Счёт клиента</td><td>bj находит открытый торговый счёт <code>ВТНКЦ…</code> (<code>OD_CLIENTS</code>, <code>STATE=0</code>). При нескольких договорах берётся самый ранний (правило депозитария).</td><td><code>OD_CLIENTS.ID</code> и <code>NUMBER</code>.</td></tr>
<tr><td>Счёт депозитария (место хранения)</td><td>наш счёт в НРД <code>HL171004001C</code> + раздел <code>36MC04136…</code>. В DEPO это запись <code>OD_CLIENTS</code> с NUMBER <code>HL171004001C/253400M18U5TB02TW421/253400_X1A47/&lt;раздел&gt;</code> (например ID 79 для раздела 36MC0413600000F00). <code>OD_CLIENTS_REC.LINK_NUMBER</code> у нас пустой везде.</td><td><code>PLACE_NUMBER</code> = раздел (или ID записи OD_CLIENTS, если удобнее).</td></tr> <tr><td>Счёт депозитария (место хранения)</td><td>наш счёт в НРД <code>HL171004001C</code> + раздел <code>36MC04136…</code>. В DEPO это запись <code>OD_CLIENTS</code> с NUMBER <code>HL171004001C/253400M18U5TB02TW421/253400_X1A47/&lt;раздел&gt;</code> (например ID 79 для раздела 36MC0413600000F00). <code>OD_CLIENTS_REC.LINK_NUMBER</code> у нас пустой везде.</td><td><code>PLACE_NUMBER</code> = раздел (или ID записи OD_CLIENTS, если удобнее).</td></tr>
<tr><td>Причины отказа</td><td>коды M2M01…M2M10 по каждой бумаге (может быть несколько), при отказе НРД по поручению 16/16-1 — код и текст из MS010.</td><td><code>M2M_REJECT_CODE</code> через <code>;</code>, текст — в <code>COMMENT</code>.</td></tr> <tr><td>Причины отказа</td><td>коды M2M01…M2M10 по каждой бумаге (может быть несколько), при отказе НРД по поручению 16/16-1 — код и текст из MS010.</td><td><code>M2M_REJECT_CODE</code> через <code>;</code>, текст — в <code>PARAMETERS</code>.</td></tr>
<tr><td>Ключи</td><td><code>GUID</code> перевода (из M2MTR) и <code>REFERENCE</code> строки (референс бумаги из M2MTR, <code>M2M…</code>).</td><td>как ключи FTV_M2M / FTV_M2M_LINES / FTV_M2M_EVENTS.</td></tr> <tr><td>Ключи</td><td><code>GUID</code> перевода (из M2MTR) и <code>REFERENCE</code> строки (референс бумаги из M2MTR, <code>M2M…</code>).</td><td>как ключи FTV_M2M / FTV_M2M_LINES / FTV_M2M_EVENTS.</td></tr>
</tbody> </tbody>
</table> </table>
@@ -54,20 +54,23 @@
<table> <table>
<thead><tr><th style="width:170px">Таблица</th><th>Поля от bj</th></tr></thead> <thead><tr><th style="width:170px">Таблица</th><th>Поля от bj</th></tr></thead>
<tbody> <tbody>
<tr><td><code>FTV_M2M</code></td><td><code>GUID</code>, <code>DIRECTION</code> (IN — к нам зачисляют, OUT — мы списываем), <code>CONTRAGENT_M2M_CODE</code>, <code>DEPONENT_NAME</code>, <code>DEPONENT_DOC</code>, <code>DEPONENT_ID</code> (если нашли), <code>COMMENT</code>. Вставка вместе со строками в одной транзакции.</td></tr> <tr><td><code>FTV_M2M</code></td><td><code>GUID</code>, <code>DIRECTION</code> (IN — к нам зачисляют, OUT — мы списываем), <code>CONTRAGENT_M2M_CODE</code>, <code>DEPONENT_NAME</code>, <code>DEPONENT_DOC</code>, <code>DEPONENT_ID</code> (если нашли). Вставка вместе со строками в одной транзакции.</td></tr>
<tr><td><code>FTV_M2M_LINES</code></td><td><code>GUID</code>, <code>REFERENCE</code>, <code>ISIN</code>, <code>SEC_CODE</code>, <code>SHARE</code> (если нашли), <code>AMOUNT</code> (штуки, дробные для паёв), <code>PLACE_NUMBER</code>, <code>DEPO_NUMBER</code> — номер торгового счёта клиента <code>ВТНКЦ…</code> (джоба находит по нему <code>DEPO</code>).</td></tr> <tr><td><code>FTV_M2M_LINES</code></td><td><code>GUID</code>, <code>REFERENCE</code>, <code>ISIN</code>, <code>SEC_CODE</code>, <code>SHARE</code> (если нашли), <code>AMOUNT</code> (штуки, дробные для паёв), <code>PLACE_NUMBER</code>, <code>DEPO_NUMBER</code> — номер торгового счёта клиента <code>ВТНКЦ…</code> (джоба находит по нему <code>DEPO</code>).</td></tr>
<tr><td><code>FTV_M2M_EVENTS</code></td><td><code>GUID</code>, <code>REFERENCE</code> (пусто = по поручению целиком), <code>EVENT_TYPE</code>, <code>EVENT_DATE</code>, <code>M2M_REJECT_CODE</code>, <code>COMMENT</code>, <code>STATE</code> (см. правила).</td></tr> <tr><td><code>FTV_M2M_EVENTS</code></td><td><code>GUID</code>, <code>REFERENCE</code> (пусто = по поручению целиком), <code>EVENT_TYPE</code>, <code>EVENT_DATE</code> (фактическое время события в bj), <code>M2M_REJECT_CODE</code>, <code>PARAMETERS</code> (параметры события, см. таблицу событий).</td></tr>
</tbody> </tbody>
</table> </table>
<p><strong><code>COMMENT</code> и <code>STATE</code> во всех таблицах заполняет только bj2depo</strong> (17.09.2026): bj их не пишет, <code>STATE</code> при вставке получает значение по умолчанию <code>TODO</code>. Номер документа DEPO bj2depo ставит в <code>FTV_M2M.DOC</code> (строки — <code>DOL</code>).</p>
<p class="muted">Поручение создаётся в DEPO на самом раннем этапе: сразу после записи в FTV_M2M (для входящих — как только пришёл M2MTR; для исходящих — как только наш M2MTR ушёл в НРД).</p> <p class="muted">Поручение создаётся в DEPO на самом раннем этапе: сразу после записи в FTV_M2M (для входящих — как только пришёл M2MTR; для исходящих — как только наш M2MTR ушёл в НРД).</p>
<h2 id="rules">Правила для событий</h2> <h2 id="rules">Правила для событий</h2>
<ul> <ul>
<li><strong>Одно событие — одна строка.</strong> Ключ идемпотентности: <code>GUID + REFERENCE + EVENT_TYPE</code>; повтор не пишется.</li> <li><strong>Одно событие — одна строка.</strong> Ключ идемпотентности: <code>GUID + REFERENCE + EVENT_TYPE</code>; повтор не пишется.</li>
<li><strong>Направление по префиксу типа:</strong> <code>BJ_*</code> пишет bj и читает bj2depo; <code>DEPO_*</code> пишет bj2depo и читает bj. Если удобнее, вместо префикса — колонка <code>SOURCE</code>.</li> <li><strong>Мастер — bj</strong> (17.09.2026): события пишет только bj (<code>BJ_*</code>); bj2depo событий не пишет, результат обработки отражает в <code>STATE</code> (<code>SUCCESS</code> / <code>ERROR</code>), <code>COMMENT</code>, <code>TimeBeg/TimeEnd</code> события и в <code>FTV_M2M.DOC</code>.</li>
<li><strong><code>STATE</code> — состояние обработки получателем:</strong> пишущая сторона ставит <code>TODO</code>; получатель после обработки — <code>SUCCESS</code> или <code>ERROR</code>, в <code>COMMENT</code> — что сделано или почему не вышло; <code>TimeBeg/TimeEnd</code> — время обработки.</li> <li><strong><code>STATE</code> и <code>COMMENT</code> пишет только bj2depo</strong>; bj при вставке их не заполняет.</li>
<li><strong>Параметры события</strong> — в <code>PARAMETERS</code>: номер депо-поручения НРД (CONTR_D_ID), номер поручения МНП, текст причины отказа.</li>
<li><strong><code>EVENT_DATE</code></strong> — фактическое время события в bj (МСК), не время вставки.</li>
<li><strong>Порядок:</strong> события одного GUID обрабатываются по <code>ID</code>. Событие по строке (<code>REFERENCE</code> задан) относится к одной бумаге; без <code>REFERENCE</code> — ко всему поручению.</li> <li><strong>Порядок:</strong> события одного GUID обрабатываются по <code>ID</code>. Событие по строке (<code>REFERENCE</code> задан) относится к одной бумаге; без <code>REFERENCE</code> — ко всему поручению.</li>
<li><strong>Коды отказа</strong> в <code>M2M_REJECT_CODE</code> через <code>;</code>, как их даёт НРД (M2M01…M2M10), человекочитаемый текст — в <code>COMMENT</code>.</li> <li><strong>Коды отказа</strong> в <code>M2M_REJECT_CODE</code> через <code>;</code>, как их даёт НРД (M2M01…M2M10); человекочитаемый текст — в <code>PARAMETERS</code>.</li>
</ul> </ul>
<h2 id="bj-events">События от bj (обрабатывает bj2depo)</h2> <h2 id="bj-events">События от bj (обрабатывает bj2depo)</h2>
@@ -79,38 +82,34 @@
<tr><td><code>BJ_DECISION_CONFIRM</code> / <code>BJ_DECISION_REJECT</code></td><td>мы ответили контрагенту по бумаге (M2MTD); при отказе — коды</td><td>строка</td><td>проставить статус строки: принято / отказано с кодами</td></tr> <tr><td><code>BJ_DECISION_CONFIRM</code> / <code>BJ_DECISION_REJECT</code></td><td>мы ответили контрагенту по бумаге (M2MTD); при отказе — коды</td><td>строка</td><td>проставить статус строки: принято / отказано с кодами</td></tr>
<tr><td><code>BJ_CP_CONFIRM</code> / <code>BJ_CP_REJECT</code></td><td>контрагент ответил по нашему исходящему (M2MTD)</td><td>строка</td><td>то же для списания</td></tr> <tr><td><code>BJ_CP_CONFIRM</code> / <code>BJ_CP_REJECT</code></td><td>контрагент ответил по нашему исходящему (M2MTD)</td><td>строка</td><td>то же для списания</td></tr>
<tr><td><code>BJ_SERVICE_ERROR</code></td><td>сервис МОСТ отверг перевод (M2MER ERROR), код M2Mxx</td><td>поручение</td><td>закрыть поручение как отклонённое</td></tr> <tr><td><code>BJ_SERVICE_ERROR</code></td><td>сервис МОСТ отверг перевод (M2MER ERROR), код M2Mxx</td><td>поручение</td><td>закрыть поручение как отклонённое</td></tr>
<tr><td><code>BJ_ORDER_SENT</code></td><td>подано депо-поручение 16 (списание) или 16/1 (зачисление) в НРД; в COMMENT — CONTR_D_ID и дата</td><td>поручение</td><td>привязать номер поручения НРД</td></tr> <tr><td><code>BJ_ORDER_SENT</code></td><td>подано депо-поручение 16 (списание) или 16/1 (зачисление) в НРД; в PARAMETERS — CONTR_D_ID (несколько — через <code>;</code>)</td><td>поручение</td><td>привязать номер поручения НРД</td></tr>
<tr><td><code>BJ_ORDER_ACCEPTED</code> / <code>BJ_ORDER_REJECTED</code></td><td>НРД принял (отчёт MS010) или отверг наше депо-поручение; при отказе — код и текст</td><td>поручение</td><td>проставить результат; при принятии — провести движение по счёту, если DEPO делает это по нашему сигналу (см. вопросы)</td></tr> <tr><td><code>BJ_ORDER_ACCEPTED</code> / <code>BJ_ORDER_REJECTED</code></td><td>НРД принял (отчёт MS010) или отверг наше депо-поручение; при отказе — код и текст</td><td>поручение</td><td>проставить результат; при принятии — провести движение по счёту, если DEPO делает это по нашему сигналу (см. вопросы)</td></tr>
<tr><td><code>BJ_ORDER_LATE</code></td><td>MS010 не получен за 5 минут после подачи (регламент)</td><td>поручение</td><td>для информации / контроля</td></tr> <tr><td><code>BJ_ORDER_LATE</code></td><td>MS010 не получен за 5 минут после подачи (регламент)</td><td>поручение</td><td>для информации / контроля</td></tr>
<tr><td><code>BJ_EXECUTED</code></td><td>перевод исполнен (по отчёту НРД)</td><td>поручение</td><td>закрыть как исполненное</td></tr> <tr><td><code>BJ_EXECUTED</code></td><td>перевод исполнен (по отчёту НРД)</td><td>поручение</td><td>закрыть как исполненное</td></tr>
<tr><td><code>BJ_CANCELLED</code></td><td>перевод отозван / аннулирован / истёк срок 30 дней без исполнения</td><td>поручение</td><td>закрыть как отменённое</td></tr> <tr><td><code>BJ_CANCELLED</code></td><td>перевод отозван / аннулирован / истёк срок 30 дней без исполнения</td><td>поручение</td><td>закрыть как отменённое</td></tr>
<tr><td><code>BJ_GATEWAY_REJECT</code></td><td>шлюз bj отказал в исходящем переводе из ЛК до отправки в НРД (сверка: лицо, счёт, бумага, остаток, место хранения, блокировки; либо данные заявки некорректны). Код в M2M_REJECT_CODE (M2Mxx или QTY/PLACE/QUIK), причина в COMMENT; одновременно вставлены FTV_M2M (OUT) + LINES</td><td>строка</td><td>закрыть строку и документ как отклонённые (см. «Действия джобы»); <strong>пометить событие SUCCESS или ERROR</strong> — только после этого bj ставит заявке в ЛК «Отклонена»</td></tr> <tr><td><code>BJ_GATEWAY_REJECT</code></td><td>шлюз bj отказал в исходящем переводе из ЛК до отправки в НРД (сверка: лицо, счёт, бумага, остаток, место хранения, блокировки; либо данные заявки некорректны). Код в M2M_REJECT_CODE (M2Mxx или QTY/PLACE/QUIK), причина в PARAMETERS; одновременно вставлены FTV_M2M (OUT) + LINES</td><td>строка</td><td>закрыть строку и документ как отклонённые (см. «Действия джобы»); <strong>пометить событие SUCCESS или ERROR</strong> — только после этого bj ставит заявке в ЛК «Отклонена»</td></tr>
<tr><td><code>BJ_PING</code></td><td>проверка готовности обработчика из настроек bj (GUID случайный, без шапки в FTV_M2M)</td><td>служебное</td><td><strong>ничего не делать, только пометить SUCCESS</strong> — bj ждёт отметку до 40 с и удаляет строку</td></tr> <tr><td><code>BJ_PING</code></td><td>проверка готовности обработчика из настроек bj (GUID случайный, без шапки в FTV_M2M)</td><td>служебное</td><td><strong>ничего не делать, только пометить SUCCESS</strong> — bj ждёт отметку до 40 с и удаляет строку</td></tr>
<tr><td><code>BJ_QUIK_BLOCKED</code> / <code>BJ_QUIK_RELEASED</code> / <code>BJ_QUIK_SIGNED</code> / <code>BJ_QUIK_DEPOSITED</code></td><td>блокировка / снятие / списание / зачисление в QUIK МНП; номер поручения МНП в COMMENT</td><td>строка</td><td>только для сверки QUIK ↔ DEPO, в DEPO ничего не менять</td></tr> <tr><td><code>BJ_QUIK_BLOCKED</code> / <code>BJ_QUIK_RELEASED</code> / <code>BJ_QUIK_SIGNED</code> / <code>BJ_QUIK_DEPOSITED</code></td><td>блокировка / снятие / списание / зачисление в QUIK МНП; номер поручения МНП в PARAMETERS</td><td>строка</td><td>только для сверки QUIK ↔ DEPO, в DEPO ничего не менять</td></tr>
</tbody> </tbody>
</table> </table>
<h2 id="depo-events">События от bj2depo (обрабатывает bj)</h2> <h2 id="depo-events">События от bj2depo</h2>
<p><strong>Согласовано:</strong> bj2depo пишет только <code>DEPO_ORDER_CREATED</code> и <code>DEPO_ERROR</code>. Остальные типы из таблицы ниже (проверка, исполнение, отмена) остаются в резерве на случай, если понадобятся позже; bj их пока не ждёт.</p> <p><strong>Не используются</strong> (17.09.2026, программист Fancy: «мастером является bj»). Прежние <code>DEPO_ORDER_CREATED</code> / <code>DEPO_ERROR</code> отменены. bj узнаёт результат так:</p>
<table> <ul>
<thead><tr><th style="width:230px">EVENT_TYPE</th><th>Когда bj2depo пишет</th><th>Уровень</th><th>Что делает bj</th></tr></thead> <li>документ DEPO создан — номер в <code>FTV_M2M.DOC</code>, строки — <code>FTV_M2M_LINES.DOL</code>; состояние документа bj читает в <code>OD_DOCS.STATE</code>;</li>
<tbody> <li>событие обработано — <code>STATE = SUCCESS</code> / <code>ERROR</code> и <code>COMMENT</code> у самого события <code>BJ_*</code>; по <code>ERROR</code> оператор bj получает оповещение.</li>
<tr><td><code>DEPO_ORDER_CREATED</code></td><td>поручение в DEPO создано; номер документа в COMMENT (и/или <code>FANSY_DOC</code> в FTV_M2M)</td><td>поручение</td><td>показывает номер в карточке перевода, далее ссылается на него</td></tr> </ul>
<tr><td><code>DEPO_ERROR</code></td><td>джоба не смогла обработать событие или поручение (текст)</td><td>любой</td><td>оповещение оператору bj</td></tr>
</tbody>
</table>
<p class="muted">Согласовано с программистом Fancy: от bj2depo достаточно двух событий, <code>DEPO_ORDER_CREATED</code> и <code>DEPO_ERROR</code>. Проверку строк по регламенту делает bj (сверка с DEPO), состояние документа bj читает сам (раздел «Обратная связь в bj»), поэтому отдельные события о проверке, исполнении и отмене не нужны.</p>
<h2 id="timeline">Порядок событий по этапам</h2> <h2 id="timeline">Порядок событий по этапам</h2>
<table> <table>
<thead><tr><th style="width:280px">Входящий перевод (мы зачисляем)</th><th>Исходящий перевод (мы списываем)</th></tr></thead> <thead><tr><th style="width:280px">Входящий перевод (мы зачисляем)</th><th>Исходящий перевод (мы списываем)</th></tr></thead>
<tbody> <tbody>
<tr><td>1. bj: FTV_M2M + LINES, <code>BJ_RECEIVED</code></td><td>1. bj: QUIK блокирует бумаги (<code>BJ_QUIK_BLOCKED</code>), M2MTR в НРД, FTV_M2M + LINES, <code>BJ_SENT</code></td></tr> <tr><td>1. bj: FTV_M2M + LINES, <code>BJ_RECEIVED</code></td><td>1. bj: QUIK блокирует бумаги (<code>BJ_QUIK_BLOCKED</code>), M2MTR в НРД, FTV_M2M + LINES, <code>BJ_SENT</code></td></tr>
<tr><td>2. bj2depo: <code>DEPO_ORDER_CREATED</code>; bj сверяет строки с DEPO</td><td>2. bj2depo: <code>DEPO_ORDER_CREATED</code></td></tr> <tr><td>2. bj2depo: документ DEPO (<code>FTV_M2M.DOC</code>); bj сверяет строки с DEPO</td><td>2. bj2depo: документ DEPO (<code>FTV_M2M.DOC</code>)</td></tr>
<tr><td>3. bj: ответ контрагенту — <code>BJ_DECISION_CONFIRM</code> / <code>BJ_DECISION_REJECT</code></td><td>3. bj: ответ контрагента — <code>BJ_CP_CONFIRM</code> / <code>BJ_CP_REJECT</code> (при отказе QUIK снимает блокировку: <code>BJ_QUIK_RELEASED</code>)</td></tr> <tr><td>3. bj: ответ контрагенту — <code>BJ_DECISION_CONFIRM</code> / <code>BJ_DECISION_REJECT</code></td><td>3. bj: ответ контрагента — <code>BJ_CP_CONFIRM</code> / <code>BJ_CP_REJECT</code> (при отказе QUIK снимает блокировку: <code>BJ_QUIK_RELEASED</code>)</td></tr>
<tr><td>4. bj: подача 16/1 — <code>BJ_ORDER_SENT</code></td><td>4. bj: подача 16 — <code>BJ_ORDER_SENT</code></td></tr> <tr><td>4. bj: подача 16/1 — <code>BJ_ORDER_SENT</code></td><td>4. bj: подача 16 — <code>BJ_ORDER_SENT</code></td></tr>
<tr><td>5. bj: <code>BJ_ORDER_ACCEPTED</code> (MS010) или <code>BJ_ORDER_REJECTED</code>; QUIK зачисляет — <code>BJ_QUIK_DEPOSITED</code></td><td>5. bj: <code>BJ_ORDER_ACCEPTED</code> / <code>BJ_ORDER_REJECTED</code>; QUIK списывает — <code>BJ_QUIK_SIGNED</code></td></tr> <tr><td>5. bj: <code>BJ_ORDER_ACCEPTED</code> (MS010) или <code>BJ_ORDER_REJECTED</code>; QUIK зачисляет — <code>BJ_QUIK_DEPOSITED</code></td><td>5. bj: <code>BJ_ORDER_ACCEPTED</code> / <code>BJ_ORDER_REJECTED</code>; QUIK списывает — <code>BJ_QUIK_SIGNED</code></td></tr>
<tr><td>6. bj2depo: движение по счёту депо по <code>BJ_ORDER_ACCEPTED</code> / <code>BJ_EXECUTED</code>, документ в «Конец обработки»</td><td>6. bj2depo: то же, списание</td></tr> <tr><td>6. bj2depo: по <code>BJ_ORDER_ACCEPTED</code> / <code>BJ_EXECUTED</code> — <code>FTV_M2M_PROCESS_WIRING</code>, документ «Обработано (ожидание формирования отчёта)»; 7. «Формирование отчёта» — сотрудник депозитария (по рассылке) или автомат документооборота</td><td>6–7. то же для списания</td></tr>
<tr><td colspan="2">Отклонения: <code>BJ_GATEWAY_REJECT</code> (шлюз bj отказал до отправки в НРД; статус в ЛК — после обработки джобой), <code>BJ_SERVICE_ERROR</code>, <code>BJ_ORDER_LATE</code> (5 мин без MS010), <code>BJ_CANCELLED</code> (30 дней без исполнения), <code>DEPO_ERROR</code>.</td></tr> <tr><td colspan="2">Отклонения: <code>BJ_GATEWAY_REJECT</code> (шлюз bj отказал до отправки в НРД; статус в ЛК — после обработки джобой), <code>BJ_SERVICE_ERROR</code>, <code>BJ_ORDER_LATE</code> (5 мин без MS010), <code>BJ_CANCELLED</code> (30 дней без исполнения), <code>DEPO_ERROR</code>.</td></tr>
</tbody> </tbody>
</table> </table>
@@ -123,26 +122,26 @@
<thead><tr><th style="width:230px">Событие</th><th style="width:70px">Уровень</th><th>Что сделать в DEPO</th><th style="width:230px">Конечное состояние</th></tr></thead> <thead><tr><th style="width:230px">Событие</th><th style="width:70px">Уровень</th><th>Что сделать в DEPO</th><th style="width:230px">Конечное состояние</th></tr></thead>
<tbody> <tbody>
<tr><td><code>BJ_RECEIVED</code>, <code>BJ_SENT</code></td><td>документ</td><td>документ уже создан по шапке; ничего не делать, пометить SUCCESS</td><td>«Начальный ввод» (0) → можно «Начало обработки» (1)</td></tr> <tr><td><code>BJ_RECEIVED</code>, <code>BJ_SENT</code></td><td>документ</td><td>документ уже создан по шапке; ничего не делать, пометить SUCCESS</td><td>«Начальный ввод» (0) → можно «Начало обработки» (1)</td></tr>
<tr><td><code>BJ_DECISION_REJECT</code> (входящий: мы отказали), <code>BJ_CP_REJECT</code> (исходящий: отказал контрагент)</td><td>строка</td><td>в <code>D_CLISER_M2M_DOL</code> строки записать код из <code>M2M_REJECT_CODE</code> в <code>M2M_STATUS</code>/<code>M2M_TEXT</code> (текст из COMMENT). Если отказаны все строки документа — выполнить шаг «Отмена» (тип шага 0→REJ), как это делает оператор вручную</td><td>строка «отказ, M2Mxx»; документ «Отмена обработки» (3)</td></tr> <tr><td><code>BJ_DECISION_REJECT</code> (входящий: мы отказали), <code>BJ_CP_REJECT</code> (исходящий: отказал контрагент)</td><td>строка</td><td>в <code>D_CLISER_M2M_DOL</code> строки записать код из <code>M2M_REJECT_CODE</code> в <code>M2M_STATUS</code>/<code>M2M_TEXT</code> (текст из PARAMETERS). Если отказаны все строки документа — выполнить шаг «Отмена» (тип шага 0→REJ), как это делает оператор вручную</td><td>строка «отказ, M2Mxx»; документ «Отмена обработки» (3)</td></tr>
<tr><td><code>BJ_DECISION_CONFIRM</code>, <code>BJ_CP_CONFIRM</code></td><td>строка</td><td>отметить строку как принятую (<code>M2M_STATUS</code> «принято»); движения по счёту пока нет — ждём поручение НРД</td><td>строка «принято»; документ «Начало обработки» (1)</td></tr> <tr><td><code>BJ_DECISION_CONFIRM</code>, <code>BJ_CP_CONFIRM</code></td><td>строка</td><td>отметить строку как принятую (<code>M2M_STATUS</code> «принято»); движения по счёту пока нет — ждём поручение НРД</td><td>строка «принято»; документ «Начало обработки» (1)</td></tr>
<tr><td><code>BJ_ORDER_SENT</code></td><td>документ</td><td>записать номер депо-поручения НРД (CONTR_D_ID из COMMENT); состояние — ожидание</td><td>«Ожидание доставки (ответа)» (10) или «Ожидание исполнения операции» (127)</td></tr> <tr><td><code>BJ_ORDER_SENT</code></td><td>документ</td><td>записать номер депо-поручения НРД (CONTR_D_ID из PARAMETERS); состояние — ожидание</td><td>«Ожидание доставки (ответа)» (10) или «Ожидание исполнения операции» (127)</td></tr>
<tr><td><code>BJ_ORDER_ACCEPTED</code> + <code>BJ_EXECUTED</code></td><td>документ</td><td><strong>провести движение по счёту депо</strong> по всем принятым строкам: входящий — зачисление на торговый счёт клиента (то, что оператор сделал вручную 14.09 для 75070271: появился остаток 10 SBER на 836), исходящий — списание; затем закрыть документ</td><td>«Конец обработки (в архиве)» (2)</td></tr> <tr><td><code>BJ_ORDER_ACCEPTED</code> + <code>BJ_EXECUTED</code></td><td>документ</td><td><strong>реализовано 17.09.2026:</strong> bj2depo вызывает <code>[dbo].[FTV_M2M_PROCESS_WIRING] @OPER_ID</code> (ID документа) — директива «Обработать» для списания / зачисления; bj процедуру не вызывает. Дальше директива «Формирование отчёта»: сотрудники депозитария по почтовой рассылке или автомат документооборота (нужна лицензия) — средствами SQL отчёт не формируется</td><td>«Обработано (ожидание формирования отчёта)» → после отчёта — конечное</td></tr>
<tr><td><code>BJ_GATEWAY_REJECT</code></td><td>строка</td><td>в <code>D_CLISER_M2M_DOL</code> строки записать код из <code>M2M_REJECT_CODE</code> и причину из COMMENT; если отказаны все строки — шаг «Отмена». Документ мог не создаться (например, бумаги с таким ISIN нет в OD_VALUES) — тогда достаточно пометить событие ERROR с текстом причины</td><td>строка «отказ шлюза»; документ «Отмена обработки» (3)</td></tr> <tr><td><code>BJ_GATEWAY_REJECT</code></td><td>строка</td><td>в <code>D_CLISER_M2M_DOL</code> строки записать код из <code>M2M_REJECT_CODE</code> и причину из PARAMETERS; если отказаны все строки — шаг «Отмена». Документ мог не создаться (например, бумаги с таким ISIN нет в OD_VALUES) — тогда достаточно пометить событие ERROR с текстом причины</td><td>строка «отказ шлюза»; документ «Отмена обработки» (3)</td></tr>
<tr><td><code>BJ_ORDER_REJECTED</code>, <code>BJ_SERVICE_ERROR</code>, <code>BJ_CANCELLED</code></td><td>документ</td><td>записать причину (COMMENT / код) в документ, выполнить шаг «Отмена»</td><td>«Отмена обработки» (3)</td></tr> <tr><td><code>BJ_ORDER_REJECTED</code>, <code>BJ_SERVICE_ERROR</code>, <code>BJ_CANCELLED</code></td><td>документ</td><td>записать причину (PARAMETERS / код) в документ, выполнить шаг «Отмена»</td><td>«Отмена обработки» (3)</td></tr>
<tr><td><code>BJ_ORDER_LATE</code></td><td>документ</td><td>только пометка/комментарий («отчёт НРД не получен за 5 минут»); состояние не менять</td><td>без изменений</td></tr> <tr><td><code>BJ_ORDER_LATE</code></td><td>документ</td><td>только пометка/комментарий («отчёт НРД не получен за 5 минут»); состояние не менять</td><td>без изменений</td></tr>
<tr><td><code>BJ_QUIK_*</code>, <code>BJ_PING</code></td><td>служебные</td><td>ничего не делать, пометить SUCCESS</td><td>без изменений</td></tr> <tr><td><code>BJ_QUIK_*</code>, <code>BJ_PING</code></td><td>служебные</td><td>ничего не делать, пометить SUCCESS</td><td>без изменений</td></tr>
</tbody> </tbody>
</table> </table>
<p class="muted">Названия и коды состояний — из справочника <code>OD_DOC_STATES</code> (D_CAT=0): 0 «Начальный ввод», 1 «Начало обработки (зарегистрирован)», 2 «Конец обработки (в архиве)», 3 «Отмена обработки», 10 «Ожидание доставки (ответа)», 127 «Ожидание исполнения операции». Шаг отмены, который выполнял оператор: <code>OD_STEP_TYPES</code> ID 75069472 «Отмена» (SYS_NAME <code>0-&gt;REJ</code>, OUT_STATE 3).</p> <p class="muted">Названия и коды состояний — из справочника <code>OD_DOC_STATES</code> (D_CAT=0): 0 «Начальный ввод», 1 «Начало обработки (зарегистрирован)», 2 «Конец обработки (в архиве)», 3 «Отмена обработки», 10 «Ожидание доставки (ответа)», 127 «Ожидание исполнения операции». Шаг отмены, который выполнял оператор: <code>OD_STEP_TYPES</code> ID 75069472 «Отмена» (SYS_NAME <code>0-&gt;REJ</code>, OUT_STATE 3).</p>
<p>Запрос, которым джоба находит отказы вместе с документом и строкой:</p> <p>Запрос, которым джоба находит отказы вместе с документом и строкой:</p>
<pre class="copyable">SELECT e.ID, e.GUID, e.REFERENCE, e.M2M_REJECT_CODE, e.COMMENT, m.DOC, l.DOL <pre class="copyable">SELECT e.ID, e.GUID, e.REFERENCE, e.M2M_REJECT_CODE, e.PARAMETERS, m.DOC, l.DOL
FROM dbo.FTV_M2M_EVENTS e FROM dbo.FTV_M2M_EVENTS e
JOIN dbo.FTV_M2M m ON m.GUID = e.GUID 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 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') WHERE e.STATE = 'TODO' AND e.EVENT_TYPE IN ('BJ_DECISION_REJECT', 'BJ_CP_REJECT')
ORDER BY e.ID;</pre> ORDER BY e.ID;</pre>
<p>Для событий по документу целиком (REFERENCE пуст) джойн со строками не нужен:</p> <p>Для событий по документу целиком (REFERENCE пуст) джойн со строками не нужен:</p>
<pre class="copyable">SELECT e.ID, e.GUID, e.EVENT_TYPE, e.M2M_REJECT_CODE, e.COMMENT, m.DOC <pre class="copyable">SELECT e.ID, e.GUID, e.EVENT_TYPE, e.M2M_REJECT_CODE, e.PARAMETERS, m.DOC
FROM dbo.FTV_M2M_EVENTS e FROM dbo.FTV_M2M_EVENTS e
JOIN dbo.FTV_M2M m ON m.GUID = e.GUID JOIN dbo.FTV_M2M m ON m.GUID = e.GUID
WHERE e.STATE = 'TODO' AND e.REFERENCE IS NULL WHERE e.STATE = 'TODO' AND e.REFERENCE IS NULL
@@ -197,20 +196,19 @@ ORDER BY e.ID;</pre>
<h2 id="sql">Примеры SQL</h2> <h2 id="sql">Примеры SQL</h2>
<p>Так bj заведёт входящий перевод (одна транзакция):</p> <p>Так bj заведёт входящий перевод (одна транзакция):</p>
<pre class="copyable">BEGIN TRAN; <pre class="copyable">BEGIN TRAN;
INSERT INTO dbo.FTV_M2M (GUID, DIRECTION, CONTRAGENT_M2M_CODE, DEPONENT_NAME, DEPONENT_DOC, DEPONENT_ID, COMMENT) 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', 11773497, N'M2MTR от MC0034900000'); 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) 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'); VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', 'M2M0000002675550', 'RU000A107UL4', 'RU000A107UL4', 18824, 1, '36MC0413600000F00', N'ВТНКЦ00000816');
INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, STATE, EVENT_TYPE, EVENT_DATE, COMMENT) INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, EVENT_TYPE, EVENT_DATE)
VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', NULL, 'TODO', 'BJ_RECEIVED', GETDATE(), N'входящий перевод получен'); VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', NULL, 'BJ_RECEIVED', '2026-09-17T09:21:26.000'); -- время получения M2MTR в bj
COMMIT;</pre> COMMIT;
<p>Так bj2depo сообщает, что документ в DEPO создан:</p> -- параметры события — в PARAMETERS, например подача поручения 16/1:
<pre class="copyable">INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, STATE, EVENT_TYPE, EVENT_DATE, COMMENT) INSERT INTO dbo.FTV_M2M_EVENTS (GUID, REFERENCE, EVENT_TYPE, EVENT_DATE, PARAMETERS)
VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', NULL, 'TODO', 'DEPO_ORDER_CREATED', GETDATE(), N'75070271');</pre> VALUES ('5325B860-3265-49B6-986D-FA84FB41FE27', NULL, 'BJ_ORDER_SENT', '2026-09-17T09:22:24.000', N'20608');</pre>
<p>Так bj забирает свои события и подтверждает обработку:</p> <p>Так bj читает результат обработки (событий от bj2depo нет):</p>
<pre class="copyable">SELECT ID, GUID, REFERENCE, EVENT_TYPE, M2M_REJECT_CODE, COMMENT <pre class="copyable">SELECT CAST(GUID AS varchar(36)) GUID, STATE, COMMENT, DOC FROM dbo.FTV_M2M WHERE GUID = @guid;
FROM dbo.FTV_M2M_EVENTS WHERE EVENT_TYPE LIKE 'DEPO_%' AND STATE = 'TODO' ORDER BY ID; SELECT ID, EVENT_TYPE, REFERENCE, STATE, COMMENT, TimeBeg, TimeEnd FROM dbo.FTV_M2M_EVENTS WHERE GUID = @guid ORDER BY ID;</pre>
UPDATE dbo.FTV_M2M_EVENTS SET STATE = 'SUCCESS', TimeBeg = GETDATE(), TimeEnd = GETDATE(), COMMENT = N'применено в bj' WHERE ID = @id;</pre>
<p>Структура таблицы справочника и то, как bj её перезаписывает:</p> <p>Структура таблицы справочника и то, как bj её перезаписывает:</p>
<pre class="copyable">CREATE TABLE dbo.FTV_M2M_HANDBOOK ( <pre class="copyable">CREATE TABLE dbo.FTV_M2M_HANDBOOK (
@@ -225,18 +223,22 @@ VALUES ('7707025725', 'MC0005600000', 'DEPO', N'…', N'АО АКБ "Центр
COMMIT;</pre> COMMIT;</pre>
<h2 id="questions">Открытые вопросы</h2> <h2 id="questions">Открытые вопросы</h2>
<p class="muted">Закрыто 15.09.2026:</p> <p class="muted">Закрыто 17.09.2026 (ответ программиста Fancy):</p>
<ul> <ul>
<li>от bj2depo достаточно <code>DEPO_ORDER_CREATED</code> и <code>DEPO_ERROR</code>; <code>DEPO_EXECUTED</code> не нужен, bj его не ждёт;</li> <li>таблица <code>FTV_M2M_HANDBOOK</code> создана и заполняется bj; обработка справочника джобой — на последнем этапе проекта (директива вендора переносится с Fancy-скриптов на SQL);</li>
<li>справочник: поля как в описании, джоба использует <code>INN</code> / <code>PARTICIPANT_CODE</code> / <code>NAME_SHORT</code>, история не нужна, перезапись целиком; таблицу создаёт наша сторона;</li> <li><code>BJ_ORDER_ACCEPTED</code> / <code>BJ_EXECUTED</code> — процедура <code>FTV_M2M_PROCESS_WIRING</code> (вызывает bj2depo), документ «Обработано (ожидание формирования отчёта)»;</li>
<li><code>REFERENCE</code> для всех исходящих переводов, включая созданные из терминала QUIK, присваивает bj при формировании M2MTR (формат <code>M2M…</code>);</li> <li><code>COMMENT</code> и <code>STATE</code> заполняет только bj2depo; параметры — в <code>PARAMETERS</code>; <code>EVENT_DATE</code> — фактическое время события (в bj сделано 17.09);</li>
<li>джоба <code>bj2depo</code> на SQL002 / <code>BJ_DEPO_DATA</code> работает (EXECUTE AS SYSDBA), логин bj — читатель базы и запись в трансферные таблицы;</li> <li>событий от bj2depo нет, мастер — bj (<code>DEPO_ORDER_CREATED</code> / <code>DEPO_ERROR</code> отменены);</li>
<li>события <code>BJ_QUIK_*</code> bj пишет для истории; джоба помечает их SUCCESS без действий.</li> <li>счёт клиента — <code>FTV_M2M_LINES.DEPO_NUMBER</code> (bj пишет номер ВТНКЦ… с 16.09).</li>
</ul> </ul>
<p class="muted">Закрыто 15.09.2026: <code>REFERENCE</code> для всех исходящих присваивает bj (<code>M2M…</code>); джоба <code>bj2depo</code> на SQL002 / <code>BJ_DEPO_DATA</code> работает; <code>BJ_QUIK_*</code> — для истории, без действий.</p>
<p>Остаётся:</p> <p>Остаётся:</p>
<ol> <ol>
<li><strong>Создать таблицу <code>FTV_M2M_HANDBOOK</code></strong> в BJ_DEPO_DATA по скрипту из раздела «Справочник участников».</li> <li><strong>События без <code>REFERENCE</code></strong> (по документу целиком: <code>BJ_RECEIVED</code>, <code>BJ_SENT</code>, <code>BJ_ORDER_SENT</code>, <code>BJ_ORDER_ACCEPTED</code>, <code>BJ_EXECUTED</code>, <code>BJ_QUIK_*</code>) джоба помечает ERROR «Не найдена строка M2M-поручение по GUID + REFERENCE // FTV_M2M_PROCESS_EVENTS // 69» — на 17.09 (перевод D744A410) так же. Для них строку искать не нужно: документ — по <code>GUID</code>.</li>
<li>Действия джобы по событиям (раздел «Действия джобы»): движение по счёту депо по <code>BJ_ORDER_ACCEPTED</code> / <code>BJ_EXECUTED</code>, запись отказов в строки и шаг «Отмена».</li> <li>Запись отказов в строки и шаг «Отмена» — в работе у программиста.</li>
<li>Создание документа «Списание / Зачисление» — доработка у программиста.</li>
<li>«Формирование отчёта» после «Обработано»: вручную по рассылке или лицензия на автомат документооборота — решение депозитария.</li>
<li>Место хранения: <code>PLACE</code> по <code>PLACE_NUMBER</code> = <code>36MC0413600000F00</code> не проставляется; в DEPO запись называется <code>HL171004001C/…/36MC0413600000F00</code> — в каком виде передавать?</li>
</ol> </ol>
</article> </article>
</div> </div>