c73f6120d3
Правильная обработка входящих переводов на ИИС вместо прежнего сплошного отказа.
Настройка AcceptIIS (карточка «Кто отвечает на переводы M2M»):
- выкл (по умолчанию) — поведение не меняется: перевод на ИИС отклоняется целиком (M2M06);
- вкл — ИИС принимается ТОЛЬКО целиком: все бумаги зачисляются на ИИС-счёт клиента;
если хоть одной бумаги нет в учёте / не проходит сверку — отказ по ВСЕМУ переводу.
Изменения:
- iisDecisions: «всё или ничего» (при выкл — отказ всем; при вкл — либо все принять,
либо все отклонить, сохраняя реальные коды отказа по проблемным бумагам);
- depo_check: при вкл приёме ИИС убран искусственный отказ «Перевод не на ИИС»,
добавлена проверка «Счёт зачисления — ИИС» (partyChecks получил флаг wantIIS);
- настройка AcceptIIS в Settings + геттер/сеттер + обработчик /admin/setup/accept-iis
+ тумблер в part_nsd.html; тесты обновлены.
Жёсткий запрет частичного ИИС в answerIncoming (коммит dc8b785) остаётся как страховка.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
5.2 KiB
Go
100 lines
5.2 KiB
Go
package lkgateway
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Частичный приём (решение 25.09.2026): каждая бумага решается сама по себе.
|
|
func TestFinishResultPartial(t *testing.T) {
|
|
res := depoResult{Secs: []depoSecResult{
|
|
{Security: "RU0009029540", Recommend: "confirm"},
|
|
{Security: "RU0007661625", Recommend: "reject", RejectCode: "M2M05"},
|
|
{Security: "RU000A0JUR61", Recommend: "confirm"},
|
|
}}
|
|
finishResult(&res)
|
|
if res.Recommend != "partial" || !strings.Contains(res.Summary, "принять 2, отказ 1") {
|
|
t.Fatalf("итог: %s %q", res.Recommend, res.Summary)
|
|
}
|
|
if res.Secs[0].Recommend != "confirm" || res.Secs[2].Recommend != "confirm" || res.Secs[1].RejectCode != "M2M05" {
|
|
t.Fatalf("бумаги решаются отдельно: %+v", res.Secs)
|
|
}
|
|
ok := depoResult{Secs: []depoSecResult{{Recommend: "confirm"}, {Recommend: "confirm"}}}
|
|
finishResult(&ok)
|
|
if ok.Recommend != "confirm" {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Автоответ: частичное согласие уходит как есть; перевод на ИИС — отказ M2M06 по всем бумагам.
|
|
func TestAutoDecisionsPartialAndIIS(t *testing.T) {
|
|
it := &IncomingTransfer{Securities: []IncomingSecurity{{ReferenceID: "A"}, {ReferenceID: "B"}}}
|
|
r := depoResult{Secs: []depoSecResult{{ReferenceID: "A", Recommend: "confirm"}, {ReferenceID: "B", Recommend: "reject", RejectCode: "M2M05"}}}
|
|
d, _ := autoDecisions(it, r)
|
|
if !d[0].Confirm || d[1].Confirm || d[1].RejectCode != "M2M05" {
|
|
t.Fatalf("частичное согласие: %+v", d)
|
|
}
|
|
it.IIS = true
|
|
d, _ = autoDecisions(it, r)
|
|
// Приём ИИС по умолчанию ВЫКЛ → отказ по всему переводу (всё-или-ничего).
|
|
// Бумага A (была к приёму) получает код ИИС M2M06, бумага B сохраняет свою причину M2M05.
|
|
if d[0].Confirm || d[1].Confirm {
|
|
t.Fatalf("ИИС выключен — принимать нельзя: %+v", d)
|
|
}
|
|
if d[0].RejectCode != "M2M06" || d[1].RejectCode != "M2M05" {
|
|
t.Fatalf("коды отказа ИИС: A=M2M06, B сохраняет M2M05: %+v", d)
|
|
}
|
|
legacy := &IncomingTransfer{RawXML: "<x><IIAAgreementDetails/></x>"}
|
|
if !incomingIsIIS(legacy) {
|
|
t.Fatal("ИИС у сохранённого раньше перевода — по исходному XML")
|
|
}
|
|
}
|
|
|
|
func TestQualOnlyCheck(t *testing.T) {
|
|
notQual := depoParty{faceOK: true, qualWarn: true, qualDet: "инвестор НЕ квалифицированный"}
|
|
qualAll := depoParty{faceOK: true, qualAll: true}
|
|
partial := depoParty{faceOK: true, qualDet: "инвестор — квалифицированный (опция DEPO): облигации"}
|
|
const q = "бумага предназначена для квалифицированных инвесторов (опция DEPO)"
|
|
|
|
if c := qualOnlyCheck(true, true, q, notQual); c.OK || c.Code != "M2M08" {
|
|
t.Fatalf("неквал + бумага для квал: %+v", c)
|
|
}
|
|
if c := qualOnlyCheck(true, true, q, qualAll); !c.OK || c.Warn {
|
|
t.Fatalf("квал по всем видам: %+v", c)
|
|
}
|
|
if c := qualOnlyCheck(true, true, q, partial); !c.OK || !c.Warn {
|
|
t.Fatalf("квал по части видов — решает оператор: %+v", c)
|
|
}
|
|
if c := qualOnlyCheck(true, false, "ограничений по обращению нет (опция DEPO)", notQual); !c.OK || c.Warn {
|
|
t.Fatalf("бумага без ограничений: %+v", c)
|
|
}
|
|
if c := qualOnlyCheck(true, true, q, depoParty{}); !c.OK {
|
|
t.Fatalf("инвестор не найден — отказ даёт M2M04, не эта проверка: %+v", c)
|
|
}
|
|
}
|
|
|
|
// Списание: бумага для квал. инвесторов у неквалифицированного у нас клиента — предупреждение, не отказ.
|
|
func TestOutQualCheck(t *testing.T) {
|
|
const q = "бумага предназначена для квалифицированных инвесторов (опция DEPO)"
|
|
notQual := depoParty{faceOK: true, qualWarn: true, qualDet: "инвестор НЕ квалифицированный"}
|
|
if c := outQualCheck(true, true, q, notQual); !c.OK || !c.Warn || c.Strict {
|
|
t.Fatalf("неквал у нас отправляет бумагу для квал: %+v", c)
|
|
}
|
|
if c := outQualCheck(true, true, q, depoParty{faceOK: true, qualAll: true}); !c.OK || c.Warn {
|
|
t.Fatalf("квал по всем видам: %+v", c)
|
|
}
|
|
if c := outQualCheck(true, false, "ограничений по обращению нет (опция DEPO)", notQual); !c.OK || c.Warn {
|
|
t.Fatalf("бумага без ограничений — зелёный: %+v", c)
|
|
}
|
|
for _, c := range partyChecks(notQual, "1", "2", "x", false) {
|
|
if strings.HasPrefix(c.Name, "Квал") {
|
|
t.Fatalf("общая жёлтая строка квалификации больше не нужна: %+v", c)
|
|
}
|
|
}
|
|
}
|