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(`
Контрагент по тестированию: ` + 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(`| Кейс | Направление | Клиент | Бумаги | Ожидали | Итог | GUID / время |
|---|---|---|---|---|---|---|
| ` + esc(c.ID) + ` | ` + dir + ` | ` + esc(c.Client) + ` | ` + strings.Join(secs, " ") + ` | ` + exp + ` | ` + res + ` | ` + gt + ` |
По каждому пройденному кейсу в этом архиве — отдельная папка 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) }