ЛК-карточка и 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
This commit is contained in:
@@ -512,6 +512,63 @@ type claimData struct {
|
||||
SubstAs string
|
||||
Flash string
|
||||
Fail *SendFailure // последняя неудачная отправка в НРД (sendfail.go)
|
||||
// SentToNSD — пакет M2MTransferRequest действительно ушёл в НРД (сделка прошла
|
||||
// стадию submitted_to_nsd). false → заявка остановлена ДО отправки (например,
|
||||
// отказ на проверке в QUIK МНП): в НРД по ней ничего не отправлялось.
|
||||
SentToNSD bool
|
||||
// QuikFailISIN/QuikFailReply — если регистрация «Вывод ЦБ (M2M)» в QUIK МНП не
|
||||
// прошла (бумаги не удалось заблокировать), это ISIN и дословный ответ модуля QUIK.
|
||||
// Пусто — такой неудачи нет. QuikNoSec — ответ модуля однозначно про нехватку/
|
||||
// отсутствие бумаг на счёте клиента (шорт/короткая позиция/недостаточно ЦБ), а не
|
||||
// про иную причину отказа; только тогда показываем «нет бумаг».
|
||||
QuikFailISIN string
|
||||
QuikFailReply string
|
||||
QuikNoSec bool
|
||||
}
|
||||
|
||||
// quikReplyIsNoSecurities — ответ модуля QUIK об отказе в регистрации означает,
|
||||
// что на счёте клиента нет/недостаточно бумаг для блокировки (списания). QUIK не
|
||||
// возвращает остаток, но на попытку заблокировать больше, чем есть, отвечает про
|
||||
// «шорт»/«короткую позицию»/«недостаточно ЦБ».
|
||||
func quikReplyIsNoSecurities(reply string) bool {
|
||||
r := strings.ToLower(reply)
|
||||
for _, s := range []string{"шорт", "короткая позиц", "недостаточно", "нет бумаг", "нет цб", "не хватает"} {
|
||||
if strings.Contains(r, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// claimSentToNSD — прошла ли сделка стадию отправки пакета в НРД.
|
||||
func claimSentToNSD(v ClaimView) bool {
|
||||
for _, st := range v.Stages {
|
||||
if st.State == "submitted_to_nsd" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// quikRegisterFailure — если по заявке была неуспешная регистрация «Вывод ЦБ (M2M)»
|
||||
// в QUIK МНП (бумаги не заблокированы), возвращает ISIN и дословный ответ модуля.
|
||||
// Это достоверное подтверждение «на счёте клиента нет/недостаточно бумаг»: проверку
|
||||
// достаточности делает сам QUIK при sp_NewInstrSec (в БД МНП остатков нет).
|
||||
func quikRegisterFailure(links []*QuikLink) (isin, reply string) {
|
||||
for _, l := range links {
|
||||
if l == nil || (l.State != "error" && l.State != "manual") {
|
||||
continue
|
||||
}
|
||||
for i := len(l.Steps) - 1; i >= 0; i-- {
|
||||
if st := l.Steps[i]; st.Action == "register" && !st.OK && strings.TrimSpace(st.Reply) != "" {
|
||||
return l.ISIN, strings.TrimSpace(st.Reply)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(l.Err) != "" {
|
||||
return l.ISIN, strings.TrimSpace(l.Err)
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// statusData — данные страницы статуса.
|
||||
@@ -741,9 +798,14 @@ func (a *admin) renderClaim(w http.ResponseWriter, r *http.Request, svc *Service
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
view.Timeline = buildClaimTimeline(r.Context(), view)
|
||||
if last := timelineLast(view.Timeline); last.After(view.UpdatedAt) {
|
||||
view.UpdatedAt = last // «Обновлена» — последнее событие по заявке, а не последняя смена состояния сделки
|
||||
// На экране: краткий операторский путь (ключевые вехи), полный технический —
|
||||
// под сворачиваемым «Технический журнал». «Обновлена» — по последнему событию
|
||||
// полного пути (а не последней смене состояния сделки).
|
||||
full := buildClaimTimeline(r.Context(), view)
|
||||
view.TimelineTech = full
|
||||
view.Timeline = claimTimelineOperator(r.Context(), view)
|
||||
if last := timelineLast(full); last.After(view.UpdatedAt) {
|
||||
view.UpdatedAt = last
|
||||
}
|
||||
var depo *OutgoingDeal
|
||||
if view.M2MGUID != "" && globalOutgoingStore != nil {
|
||||
@@ -773,10 +835,13 @@ func (a *admin) renderClaim(w http.ResponseWriter, r *http.Request, svc *Service
|
||||
substAs = rt.As // в НРД ушли данные тест-инвестора вместо клиента (режим «подмена»)
|
||||
}
|
||||
}
|
||||
qfISIN, qfReply := quikRegisterFailure(quik)
|
||||
render(w, a.claim, claimData{
|
||||
Quik: quik, Watch: watch, DepoL: depoL, Fail: fail,
|
||||
page: nowPage("Заявка", "claims"),
|
||||
Claim: view, Depo: depo, SubstAs: substAs, Flash: r.URL.Query().Get("flash"),
|
||||
SentToNSD: claimSentToNSD(view), QuikFailISIN: qfISIN, QuikFailReply: qfReply,
|
||||
QuikNoSec: quikReplyIsNoSecurities(qfReply),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user