Входящие M2M: частичный приём — каждая бумага решается сама по себе (НРД допускает: по каждой принятой свой черновик 16/1); переводы на ИИС не принимаем — отказ M2M07 по всем бумагам
- finishResult: итог confirm / reject / partial, без отказа всем пакетом; - autoDecisions и ручной ответ: без правила «целиком», квал-предупреждение — отказ только по своей бумаге; - признак ИИС из заголовка запроса (IIAAgreementDetails), у сохранённых раньше — по исходному XML; - форма оператора: без автоматического «отказ всем», подсказка про ИИС; тексты и тесты. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -54,6 +54,7 @@ var templateFuncs = template.FuncMap{
|
|||||||
"devOn": authTemplateFuncs["devOn"],
|
"devOn": authTemplateFuncs["devOn"],
|
||||||
"devLicensed": authTemplateFuncs["devLicensed"],
|
"devLicensed": authTemplateFuncs["devLicensed"],
|
||||||
"mod": func(f string) bool { return globalRC == nil || moduleAllowed(globalRC, f) }, // модуль в лицензии
|
"mod": func(f string) bool { return globalRC == nil || moduleAllowed(globalRC, f) }, // модуль в лицензии
|
||||||
|
"isIIS": incomingIsIIS, // входящий перевод на ИИС
|
||||||
"depoSubs": func() any { return depoWizardSubs }, // экраны шага 3 мастера
|
"depoSubs": func() any { return depoWizardSubs }, // экраны шага 3 мастера
|
||||||
"authOpen": authOpen,
|
"authOpen": authOpen,
|
||||||
"internalDocs": func() bool { return internalDocsBuilt },
|
"internalDocs": func() bool { return internalDocsBuilt },
|
||||||
|
|||||||
@@ -339,35 +339,31 @@ func finishSec(sr *depoSecResult) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// finishResult — итог по переводу. Пакет принимается только целиком (решение 16.09.2026): если хотя
|
// finishResult — итог по переводу. Частичный приём (решение 25.09.2026): каждая бумага решается сама
|
||||||
// бы по одной бумаге отказ, отказ рекомендуется по всем бумагам с кодом первой отказной.
|
// по себе — прошедшие проверки принимаются, остальным отказ со своим кодом. НРД так допускает: по
|
||||||
|
// каждой принятой бумаге приходит свой черновик 16/1 и подаётся своё поручение. Переводы на ИИС не
|
||||||
|
// принимаются совсем — это отдельная проверка по каждой бумаге (отказ всем).
|
||||||
func finishResult(res *depoResult) {
|
func finishResult(res *depoResult) {
|
||||||
first := -1
|
conf, rej, code, sec := 0, 0, "", ""
|
||||||
for i, s := range res.Secs {
|
for _, s := range res.Secs {
|
||||||
if s.Recommend != "confirm" {
|
if s.Recommend == "confirm" {
|
||||||
first = i
|
conf++
|
||||||
break
|
continue
|
||||||
|
}
|
||||||
|
rej++
|
||||||
|
if code == "" {
|
||||||
|
code, sec = s.RejectCode, s.Security
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case len(res.Secs) == 0:
|
case len(res.Secs) == 0:
|
||||||
case first < 0:
|
case rej == 0:
|
||||||
res.Recommend, res.Summary = "confirm", "принять"
|
res.Recommend, res.Summary = "confirm", "принять"
|
||||||
default:
|
case conf == 0:
|
||||||
code, sec := res.Secs[first].RejectCode, res.Secs[first].Security
|
|
||||||
passed := 0
|
|
||||||
for i := range res.Secs {
|
|
||||||
if res.Secs[i].Recommend == "confirm" {
|
|
||||||
passed++
|
|
||||||
res.Secs[i].Recommend, res.Secs[i].RejectCode = "reject", code
|
|
||||||
res.Secs[i].Checks = append(res.Secs[i].Checks, depoCheck{Name: "Пакет принимается только целиком", OK: false, Code: code,
|
|
||||||
Detail: "сама бумага проверку прошла; отказ всем пакетом из-за " + sec + " (" + code + ")"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.Recommend, res.Summary = "reject", "отказ "+code
|
res.Recommend, res.Summary = "reject", "отказ "+code
|
||||||
if passed > 0 {
|
default:
|
||||||
res.Summary = fmt.Sprintf("отказ пакетом %s (из-за %s)", code, sec)
|
res.Recommend = "partial"
|
||||||
}
|
res.Summary = fmt.Sprintf("частично: принять %d, отказ %d (первый — %s по %s)", conf, rej, code, sec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,6 +414,10 @@ func runDepoCheck(it *IncomingTransfer) depoResult {
|
|||||||
for _, s := range it.Securities {
|
for _, s := range it.Securities {
|
||||||
isin := strings.ToUpper(strings.TrimSpace(s.ISIN))
|
isin := strings.ToUpper(strings.TrimSpace(s.ISIN))
|
||||||
sr := depoSecResult{ReferenceID: s.ReferenceID, Security: pick(isin, s.SecurityCode)}
|
sr := depoSecResult{ReferenceID: s.ReferenceID, Security: pick(isin, s.SecurityCode)}
|
||||||
|
if incomingIsIIS(it) { // решение 25.09.2026: входящие переводы на ИИС не принимаем совсем
|
||||||
|
sr.Checks = append(sr.Checks, depoCheck{Name: "Перевод не на ИИС", OK: false, Code: "M2M07",
|
||||||
|
Detail: "перевод на ИИС (в запросе договор ИИС) — входящие переводы на ИИС не принимаем, отказ по всем бумагам"})
|
||||||
|
}
|
||||||
ruOK := strings.HasPrefix(isin, "RU")
|
ruOK := strings.HasPrefix(isin, "RU")
|
||||||
sr.Checks = append(sr.Checks, depoCheck{Name: "Российский выпуск (RU-ISIN)", OK: ruOK, Code: "M2M01",
|
sr.Checks = append(sr.Checks, depoCheck{Name: "Российский выпуск (RU-ISIN)", OK: ruOK, Code: "M2M01",
|
||||||
Detail: pick3(ruOK, "допускается к переводам M2M", "не-RU ISIN — к M2M не допускается")})
|
Detail: pick3(ruOK, "допускается к переводам M2M", "не-RU ISIN — к M2M не допускается")})
|
||||||
|
|||||||
@@ -5,47 +5,50 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Пакет принимается только целиком: отказ по одной бумаге — отказ по всем с тем же кодом.
|
// Частичный приём (решение 25.09.2026): каждая бумага решается сама по себе.
|
||||||
func TestFinishResultWholePackage(t *testing.T) {
|
func TestFinishResultPartial(t *testing.T) {
|
||||||
res := depoResult{Secs: []depoSecResult{
|
res := depoResult{Secs: []depoSecResult{
|
||||||
{Security: "RU0009029540", Recommend: "confirm"},
|
{Security: "RU0009029540", Recommend: "confirm"},
|
||||||
{Security: "RU0007661625", Recommend: "reject", RejectCode: "M2M05"},
|
{Security: "RU0007661625", Recommend: "reject", RejectCode: "M2M05"},
|
||||||
{Security: "RU000A0JUR61", Recommend: "confirm"},
|
{Security: "RU000A0JUR61", Recommend: "confirm"},
|
||||||
}}
|
}}
|
||||||
finishResult(&res)
|
finishResult(&res)
|
||||||
if res.Recommend != "reject" || !strings.Contains(res.Summary, "отказ пакетом M2M05") {
|
if res.Recommend != "partial" || !strings.Contains(res.Summary, "принять 2, отказ 1") {
|
||||||
t.Fatalf("итог: %s %q", res.Recommend, res.Summary)
|
t.Fatalf("итог: %s %q", res.Recommend, res.Summary)
|
||||||
}
|
}
|
||||||
for _, s := range res.Secs {
|
if res.Secs[0].Recommend != "confirm" || res.Secs[2].Recommend != "confirm" || res.Secs[1].RejectCode != "M2M05" {
|
||||||
if s.Recommend != "reject" || s.RejectCode != "M2M05" {
|
t.Fatalf("бумаги решаются отдельно: %+v", res.Secs)
|
||||||
t.Fatalf("бумага %s: %s %s", s.Security, s.Recommend, s.RejectCode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if last := res.Secs[0].Checks[len(res.Secs[0].Checks)-1]; last.OK || !strings.Contains(last.Detail, "RU0007661625") {
|
|
||||||
t.Fatalf("пояснение отказа пакетом: %+v", last)
|
|
||||||
}
|
|
||||||
|
|
||||||
ok := depoResult{Secs: []depoSecResult{{Recommend: "confirm"}, {Recommend: "confirm"}}}
|
ok := depoResult{Secs: []depoSecResult{{Recommend: "confirm"}, {Recommend: "confirm"}}}
|
||||||
finishResult(&ok)
|
finishResult(&ok)
|
||||||
if ok.Recommend != "confirm" {
|
if ok.Recommend != "confirm" {
|
||||||
t.Fatalf("все бумаги прошли: %s", ok.Recommend)
|
t.Fatalf("все бумаги прошли: %s", ok.Recommend)
|
||||||
}
|
}
|
||||||
|
no := depoResult{Secs: []depoSecResult{{Recommend: "reject", RejectCode: "M2M01"}, {Recommend: "reject", RejectCode: "M2M05"}}}
|
||||||
|
finishResult(&no)
|
||||||
|
if no.Recommend != "reject" || no.Summary != "отказ M2M01" {
|
||||||
|
t.Fatalf("все отклонены: %s %q", no.Recommend, no.Summary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPackageDecisions(t *testing.T) {
|
// Автоответ: частичное согласие уходит как есть; перевод на ИИС — отказ M2M07 по всем бумагам.
|
||||||
in := []SecurityDecision{{ReferenceID: "A", Confirm: true}, {ReferenceID: "B", RejectCode: "M2M08"}, {ReferenceID: "C", Confirm: true}}
|
func TestAutoDecisionsPartialAndIIS(t *testing.T) {
|
||||||
out := packageDecisions(in)
|
it := &IncomingTransfer{Securities: []IncomingSecurity{{ReferenceID: "A"}, {ReferenceID: "B"}}}
|
||||||
for _, d := range out {
|
r := depoResult{Secs: []depoSecResult{{ReferenceID: "A", Recommend: "confirm"}, {ReferenceID: "B", Recommend: "reject", RejectCode: "M2M05"}}}
|
||||||
if d.Confirm || d.RejectCode != "M2M08" {
|
d, _ := autoDecisions(it, r)
|
||||||
t.Fatalf("частичное согласие не должно уходить: %+v", out)
|
if !d[0].Confirm || d[1].Confirm || d[1].RejectCode != "M2M05" {
|
||||||
|
t.Fatalf("частичное согласие: %+v", d)
|
||||||
|
}
|
||||||
|
it.IIS = true
|
||||||
|
d, _ = autoDecisions(it, r)
|
||||||
|
for _, x := range d {
|
||||||
|
if x.Confirm || x.RejectCode != "M2M07" {
|
||||||
|
t.Fatalf("ИИС не принимаем: %+v", d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !in[0].Confirm {
|
legacy := &IncomingTransfer{RawXML: "<x><IIAAgreementDetails/></x>"}
|
||||||
t.Fatal("исходный срез не должен меняться")
|
if !incomingIsIIS(legacy) {
|
||||||
}
|
t.Fatal("ИИС у сохранённого раньше перевода — по исходному XML")
|
||||||
all := []SecurityDecision{{ReferenceID: "A", Confirm: true}, {ReferenceID: "B", Confirm: true}}
|
|
||||||
if got := packageDecisions(all); !got[0].Confirm || !got[1].Confirm {
|
|
||||||
t.Fatalf("согласие по всем: %+v", got)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type IncomingTransfer struct {
|
|||||||
DocNumber string `json:"doc_number,omitempty"`
|
DocNumber string `json:"doc_number,omitempty"`
|
||||||
ReceivingINN string `json:"receiving_inn,omitempty"` // принимающий депозитарий (мы)
|
ReceivingINN string `json:"receiving_inn,omitempty"` // принимающий депозитарий (мы)
|
||||||
Securities []IncomingSecurity `json:"securities"`
|
Securities []IncomingSecurity `json:"securities"`
|
||||||
|
IIS bool `json:"iis,omitempty"` // перевод на ИИС (в запросе есть IIAAgreementDetails)
|
||||||
RawXML string `json:"raw_xml"`
|
RawXML string `json:"raw_xml"`
|
||||||
DecisionXML string `json:"decision_xml,omitempty"` // наш ответ M2MTransferDecision (для выгрузки регулятору)
|
DecisionXML string `json:"decision_xml,omitempty"` // наш ответ M2MTransferDecision (для выгрузки регулятору)
|
||||||
Status string `json:"status"` // pending | answered
|
Status string `json:"status"` // pending | answered
|
||||||
@@ -135,6 +136,7 @@ func (s *IncomingStore) AddFromRequest(req *m2m.M2MTransferRequest, rawXML strin
|
|||||||
id, _ := newID()
|
id, _ := newID()
|
||||||
inv := req.Data.InvestorInformation
|
inv := req.Data.InvestorInformation
|
||||||
it := &IncomingTransfer{
|
it := &IncomingTransfer{
|
||||||
|
IIS: req.Header.IIAAgreementDetails != nil,
|
||||||
ID: id,
|
ID: id,
|
||||||
GUID: guid,
|
GUID: guid,
|
||||||
RequestCreated: req.Header.CreationTimestamp.Time,
|
RequestCreated: req.Header.CreationTimestamp.Time,
|
||||||
@@ -358,3 +360,8 @@ func newID() (string, error) {
|
|||||||
}
|
}
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// incomingIsIIS — перевод на ИИС: по признаку из запроса, а у сохранённых раньше — по исходному XML.
|
||||||
|
func incomingIsIIS(it *IncomingTransfer) bool {
|
||||||
|
return it.IIS || strings.Contains(it.RawXML, "IIAAgreementDetails")
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func answerIncoming(ctx context.Context, deps incomingDeps, id string, decisions
|
|||||||
}
|
}
|
||||||
nsd := cfg.NSD
|
nsd := cfg.NSD
|
||||||
|
|
||||||
decisions = packageDecisions(decisions)
|
decisions = iisDecisions(it, decisions)
|
||||||
|
|
||||||
// Карта решений по ReferenceID.
|
// Карта решений по ReferenceID.
|
||||||
byRef := make(map[string]SecurityDecision, len(decisions))
|
byRef := make(map[string]SecurityDecision, len(decisions))
|
||||||
@@ -154,32 +154,15 @@ func answerIncoming(ctx context.Context, deps incomingDeps, id string, decisions
|
|||||||
// transferringINN — ИНН нашей организации (передающий депозитарий в запросе, принимающий — в решении).
|
// transferringINN — ИНН нашей организации (передающий депозитарий в запросе, принимающий — в решении).
|
||||||
func transferringINN(s Settings) string { return orgINN(s) }
|
func transferringINN(s Settings) string { return orgINN(s) }
|
||||||
|
|
||||||
// packageDecisions — пакет принимается только целиком (решение 16.09.2026): если хотя бы по одной бумаге
|
// iisDecisions — входящие переводы на ИИС не принимаем (решение 25.09.2026): отказ M2M07 по всем
|
||||||
// отказ, отказываем по всем; принятым бумагам ставится код первой отказной.
|
// бумагам, даже если оператор отметил «принять». Остальные переводы — по каждой бумаге отдельно.
|
||||||
func packageDecisions(decisions []SecurityDecision) []SecurityDecision {
|
func iisDecisions(it *IncomingTransfer, decisions []SecurityDecision) []SecurityDecision {
|
||||||
code := ""
|
if it == nil || !incomingIsIIS(it) {
|
||||||
for _, d := range decisions {
|
|
||||||
if !d.Confirm {
|
|
||||||
code = d.RejectCode
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if code == "" {
|
|
||||||
for _, d := range decisions {
|
|
||||||
if !d.Confirm {
|
|
||||||
code = "M2M05"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if code == "" {
|
|
||||||
return decisions
|
return decisions
|
||||||
}
|
}
|
||||||
out := make([]SecurityDecision, len(decisions))
|
out := make([]SecurityDecision, len(decisions))
|
||||||
for i, d := range decisions {
|
for i, d := range decisions {
|
||||||
if d.Confirm {
|
d.Confirm, d.RejectCode = false, "M2M07"
|
||||||
d.Confirm, d.RejectCode = false, code
|
|
||||||
}
|
|
||||||
out[i] = d
|
out[i] = d
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package lkgateway
|
|||||||
// Автоматический режим M2M (решение 16.09.2026): ручные шаги оператора выполняются без участия
|
// Автоматический режим M2M (решение 16.09.2026): ручные шаги оператора выполняются без участия
|
||||||
// человека, чтобы соблюдать сроки и убрать человеческий фактор.
|
// человека, чтобы соблюдать сроки и убрать человеческий фактор.
|
||||||
//
|
//
|
||||||
// - Входящий перевод ждёт решения → свежая сверка с DEPO → решение по её рекомендации: пакет
|
// - Входящий перевод ждёт решения → свежая сверка с DEPO → решение по её рекомендации: каждая
|
||||||
// принимается только целиком; предупреждение со Strict (бумага для квалифицированных инвесторов
|
// бумага решается сама по себе (частичный приём), переводы на ИИС — отказ M2M07 по всем бумагам;
|
||||||
|
// предупреждение со Strict (бумага для квалифицированных инвесторов
|
||||||
// при неполной квалификации клиента) — отказ M2M08. Без сверки ничего не принимается: если DEPO
|
// при неполной квалификации клиента) — отказ M2M08. Без сверки ничего не принимается: если DEPO
|
||||||
// или ИШ недоступны, bj повторяет каждые m2mAutoRetry и шлёт оповещение.
|
// или ИШ недоступны, bj повторяет каждые m2mAutoRetry и шлёт оповещение.
|
||||||
// - После нашего согласия НРД присылает черновик 16/1 (MF010) → поручение подаётся автоматически.
|
// - После нашего согласия НРД присылает черновик 16/1 (MF010) → поручение подаётся автоматически.
|
||||||
@@ -144,15 +145,19 @@ func incomingHasConfirmed(it *IncomingTransfer) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// autoDecisions — решение по результату сверки: рекомендация по каждой бумаге, Strict-предупреждение
|
// autoDecisions — решение по результату сверки: рекомендация по каждой бумаге, Strict-предупреждение
|
||||||
// даёт отказ M2M08, пакет только целиком. why — пояснение для карточки.
|
// даёт отказ M2M08 по этой бумаге, остальные решаются сами по себе; перевод на ИИС — отказ всем. why — пояснение для карточки.
|
||||||
func autoDecisions(it *IncomingTransfer, r depoResult) (decisions []SecurityDecision, why string) {
|
func autoDecisions(it *IncomingTransfer, r depoResult) (decisions []SecurityDecision, why string) {
|
||||||
bySec := map[string]depoSecResult{}
|
bySec := map[string]depoSecResult{}
|
||||||
|
strictSec := map[string]bool{} // бумаги для квал. инвесторов при неполной квалификации — отказ M2M08 именно по ним
|
||||||
strict := ""
|
strict := ""
|
||||||
for _, sr := range r.Secs {
|
for _, sr := range r.Secs {
|
||||||
bySec[sr.ReferenceID] = sr
|
bySec[sr.ReferenceID] = sr
|
||||||
for _, c := range sr.Checks {
|
for _, c := range sr.Checks {
|
||||||
if c.Strict && c.Warn && strict == "" {
|
if c.Strict && c.Warn {
|
||||||
strict = sr.Security
|
strictSec[sr.ReferenceID] = true
|
||||||
|
if strict == "" {
|
||||||
|
strict = sr.Security
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +167,7 @@ func autoDecisions(it *IncomingTransfer, r depoResult) (decisions []SecurityDeci
|
|||||||
switch {
|
switch {
|
||||||
case !ok:
|
case !ok:
|
||||||
d.RejectCode = "M2M05"
|
d.RejectCode = "M2M05"
|
||||||
case strict != "":
|
case strictSec[sec.ReferenceID]:
|
||||||
d.RejectCode = "M2M08"
|
d.RejectCode = "M2M08"
|
||||||
case sr.Recommend == "confirm":
|
case sr.Recommend == "confirm":
|
||||||
d.Confirm = true
|
d.Confirm = true
|
||||||
@@ -174,10 +179,10 @@ func autoDecisions(it *IncomingTransfer, r depoResult) (decisions []SecurityDeci
|
|||||||
}
|
}
|
||||||
decisions = append(decisions, d)
|
decisions = append(decisions, d)
|
||||||
}
|
}
|
||||||
decisions = packageDecisions(decisions)
|
decisions = iisDecisions(it, decisions)
|
||||||
why = r.Summary
|
why = r.Summary
|
||||||
if strict != "" {
|
if strict != "" {
|
||||||
why = "отказ M2M08: бумага " + strict + " для квалифицированных инвесторов, квалификация клиента неполная"
|
why += "; отказ M2M08 по " + strict + ": бумага для квалифицированных инвесторов, квалификация клиента неполная"
|
||||||
}
|
}
|
||||||
return decisions, why
|
return decisions, why
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,25 +16,21 @@ func TestAutoDecisions(t *testing.T) {
|
|||||||
t.Fatalf("согласие: %+v", dec)
|
t.Fatalf("согласие: %+v", dec)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отказ по одной — отказ всем пакетом с её кодом.
|
// Отказ по одной — частичный приём: остальные принимаются (решение 25.09.2026).
|
||||||
r = depoResult{Summary: "отказ пакетом M2M05", Secs: []depoSecResult{{ReferenceID: "A", Recommend: "confirm"}, {ReferenceID: "B", Recommend: "reject", RejectCode: "M2M05"}}}
|
r = depoResult{Summary: "частично", Secs: []depoSecResult{{ReferenceID: "A", Recommend: "confirm"}, {ReferenceID: "B", Recommend: "reject", RejectCode: "M2M05"}}}
|
||||||
dec, _ = autoDecisions(it, r)
|
dec, _ = autoDecisions(it, r)
|
||||||
for _, d := range dec {
|
if !dec[0].Confirm || dec[1].Confirm || dec[1].RejectCode != "M2M05" {
|
||||||
if d.Confirm || d.RejectCode != "M2M05" {
|
t.Fatalf("частичный приём: %+v", dec)
|
||||||
t.Fatalf("пакет целиком: %+v", dec)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Бумага для квал. инвесторов при неполной квалификации — отказ M2M08 всем.
|
// Бумага для квал. инвесторов при неполной квалификации — отказ M2M08 только по ней.
|
||||||
r = depoResult{Summary: "принять", Secs: []depoSecResult{
|
r = depoResult{Summary: "принять", Secs: []depoSecResult{
|
||||||
{ReferenceID: "A", Security: "RU000A10F5K6", Recommend: "confirm", Checks: []depoCheck{{Name: "Бумага только для квал. инвесторов", OK: true, Warn: true, Strict: true, Code: "M2M08"}}},
|
{ReferenceID: "A", Security: "RU000A10F5K6", Recommend: "confirm", Checks: []depoCheck{{Name: "Бумага только для квал. инвесторов", OK: true, Warn: true, Strict: true, Code: "M2M08"}}},
|
||||||
{ReferenceID: "B", Recommend: "confirm", Checks: []depoCheck{{Name: "Квал-инвестор (если бумага только для квал.)", OK: true, Warn: true, Code: "M2M08"}}},
|
{ReferenceID: "B", Recommend: "confirm", Checks: []depoCheck{{Name: "Квал-инвестор (если бумага только для квал.)", OK: true, Warn: true, Code: "M2M08"}}},
|
||||||
}}
|
}}
|
||||||
dec, why := autoDecisions(it, r)
|
dec, why := autoDecisions(it, r)
|
||||||
for _, d := range dec {
|
if dec[0].Confirm || dec[0].RejectCode != "M2M08" || !dec[1].Confirm {
|
||||||
if d.Confirm || d.RejectCode != "M2M08" {
|
t.Fatalf("строгое предупреждение — отказ только по этой бумаге: %+v", dec)
|
||||||
t.Fatalf("строгое предупреждение: %+v", dec)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !strings.Contains(why, "RU000A10F5K6") {
|
if !strings.Contains(why, "RU000A10F5K6") {
|
||||||
t.Fatalf("пояснение: %q", why)
|
t.Fatalf("пояснение: %q", why)
|
||||||
@@ -49,9 +45,9 @@ func TestAutoDecisions(t *testing.T) {
|
|||||||
t.Fatalf("информационное предупреждение не должно отказывать: %+v", dec)
|
t.Fatalf("информационное предупреждение не должно отказывать: %+v", dec)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Бумага без результата сверки — не принимаем.
|
// Бумага без результата сверки — не принимаем (остальные решаются сами по себе).
|
||||||
r = depoResult{Secs: []depoSecResult{{ReferenceID: "A", Recommend: "confirm"}}}
|
r = depoResult{Secs: []depoSecResult{{ReferenceID: "A", Recommend: "confirm"}}}
|
||||||
if dec, _ = autoDecisions(it, r); dec[0].Confirm || dec[1].Confirm {
|
if dec, _ = autoDecisions(it, r); !dec[0].Confirm || dec[1].Confirm {
|
||||||
t.Fatalf("без сверки по бумаге: %+v", dec)
|
t.Fatalf("без сверки по бумаге: %+v", dec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
.dc-rec{display:inline-block;padding:5px 14px;border-radius:999px;font-weight:700;font-size:14px}
|
.dc-rec{display:inline-block;padding:5px 14px;border-radius:999px;font-weight:700;font-size:14px}
|
||||||
.dc-rec.confirm{background:var(--ok-weak,#e6f4ea);color:var(--ok,#12813f)}
|
.dc-rec.confirm{background:var(--ok-weak,#e6f4ea);color:var(--ok,#12813f)}
|
||||||
.dc-rec.reject{background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)}
|
.dc-rec.reject{background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)}
|
||||||
|
.dc-rec.partial,.dc-rec.mixed{background:var(--warn-weak,#fff4e0);color:var(--warn,#9a6b12)}
|
||||||
.dc-rec.mixed{background:var(--warn-weak,#fff4e0);color:var(--warn,#9a6b12)}
|
.dc-rec.mixed{background:var(--warn-weak,#fff4e0);color:var(--warn,#9a6b12)}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<h2>Ход исполнения</h2>
|
<h2>Ход исполнения</h2>
|
||||||
<ol>
|
<ol>
|
||||||
<li><b>Пакет отправлен в НРД.</b> Сервис MOEX МОСТ проверяет формат и присылает квитанцию. Если есть ошибка, поручение завершается отказом с кодом, а блокировка снимается.</li>
|
<li><b>Пакет отправлен в НРД.</b> Сервис MOEX МОСТ проверяет формат и присылает квитанцию. Если есть ошибка, поручение завершается отказом с кодом, а блокировка снимается.</li>
|
||||||
<li><b>Решение принимающего депозитария.</b> Депозитарий соглашается или отказывает (код отказа виден в карточке). Пакет принимается только целиком.</li>
|
<li><b>Решение принимающего депозитария.</b> Депозитарий соглашается или отказывает по каждой бумаге отдельно (код отказа виден в карточке): возможен частичный приём. Переводы на ИИС не принимаются.</li>
|
||||||
<li><b>Черновик поручения 16.</b> После согласия НРД присылает черновик поручения на списание.</li>
|
<li><b>Черновик поручения 16.</b> После согласия НРД присылает черновик поручения на списание.</li>
|
||||||
<li><b>Поручение 16 подано.</b> Нажмите «Подать поручение 16 по черновику». Если включён автоматический режим M2M, bj подаёт поручение сам.</li>
|
<li><b>Поручение 16 подано.</b> Нажмите «Подать поручение 16 по черновику». Если включён автоматический режим M2M, bj подаёт поручение сам.</li>
|
||||||
<li><b>Отчёт НРД об исполнении.</b> Когда отчёт (MS010) получен, бумаги списаны. Кнопка «Документы НРД» скачивает архив с квитанциями и отчётом, в том числе в PDF.</li>
|
<li><b>Отчёт НРД об исполнении.</b> Когда отчёт (MS010) получен, бумаги списаны. Кнопка «Документы НРД» скачивает архив с квитанциями и отчётом, в том числе в PDF.</li>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
.dc-rec{display:inline-block;padding:4px 12px;border-radius:999px;font-weight:700;font-size:13px}
|
.dc-rec{display:inline-block;padding:4px 12px;border-radius:999px;font-weight:700;font-size:13px}
|
||||||
.dc-rec.confirm{background:var(--ok-weak,#e6f4ea);color:var(--ok,#12813f)}
|
.dc-rec.confirm{background:var(--ok-weak,#e6f4ea);color:var(--ok,#12813f)}
|
||||||
.dc-rec.reject{background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)}
|
.dc-rec.reject{background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)}
|
||||||
.dc-rec.mixed{background:var(--warn-weak,#fff4e0);color:var(--warn,#9a6b12)}
|
.dc-rec.mixed,.dc-rec.partial{background:var(--warn-weak,#fff4e0);color:var(--warn,#9a6b12)}
|
||||||
</style>
|
</style>
|
||||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||||
<h2 style="margin:0">Сверка с DEPO — {{if devOn}}эмуляция ответа{{else}}рекомендация по ответу{{end}}</h2>
|
<h2 style="margin:0">Сверка с DEPO — {{if devOn}}эмуляция ответа{{else}}рекомендация по ответу{{end}}</h2>
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="muted" style="font-size:12.5px;margin:10px 0 0">Пакет принимается только целиком: при отказе по одной бумаге отказ ставится всем бумагам с тем же кодом (так и будет отправлено).</p>
|
<p class="muted" style="font-size:12.5px;margin:10px 0 0">Решение по каждой бумаге отдельно: принятые бумаги НРД переведёт (по каждой придёт свой черновик 16/1), по остальным уйдёт отказ с выбранным кодом.{{if and $.It (isIIS $.It)}} <b>Перевод на ИИС — такие мы не принимаем: отказ M2M07 по всем бумагам будет отправлен в любом случае.</b>{{end}}</p>
|
||||||
<div style="display:flex;gap:8px;margin-top:14px;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;margin-top:14px;flex-wrap:wrap">
|
||||||
<button type="submit" class="btn btn-ok">Отправить решение{{if devOn}} роботу{{end}}</button>
|
<button type="submit" class="btn btn-ok">Отправить решение{{if devOn}} роботу{{end}}</button>
|
||||||
<button type="button" class="btn" onclick="m2mSetAll('confirm')">Принять все</button>
|
<button type="button" class="btn" onclick="m2mSetAll('confirm')">Принять все</button>
|
||||||
@@ -161,16 +161,6 @@
|
|||||||
document.querySelectorAll('input.dec').forEach(function(r){
|
document.querySelectorAll('input.dec').forEach(function(r){
|
||||||
r.addEventListener('change', function(){
|
r.addEventListener('change', function(){
|
||||||
toggle(r.getAttribute('data-ref'));
|
toggle(r.getAttribute('data-ref'));
|
||||||
// Пакет целиком: отказ по одной бумаге — отказ по всем с тем же кодом.
|
|
||||||
if(r.value === 'reject' && r.checked){
|
|
||||||
var src = document.querySelector('select[name="reject_'+r.getAttribute('data-ref')+'"]');
|
|
||||||
document.querySelectorAll('input.dec[value="reject"]').forEach(function(o){
|
|
||||||
if(o.checked) return;
|
|
||||||
o.checked = true; toggle(o.getAttribute('data-ref'));
|
|
||||||
var sel = document.querySelector('select[name="reject_'+o.getAttribute('data-ref')+'"]');
|
|
||||||
if(sel && src) sel.value = src.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
window.m2mSetAll = function(val){
|
window.m2mSetAll = function(val){
|
||||||
|
|||||||
@@ -107,6 +107,7 @@
|
|||||||
.dc-rec{display:inline-block;padding:4px 12px;border-radius:999px;font-weight:700;font-size:13px}
|
.dc-rec{display:inline-block;padding:4px 12px;border-radius:999px;font-weight:700;font-size:13px}
|
||||||
.dc-rec.confirm{background:var(--ok-weak,#e6f4ea);color:var(--ok,#12813f)}
|
.dc-rec.confirm{background:var(--ok-weak,#e6f4ea);color:var(--ok,#12813f)}
|
||||||
.dc-rec.reject{background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)}
|
.dc-rec.reject{background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)}
|
||||||
|
.dc-rec.partial,.dc-rec.mixed{background:var(--warn-weak,#fff4e0);color:var(--warn,#9a6b12)}
|
||||||
.dc-rec.mixed{background:var(--warn-weak,#fff4e0);color:var(--warn,#9a6b12)}
|
.dc-rec.mixed{background:var(--warn-weak,#fff4e0);color:var(--warn,#9a6b12)}
|
||||||
</style>
|
</style>
|
||||||
<div style="margin-top:18px;padding:12px 14px;border:1px solid var(--border);border-radius:12px;background:var(--card)">
|
<div style="margin-top:18px;padding:12px 14px;border:1px solid var(--border);border-radius:12px;background:var(--card)">
|
||||||
|
|||||||
@@ -313,7 +313,7 @@
|
|||||||
{{template "orgSection" .}}
|
{{template "orgSection" .}}
|
||||||
<div class="card" id="m2m-auto">
|
<div class="card" id="m2m-auto">
|
||||||
<h2><span class="dot {{if .Settings.M2MAuto.Enabled}}ok{{else}}warn{{end}}"></span>Автоматический режим M2M</h2>
|
<h2><span class="dot {{if .Settings.M2MAuto.Enabled}}ok{{else}}warn{{end}}"></span>Автоматический режим M2M</h2>
|
||||||
<p class="muted">Без участия оператора, чтобы соблюдать сроки: <strong>ответ на входящие переводы</strong> по свежей сверке с DEPO (пакет принимается только целиком; бумага для квалифицированных инвесторов при неполной или непрочитанной квалификации клиента — отказ M2M08), <strong>подача поручения 16/1</strong>, как только НРД пришлёт черновик после нашего согласия, и <strong>подача поручения 16</strong> по исходящим после согласия контрагента. Если DEPO или ИШ недоступны — bj повторяет каждые 3 минуты и шлёт оповещение; без сверки ничего не принимается. Обрабатывается только то, что пришло не раньше чем за сутки до включения. Кнопки в карточках остаются для ручной работы.</p>
|
<p class="muted">Без участия оператора, чтобы соблюдать сроки: <strong>ответ на входящие переводы</strong> по свежей сверке с DEPO (решение по каждой бумаге отдельно — возможен частичный приём; переводы на ИИС — отказ M2M07; бумага для квалифицированных инвесторов при неполной или непрочитанной квалификации клиента — отказ M2M08), <strong>подача поручения 16/1</strong>, как только НРД пришлёт черновик после нашего согласия, и <strong>подача поручения 16</strong> по исходящим после согласия контрагента. Если DEPO или ИШ недоступны — bj повторяет каждые 3 минуты и шлёт оповещение; без сверки ничего не принимается. Обрабатывается только то, что пришло не раньше чем за сутки до включения. Кнопки в карточках остаются для ручной работы.</p>
|
||||||
<form method="post" action="/admin/setup/m2m-auto" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:10px">
|
<form method="post" action="/admin/setup/m2m-auto" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:10px">
|
||||||
<input type="hidden" name="section" value="m2m-auto">
|
<input type="hidden" name="section" value="m2m-auto">
|
||||||
<label style="display:flex;gap:8px;align-items:center;font-weight:600;margin:0"><input type="checkbox" name="enabled" value="1" {{if .Settings.M2MAuto.Enabled}}checked{{end}}> Автоматический режим включён</label>
|
<label style="display:flex;gap:8px;align-items:center;font-weight:600;margin:0"><input type="checkbox" name="enabled" value="1" {{if .Settings.M2MAuto.Enabled}}checked{{end}}> Автоматический режим включён</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user