Files
Bridge-and-Join-s/internal/lkgateway/web/templates/admin_claim.html
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

422 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "content"}}
<style>
/* Основной контент — в обычном потоке (широкий, по центру). Таймлайн —
отдельной узкой панелью, прижатой к правому краю экрана (fixed) на широких
мониторах; на узких уходит под контент. */
.claim-grid { display:block }
.claim-main { min-width:0 }
.claim-tl { position: fixed; right: 18px; top: 84px; width: 216px; max-height: 84vh; overflow:auto;
background: var(--card,#fff); border:1px solid var(--border,#e3e3e8);
border-radius:14px; padding:14px 16px; box-shadow:var(--shadow-lg,0 8px 30px rgba(0,0,0,.10)); z-index:40; }
.claim-tl h3 { margin:0 0 12px; font-size:13px }
.tl-step { position:relative; padding:0 0 2px 20px }
.tl-step::before { content:''; position:absolute; left:3px; top:3px; width:10px; height:10px; border-radius:50%;
background:var(--c); box-shadow:0 0 0 3px color-mix(in srgb, var(--c) 20%, transparent); }
.tl-time { font-size:10.5px; color:var(--muted,#888); font-variant-numeric:tabular-nums }
.tl-title { font-size:12.5px; font-weight:600; margin:1px 0; line-height:1.3 }
.tl-detail { font-size:10.5px; color:var(--muted,#888); line-height:1.4; margin-top:2px }
/* стрелка-коннектор: вертикальная линия + ровный CSS-треугольник + длительность */
.tl-arrow { position:relative; min-height:20px; margin:1px 0 1px 0; padding-left:20px; display:flex; align-items:center }
.tl-arrow .ln { position:absolute; left:7px; top:0; width:2px; height:20px; background:var(--c); opacity:.5 }
.tl-arrow .ar { position:absolute; left:3px; top:11px; width:0; height:0;
border-left:5px solid transparent; border-right:5px solid transparent; border-top:7px solid var(--c) }
.tl-arrow .dt { font-size:10.5px; color:var(--muted,#999) }
.tl-db{--c:#2563eb} .tl-crane{--c:#16a34a} .tl-gateway{--c:#7c3aed} .tl-response{--c:#0891b2} .tl-reject{--c:#dc2626}
.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">
<p style="margin:0 0 12px"><a href="/admin/claims" class="btn" onclick="if(history.length>1){history.back();return false}">← Назад</a></p>
{{if .Flash}}<div class="card" style="background:var(--accent-weak,#e7f0fb)">{{.Flash}}</div>{{end}}
<div class="card">
<h2 style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">Заявка <code>{{slice .Claim.ID 0 8}}</code> · <span class="badge {{.Claim.Status}}">{{ruState .Claim.Status}}</span>{{if and (or .Fail (eq .Claim.Status "validated")) (can "operate")}}<form method="post" action="/admin/claims/{{.Claim.ID}}/resend" style="margin:0 0 0 auto" onsubmit="this.querySelector('button').disabled=true;this.querySelector('button').textContent='Отправляем…';"><button type="submit" class="btn">Отправить повторно</button></form>{{end}}{{if and (or (eq .Claim.Status "draft") (eq .Claim.Status "validated")) (can "operate")}}<form method="post" action="/admin/claims/{{.Claim.ID}}/delete-draft" style="margin:0{{if not (or .Fail (eq .Claim.Status "validated"))}} 0 0 auto{{end}}" onsubmit="return confirm('Удалить заявку? Она ещё не уходила в НРД. Если по ней бумаги заблокированы в QUIK МНП, блокировка будет снята.');"><button type="submit" class="btn" style="background:var(--card);color:var(--err,#c0392b);border:1px solid var(--err,#c0392b)">{{if eq .Claim.Status "draft"}}Удалить черновик{{else}}Удалить{{end}}</button></form>{{end}}</h2>
{{with realTest (print .Claim.M2MGUID)}}<div class="card" style="background:var(--warn-weak,#fbf3d9);margin:8px 0">
<b>Тест «боевой ЛК → тестовый контур НРД»</b>. У нас заявка идёт как клиент: <b>{{.Client}}</b> — DEPO, QUIK МНП (боевая запись), статусы в ЛК.
В НРД запрос отправлен с данными тест-инвестора <b>{{.As}}</b>; GUID, референсы, бумаги и счета — настоящие.{{if .LKDone}} Итог передан в ЛК.{{else}} «Исполнена» в ЛК — после отчёта НРД об исполнении (MS010).{{end}}
</div>{{end}}
{{with .Fail}}<div class="card" style="background:var(--err-weak,#fbe9e7);margin:8px 0">
<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>
<tr><td class="muted">Обновлена</td><td>{{.Claim.UpdatedAt.Format "02.01.2006 15:04:05"}}</td></tr>
<tr><td class="muted">M2M GUID</td><td><code>{{.Claim.M2MGUID}}</code></td></tr>
<tr><td class="muted">Инвестор</td><td>{{.Claim.Investor.LastName}} {{.Claim.Investor.FirstName}} {{.Claim.Investor.MiddleName}}</td></tr>
<tr><td class="muted">Документ</td><td>тип {{.Claim.Investor.Document.DocumentType}}, серия {{.Claim.Investor.Document.Series}}, номер {{.Claim.Investor.Document.Number}}</td></tr>
<tr><td class="muted">Передающий депозитарий</td><td><code>ИНН {{.Claim.TransferringDepositoryINN}}</code></td></tr>
<tr><td class="muted">Принимающий депозитарий</td><td><code>ИНН {{.Claim.ReceivingDepositoryINN}}</code></td></tr>
{{if .Claim.IIAAgreement}}
<tr><td class="muted">ИИС</td><td>{{.Claim.IIAAgreement.AgreementType}} № {{.Claim.IIAAgreement.AgreementNumber}} от {{.Claim.IIAAgreement.AgreementDate}}, брокер ИНН {{.Claim.IIAAgreement.BrokerINN}}</td></tr>
{{end}}
</table>
<div style="margin-top:14px">
<a class="btn btn-ok" href="/admin/claims/{{.Claim.ID}}/export.zip" download>⤓ Выгрузить путь заявки (для регулятора)</a>
</div>
<p class="muted" style="font-size:12px;margin-top:8px">ZIP: отчёт-доказательство (хронология пути) + дословные XML запроса и ответа НРД (windows-1251).</p>
{{if .Claim.Investor.ID}}
<form method="get" action="/admin/claims-export" style="margin-top:14px;padding-top:14px;border-top:1px solid var(--border);display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
<input type="hidden" name="investor" value="{{.Claim.Investor.ID}}">
<div><label style="font-size:12px">Заявки клиента с</label><input type="date" name="from" style="margin-top:3px"></div>
<div><label style="font-size:12px">по</label><input type="date" name="to" style="margin-top:3px"></div>
<button type="submit" class="btn">⤓ Выгрузить за период</button>
<span class="muted" style="font-size:12px">Пусто = без ограничения. ZIP: сводка CSV + комплект по каждой заявке.</span>
</form>
{{end}}
</div>
{{if devOn}}{{with .Claim.TestResult}}
<div class="card" style="background:{{if eq .Status "ok"}}var(--ok-weak){{else if eq .Status "mismatch"}}var(--err-weak){{else if eq .Status "error"}}var(--err-weak){{else}}var(--warn-weak){{end}}">
<h2>
{{if eq .Status "ok"}}<span style="color:var(--ok)">✓ Тест пройден</span>
{{else if eq .Status "mismatch"}}<span style="color:var(--err)">Результат не совпал</span>
{{else if eq .Status "error"}}<span style="color:var(--err)">Ошибка сервиса НРД</span>
{{else}}<span style="color:var(--warn)">⏳ Ожидание ответа</span>{{end}}
· сценарий {{.Scenario}} «{{.Title}}»
</h2>
<table>
<tr><td style="width:200px" class="muted">Ожидали</td><td>{{.Expected}}</td></tr>
<tr><td class="muted">Получили</td><td><b>{{.Got}}</b></td></tr>
{{if .Note}}<tr><td class="muted">Вывод</td><td>{{.Note}}</td></tr>{{end}}
</table>
</div>
{{end}}{{end}}
<div class="card">
<h2>Ценные бумаги ({{len .Claim.Securities}})</h2>
<table>
<thead><tr><th>Код</th><th>ISIN / описание</th><th>Количество</th><th>Счетов депо</th></tr></thead>
<tbody>
{{range .Claim.Securities}}
<tr>
<td><code>{{.SecurityCode}}</code></td>
<td>{{if .SecurityDetails.ISIN}}<code>{{.SecurityDetails.ISIN}}</code>{{else if .SecurityDetails.SecurityInfo}}{{.SecurityDetails.SecurityInfo.Classification}} / {{.SecurityDetails.SecurityInfo.Category}}{{if .SecurityDetails.SecurityInfo.IdentificationDetails.FundShares}} · ПИФ {{.SecurityDetails.SecurityInfo.IdentificationDetails.FundShares.RegNumber}} класс {{.SecurityDetails.SecurityInfo.IdentificationDetails.FundShares.Class}}{{end}}{{end}}</td>
<td>{{if .Quantity.Whole}}{{.Quantity.Whole}}{{else}}{{.Quantity.Fractional}}{{end}}</td>
<td>{{len .SettlementAccounts}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{/* ===== QUIK МНП: поручение «Вывод ЦБ (M2M)» ===== */}}
{{$back := printf "/admin/claims/%s" .Claim.ID}}
<div class="card">
<style>
.qk-steps{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0}
.qk-step{padding:6px 10px;border-radius:9px;border:1.5px solid var(--border);font-size:12.5px;background:var(--card-2)}
.qk-step.ok{border-color:color-mix(in srgb,#16a34a 45%,transparent);background:color-mix(in srgb,#16a34a 12%,var(--card));color:#0f7a37;font-weight:600}
.qk-step.bad{border-color:color-mix(in srgb,#dc2626 45%,transparent);background:color-mix(in srgb,#dc2626 10%,var(--card));color:#b3261e;font-weight:600}
.qk-state{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:700}
.qk-state.registered{background:color-mix(in srgb,#d97706 15%,var(--card));color:#9a6b12}
.qk-state.signed,.qk-state.deposited{background:color-mix(in srgb,#16a34a 15%,var(--card));color:#0f7a37}
.qk-state.denied{background:var(--card-2);color:var(--muted)}
.qk-state.error{background:color-mix(in srgb,#dc2626 15%,var(--card));color:#b3261e}
.qk-state.manual{background:color-mix(in srgb,#2563eb 12%,var(--card));color:#1d4ed8}
</style>
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
<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>
{{if not .Quik}}<p class="muted" style="margin-top:10px">Поручений МНП по этой заявке нет{{if .Claim.M2MGUID}} — можно зарегистрировать вручную:
<form method="post" action="/admin/quik/op" style="margin:6px 0 0;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="hidden" name="guid" value="{{.Claim.M2MGUID}}"><input type="hidden" name="op" value="register"><input type="hidden" name="back" value="{{$back}}">
<select name="isin" style="font-size:12.5px">{{range .Claim.Securities}}<option value="{{.SecurityDetails.ISIN}}">{{.SecurityDetails.ISIN}} — {{.SecurityCode}}</option>{{end}}</select>
<button class="btn" style="font-size:12px" onclick="return confirm('Зарегистрировать «Вывод ЦБ (M2M)» в QUIK? Бумаги будут заблокированы.')">⊕ Зарегистрировать в QUIK</button>
<span class="muted" style="font-size:12px">или привязать поручение, поданное клиентом из терминала:</span>
<input type="text" name="instr_no" placeholder="№ поручения" style="font-size:12px;width:130px" form="qk-link">
</form>
<form id="qk-link" method="post" action="/admin/quik/op" style="margin:4px 0 0"><input type="hidden" name="guid" value="{{.Claim.M2MGUID}}"><input type="hidden" name="op" value="link"><input type="hidden" name="back" value="{{$back}}"><select name="isin" style="font-size:12.5px">{{range .Claim.Securities}}<option value="{{.SecurityDetails.ISIN}}">{{.SecurityDetails.ISIN}}</option>{{end}}</select> <button class="btn" style="font-size:12px">⛓ Привязать</button></form>{{end}}</p>{{end}}
{{range .Quik}}
<div style="border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-top:10px">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<b><code>{{.ISIN}}</code> × {{.Qty}}</b>
<span class="muted" style="font-size:12px">клиент QUIK <code>{{.ClientID}}</code>{{if .Contract}} · {{.Contract}}{{end}}{{if .InstrNo}} · поручение МНП <b>№{{.InstrNo}}</b>{{end}}</span>
<span class="qk-state {{.State}}">{{.StateRu}}</span>
</div>
{{if .Err}}<div style="color:#b3261e;font-size:12.5px;margin-top:4px">{{.Err}}</div>{{end}}
{{if .Steps}}<div class="qk-steps">{{range .Steps}}<span class="qk-step {{if .OK}}ok{{else}}bad{{end}}" title="{{.Reply}}">{{msk .At}} · {{.Action}}{{if .ActionID}} #{{.ActionID}}{{end}}: {{.Reply}}</span>{{end}}</div>{{end}}
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:6px">
{{if eq .State "registered"}}
<form method="post" action="/admin/quik/op" style="margin:0;display:inline"><input type="hidden" name="guid" value="{{.GUID}}"><input type="hidden" name="isin" value="{{.ISIN}}"><input type="hidden" name="op" value="sign"><input type="hidden" name="back" value="{{$back}}"><button class="btn btn-ok" style="font-size:12px" onclick="return confirm('Подписать поручение МНП №{{.InstrNo}}? Бумаги будут СПИСАНЫ с позиции клиента в QUIK.')">✎ Подписать (исполнить)</button></form>
<form method="post" action="/admin/quik/op" style="margin:0;display:inline"><input type="hidden" name="guid" value="{{.GUID}}"><input type="hidden" name="isin" value="{{.ISIN}}"><input type="hidden" name="op" value="deny"><input type="hidden" name="back" value="{{$back}}"><input type="text" name="reason" placeholder="причина для клиента" style="font-size:12px;width:200px"><button class="btn" style="font-size:12px" onclick="return confirm('Снять поручение №{{.InstrNo}}? Блокировка в QUIK будет снята, клиент увидит причину.')">✕ Снять</button></form>
{{else if or (eq .State "error") (eq .State "manual") (not .InstrNo)}}
<form method="post" action="/admin/quik/op" style="margin:0;display:inline"><input type="hidden" name="guid" value="{{.GUID}}"><input type="hidden" name="isin" value="{{.ISIN}}"><input type="hidden" name="op" value="register"><input type="hidden" name="back" value="{{$back}}"><button class="btn" style="font-size:12px" onclick="return confirm('Зарегистрировать «Вывод ЦБ (M2M)» в QUIK? Бумаги будут заблокированы на позиции клиента.')">⊕ Зарегистрировать (заблокировать)</button></form>
<form method="post" action="/admin/quik/op" style="margin:0;display:inline"><input type="hidden" name="guid" value="{{.GUID}}"><input type="hidden" name="isin" value="{{.ISIN}}"><input type="hidden" name="op" value="link"><input type="hidden" name="back" value="{{$back}}"><input type="text" name="instr_no" placeholder="№ поручения из QUIK" style="font-size:12px;width:150px"><button class="btn" style="font-size:12px">⛓ Привязать</button></form>
{{end}}
</div>
</div>
{{end}}
</div>
{{if .Depo}}
<div class="card">
<style>
.ord-steps{display:flex;gap:12px;margin:14px 0 6px;flex-wrap:wrap}
.ord-step{flex:1 1 190px;position:relative;display:flex;gap:12px;align-items:flex-start;padding:14px 16px;border-radius:13px;border:2px solid var(--border);background:var(--card)}
.ord-step .dot{width:30px;height:30px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:800;background:var(--border);color:var(--muted)}
.ord-step .t{font-size:14px;font-weight:700;line-height:1.2}
.ord-step .s{font-size:11.5px;margin-top:3px;font-weight:600;color:var(--muted)}
.ord-step.done{border-color:#16a34a;background:color-mix(in srgb,#16a34a 13%,var(--card))}
.ord-step.done .dot{background:#16a34a;color:#fff}
.ord-step.done .s{color:#0f7a37}
.ord-step.wait{border-color:#f59e0b;background:color-mix(in srgb,#f59e0b 15%,var(--card))}
.ord-step.wait .dot{background:#f59e0b;color:#fff;box-shadow:0 0 0 4px color-mix(in srgb,#f59e0b 28%,transparent)}
.ord-step.wait .s{color:#b45309}
.ord-step.todo{opacity:.55}
.ord-step.bad{border-color:#dc2626;background:color-mix(in srgb,#dc2626 12%,var(--card))}
.ord-step.bad .dot{background:#dc2626;color:#fff}
.ord-step.bad .s{color:#b3261e}
.ord-dl.bad{border-color:#dc2626;color:#b3261e}
.ord-dl{display:inline-block;margin-top:9px;font-size:11.5px;font-weight:700;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid #16a34a;color:#0f7a37;text-decoration:none;white-space:nowrap}
.ord-dl:hover{background:color-mix(in srgb,#16a34a 12%,var(--card))}
</style>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<h2 style="margin:0">Депозитарное поручение 16 (списание)</h2>
{{if and .Depo.Done .Depo.Rejected}}<span class="badge rejected">завершено отказом · MS010</span>
{{else if .Depo.Done}}<span class="badge confirmed">проведено · MS010</span>
{{else if .Depo.Submitted}}<span class="badge awaiting_decision">подано, ждём MS010</span>
{{else}}<span class="badge" style="background:var(--card-2);color:var(--muted)">требует подачи</span>{{end}}
</div>
{{if .Watch}}<div style="margin:8px 0 0;padding:8px 12px;border-radius:9px;font-size:13px;font-weight:600;{{if eq .Watch.Kind "ok"}}background:color-mix(in srgb,#16a34a 12%,var(--card));color:#0f7a37{{else if eq .Watch.Kind "warn"}}background:color-mix(in srgb,#d97706 14%,var(--card));color:#9a6b12{{else if eq .Watch.Kind "bad"}}background:color-mix(in srgb,#dc2626 12%,var(--card));color:#b3261e{{else}}background:var(--card-2);color:var(--muted){{end}}">⏱ Срок по регламенту: {{.Watch.StatusRu}}</div>{{end}}
<p class="muted" style="font-size:12.5px;margin:6px 0 0">Мы — списывающий депозитарий. НРД прислал черновик формы 16; подайте его — НРД сматчит со встречным 16/1 контрагента и пришлёт отчёт <b>MS010</b> о проведённом списании (бумага ушла контрагенту).</p>
<div class="ord-steps">
<div class="ord-step done">
<span class="dot">✓</span>
<div><div class="t">Черновик 16 получен</div><div class="s">{{msk .Depo.DraftAt}}</div>
<a class="ord-dl" href="/admin/outgoing/{{.Depo.GUID}}/draft.zip">⬇ Черновик от НРД</a></div>
</div>
<div class="ord-step {{if .Depo.Submitted}}done{{else}}wait{{end}}">
<span class="dot">{{if .Depo.Submitted}}✓{{else}}2{{end}}</span>
<div><div class="t">Поручение 16 подано</div><div class="s">{{if .Depo.Submitted}}{{msk .Depo.OrderAt}}{{else}}нужно подать в НРД{{end}}</div>
{{if .Depo.OrderXML}}<a class="ord-dl" href="/admin/outgoing/{{.Depo.GUID}}/order.xml">⬇ Отправленная форма 16</a>{{end}}</div>
</div>
<div class="ord-step {{if and .Depo.Done .Depo.Rejected}}bad{{else if .Depo.Done}}done{{else if .Depo.Submitted}}wait{{else}}todo{{end}}">
<span class="dot">{{if and .Depo.Done .Depo.Rejected}}✕{{else if .Depo.Done}}✓{{else}}3{{end}}</span>
<div><div class="t">{{if and .Depo.Done .Depo.Rejected}}Операция завершена отказом · MS010{{else}}Списание проведено · MS010{{end}}</div><div class="s">{{if and .Depo.Done .Depo.Rejected}}НРД отказал в исполнении — см. отчёт{{else if .Depo.Done}}отчёт получен от НРД{{else if .Depo.Submitted}}ждём встречный 16/1 + MS010{{else}}после подачи{{end}}</div>
{{if .Depo.Responses}}<a class="ord-dl{{if .Depo.Rejected}} bad{{end}}" href="/admin/outgoing/{{.Depo.GUID}}/ms010.zip">⬇ Ответ НРД (MS010)</a>{{end}}</div>
</div>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:8px">
{{if not .Depo.Submitted}}
<form method="post" action="/admin/outgoing/{{.Depo.GUID}}/submit-order?back=/admin/claims/{{.Claim.ID}}" onsubmit="return confirm('Подать поручение 16 (списание, тип ЭД ORDXM) в НРД?');">
<button type="submit" class="btn btn-ok">▶ Подать поручение 16 в НРД</button>
</form>
{{end}}
<a href="/admin/outgoing/{{.Depo.GUID}}/export.zip" class="btn">⬇ Выгрузка 16 для регулятора (весь пакет)</a>
</div>
{{if .Depo.Submitted}}
<div class="muted" style="font-size:12px;margin-top:10px">Подано {{msk .Depo.OrderAt}} · пакет ИШ <code>{{.Depo.Pkgs}}</code></div>
{{if .Depo.Responses}}
<table style="margin-top:8px">
<thead><tr><th style="width:150px">Время</th><th>Ответ НРД</th></tr></thead>
<tbody>{{range .Depo.Responses}}<tr><td style="white-space:nowrap">{{.Time}}</td><td>{{.Kind}}</td></tr>{{end}}</tbody>
</table>
{{else}}
<p class="muted" style="font-size:12px;margin-top:8px">Ответ НРД (квитанция → принятие → отчёт MS010 о списании) появится здесь. Обновите страницу.</p>
{{end}}
{{end}}
</div>
{{end}}
<div class="card">
<h2>История FSM</h2>
<table>
<thead><tr><th>Состояние</th><th>Вошли</th><th>Вышли</th><th>Причина</th></tr></thead>
<tbody>
{{range .Claim.Stages}}
<tr>
<td><span class="badge {{.State}}">{{ruState .State}}</span></td>
<td>{{.EnteredAt.Format "15:04:05.000"}}</td>
<td>{{if .LeftAt}}{{.LeftAt.Format "15:04:05.000"}}{{else}}<span class="muted">сейчас</span>{{end}}</td>
<td class="muted">{{.Reason}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{if .Claim.RequestRawXML}}
<div class="card">
<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>
<p class="muted">XML ниже — исходная заявка клиента (наша запись). В НРД ушла её копия с подменённым блоком инвестора ({{.SubstAs}}), подписанная ИШ. Тип пакета <code>#M2MTR</code>.</p>
{{else}}
<p class="muted">Точный XML, который bj-server сформировал и ИШ подписал ключом и отправил в НРД. Тип пакета <code>#M2MTR</code>.</p>
{{end}}
<a class="btn btn-ok" href="/admin/claims/{{.Claim.ID}}/request.xml" download style="margin-right:8px">⤓ Скачать XML запроса</a>
<button type="button" class="btn" onclick="copyReq(this)">Скопировать</button>
<pre id="raw-request" style="white-space:pre-wrap;word-break:break-all;background:var(--surface-2,#f5f5f7);padding:12px;border-radius:8px;font-size:12px;overflow:auto;max-height:340px">{{.Claim.RequestRawXML}}</pre>
<script>
function copyReq(btn){
var t=document.getElementById('raw-request').innerText;
window.bjCopy(t).then(function(){
var o=btn.textContent; btn.textContent='Скопировано ✓';
setTimeout(function(){btn.textContent=o;},1500);
});
}
</script>
</div>
{{end}}
{{if .Claim.M2MResponse}}
<div class="card">
<h2>Ответ сервиса МОСТ (M2MTransferResponse)</h2>
<p>
{{if eq .Claim.M2MResponse.StatusCode "ERROR"}}
<span class="badge err">● ERROR — заявка отклонена сервисом НРД</span>
{{else}}
<span class="badge ok">● {{.Claim.M2MResponse.StatusCode}} — принято в обработку</span>
{{end}}
</p>
<p class="muted">GUID <code>{{.Claim.M2MResponse.GUID}}</code></p>
<table>
<thead><tr><th>ReferenceID</th><th>Код</th><th>Текст ответа НРД</th></tr></thead>
<tbody>
{{range .Claim.M2MResponse.Responses}}
<tr><td><code>{{.ReferenceID}}</code></td><td><code>{{.Code}}</code></td><td>{{if .Text}}{{.Text}}{{else}}{{m2mCode .Code}}{{end}}</td></tr>
{{end}}
</tbody>
</table>
{{if eq .Claim.M2MResponse.StatusCode "ERROR"}}
<p class="muted" style="margin-top:10px">
Это отказ на сервисном уровне — запрос не дошёл до контрагента. Решение
(M2MTransferDecision) по такой заявке не придёт. Устраните причину по коду
выше и отправьте новую заявку.
</p>
{{end}}
{{if .Claim.M2MResponse.RawXML}}
<details style="margin-top:12px">
<summary style="cursor:pointer;font-weight:600">
Сырой ответ НРД (для техподдержки M2MOST@nsd.ru)
</summary>
<p class="muted" style="margin:8px 0">
Точные байты ответа сервиса МОСТ. Можно дословно переслать в поддержку НРД.
</p>
<a class="btn btn-ok" href="/admin/claims/{{.Claim.ID}}/response.xml" download style="margin-right:8px">⤓ Скачать XML ответа</a>
<button type="button" class="btn" onclick="copyRaw(this)">Скопировать</button>
<pre id="raw-response" style="white-space:pre-wrap;word-break:break-all;background:var(--surface-2,#f5f5f7);padding:12px;border-radius:8px;font-size:12px;overflow:auto;max-height:340px">{{.Claim.M2MResponse.RawXML}}</pre>
</details>
<script>
function copyRaw(btn){
var t=document.getElementById('raw-response').innerText;
window.bjCopy(t).then(function(){
var o=btn.textContent; btn.textContent='Скопировано ✓';
setTimeout(function(){btn.textContent=o;},1500);
});
}
</script>
{{end}}
</div>
{{end}}
{{if .Claim.M2MDecision}}
<div class="card">
<h2>Решение принимающей стороны (M2MTransferDecision)</h2>
<p class="muted">GUID <code>{{.Claim.M2MDecision.GUID}}</code></p>
<table>
<thead><tr><th>ReferenceID</th><th>Решение</th><th>Коды отказа</th></tr></thead>
<tbody>
{{range .Claim.M2MDecision.Securities}}
<tr>
<td><code>{{.ReferenceID}}</code></td>
<td><span class="badge {{if eq .Outcome "confirmed"}}confirmed{{else}}rejected{{end}}">{{ruOutcome .Outcome}}</span></td>
<td>{{range .RejectCodes}}{{m2mCode .}}<br>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{if .Claim.LastCallback}}
<div class="card">
<h2>Последний callback в ЛК</h2>
<table>
<tr><td style="width:160px" class="muted">Статус</td><td><span class="badge {{.Claim.LastCallback.NewStatus}}">{{ruState .Claim.LastCallback.NewStatus}}</span></td></tr>
{{if .Claim.LastCallback.ReasonCode}}
<tr><td class="muted">Код причины</td><td><code>{{.Claim.LastCallback.ReasonCode}}</code> {{.Claim.LastCallback.ReasonText}}</td></tr>
{{end}}
<tr><td class="muted">Время</td><td>{{.Claim.LastCallback.UpdatedAt.Format "02.01.2006 15:04:05"}}</td></tr>
</table>
</div>
{{end}}
</div>{{/* /claim-main */}}
{{if .Claim.Timeline}}
<aside class="claim-tl">
<h3>Путь заявки</h3>
{{range $i, $e := .Claim.Timeline}}
{{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}}
{{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 */}}
{{end}}