Files
Bridge-and-Join-s/internal/lkgateway/claimnotsent_render_test.go
T
fontvielle c476cfd7a3 ЛК-карточка и M2M: операторский путь заявки, баннеры «в НРД не отправлено», дубль-поручение, idempotency, фикс DEPO-алертов
- Карточка заявки: операторский «Путь заявки» (claimTimelineOperator) короткими
  понятными вехами + сворачиваемый технический журнал (buildClaimTimeline);
  QUIK показан пошагово (register/sign/deny/deposit) с верными временами, отказ
  регистрации больше не выглядит успехом (quikActionFailTitles).
- Баннеры: «в НРД не отправлено», когда пакет сформирован, но не ушёл (нет
  submitted_to_nsd и не Fail); заголовок карточки и бейдж учитывают это; красный
  баннер QUIK при отказе, строгий «нет бумаг» только при реальном шорт/недостатке.
- НРД-ответ «Поручение с исходящим номером … уже существует» теперь REG_DUP
  (предупреждение-дубль, не красный «Отказ в регистрации»); не влияет на od.Rejected.
- Idempotency: submitguard.go (inflightSubmit) + защита точек подачи от
  двойного срабатывания (manual + auto-loop, двойной клик): submitOutgoingFor,
  submitIncomingOrderFor, QuikOutRegister (ранний возврат при registered/signed).
- DEPO-алерты: водяной знак depoFailAlertedID засеивается текущим MAX(ID) на
  старте процесса (Hlth03) — после рестарта bj больше не поднимает повторно
  алерты про уже известные ошибки обработки событий BJ_*.
- Тесты: claimnotsent_render_test.go (хелперы + рендер карточки).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XG3zNMMf2LMoDpXo22mwMX
2026-10-15 17:47:28 +03:00

101 lines
5.1 KiB
Go

package lkgateway
import (
"bytes"
"strings"
"testing"
"time"
)
// buildClaimDataFromView повторяет вычисления renderClaim для теста.
func buildClaimDataFromView(v ClaimView, quik []*QuikLink, fail *SendFailure) claimData {
qfISIN, qfReply := quikRegisterFailure(quik)
return claimData{
page: page{Title: "Заявка"},
Claim: v, Quik: quik, Fail: fail,
SentToNSD: claimSentToNSD(v), QuikFailISIN: qfISIN, QuikFailReply: qfReply,
QuikNoSec: quikReplyIsNoSecurities(qfReply),
}
}
func TestClaimHelpers(t *testing.T) {
sent := ClaimView{Stages: []StageView{{State: "draft"}, {State: "validated"}, {State: "submitted_to_nsd"}, {State: "awaiting_decision"}}}
if !claimSentToNSD(sent) {
t.Fatal("submitted_to_nsd должна считаться отправкой в НРД")
}
notSent := ClaimView{Stages: []StageView{{State: "draft"}, {State: "validated"}, {State: "rejected"}}}
if claimSentToNSD(notSent) {
t.Fatal("validated→rejected без submitted_to_nsd — в НРД не отправлялось")
}
for _, r := range []string{"Данный инструмент запрещен для операции шорт", "Недостаточно ценных бумаг", "короткая позиция"} {
if !quikReplyIsNoSecurities(r) {
t.Fatalf("ответ %q должен распознаваться как нехватка бумаг", r)
}
}
if quikReplyIsNoSecurities("Неизвестный депозитарий получателя") {
t.Fatal("ошибка про депозитарий — не нехватка бумаг")
}
links := []*QuikLink{{GUID: "g", ISIN: "RU0009029540", State: "error",
Err: "МНП отверг регистрацию: Данный инструмент запрещен для операции шорт",
Steps: []QuikStep{{Action: "register", Reply: "Данный инструмент запрещен для операции шорт", OK: false, At: time.Now()}}}}
isin, reply := quikRegisterFailure(links)
if isin != "RU0009029540" || !strings.Contains(reply, "шорт") {
t.Fatalf("quikRegisterFailure вернул %q / %q", isin, reply)
}
}
// Отклонена до отправки + QUIK сообщил о нехватке бумаг (как e3607d2a).
func TestClaimRenderNotSentNoSec(t *testing.T) {
a, err := newAdmin()
if err != nil {
t.Fatal(err)
}
v := ClaimView{ID: "e3607d2a-2608-441b-b49a-e99ef3e4edd1", Status: "rejected",
RequestRawXML: "<M2MTransferRequest/>",
Stages: []StageView{{State: "draft"}, {State: "validated"}, {State: "rejected"}}}
quik := []*QuikLink{{GUID: "67f09cec", ISIN: "RU0009029540", State: "error",
Err: "МНП отверг регистрацию: Данный инструмент запрещен для операции шорт",
Steps: []QuikStep{{Action: "register", Reply: "Данный инструмент запрещен для операции шорт", OK: false, At: time.Now()}}}}
var b bytes.Buffer
if err := a.claim.ExecuteTemplate(&b, "layout", buildClaimDataFromView(v, quik, nil)); err != nil {
t.Fatal(err)
}
s := b.String()
for _, want := range []string{
"В НРД ничего не отправлено", // баннер отмены до отправки
"Сформированный пакет (M2MTransferRequest)", // заголовок карточки
"в НРД не отправлен", // бейдж
"нет (недостаточно) бумаг", // карточка QUIK: нехватка
"Данный инструмент запрещен для операции шорт", // дословный ответ QUIK
} {
if !strings.Contains(s, want) {
t.Fatalf("нет %q", want)
}
}
if strings.Contains(s, "Отправленный пакет (M2MTransferRequest)") {
t.Fatal("для неотправленной заявки не должно быть заголовка «Отправленный пакет»")
}
}
// Нормально отправленная заявка — старый заголовок сохраняется, баннеров нет.
func TestClaimRenderSentOK(t *testing.T) {
a, err := newAdmin()
if err != nil {
t.Fatal(err)
}
v := ClaimView{ID: "11111111-2222-3333-4444-555555555555", Status: "awaiting_decision",
RequestRawXML: "<M2MTransferRequest/>",
Stages: []StageView{{State: "draft"}, {State: "validated"}, {State: "submitted_to_nsd"}, {State: "awaiting_decision"}}}
var b bytes.Buffer
if err := a.claim.ExecuteTemplate(&b, "layout", buildClaimDataFromView(v, nil, nil)); err != nil {
t.Fatal(err)
}
s := b.String()
if !strings.Contains(s, "Отправленный пакет (M2MTransferRequest)") {
t.Fatal("отправленная заявка должна показывать «Отправленный пакет»")
}
if strings.Contains(s, "В НРД ничего не отправлено") || strings.Contains(s, "в НРД не отправлен") {
t.Fatal("для отправленной заявки баннеры «не отправлено» показывать нельзя")
}
}