feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk) - трансферные таблицы FTV_M2M: создание при установке, события, справочник участников - сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO - боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК - онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML) - модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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 deps.igwClient == nil || deps.igwChan == "" {
|
||||
return fmt.Errorf("ИШ не настроен — нельзя отправить решение")
|
||||
}
|
||||
|
||||
cfg := deps.rc.Snapshot()
|
||||
ourCode := deps.ourCode // наш депкод (отправитель решения)
|
||||
if ourCode == "" {
|
||||
ourCode = it.ReceiverCode // на кого пришёл запрос — тем и отвечаем
|
||||
}
|
||||
nsd := cfg.NSD
|
||||
|
||||
// Карта решений по 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 — ИНН нашей организации (принимающий депозитарий в решении).
|
||||
// ИНН ООО ИК «Фонтвьель» (участник M2M MC0413600000).
|
||||
func transferringINN(_ Settings) string {
|
||||
return "7703807489"
|
||||
}
|
||||
Reference in New Issue
Block a user