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
@@ -0,0 +1,77 @@
package lkgateway
import (
"testing"
"time"
)
// Строчные события (договорённость с Fancy 18.09.2026): пакет исполняется по бумагам,
// состояние объекта складывается из состояний строк.
func TestOrderWatchLinesPartial(t *testing.T) {
now := time.Now()
w := &OrderWatch{GUID: "G1", Lines: map[string]*OrderLineWatch{
"cd-sber": {Ref: "R1", ISIN: "RU0009029540", MS010At: now}, // исполнено
"cd-tech": {Ref: "R2", ISIN: "RU000A106YF0", MS010At: now, Rejected: true}, // отказ
"cd-gaz": {Ref: "R3", ISIN: "RU0007661625"}, // ждём
}}
lines := []orderLine{{ContrDID: "cd-sber", Ref: "R1"}, {ContrDID: "cd-tech", Ref: "R2"}, {ContrDID: "cd-gaz", Ref: "R3"}}
if allLinesMS010(w, lines) {
t.Error("не все строки получили MS010 — объект не должен считаться закрытым")
}
total, executed, rejected, expired := w.linesSummary()
if total != 3 || executed != 1 || rejected != 1 || expired != 0 {
t.Errorf("сводка строк: total=%d executed=%d rejected=%d expired=%d", total, executed, rejected, expired)
}
if got := w.StatusRu(); got != "исполнено частично: 1 из 3 бумаг, отвергнуто 1, ожидаем MS010 по 1" {
t.Errorf("частичный статус: %q", got)
}
if w.Kind() != "warn" {
t.Errorf("частичное исполнение должно быть warn, got %q", w.Kind())
}
// все строки закрыты → объект закрыт
w.Lines["cd-gaz"].MS010At = now
if !allLinesMS010(w, lines) {
t.Error("все строки с MS010 — объект должен быть закрыт")
}
// строка, истёкшая по сроку, тоже финальна
if !(&OrderLineWatch{ExpiredAt: now}).lineDone() || (&OrderLineWatch{}).lineDone() || (*OrderLineWatch)(nil).lineDone() {
t.Error("lineDone: истёкшая строка финальна, пустая/nil — нет")
}
}
// Строки входящего 16/1: новые записи хранят все contr_d_id с REFERENCE; старые — только первый.
func TestIncomingOrderLines(t *testing.T) {
it := &IncomingTransfer{OrderLines: []IncomingOrderLine{
{ContrDID: "c1", ISIN: "RU0009029540", Ref: "R1"},
{ContrDID: "c2", ISIN: "RU000A106YF0", Ref: "R2"},
}}
got := incomingOrderLines(it)
if len(got) != 2 || got[0].ContrDID != "c1" || got[0].Ref != "R1" || got[1].Ref != "R2" {
t.Errorf("строки нового формата: %+v", got)
}
legacy := &IncomingTransfer{OrderContrDID: "c-old"}
if got := incomingOrderLines(legacy); len(got) != 1 || got[0].ContrDID != "c-old" || got[0].Ref != "" {
t.Errorf("старая запись: одна строка без REFERENCE, got %+v", got)
}
if incomingOrderLines(&IncomingTransfer{}) != nil || incomingOrderLines(nil) != nil {
t.Error("без поручений строк нет")
}
}
// Строки исходящего 16 без репозитория: contr_d_id/ISIN из черновиков, REFERENCE пустой.
func TestOutgoingLineRefsNoRepo(t *testing.T) {
od := &OutgoingDeal{GUID: "G2", Secs: []OutgoingSec{
{ISIN: "ru0009029540", ContrDID: "cd1"},
{ISIN: "RU000A106YF0", ContrDID: ""}, // без номера поручения — не подавалась
{ISIN: "RU0007661625", ContrDID: "cd3"},
}}
got := outgoingLineRefs(t.Context(), nil, od)
if len(got) != 2 || got[0].ContrDID != "cd1" || got[0].ISIN != "RU0009029540" || got[1].ContrDID != "cd3" {
t.Errorf("строки исходящего: %+v", got)
}
if outgoingLineRefs(t.Context(), nil, nil) != nil {
t.Error("nil-сделка → нет строк")
}
}