feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk) - трансферные таблицы FTV_M2M: создание при установке, события, справочник участников - сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO - боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК - онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML) - модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
package lkgateway
|
||||
|
||||
// incoming_export.go — выгрузка входящего перевода для контролирующих органов
|
||||
// (аналог выгрузки по исходящим заявкам): ZIP с отчётом-доказательством, исходным
|
||||
// XML запроса контрагента и XML нашего ответа-решения. Фиксирует весь путь:
|
||||
// когда запрос сформирован, когда мы забрали его из шлюза, когда ответили.
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// exportIncoming отдаёт ZIP-пакет доказательств по одному входящему переводу.
|
||||
func exportIncoming(w http.ResponseWriter, r *http.Request, deps incomingDeps, id string) {
|
||||
it, ok := deps.store.Get(id)
|
||||
if !ok {
|
||||
http.Error(w, "входящий перевод не найден", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="vhodyaschiy-%s.zip"`, shortID(it.ID)))
|
||||
zw := zip.NewWriter(w)
|
||||
defer zw.Close()
|
||||
|
||||
if f, e := zw.Create("otchet.html"); e == nil {
|
||||
_, _ = f.Write(incomingReportHTML(it))
|
||||
}
|
||||
if it.RawXML != "" {
|
||||
if f, e := zw.Create("request.xml"); e == nil {
|
||||
_, _ = f.Write(faithfulXMLBytes(it.RawXML))
|
||||
}
|
||||
}
|
||||
if it.DecisionXML != "" {
|
||||
if f, e := zw.Create("decision.xml"); e == nil {
|
||||
_, _ = f.Write(faithfulXMLBytes(it.DecisionXML))
|
||||
}
|
||||
}
|
||||
if it.OrderXML != "" {
|
||||
if f, e := zw.Create("poruchenie-16-1.xml"); e == nil {
|
||||
_, _ = f.Write(faithfulXMLBytes(it.OrderXML))
|
||||
}
|
||||
}
|
||||
// Ответ НРД по поручению (отчёт MS010 + квитанции) — сырые файлы из INBOX.
|
||||
for _, rf := range collectOrderResponseRaw(it.OrderContrDID) {
|
||||
if f, e := zw.Create("otvet-nrd/" + rf.Name); e == nil {
|
||||
_, _ = f.Write(rf.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// incomingReportHTML формирует HTML-отчёт-доказательство по входящему переводу.
|
||||
func incomingReportHTML(it *IncomingTransfer) []byte {
|
||||
esc := html.EscapeString
|
||||
msk := func(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "—"
|
||||
}
|
||||
return t.In(time.FixedZone("МСК", 3*3600)).Format("02.01.2006 15:04:05") + " МСК"
|
||||
}
|
||||
dur := func(a, b time.Time) string {
|
||||
if a.IsZero() || b.IsZero() {
|
||||
return "—"
|
||||
}
|
||||
d := b.Sub(a)
|
||||
if d < 0 {
|
||||
d = -d
|
||||
}
|
||||
return d.Round(time.Second).String()
|
||||
}
|
||||
senderName := handbookNameByCode(it.SenderCode)
|
||||
fio := strings.TrimSpace(it.InvestorLast + " " + it.InvestorFirst + " " + it.InvestorMiddle)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(`<!doctype html><html lang="ru"><head><meta charset="utf-8">`)
|
||||
b.WriteString(`<title>Отчёт о входящем переводе M2M</title><style>`)
|
||||
b.WriteString(`body{font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#1a1a1a;max-width:900px;margin:24px auto;padding:0 20px;line-height:1.5}`)
|
||||
b.WriteString(`h1{font-size:20px;margin:0 0 4px}h2{font-size:15px;margin:22px 0 8px;color:#1f4e79}`)
|
||||
b.WriteString(`table{border-collapse:collapse;width:100%;font-size:13px;margin:6px 0}`)
|
||||
b.WriteString(`td,th{border:1px solid #ccc;padding:6px 9px;text-align:left;vertical-align:top}th{background:#f0f0f0}`)
|
||||
b.WriteString(`.k{color:#666;width:280px}.muted{color:#666}.mono{font-family:monospace}`)
|
||||
b.WriteString(`.brand{display:flex;align-items:center;gap:14px;padding:6px 0 14px;border-bottom:2px solid #ff0508;margin-bottom:16px}`)
|
||||
b.WriteString(`.brand svg{display:block;flex:none}.brand .svc{font-size:12px;color:#666;line-height:1.3}`)
|
||||
b.WriteString(`pre{white-space:pre-wrap;word-break:break-all;background:#f5f5f7;padding:12px;border-radius:8px;font-size:11px;max-height:420px;overflow:auto}`)
|
||||
b.WriteString(`@media print{body{margin:0}}</style></head><body>`)
|
||||
|
||||
b.WriteString(`<div class="brand">` + moexMostLogoSVG +
|
||||
`<span class="svc">Сервис быстрых переводов ценных бумаг<br>MOEX МОСТ · НКО АО НРД</span>` + orgLogoIMG() + `</div>`)
|
||||
b.WriteString(`<h1>Отчёт о входящем переводе ценных бумаг (M2M / MOEX МОСТ)</h1>`)
|
||||
b.WriteString(`<div class="muted">Доказательство для контролирующих органов. Внутренний id bj: ` + esc(it.ID) +
|
||||
` · Сформировано: ` + time.Now().In(time.FixedZone("МСК", 3*3600)).Format("02.01.2006 15:04:05") + ` МСК</div>`)
|
||||
|
||||
// Реквизиты
|
||||
b.WriteString(`<h2>Реквизиты перевода</h2><table>`)
|
||||
row := func(k, v string) { b.WriteString(`<tr><td class="k">` + k + `</td><td>` + v + `</td></tr>`) }
|
||||
row("GUID перевода", `<span class="mono">`+esc(it.GUID)+`</span>`)
|
||||
from := esc(it.SenderCode)
|
||||
if it.SenderINN != "" {
|
||||
from += ` · ИНН ` + esc(it.SenderINN)
|
||||
}
|
||||
if senderName != "" {
|
||||
from = `<strong>` + esc(senderName) + `</strong> · ` + from
|
||||
}
|
||||
row("Передающий депозитарий (списывающий)", from)
|
||||
to := esc(it.ReceiverCode)
|
||||
if it.ReceivingINN != "" {
|
||||
to += ` · ИНН ` + esc(it.ReceivingINN)
|
||||
}
|
||||
row("Принимающий депозитарий (мы)", to)
|
||||
row("Инвестор (ФИО)", `<strong>`+esc(fio)+`</strong>`)
|
||||
doc := esc(it.DocType)
|
||||
if it.DocSeries != "" {
|
||||
doc += ` · серия ` + esc(it.DocSeries)
|
||||
}
|
||||
if it.DocNumber != "" {
|
||||
doc += ` · № ` + esc(it.DocNumber)
|
||||
}
|
||||
row("Документ инвестора", doc)
|
||||
b.WriteString(`</table>`)
|
||||
|
||||
// Хронология (фиксация времени прохождения)
|
||||
b.WriteString(`<h2>Хронология прохождения сообщения</h2><table>`)
|
||||
row("Запрос сформирован отправителем", msk(it.RequestCreated))
|
||||
row("Получено нами (забрано из шлюза ИШ)", msk(it.ReceivedAt)+` <span class="muted">(время доставки: `+dur(it.RequestCreated, it.ReceivedAt)+`)</span>`)
|
||||
if it.Status == "answered" {
|
||||
row("Ответ (решение) отправлен", msk(it.AnsweredAt)+` <span class="muted">(время обработки: `+dur(it.ReceivedAt, it.AnsweredAt)+`)</span>`)
|
||||
} else {
|
||||
row("Ответ (решение) отправлен", `— <span class="muted">(ещё не отвечено)</span>`)
|
||||
}
|
||||
status := "ждёт решения"
|
||||
if it.Status == "answered" {
|
||||
status = "отвечено"
|
||||
if it.Auto {
|
||||
status += " (автоматически, после проверки в Fancy)"
|
||||
}
|
||||
}
|
||||
row("Статус", esc(status))
|
||||
if it.AnswerNote != "" {
|
||||
row("Результат", esc(it.AnswerNote))
|
||||
}
|
||||
b.WriteString(`</table>`)
|
||||
|
||||
// Бумаги
|
||||
b.WriteString(`<h2>Ценные бумаги</h2><table>`)
|
||||
b.WriteString(`<tr><th>Код НРД / ISIN</th><th>Кол-во</th><th>Счёт зачисления</th><th>Решение</th></tr>`)
|
||||
for _, s := range it.Securities {
|
||||
code := esc(s.SecurityCode)
|
||||
if s.ISIN != "" {
|
||||
code += ` / ` + esc(s.ISIN)
|
||||
}
|
||||
acc := ""
|
||||
if s.DeponentCode != "" {
|
||||
acc += `депонент ` + esc(s.DeponentCode)
|
||||
}
|
||||
if s.AccountID != "" {
|
||||
acc += `<br>счёт ` + esc(s.AccountID)
|
||||
}
|
||||
if s.SectionID != "" {
|
||||
acc += ` / раздел ` + esc(s.SectionID)
|
||||
}
|
||||
if s.SettleINN != "" {
|
||||
acc += `<br>расч.деп. ИНН ` + esc(s.SettleINN)
|
||||
}
|
||||
dec := "—"
|
||||
switch s.Decision {
|
||||
case "confirmed":
|
||||
dec = "принято (зачислено)"
|
||||
case "rejected":
|
||||
dec = "отказ · " + esc(m2mCodeLabel(s.RejectCode))
|
||||
}
|
||||
b.WriteString(`<tr><td class="mono">` + code + `</td><td>` + esc(s.Quantity) + `</td><td style="font-size:12px">` + acc + `</td><td>` + dec + `</td></tr>`)
|
||||
}
|
||||
b.WriteString(`</table>`)
|
||||
|
||||
// Депозитарное исполнение: поручение 16/1 (ORDXM) + ответы НРД (движение).
|
||||
if it.OrderSubmitted {
|
||||
resps := findOrderResponses(it.OrderContrDID)
|
||||
var ms010At string
|
||||
for _, rp := range resps {
|
||||
if rp.Done {
|
||||
ms010At = rp.Time
|
||||
}
|
||||
}
|
||||
b.WriteString(`<h2>Депозитарное исполнение (поручение 16/1) — хронология</h2><table>`)
|
||||
if dr := draftReceivedTime(it.GUID); dr != "" {
|
||||
row("Черновик 16/1 получен от НРД (MF010)", esc(dr)+` МСК`)
|
||||
}
|
||||
row("Поручение 16/1 подано в НРД", msk(it.OrderAt))
|
||||
if ms010At != "" {
|
||||
row("Отчёт MS010 получен (операция проведена)", esc(ms010At)+` МСК <span class="muted">— бумаги зачислены</span>`)
|
||||
} else {
|
||||
row("Отчёт MS010 получен", `— <span class="muted">(ещё не получен)</span>`)
|
||||
}
|
||||
row("Тип ЭД / пакет(ы) ИШ", "ORDXM · "+esc(it.OrderPkgs))
|
||||
if it.OrderContrDID != "" {
|
||||
row("Номер поручения (contr_d_id)", esc(it.OrderContrDID))
|
||||
}
|
||||
b.WriteString(`</table>`)
|
||||
if len(resps) > 0 {
|
||||
b.WriteString(`<h3 style="font-size:13px;margin:12px 0 6px">Ответы НРД по поручению</h3><table>`)
|
||||
b.WriteString(`<tr><th>Время получения</th><th>Тип ответа</th><th>Пакет</th></tr>`)
|
||||
for _, rp := range resps {
|
||||
b.WriteString(`<tr><td>` + esc(rp.Time) + ` МСК</td><td>` + esc(rp.Kind) + `</td><td class="mono">` + esc(rp.Pkg) + `</td></tr>`)
|
||||
}
|
||||
b.WriteString(`</table>`)
|
||||
}
|
||||
if it.OrderXML != "" {
|
||||
b.WriteString(`<h3 style="font-size:13px;margin:12px 0 6px">Отправленное поручение 16/1 (дословно)</h3><pre>` + esc(it.OrderXML) + `</pre>`)
|
||||
}
|
||||
}
|
||||
|
||||
// Все поля запроса (разбор XML)
|
||||
if fields := renderXMLFieldsTable(it.RawXML); fields != "" {
|
||||
b.WriteString(`<h2>Все поля запроса (разбор XML)</h2>` + fields)
|
||||
}
|
||||
|
||||
// Приложения — сырые XML (также отдельными файлами в архиве)
|
||||
if it.RawXML != "" {
|
||||
b.WriteString(`<h2>Исходный XML запроса контрагента (request.xml)</h2><pre>` + esc(it.RawXML) + `</pre>`)
|
||||
}
|
||||
if it.DecisionXML != "" {
|
||||
b.WriteString(`<h2>XML нашего ответа-решения (decision.xml)</h2><pre>` + esc(it.DecisionXML) + `</pre>`)
|
||||
}
|
||||
|
||||
b.WriteString(`<p class="muted" style="margin-top:20px;font-size:11px">Отчёт сформирован автоматически системой Bridge-and-Join-s (bj). Логотип MOEX МОСТ — собственность НКО АО НРД.</p>`)
|
||||
b.WriteString(`</body></html>`)
|
||||
return []byte(b.String())
|
||||
}
|
||||
Reference in New Issue
Block a user