0c081a7857
По ответам программиста 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
78 lines
3.8 KiB
Go
78 lines
3.8 KiB
Go
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-сделка → нет строк")
|
||
}
|
||
}
|