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:
zuevav
2026-09-22 16:27:52 +03:00
parent e16d4454ce
commit 0c081a7857
9 changed files with 488 additions and 87 deletions
+76 -35
View File
@@ -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
}