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(`Эталон обмена для Fancy`) b.WriteString(`
` + moexMostLogoSVG + orgLogoIMG() + `
`) b.WriteString(`

Эталон тестового обмена M2M для интеграции с Fancy

`) cpName := plan.CounterpartyName if cpName == "" { cpName = plan.CounterpartyCode } 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(`

1. Входящие (контрагент → мы) — какое решение должна дать Fancy

`) b.WriteString(``) for i, fi := range in { it := fi.it var secs []string for _, s := range it.Securities { key := s.ISIN if key == "" { key = s.SecurityCode } secs = append(secs, esc(key)+" × "+esc(s.Quantity)) } dcls := "ok" if fi.code != "" { dcls = "no" } fioIn := strings.TrimSpace(it.InvestorLast + " " + it.InvestorFirst + " " + it.InvestorMiddle) b.WriteString(``) b.WriteString(fmt.Sprintf(``, i+1, esc(shortFolder(fi.folder)))) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) } b.WriteString(`
№ / папкаВремяИнвесторБумага × кол-воНаше решениеПравило для Fancy
%02d
%s
` + esc(it.ReceivedAt.In(time.FixedZone("МСК", 3*3600)).Format("02.01 15:04")) + `` + esc(fioIn) + `
пасп. ` + esc(it.DocSeries) + ` № ` + esc(it.DocNumber) + `
` + strings.Join(secs, "
") + `
` + esc(fi.decision) + `` + esc(fancyRule[fi.code]) + `
`) // Исходящие b.WriteString(`

2. Исходящие (мы → контрагент) — что мы отправляли и что нам ответили

`) b.WriteString(``) for i, oi := range out { v := oi.v var secs []string for _, s := range v.Securities { secs = append(secs, esc(s.SecurityCode)) } ocls := "ok" if v.Status == "rejected" { ocls = "no" } res := esc(oi.outcome) if oi.code != "" { res += `
` + esc(m2mCodeText(oi.code)) + `` } b.WriteString(``) b.WriteString(fmt.Sprintf(``, i+1, esc(shortFolder(oi.folder)))) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) } b.WriteString(`
№ / папкаВремяИнвесторБумагаОтвет контрагента
%02d
%s
` + esc(v.CreatedAt.In(time.FixedZone("МСК", 3*3600)).Format("02.01 15:04")) + `` + esc(fio(v.Investor)) + `
пасп. ` + esc(v.Investor.Document.Series) + ` № ` + esc(v.Investor.Document.Number) + `
` + strings.Join(secs, "
") + `
` + res + `
`) b.WriteString(`

Коды отказа — что проверяет Fancy

`) for _, c := range []string{"M2M01", "M2M02", "M2M03", "M2M04", "M2M05", "M2M06", "M2M08"} { rule := fancyRule[c] if rule == "" { rule = m2mCodeText(c) } b.WriteString(``) } b.WriteString(`
КодЗначениеПравило
` + c + `` + esc(m2mCodeText(c)) + `` + esc(rule) + `
`) b.WriteString(`

Все 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(`Как работает процесс M2M (16 / 16.1)`) b.WriteString(`
` + moexMostLogoSVG + orgLogoIMG() + `
`) b.WriteString(`

Как работает процесс перевода ценных бумаг (M2M)

`) b.WriteString(`

Наглядная схема процесса для программиста: как мы формируем и отправляем, что присылает НРД (файлы 16 / 16.1), как мы их обрабатываем и кладём в шлюз, и как итог — бумаги списываются и зачисляются. Ниже — обе стороны и пошаговые детали.

`) chip := func(cls, title, sub string) string { s := "" if sub != "" { s = `` + sub + `` } return `
` + title + s + `
` } arr := `→` b.WriteString(`

Схема: исходящий перевод (мы отправляем, бумаги СПИСЫВАЮТСЯ)

`) b.WriteString(`
` + chip("c-us", "1. Мы сформировали заявку", "M2MTransferRequest") + arr + chip("c-nsd", "2. Ушло в НРД", "через шлюз ИШ") + arr + chip("c-cp", "3. Контрагент ответил", "согласие") + arr + chip("c-nsd", "4. НРД прислал файл 16", "черновик поручения (списание)") + arr + chip("c-us", "5. Мы обработали → шлюз", "подали поручение ORDXM") + arr + chip("c-done", "6. ✓ Бумаги списаны", "отчёт MS010") + `
`) b.WriteString(`

Схема: входящий перевод (нам присылают, бумаги ЗАЧИСЛЯЮТСЯ)

`) b.WriteString(`
` + chip("c-cp", "1. Контрагент отправил", "M2MTransferRequest") + arr + chip("c-nsd", "2. НРД прислал нам", "запрос в INBOX") + arr + chip("c-us", "3. Мы ответили", "решение: принять/отказать") + arr + chip("c-nsd", "4. НРД прислал файл 16.1", "черновик поручения (зачисление)") + arr + chip("c-us", "5. Мы обработали → шлюз", "подали поручение ORDXM") + arr + chip("c-done", "6. ✓ Бумаги зачислены", "отчёт MS010") + `
`) // Компоненты b.WriteString(`

Участники и компоненты

`) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(`
КтоРоль
НРД / МОСТСервис быстрых переводов ценных бумаг MOEX МОСТ. Проводит сделку, формирует черновики поручений, выдаёт отчёты.
ИШ (шлюз)Интеграционный шлюз НРД (igate). Приём/отправка пакетов, ГОСТ-подпись. Папки обмена: INBOX (пришло от НРД), OUTBOX/SENT/ERRORS. REST API: POST /api/package/{канал}/file, GET /api/package?channel=&type=, GET /api/package/{id}.
bj (мы)Наша система. Читает INBOX, принимает решения, упаковывает и шлёт документы через ИШ, отслеживает ответы.
КонтрагентДепозитарий на другой стороне перевода (в тестах — Банк ЗЕНИТ).
FancyУчётная (депозитарная) система. Должна принимать решения по входящим и формировать исходящие так же, как мы делали руками.
`) // Типы ЭД b.WriteString(`

Типы электронных документов (ЭД)

`) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(``) b.WriteString(`
ДокументТип / пакетКто шлёт → комуСмысл
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) b.WriteString(`

Процесс А. Входящий перевод — мы ЗАЧИСЛЯЮЩИЙ (поручение 16/1)

`) b.WriteString(`

Контрагент списывает бумаги и переводит их нам. Мы решаем — принять или отказать, затем подаём встречное поручение 16/1 (зачисление).

`) render := func(items []pstep) { for i, it := range items { var dl string if it.href != "" { dl = `
⬇ ` + it.dlabel + `
` } did := "" if it.did != "" { did = `
Что делаем: ` + it.did + `
` } sys := "" if it.system != "" { sys = `
Куда вносится: ` + it.system + `
` } b.WriteString(`
` + fmt.Sprintf("%d", i+1) + `
` + it.who + it.text + dl + did + sys + `
`) } } hIf := func(cond bool, href string) string { if cond { return href } return "" } inURL := func(seg string) string { return "/admin/incoming/" + ex.InID + "/" + seg } clURL := func(seg string) string { return "/admin/claims/" + ex.OutClaimID + "/" + seg } outURL := func(seg string) string { return "/admin/outgoing/" + ex.OutGUID + "/" + seg } render([]pstep{ {who: `НРД`, text: `Кладёт нам в 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)

`) 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 сверяет данные из запроса со своей картотекой. Если всё совпало и ограничений нет — принимаем. Если что-то не совпало — отказываем с соответствующим кодом.

`) b.WriteString(``) chk := func(field, against, ok, code string) { b.WriteString(``) } chk("Ценная бумага (ISIN / код НРД)", "справочник обслуживаемых выпусков", "выпуск обслуживаем", "не обслуживаем → M2M05") chk("Допустимость к M2M (ISIN)", "правила сервиса МОСТ (только RU-ISIN)", "российский выпуск, допустим", "иностранный/недопустим → M2M01") chk("Инвестор (ФИО + документ ДУЛ: тип, серия, номер)", "картотека клиентов", "клиент найден", "клиента нет → M2M04") chk("Счёт депо зачисления", "счета клиента", "счёт определён", "счёт не определён → M2M02") chk("Тип счёта депо", "признак «торговый»", "счёт торговый", "счёт неторговый → M2M03") chk("Статус счёта депо", "блокировки/приостановки", "операции разрешены", "приостановлены → M2M06") chk("Правовые ограничения по бумаге", "квалификация инвестора / закон РФ", "ограничений нет", "нарушение (напр. квалинвестор) → M2M08") b.WriteString(`
Данные из запросаС чем сверяем в Fancy✓ Совпало → принимаем✗ Не совпало → отказ
` + field + `` + against + `` + ok + `` + code + `
`) b.WriteString(`

Если по всем строкам «совпало» — бумага принимается (Confirmation). Если хотя бы одна проверка не прошла — бумага отклоняется с первым сработавшим кодом. Для исходящих Fancy формирует запрос из этих же данных клиента.

`) // Как отправляем через шлюз b.WriteString(`

Как именно отправляем через шлюз ИШ

`) b.WriteString(`

Упаковка

Документ (XML в windows-1251) кладётся в ZIP вместе с config.xml, где указан тип пакета. Для M2M-документов тип начинается с # (например #M2MTD). Для поручения депо — <package>K</package> (тип ORDXM).

`) b.WriteString(`

Отправка

POST /api/package/{канал}/file (multipart, поле File = zip, Type=ARCHIVE) → ИШ возвращает id пакета. Подпись ГОСТ ставит сам ИШ — нам подписывать не нужно. Статус: GET /api/package/status/{id} (NEW → SENT → ERROR).

`) b.WriteString(`

Приём

Входящие ИШ складывает в INBOX отдельными пакетами-папками. Мы читаем их (наш поллер), а также тянем через GET /api/package?channel=&type= и GET /api/package/{id}.

`) b.WriteString(`
Ключевое про 16 и 16/1. Оба черновика приходят одним типом файла ..._MF010.xml; отличаются только полем <order_t_id>: 16 — списание (наш исходящий), 16/1 — зачисление (наш входящий). Связь с M2M-сделкой — по <based_on> (GUID). Номер поручения — <contr_d_id>: по нему мы потом находим отчёт MS010. Подаются они одинаково — типом ЭД ORDXM.
`) b.WriteString(`

Дословные примеры файлов каждого шага — в этом же архиве (папки 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 }