ЛК-карточка и 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:
fontvielle
2026-10-15 17:47:28 +03:00
parent 5beb726e5b
commit c476cfd7a3
12 changed files with 552 additions and 12 deletions
@@ -25,6 +25,11 @@
.tl-quik{--c:#b45309} .tl-ok{--c:#16a34a} .tl-warn{--c:#d97706} .tl-wait{--c:#94a3b8}
/* нет места справа (узкие/средние экраны) — таймлайн в потоке под контентом */
@media (max-width:1640px){ .claim-tl{ position:static; width:auto; max-height:none; margin-top:16px; box-shadow:none } }
.tl-tech{ margin-top:12px; border-top:1px dashed var(--border,#e3e3e8); padding-top:8px }
.tl-tech>summary{ cursor:pointer; font-size:11px; color:var(--muted,#888); list-style:none; user-select:none }
.tl-tech>summary::-webkit-details-marker{ display:none }
.tl-tech>summary::before{ content:'▸ '; }
.tl-tech[open]>summary::before{ content:'▾ '; }
</style>
<div class="claim-grid">
<div class="claim-main">
@@ -40,6 +45,10 @@
<b>Перевод не доставлен в НРД</b> — {{sendFailText .}}.<br>
<span class="muted">Попытка {{.At.Format "02.01.2006 15:04:05"}}{{if .PackageID}}, пакет ИШ {{.PackageID}}{{end}}{{if gt .Attempts 1}}, попыток: {{.Attempts}}{{end}}. Отправка в DEPO не отмечена, блокировка в QUIK МНП сохраняется. {{if .GaveUp}}Сутки прошли — автоповтор остановлен: отправьте вручную или удалите заявку.{{else}}Ничего нажимать не нужно: как только НРД снова ответит, bj сам отправит тот же запрос с тем же GUID (повтор — в течение суток). Кнопка «Отправить повторно» — если нужно попробовать прямо сейчас.{{end}}</span>
</div>{{end}}
{{if and .Claim.RequestRawXML (not .SentToNSD) (not .Fail)}}<div class="card" style="background:var(--warn-weak,#fbf3d9);margin:8px 0">
<b>⛔ В НРД ничего не отправлено — отмена до отправки.</b><br>
<span class="muted">Пакет <code>M2MTransferRequest</code> сформирован, но в НРД <b>не направлялся</b>: заявка остановлена на проверке перед отправкой{{if .QuikFailReply}} — в QUIK МНП не удалось заблокировать бумаги{{if .QuikFailISIN}} ({{.QuikFailISIN}}){{end}}{{if .QuikNoSec}}: на счёте клиента нет/недостаточно бумаг для списания{{end}}{{end}}. Регистрация «Вывод ЦБ (M2M)» и блокировка идут в QUIK МНП <u>до</u> отправки в НРД, поэтому поручение 16 и запрос перевода в НРД не создавались. Устраните причину{{if .QuikNoSec}} (зачислите бумаги на счёт клиента){{end}} и отправьте заявку повторно.{{if .QuikFailReply}}<br>Ответ модуля QUIK: <code>{{.QuikFailReply}}</code>{{end}}</span>
</div>{{end}}
{{$l := liveOut .Claim}}<div class="live live-big live-{{$l.Kind}}" data-live="out:{{.Claim.ID}}" data-sig="{{$l.Sig}}"{{if $l.Active}} data-active="1"{{else}} title="Завершено: состояние больше не проверяется"{{end}}>{{$l.Text}}</div>
<table>
<tr><td style="width:200px" class="muted">Создана</td><td>{{.Claim.CreatedAt.Format "02.01.2006 15:04:05"}}</td></tr>
@@ -122,6 +131,17 @@
<h2 style="margin:0">QUIK МНП — блокировка и списание у клиента</h2>
<a href="/admin/quik" class="muted" style="font-size:12px">все поручения →</a>
</div>
{{if .QuikFailReply}}<div style="margin:10px 0 0;padding:12px 14px;border-radius:10px;background:color-mix(in srgb,#dc2626 12%,var(--card));color:#b3261e;font-size:13px">
{{if .QuikNoSec}}
<b>❌ Бумаги на счёте клиента в QUIK не заблокированы — списывать нечего.</b><br>
<span style="font-weight:600">На торговом счёте клиента в QUIK нет (недостаточно) бумаг{{if .QuikFailISIN}} <code>{{.QuikFailISIN}}</code>{{end}} для списания.</span> QUIK проверяет остаток на своей стороне при регистрации поручения и отклонил «Вывод ЦБ (M2M)» (заблокировать больше, чем есть на счёте, нельзя).
{{else}}
<b>❌ Регистрация «Вывод ЦБ (M2M)» в QUIK МНП не прошла — бумаги не заблокированы.</b><br>
<span style="font-weight:600">QUIK отклонил поручение{{if .QuikFailISIN}} по <code>{{.QuikFailISIN}}</code>{{end}}.</span>
{{end}}
Поэтому в НРД по заявке <b>ничего не отправлялось</b>: регистрация и блокировка в QUIK МНП идут <u>до</u> отправки пакета в НРД.<br>
<span class="muted" style="color:inherit;opacity:.85">Ответ модуля QUIK: <code>{{.QuikFailReply}}</code></span>
</div>{{end}}
{{if .DepoL}}<div style="margin:8px 0 0;padding:8px 12px;border-radius:9px;font-size:13px;{{if .DepoL.Errors}}background:color-mix(in srgb,#dc2626 12%,var(--card));color:#b3261e{{else}}background:var(--card-2){{end}}">🗂 DEPO (bj2depo): {{if .DepoL.DocNo}}поручение <b>№{{.DepoL.DocNo}}</b> создано {{msk .DepoL.DocAt}}{{if .DepoL.DocState}} · состояние в DEPO: <b>{{.DepoL.DocState}}</b>{{end}}{{else}}поручение ещё не создано{{end}}{{range .DepoL.Errors}}<div>ошибка: {{.}}</div>{{end}}</div>{{end}}
<p class="muted" style="font-size:12.5px;margin:6px 0 0">При отправке в НРД bj регистрирует в Модуле неторговых поручений «Вывод ЦБ (M2M)» — QUIK блокирует бумаги на позиции клиента. Отказ контрагента или НРД → поручение снимается с причиной (клиент видит её в терминале). Приём НРД нашего поручения 16 → подпись депозитария, бумаги списываются.</p>
@@ -249,8 +269,10 @@
{{if .Claim.RequestRawXML}}
<div class="card">
<h2>Отправленный пакет (M2MTransferRequest){{if .SubstAs}} <span class="badge awaiting_decision">подмена личности</span>{{end}}</h2>
{{if .SubstAs}}
<h2>{{if .SentToNSD}}Отправленный пакет (M2MTransferRequest){{else}}Сформированный пакет (M2MTransferRequest){{end}}{{if .SubstAs}} <span class="badge awaiting_decision">подмена личности</span>{{end}}{{if not .SentToNSD}} <span class="badge rejected">в НРД не отправлен</span>{{end}}</h2>
{{if not .SentToNSD}}
<p class="muted">Этот XML был <b>сформирован</b>, но в НРД <b>не отправлялся</b> — заявка остановлена до отправки{{if not .Fail}} (проверка перед отправкой не пройдена){{else}} (НРД не принял пакет — см. выше){{end}}. Тип пакета <code>#M2MTR</code>.</p>
{{else if .SubstAs}}
<div class="dq-warn" style="margin:0 0 10px;padding:12px 14px;border-radius:8px;background:var(--warn-weak,#fbf3d9)">
<b>⚠ В НРД отправлены ПОДМЕНЁННЫЕ данные, не эти.</b> Режим эмуляции «Подмена личности»: в пакете, ушедшем в НРД, блок инвестора заменён на тест-инвестора <b>{{.SubstAs}}</b>. XML ниже — наша запись с <b>реальными данными клиента</b> (так заявка пришла из ЛК); в НРД реальные данные клиента НЕ отправлялись.
</div>
@@ -373,6 +395,26 @@
{{if $e.Detail}}<div class="tl-detail">{{$e.Detail}}</div>{{end}}
</div>
{{end}}
{{if .Claim.TimelineTech}}
<details class="tl-tech">
<summary>Технический журнал · {{len .Claim.TimelineTech}} шаг.</summary>
<div style="margin-top:8px">
{{range $i, $e := .Claim.TimelineTech}}
{{if $i}}
<div class="tl-arrow tl-{{$e.Kind}}">
<span class="ln"></span><span class="ar"></span>
{{if $e.Since}}<span class="dt">через {{$e.Since}}</span>{{end}}
</div>
{{end}}
<div class="tl-step tl-{{$e.Kind}}">
<div class="tl-time">{{if $e.Time.IsZero}}ожидание{{else}}{{$e.Time.Format "02.01 15:04:05"}} МСК{{end}}</div>
<div class="tl-title">{{$e.Title}}</div>
{{if $e.Detail}}<div class="tl-detail">{{$e.Detail}}</div>{{end}}
</div>
{{end}}
</div>
</details>
{{end}}
</aside>
{{end}}
</div>{{/* /claim-grid */}}