DEPO: строчные события по депо-поручениям (частичное исполнение пакета)
По ответам программиста Fancy (В. Кокин, 21.09.2026) на 8 вопросов из docs/fancy-events-partial-m2m.md. Одно депо-поручение (MF010) = одна бумага пакета, поэтому подача и исход теперь пишутся в DEPO ПО СТРОКАМ с REFERENCE и сразу по MS010 этой строки, не дожидаясь остальных: пакет может исполниться частично (Сбер — да, Т-Технологии — отказ). События на весь GUID (пустой REFERENCE) больше не пишем — переход одновременно с bj2depo. - BJ_ORDER_SENT / BJ_ORDER_ACCEPTED / BJ_EXECUTED / BJ_ORDER_REJECTED / BJ_ORDER_LATE / BJ_CANCELLED — по строке (depoOnOrderLineSent/Result); contr_d_id кладём в COMMENT (джобе связка не нужна — по REFERENCE найдёт). - OrderWatch.Lines — состояние по строкам (contr_d_id → REFERENCE/ISIN/MS010/ отказ/истечение); объект закрыт, когда MS010 по всем строкам; 30 дней — BJ_CANCELLED по каждой незакрытой строке, QUIK-блок снимается по её ISIN. - Входящий 16/1 хранит ВСЕ contr_d_id (OrderLines) — раньше только первый (дефект «входящие отслеживают только первый contr_d_id»). Исходящий 16: REFERENCE по ISIN из бумаг M2MTR (outgoingLineRefs, кэш по GUID; svc в deps). - Карточка: «исполнено частично: N из M» (StatusRu), бейдж warn — как статус «частично» в DEPO. Backfill и эмуляция — тоже по строкам. - Попутно: EXEC процедуры обработчика DEPO — через execProc с проверкой имени (sqlident.go), чтобы значение из настроек не стало произвольным T-SQL. Тесты: TestOrderWatchLinesPartial, TestIncomingOrderLines, TestOutgoingLineRefsNoRepo; весь пакет зелёный. РАЗВЁРТЫВАНИЕ — только одновременно с bj2depo (п. 8 ответов Fancy). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -380,23 +380,44 @@ func depoOnServiceError(rc *RuntimeConfig, guid, codes, reason string) {
|
||||
depoEvent(rc, guid, "", "BJ_SERVICE_ERROR", strings.ReplaceAll(strings.TrimSpace(codes), " ", ";"), reason)
|
||||
}
|
||||
|
||||
// СТРОЧНЫЕ события по депо-поручениям (договорённость с Fancy 18.09.2026, ответы
|
||||
// В. Кокина): одно депо-поручение = одна бумага пакета, поэтому подача и исход
|
||||
// пишутся в DEPO ПО СТРОКАМ с REFERENCE и сразу по MS010 этой строки, не дожидаясь
|
||||
// остальных — пакет может исполниться частично. Связка contr_d_id↔REFERENCE джобе
|
||||
// bj2depo не нужна (по REFERENCE она находит всё); contr_d_id кладём в COMMENT для
|
||||
// разбора. Событие на весь GUID (пустой REFERENCE) больше не пишем: переход на
|
||||
// строчные — одновременно на bj и bj2depo.
|
||||
|
||||
// depoOnOrderLineSent — подано депо-поручение по строке (REFERENCE), contr_d_id — в комментарии.
|
||||
func depoOnOrderLineSent(rc *RuntimeConfig, guid, ref, contrDID string) {
|
||||
depoEvent(rc, guid, ref, "BJ_ORDER_SENT", "", contrDID)
|
||||
}
|
||||
|
||||
// depoOnOrderLineResult — исход по строке: accepted | rejected | late | expired.
|
||||
// accepted → BJ_ORDER_ACCEPTED + BJ_EXECUTED; rejected → BJ_ORDER_REJECTED;
|
||||
// late → BJ_ORDER_LATE (5 мин без MS010); expired → BJ_CANCELLED (30 дней по бумаге).
|
||||
func depoOnOrderLineResult(rc *RuntimeConfig, guid, ref, kind, text string) {
|
||||
switch kind {
|
||||
case "accepted":
|
||||
depoEvent(rc, guid, ref, "BJ_ORDER_ACCEPTED", "", "")
|
||||
depoEvent(rc, guid, ref, "BJ_EXECUTED", "", "")
|
||||
case "rejected":
|
||||
depoEvent(rc, guid, ref, "BJ_ORDER_REJECTED", "", text)
|
||||
case "late":
|
||||
depoEvent(rc, guid, ref, "BJ_ORDER_LATE", "", "")
|
||||
case "expired":
|
||||
depoEvent(rc, guid, ref, "BJ_CANCELLED", "", "")
|
||||
}
|
||||
}
|
||||
|
||||
// depoOnOrderSent / depoOnOrderResult — прежний формат «на весь GUID» (пустой
|
||||
// REFERENCE). Оставлены для совместимости вызовов; новые пути пишут по строкам.
|
||||
func depoOnOrderSent(rc *RuntimeConfig, guid string, contrDIDs []string, form string) {
|
||||
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", "", "")
|
||||
depoEvent(rc, guid, "", "BJ_EXECUTED", "", "")
|
||||
case "rejected":
|
||||
depoEvent(rc, guid, "", "BJ_ORDER_REJECTED", "", text)
|
||||
case "late":
|
||||
depoEvent(rc, guid, "", "BJ_ORDER_LATE", "", "")
|
||||
case "expired":
|
||||
depoEvent(rc, guid, "", "BJ_CANCELLED", "", "")
|
||||
}
|
||||
depoOnOrderLineResult(rc, guid, "", kind, text)
|
||||
}
|
||||
|
||||
func depoOnQuik(rc *RuntimeConfig, guid, isin, evt, instrNo string) {
|
||||
@@ -627,7 +648,7 @@ func depoJobOnce(rc *RuntimeConfig) {
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
_, err = db.ExecContext(ctx, "exec "+proc)
|
||||
_, err = execProc(ctx, db, proc)
|
||||
res := fmt.Sprintf("%s: OK за %s", start.Format("02.01 15:04:05"), time.Since(start).Round(time.Millisecond))
|
||||
if err != nil {
|
||||
res = fmt.Sprintf("%s: ОШИБКА %s", start.Format("02.01 15:04:05"), quikErrorDetail(err))
|
||||
@@ -743,7 +764,7 @@ func depoJobCheck(ctx context.Context, rc *RuntimeConfig) (string, bool) {
|
||||
return "у логина " + f.User + " нет права EXECUTE на " + depoJobProcName(f) + " — выполните на сервере: " + strings.ReplaceAll(depoJobGrantScript(f), "\n", " "), false
|
||||
}
|
||||
start := time.Now()
|
||||
if _, err := db.ExecContext(ctx, "exec "+depoJobProcName(f)); err != nil {
|
||||
if _, err := execProc(ctx, db, depoJobProcName(f)); err != nil {
|
||||
return "вызов " + depoJobProcName(f) + ": " + quikErrorDetail(err), false
|
||||
}
|
||||
return fmt.Sprintf("право есть, процедура %s выполнена за %s; bj будет вызывать её каждые %d с", depoJobProcName(f), time.Since(start).Round(time.Millisecond), depoJobEvery(f)), true
|
||||
@@ -867,15 +888,18 @@ func depoReplayHistory(ctx context.Context, rc *RuntimeConfig, svc *Service, onl
|
||||
}
|
||||
}
|
||||
if it.OrderSubmitted {
|
||||
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)
|
||||
} else {
|
||||
ev(at, it.GUID, "", "BJ_ORDER_ACCEPTED", "", "")
|
||||
ev(at, it.GUID, "", "BJ_EXECUTED", "", "")
|
||||
// по строкам: у каждой бумаги своё поручение 16/1 и свой MS010
|
||||
for _, ln := range incomingOrderLines(it) {
|
||||
ev(it.OrderAt, it.GUID, ln.Ref, "BJ_ORDER_SENT", "", ln.ContrDID)
|
||||
resps := findOrderResponses(ln.ContrDID)
|
||||
if done, rejected, text := orderRespVerdict(resps); done {
|
||||
at := orderRespAt(resps)
|
||||
if rejected {
|
||||
ev(at, it.GUID, ln.Ref, "BJ_ORDER_REJECTED", "", text)
|
||||
} else {
|
||||
ev(at, it.GUID, ln.Ref, "BJ_ORDER_ACCEPTED", "", "")
|
||||
ev(at, it.GUID, ln.Ref, "BJ_EXECUTED", "", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -934,15 +958,26 @@ func depoReplayHistory(ctx context.Context, rc *RuntimeConfig, svc *Service, onl
|
||||
}
|
||||
if globalOutgoingStore != nil {
|
||||
if od, ok := scanForm16Deal(globalOutgoingStore, guid); ok && od.Submitted {
|
||||
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)
|
||||
} else {
|
||||
ev(at, guid, "", "BJ_ORDER_ACCEPTED", "", "")
|
||||
ev(at, guid, "", "BJ_EXECUTED", "", "")
|
||||
// по строкам: один черновик MF010 = одна бумага; REFERENCE — по ISIN из M2MTR
|
||||
isinRef := map[string]string{}
|
||||
for ref, isin := range refISIN {
|
||||
isinRef[strings.ToUpper(isin)] = ref
|
||||
}
|
||||
for _, sec := range od.Secs {
|
||||
if sec.ContrDID == "" {
|
||||
continue
|
||||
}
|
||||
ref := isinRef[strings.ToUpper(sec.ISIN)]
|
||||
ev(od.OrderAt, guid, ref, "BJ_ORDER_SENT", "", sec.ContrDID)
|
||||
resps := findOrderResponses(sec.ContrDID)
|
||||
if done, rejected, text := orderRespVerdict(resps); done {
|
||||
at := orderRespAt(resps)
|
||||
if rejected {
|
||||
ev(at, guid, ref, "BJ_ORDER_REJECTED", "", text)
|
||||
} else {
|
||||
ev(at, guid, ref, "BJ_ORDER_ACCEPTED", "", "")
|
||||
ev(at, guid, ref, "BJ_EXECUTED", "", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -998,10 +1033,16 @@ func depoSimulateIncoming(ctx context.Context, rc *RuntimeConfig, it *IncomingTr
|
||||
depoEventAt(rc, now.Add(time.Minute), guid, s.ReferenceID, "BJ_DECISION_CONFIRM", "", "")
|
||||
}
|
||||
}
|
||||
// Исходы — по строкам (REFERENCE), как в бою: по каждой принятой бумаге своё поручение.
|
||||
if anyConfirmed {
|
||||
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", "", "")
|
||||
for _, s := range it.Securities {
|
||||
if s.Decision == "rejected" {
|
||||
continue
|
||||
}
|
||||
depoEventAt(rc, now.Add(2*time.Minute), guid, s.ReferenceID, "BJ_ORDER_SENT", "", "")
|
||||
depoEventAt(rc, now.Add(3*time.Minute), guid, s.ReferenceID, "BJ_ORDER_ACCEPTED", "", "")
|
||||
depoEventAt(rc, now.Add(3*time.Minute), guid, s.ReferenceID, "BJ_EXECUTED", "", "")
|
||||
}
|
||||
}
|
||||
return guid, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user