Тест «боевой ЛК → TEST3»: заявки выбранных клиентов ЕСИА Финанс — у нас всё по-настоящему (DEPO, QUIK МНП боевая запись только для них), в НРД с подменой инвестора на тест-клиента; «Исполнена» в ЛК по MS010; повтор отправки тоже с подменой; пометка и шаги ЛК в пути заявки; включатель на «Тестирование M2M»
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -52,6 +52,7 @@ var templateFuncs = template.FuncMap{
|
|||||||
"curUser": authTemplateFuncs["curUser"],
|
"curUser": authTemplateFuncs["curUser"],
|
||||||
"can": authTemplateFuncs["can"],
|
"can": authTemplateFuncs["can"],
|
||||||
"sendFailText": sendFailText,
|
"sendFailText": sendFailText,
|
||||||
|
"realTest": realTestGet,
|
||||||
"nsdSegTip": nsdSegTip,
|
"nsdSegTip": nsdSegTip,
|
||||||
"devOn": authTemplateFuncs["devOn"],
|
"devOn": authTemplateFuncs["devOn"],
|
||||||
"devLicensed": authTemplateFuncs["devLicensed"],
|
"devLicensed": authTemplateFuncs["devLicensed"],
|
||||||
@@ -676,9 +677,9 @@ func buildClaimTimeline(ctx context.Context, v ClaimView) []TimelineEvent {
|
|||||||
tl = append(tl, ev)
|
tl = append(tl, ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Заявка ЛК, прошедшая шлюз bj (lkflow.go): путь целиком из сохранённых шагов —
|
// Заявка ЛК, прошедшая шлюз bj (lkflow.go), без сделки в НРД (отказ шлюза, имитатор): путь целиком
|
||||||
// проверка шлюза, Fancy, имитатор НРД, статус в ЛК.
|
// из сохранённых шагов. Если заявка ушла в настоящий НРД — шаги ЛК входят в общий путь ниже.
|
||||||
if v.M2MGUID != "" {
|
if v.M2MGUID != "" && realTestGet(string(v.M2MGUID)) == nil {
|
||||||
if f := globalLKFlows.Get(string(v.M2MGUID)); f != nil && len(f.Steps) > 0 {
|
if f := globalLKFlows.Get(string(v.M2MGUID)); f != nil && len(f.Steps) > 0 {
|
||||||
var prev time.Time
|
var prev time.Time
|
||||||
for _, st := range f.Steps {
|
for _, st := range f.Steps {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
//go:build live
|
||||||
|
|
||||||
|
package lkgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Только чтение: кто в DEPO стоит за договором (portfolio) и его бумаги. TL_CONTRACT=БР-859.
|
||||||
|
func TestContractLive(t *testing.T) {
|
||||||
|
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fs := rc.Snapshot().Fancy
|
||||||
|
ctx := context.Background()
|
||||||
|
cs, err := depoListContracts(ctx, fs, "859", 20)
|
||||||
|
fmt.Println("договоры:", cs, err)
|
||||||
|
db, err := fancyOpen(fs, fs.DepoDB())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
rows, err := db.QueryContext(ctx, `SELECT p.PORT_NUM, c.NUMBER, ISNULL(f.NAME,''), ISNULL(f.DOC_SER,''), ISNULL(f.DOC_NUM,''), c.STATE
|
||||||
|
FROM OD_CLIENTS_PORTS p JOIN OD_CLIENTS c ON c.ID=p.CLIENT JOIN OD_FACES f ON f.ID=c.FACE
|
||||||
|
WHERE p.PORT_NUM LIKE @p1`, "%859%")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
var port, num, name, ser, dn string
|
||||||
|
var st int
|
||||||
|
_ = rows.Scan(&port, &num, &name, &ser, &dn, &st)
|
||||||
|
fmt.Printf("портфель %s счёт %s ФИО %q ДУЛ %s %s state %d\n", port, num, name, ser, dn, st)
|
||||||
|
}
|
||||||
|
pos, err := depoClientPositions(ctx, fs, "БР-859")
|
||||||
|
fmt.Println("позиции:", err)
|
||||||
|
for _, p := range pos {
|
||||||
|
fmt.Printf(" %+v\n", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -130,7 +130,13 @@ func claimTimelineFull(ctx context.Context, v ClaimView) []TimelineEvent {
|
|||||||
origin = fmt.Sprintf("Заявка из личного кабинета ЕСИА Финанс (заявка ЛК #%d).", lkID)
|
origin = fmt.Sprintf("Заявка из личного кабинета ЕСИА Финанс (заявка ЛК #%d).", lkID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
add(v.CreatedAt, "Заявка создана", "db", origin)
|
if f := globalLKFlows.Get(guid); guid != "" && f != nil && len(f.Steps) > 0 {
|
||||||
|
for _, st := range f.Steps { // путь заявки ЛК: забрана из кабинета, проверка шлюза, подмена, статусы в ЛК
|
||||||
|
add(st.At, st.Title, st.Kind, st.Detail)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
add(v.CreatedAt, "Заявка создана", "db", origin)
|
||||||
|
}
|
||||||
|
|
||||||
// состояния сделки M2M
|
// состояния сделки M2M
|
||||||
decided := false
|
decided := false
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
"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"
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -94,13 +95,32 @@ func lkProcessEmulated(ctx context.Context, rc *RuntimeConfig, svc *Service, ep
|
|||||||
globalLKFlows.step(guid, id, "crane", "Проверка шлюза bj: можно исполнить",
|
globalLKFlows.step(guid, id, "crane", "Проверка шлюза bj: можно исполнить",
|
||||||
fmt.Sprintf("Сверка с DEPO: %s (инвестор %s, счёт %s).", res.Summary, pick(res.Investor, "—"), pick(res.Account, "—")))
|
fmt.Sprintf("Сверка с DEPO: %s (инвестор %s, счёт %s).", res.Summary, pick(res.Investor, "—"), pick(res.Account, "—")))
|
||||||
|
|
||||||
if _, err := svc.SendBuiltRequestVia(ctx, lkEmuSender, req, inv.ID, nil); err != nil {
|
// Тест «боевой ЛК → 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") {
|
if strings.Contains(err.Error(), "QUIK") {
|
||||||
reason := "Отказ: не удалось заблокировать бумаги клиента — " + err.Error()
|
reason := "Отказ: не удалось заблокировать бумаги клиента — " + err.Error()
|
||||||
globalLKFlows.step(guid, id, "reject", "Блокировка в QUIK не удалась", err.Error())
|
globalLKFlows.step(guid, id, "reject", "Блокировка в QUIK не удалась", err.Error())
|
||||||
lkGatewayReject(ctx, rc, svc, id, inv.ID, req, "QUIK", "Отказ: не удалось заблокировать ценные бумаги", reason)
|
lkGatewayReject(ctx, rc, svc, id, inv.ID, req, "QUIK", "Отказ: не удалось заблокировать ценные бумаги", reason)
|
||||||
return "rejected", nil
|
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)
|
return "", fmt.Errorf("имитация отправки: %w", err)
|
||||||
}
|
}
|
||||||
lkMarkSent(id)
|
lkMarkSent(id)
|
||||||
@@ -110,11 +130,18 @@ func lkProcessEmulated(ctx context.Context, rc *RuntimeConfig, svc *Service, ep
|
|||||||
if devMode(rc) {
|
if devMode(rc) {
|
||||||
sentTitle = "Бумаги заблокированы, заявка передана имитатору НРД"
|
sentTitle = "Бумаги заблокированы, заявка передана имитатору НРД"
|
||||||
}
|
}
|
||||||
globalLKFlows.step(guid, id, "gateway", sentTitle,
|
sentDetail := lkQuikSummary(guid) + fmt.Sprintf(" В НРД не отправлялась; ответ контрагента — через %s.", lkEmuDecisionDelay)
|
||||||
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 {
|
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)
|
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)
|
log.Printf("lk-gateway: ЛК #%d (%s): сверка пройдена, бумаги заблокированы, заявка передана имитатору НРД — решение контрагента через %s", id, shortGUID(guid), lkEmuDecisionDelay)
|
||||||
return "sent", nil
|
return "sent", nil
|
||||||
}
|
}
|
||||||
@@ -284,7 +311,13 @@ func (s *Server) applyDecisionEffects(ctx context.Context, decision *m2m.M2MTran
|
|||||||
log.Printf("lk-gateway: %s: finalize(%s) GUID=%s: %v", source, fancyStatus, decision.Header.GUID, e)
|
log.Printf("lk-gateway: %s: finalize(%s) GUID=%s: %v", source, fancyStatus, decision.Header.GUID, e)
|
||||||
}
|
}
|
||||||
_ = note // клиенту в ЛК — только короткий итог, подробности в журнале bj
|
_ = note // клиенту в ЛК — только короткий итог, подробности в журнале bj
|
||||||
OnLKOutcome(ctx, s.rc, string(decision.Header.GUID), lkStatus, lkReason)
|
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)
|
quikOnDecision(ctx, s.rc, s.svc, decision)
|
||||||
go depoOnCounterpartyDecision(s.rc, decision)
|
go depoOnCounterpartyDecision(s.rc, decision)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,8 @@ func registerM2MTest(mux *http.ServeMux, a *admin, h *setupHandlers) {
|
|||||||
OurINN string
|
OurINN string
|
||||||
Passed, Failed, Prog, Total, Excluded int
|
Passed, Failed, Prog, Total, Excluded int
|
||||||
Pending int
|
Pending int
|
||||||
|
RealNSD RealNSDTestSettings // тест «боевой ЛК → TEST3» (realtest.go)
|
||||||
|
RealNSDAs string
|
||||||
}{
|
}{
|
||||||
page: nowPage("Тестирование M2M", "m2mtest"), Rows: rows,
|
page: nowPage("Тестирование M2M", "m2mtest"), Rows: rows,
|
||||||
CounterpartyName: plan.CounterpartyName, CounterpartyCode: plan.CounterpartyCode,
|
CounterpartyName: plan.CounterpartyName, CounterpartyCode: plan.CounterpartyCode,
|
||||||
@@ -339,6 +341,10 @@ func registerM2MTest(mux *http.ServeMux, a *admin, h *setupHandlers) {
|
|||||||
OurCode: ourCode, OurName: m2mPartyName(ourCode), OurINN: ourINN,
|
OurCode: ourCode, OurName: m2mPartyName(ourCode), OurINN: ourINN,
|
||||||
Passed: passed, Failed: failed, Prog: prog, Total: total, Excluded: excluded,
|
Passed: passed, Failed: failed, Prog: prog, Total: total, Excluded: excluded,
|
||||||
Pending: total - passed - failed - prog - excluded,
|
Pending: total - passed - failed - prog - excluded,
|
||||||
|
RealNSD: h.rc.Snapshot().LK.RealNSDTest,
|
||||||
|
}
|
||||||
|
if l, f, m, sr, n := data.RealNSD.as(); l != "" {
|
||||||
|
data.RealNSDAs = strings.TrimSpace(l + " " + f + " " + m + ", " + sr + " " + n)
|
||||||
}
|
}
|
||||||
render(w, a.m2mtest, data)
|
render(w, a.m2mtest, data)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -335,7 +335,14 @@ func orderWatchCheck(ctx context.Context, rc *RuntimeConfig, guid, direction str
|
|||||||
latest = st.MS010At
|
latest = st.MS010At
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
globalOrderWatch.update(guid, func(w *OrderWatch) { w.MS010At = latest })
|
w = globalOrderWatch.update(guid, func(w *OrderWatch) { w.MS010At = latest })
|
||||||
|
if direction == "out" { // тест «боевой ЛК → TEST3»: итог в ЛК — по исполнению НРД (realtest.go)
|
||||||
|
partial := ""
|
||||||
|
if total, executed, rejected, _ := w.linesSummary(); total > 1 && executed > 0 && rejected > 0 {
|
||||||
|
partial = fmt.Sprintf("исполнено %d из %d бумаг", executed, total)
|
||||||
|
}
|
||||||
|
go realTestOnExecuted(context.Background(), rc, guid, w.Rejected, partial)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ func quikStep(action string, aid int64, r QuikActionResult, ok bool) QuikStep {
|
|||||||
|
|
||||||
// QuikOutRegister — исходящий перевод: зарегистрировать «Вывод ЦБ (M2M)» (QUIK блокирует бумаги).
|
// QuikOutRegister — исходящий перевод: зарегистрировать «Вывод ЦБ (M2M)» (QUIK блокирует бумаги).
|
||||||
func QuikOutRegister(ctx context.Context, rc *RuntimeConfig, guid, clientID, contract, isin string, qty float64, recipientINN, recipientName string, passExpenses bool) (*QuikLink, error) {
|
func QuikOutRegister(ctx context.Context, rc *RuntimeConfig, guid, clientID, contract, isin string, qty float64, recipientINN, recipientName string, passExpenses bool) (*QuikLink, error) {
|
||||||
s := rc.Snapshot().Quik
|
s := quikSettingsFor(rc, guid)
|
||||||
l := globalQuikLinks.update(guid, isin, func(l *QuikLink) {
|
l := globalQuikLinks.update(guid, isin, func(l *QuikLink) {
|
||||||
l.Direction, l.ClientID, l.Contract, l.ISIN, l.Qty = "out", clientID, contract, isin, qty
|
l.Direction, l.ClientID, l.Contract, l.ISIN, l.Qty = "out", clientID, contract, isin, qty
|
||||||
})
|
})
|
||||||
@@ -524,7 +524,7 @@ func QuikOutRegister(ctx context.Context, rc *RuntimeConfig, guid, clientID, con
|
|||||||
|
|
||||||
// QuikOutSign — НРД подтвердил перевод: подписать поручение → исполнено, бумаги списаны.
|
// QuikOutSign — НРД подтвердил перевод: подписать поручение → исполнено, бумаги списаны.
|
||||||
func QuikOutSign(ctx context.Context, rc *RuntimeConfig, guid, isin, comment string) (*QuikLink, error) {
|
func QuikOutSign(ctx context.Context, rc *RuntimeConfig, guid, isin, comment string) (*QuikLink, error) {
|
||||||
s := rc.Snapshot().Quik
|
s := quikSettingsFor(rc, guid)
|
||||||
l := globalQuikLinks.Get(guid, isin)
|
l := globalQuikLinks.Get(guid, isin)
|
||||||
if l == nil || l.InstrNo == "" {
|
if l == nil || l.InstrNo == "" {
|
||||||
return l, errors.New("нет зарегистрированного поручения МНП по этому переводу")
|
return l, errors.New("нет зарегистрированного поручения МНП по этому переводу")
|
||||||
@@ -558,7 +558,7 @@ func QuikOutSign(ctx context.Context, rc *RuntimeConfig, guid, isin, comment str
|
|||||||
|
|
||||||
// QuikOutDeny — перевод отклонён (контрагентом, НРД или оператором): снять поручение с причиной.
|
// QuikOutDeny — перевод отклонён (контрагентом, НРД или оператором): снять поручение с причиной.
|
||||||
func QuikOutDeny(ctx context.Context, rc *RuntimeConfig, guid, isin, reason string) (*QuikLink, error) {
|
func QuikOutDeny(ctx context.Context, rc *RuntimeConfig, guid, isin, reason string) (*QuikLink, error) {
|
||||||
s := rc.Snapshot().Quik
|
s := quikSettingsFor(rc, guid)
|
||||||
l := globalQuikLinks.Get(guid, isin)
|
l := globalQuikLinks.Get(guid, isin)
|
||||||
if l == nil || l.InstrNo == "" {
|
if l == nil || l.InstrNo == "" {
|
||||||
return l, errors.New("нет зарегистрированного поручения МНП по этому переводу")
|
return l, errors.New("нет зарегистрированного поручения МНП по этому переводу")
|
||||||
|
|||||||
@@ -44,8 +44,11 @@ func quikSecISIN(s m2m.RequestSecurity) string {
|
|||||||
// отправку не блокируем (оповещаем); если QUIK отверг регистрацию — отправку
|
// отправку не блокируем (оповещаем); если QUIK отверг регистрацию — отправку
|
||||||
// ОТМЕНЯЕМ (нельзя переводить то, что не заблокировано).
|
// ОТМЕНЯЕМ (нельзя переводить то, что не заблокировано).
|
||||||
func quikOutgoingBeforeSend(ctx context.Context, rc *RuntimeConfig, req *m2m.M2MTransferRequest) error {
|
func quikOutgoingBeforeSend(ctx context.Context, rc *RuntimeConfig, req *m2m.M2MTransferRequest) error {
|
||||||
q := rc.Snapshot().Quik
|
if req == nil {
|
||||||
if !quikActive(q) || !q.AutoOut || req == nil || manualQuikSkipped(ctx) { // ручной ввод: заблокировано сотрудником
|
return nil
|
||||||
|
}
|
||||||
|
q := quikSettingsFor(rc, string(req.Header.GUID)) // тест «боевой ЛК → TEST3» — боевая запись (realtest.go)
|
||||||
|
if !quikActive(q) || !q.AutoOut || manualQuikSkipped(ctx) { // ручной ввод: заблокировано сотрудником
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
guid := string(req.Header.GUID)
|
guid := string(req.Header.GUID)
|
||||||
@@ -88,11 +91,14 @@ func quikOutgoingBeforeSend(ctx context.Context, rc *RuntimeConfig, req *m2m.M2M
|
|||||||
|
|
||||||
// quikOnDecision — ответ контрагента: по отказанным бумагам снимаем поручения.
|
// quikOnDecision — ответ контрагента: по отказанным бумагам снимаем поручения.
|
||||||
func quikOnDecision(ctx context.Context, rc *RuntimeConfig, svc *Service, decision *m2m.M2MTransferDecision) {
|
func quikOnDecision(ctx context.Context, rc *RuntimeConfig, svc *Service, decision *m2m.M2MTransferDecision) {
|
||||||
q := rc.Snapshot().Quik
|
if decision == nil {
|
||||||
if !quikActive(q) || !q.AutoOut || decision == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guid := string(decision.Header.GUID)
|
guid := string(decision.Header.GUID)
|
||||||
|
q := quikSettingsFor(rc, guid)
|
||||||
|
if !quikActive(q) || !q.AutoOut {
|
||||||
|
return
|
||||||
|
}
|
||||||
links := globalQuikLinks.ForGUID(guid)
|
links := globalQuikLinks.ForGUID(guid)
|
||||||
if len(links) == 0 {
|
if len(links) == 0 {
|
||||||
return
|
return
|
||||||
@@ -119,7 +125,7 @@ func quikOnDecision(ctx context.Context, rc *RuntimeConfig, svc *Service, decisi
|
|||||||
|
|
||||||
// quikOnServiceError — сервис МОСТ отверг перевод целиком: снимаем все поручения.
|
// quikOnServiceError — сервис МОСТ отверг перевод целиком: снимаем все поручения.
|
||||||
func quikOnServiceError(ctx context.Context, rc *RuntimeConfig, guid, reason string) {
|
func quikOnServiceError(ctx context.Context, rc *RuntimeConfig, guid, reason string) {
|
||||||
q := rc.Snapshot().Quik
|
q := quikSettingsFor(rc, guid)
|
||||||
if !quikActive(q) || !q.AutoOut {
|
if !quikActive(q) || !q.AutoOut {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -148,13 +154,13 @@ func orderRespVerdict(resps []orderResponse) (done, rejected bool, text string)
|
|||||||
// quikCycleTick — один проход опросчика.
|
// quikCycleTick — один проход опросчика.
|
||||||
func quikCycleTick(ctx context.Context, rc *RuntimeConfig) {
|
func quikCycleTick(ctx context.Context, rc *RuntimeConfig) {
|
||||||
q := rc.Snapshot().Quik
|
q := rc.Snapshot().Quik
|
||||||
if !quikActive(q) || q.Server == "" {
|
if (!quikActive(q) && !realTestAny()) || q.Server == "" { // тестовые заявки «боевой ЛК → TEST3» — всегда
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// исходящие: наше поручение 16 принято НРД → подпись; отказ → снятие
|
// исходящие: наше поручение 16 принято НРД → подпись; отказ → снятие
|
||||||
if q.AutoOut && globalOutgoingStore != nil {
|
if q.AutoOut && globalOutgoingStore != nil {
|
||||||
for _, od := range scanForm16Drafts(globalOutgoingStore) {
|
for _, od := range scanForm16Drafts(globalOutgoingStore) {
|
||||||
if !od.Submitted {
|
if !od.Submitted || (!quikActive(q) && realTestGet(od.GUID) == nil) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
links := globalQuikLinks.ForGUID(od.GUID)
|
links := globalQuikLinks.ForGUID(od.GUID)
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package lkgateway
|
||||||
|
|
||||||
|
// Тест «боевой ЛК → тестовый контур НРД» (29.09.2026). Заявка реального клиента из ЛК ЕСИА Финанс
|
||||||
|
// проходит у нас всё по-настоящему: проверка шлюза и поручение в DEPO, блокировка и списание в QUIK
|
||||||
|
// МНП (боевая запись — только для таких заявок, остальные остаются в имитации), статусы в ЛК. В НРД
|
||||||
|
// (TEST3) запрос уходит с подменой блока инвестора на тест-клиента, зарегистрированного в тестовом
|
||||||
|
// контуре (Бубликов): GUID, референсы, бумаги и счета — настоящие, поэтому все ответы НРД по GUID
|
||||||
|
// применяются к заявке клиента. «Исполнена» в ЛК — только по отчёту НРД об исполнении (MS010).
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
||||||
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RealNSDTestSettings — кто идёт в НРД с подменой и под чьими данными.
|
||||||
|
type RealNSDTestSettings struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
// Docs — паспорта клиентов (серия номер), чьи заявки идут в НРД с подменой, через «;».
|
||||||
|
Docs string `json:"docs,omitempty"`
|
||||||
|
// As — тест-инвестор, зарегистрированный в тестовом контуре НРД. Пусто — Бубликов (К1).
|
||||||
|
AsLast string `json:"as_last,omitempty"`
|
||||||
|
AsFirst string `json:"as_first,omitempty"`
|
||||||
|
AsMiddle string `json:"as_middle,omitempty"`
|
||||||
|
AsSeries string `json:"as_series,omitempty"`
|
||||||
|
AsNumber string `json:"as_number,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s RealNSDTestSettings) as() (last, first, middle, ser, num string) {
|
||||||
|
if s.AsLast == "" || s.AsNumber == "" {
|
||||||
|
k := m2mTestClients["К1"]
|
||||||
|
return k.Last, k.First, k.Middle, k.DocSeries, k.DocNumber
|
||||||
|
}
|
||||||
|
return s.AsLast, s.AsFirst, s.AsMiddle, s.AsSeries, s.AsNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
func normDoc(ser, num string) string {
|
||||||
|
return strings.ReplaceAll(strings.TrimSpace(ser)+strings.TrimSpace(num), " ", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// realTestMatch — заявка этого клиента идёт в НРД с подменой.
|
||||||
|
func realTestMatch(rc *RuntimeConfig, ser, num string) bool {
|
||||||
|
if rc == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t := rc.Snapshot().LK.RealNSDTest
|
||||||
|
if !t.Enabled || normDoc(ser, num) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, d := range strings.Split(t.Docs, ";") {
|
||||||
|
f := strings.Fields(d)
|
||||||
|
if len(f) >= 1 && normDoc(strings.Join(f[:len(f)-1], ""), f[len(f)-1]) == normDoc(ser, num) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RealTestRec — заявка, ушедшая в НРД с подменой.
|
||||||
|
type RealTestRec struct {
|
||||||
|
GUID string `json:"guid"`
|
||||||
|
ClaimID int `json:"claim_id,omitempty"` // заявка ЛК
|
||||||
|
Client string `json:"client"` // ФИО и паспорт клиента
|
||||||
|
As string `json:"as"` // под чьими данными ушло в НРД
|
||||||
|
At time.Time `json:"at"`
|
||||||
|
LKDone bool `json:"lk_done,omitempty"` // итог передан в ЛК
|
||||||
|
}
|
||||||
|
|
||||||
|
var realTestFile = "/var/lib/bj/.bj/real-nsd-test.json"
|
||||||
|
|
||||||
|
var realTests = struct {
|
||||||
|
sync.Mutex
|
||||||
|
loaded bool
|
||||||
|
m map[string]*RealTestRec
|
||||||
|
}{}
|
||||||
|
|
||||||
|
func realTestLoad() {
|
||||||
|
if realTests.loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
realTests.loaded, realTests.m = true, map[string]*RealTestRec{}
|
||||||
|
if raw, err := os.ReadFile(realTestFile); err == nil {
|
||||||
|
_ = json.Unmarshal(raw, &realTests.m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func realTestSave() {
|
||||||
|
if raw, err := json.MarshalIndent(realTests.m, "", " "); err == nil {
|
||||||
|
_ = os.WriteFile(realTestFile, raw, 0o600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func realTestGet(guid string) *RealTestRec {
|
||||||
|
if guid == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
realTests.Lock()
|
||||||
|
defer realTests.Unlock()
|
||||||
|
realTestLoad()
|
||||||
|
if r, ok := realTests.m[strings.ToLower(guid)]; ok {
|
||||||
|
cp := *r
|
||||||
|
return &cp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func realTestPut(r RealTestRec) {
|
||||||
|
realTests.Lock()
|
||||||
|
defer realTests.Unlock()
|
||||||
|
realTestLoad()
|
||||||
|
r.GUID = strings.ToLower(r.GUID)
|
||||||
|
realTests.m[r.GUID] = &r
|
||||||
|
realTestSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
// realTestAny — есть тестовые заявки, по которым ещё ждём итог (опросчик QUIK должен работать).
|
||||||
|
func realTestAny() bool {
|
||||||
|
realTests.Lock()
|
||||||
|
defer realTests.Unlock()
|
||||||
|
realTestLoad()
|
||||||
|
for _, r := range realTests.m {
|
||||||
|
if !r.LKDone {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func realTestMarkLKDone(guid string) bool {
|
||||||
|
realTests.Lock()
|
||||||
|
defer realTests.Unlock()
|
||||||
|
realTestLoad()
|
||||||
|
r, ok := realTests.m[strings.ToLower(guid)]
|
||||||
|
if !ok || r.LKDone {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r.LKDone = true
|
||||||
|
realTestSave()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// substituteSender — отправитель в НРД с подменой блока инвестора; сделка у нас хранит исходный запрос.
|
||||||
|
type substituteSender struct {
|
||||||
|
real m2mcore.NSDSender
|
||||||
|
inv m2m.InvestorInformation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s substituteSender) Send(ctx context.Context, req *m2m.M2MTransferRequest) (*m2m.M2MTransferResponse, error) {
|
||||||
|
cp := *req
|
||||||
|
cp.Data.InvestorInformation = s.inv
|
||||||
|
return s.real.Send(ctx, &cp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s substituteSender) SendDecision(ctx context.Context, d *m2m.M2MTransferDecision) error {
|
||||||
|
return s.real.SendDecision(ctx, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// realTestInvestor — блок инвестора тест-клиента в формате запроса НРД; тип документа — как у клиента.
|
||||||
|
func realTestInvestor(rc *RuntimeConfig, docType m2m.IdentityDocumentCode) (m2m.InvestorInformation, string) {
|
||||||
|
last, first, middle, ser, num := rc.Snapshot().LK.RealNSDTest.as()
|
||||||
|
inv := m2m.InvestorInformation{LastName: last, FirstName: first, MiddleName: middle}
|
||||||
|
inv.IdentityDocument.DocumentType = docType
|
||||||
|
if ser != "" {
|
||||||
|
sv := m2m.IdentityDocSerial(ser)
|
||||||
|
inv.IdentityDocument.DocumentSeries = &sv
|
||||||
|
}
|
||||||
|
inv.IdentityDocument.DocumentNumber = m2m.IdentityDocSerial(num)
|
||||||
|
return inv, strings.TrimSpace(fmt.Sprintf("%s %s %s, %s %s", last, first, middle, ser, num))
|
||||||
|
}
|
||||||
|
|
||||||
|
// realTestSenderFor — отправитель для заявки: с подменой, если заявка из теста; иначе def.
|
||||||
|
func realTestSenderFor(rc *RuntimeConfig, guid string, req *m2m.M2MTransferRequest, def m2mcore.NSDSender) m2mcore.NSDSender {
|
||||||
|
if realTestGet(guid) == nil || req == nil || rc == nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
inv, _ := realTestInvestor(rc, req.Data.InvestorInformation.IdentityDocument.DocumentType)
|
||||||
|
return substituteSender{real: def, inv: inv}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quikSettingsFor — настройки МНП для перевода: у тестовых заявок «боевой ЛК → TEST3» запись в МНП
|
||||||
|
// настоящая (по решению 29.09.2026), у остальных — как в настройках.
|
||||||
|
func quikSettingsFor(rc *RuntimeConfig, guid string) QuikMNPSettings {
|
||||||
|
s := rc.Snapshot().Quik
|
||||||
|
if realTestGet(guid) != nil {
|
||||||
|
s.Emulate, s.WritesEnabled = false, true
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// realTestOnExecuted — НРД исполнил (или отверг) наше поручение 16: итог — в ЛК.
|
||||||
|
func realTestOnExecuted(ctx context.Context, rc *RuntimeConfig, guid string, rejected bool, partial string) {
|
||||||
|
if realTestGet(guid) == nil || !realTestMarkLKDone(guid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status, reason := "done", "Перевод исполнен: бумаги списаны"
|
||||||
|
switch {
|
||||||
|
case rejected && partial == "":
|
||||||
|
status, reason = "rejected", "НРД не исполнил поручение на списание"
|
||||||
|
case partial != "":
|
||||||
|
reason = "Перевод исполнен частично: " + partial
|
||||||
|
}
|
||||||
|
globalLKFlows.stepIfExists(guid, pick3(status == "done", "response", "reject"), "Итог НРД (MS010) → статус в ЛК", reason)
|
||||||
|
OnLKOutcome(ctx, rc, guid, status, reason)
|
||||||
|
log.Printf("real-test: %s — итог в ЛК: %s (%s)", shortGUID(guid), status, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerRealNSDTest — POST /admin/m2m-testing/real-nsd: включить/выключить тест и список клиентов.
|
||||||
|
func registerRealNSDTest(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||||
|
mux.HandleFunc("/admin/m2m-testing/real-nsd", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lk := rc.Snapshot().LK
|
||||||
|
lk.RealNSDTest.Enabled = r.FormValue("enabled") != ""
|
||||||
|
lk.RealNSDTest.Docs = strings.TrimSpace(r.FormValue("docs"))
|
||||||
|
if err := rc.UpdateLK(lk); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("real-test: %v, клиенты: %s", lk.RealNSDTest.Enabled, lk.RealNSDTest.Docs)
|
||||||
|
http.Redirect(w, r, "/admin/m2m-testing#real-nsd", http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package lkgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
||||||
|
)
|
||||||
|
|
||||||
|
type captureSender struct{ got *m2m.M2MTransferRequest }
|
||||||
|
|
||||||
|
func (c *captureSender) Send(_ context.Context, req *m2m.M2MTransferRequest) (*m2m.M2MTransferResponse, error) {
|
||||||
|
c.got = req
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (c *captureSender) SendDecision(context.Context, *m2m.M2MTransferDecision) error { return nil }
|
||||||
|
|
||||||
|
func TestRealNSDTest(t *testing.T) {
|
||||||
|
old := realTestFile
|
||||||
|
realTestFile = filepath.Join(t.TempDir(), "rt.json")
|
||||||
|
realTests.loaded = false
|
||||||
|
defer func() { realTestFile, realTests.loaded = old, false }()
|
||||||
|
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
lk := rc.Snapshot().LK
|
||||||
|
lk.RealNSDTest = RealNSDTestSettings{Enabled: true, Docs: "1110 754503; 4500 000001"}
|
||||||
|
_ = rc.UpdateLK(lk)
|
||||||
|
|
||||||
|
if !realTestMatch(rc, "1110", "754503") || !realTestMatch(rc, "11 10", "754503") || realTestMatch(rc, "1110", "000000") {
|
||||||
|
t.Fatal("выбор клиентов по паспорту")
|
||||||
|
}
|
||||||
|
|
||||||
|
// подмена: в НРД — Бубликов, у нас запрос не меняется
|
||||||
|
req := sendFailRequest()
|
||||||
|
req.Data.InvestorInformation.LastName = "Погонщикова"
|
||||||
|
inv, as := realTestInvestor(rc, req.Data.InvestorInformation.IdentityDocument.DocumentType)
|
||||||
|
cap := &captureSender{}
|
||||||
|
if _, err := (substituteSender{real: cap, inv: inv}).Send(context.Background(), req); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cap.got.Data.InvestorInformation.LastName != "Бубликов" || string(cap.got.Data.InvestorInformation.IdentityDocument.DocumentNumber) != "289090" ||
|
||||||
|
req.Data.InvestorInformation.LastName != "Погонщикова" || cap.got.Header.GUID != req.Header.GUID || as == "" {
|
||||||
|
t.Fatalf("подмена: ушло %+v, у нас %s", cap.got.Data.InvestorInformation, req.Data.InvestorInformation.LastName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QUIK: боевая запись только для тестовой заявки
|
||||||
|
q := rc.Snapshot().Quik
|
||||||
|
q.Emulate, q.WritesEnabled = true, false
|
||||||
|
_ = rc.UpdateQuik(q)
|
||||||
|
guid := string(req.Header.GUID)
|
||||||
|
if s := quikSettingsFor(rc, guid); !s.Emulate || s.WritesEnabled {
|
||||||
|
t.Fatal("обычная заявка — как в настройках")
|
||||||
|
}
|
||||||
|
realTestPut(RealTestRec{GUID: guid, Client: "Погонщикова", As: as})
|
||||||
|
if s := quikSettingsFor(rc, guid); s.Emulate || !s.WritesEnabled {
|
||||||
|
t.Fatal("тестовая заявка — боевая запись")
|
||||||
|
}
|
||||||
|
if _, ok := realTestSenderFor(rc, guid, req, cap).(substituteSender); !ok {
|
||||||
|
t.Fatal("повторная отправка тестовой заявки — тоже с подменой")
|
||||||
|
}
|
||||||
|
if !realTestAny() {
|
||||||
|
t.Fatal("есть незавершённая тестовая заявка")
|
||||||
|
}
|
||||||
|
realTestOnExecuted(context.Background(), rc, guid, false, "")
|
||||||
|
if r := realTestGet(guid); r == nil || !r.LKDone || realTestAny() {
|
||||||
|
t.Fatalf("итог в ЛК: %+v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,6 +274,10 @@ type LKSettings struct {
|
|||||||
// Строже, но мобильных клиентов будет выбрасывать при смене адреса оператором.
|
// Строже, но мобильных клиентов будет выбрасывать при смене адреса оператором.
|
||||||
BindNetwork bool `json:"lk_bind_network,omitempty"`
|
BindNetwork bool `json:"lk_bind_network,omitempty"`
|
||||||
|
|
||||||
|
// RealNSDTest — тест «боевой ЛК → тестовый контур НРД»: заявки указанных клиентов проходят всё
|
||||||
|
// по-настоящему (DEPO, QUIK МНП, статусы в ЛК), а в НРД уходят с данными тест-инвестора (realtest.go).
|
||||||
|
RealNSDTest RealNSDTestSettings `json:"real_nsd_test,omitempty"`
|
||||||
|
|
||||||
// Интеграция с ЛК (потребление back_office API платформы): два окружения.
|
// Интеграция с ЛК (потребление back_office API платформы): два окружения.
|
||||||
ActiveEnv string `json:"lk_active_env"` // "test" | "prod"
|
ActiveEnv string `json:"lk_active_env"` // "test" | "prod"
|
||||||
Test LKEndpoint `json:"lk_test"`
|
Test LKEndpoint `json:"lk_test"`
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//go:build live
|
||||||
|
|
||||||
|
package lkgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Разовое зачисление в МНП «Ввод ЦБ (M2M)» для подготовки теста. Запуск от bj:
|
||||||
|
// DEP_SER=1110 DEP_NUM=754503 DEP_ISIN=RU0009029540 DEP_QTY=100 DEP_GO=1
|
||||||
|
func TestQuikDepositLive(t *testing.T) {
|
||||||
|
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
globalRC = rc
|
||||||
|
ctx := context.Background()
|
||||||
|
code, contract, err := quikResolveClient(ctx, rc, os.Getenv("DEP_SER"), os.Getenv("DEP_NUM"))
|
||||||
|
fmt.Println("клиент QUIK:", code, contract, err)
|
||||||
|
if err != nil || os.Getenv("DEP_GO") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qty, _ := strconv.ParseFloat(os.Getenv("DEP_QTY"), 64)
|
||||||
|
s := rc.Snapshot().Quik
|
||||||
|
s.Emulate, s.WritesEnabled = false, true // только для этого вызова
|
||||||
|
aid, err := quikExecNewInstr(ctx, s, false, "BJ-SBER-TEST", code, os.Getenv("DEP_ISIN"), qty, "7722061076", false,
|
||||||
|
"bj: зачисление для теста M2M (боевой ЛК -> TEST3)")
|
||||||
|
fmt.Println("ActionID:", aid, err)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r := quikWaitAction(ctx, s, aid, 90*time.Second)
|
||||||
|
fmt.Printf("итог МНП: %+v\n", r)
|
||||||
|
}
|
||||||
@@ -173,10 +173,10 @@ func (s *Service) ResendClaim(ctx context.Context, id string) error {
|
|||||||
guid := string(d.Request.Header.GUID)
|
guid := string(d.Request.Header.GUID)
|
||||||
switch {
|
switch {
|
||||||
case d.State == m2mcore.StateValidated:
|
case d.State == m2mcore.StateValidated:
|
||||||
_, err := s.SendBuiltRequest(ctx, d.Request, d.InvestorID, d.SignedClaim)
|
_, err := s.SendBuiltRequestVia(ctx, realTestSenderFor(globalRC, guid, d.Request, s.sender), d.Request, d.InvestorID, d.SignedClaim)
|
||||||
return err
|
return err
|
||||||
case (d.State == m2mcore.StateAwaitingDecision || d.State == m2mcore.StateSubmittedToNSD) && sendFailGet(guid) != nil:
|
case (d.State == m2mcore.StateAwaitingDecision || d.State == m2mcore.StateSubmittedToNSD) && sendFailGet(guid) != nil:
|
||||||
if _, err := s.sender.Send(ctx, d.Request); err != nil {
|
if _, err := realTestSenderFor(globalRC, guid, d.Request, s.sender).Send(ctx, d.Request); err != nil {
|
||||||
s.noteSendFailure(ctx, d, d.Request, err)
|
s.noteSendFailure(ctx, d, d.Request, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,10 @@
|
|||||||
{{if .Flash}}<div class="card" style="background:var(--accent-weak,#e7f0fb)">{{.Flash}}</div>{{end}}
|
{{if .Flash}}<div class="card" style="background:var(--accent-weak,#e7f0fb)">{{.Flash}}</div>{{end}}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">Заявка <code>{{slice .Claim.ID 0 8}}</code> · <span class="badge {{.Claim.Status}}">{{ruState .Claim.Status}}</span>{{if and (or .Fail (eq .Claim.Status "validated")) (can "operate")}}<form method="post" action="/admin/claims/{{.Claim.ID}}/resend" style="margin:0 0 0 auto" onsubmit="this.querySelector('button').disabled=true;this.querySelector('button').textContent='Отправляем…';"><button type="submit" class="btn">Отправить повторно</button></form>{{end}}{{if and (or (eq .Claim.Status "draft") (eq .Claim.Status "validated")) (can "operate")}}<form method="post" action="/admin/claims/{{.Claim.ID}}/delete-draft" style="margin:0{{if not (or .Fail (eq .Claim.Status "validated"))}} 0 0 auto{{end}}" onsubmit="return confirm('Удалить заявку? Она ещё не уходила в НРД. Если по ней бумаги заблокированы в QUIK МНП, блокировка будет снята.');"><button type="submit" class="btn" style="background:var(--card);color:var(--err,#c0392b);border:1px solid var(--err,#c0392b)">{{if eq .Claim.Status "draft"}}Удалить черновик{{else}}Удалить{{end}}</button></form>{{end}}</h2>
|
<h2 style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">Заявка <code>{{slice .Claim.ID 0 8}}</code> · <span class="badge {{.Claim.Status}}">{{ruState .Claim.Status}}</span>{{if and (or .Fail (eq .Claim.Status "validated")) (can "operate")}}<form method="post" action="/admin/claims/{{.Claim.ID}}/resend" style="margin:0 0 0 auto" onsubmit="this.querySelector('button').disabled=true;this.querySelector('button').textContent='Отправляем…';"><button type="submit" class="btn">Отправить повторно</button></form>{{end}}{{if and (or (eq .Claim.Status "draft") (eq .Claim.Status "validated")) (can "operate")}}<form method="post" action="/admin/claims/{{.Claim.ID}}/delete-draft" style="margin:0{{if not (or .Fail (eq .Claim.Status "validated"))}} 0 0 auto{{end}}" onsubmit="return confirm('Удалить заявку? Она ещё не уходила в НРД. Если по ней бумаги заблокированы в QUIK МНП, блокировка будет снята.');"><button type="submit" class="btn" style="background:var(--card);color:var(--err,#c0392b);border:1px solid var(--err,#c0392b)">{{if eq .Claim.Status "draft"}}Удалить черновик{{else}}Удалить{{end}}</button></form>{{end}}</h2>
|
||||||
|
{{with realTest (print .Claim.M2MGUID)}}<div class="card" style="background:var(--warn-weak,#fbf3d9);margin:8px 0">
|
||||||
|
<b>Тест «боевой ЛК → тестовый контур НРД»</b>. У нас заявка идёт как клиент: <b>{{.Client}}</b> — DEPO, QUIK МНП (боевая запись), статусы в ЛК.
|
||||||
|
В НРД запрос отправлен с данными тест-инвестора <b>{{.As}}</b>; GUID, референсы, бумаги и счета — настоящие.{{if .LKDone}} Итог передан в ЛК.{{else}} «Исполнена» в ЛК — после отчёта НРД об исполнении (MS010).{{end}}
|
||||||
|
</div>{{end}}
|
||||||
{{with .Fail}}<div class="card" style="background:var(--err-weak,#fbe9e7);margin:8px 0">
|
{{with .Fail}}<div class="card" style="background:var(--err-weak,#fbe9e7);margin:8px 0">
|
||||||
<b>Перевод не доставлен в НРД</b> — {{sendFailText .}}.<br>
|
<b>Перевод не доставлен в НРД</b> — {{sendFailText .}}.<br>
|
||||||
<span class="muted">Попытка {{.At.Format "02.01.2006 15:04:05"}}{{if .PackageID}}, пакет ИШ {{.PackageID}}{{end}}{{if gt .Attempts 1}}, попыток: {{.Attempts}}{{end}}. Отправка в DEPO не отмечена, блокировка в QUIK МНП сохраняется. {{if .GaveUp}}Сутки прошли — автоповтор остановлен: отправьте вручную или удалите заявку.{{else}}Ничего нажимать не нужно: как только НРД снова ответит, bj сам отправит тот же запрос с тем же GUID (повтор — в течение суток). Кнопка «Отправить повторно» — если нужно попробовать прямо сейчас.{{end}}</span>
|
<span class="muted">Попытка {{.At.Format "02.01.2006 15:04:05"}}{{if .PackageID}}, пакет ИШ {{.PackageID}}{{end}}{{if gt .Attempts 1}}, попыток: {{.Attempts}}{{end}}. Отправка в DEPO не отмечена, блокировка в QUIK МНП сохраняется. {{if .GaveUp}}Сутки прошли — автоповтор остановлен: отправьте вручную или удалите заявку.{{else}}Ничего нажимать не нужно: как только НРД снова ответит, bj сам отправит тот же запрос с тем же GUID (повтор — в течение суток). Кнопка «Отправить повторно» — если нужно попробовать прямо сейчас.{{end}}</span>
|
||||||
|
|||||||
@@ -62,6 +62,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
<div class="card" id="real-nsd" style="margin:10px 0;{{if .RealNSD.Enabled}}background:var(--warn-weak,#fbf3d9){{end}}">
|
||||||
|
<h3 style="margin:0 0 6px;font-size:15px">Тест «боевой ЛК → тестовый контур НРД» {{if .RealNSD.Enabled}}<span class="badge awaiting_decision">включён</span>{{else}}<span class="badge draft">выключен</span>{{end}}</h3>
|
||||||
|
<p class="muted" style="margin:0 0 8px;font-size:13px">Заявки клиентов из списка (из ЛК ЕСИА Финанс) проходят у нас всё по-настоящему: проверка шлюза и поручение в DEPO, блокировка и списание в QUIK МНП (<b>боевая запись</b>), статусы в ЛК. В НРД (TEST3) запрос уходит с данными тест-инвестора <b>{{.RealNSDAs}}</b> — GUID, референсы, бумаги и счета настоящие, ответы НРД применяются к заявке клиента. «Исполнена» в ЛК — по отчёту НРД об исполнении (MS010). Остальные заявки ЛК — как раньше (без отправки в НРД).</p>
|
||||||
|
{{if can "settings"}}<form method="post" action="/admin/m2m-testing/real-nsd" class="cpf" onsubmit="return !this.enabled.checked || confirm('Включить? Для клиентов из списка QUIK МНП будет записывать по-настоящему (блокировка и списание бумаг).');">
|
||||||
|
<label style="flex-direction:row;align-items:center;gap:6px;font-size:13px;color:var(--text)"><input type="checkbox" name="enabled" style="min-width:0"{{if .RealNSD.Enabled}} checked{{end}}> включить</label>
|
||||||
|
<label style="min-width:280px">Паспорта клиентов (серия номер, через «;»)
|
||||||
|
<input name="docs" value="{{.RealNSD.Docs}}" placeholder="1110 754503"></label>
|
||||||
|
<button class="b-init" style="background:var(--card);border-color:var(--border);color:var(--text)">Сохранить</button>
|
||||||
|
</form>{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/admin/m2m-testing/counterparty" class="cpf" id="cpForm">
|
<form method="post" action="/admin/m2m-testing/counterparty" class="cpf" id="cpForm">
|
||||||
<label style="min-width:340px">Контрагент (из справочника M2M)
|
<label style="min-width:340px">Контрагент (из справочника M2M)
|
||||||
<input id="cp-search" list="cp-list" autocomplete="off" placeholder="начните вводить наименование или код…"
|
<input id="cp-search" list="cp-list" autocomplete="off" placeholder="начните вводить наименование или код…"
|
||||||
|
|||||||
Reference in New Issue
Block a user