627dbca6af
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
173 lines
13 KiB
Go
173 lines
13 KiB
Go
package lkgateway
|
||
|
||
import (
|
||
"html"
|
||
"strings"
|
||
)
|
||
|
||
// m2mCodeInfo — запись справочника кодов M2M: код, краткое название и развёрнутое
|
||
// пояснение простым языком (когда и почему возникает).
|
||
type m2mCodeInfo struct {
|
||
Code, Short, Full string
|
||
}
|
||
|
||
// m2mRejectCatalog — коды ОТКАЗА зачисляющей (принимающей) стороны. Приходят в
|
||
// решении M2MTransferDecision по конкретной бумаге.
|
||
var m2mRejectCatalog = []m2mCodeInfo{
|
||
{"M2M01", "Ценная бумага не допускается к переводам M2M",
|
||
"Бумагу нельзя переводить через сервис M2M. Типичный случай — иностранный выпуск (ISIN не начинается с «RU»): сервис МОСТ такие бумаги между депозитариями не пропускает."},
|
||
{"M2M02", "Не может быть определён счёт зачисления",
|
||
"Зачисляющий депозитарий не смог определить, на какой счёт депо зачислять бумаги: клиент не указал счёт по умолчанию, а он для операции необходим."},
|
||
{"M2M03", "Счёт депо зачисления не торговый",
|
||
"Указанный счёт зачисления не является торговым — зачисление бумаг на него невозможно."},
|
||
{"M2M04", "Инвестор не найден",
|
||
"У зачисляющего депозитария нет такого клиента (владельца бумаг): некому зачислять. Возникает, если инвестор не заведён у принимающей стороны."},
|
||
{"M2M05", "Бумага не обслуживается зачисляющим депозитарием",
|
||
"Зачисляющий депозитарий не оказывает услуги по учёту прав на данную ценную бумагу (не работает с этим выпуском)."},
|
||
{"M2M06", "Приостановление операций по счёту зачисления",
|
||
"По счёту депо зачисления действует приостановка/блокировка операций — зачислить бумаги нельзя."},
|
||
{"M2M07", "Отказ принимающей стороны",
|
||
"Общий отказ зачисляющего депозитария. В нашей системе применяется также для повторного (дублирующего) перевода — тот же инвестор и бумаги уже принимались ранее."},
|
||
{"M2M08", "Операция нарушит законодательство РФ",
|
||
"Проведение перевода приведёт к нарушению закона: например, бумага предназначена только для квалифицированных инвесторов, а инвестор не квалифицирован."},
|
||
{"M2M09", "Отказ принимающей стороны (доп.)",
|
||
"Дополнительный код отказа зачисляющей стороны."},
|
||
}
|
||
|
||
// m2mServiceCatalog — коды сервиса МОСТ (ошибки/информирование). Приходят в ответе
|
||
// сервиса M2MTransferResponse.
|
||
var m2mServiceCatalog = []m2mCodeInfo{
|
||
{"M2M10", "Получено невалидное сообщение",
|
||
"Формат или структура запроса не прошли проверку сервиса МОСТ (сообщение не соответствует схеме)."},
|
||
{"M2M14", "Списывающий не в справочнике участников M2M",
|
||
"Отправитель (списывающий депозитарий) не зарегистрирован в справочнике участников сервиса M2M."},
|
||
{"M2M15", "Зачисляющий не в справочнике участников M2M",
|
||
"Получатель (зачисляющий депозитарий) не зарегистрирован в справочнике участников сервиса M2M."},
|
||
{"M2M17", "Неверно указан ИНН списывающего депозитария",
|
||
"ИНН передающей (списывающей) стороны указан неверно или не совпадает со справочником."},
|
||
{"M2M18", "Неверно указан ИНН зачисляющего депозитария",
|
||
"ИНН принимающей (зачисляющей) стороны указан неверно или не совпадает со справочником."},
|
||
{"M2M19", "Недопустимое место расчётов",
|
||
"Указанные депонент / счёт депо / раздел / расчётный депозитарий не входят в допустимые для перевода M2M."},
|
||
{"M2M30", "Неверно указан код НРД ценной бумаги",
|
||
"Код НРД ценной бумаги указан неверно (не найден в справочнике инструментов)."},
|
||
{"M2M31", "Ответ по этому GUID уже обработан",
|
||
"Ответ на запрос на перевод M2M с указанным GUID уже был обработан ранее — повторная (дублирующая) отправка ответа/решения."},
|
||
}
|
||
|
||
// m2mSpravkaHTML — справка по кодам ошибок/отказа M2M простым языком (для страницы
|
||
// и для вложения в пакет отчётов контрагенту).
|
||
func m2mSpravkaHTML(standalone bool) []byte {
|
||
esc := html.EscapeString
|
||
var b strings.Builder
|
||
if standalone {
|
||
b.WriteString(`<!doctype html><html lang="ru"><head><meta charset="utf-8"><title>Справка по кодам M2M</title><style>`)
|
||
b.WriteString(`body{font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#1a1a1a;max-width:900px;margin:24px auto;padding:0 20px;line-height:1.5}`)
|
||
b.WriteString(`h1{font-size:20px}h2{font-size:15px;margin:22px 0 8px;color:#1f4e79}table{border-collapse:collapse;width:100%;font-size:13px;margin:8px 0}`)
|
||
b.WriteString(`td,th{border:1px solid #ccc;padding:7px 10px;text-align:left;vertical-align:top}th{background:#f0f0f0}`)
|
||
b.WriteString(`.c{font-family:monospace;font-weight:700;white-space:nowrap}.muted{color:#666}</style></head><body>`)
|
||
}
|
||
b.WriteString(`<h1>Справка по кодам ошибок и отказов M2M (MOEX МОСТ)</h1>`)
|
||
b.WriteString(`<p class="muted">Коды, которыми сервис МОСТ и зачисляющий депозитарий сообщают о причине отказа или ошибки при переводе ценных бумаг между депозитариями.</p>`)
|
||
|
||
section := func(title, note string, cat []m2mCodeInfo) {
|
||
b.WriteString(`<h2>` + title + `</h2>`)
|
||
if note != "" {
|
||
b.WriteString(`<p class="muted" style="font-size:12.5px">` + note + `</p>`)
|
||
}
|
||
b.WriteString(`<table><tr><th style="width:90px">Код</th><th style="width:34%">Кратко</th><th>Что это значит</th></tr>`)
|
||
for _, ci := range cat {
|
||
b.WriteString(`<tr><td class="c">` + esc(ci.Code) + `</td><td>` + esc(ci.Short) + `</td><td>` + esc(ci.Full) + `</td></tr>`)
|
||
}
|
||
b.WriteString(`</table>`)
|
||
}
|
||
section("Отказ зачисляющей (принимающей) стороны",
|
||
"Приходят в решении M2MTransferDecision по конкретной ценной бумаге — принимающий депозитарий отказал в зачислении.",
|
||
m2mRejectCatalog)
|
||
section("Ошибки и информирование сервиса МОСТ",
|
||
"Приходят в ответе сервиса M2MTransferResponse — сервис отклонил или прокомментировал запрос.",
|
||
m2mServiceCatalog)
|
||
b.WriteString(`<p class="muted" style="font-size:12px;margin-top:16px">Прочие коды сервиса (M2M11–M2M13, M2M16, M2M20–M2M29, M2M32–M2M43) определяются Спецификацией электронных документов НРД (Приложение 5) и в данной справке не расшифрованы.</p>`)
|
||
if standalone {
|
||
b.WriteString(`</body></html>`)
|
||
}
|
||
return []byte(b.String())
|
||
}
|
||
|
||
// m2mCodeTexts — человекочитаемые названия кодов ошибок/отказа M2M НРД.
|
||
|
||
// m2mCodeTexts — человекочитаемые названия кодов ошибок/отказа M2M НРД.
|
||
// Источники: ответы сервиса МОСТ (Text), инструкция по тестированию с роботом
|
||
// (Приложение 5, набор данных об ошибках). M2M01–M2M09 — тестовые коды отказа
|
||
// робота (имитация отказа принимающей стороны).
|
||
var m2mCodeTexts = map[string]string{
|
||
"M2M01": "Ценная бумага не допускается к переводам M2M",
|
||
"M2M02": "Не может быть определён счёт для зачисления ценных бумаг",
|
||
"M2M03": "Счёт депо зачисления не торговый",
|
||
"M2M04": "Инвестор не найден",
|
||
"M2M05": "Зачисляющий депозитарий не оказывает услуги по учёту прав на ценную бумагу",
|
||
"M2M06": "Приостановление операций по счёту депо зачисления",
|
||
"M2M07": "Отказ принимающей стороны",
|
||
"M2M08": "Проведение операции приведёт к нарушению законодательства РФ",
|
||
"M2M09": "Отказ принимающей стороны (дополнительный код)",
|
||
"M2M10": "Получено невалидное сообщение",
|
||
"M2M14": "Отправитель не зарегистрирован в справочнике участников M2M",
|
||
"M2M15": "Получатель (зачисляющий депозитарий) не зарегистрирован в справочнике участников M2M", // предположительно (пара к M2M14); уточнить по Спецификации ЭД
|
||
"M2M17": "Неверно указан ИНН списывающего депозитария",
|
||
"M2M18": "Неверно указан ИНН зачисляющего депозитария", // предположительно (по соседним кодам); уточнить по Спецификации ЭД
|
||
"M2M19": "Недопустимое место расчётов",
|
||
"M2M30": "Неверно указан код НРД ценной бумаги",
|
||
// Внутренние причины отказа сверки списания (не коды НРД): недостаточно бумаг
|
||
// на торговом счёте и неверное место хранения в запросе.
|
||
"QTY": "Недостаточно бумаг на торговом счёте депо",
|
||
"PLACE": "Неверное место хранения (не наш торговый счёт/раздел в НРД)",
|
||
}
|
||
|
||
// m2mCodeText возвращает только человеческий текст кода (без самого кода).
|
||
// Для неизвестного кода — пустую строку.
|
||
func m2mCodeText(code string) string {
|
||
return m2mCodeTexts[strings.ToUpper(strings.TrimSpace(code))]
|
||
}
|
||
|
||
// rejectCodesHuman рендерит коды отказа как «M2M01 (словесное описание)» через
|
||
// «; ». Для отчёта регулятору — чтобы рядом с номером кода была расшифровка.
|
||
func rejectCodesHuman(codes []string) string {
|
||
var parts []string
|
||
for _, c := range codes {
|
||
c = strings.TrimSpace(c)
|
||
if c == "" {
|
||
continue
|
||
}
|
||
if t := m2mCodeText(c); t != "" {
|
||
parts = append(parts, c+" ("+t+")")
|
||
} else {
|
||
parts = append(parts, c)
|
||
}
|
||
}
|
||
return strings.Join(parts, "; ")
|
||
}
|
||
|
||
// m2mCodeLabel возвращает «Человеческий текст (M2Mxx)». Для неизвестного кода —
|
||
// сам код. Используется в шаблонах для понятного отображения кодов.
|
||
func m2mCodeLabel(code string) string {
|
||
code = strings.TrimSpace(code)
|
||
if code == "" {
|
||
return ""
|
||
}
|
||
if t := m2mCodeText(code); t != "" {
|
||
return t + " (" + code + ")"
|
||
}
|
||
return code
|
||
}
|
||
|
||
// m2mRejectOptions — варианты кодов отказа для формы решения по входящим
|
||
// (в порядке полезности). Каждый — {code, label}.
|
||
func m2mRejectOptions() []struct{ Code, Label string } {
|
||
codes := []string{"M2M05", "M2M01", "M2M02", "M2M03", "M2M04", "M2M06", "M2M07", "M2M08", "M2M09"}
|
||
out := make([]struct{ Code, Label string }, 0, len(codes))
|
||
for _, c := range codes {
|
||
out = append(out, struct{ Code, Label string }{c, m2mCodeLabel(c)})
|
||
}
|
||
return out
|
||
}
|