Карточка входящего: отклонённый перевод — 16/1 не требуется, операция завершена; черновик отмечается только если реально получен
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func renderIncoming(t *testing.T, d incomingDetailData) string {
|
||||
t.Helper()
|
||||
a, err := newAdmin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
d.page = nowPage("Входящий перевод", "incoming")
|
||||
render(rec, a.incomingDetail, d)
|
||||
if rec.Code != 200 {
|
||||
t.Fatalf("код %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
return rec.Body.String()
|
||||
}
|
||||
|
||||
// Отклонённый перевод: 16/1 не требуется, операция завершена, без кнопок подачи и «черновик получен».
|
||||
func TestIncomingDetailRejected(t *testing.T) {
|
||||
it := &IncomingTransfer{ID: "x", GUID: "9F5E120D-EFD1-46EE-A9BA-35CB2E794B81", Status: "answered", Auto: true, AnsweredAt: time.Now(),
|
||||
Securities: []IncomingSecurity{{ReferenceID: "R1", SecurityCode: "SBER/03", Decision: "rejected", RejectCode: "M2M04"}}}
|
||||
rejected, codes := incomingRejectedAll(it)
|
||||
if !rejected || codes != "M2M04" {
|
||||
t.Fatalf("incomingRejectedAll: %v %q", rejected, codes)
|
||||
}
|
||||
body := renderIncoming(t, incomingDetailData{It: it, Rejected: rejected, RejectCodes: codes, AutoMode: true})
|
||||
for _, want := range []string{"не требуется — перевод отклонён", "Операция завершена отказом", "автоматически", "Зачисление в QUIK не требуется"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("нет %q", want)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"Подать поручение 16/1 в НРД", "Черновик от НРД", "нужно подать"} {
|
||||
if strings.Contains(body, bad) {
|
||||
t.Errorf("лишнее в завершённом переводе: %q", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Перевод ждёт решения: шаги 16/1 не отмечены выполненными.
|
||||
func TestIncomingDetailPending(t *testing.T) {
|
||||
it := &IncomingTransfer{ID: "y", GUID: "G", Status: "pending", Securities: []IncomingSecurity{{ReferenceID: "R1", SecurityCode: "SBER/03"}}}
|
||||
body := renderIncoming(t, incomingDetailData{It: it})
|
||||
if !strings.Contains(body, "после нашего согласия") || strings.Contains(body, "Черновик от НРД") || strings.Contains(body, "Подать поручение 16/1 в НРД") {
|
||||
t.Error("ожидающий перевод не должен показывать полученный черновик и кнопку подачи")
|
||||
}
|
||||
}
|
||||
@@ -176,20 +176,11 @@ func registerIncoming(mux *http.ServeMux, a *admin, deps incomingDeps) {
|
||||
}
|
||||
}
|
||||
}
|
||||
data := struct {
|
||||
page
|
||||
It *IncomingTransfer
|
||||
Flash string
|
||||
OrderResponses []orderResponse
|
||||
OrderDone bool
|
||||
OrderDoneAt string
|
||||
DraftReceived string
|
||||
Depo depoResult // сверка с DEPO — эмуляция ответа, ничего не отправляется
|
||||
Quik []*QuikLink
|
||||
QuikOn bool
|
||||
Watch *OrderWatch
|
||||
DepoL *DepoLink
|
||||
}{nowPage("Входящий перевод", "incoming"), it, r.URL.Query().Get("flash"), orderResps, orderDone, orderDoneAt, draftReceivedTime(it.GUID), depoResult{}, globalQuikLinks.ForGUID(it.GUID), deps.rc.Snapshot().Quik.WritesEnabled, globalOrderWatch.Get(it.GUID), globalDepoLinks.Get(it.GUID)}
|
||||
rejected, codes := incomingRejectedAll(it)
|
||||
data := incomingDetailData{page: nowPage("Входящий перевод", "incoming"), It: it, Flash: r.URL.Query().Get("flash"),
|
||||
OrderResponses: orderResps, OrderDone: orderDone, OrderDoneAt: orderDoneAt, DraftReceived: draftReceivedTime(it.GUID),
|
||||
Quik: globalQuikLinks.ForGUID(it.GUID), QuikOn: quikActive(deps.rc.Snapshot().Quik), Watch: globalOrderWatch.Get(it.GUID), DepoL: globalDepoLinks.Get(it.GUID),
|
||||
Rejected: rejected, RejectCodes: codes, AutoMode: deps.rc.Snapshot().M2MAuto.Enabled}
|
||||
// Сверка выполняется при открытии карточки; итог сохраняем для бейджа в списке.
|
||||
data.Depo = runDepoCheck(it)
|
||||
if data.Depo.Ran {
|
||||
@@ -234,3 +225,38 @@ func handleIncomingAnswer(w http.ResponseWriter, r *http.Request, deps incomingD
|
||||
}
|
||||
http.Redirect(w, r, "/admin/incoming/"+id+"?flash="+url.QueryEscape(msg), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// incomingDetailData — карточка входящего перевода.
|
||||
type incomingDetailData struct {
|
||||
page
|
||||
It *IncomingTransfer
|
||||
Flash string
|
||||
OrderResponses []orderResponse
|
||||
OrderDone bool
|
||||
OrderDoneAt string
|
||||
DraftReceived string
|
||||
Depo depoResult // сверка с DEPO — эмуляция ответа, ничего не отправляется
|
||||
Quik []*QuikLink
|
||||
QuikOn bool
|
||||
Watch *OrderWatch
|
||||
DepoL *DepoLink
|
||||
Rejected bool // решение отправлено и ни одна бумага не принята — перевод завершён отказом
|
||||
RejectCodes string // коды отказа через запятую
|
||||
AutoMode bool // автоматический режим M2M (поручение 16/1 подаст bj)
|
||||
}
|
||||
|
||||
// incomingRejectedAll — перевод отвечен и ни одна бумага не принята (поручение 16/1 не нужно).
|
||||
func incomingRejectedAll(it *IncomingTransfer) (bool, string) {
|
||||
if it.Status != "answered" || incomingHasConfirmed(it) {
|
||||
return false, ""
|
||||
}
|
||||
var codes []string
|
||||
seen := map[string]bool{}
|
||||
for _, s := range it.Securities {
|
||||
if s.RejectCode != "" && !seen[s.RejectCode] {
|
||||
seen[s.RejectCode] = true
|
||||
codes = append(codes, s.RejectCode)
|
||||
}
|
||||
}
|
||||
return true, strings.Join(codes, ", ")
|
||||
}
|
||||
|
||||
@@ -227,24 +227,39 @@
|
||||
.ord-dl:hover{background:color-mix(in srgb,#16a34a 12%,var(--card))}
|
||||
</style>
|
||||
|
||||
{{if .Rejected}}
|
||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<h2 style="margin:0">Депозитарное поручение 16/1</h2>
|
||||
<span class="badge rejected">не требуется — перевод отклонён</span>
|
||||
</div>
|
||||
<div class="ord-steps">
|
||||
<div class="ord-step done">
|
||||
<span class="dot">✓</span>
|
||||
<div><div class="t">Операция завершена отказом</div>
|
||||
<div class="s">Отказ {{.RejectCodes}}{{with .RejectCodes}} ({{m2mCode .}}){{end}} отправлен {{msk .It.AnsweredAt}}{{if .It.Auto}} автоматически{{end}}. Бумаги не зачисляются, поручение 16/1 не подаётся — со стороны депозитария действий не требуется.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<h2 style="margin:0">Депозитарное поручение 16/1</h2>
|
||||
{{if .OrderDone}}<span class="badge confirmed">проведено · MS010</span>
|
||||
{{else if .It.OrderSubmitted}}<span class="badge awaiting_decision">подано, ждём MS010</span>
|
||||
{{else if not $answered}}<span class="badge" style="background:var(--card-2);color:var(--muted)">после решения по переводу</span>
|
||||
{{else if .AutoMode}}<span class="badge awaiting_decision">подаст bj автоматически</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}}
|
||||
|
||||
|
||||
<div class="ord-steps">
|
||||
<div class="ord-step done">
|
||||
<span class="dot">✓</span>
|
||||
<div><div class="t">Черновик получен</div><div class="s">{{if .DraftReceived}}{{.DraftReceived}}{{else}}сформирован НРД (MF010){{end}}</div>
|
||||
<a class="ord-dl" href="/admin/incoming/{{.It.ID}}/draft.zip">⬇ Черновик от НРД</a></div>
|
||||
<div class="ord-step {{if .DraftReceived}}done{{else if $answered}}wait{{else}}todo{{end}}">
|
||||
<span class="dot">{{if .DraftReceived}}✓{{else}}1{{end}}</span>
|
||||
<div><div class="t">Черновик получен</div><div class="s">{{if .DraftReceived}}{{.DraftReceived}}{{else if $answered}}ждём черновик MF010 от НРД{{else}}после нашего согласия{{end}}</div>
|
||||
{{if .DraftReceived}}<a class="ord-dl" href="/admin/incoming/{{.It.ID}}/draft.zip">⬇ Черновик от НРД</a>{{end}}</div>
|
||||
</div>
|
||||
<div class="ord-step {{if .It.OrderSubmitted}}done{{else}}wait{{end}}">
|
||||
<div class="ord-step {{if .It.OrderSubmitted}}done{{else if .DraftReceived}}wait{{else}}todo{{end}}">
|
||||
<span class="dot">{{if .It.OrderSubmitted}}✓{{else}}2{{end}}</span>
|
||||
<div><div class="t">Поручение подано в НРД</div><div class="s">{{if .It.OrderSubmitted}}{{msk .It.OrderAt}}{{else}}нужно подать{{end}}</div>
|
||||
<div><div class="t">Поручение подано в НРД</div><div class="s">{{if .It.OrderSubmitted}}{{msk .It.OrderAt}}{{else if .AutoMode}}bj подаст автоматически по черновику{{else if .DraftReceived}}нужно подать{{else}}после черновика{{end}}</div>
|
||||
{{if .It.OrderXML}}<a class="ord-dl" href="/admin/incoming/{{.It.ID}}/order.xml">⬇ Отправленная форма 16/1</a>{{end}}</div>
|
||||
</div>
|
||||
<div class="ord-step {{if .OrderDone}}done{{else if .It.OrderSubmitted}}wait{{else}}todo{{end}}">
|
||||
@@ -255,7 +270,7 @@
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
|
||||
{{if not .It.OrderSubmitted}}
|
||||
{{if and (not .It.OrderSubmitted) .DraftReceived}}
|
||||
<form method="post" action="/admin/incoming/{{.It.ID}}/submit-order" onsubmit="return confirm('Подать поручение 16/1 (тип ЭД ORDXM) в НРД (TEST3)?');">
|
||||
<button type="submit" class="btn btn-ok">▶ Подать поручение 16/1 в НРД</button>
|
||||
</form>
|
||||
@@ -284,6 +299,7 @@
|
||||
<details style="margin-top:12px"><summary style="cursor:pointer;font-size:12px;color:var(--muted)">Как это работает</summary>
|
||||
<p class="muted" style="font-size:12px;margin-top:6px">После нашего согласия на прием НРД формирует <b>черновик поручения 16/1</b> (MF010). Депозитарий подаёт его в НРД (тип ЭД <code>ORDXM</code>) — НРД сматчит со встречным поручением 16 контрагента, проведёт зачисление и выпустит отчёт <code>XMLRP</code> = <b>MS010</b>. Кнопка «Подать» отправляет черновик через ИШ (подпись ГОСТ ставит ИШ); пока Fancy-DEPO не подключена, можно скачать черновик и обработать вручную.</p>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -367,6 +383,9 @@
|
||||
<a href="/admin/quik" class="muted" style="font-size:12px">все поручения →</a>
|
||||
</div>
|
||||
{{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}}
|
||||
{{if .Rejected}}
|
||||
<p class="muted" style="font-size:12.5px;margin:10px 0 0">Зачисление в QUIK не требуется — перевод отклонён ({{.RejectCodes}}), бумаги клиенту не поступают.</p>
|
||||
{{else}}
|
||||
|
||||
<p class="muted" style="font-size:12.5px;margin:6px 0 0">Когда НРД принимает наше поручение 16/1 (отчёт MS010), bj регистрирует в Модуле неторговых поручений «Ввод ЦБ (M2M)» по ISIN — оно исполняется сразу, бумаги появляются на позиции клиента в QUIK.{{if not .QuikOn}} <b>Записи в МНП сейчас выключены</b> (Настройка → QUIK МНП).{{end}}</p>
|
||||
{{if not .Quik}}<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;align-items:center"><span class="muted">Зачислений по этому переводу ещё нет.</span>
|
||||
@@ -390,5 +409,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user