package lkgateway import ( "context" "fmt" "log" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml" ) // SecurityDecision — наше решение по одной бумаге входящего перевода. type SecurityDecision struct { ReferenceID string Confirm bool // true — принять, false — отказать RejectCode string // код отказа (по умолчанию M2M05), если Confirm=false } // incomingDeps — зависимости для формирования и отправки решения. type incomingDeps struct { store *IncomingStore igwClient *igw.Client igwChan string rc *RuntimeConfig ourCode string // наш депкод (DefaultSender) } // answerIncoming формирует M2MTransferDecision на входящий перевод и отправляет // его обратно через ИШ. GUID решения = GUID исходного запроса робота (так // требует протокол: связь ответа с запросом). Реквизиты зачисления для // подтверждённых бумаг берутся из настроек НРД (наш депо-счёт). func answerIncoming(ctx context.Context, deps incomingDeps, id string, decisions []SecurityDecision) error { it, ok := deps.store.Get(id) if !ok { return fmt.Errorf("входящий перевод %s не найден", id) } if it.Status == "answered" { return fmt.Errorf("по переводу уже отправлено решение (%s)", it.AnswerNote) } if deps.igwClient == nil || deps.igwChan == "" { return fmt.Errorf("ИШ не настроен — нельзя отправить решение") } cfg := deps.rc.Snapshot() ourCode := deps.ourCode // наш депкод (отправитель решения) if ourCode == "" { ourCode = it.ReceiverCode // на кого пришёл запрос — тем и отвечаем } nsd := cfg.NSD decisions = iisDecisions(it, decisions) // Карта решений по ReferenceID. byRef := make(map[string]SecurityDecision, len(decisions)) for _, d := range decisions { byRef[d.ReferenceID] = d } // Собираем Decision по каждой бумаге входящего перевода. var decSecs []m2m.DecisionSecurity markMap := make(map[string]IncomingSecurity, len(it.Securities)) confirmed, rejected := 0, 0 for _, sec := range it.Securities { d, has := byRef[sec.ReferenceID] if !has { // По умолчанию — отказ (безопаснее не принимать без явного решения). d = SecurityDecision{ReferenceID: sec.ReferenceID, Confirm: false} } ds := m2m.DecisionSecurity{ReferenceID: m2m.ReferenceID(sec.ReferenceID)} mark := IncomingSecurity{ReferenceID: sec.ReferenceID} if d.Confirm { ds.TransferDecision = m2m.DecisionTransfer{ Confirmation: &m2m.Confirmation{ SettlementAccount: m2m.DecisionSettlementAccount{ SettlementRequisites: m2m.SettlementRequisites{ // ИНН МЕСТА РАСЧЁТОВ (НРД), а не наш собственный — иначе // M2M35 «место расчётов отсутствует в реквизитах». INN: m2m.OrganizationINN(settlementPlaceINN("nrd")), }, SettlementLocation: m2m.SettlementDepositoryLocation{ DeponentCode: nsd.DeponentCode, AccountID: m2m.AccountID(nsd.AccountID), SectionID: nsd.SectionID, }, }, }, } mark.Decision = "confirmed" confirmed++ } else { code := d.RejectCode if code == "" { code = "M2M05" // дефолтный код отказа } ds.TransferDecision = m2m.DecisionTransfer{ Rejection: &m2m.Rejection{Codes: []string{code}}, } mark.Decision = "rejected" mark.RejectCode = code rejected++ } decSecs = append(decSecs, ds) markMap[sec.ReferenceID] = mark } // Защита ИИС: перевод по договору ИИС принимается только ЦЕЛИКОМ — все бумаги // или ни одной. Частичное решение (часть принять, часть отклонить) дало бы // неполный перевод ИИС и два ИИС у клиента с потерей льгот. Этот запрет не // обойти ни ручным решением оператора, ни будущим смягчением политики приёма. if incomingIsIIS(it) && confirmed > 0 && rejected > 0 { return fmt.Errorf("перевод на ИИС принимается только целиком: нельзя принять часть бумаг (%d) и отклонить часть (%d) — примите все бумаги или отклоните весь перевод", confirmed, rejected) } dec := &m2m.M2MTransferDecision{ Header: m2m.DecisionHeader{ GUID: m2m.UUID(it.GUID), // GUID из запроса робота! CreationTimestamp: nsdxml.Now(), SenderCode: m2m.DeponentCode(ourCode), ReceiverCode: m2m.DeponentCode(it.SenderCode), // обратно роботу CostInfo: m2m.CostInfo{No: &m2m.CostInfoNo{}}, }, Data: m2m.DecisionData{ ReceivingDepository: m2m.SettlementRequisites{INN: m2m.OrganizationINN(transferringINN(cfg))}, Securities: decSecs, }, } if err := dec.Validate(); err != nil { return fmt.Errorf("собранное решение невалидно: %w", err) } xmlBytes, err := nsdxml.Marshal(dec) if err != nil { return fmt.Errorf("сериализация решения: %w", err) } deps.store.SetDecisionXML(id, string(nsdxml.DecodeWindows1251(xmlBytes))) zipBody, err := igw.PackXML(xmlBytes, "M2MTransferDecision.xml", "#M2MTD") if err != nil { return fmt.Errorf("упаковка решения: %w", err) } sctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() pkgID, err := deps.igwClient.SendPackage(sctx, deps.igwChan, "#M2MTD", zipBody) if err != nil { return fmt.Errorf("отправка решения в ИШ: %w", err) } note := fmt.Sprintf("Решение отправлено (пакет ИШ %s): принято %d, отказано %d", pkgID, confirmed, rejected) deps.store.MarkAnswered(id, markMap, note) go depoOnIncomingDecision(deps.rc, it, decisions) log.Printf("lk-gateway: ответ на входящий перевод %s (GUID %s) отправлен: %s", id, it.GUID, note) // Письмо депозитарию: перевод принят/зачислён (если что-то приняли). if confirmed > 0 { if upd, ok := deps.store.Get(id); ok { go notifyDepositoryAccepted(deps.rc, upd, confirmed, rejected) } } return nil } // transferringINN — ИНН нашей организации (передающий депозитарий в запросе, принимающий — в решении). func transferringINN(s Settings) string { return orgINN(s) } // iisRejectCode — код отказа по переводу на ИИС: M2M06 «Приостановление операций по счёту депо // зачисления» (решение депозитария 07.10.2026; до этого — M2M07). const iisRejectCode = "M2M06" // iisDecisions применяет к ИИС-переводу правило «ВСЁ ИЛИ НИЧЕГО»: ИИС переводится только целиком, // иначе у клиента окажется два ИИС с потерей льгот. // - приём ИИС выключен (по умолчанию, AcceptIIS=false) → отказ по всем бумагам (iisRejectCode); // - приём ИИС включён и подтверждена КАЖДАЯ бумага перевода → принимаем ИИС полностью; // - приём ИИС включён, но хоть одна бумага не подтверждена → отказ по ВСЕМ бумагам (у бумаг с // явной причиной отказа её код сохраняется, у остальных — iisRejectCode). // // Не-ИИС переводы не трогаем — решение по каждой бумаге отдельно. func iisDecisions(it *IncomingTransfer, decisions []SecurityDecision) []SecurityDecision { if it == nil || !incomingIsIIS(it) { return decisions } 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 } 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() }