package lkgateway // m2mtest_bundle.go — единая выгрузка отчётов по проведённым тестам M2M одним ZIP: // комплект доказательств по каждому пройденному кейсу (исходящие заявки + входящие // переводы) плюс сопроводительная HTML-сводка «что провели за сегодня». Удобно // отправить контрагенту, чтобы он проверил результаты тестирования. import ( "archive/zip" "bytes" "fmt" "html" "net/http" "strings" "time" ) // globalIncomingStore — стор входящих переводов для сводной выгрузки (ставится в server.go). var globalIncomingStore *IncomingStore // writeM2MReportBundle собирает ZIP: svodka.html + папка на каждый пройденный кейс. func writeM2MReportBundle(w http.ResponseWriter, r *http.Request, h *setupHandlers) { ctx := r.Context() m2mMu.Lock() plan := m2mLoadPlan() m2mMu.Unlock() // Входящие по GUID (id заявки == GUID, поэтому исходящие резолвим напрямую GetClaim). incomingByGUID := map[string]*IncomingTransfer{} if globalIncomingStore != nil { for _, it := range globalIncomingStore.List() { incomingByGUID[strings.ToUpper(strings.TrimSpace(it.GUID))] = it } } var buf bytes.Buffer zw := zip.NewWriter(&buf) // Сопроводительная сводка. if f, e := zw.Create("svodka.html"); e == nil { _, _ = f.Write(m2mBundleCoverHTML(plan)) } // Справка по кодам ошибок/отказов — чтобы контрагент понимал коды в отчётах. if f, e := zw.Create("spravka-kody-oshibok.html"); e == nil { _, _ = f.Write(m2mSpravkaHTML(true)) } included := 0 seen := map[string]bool{} // 1) Исходящие: по каждому инициированному нами кейсу (есть GUID в плане). for _, c := range m2mCases { st := plan.States[c.ID] g := strings.TrimSpace(st.GUID) gu := strings.ToUpper(g) if g == "" || seen[gu] { continue } folder := "ishodyashie/kejs-" + m2mSafeName(c.ID) + "/" if v, err := h.svc.GetClaim(ctx, g); err == nil { // id заявки == GUID v.Timeline = buildClaimTimeline(ctx, v) writeClaimBundle(ctx, zw, h.svc, v, folder) seen[gu] = true included++ } else if it := incomingByGUID[gu]; it != nil { writeIncomingBundleZip(zw, it, folder) seen[gu] = true included++ } } // 2) Входящие: все отвеченные переводы от контрагента по тестам (там же MS010). cpCode := strings.ToUpper(strings.TrimSpace(plan.CounterpartyCode)) if globalIncomingStore != nil { for _, it := range globalIncomingStore.List() { g := strings.ToUpper(strings.TrimSpace(it.GUID)) if seen[g] || it.Status != "answered" { continue } if cpCode != "" && strings.ToUpper(it.SenderCode) != cpCode { continue // только от контрагента, кому шлём пакет } writeIncomingBundleZip(zw, it, "vhodyashie/perevod-"+m2mSafeName(it.ID)+"/") seen[g] = true included++ } } if err := zw.Close(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fname := fmt.Sprintf("m2m-testy-%s-%s.zip", m2mSafeName(plan.CounterpartyCode), time.Now().Format("2006-01-02")) w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", `attachment; filename="`+fname+`"`) _, _ = w.Write(buf.Bytes()) } // writeIncomingBundleZip пишет комплект по входящему переводу в подпапку prefix // (аналог exportIncoming): отчёт + request/decision + поручение 16/1 + ответ НРД. func writeIncomingBundleZip(zw *zip.Writer, it *IncomingTransfer, prefix string) { if f, e := zw.Create(prefix + "otchet.html"); e == nil { _, _ = f.Write(incomingReportHTML(it)) } if it.RawXML != "" { if f, e := zw.Create(prefix + "request.xml"); e == nil { _, _ = f.Write(faithfulXMLBytes(it.RawXML)) } } if it.DecisionXML != "" { if f, e := zw.Create(prefix + "decision.xml"); e == nil { _, _ = f.Write(faithfulXMLBytes(it.DecisionXML)) } } if it.OrderXML != "" { if f, e := zw.Create(prefix + "poruchenie-16-1.xml"); e == nil { _, _ = f.Write(faithfulXMLBytes(it.OrderXML)) } } if docs := nsdOrderDocs(it.OrderContrDID); len(docs) > 0 { writeNSDDocsZip(zw, prefix+"Ответ НРД/", "Документы НРД по поручению 16/1 № "+it.OrderContrDID, docs, false) } } // m2mBundleCoverHTML — сопроводительная сводка: что и с каким результатом провели. func m2mBundleCoverHTML(plan m2mPlan) []byte { esc := html.EscapeString var b strings.Builder b.WriteString(`Сводка тестирования M2M`) b.WriteString(`
`) b.WriteString(`
` + moexMostLogoSVG + orgLogoIMG() + `
`) b.WriteString(`

Сводка тестирования переводов ценных бумаг (M2M / MOEX МОСТ)

`) b.WriteString(`
Сформировано: ` + time.Now().In(time.FixedZone("МСК", 3*3600)).Format("02.01.2006 15:04") + ` МСК
`) cp := plan.CounterpartyName if cp == "" { cp = plan.CounterpartyCode } b.WriteString(`

Контрагент по тестированию: ` + esc(cp) + ``) if plan.CounterpartyCode != "" { b.WriteString(` (код ` + esc(plan.CounterpartyCode) + ``) if plan.CounterpartyINN != "" { b.WriteString(`, ИНН ` + esc(plan.CounterpartyINN)) } b.WriteString(`)`) } b.WriteString(`.

`) // Считаем итоги. var passed, failed, prog int type row struct { c m2mCase st m2mCaseState } var rows []row for _, c := range m2mCases { st := plan.States[c.ID] if st.Status == "" && st.GUID == "" { continue // кейс не запускали } switch st.Status { case "passed": passed++ case "failed": failed++ case "in_progress": prog++ } rows = append(rows, row{c, st}) } b.WriteString(fmt.Sprintf(`

Итого прогнали: %d · пройдено %d · не прошло %d · в процессе %d.

`, len(rows), passed, failed, prog)) b.WriteString(`

Проведённые кейсы

`) b.WriteString(``) for _, rw := range rows { c, st := rw.c, rw.st dir := esc(c.Debit) + " → " + esc(c.Credit) var secs []string for _, s := range c.Secs { secs = append(secs, esc(s.Code)+"×"+esc(s.Qty)) } exp := "успех" if c.Expect != "" { exp = esc(m2mCodeLabel(c.Expect)) } var res string switch st.Status { case "passed": res = `✓ пройдено` case "failed": res = `✗ не прошло` case "in_progress": res = `⏳ в процессе` default: res = `—` } gt := "" if st.GUID != "" { gt = `` + esc(st.GUID) + `` } if !st.Updated.IsZero() { gt += `
` + st.Updated.In(time.FixedZone("МСК", 3*3600)).Format("02.01 15:04:05") + `
` } b.WriteString(``) } b.WriteString(`
КейсНаправлениеКлиентБумагиОжидалиИтогGUID / время
` + esc(c.ID) + `` + dir + `` + esc(c.Client) + `` + strings.Join(secs, "
") + `
` + exp + `` + res + `` + gt + `
`) b.WriteString(`

По каждому пройденному кейсу в этом архиве — отдельная папка kejs-<номер>/ с отчётом-доказательством (otchet.html) и дословными XML запроса/ответа НРД. Для входящих также приложен ответ НРД (отчёт MS010) в папке otvet-nrd/.

`) b.WriteString(``) return []byte(b.String()) } // m2mSafeName — безопасное имя папки/файла из id кейса или кода. func m2mSafeName(s string) string { s = strings.TrimSpace(s) repl := func(r rune) rune { switch r { case '/', '\\', ' ', ':', '*', '?', '"', '<', '>', '|': return '-' } return r } return strings.Map(repl, s) }