Документы НРД по поручению: папки с понятными названиями, опись и 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:
zuevav
2026-09-16 16:26:25 +03:00
parent 6a2e9b7122
commit 28c0ed7349
8 changed files with 451 additions and 28 deletions
+2 -4
View File
@@ -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)
}
}
+6 -9
View File
@@ -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, "принят"):
+2 -4
View File
@@ -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)
}
}
+363
View File
@@ -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)
}
+30
View File
@@ -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)
}
+43
View File
@@ -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)
}
}
+2 -4
View File
@@ -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)
}
}
+3 -7
View File
@@ -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"))