Files
Bridge-and-Join-s/internal/lkgateway/lkemulate.go
T

331 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// Имитация НРД для заявок ЛК ЕСИА Финанс. Заявка проходит весь путь, как в бою:
//
// ЛК (depo_m2m) → журнал исходящих заявок bj → проверка шлюза (сверка списания с DEPO)
// отказ → сделка «Отклонена» → отказ в Fancy (DEPO) → ответ Fancy → ЛК «Отклонена» (lkflow.go)
// принять → блокировка в QUIK МНП → DEPO (BJ_SENT) → «отправка» в имитатор НРД
// → через lkEmuDecisionDelay решение контрагента → тот же обработчик, что у
// настоящего M2MTransferDecision (сделка, ЛК «Исполнена», QUIK, DEPO)
//
// В НРД ничего не уходит. Включается флагом LK.EmulateNSD и принудительно, если активен
// боевой ЛК, а профиль ИШ тестовый: настоящие заявки клиентов в тестовый НРД не шлём.
import (
"context"
"encoding/json"
"fmt"
"log"
"strconv"
"strings"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/mock"
)
const lkEmuDecisionDelay = 30 * time.Second
// lkEmuNote дописывается к статусу в ЛК, чтобы было видно, что НРД не участвовал.
const lkEmuNote = " (тестовый режим: в НРД не отправлялось)"
// lkEmuSender — имитатор НРД для заявок ЛК; создаётся в Server.Run.
var lkEmuSender *mock.Sender
// lkEmulateNSD — идут ли заявки ЛК через имитатор НРД и почему.
func lkEmulateNSD(rc *RuntimeConfig) (bool, string) {
s := rc.Snapshot()
if s.LK.EmulateNSD && devMode(rc) {
return true, "включена в настройках"
}
if s.LK.ActiveEnv == "prod" && strings.HasPrefix(strings.ToLower(s.NSD.Profile), "test") {
return true, "боевой ЛК при тестовом контуре НРД (профиль ИШ " + s.NSD.Profile + ")"
}
return false, ""
}
// lkProcessEmulated — путь заявки ЛК без отправки в НРД. Возвращает "rejected" или
// "sent"; ошибка — заявку нужно повторить на следующем проходе (DEPO недоступна и т.п.).
func lkProcessEmulated(ctx context.Context, rc *RuntimeConfig, svc *Service, ep LKEndpoint, id int, claimReq CreateClaimRequest) (string, error) {
if lkEmuSender == nil {
return "", fmt.Errorf("имитатор НРД не запущен")
}
inv := claimReq.Investor
// Формат полей заявки не решает, берём ли её: решает проверка шлюза.
var secs []depoOutSec
acc, section := "", ""
for _, sec := range claimReq.Securities {
qty := sec.Quantity.Fractional
if qty == "" {
qty = strconv.FormatUint(sec.Quantity.Whole, 10)
}
secs = append(secs, depoOutSec{ISIN: sec.SecurityDetails.ISIN, Code: sec.SecurityCode, Qty: qty})
if len(sec.SettlementAccounts) > 0 && acc == "" {
loc := sec.SettlementAccounts[0].SettlementLocation
acc, section = loc.AccountID, loc.SectionID
}
}
res := runDepoCheckOutAccount(inv.LastName, inv.FirstName, inv.MiddleName, inv.Document.Series, inv.Document.Number, inv.BirthDate, secs, acc, section, claimReq.ClientAccount)
if !res.Ran {
return "", fmt.Errorf("сверка с DEPO не выполнена: %s", res.Err)
}
// Пакет собираем и при некорректных данных: заявка должна попасть в журнал и в Fancy.
req, err := svc.BuildRequestUnchecked(ctx, claimReq)
if err != nil {
return "", fmt.Errorf("сборка пакета: %w", err)
}
guid := string(req.Header.GUID)
log.Printf("lk-gateway: ЛК #%d (%s): сверка с DEPO — %s (инвестор %s, счёт %s)", id, shortGUID(guid), res.Summary, pick(res.Investor, "не найден"), pick(res.Account, "—"))
globalLKFlows.step(guid, id, "db", "Заявка из ЛК ЕСИА Финанс", fmt.Sprintf("Забрана коннектором bj из личного кабинета (заявка ЛК #%d).", id))
code, short, reason := "", "", ""
if res.Recommend != "confirm" {
code, short, reason = lkDepoRejectText(res)
} else if verr := req.Validate(); verr != nil {
short, reason = "Отказ: данные заявки не прошли проверку", "Отказ: данные заявки не прошли проверку формата НРД — "+verr.Error()
}
if reason != "" {
globalLKFlows.step(guid, id, "reject", "Проверка шлюза bj: отказ", reason)
lkGatewayReject(ctx, rc, svc, id, inv.ID, req, code, short, reason)
log.Printf("lk-gateway: ЛК #%d (%s): отказ шлюза, передан в Fancy — %s", id, shortGUID(guid), reason)
return "rejected", nil
}
globalLKFlows.step(guid, id, "crane", "Проверка шлюза bj: можно исполнить",
fmt.Sprintf("Сверка с DEPO: %s (инвестор %s, счёт %s).", res.Summary, pick(res.Investor, "—"), pick(res.Account, "—")))
// Тест «боевой ЛК → TEST3» (realtest.go): клиент из списка — в настоящий НРД с подменой инвестора.
sender := m2mcore.NSDSender(lkEmuSender)
realNSD := realTestMatch(rc, inv.Document.Series, inv.Document.Number) && svc.sender != nil
if realNSD {
asInv, asText := realTestInvestor(rc, req.Data.InvestorInformation.IdentityDocument.DocumentType)
realTestPut(RealTestRec{GUID: guid, ClaimID: id, At: time.Now(), As: asText,
Client: strings.TrimSpace(fmt.Sprintf("%s %s %s, %s %s", inv.LastName, inv.FirstName, inv.MiddleName, inv.Document.Series, inv.Document.Number))})
sender = substituteSender{real: svc.sender, inv: asInv}
globalLKFlows.step(guid, id, "gateway", "Тест: в НРД (TEST3) — с данными тест-инвестора",
"У нас заявка проходит как клиент ("+inv.LastName+"): DEPO, QUIK МНП (боевая запись), статусы в ЛК. В НРД блок инвестора заменён на "+asText+"; GUID и референсы — настоящие.")
log.Printf("lk-gateway: ЛК #%d (%s): тест «боевой ЛК → TEST3» — в НРД как %s", id, shortGUID(guid), asText)
}
if _, err := svc.SendBuiltRequestVia(ctx, sender, req, inv.ID, nil); err != nil {
if strings.Contains(err.Error(), "QUIK") {
reason := "Отказ: не удалось заблокировать бумаги клиента — " + err.Error()
globalLKFlows.step(guid, id, "reject", "Блокировка в QUIK не удалась", err.Error())
lkGatewayReject(ctx, rc, svc, id, inv.ID, req, "QUIK", "Отказ: не удалось заблокировать ценные бумаги", reason)
return "rejected", nil
}
if realNSD {
// НРД не принял пакет — заявка «Не отправлена», повтор автоматически (sendfail.go); в ЛК — «В работе»
globalLKFlows.step(guid, id, "reject", "НРД не принял пакет — повтор автоматически", err.Error())
lkMarkSent(id)
lkLinkGUID(guid, ep, id)
return "sent", nil
}
return "", fmt.Errorf("имитация отправки: %w", err)
}
lkMarkSent(id)
lkLinkGUID(guid, ep, id)
globalLKFlows.update(guid, func(f *LKFlow) { f.Stage = "sent" })
sentTitle := "Бумаги заблокированы, заявка обработана шлюзом bj без отправки в НРД"
if devMode(rc) {
sentTitle = "Бумаги заблокированы, заявка передана имитатору НРД"
}
sentDetail := lkQuikSummary(guid) + fmt.Sprintf(" В НРД не отправлялась; ответ контрагента — через %s.", lkEmuDecisionDelay)
if realNSD {
sentTitle, sentDetail = "Бумаги заблокированы, запрос перевода отправлен в НРД (TEST3)", lkQuikSummary(guid)+" Ждём решения контрагента от НРД."
}
globalLKFlows.step(guid, id, "gateway", sentTitle, sentDetail)
if pc, pb := lkPatch(ctx, ep, fmt.Sprintf("/claims/%d", id), []byte(`{"claim":{"status":"in_process"}}`)); pc >= 400 {
log.Printf("lk-gateway: ЛК #%d: PATCH «В работе» не прошёл: HTTP %d %s", id, pc, pb)
}
if realNSD {
log.Printf("lk-gateway: ЛК #%d (%s): сверка пройдена, бумаги заблокированы, отправлено в НРД TEST3 с подменой инвестора", id, shortGUID(guid))
return "sent", nil
}
log.Printf("lk-gateway: ЛК #%d (%s): сверка пройдена, бумаги заблокированы, заявка передана имитатору НРД — решение контрагента через %s", id, shortGUID(guid), lkEmuDecisionDelay)
return "sent", nil
}
// lkQuikSummary — что сделал QUIK МНП по переводу (для шага пути).
func lkQuikSummary(guid string) string {
links := globalQuikLinks.ForGUID(guid)
if len(links) == 0 {
return "QUIK: блокировка не выполнялась (цикл QUIK выключен или клиент не найден в DEPO)."
}
var parts []string
for _, l := range links {
p := fmt.Sprintf("%s × %v — %s", l.ISIN, l.Qty, l.StateRu())
if l.InstrNo != "" {
p += ", поручение №" + l.InstrNo
}
if l.Err != "" {
p += ": " + l.Err
}
parts = append(parts, p)
}
return "QUIK: " + strings.Join(parts, "; ") + "."
}
// lkDepoRejectText — код отказа по результату сверки: short — короткий текст для клиента в ЛК
// («Отказ по результатам проверки депозитария: <причина> (код)»), text — с подробностями
// проверок для журнала bj и Fancy.
func lkDepoRejectText(res depoResult) (code, short, text string) {
var codes, labels, details []string
for _, s := range res.Secs {
for _, c := range s.Checks {
if c.OK {
continue
}
if c.Code != "" && !containsStr(codes, c.Code) {
codes = append(codes, c.Code)
labels = append(labels, m2mCodeLabel(c.Code))
}
details = append(details, c.Name+": "+c.Detail)
}
}
if len(codes) > 0 {
code = codes[0]
}
short = "Отказ по результатам проверки депозитария"
if len(labels) > 0 {
short += ": " + labels[0]
}
text = "Отказ по результатам проверки депозитария"
if len(labels) > 0 {
text += ": " + strings.Join(labels, "; ")
}
if len(details) > 0 {
text += ". Причина: " + strings.Join(details, "; ")
}
if r := []rune(text); len(r) > 1000 {
text = string(r[:1000]) + "…"
}
return code, short, text
}
func containsStr(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
// lkPatchRejected — заявка ЛК «Отклонена» с причиной в client_notation; помечается
// обработанной, чтобы поллер не брал её повторно. Возвращает HTTP-код и ответ ЛК.
func lkPatchRejected(ctx context.Context, ep LKEndpoint, id int, reason string) (int, string) {
lkMarkSent(id)
body, _ := json.Marshal(map[string]any{"claim": map[string]any{"status": "rejected", "client_notation": reason}})
pc, pb := lkPatch(ctx, ep, fmt.Sprintf("/claims/%d", id), body)
if pc >= 400 || pc == 0 {
log.Printf("lk-gateway: ЛК #%d: PATCH «Отклонена» не прошёл: HTTP %d %s", id, pc, pb)
} else {
log.Printf("lk-gateway: ЛК #%d: отклонена — %s", id, reason)
}
return pc, pb
}
// lkDataError — заявку ЛК нельзя отправлять из-за её данных: повтор не поможет, отказываем.
type lkDataError struct{ Code, Text string }
func (e *lkDataError) Error() string { return e.Text }
// lkRejectInvalid — отказ по данным заявки в реальном режиме (без имитации, до отправки в НРД).
func lkRejectInvalid(ctx context.Context, rc *RuntimeConfig, ep LKEndpoint, id int, de *lkDataError) {
reason := "Отказ: заявка не может быть исполнена"
if de.Code != "" {
reason = "Отказ: " + m2mCodeLabel(de.Code)
}
log.Printf("lk-gateway: ЛК #%d: отказ по данным заявки — %s (%s)", id, de.Text, de.Code)
if pc, pb := lkPatchRejected(ctx, ep, id, reason); pc >= 400 || pc == 0 {
raiseSystemAlert(rc, "ЛК: не удалось отклонить заявку", fmt.Sprintf("Заявка ЛК #%d отклонена (%s), но статус в ЛК не изменён: HTTP %d %s", id, reason, pc, pb))
}
}
// consumeEmulatedDecisions — решения контрагента от имитатора НРД идут тем же путём,
// что настоящий M2MTransferDecision из ИШ.
func (s *Server) consumeEmulatedDecisions(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case d := <-lkEmuSender.Decisions():
if d == nil {
continue
}
guid := string(d.Header.GUID)
log.Printf("lk-gateway: имитатор НРД: решение контрагента по %s", shortGUID(guid))
conf, rej := 0, 0
for _, sec := range d.Data.Securities {
if sec.TransferDecision.Confirmation != nil {
conf++
} else {
rej++
}
}
respTitle := "Решение по заявке (без отправки в НРД)"
if devMode(s.rc) {
respTitle = "Решение контрагента (имитатор НРД)"
}
globalLKFlows.stepIfExists(guid, "response", respTitle, fmt.Sprintf("Принято %d, отклонено %d.", conf, rej))
s.applyDecisionEffects(ctx, d, "имитатор НРД", lkEmuNote)
}
}
}
// applyDecisionEffects — M2MTransferDecision контрагента: сделка, статус в таблице
// обмена Fancy и в ЛК, снятие блокировок QUIK по отказанным бумагам, событие DEPO.
// note дописывается к статусу в ЛК.
func (s *Server) applyDecisionEffects(ctx context.Context, decision *m2m.M2MTransferDecision, source, note string) {
if err := s.svc.ApplyDecision(ctx, decision); err != nil {
// Детерминированные ошибки (сделка уже в терминальном статусе — напр.
// повторный/встречный Decision, «недопустимый переход FSM») повтором
// не лечатся. Логируем и помечаем обработанным, иначе поллер зациклится.
log.Printf("lk-gateway: %s: Decision GUID=%s не применён (пропуск): %v", source, decision.Header.GUID, err)
return
}
// Разбираем исход по бумагам: сколько принято/отклонено и с какими кодами.
conf, rej := 0, 0
var rejCodes []string
for _, sec := range decision.Data.Securities {
if sec.TransferDecision.Confirmation != nil {
conf++
} else if sec.TransferDecision.Rejection != nil {
rej++
rejCodes = append(rejCodes, sec.TransferDecision.Rejection.Codes...)
}
}
lkStatus, lkReason, fancyStatus, fancyVerdict := "done", "Перевод исполнен", "done", "approved"
switch {
case conf == 0 && rej > 0: // полный отказ принимающей стороны
lkStatus, fancyStatus, fancyVerdict = "rejected", "declined", "rejected"
lkReason = "Отказ принимающей стороны: " + rejectCodesLabel(rejCodes)
case rej > 0: // частичный приём
lkReason = fmt.Sprintf("Частично исполнено: принято %d, отклонено %d (%s)", conf, rej, rejectCodesLabel(rejCodes))
default: // всё принято
lkReason = "Перевод исполнен"
}
// Итог для заявки модуля ЛК (bj-lk), если перевод оттуда.
if e := lkFinalizeByGUID(ctx, s.rc, string(decision.Header.GUID), fancyStatus, fancyVerdict, lkReason); e != nil {
log.Printf("lk-gateway: %s: finalize(%s) GUID=%s: %v", source, fancyStatus, decision.Header.GUID, e)
}
_ = note // клиенту в ЛК — только короткий итог, подробности в журнале bj
if realTestGet(string(decision.Header.GUID)) != nil && conf > 0 {
// тест «боевой ЛК → TEST3»: согласие контрагента — ещё не исполнение; «Исполнена» — по MS010 (realtest.go)
globalLKFlows.stepIfExists(string(decision.Header.GUID), "response", "Контрагент согласился — ждём исполнения НРД",
fmt.Sprintf("Принято %d, отклонено %d. Статус в ЛК — «В работе» до отчёта НРД об исполнении (MS010).", conf, rej))
} else {
OnLKOutcome(ctx, s.rc, string(decision.Header.GUID), lkStatus, lkReason)
}
quikOnDecision(ctx, s.rc, s.svc, decision)
go depoOnCounterpartyDecision(s.rc, decision)
}
func newLKEmuSender() *mock.Sender {
cfg := mock.DefaultConfig()
cfg.DecisionDelay = lkEmuDecisionDelay
cfg.NSDSenderCode = "MC0010300000"
return mock.NewSender(cfg)
}