c476cfd7a3
- Карточка заявки: операторский «Путь заявки» (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
101 lines
5.1 KiB
Go
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("для отправленной заявки баннеры «не отправлено» показывать нельзя")
|
|
}
|
|
}
|