ЛК: итог «Исполнена» по MS010, GUID без учёта регистра, заявка при отказе МНП берётся один раз; тестовый контур не трогает боевой ЛК
Перевод 97EAD00C (заявка ЛК #71741, 23.10.2026) исполнен в QUIK и НРД (MS010), но в ЛК статус не изменился, а сделка осталась «Подтверждена»: - связки guid → заявка ЛК искались с учётом регистра: мы шлём GUID строчными, НРД отвечает заглавными — OnLKOutcome молча не находил заявку. Ключи — строчными (lkGUIDKey), старые записи приводятся при загрузке; итог модуля ЛК (lk.transfer_requests) — lower(guid); - после MS010 по всем строкам поручения 16 сделка → «Исполнена» (CompleteSUB16), итог — в ЛК (outOnExecuted, каждый проход order-watch, повтор безвреден); в бою согласие контрагента — ещё не «Исполнена»: статус ждёт MS010, как в тесте «боевой ЛК → TEST3»; имитатор НРД — итог сразу. Тестовый 28 с боевым ЛК взял ту же заявку и после отказа МНП («запрещен для операции шорт» — бумагу уже заблокировал 9) брал её каждые 10 секунд: 108 сделок с новыми GUID и столько же писем. - Сделка заведена, а отправка не прошла (МНП не заблокировал) — заявка помечается взятой и связывается с переводом: дальше — оператор в карточке. - Боевой ЛК при тестовом профиле ИШ (lkProdOnTestNSD): заявки клиентов не берутся (кроме теста «боевой ЛК → тестовый НРД»), портфели не заполняются, предупреждение в настройках ЛК. - Письма: в теме и первой строке — сервер bj и контур НРД («[bj ТЕСТ …]» у тестового); письма о входящих — метка только у тестового. - История FSM — по МСК с датой, «текущее состояние» вместо «сейчас». - Карточка теста «боевой ЛК → тестовый НРД» — только в режиме разработки (включённый тест виден всегда, чтобы его выключить). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
|
||||
)
|
||||
|
||||
// lkLinksTemp — связки ЛК во временном каталоге, состояние в памяти сброшено.
|
||||
func lkLinksTemp(t *testing.T, links string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
oldL, oldO, oldR := lkLinksFile, lkOriginFile, realTestFile
|
||||
lkLinksFile, lkOriginFile, realTestFile = filepath.Join(dir, "links.json"), filepath.Join(dir, "origin.json"), filepath.Join(dir, "realtest.json")
|
||||
if links != "" {
|
||||
if err := os.WriteFile(lkLinksFile, []byte(links), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
reset := func() {
|
||||
lkLinksMu.Lock()
|
||||
lkLinks, lkLoaded = map[string]int{}, false
|
||||
lkLinksMu.Unlock()
|
||||
lkOriginMu.Lock()
|
||||
lkOrigin, lkOriginLoaded = map[string]int{}, false
|
||||
lkOriginMu.Unlock()
|
||||
realTests.Lock()
|
||||
realTests.loaded, realTests.m = false, nil
|
||||
realTests.Unlock()
|
||||
}
|
||||
reset()
|
||||
t.Cleanup(func() {
|
||||
lkLinksFile, lkOriginFile, realTestFile = oldL, oldO, oldR
|
||||
reset()
|
||||
})
|
||||
}
|
||||
|
||||
// НРД отвечает GUID заглавными, мы шлём строчными (23.10.2026: статус в ЛК по 97EAD00C не ушёл).
|
||||
func TestLKLinksIgnoreGUIDCase(t *testing.T) {
|
||||
lkLinksTemp(t, `{"97ead00c-fd0d-49ab-85a1-15eb49f3201a":71741}`) // запись до исправления
|
||||
up := "97EAD00C-FD0D-49AB-85A1-15EB49F3201A"
|
||||
if !lkLinked(up) {
|
||||
t.Fatal("связка не найдена по GUID заглавными")
|
||||
}
|
||||
lkRecordOrigin("97ead00c-fd0d-49ab-85a1-15eb49f3201a", 71741)
|
||||
if id, ok := LKClaimIDByGUID(up); !ok || id != 71741 {
|
||||
t.Fatalf("происхождение по GUID заглавными: %d %v", id, ok)
|
||||
}
|
||||
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
OnLKOutcome(context.Background(), rc, up, "done", "Перевод исполнен") // ЛК не настроен — только закрытие связки
|
||||
if lkLinked(up) {
|
||||
t.Fatal("после итога связка должна закрыться")
|
||||
}
|
||||
}
|
||||
|
||||
// MS010 по нашему поручению 16: сделка «Подтверждена» → «Исполнена», итог — в ЛК.
|
||||
func TestOutOnExecuted(t *testing.T) {
|
||||
lkLinksTemp(t, "")
|
||||
ctx := context.Background()
|
||||
repo := m2mcore.NewMemoryRepository()
|
||||
svc := NewService(Config{Repository: repo, Sender: &flakySender{}})
|
||||
guid := "c02a1d5e-c2af-4799-bab4-953f133c5133"
|
||||
req := sendFailRequest()
|
||||
now := time.Now()
|
||||
if _, err := repo.Create(ctx, &m2mcore.Deal{ID: "d1", GUID: m2m.UUID(guid), State: m2mcore.StateConfirmed, Request: req, CreatedAt: now, UpdatedAt: now}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lkLinkGUID(guid, LKEndpoint{}, 71741)
|
||||
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := &OrderWatch{GUID: guid, Direction: "out", MS010At: now, Lines: map[string]*OrderLineWatch{"42391": {ISIN: "RU0009029540", MS010At: now}}}
|
||||
outOnExecuted(ctx, rc, svc, guid, w)
|
||||
d, _ := repo.GetByGUID(ctx, m2m.UUID(guid))
|
||||
if d.State != m2mcore.StateDone {
|
||||
t.Fatalf("сделка: %s, нужно done", d.State)
|
||||
}
|
||||
if lkLinked(guid) {
|
||||
t.Fatal("итог в ЛК не передан — связка не закрыта")
|
||||
}
|
||||
outOnExecuted(ctx, rc, svc, guid, w) // повтор каждый проход — без ошибок и изменений
|
||||
if d, _ := repo.GetByGUID(ctx, m2m.UUID(guid)); d.State != m2mcore.StateDone {
|
||||
t.Fatalf("после повтора: %s", d.State)
|
||||
}
|
||||
}
|
||||
|
||||
// Тестовый контур НРД с боевым ЛК: заявки клиентов не берутся, портфели не заполняются.
|
||||
func TestLKProdOnTestNSD(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
set := func(env, profile string) bool {
|
||||
lk := rc.Snapshot().LK
|
||||
lk.ActiveEnv = env
|
||||
if err := rc.UpdateLK(lk); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := rc.Snapshot().NSD
|
||||
n.Profile = profile
|
||||
if err := rc.UpdateNSD(n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return lkProdOnTestNSD(rc)
|
||||
}
|
||||
if !set("prod", "test3-gost") || set("prod", "prod-gost") || set("test", "test3-gost") {
|
||||
t.Fatal("боевой ЛК + тестовый контур — только эта пара")
|
||||
}
|
||||
if tag, line := alertServer("test3-gost"); !strings.HasPrefix(tag, "[bj ТЕСТ ") || !strings.Contains(line, "ТЕСТОВЫЙ контур") {
|
||||
t.Fatalf("метка тестового сервера: %q %q", tag, line)
|
||||
}
|
||||
if tag, _ := alertServer("prod-gost"); strings.Contains(tag, "ТЕСТ") {
|
||||
t.Fatalf("метка боевого сервера: %q", tag)
|
||||
}
|
||||
}
|
||||
|
||||
// История FSM — по МСК (время хранится в UTC), текущее состояние подписано.
|
||||
func TestClaimStagesMSK(t *testing.T) {
|
||||
a, err := newAdmin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in := time.Date(2026, 10, 23, 12, 17, 19, 502000000, time.UTC)
|
||||
left := in.Add(time.Minute)
|
||||
rec := httptest.NewRecorder()
|
||||
render(rec, a.claim, claimData{page: nowPage("Заявка", "claims"), Claim: ClaimView{ID: "0123456789abcdef", Status: "confirmed",
|
||||
Stages: []StageView{{State: "awaiting_decision", EnteredAt: in, LeftAt: &left}, {State: "confirmed", EnteredAt: left}}}})
|
||||
body := rec.Body.String()
|
||||
for _, want := range []string{"23.10 15:17:19.502", "23.10 15:18:19.502", "текущее состояние"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("в истории нет %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(body, "12:17:19") {
|
||||
t.Error("время в UTC")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user