Документы НРД по поручению: папки с понятными названиями, опись и PDF
Пакеты НРД из INBOX ИШ (K…, 508…) распознаются по содержимому: квитанция о регистрации (или отказ), уведомление GS036, отчёт о статусе GS116 (сквитовано / не сквитовано), отчёт об исполнении MS010, ошибка. В ZIP ответа НРД и выгрузках для регулятора (16 и 16/1) — папки «01 Квитанция о регистрации поручения — K1699092» и т. д., «Опись.txt» с назначением и реквизитами каждого документа и «Документы НРД.pdf» (читаемое представление, печать через Chromium; без него — HTML). В карточке перевода ответы НРД подписаны так же. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,10 +45,8 @@ func exportIncoming(w http.ResponseWriter, r *http.Request, deps incomingDeps, i
|
||||
}
|
||||
}
|
||||
// Ответ НРД по поручению (отчёт MS010 + квитанции) — сырые файлы из INBOX.
|
||||
for _, rf := range collectOrderResponseRaw(it.OrderContrDID) {
|
||||
if f, e := zw.Create("otvet-nrd/" + rf.Name); e == nil {
|
||||
_, _ = f.Write(rf.Data)
|
||||
}
|
||||
if docs := nsdOrderDocs(it.OrderContrDID); len(docs) > 0 {
|
||||
writeNSDDocsZip(zw, "Ответ НРД/", "Документы НРД по поручению 16/1 (зачисление) № "+it.OrderContrDID, docs, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,8 +139,8 @@ func submitIncomingOrderFor(ctx context.Context, deps incomingDeps, it *Incoming
|
||||
// writeOrderResponseZip отдаёт на скачивание ZIP с сырыми файлами ответа НРД
|
||||
// (отчёт MS010 + квитанции) по поручению с данным contr_d_id.
|
||||
func writeOrderResponseZip(w http.ResponseWriter, contrDID, fname string) {
|
||||
files := collectOrderResponseRaw(contrDID)
|
||||
if len(files) == 0 {
|
||||
docs := nsdOrderDocs(contrDID)
|
||||
if len(docs) == 0 {
|
||||
http.Error(w, "ответ НРД (MS010) по этому поручению в INBOX ИШ ещё не найден", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
@@ -148,11 +148,7 @@ func writeOrderResponseZip(w http.ResponseWriter, contrDID, fname string) {
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.zip"`, fname))
|
||||
zw := zip.NewWriter(w)
|
||||
defer zw.Close()
|
||||
for _, f := range files {
|
||||
if wf, e := zw.Create(f.Name); e == nil {
|
||||
_, _ = wf.Write(f.Data)
|
||||
}
|
||||
}
|
||||
writeNSDDocsZip(zw, "", "Документы НРД по поручению № "+contrDID, docs, true)
|
||||
}
|
||||
|
||||
// downloadSentOrder отдаёт на скачивание XML отправленного поручения 16/1
|
||||
@@ -360,8 +356,9 @@ func orderRespKind(pkg, body string) (string, bool) {
|
||||
u := strings.ToUpper(body)
|
||||
low := strings.ToLower(body)
|
||||
switch {
|
||||
case strings.Contains(u, "MS010") || strings.Contains(u, "SESE.025"):
|
||||
return "Отчёт по поручению депо MS010 — операция проведена, бумаги зачислены", true
|
||||
case strings.Contains(u, "MS010") || strings.Contains(u, "SESE.025") || strings.Contains(u, "SESE.024") || strings.Contains(u, "GS036") || strings.Contains(u, "GS116") || strings.Contains(low, "<reg_no>"):
|
||||
d := classifyNSDDoc(pkg, body)
|
||||
return d.Title + " — " + d.About, d.Done
|
||||
case strings.Contains(u, "ERROR") || strings.Contains(strings.ToUpper(pkg), "ERROR") || strings.Contains(low, "ошибк"):
|
||||
return "Ошибка обработки пакета (CERROR)", false
|
||||
case strings.Contains(u, "REORD") || strings.Contains(low, "принят"):
|
||||
|
||||
@@ -115,10 +115,8 @@ func writeIncomingBundleZip(zw *zip.Writer, it *IncomingTransfer, prefix string)
|
||||
_, _ = f.Write(faithfulXMLBytes(it.OrderXML))
|
||||
}
|
||||
}
|
||||
for _, rf := range collectOrderResponseRaw(it.OrderContrDID) {
|
||||
if f, e := zw.Create(prefix + "otvet-nrd/" + rf.Name); e == nil {
|
||||
_, _ = f.Write(rf.Data)
|
||||
}
|
||||
if docs := nsdOrderDocs(it.OrderContrDID); len(docs) > 0 {
|
||||
writeNSDDocsZip(zw, prefix+"Ответ НРД/", "Документы НРД по поручению 16/1 № "+it.OrderContrDID, docs, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
package lkgateway
|
||||
|
||||
// nsddocs.go — документы НРД по депо-поручению в понятном виде. Пакеты в INBOX ИШ называются
|
||||
// номерами (K1699092, 50818781 …); по содержимому определяем, что это за документ (квитанция о
|
||||
// регистрации, уведомление GS036, отчёт о статусе GS116, отчёт об исполнении MS010, ошибка) и
|
||||
// выгружаем папками с названием, описью и PDF для депозитария (как печатные формы из канала НРД).
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// nsdDoc — один документ (пакет) НРД по поручению.
|
||||
type nsdDoc struct {
|
||||
Pkg string
|
||||
Time time.Time
|
||||
Code string // REG | REG_REJECT | GS036 | GS116 | MS010 | ERROR | ""
|
||||
Title string
|
||||
About string
|
||||
Fields [][2]string
|
||||
Done bool // отчёт об исполнении (MS010)
|
||||
Rejected bool
|
||||
Files []respFile // имена без папки пакета
|
||||
}
|
||||
|
||||
var nsdTagCache sync.Map // имя элемента → *regexp.Regexp
|
||||
|
||||
// xmlTag — первое значение элемента name (без учёта префикса пространства имён).
|
||||
func xmlTag(body, name string) string {
|
||||
v, ok := nsdTagCache.Load(name)
|
||||
if !ok {
|
||||
v, _ = nsdTagCache.LoadOrStore(name, regexp.MustCompile(`<(?:\w+:)?`+name+`(?:\s[^>]*)?>\s*([^<]*?)\s*</(?:\w+:)?`+name+`>`))
|
||||
}
|
||||
if m := v.(*regexp.Regexp).FindStringSubmatch(body); m != nil {
|
||||
return html.UnescapeString(m[1])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func addField(d *nsdDoc, k, v string) {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
d.Fields = append(d.Fields, [2]string{k, strings.TrimSpace(v)})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
othrIDRe = regexp.MustCompile(`(?s)<OthrId>\s*<Id>([^<]+)</Id>`)
|
||||
sfkpgAcctRe = regexp.MustCompile(`(?s)<SfkpgAcct>\s*<Id>([^<]+)</Id>`)
|
||||
gs036PairRe = regexp.MustCompile(`(?s)<Ntfctns>\s*(?:<NtfctnHdr>([^<]*)</NtfctnHdr>)?\s*(?:<NtfctnTxt>([^<]*)</NtfctnTxt>)?\s*</Ntfctns>`)
|
||||
prtryIDRe = regexp.MustCompile(`(?s)<(Rcvg|Dlvrg)SttlmPties>.*?<PrtryId>\s*<Id>([^<]+)</Id>`)
|
||||
qtyUnitRe = regexp.MustCompile(`(?s)<(?:SttldQty|SttlmQty)>\s*<Qty>\s*<(?:Unit|FaceAmt|AmtsdVal)>([^<]+)<`)
|
||||
dateInRe = regexp.MustCompile(`(?s)<(FctvSttlmDt|SttlmDt)>\s*<Dt>\s*<Dt>([^<]+)</Dt>`)
|
||||
)
|
||||
|
||||
func securityLine(body string) string {
|
||||
code, isin, desc := "", xmlTag(body, "ISIN"), xmlTag(body, "Desc")
|
||||
if m := othrIDRe.FindStringSubmatch(body); m != nil {
|
||||
code = m[1]
|
||||
}
|
||||
parts := []string{}
|
||||
for _, p := range []string{code, isin, desc} {
|
||||
if p != "" {
|
||||
parts = append(parts, p)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " · ")
|
||||
}
|
||||
|
||||
// classifyNSDDoc — что за документ НРД в пакете pkg (текст всех файлов пакета — body).
|
||||
func classifyNSDDoc(pkg, body string) nsdDoc {
|
||||
d := nsdDoc{Pkg: pkg}
|
||||
biz := xmlTag(body, "BizSvc")
|
||||
up := strings.ToUpper(body)
|
||||
switch {
|
||||
case biz == "MS010" || strings.Contains(body, "sese.025"):
|
||||
d.Code, d.Done = "MS010", true
|
||||
op := map[string]string{"DELI": "списание", "RECE": "зачисление"}[xmlTag(body, "SctiesMvmntTp")]
|
||||
d.Title = "Отчёт об исполнении поручения (MS010)"
|
||||
d.About = "Итоговый документ: НРД исполнил депо-поручение" + pick3(op != "", " — "+op+" бумаг проведено", "") + "."
|
||||
addField(&d, "№ поручения", xmlTag(body, "AcctOwnrTxId"))
|
||||
addField(&d, "Референс сделки M2M", xmlTag(body, "TradId"))
|
||||
addField(&d, "Операция", op)
|
||||
addField(&d, "Ценная бумага", securityLine(body))
|
||||
if m := qtyUnitRe.FindStringSubmatch(body); m != nil {
|
||||
addField(&d, "Количество", m[1])
|
||||
}
|
||||
if m := sfkpgAcctRe.FindStringSubmatch(body); m != nil {
|
||||
addField(&d, "Счёт депо", m[1])
|
||||
}
|
||||
if m := dateInRe.FindStringSubmatch(body); m != nil {
|
||||
addField(&d, "Дата расчётов", m[2])
|
||||
}
|
||||
case biz == "GS116" || strings.Contains(body, "sese.024"):
|
||||
d.Code = "GS116"
|
||||
d.Title = "Отчёт о статусе поручения (GS116)"
|
||||
switch {
|
||||
case strings.Contains(body, "<Mtchd>"):
|
||||
d.About = "Поручение сквитовано со встречным поручением контрагента (MACH) — НРД переходит к исполнению."
|
||||
addField(&d, "Статус сверки", "сквитовано (MACH)")
|
||||
case strings.Contains(body, "<NMtchd>"):
|
||||
d.About = "Поручение не сквитовано: встречное поручение контрагента не найдено или не совпадает по реквизитам."
|
||||
addField(&d, "Статус сверки", "не сквитовано")
|
||||
default:
|
||||
d.About = "Промежуточный отчёт НРД о статусе поручения."
|
||||
}
|
||||
addField(&d, "№ поручения", xmlTag(body, "AcctOwnrTxId"))
|
||||
addField(&d, "Рег. № в НРД", xmlTag(body, "AcctSvcrTxId"))
|
||||
addField(&d, "Ценная бумага", securityLine(body))
|
||||
if m := qtyUnitRe.FindStringSubmatch(body); m != nil {
|
||||
addField(&d, "Количество", m[1])
|
||||
}
|
||||
if m := prtryIDRe.FindStringSubmatch(body); m != nil {
|
||||
addField(&d, pick3(m[1] == "Rcvg", "Получатель", "Отправитель"), m[2])
|
||||
}
|
||||
if m := dateInRe.FindStringSubmatch(body); m != nil {
|
||||
addField(&d, "Дата расчётов", m[2])
|
||||
}
|
||||
case biz == "GS036" || strings.Contains(body, "gs036"):
|
||||
d.Code = "GS036"
|
||||
subject := ""
|
||||
for _, m := range gs036PairRe.FindAllStringSubmatch(body, -1) {
|
||||
hdr, txt := strings.TrimSpace(html.UnescapeString(m[1])), strings.TrimSpace(html.UnescapeString(m[2]))
|
||||
switch {
|
||||
case hdr == "" && txt != "" && subject == "":
|
||||
subject = txt
|
||||
case hdr != "":
|
||||
addField(&d, hdr, txt)
|
||||
}
|
||||
}
|
||||
d.Title = "Уведомление НРД (GS036)"
|
||||
if subject = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(subject, "об "), "о ")); subject != "" {
|
||||
prep := "о "
|
||||
if r := []rune(subject); strings.ContainsRune("аоуэиыАОУЭИЫ", r[0]) {
|
||||
prep = "об "
|
||||
}
|
||||
d.Title = "Уведомление " + prep + strings.TrimSuffix(subject, " к поручению депо") + " (GS036)"
|
||||
}
|
||||
d.About = "Промежуточное уведомление по поручению; итог — в отчёте о статусе (GS116) и об исполнении (MS010)."
|
||||
case strings.Contains(body, "<reg_no>") || strings.Contains(body, "<contragent_doc_id>"):
|
||||
abort, cause := xmlTag(body, "abort_code"), xmlTag(body, "abort_cause")
|
||||
if abort != "" || cause != "" {
|
||||
d.Code, d.Rejected = "REG_REJECT", true
|
||||
d.Title = "Отказ в регистрации поручения"
|
||||
d.About = "НРД не принял поручение к исполнению: " + strings.TrimSpace(abort+" "+cause) + "."
|
||||
} else {
|
||||
d.Code = "REG"
|
||||
d.Title = "Квитанция о регистрации поручения"
|
||||
d.About = "НРД принял поручение к обработке и присвоил регистрационный номер."
|
||||
}
|
||||
addField(&d, "Рег. № в НРД", xmlTag(body, "reg_no"))
|
||||
addField(&d, "Дата регистрации", xmlTag(body, "reg_date"))
|
||||
addField(&d, "№ поручения", xmlTag(body, "contragent_doc_id"))
|
||||
addField(&d, "Код отказа", abort)
|
||||
addField(&d, "Причина отказа", cause)
|
||||
case strings.Contains(up, "ERROR") || strings.Contains(strings.ToUpper(pkg), "ERROR") || strings.Contains(strings.ToLower(body), "ошибк"):
|
||||
d.Code, d.Rejected = "ERROR", true
|
||||
d.Title = "Ошибка обработки пакета"
|
||||
d.About = "НРД не смог обработать пакет: " + orderRespSummary(body)
|
||||
case biz != "":
|
||||
d.Title = "Отчёт НРД (" + biz + ")"
|
||||
d.About = "Документ НРД по поручению."
|
||||
default:
|
||||
d.Title = "Документ НРД"
|
||||
d.About = "Документ НРД по поручению (тип не распознан — см. файлы в папке)."
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// pkgFiles — файлы пакета (xml/rpt/htm/dbf/sgn, содержимое zip) без имени папки.
|
||||
func pkgFiles(dir string) []respFile {
|
||||
var out []respFile
|
||||
files, _ := os.ReadDir(dir)
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
p := filepath.Join(dir, f.Name())
|
||||
low := strings.ToLower(f.Name())
|
||||
switch {
|
||||
case strings.HasSuffix(low, ".xml"), strings.HasSuffix(low, ".rpt"), strings.HasSuffix(low, ".htm"), strings.HasSuffix(low, ".dbf"), strings.HasSuffix(low, ".sgn"):
|
||||
if b, e := os.ReadFile(p); e == nil {
|
||||
out = append(out, respFile{Name: f.Name(), Data: b})
|
||||
}
|
||||
case strings.HasSuffix(low, ".zip"):
|
||||
if b, e := os.ReadFile(p); e == nil {
|
||||
if zr, e2 := zip.NewReader(bytes.NewReader(b), int64(len(b))); e2 == nil {
|
||||
for _, zf := range zr.File {
|
||||
if rc, e3 := zf.Open(); e3 == nil {
|
||||
data, _ := io.ReadAll(rc)
|
||||
rc.Close()
|
||||
out = append(out, respFile{Name: zf.Name, Data: data})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// nsdOrderDocs — документы НРД по поручениям (contr_d_id), по времени.
|
||||
func nsdOrderDocs(contrDIDs ...string) []nsdDoc {
|
||||
entries, err := os.ReadDir(igwInboxDir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
var out []nsdDoc
|
||||
for _, cd := range contrDIDs {
|
||||
cd = strings.TrimSpace(cd)
|
||||
if cd == "" {
|
||||
continue
|
||||
}
|
||||
for _, e := range entries {
|
||||
dir := e.Name()
|
||||
if !e.IsDir() || strings.HasPrefix(dir, "#") || dir == "INBOX" || seen[dir] {
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(igwInboxDir, dir)
|
||||
body, mt := readPkgText(full)
|
||||
if body == "" || !strings.Contains(body, cd) {
|
||||
continue
|
||||
}
|
||||
seen[dir] = true
|
||||
d := classifyNSDDoc(dir, body)
|
||||
d.Time, d.Files = mt, pkgFiles(full)
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Time.Before(out[j].Time) })
|
||||
return out
|
||||
}
|
||||
|
||||
var folderBadChars = strings.NewReplacer("/", "-", `\`, "-", ":", " ", "*", "", "?", "", `"`, "", "<", "", ">", "", "|", "")
|
||||
|
||||
// nsdDocFolder — «02 Уведомление об отсутствии встречного поручения (GS036) — 50818777».
|
||||
func nsdDocFolder(i int, d nsdDoc) string {
|
||||
return folderBadChars.Replace(fmt.Sprintf("%02d %s — %s", i+1, d.Title, d.Pkg))
|
||||
}
|
||||
|
||||
func mskTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "—"
|
||||
}
|
||||
return t.In(time.FixedZone("МСК", 3*3600)).Format("02.01.2006 15:04:05") + " МСК"
|
||||
}
|
||||
|
||||
// writeNSDDocsZip — документы НРД папками с понятными названиями + «Опись.txt» + PDF (если withPDF).
|
||||
func writeNSDDocsZip(zw *zip.Writer, prefix, title string, docs []nsdDoc, withPDF bool) {
|
||||
var opis strings.Builder
|
||||
opis.WriteString(title + "\r\n")
|
||||
opis.WriteString("Сформировано bj " + mskTime(time.Now()) + ". Папки — документы НРД в порядке поступления; внутри — подлинные файлы из INBOX ИШ (XML, печатная форма .rpt, таблицы .dbf, подпись .SGN).\r\n\r\n")
|
||||
for i, d := range docs {
|
||||
folder := nsdDocFolder(i, d)
|
||||
for _, f := range d.Files {
|
||||
if w, e := zw.Create(prefix + folder + "/" + f.Name); e == nil {
|
||||
_, _ = w.Write(f.Data)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&opis, "%s\r\n Получен: %s\r\n Что это: %s\r\n", folder, mskTime(d.Time), d.About)
|
||||
for _, kv := range d.Fields {
|
||||
fmt.Fprintf(&opis, " %s: %s\r\n", kv[0], kv[1])
|
||||
}
|
||||
opis.WriteString("\r\n")
|
||||
}
|
||||
if w, e := zw.Create(prefix + "Опись.txt"); e == nil {
|
||||
_, _ = w.Write([]byte("\ufeff" + opis.String()))
|
||||
}
|
||||
if !withPDF {
|
||||
return
|
||||
}
|
||||
page := nsdDocsReportHTML(title, docs)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
if pdf, err := htmlToPDF(ctx, page); err == nil {
|
||||
if w, e := zw.Create(prefix + "Документы НРД.pdf"); e == nil {
|
||||
_, _ = w.Write(pdf)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
log.Printf("nsd-docs: PDF не собран (%v) — в архиве HTML", err)
|
||||
}
|
||||
if w, e := zw.Create(prefix + "Документы НРД.html"); e == nil {
|
||||
_, _ = w.Write(page)
|
||||
}
|
||||
}
|
||||
|
||||
// nsdDocsReportHTML — читаемая сводка документов НРД (для PDF).
|
||||
func nsdDocsReportHTML(title string, docs []nsdDoc) []byte {
|
||||
esc := html.EscapeString
|
||||
var b strings.Builder
|
||||
b.WriteString(`<!doctype html><html lang="ru"><head><meta charset="utf-8"><title>` + esc(title) + `</title><style>`)
|
||||
b.WriteString(`@page{size:A4;margin:16mm 14mm}body{font-family:"PT Sans","DejaVu Sans",Arial,sans-serif;color:#1a1a1a;font-size:12px;line-height:1.45}`)
|
||||
b.WriteString(`h1{font-size:18px;margin:0 0 4px}.sub{color:#666;margin:0 0 14px}.doc{border:1px solid #cfd4dc;border-radius:8px;padding:10px 12px;margin:0 0 12px;break-inside:avoid}`)
|
||||
b.WriteString(`.doc h2{font-size:14px;margin:0 0 2px}.meta{color:#666;font-size:11px;margin-bottom:6px}.about{margin:4px 0 8px}`)
|
||||
b.WriteString(`table{border-collapse:collapse;width:100%}td{border-top:1px solid #e4e7ec;padding:4px 6px;vertical-align:top}td.k{color:#555;width:38%}`)
|
||||
b.WriteString(`.ok{color:#0f7a37}.bad{color:#b3261e}.brand{border-bottom:2px solid #ff0508;padding-bottom:8px;margin-bottom:12px}</style></head><body>`)
|
||||
b.WriteString(`<div class="brand"><h1>` + esc(title) + `</h1><p class="sub">Документы НРД по депо-поручению в порядке поступления · сформировано bj ` + esc(mskTime(time.Now())) + `</p></div>`)
|
||||
for i, d := range docs {
|
||||
cls := ""
|
||||
switch {
|
||||
case d.Done:
|
||||
cls = "ok"
|
||||
case d.Rejected:
|
||||
cls = "bad"
|
||||
}
|
||||
fmt.Fprintf(&b, `<div class="doc"><h2 class="%s">%d. %s</h2><div class="meta">пакет НРД %s · получен %s</div><div class="about">%s</div>`,
|
||||
cls, i+1, esc(d.Title), esc(d.Pkg), esc(mskTime(d.Time)), esc(d.About))
|
||||
if len(d.Fields) > 0 {
|
||||
b.WriteString("<table>")
|
||||
for _, kv := range d.Fields {
|
||||
fmt.Fprintf(&b, `<tr><td class="k">%s</td><td>%s</td></tr>`, esc(kv[0]), esc(kv[1]))
|
||||
}
|
||||
b.WriteString("</table>")
|
||||
}
|
||||
b.WriteString("</div>")
|
||||
}
|
||||
b.WriteString(`<p class="sub">Подлинные документы — файлы в папках архива (XML с подписью НРД .SGN). Этот PDF — их читаемое представление.</p></body></html>`)
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
// htmlToPDF — печать HTML в PDF headless-браузером (Chromium есть на серверах bj с Astra).
|
||||
func htmlToPDF(ctx context.Context, page []byte) ([]byte, error) {
|
||||
bin := ""
|
||||
for _, name := range []string{"chromium", "chromium-browser", "google-chrome"} {
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
bin = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if bin == "" {
|
||||
return nil, fmt.Errorf("браузер для печати в PDF не найден")
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "bj-pdf-")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
in, out := filepath.Join(dir, "page.html"), filepath.Join(dir, "page.pdf")
|
||||
if err := os.WriteFile(in, page, 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, bin, "--headless=new", "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage",
|
||||
"--no-pdf-header-footer", "--user-data-dir="+filepath.Join(dir, "profile"), "--print-to-pdf="+out, "file://"+in)
|
||||
cmd.Env = append(os.Environ(), "HOME="+dir)
|
||||
if msg, err := cmd.CombinedOutput(); err != nil {
|
||||
return nil, fmt.Errorf("%v: %s", err, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
return os.ReadFile(out)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build live
|
||||
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Живой прогон: документы НРД по поручению 20608 (исходящий 16.09) → ZIP с папками, описью и PDF.
|
||||
// go test -c -tags live ./internal/lkgateway/ ; запуск от bj: -test.run TestNSDDocsLive -test.v
|
||||
func TestNSDDocsLive(t *testing.T) {
|
||||
docs := nsdOrderDocs("20608")
|
||||
for i, d := range docs {
|
||||
t.Logf("%s | %v", nsdDocFolder(i, d), d.Fields)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
writeNSDDocsZip(zw, "", "Документы НРД по поручению 16 (списание) № 20608", docs, true)
|
||||
zw.Close()
|
||||
zr, _ := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
||||
for _, f := range zr.File {
|
||||
if f.Name == "Опись.txt" || f.Name == "Документы НРД.pdf" || f.Name == "Документы НРД.html" {
|
||||
t.Logf("в архиве: %s (%d байт)", f.Name, f.UncompressedSize64)
|
||||
}
|
||||
}
|
||||
_ = os.WriteFile(os.TempDir()+"/nsd-docs-20608.zip", buf.Bytes(), 0o644)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassifyNSDDoc(t *testing.T) {
|
||||
cases := []struct{ pkg, body, code, title string }{
|
||||
{"K1699092", `<Batch><Document><reg_no>14462249</reg_no><reg_date>2026-09-16 16:03:53</reg_date><contragent_doc_id>20608</contragent_doc_id><abort_code /><abort_cause /></Document></Batch>`,
|
||||
"REG", "Квитанция о регистрации поручения"},
|
||||
{"K1", `<Batch><Document><reg_no>1</reg_no><contragent_doc_id>20608</contragent_doc_id><abort_code>12</abort_code><abort_cause>Нет встречного</abort_cause></Document></Batch>`,
|
||||
"REG_REJECT", "Отказ в регистрации поручения"},
|
||||
{"50818777", `<BizSvc>GS036</BizSvc><GS036><Ntfctns><NtfctnTxt> отсутствии встречного поручения к поручению депо</NtfctnTxt></Ntfctns><Ntfctns><NtfctnHdr>Код операции</NtfctnHdr><NtfctnTxt>16</NtfctnTxt></Ntfctns></GS036>`,
|
||||
"GS036", "Уведомление об отсутствии встречного поручения (GS036)"},
|
||||
{"50818779", `<BizSvc>GS116</BizSvc><AcctOwnrTxId>20608</AcctOwnrTxId><AcctSvcrTxId>14462249</AcctSvcrTxId><MtchgSts><Mtchd><Rsn><Id>MACH</Id></Rsn></Mtchd></MtchgSts>`,
|
||||
"GS116", "Отчёт о статусе поручения (GS116)"},
|
||||
{"50818781", `<BizSvc>MS010</BizSvc><AcctOwnrTxId>20608</AcctOwnrTxId><SctiesMvmntTp>DELI</SctiesMvmntTp><SttldQty><Qty><Unit>10</Unit></Qty></SttldQty><SfkpgAcct><Id>HL171004001C</Id></SfkpgAcct>`,
|
||||
"MS010", "Отчёт об исполнении поручения (MS010)"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
d := classifyNSDDoc(c.pkg, c.body)
|
||||
if d.Code != c.code || d.Title != c.title {
|
||||
t.Errorf("%s: %s «%s», ожидали %s «%s»", c.pkg, d.Code, d.Title, c.code, c.title)
|
||||
}
|
||||
}
|
||||
ms := classifyNSDDoc("50818781", cases[4].body)
|
||||
if !ms.Done || !strings.Contains(ms.About, "списание") {
|
||||
t.Errorf("MS010: %+v", ms)
|
||||
}
|
||||
var acc string
|
||||
for _, kv := range ms.Fields {
|
||||
if kv[0] == "Счёт депо" {
|
||||
acc = kv[1]
|
||||
}
|
||||
}
|
||||
if acc != "HL171004001C" {
|
||||
t.Errorf("счёт депо: %q (%v)", acc, ms.Fields)
|
||||
}
|
||||
if f := nsdDocFolder(1, classifyNSDDoc("50818777", cases[2].body)); f != "02 Уведомление об отсутствии встречного поручения (GS036) — 50818777" {
|
||||
t.Errorf("папка: %q", f)
|
||||
}
|
||||
}
|
||||
@@ -32,10 +32,8 @@ func exportOutgoing(w http.ResponseWriter, r *http.Request, deps outgoingDeps, g
|
||||
}
|
||||
}
|
||||
// Ответ НРД по поручению (отчёт MS010 + квитанции) — сырые файлы из INBOX.
|
||||
for _, rf := range collectOutgoingResponseRaw(deal.ContrDIDs) {
|
||||
if f, e := zw.Create("otvet-nrd/" + rf.Name); e == nil {
|
||||
_, _ = f.Write(rf.Data)
|
||||
}
|
||||
if docs := nsdOrderDocs(deal.ContrDIDs...); len(docs) > 0 {
|
||||
writeNSDDocsZip(zw, "Ответ НРД/", "Документы НРД по поручению 16 (списание) № "+strings.Join(deal.ContrDIDs, ", "), docs, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -341,19 +341,15 @@ func registerOutgoing(mux *http.ServeMux, a *admin, deps outgoingDeps) {
|
||||
http.Error(w, "исходящее поручение не найдено", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
files := collectOutgoingResponseRaw(sub.ContrDIDs)
|
||||
if len(files) == 0 {
|
||||
docs := nsdOrderDocs(sub.ContrDIDs...)
|
||||
if len(docs) == 0 {
|
||||
http.Error(w, "ответ НРД (MS010) по этому поручению в INBOX ИШ ещё не найден", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="otvet-nrd-ms010-`+shortID(guid)+`.zip"`)
|
||||
zw := zip.NewWriter(w)
|
||||
for _, f := range files {
|
||||
if wf, e := zw.Create(f.Name); e == nil {
|
||||
_, _ = wf.Write(f.Data)
|
||||
}
|
||||
}
|
||||
writeNSDDocsZip(zw, "", "Документы НРД по поручению 16 (списание) № "+strings.Join(sub.ContrDIDs, ", "), docs, true)
|
||||
zw.Close()
|
||||
case strings.HasSuffix(p, "/export.zip"):
|
||||
exportOutgoing(w, r, deps, strings.TrimSuffix(p, "/export.zip"))
|
||||
|
||||
Reference in New Issue
Block a user