Files

187 lines
6.9 KiB
Go

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 = packageDecisions(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
}
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) }
// packageDecisions — пакет принимается только целиком (решение 16.09.2026): если хотя бы по одной бумаге
// отказ, отказываем по всем; принятым бумагам ставится код первой отказной.
func packageDecisions(decisions []SecurityDecision) []SecurityDecision {
code := ""
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
}
out := make([]SecurityDecision, len(decisions))
for i, d := range decisions {
if d.Confirm {
d.Confirm, d.RejectCode = false, code
}
out[i] = d
}
return out
}