package lkgateway // fancy_bundle.go — полный «эталон» тестового взаимодействия с контрагентом (Банк // ЗЕНИТ) для программиста Fancy: ОБЕ стороны обмена — // • входящие (контрагент → мы): что пришло + какое решение мы дали руками; // • исходящие (мы → контрагент): что мы отправили + что нам ответил контрагент. // По каждому переводу — дословные XML (request/response/decision) и пояснение. // Цель: настроить Fancy так, чтобы она формировала те же запросы и те же ответы. import ( "archive/zip" "bytes" "fmt" "html" "net/http" "sort" "strings" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore" ) // fancyRule — правило ответа Fancy по коду решения (что Fancy должна проверить). var fancyRule = map[string]string{ "": "Все проверки пройдены: бумага обслуживается зачисляющим, счёт зачисления определён и является торговым, инвестор найден, ограничений нет → ПРИНЯТЬ (Confirmation).", "M2M02": "Счёт депо для зачисления не может быть определён (у инвестора нет счёта по умолчанию, а он требуется) → отказ M2M02.", "M2M03": "Счёт депо зачисления не является торговым → отказ M2M03.", "M2M04": "Инвестор не найден у зачисляющего депозитария → отказ M2M04.", "M2M05": "Зачисляющий не обслуживает данную бумагу (например, Газпром RU0007661625) → отказ M2M05.", "M2M06": "По счёту депо зачисления приостановлены операции → отказ M2M06.", "M2M08": "Операция нарушит закон РФ (например, бумага для квалинвесторов, а инвестор не квалифицирован) → отказ M2M08.", } type fancyItem struct { it *IncomingTransfer code string decision string folder string } type fancyOutItem struct { v ClaimView req []byte resp []byte dec []byte outcome string code string folder string } // isTestClientDoc — документ принадлежит тест-клиенту плана (К1/К2 из m2mTestClients), // а не робот-заявке (2001/111111, 3333/100000 и т.п.). func isTestClientDoc(series, number string) bool { for _, cl := range m2mTestClients { if cl.DocSeries == series && cl.DocNumber == number { return true } } return false } // writeFancyBundle собирает ZIP-эталон обмена с контрагентом (обе стороны). func writeFancyBundle(w http.ResponseWriter, r *http.Request, h *setupHandlers) { ctx := r.Context() m2mMu.Lock() plan := m2mLoadPlan() m2mMu.Unlock() cp := strings.ToUpper(strings.TrimSpace(plan.CounterpartyCode)) // --- входящие (контрагент → мы) --- var in []fancyItem if globalIncomingStore != nil { for _, it := range globalIncomingStore.List() { if it.Status != "answered" { continue } if cp != "" && strings.ToUpper(it.SenderCode) != cp { continue } code, dec := fancyDecision(it) in = append(in, fancyItem{it: it, code: code, decision: dec}) } } sort.Slice(in, func(i, j int) bool { return in[i].it.ReceivedAt.Before(in[j].it.ReceivedAt) }) for i := range in { tag := "priem" if in[i].code != "" { tag = "otkaz-" + in[i].code } in[i].folder = fmt.Sprintf("vhodyashie--kontragent-nam/%02d-%s/", i+1, tag) } // --- исходящие (мы → контрагент), только реальные тест-клиенты --- var out []fancyOutItem if page, err := h.svc.ListClaims(ctx, m2mcore.Filter{Limit: 5000}); err == nil { for _, v := range page.Items { if !isTestClientDoc(v.Investor.Document.Series, v.Investor.Document.Number) { continue } oi := fancyOutItem{v: v, outcome: nsdOutcomeSummary(v), code: claimRejectCode(v)} oi.req, _ = h.svc.ClaimRawXML(ctx, v.ID, "request") oi.resp, _ = h.svc.ClaimRawXML(ctx, v.ID, "response") oi.dec, _ = h.svc.ClaimRawXML(ctx, v.ID, "decision") out = append(out, oi) } } sort.Slice(out, func(i, j int) bool { return out[i].v.CreatedAt.Before(out[j].v.CreatedAt) }) for i := range out { tag := "prinyato" if out[i].v.Status == "rejected" { tag = "otkaz" if out[i].code != "" { tag += "-" + out[i].code } } out[i].folder = fmt.Sprintf("ishodyashie--my-kontragentu/%02d-%s/", i+1, tag) } var buf bytes.Buffer zw := zip.NewWriter(&buf) if f, e := zw.Create("00-kak-rabotaet-process.html"); e == nil { _, _ = f.Write(fancyProcessHTML(processExample{})) // офлайн: файлы шагов — в папках ниже } if f, e := zw.Create("opisanie-dlya-fancy.html"); e == nil { _, _ = f.Write(fancyBundleDocHTML(plan, in, out)) } // входящие for _, fi := range in { if fi.it.RawXML != "" { writeZipFile(zw, fi.folder+"request-ot-kontragenta.xml", faithfulXMLBytes(fi.it.RawXML)) } if fi.it.DecisionXML != "" { writeZipFile(zw, fi.folder+"nash-otvet-decision.xml", faithfulXMLBytes(fi.it.DecisionXML)) } writeZipFile(zw, fi.folder+"opisanie.txt", []byte(fancyItemNote(fi))) } // исходящие for _, oi := range out { if len(oi.req) > 0 { writeZipFile(zw, oi.folder+"nash-request.xml", oi.req) } if len(oi.resp) > 0 { writeZipFile(zw, oi.folder+"otvet-servisa-response.xml", oi.resp) } if len(oi.dec) > 0 { writeZipFile(zw, oi.folder+"otvet-kontragenta-decision.xml", oi.dec) } writeZipFile(zw, oi.folder+"opisanie.txt", []byte(fancyOutNote(oi))) } if err := zw.Close(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fname := "fancy-etalon-obmena-" + time.Now().Format("2006-01-02") + ".zip" w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", `attachment; filename="`+fname+`"`) _, _ = w.Write(buf.Bytes()) } func writeZipFile(zw *zip.Writer, name string, data []byte) { if f, e := zw.Create(name); e == nil { _, _ = f.Write(data) } } // claimRejectCode — первый код отказа по заявке (из решения контрагента или ответа сервиса). func claimRejectCode(v ClaimView) string { if v.M2MDecision != nil { for _, s := range v.M2MDecision.Securities { if len(s.RejectCodes) > 0 { return s.RejectCodes[0] } } } if v.M2MResponse != nil { for _, e := range v.M2MResponse.Responses { if e.Code != "" { return e.Code } } } return "" } // fancyDecision — код и человекочитаемое решение по входящему (по бумагам). func fancyDecision(it *IncomingTransfer) (code, text string) { conf, rej := 0, 0 var codes []string for _, s := range it.Securities { switch s.Decision { case "confirmed": conf++ case "rejected": rej++ if s.RejectCode != "" && !contains(codes, s.RejectCode) { codes = append(codes, s.RejectCode) } } } switch { case rej == 0 && conf > 0: return "", "Принято" case conf == 0 && rej > 0: return codes[0], "Отказано (" + strings.Join(codes, ", ") + ")" case conf > 0 && rej > 0: return codes[0], fmt.Sprintf("Частично: принято %d, отказано %d (%s)", conf, rej, strings.Join(codes, ", ")) } return "", "Отвечено" } func contains(ss []string, s string) bool { for _, x := range ss { if x == s { return true } } return false } func fancyItemNote(fi fancyItem) string { it := fi.it var b strings.Builder b.WriteString("=== ВХОДЯЩИЙ (контрагент → мы). Fancy должна дать такое же решение ===\n\n") fmt.Fprintf(&b, "От контрагента: %s\n", it.SenderCode) fmt.Fprintf(&b, "GUID: %s\n", it.GUID) fmt.Fprintf(&b, "Получен: %s МСК\n", mskFmt(it.ReceivedAt)) fmt.Fprintf(&b, "Инвестор: %s %s %s, паспорт %s № %s\n", it.InvestorLast, it.InvestorFirst, it.InvestorMiddle, it.DocSeries, it.DocNumber) b.WriteString("Бумаги:\n") for _, s := range it.Securities { key := s.ISIN if key == "" { key = s.SecurityCode } fmt.Fprintf(&b, " - %s × %s → %s %s\n", key, s.Quantity, ruDecision(s.Decision), s.RejectCode) } fmt.Fprintf(&b, "\nНАШЕ РЕШЕНИЕ: %s\n", fi.decision) fmt.Fprintf(&b, "ПРАВИЛО ДЛЯ FANCY: %s\n", fancyRule[fi.code]) b.WriteString("\nФайлы: request-ot-kontragenta.xml — что прислал контрагент; nash-otvet-decision.xml — что мы ответили (эталон).\n") return b.String() } func fancyOutNote(oi fancyOutItem) string { v := oi.v var b strings.Builder b.WriteString("=== ИСХОДЯЩИЙ (мы → контрагент). Fancy должна сформировать такой же запрос ===\n\n") fmt.Fprintf(&b, "GUID: %s\n", v.M2MGUID) fmt.Fprintf(&b, "Создан: %s МСК\n", mskFmt(v.CreatedAt)) fmt.Fprintf(&b, "Инвестор: %s, паспорт %s № %s\n", fio(v.Investor), v.Investor.Document.Series, v.Investor.Document.Number) b.WriteString("Бумаги:\n") for _, s := range v.Securities { q := s.Quantity.Fractional if q == "" { q = fmt.Sprintf("%d", s.Quantity.Whole) } fmt.Fprintf(&b, " - %s × %s\n", s.SecurityCode, q) } fmt.Fprintf(&b, "\nОТВЕТ КОНТРАГЕНТА/СЕРВИСА: %s (статус заявки: %s)\n", oi.outcome, russianState(v.Status)) if oi.code != "" { fmt.Fprintf(&b, "Код: %s — %s\n", oi.code, m2mCodeText(oi.code)) } b.WriteString("\nФайлы: nash-request.xml — что мы отправили; otvet-servisa-response.xml / otvet-kontragenta-decision.xml — что нам ответили.\n") return b.String() } func mskFmt(t time.Time) string { return t.In(time.FixedZone("МСК", 3*3600)).Format("02.01.2006 15:04:05") } func ruDecision(d string) string { switch d { case "confirmed": return "ПРИНЯТО" case "rejected": return "ОТКАЗ" } return "—" } // fancyBundleDocHTML — сопроводительное описание для программиста Fancy (обе стороны). func fancyBundleDocHTML(plan m2mPlan, in []fancyItem, out []fancyOutItem) []byte { esc := html.EscapeString var b strings.Builder b.WriteString(`
Полная запись тестового обмена с контрагентом ` + esc(cpName) + ` (код ` + esc(plan.CounterpartyCode) + `), обе стороны. Задача: настроить Fancy так, чтобы по такому же входящему она давала такое же решение, а исходящие формировала так же, как здесь.
`) b.WriteString(fmt.Sprintf(`В архиве: %d входящих (папка vhodyashie--kontragent-nam/) и %d исходящих (папка ishodyashie--my-kontragentu/), только по реальным тест-клиентам (Бубликов, Баранкин) — робот-заявки исключены. В каждой папке дословные XML и opisanie.txt.
`, len(in), len(out))) // Входящие b.WriteString(`| № / папка | Время | Инвестор | Бумага × кол-во | Наше решение | Правило для Fancy |
|---|---|---|---|---|---|
| %02d %s | `, i+1, esc(shortFolder(fi.folder))))
b.WriteString(`` + esc(it.ReceivedAt.In(time.FixedZone("МСК", 3*3600)).Format("02.01 15:04")) + ` | `) b.WriteString(`` + esc(fioIn) + ` пасп. ` + esc(it.DocSeries) + ` № ` + esc(it.DocNumber) + ` | `)
b.WriteString(`` + strings.Join(secs, " ") + ` | `)
b.WriteString(`` + esc(fi.decision) + ` | `) b.WriteString(`` + esc(fancyRule[fi.code]) + ` | `) b.WriteString(`
| № / папка | Время | Инвестор | Бумага | Ответ контрагента |
|---|---|---|---|---|
| %02d %s | `, i+1, esc(shortFolder(oi.folder))))
b.WriteString(`` + esc(v.CreatedAt.In(time.FixedZone("МСК", 3*3600)).Format("02.01 15:04")) + ` | `) b.WriteString(`` + esc(fio(v.Investor)) + ` пасп. ` + esc(v.Investor.Document.Series) + ` № ` + esc(v.Investor.Document.Number) + ` | `)
b.WriteString(`` + strings.Join(secs, " ") + ` | `)
b.WriteString(`` + res + ` | `) b.WriteString(`
| Код | Значение | Правило |
|---|---|---|
| ` + c + ` | ` + esc(m2mCodeText(c)) + ` | ` + esc(rule) + ` |
Все XML — дословные (windows-1251), как прошли через НРД. Отобраны только реальные тест-клиенты; служебные робот-прогоны в пакет не включены.
`) b.WriteString(``) return []byte(b.String()) } // fancyProcessHTML — описание всего процесса M2M «как это работает у нас»: // что даёт НРД, как мы обрабатываем, как отправляем обратно через шлюз ИШ. // Отдельный акцент на депозитарных поручениях 16 (списание) и 16/1 (зачисление). func fancyProcessHTML(ex processExample) []byte { var b strings.Builder b.WriteString(`Наглядная схема процесса для программиста: как мы формируем и отправляем, что присылает НРД (файлы 16 / 16.1), как мы их обрабатываем и кладём в шлюз, и как итог — бумаги списываются и зачисляются. Ниже — обе стороны и пошаговые детали.
`) chip := func(cls, title, sub string) string { s := "" if sub != "" { s = `` + sub + `` } return `| Кто | Роль |
|---|---|
| НРД / МОСТ | Сервис быстрых переводов ценных бумаг MOEX МОСТ. Проводит сделку, формирует черновики поручений, выдаёт отчёты. |
| ИШ (шлюз) | Интеграционный шлюз НРД (igate). Приём/отправка пакетов, ГОСТ-подпись. Папки обмена: INBOX (пришло от НРД), OUTBOX/SENT/ERRORS. REST API: POST /api/package/{канал}/file, GET /api/package?channel=&type=, GET /api/package/{id}. |
| bj (мы) | Наша система. Читает INBOX, принимает решения, упаковывает и шлёт документы через ИШ, отслеживает ответы. |
| Контрагент | Депозитарий на другой стороне перевода (в тестах — Банк ЗЕНИТ). |
| Fancy | Учётная (депозитарная) система. Должна принимать решения по входящим и формировать исходящие так же, как мы делали руками. |
| Документ | Тип / пакет | Кто шлёт → кому | Смысл |
|---|---|---|---|
| M2MTransferRequest | #M2MTR | инициатор → НРД → контрагент | Запрос на перевод ценных бумаг |
| M2MTransferResponse | #M2MER | НРД → инициатор | Ответ сервиса МОСТ (коды M2Mxx, ошибки/информирование) |
| M2MTransferDecision | #M2MTD | зачисляющий → НРД → списывающий | Решение по бумагам: принять / отказать (с кодом) |
| Черновик поручения (MF010) | #M2MDO | НРД → обе стороны | Черновик формы 16 (списание) или 16/1 (зачисление). Отличаются полем <order_t_id> |
| Поручение депо | ORDXM | депозитарий → НРД | Поданный черновик 16 / 16/1 как классическое поручение (config.xml <package>K</package>) |
| Уведомление о принятии | REORD | НРД → депозитарий | НРД принял поручение депо |
| Отчёт по поручению депо | XMLRP = MS010 | НРД → депозитарий | Операция проведена (внутри zip: repxml.xml с MS010/sese.025 + dbf) |
Контрагент списывает бумаги и переводит их нам. Мы решаем — принять или отказать, затем подаём встречное поручение 16/1 (зачисление).
`) render := func(items []pstep) { for i, it := range items { var dl string if it.href != "" { dl = `` } did := "" if it.did != "" { did = `INBOX пакет #M2MTR — M2MTransferRequest (инвестор, документ, какие бумаги нам переводят).`,
href: hIf(ex.InID != "" && ex.InHasReq, inURL("request.xml")), dlabel: "Запрос контрагента (request.xml)",
did: `разбираем, показываем во «Входящих», сверяем данные по каждой бумаге.`, system: `реестр bj; сверка данных — Fancy (депозитарный учёт).`},
{who: `bj → ИШ`, text: `Формируем M2MTransferDecision — по каждой бумаге принять/отказать (+ код отказа), шлём пакетом #M2MTD (GUID ответа = GUID запроса).`,
href: hIf(ex.InID != "" && ex.InHasDec, inURL("decision.xml")), dlabel: "Наше решение (decision.xml)",
did: `сверяем данные запроса с картотекой (см. раздел «Что сверяем» ниже) и решаем.`, system: `уходит в НРД через шлюз ИШ.`},
{who: `НРД`, text: `Если приняли — кладёт в INBOX черновик поручения 16/1 (..._MF010.xml, <order_t_id>16/1</order_t_id>, <based_on>=GUID, <contr_d_id>=№ поручения).`,
href: hIf(ex.InID != "" && ex.InHasDraft, inURL("draft.zip")), dlabel: "Черновик 16/1 от НРД (draft.zip)",
did: `связываем по based_on=GUID, готовим к подаче.`, system: `Fancy (депоучёт) — формирование/подача поручения.`},
{who: `bj → ИШ`, text: `Подаём черновик 16/1 в НРД как поручение депо ORDXM: XML как есть (windows-1251) в zip с <package>K</package>, POST /api/package/{канал}/file. ИШ подписывает ГОСТ.`,
href: hIf(ex.InID != "" && ex.InHasOrder, inURL("order.xml")), dlabel: "Наше поручение 16/1 (order.xml)",
did: `упаковали и отправили поручение депо в НРД.`, system: `НРД через шлюз ИШ.`},
{who: `НРД`, text: `Присылает квитанцию XCONF → принятие REORD → сматчив встречное поручение 16 контрагента, проводит зачисление и присылает отчёт MS010 (XMLRP).`,
href: hIf(ex.InID != "" && ex.InHasMS010, inURL("ms010.zip")), dlabel: "Ответ НРД MS010 (ms010.zip)",
did: `находим по contr_d_id, статус «Проведено · MS010» — бумаги зачислены.`, system: `провести зачисление в Fancy; блокировки бумаг — QUIK МНП.`},
})
// Исходящий процесс (16)
b.WriteString(`Мы списываем бумаги инвестора и переводим их контрагенту. Зеркально процессу А, но черновик приходит формы 16 (списание).
`) outRespHref := "" if ex.OutClaimID != "" { if ex.OutHasDec { outRespHref = clURL("decision.xml") } else if ex.OutHasResp { outRespHref = clURL("response.xml") } } render([]pstep{ {who: `bj → ИШ`, text: `Формируем M2MTransferRequest (инвестор, бумаги, счёт списания) и шлём пакетом#M2MTR.`,
href: hIf(ex.OutClaimID != "" && ex.OutHasReq, clURL("request.xml")), dlabel: "Наш запрос (request.xml)",
did: `собираем запрос по заявке клиента.`, system: `запрос из Fancy (заявка клиента) → НРД через ИШ.`},
{who: `НРД / Контрагент`, text: `Приходит #M2MER (M2MTransferResponse, коды сервиса) и M2MTransferDecision контрагента — согласие/отказ.`,
href: outRespHref, dlabel: "Ответ (decision/response.xml)",
did: `разбираем ответ; при согласии ждём черновик 16.`, system: `результат — в Fancy (статус заявки).`},
{who: `НРД`, text: `После согласия кладёт в INBOX черновик поручения 16 (..._MF010.xml, <order_t_id>16</order_t_id> — списание).`,
href: hIf(ex.OutGUID != "" && ex.OutHasDraft, outURL("draft.zip")), dlabel: "Черновик 16 от НРД (draft.zip)",
did: `связываем по GUID, готовим к подаче.`, system: `Fancy (депоучёт) — подача поручения.`},
{who: `bj → ИШ`, text: `Подаём черновик 16 в НРД как поручение депо ORDXM (тот же механизм: zip + <package>K</package>, ИШ подписывает).`,
href: hIf(ex.OutGUID != "" && ex.OutHasOrder, outURL("order.xml")), dlabel: "Наше поручение 16 (order.xml)",
did: `упаковали и отправили поручение депо.`, system: `НРД через шлюз ИШ.`},
{who: `НРД`, text: `Матчит наш 16 со встречным 16/1 контрагента, проводит списание и присылает отчёт MS010 — бумага ушла контрагенту.`,
href: hIf(ex.OutGUID != "" && ex.OutHasMS010, outURL("ms010.zip")), dlabel: "Ответ НРД MS010 (ms010.zip)",
did: `статус «Списание проведено · MS010».`, system: `провести списание в Fancy; блокировки — QUIK МНП.`},
})
// Что Fancy сверяет по данным
b.WriteString(`По входящему запросу Fancy сверяет данные из запроса со своей картотекой. Если всё совпало и ограничений нет — принимаем. Если что-то не совпало — отказываем с соответствующим кодом.
`) b.WriteString(`| Данные из запроса | С чем сверяем в Fancy | ✓ Совпало → принимаем | ✗ Не совпало → отказ |
|---|---|---|---|
| ` + field + ` | ` + against + ` | ` + ok + ` | ` + code + ` |
Если по всем строкам «совпало» — бумага принимается (Confirmation). Если хотя бы одна проверка не прошла — бумага отклоняется с первым сработавшим кодом. Для исходящих Fancy формирует запрос из этих же данных клиента.
`) // Как отправляем через шлюз b.WriteString(`Документ (XML в windows-1251) кладётся в ZIP вместе с config.xml, где указан тип пакета. Для M2M-документов тип начинается с # (например #M2MTD). Для поручения депо — <package>K</package> (тип ORDXM).
POST /api/package/{канал}/file (multipart, поле File = zip, Type=ARCHIVE) → ИШ возвращает id пакета. Подпись ГОСТ ставит сам ИШ — нам подписывать не нужно. Статус: GET /api/package/status/{id} (NEW → SENT → ERROR).
Входящие ИШ складывает в INBOX отдельными пакетами-папками. Мы читаем их (наш поллер), а также тянем через GET /api/package?channel=&type= и GET /api/package/{id}.
..._MF010.xml; отличаются только полем <order_t_id>: 16 — списание (наш исходящий), 16/1 — зачисление (наш входящий). Связь с M2M-сделкой — по <based_on> (GUID). Номер поручения — <contr_d_id>: по нему мы потом находим отчёт MS010. Подаются они одинаково — типом ЭД ORDXM.Дословные примеры файлов каждого шага — в этом же архиве (папки vhodyashie / ishodyashie).
`) b.WriteString(``) return []byte(b.String()) } // pstep — шаг процесса с файлом, описанием действия и целевой системой. type pstep struct{ who, text, href, dlabel, did, system string } // processExample — конкретный завершённый пример перевода (по одному на сторону) // для скачивания файлов каждого шага на странице процесса. type processExample struct { InID string InHasReq, InHasDec, InHasDraft, InHasOrder, InHasMS010 bool OutClaimID, OutGUID string OutHasReq, OutHasResp, OutHasDec, OutHasDraft, OutHasOrder bool OutHasMS010 bool } // buildProcessExample подбирает наиболее полный пример входящего и исходящего // перевода и выясняет, какие файлы по нему доступны для скачивания. func buildProcessExample(r *http.Request, h *setupHandlers) processExample { ctx := r.Context() var ex processExample // Входящий: предпочитаем тот, где подано поручение (полная цепочка до MS010). if globalIncomingStore != nil { var best *IncomingTransfer for _, it := range globalIncomingStore.List() { if it.Status != "answered" { continue } if it.OrderSubmitted { best = it break } if best == nil { best = it } } if best != nil { ex.InID = best.ID ex.InHasReq = best.RawXML != "" ex.InHasDec = best.DecisionXML != "" ex.InHasDraft = len(findMF010Files(best.GUID)) > 0 ex.InHasOrder = best.OrderXML != "" ex.InHasMS010 = len(collectOrderResponseRaw(best.OrderContrDID)) > 0 } } // Исходящий: реальный тест-клиент, предпочитаем с черновиком 16 / поданным поручением. if page, err := h.svc.ListClaims(ctx, m2mcore.Filter{Limit: 5000}); err == nil { var best ClaimView bestScore := -1 for _, v := range page.Items { if !isTestClientDoc(v.Investor.Document.Series, v.Investor.Document.Number) { continue } score := 0 g := string(v.M2MGUID) if globalOutgoingStore != nil { if _, ok := scanForm16Deal(globalOutgoingStore, g); ok { score += 3 } if sub, ok := globalOutgoingStore.Get(g); ok && sub.OrderXML != "" { score += 3 } } if v.Status != "" { score++ } if score > bestScore { best = v bestScore = score } } if bestScore >= 0 { ex.OutClaimID = best.ID ex.OutGUID = string(best.M2MGUID) req, _ := h.svc.ClaimRawXML(ctx, best.ID, "request") ex.OutHasReq = len(req) > 0 resp, _ := h.svc.ClaimRawXML(ctx, best.ID, "response") ex.OutHasResp = len(resp) > 0 dec, _ := h.svc.ClaimRawXML(ctx, best.ID, "decision") ex.OutHasDec = len(dec) > 0 if globalOutgoingStore != nil { if _, ok := scanForm16Deal(globalOutgoingStore, ex.OutGUID); ok { ex.OutHasDraft = true } if sub, ok := globalOutgoingStore.Get(ex.OutGUID); ok { ex.OutHasOrder = sub.OrderXML != "" ex.OutHasMS010 = len(collectOutgoingResponseRaw(sub.ContrDIDs)) > 0 } } } } return ex } func shortFolder(f string) string { f = strings.TrimSuffix(f, "/") if i := strings.LastIndex(f, "/"); i >= 0 { return f[i+1:] } return f }