M2M ИИС: приём «всё или ничего» (настройка AcceptIIS, по умолчанию выкл)

Правильная обработка входящих переводов на ИИС вместо прежнего сплошного отказа.
Настройка 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>
This commit is contained in:
fontvielle
2026-10-14 10:43:21 +03:00
parent dc8b785a4b
commit c73f6120d3
8 changed files with 157 additions and 24 deletions
+16 -6
View File
@@ -368,8 +368,18 @@ func finishResult(res *depoResult) {
}
// partyChecks — общие проверки инвестора/счёта/ИИС/квал (одинаковы для обеих сверок).
func partyChecks(p depoParty, ser, num, fio string) []depoCheck {
func partyChecks(p depoParty, ser, num, fio string, wantIIS bool) []depoCheck {
ser = strings.ReplaceAll(strings.TrimSpace(ser), " ", "")
// Счёт зачисления: для обычного перевода должен быть НЕ ИИС; для перевода на ИИС —
// наоборот, обязан быть ИИС-счётом клиента (иначе ИИС-бумаги зачислять некуда).
iisChk := depoCheck{Name: "Счёт не ИИС", OK: p.iisOK, Code: iisRejectCode, Detail: p.iisDet}
if wantIIS {
det := p.iisDet
if p.iisOK {
det = "перевод на ИИС, но счёт зачисления клиента НЕ ИИС-счёт — зачислять ИИС-бумаги некуда (" + p.iisDet + ")"
}
iisChk = depoCheck{Name: "Счёт зачисления — ИИС", OK: !p.iisOK, Code: iisRejectCode, Detail: det}
}
return []depoCheck{
// Инвестор — наш клиент: лицо по паспорту с тем же ФИО И хотя бы один торговый счёт (договор).
// Физлицо в картотеке без счёта у нас — не клиент депозитария: отказ M2M04, а не M2M02 (16.09.2026).
@@ -382,7 +392,7 @@ func partyChecks(p depoParty, ser, num, fio string) []depoCheck {
Detail: pick3(p.accAny != "", p.accAny, pick3(p.faceOK, "у инвестора нет счёта "+depoAccPrefix()+"… в OD_CLIENTS", "не проверяется — инвестор не найден"))},
{Name: "Статус счёта строго «открыт»", OK: p.accOpen != "", Code: "M2M02",
Detail: pick3(p.accAny == "", "не проверяется — счёта нет", pick3(p.accOpen != "", p.accOpen+" — открыт (STATE=0, без даты закрытия)", p.accAny+" — НЕ открыт (закрыт или STATE≠0)"))},
{Name: "Счёт не ИИС", OK: p.iisOK, Code: iisRejectCode, Detail: p.iisDet},
iisChk,
}
}
@@ -414,9 +424,9 @@ func runDepoCheck(it *IncomingTransfer) depoResult {
for _, s := range it.Securities {
isin := strings.ToUpper(strings.TrimSpace(s.ISIN))
sr := depoSecResult{ReferenceID: s.ReferenceID, Security: pick(isin, s.SecurityCode)}
if incomingIsIIS(it) { // решение 25.09.2026: входящие переводы на ИИС не принимаем совсем
if incomingIsIIS(it) && !m2mAcceptIIS() { // приём ИИС выключен — отклоняем перевод целиком
sr.Checks = append(sr.Checks, depoCheck{Name: "Перевод не на ИИС", OK: false, Code: iisRejectCode,
Detail: "перевод на ИИС (в запросе договор ИИС) — входящие переводы на ИИС не принимаем, отказ по всем бумагам"})
Detail: "перевод на ИИС (в запросе договор ИИС) — приём ИИС выключен, отказ по всем бумагам"})
}
ruOK := strings.HasPrefix(isin, "RU")
sr.Checks = append(sr.Checks, depoCheck{Name: "Российский выпуск (RU-ISIN)", OK: ruOK, Code: "M2M01",
@@ -431,7 +441,7 @@ func runDepoCheck(it *IncomingTransfer) depoResult {
qualOnly, qdet = depoSecurityQualOnly(ctx, db, valID)
}
sr.Checks = append(sr.Checks, qualOnlyCheck(found, qualOnly, qdet, p))
sr.Checks = append(sr.Checks, partyChecks(p, it.DocSeries, it.DocNumber, fio)...)
sr.Checks = append(sr.Checks, partyChecks(p, it.DocSeries, it.DocNumber, fio, incomingIsIIS(it) && m2mAcceptIIS())...)
blockOK, blockDet := true, "не проверяется — нет открытого счёта или бумаги"
if p.accOpen != "" && found {
blockOK, blockDet = depoBlocking(ctx, db, valID, p.clientID)
@@ -556,7 +566,7 @@ func runDepoCheckOutAccount(last, first, middle, ser, num, birth string, secs []
qualOnly, qdet = depoSecurityQualOnly(ctx, db, valID)
}
sr.Checks = append(sr.Checks, outQualCheck(found, qualOnly, qdet, p))
sr.Checks = append(sr.Checks, partyChecks(p, ser, num, fio)...)
sr.Checks = append(sr.Checks, partyChecks(p, ser, num, fio, false)...)
if birthCheck != nil {
sr.Checks = append(sr.Checks, *birthCheck)
}