Files
Bridge-and-Join-s/internal/lkgateway/orderwatch_lines_test.go
zuevav 0c081a7857 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
2026-09-22 16:27:52 +03:00

78 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-сделка → нет строк")
}
}