diff --git a/internal/lkgateway/depo_check.go b/internal/lkgateway/depo_check.go
index 1572c80..e70eddb 100644
--- a/internal/lkgateway/depo_check.go
+++ b/internal/lkgateway/depo_check.go
@@ -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)
}
diff --git a/internal/lkgateway/depo_check_test.go b/internal/lkgateway/depo_check_test.go
index 3ce4ce3..5147da2 100644
--- a/internal/lkgateway/depo_check_test.go
+++ b/internal/lkgateway/depo_check_test.go
@@ -5,17 +5,17 @@ import "testing"
func TestPartyChecksFIO(t *testing.T) {
p := depoParty{faceOK: true, faceName: "Сёмин Пётр Ильич", accAny: "ВТНКЦ00000001"}
p.fioMatch = normFIO(p.faceName) == normFIO("СЕМИН петр ильич")
- if c := partyChecks(p, "4500", "123456", "СЕМИН петр ильич")[0]; !c.OK {
+ if c := partyChecks(p, "4500", "123456", "СЕМИН петр ильич", false)[0]; !c.OK {
t.Errorf("регистр, пробелы и ё не должны давать расхождение: %+v", c)
}
p.fioMatch = normFIO(p.faceName) == normFIO("Сёмин Павел Ильич")
- c := partyChecks(p, "4500", "123456", "Сёмин Павел Ильич")[0]
+ c := partyChecks(p, "4500", "123456", "Сёмин Павел Ильич", false)[0]
if c.OK || c.Code != "M2M04" {
t.Errorf("паспорт совпал, ФИО нет — ожидали отказ M2M04: %+v", c)
}
// Физлицо в картотеке без торгового счёта — не наш клиент: M2M04 первой проверкой.
noAcc := depoParty{faceOK: true, faceName: "Баранкин Сергей Сергеевич", fioMatch: true}
- checks := partyChecks(noAcc, "3763", "290089", "Баранкин Сергей Сергеевич")
+ checks := partyChecks(noAcc, "3763", "290089", "Баранкин Сергей Сергеевич", false)
if c := checks[0]; c.OK || c.Code != "M2M04" {
t.Errorf("лицо без счёта — ожидали отказ M2M04: %+v", c)
}
diff --git a/internal/lkgateway/depo_package_test.go b/internal/lkgateway/depo_package_test.go
index cee3e8b..c6dedfe 100644
--- a/internal/lkgateway/depo_package_test.go
+++ b/internal/lkgateway/depo_package_test.go
@@ -41,10 +41,13 @@ func TestAutoDecisionsPartialAndIIS(t *testing.T) {
}
it.IIS = true
d, _ = autoDecisions(it, r)
- for _, x := range d {
- if x.Confirm || x.RejectCode != "M2M06" {
- t.Fatalf("ИИС не принимаем: %+v", d)
- }
+ // Приём ИИС по умолчанию ВЫКЛ → отказ по всему переводу (всё-или-ничего).
+ // Бумага 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: ""}
if !incomingIsIIS(legacy) {
@@ -88,7 +91,7 @@ func TestOutQualCheck(t *testing.T) {
if c := outQualCheck(true, false, "ограничений по обращению нет (опция DEPO)", notQual); !c.OK || c.Warn {
t.Fatalf("бумага без ограничений — зелёный: %+v", c)
}
- for _, c := range partyChecks(notQual, "1", "2", "x") {
+ for _, c := range partyChecks(notQual, "1", "2", "x", false) {
if strings.HasPrefix(c.Name, "Квал") {
t.Fatalf("общая жёлтая строка квалификации больше не нужна: %+v", c)
}
diff --git a/internal/lkgateway/incoming_answer.go b/internal/lkgateway/incoming_answer.go
index 3137211..4c61aee 100644
--- a/internal/lkgateway/incoming_answer.go
+++ b/internal/lkgateway/incoming_answer.go
@@ -162,20 +162,62 @@ func answerIncoming(ctx context.Context, deps incomingDeps, id string, decisions
// transferringINN — ИНН нашей организации (передающий депозитарий в запросе, принимающий — в решении).
func transferringINN(s Settings) string { return orgINN(s) }
-// iisRejectCode — код отказа по переводу на ИИС: M2M06 «Приостановление операций по счёту депо зачисления»
-// (решение депозитария 07.10.2026; до этого — M2M07).
+// iisRejectCode — код отказа по переводу на ИИС: M2M06 «Приостановление операций по счёту депо
+// зачисления» (решение депозитария 07.10.2026; до этого — M2M07).
const iisRejectCode = "M2M06"
-// iisDecisions — входящие переводы на ИИС не принимаем (решение 25.09.2026): отказ iisRejectCode по всем
-// бумагам, даже если оператор отметил «принять». Остальные переводы — по каждой бумаге отдельно.
+// iisDecisions применяет к ИИС-переводу правило «ВСЁ ИЛИ НИЧЕГО»: ИИС переводится только целиком,
+// иначе у клиента окажется два ИИС с потерей льгот.
+// - приём ИИС выключен (по умолчанию, AcceptIIS=false) → отказ по всем бумагам (iisRejectCode);
+// - приём ИИС включён и подтверждена КАЖДАЯ бумага перевода → принимаем ИИС полностью;
+// - приём ИИС включён, но хоть одна бумага не подтверждена → отказ по ВСЕМ бумагам (у бумаг с
+// явной причиной отказа её код сохраняется, у остальных — iisRejectCode).
+//
+// Не-ИИС переводы не трогаем — решение по каждой бумаге отдельно.
func iisDecisions(it *IncomingTransfer, decisions []SecurityDecision) []SecurityDecision {
if it == nil || !incomingIsIIS(it) {
return decisions
}
- out := make([]SecurityDecision, len(decisions))
- for i, d := range decisions {
- d.Confirm, d.RejectCode = false, iisRejectCode
- out[i] = d
+ rejectAll := func() []SecurityDecision {
+ explicit := make(map[string]SecurityDecision, len(decisions))
+ for _, d := range decisions {
+ explicit[d.ReferenceID] = d
+ }
+ out := make([]SecurityDecision, 0, len(it.Securities))
+ for _, sec := range it.Securities {
+ d := SecurityDecision{ReferenceID: sec.ReferenceID, RejectCode: iisRejectCode}
+ if e, ok := explicit[sec.ReferenceID]; ok && !e.Confirm && e.RejectCode != "" {
+ d.RejectCode = e.RejectCode // сохраняем реальную причину отказа оператора/сверки
+ }
+ out = append(out, d)
+ }
+ return out
}
- return out
+ if !m2mAcceptIIS() {
+ return rejectAll() // приём ИИС выключен — отклоняем перевод целиком
+ }
+ if len(it.Securities) == 0 {
+ return rejectAll()
+ }
+ // Принимаем ИИС, только если подтверждена КАЖДАЯ бумага перевода.
+ confirm := make(map[string]bool, len(decisions))
+ for _, d := range decisions {
+ if d.Confirm {
+ confirm[d.ReferenceID] = true
+ }
+ }
+ for _, sec := range it.Securities {
+ if !confirm[sec.ReferenceID] {
+ return rejectAll() // хоть одна бумага не принимается — отказ по всему ИИС
+ }
+ }
+ return decisions // все бумаги подтверждены — принимаем ИИС полностью
+}
+
+// m2mAcceptIIS — включён ли приём входящих переводов на ИИС в текущих настройках.
+func m2mAcceptIIS() bool {
+ if globalRC == nil {
+ return false
+ }
+ return globalRC.AcceptIIS()
}
diff --git a/internal/lkgateway/m2mauto.go b/internal/lkgateway/m2mauto.go
index e2c6a3e..11739ec 100644
--- a/internal/lkgateway/m2mauto.go
+++ b/internal/lkgateway/m2mauto.go
@@ -289,6 +289,47 @@ func m2mAutoForm16(ctx context.Context, rc *RuntimeConfig, d outgoingDeps, od *O
}
// saveM2MAuto — POST /admin/setup/m2m-auto.
+// saveAcceptIIS — POST /admin/setup/accept-iis: приём входящих переводов на ИИС (всё-или-ничего).
+func (h *setupHandlers) saveAcceptIIS(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "method", http.StatusMethodNotAllowed)
+ return
+ }
+ cur := h.rc.AcceptIIS()
+ on := r.FormValue("enabled") != ""
+ if m := r.FormValue("mode"); m != "" {
+ on = m == "on"
+ }
+ ajax := r.Header.Get("X-BJ-Ajax") != ""
+ reply := func(msg string) {
+ if ajax {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ _, _ = w.Write([]byte(msg))
+ return
+ }
+ setupFlash(w, r, "Приём ИИС: "+msg)
+ }
+ if on == cur {
+ reply("без изменений")
+ return
+ }
+ if err := h.rc.UpdateAcceptIIS(on); err != nil {
+ reply("ошибка сохранения: " + err.Error())
+ return
+ }
+ who := ""
+ if u := currentUser(w); u != nil {
+ who = u.Login
+ }
+ if on {
+ log.Printf("accept-iis: приём входящих переводов на ИИС ВКЛЮЧЁН (%s)", who)
+ reply("включён — ИИС принимается только ЦЕЛИКОМ: все бумаги на ИИС-счёт клиента, иначе отказ по всему переводу")
+ return
+ }
+ log.Printf("accept-iis: приём входящих переводов на ИИС выключен (%s)", who)
+ reply("выключен — входящие переводы на ИИС отклоняются целиком (M2M06)")
+}
+
func (h *setupHandlers) saveM2MAuto(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
diff --git a/internal/lkgateway/runtimeconfig.go b/internal/lkgateway/runtimeconfig.go
index be78518..9e2d8ed 100644
--- a/internal/lkgateway/runtimeconfig.go
+++ b/internal/lkgateway/runtimeconfig.go
@@ -21,6 +21,22 @@ type RuntimeConfig struct {
data Settings
}
+// AcceptIIS — принимаем ли входящие переводы на ИИС (режим «всё или ничего»).
+func (r *RuntimeConfig) AcceptIIS() bool {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ return r.data.AcceptIIS
+}
+
+// UpdateAcceptIIS — включить/выключить приём входящих переводов на ИИС.
+func (r *RuntimeConfig) UpdateAcceptIIS(on bool) error {
+ r.mu.Lock()
+ r.data.AcceptIIS = on
+ r.data.UpdatedAt = time.Now().UTC()
+ r.mu.Unlock()
+ return r.save()
+}
+
// Settings — сериализуемое представление настроек.
type Settings struct {
Postgres PostgresSettings `json:"postgres"`
@@ -45,6 +61,7 @@ type Settings struct {
HTTPS HTTPSSettings `json:"https,omitempty"`
Time TimeSettings `json:"time,omitempty"`
WorkHours WorkHoursSettings `json:"work_hours,omitempty"` // рабочее время: запросы M2M вне него — отказ M2M06
+ AcceptIIS bool `json:"accept_iis,omitempty"` // принимать входящие переводы на ИИС (режим «всё или ничего»); по умолчанию выкл — ИИС отклоняется целиком
OneC OneCSettings `json:"onec,omitempty"` // база 1С бэк-офиса (модуль onec-back)
NSDEmu NSDEmuSettings `json:"nsd_emu,omitempty"` // эмулятор НРД (страница «Эмуляторы», только режим разработки)
MNPEmu MNPEmuSettings `json:"mnp_emu,omitempty"` // эмулятор МНП QUIK (страница «Эмуляторы», только режим разработки)
diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go
index 2e0054a..ac02af6 100644
--- a/internal/lkgateway/setup.go
+++ b/internal/lkgateway/setup.go
@@ -117,6 +117,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service
registerQuikExportSetup(mux, h)
mux.HandleFunc("/admin/setup/versions/refresh", h.refreshVersionsHandler)
mux.HandleFunc("/admin/setup/m2m-auto", h.saveM2MAuto)
+ mux.HandleFunc("/admin/setup/accept-iis", h.saveAcceptIIS)
registerUsersSetup(mux, h)
mux.HandleFunc("/admin/setup/npr", h.saveNPR)
mux.HandleFunc("/admin/setup/npr/test", h.testNPR)
diff --git a/internal/lkgateway/web/templates/part_nsd.html b/internal/lkgateway/web/templates/part_nsd.html
index 99770ff..1a0f70a 100644
--- a/internal/lkgateway/web/templates/part_nsd.html
+++ b/internal/lkgateway/web/templates/part_nsd.html
@@ -32,14 +32,33 @@
Что bj делает сам в автоматическом режиме
- - Входящие переводы — отвечает по свежей сверке с DEPO, по каждой бумаге отдельно (возможен частичный приём). Переводы на ИИС — отказ M2M06; бумага для квалифицированных инвесторов, а у клиента нет квалификации, — отказ M2M08.
+ - Входящие переводы — отвечает по свежей сверке с DEPO, по каждой бумаге отдельно (возможен частичный приём). Переводы на ИИС — только ЦЕЛИКОМ (если включён приём ИИС ниже: все бумаги на ИИС-счёт клиента, иначе отказ по всему переводу; выключено — отказ M2M06). Бумага для квалифицированных инвесторов, а у клиента нет квалификации, — отказ M2M08.
- Поручение 16/1 — подаёт, как только НРД пришлёт черновик после нашего согласия.
- Исходящие переводы — поручение 16 подаёт после согласия контрагента.
- Если DEPO или шлюз НРД недоступны, bj повторяет каждые 3 минуты и шлёт оповещение; без сверки ничего не принимается.
- При включении берутся переводы, пришедшие не раньше чем за сутки до этого. Кнопки в карточках остаются для ручной работы.
+
+