Отправка в НРД с подтверждением доставки: ERROR от ИШ — заявка «Не отправлена», без BJ_SENT; «Отправить повторно» тем же GUID; сторож связи ИШ↔НРД по журналу ИШ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package lkgateway
|
||||
|
||||
// Неудачные отправки перевода в НРД. ИШ принимает пакет сразу, а в НРД передаёт следом; если контур
|
||||
// НРД недоступен, пакет получает статус ERROR (igw.NotDeliveredError). Такая заявка остаётся
|
||||
// «Не отправлена»: BJ_SENT в DEPO не пишется, в карточке — причина и кнопка «Отправить повторно»
|
||||
// (тот же GUID, блокировка в QUIK МНП сохраняется).
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"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/igw"
|
||||
)
|
||||
|
||||
// SendFailure — последняя неудачная отправка заявки.
|
||||
type SendFailure struct {
|
||||
DealID string `json:"deal_id"`
|
||||
PackageID string `json:"package_id,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Error string `json:"error"`
|
||||
At time.Time `json:"at"`
|
||||
Attempts int `json:"attempts"`
|
||||
}
|
||||
|
||||
var sendFailFile = "/var/lib/bj/.bj/send-failures.json"
|
||||
|
||||
var sendFails = struct {
|
||||
sync.Mutex
|
||||
loaded bool
|
||||
m map[string]SendFailure // GUID → неудача
|
||||
}{}
|
||||
|
||||
func sendFailLoad() {
|
||||
if sendFails.loaded {
|
||||
return
|
||||
}
|
||||
sendFails.loaded = true
|
||||
sendFails.m = map[string]SendFailure{}
|
||||
if raw, err := os.ReadFile(sendFailFile); err == nil {
|
||||
_ = json.Unmarshal(raw, &sendFails.m)
|
||||
}
|
||||
}
|
||||
|
||||
func sendFailSave() {
|
||||
if raw, err := json.MarshalIndent(sendFails.m, "", " "); err == nil {
|
||||
_ = os.WriteFile(sendFailFile, raw, 0o600)
|
||||
}
|
||||
}
|
||||
|
||||
// sendFailGet — неудачная отправка по GUID (nil — не было или уже отправлено).
|
||||
func sendFailGet(guid string) *SendFailure {
|
||||
sendFails.Lock()
|
||||
defer sendFails.Unlock()
|
||||
sendFailLoad()
|
||||
if f, ok := sendFails.m[guid]; ok {
|
||||
return &f
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendFailNote(guid, dealID string, err error) SendFailure {
|
||||
sendFails.Lock()
|
||||
defer sendFails.Unlock()
|
||||
sendFailLoad()
|
||||
f := sendFails.m[guid]
|
||||
f.DealID, f.Error, f.At, f.Attempts = dealID, err.Error(), time.Now(), f.Attempts+1
|
||||
f.PackageID, f.Code = "", ""
|
||||
var nd *igw.NotDeliveredError
|
||||
if errors.As(err, &nd) {
|
||||
f.PackageID, f.Code = nd.PackageID, nd.Code
|
||||
}
|
||||
sendFails.m[guid] = f
|
||||
sendFailSave()
|
||||
return f
|
||||
}
|
||||
|
||||
func sendFailClear(guid string) {
|
||||
sendFails.Lock()
|
||||
defer sendFails.Unlock()
|
||||
sendFailLoad()
|
||||
if _, ok := sendFails.m[guid]; ok {
|
||||
delete(sendFails.m, guid)
|
||||
sendFailSave()
|
||||
}
|
||||
}
|
||||
|
||||
// noteSendFailure — записать неудачу, событие сделки и оповещение.
|
||||
func (s *Service) noteSendFailure(ctx context.Context, deal *m2mcore.Deal, req *m2m.M2MTransferRequest, err error) {
|
||||
guid := string(req.Header.GUID)
|
||||
f := sendFailNote(guid, deal.ID, err)
|
||||
_ = s.repo.AppendEvent(ctx, deal.ID, m2mcore.Event{Type: "send_failed", Payload: f, CreatedAt: time.Now().UTC(), Actor: "system"})
|
||||
log.Printf("lk-gateway: перевод %s (заявка %s) НЕ доставлен в НРД: %v", shortGUID(guid), shortID(deal.ID), err)
|
||||
inv := req.Data.InvestorInformation
|
||||
raiseSystemAlert(globalRC, "Перевод не доставлен в НРД",
|
||||
fmt.Sprintf("Перевод %s (%s %s): %v. Заявка осталась «Не отправлена», в DEPO отправка не отмечена. Когда НРД снова будет доступен — «Отправить повторно» в карточке заявки.",
|
||||
shortGUID(guid), inv.LastName, inv.FirstName, err))
|
||||
}
|
||||
|
||||
// unsentDeal — заявка с этим GUID, которая уже создавалась, но в НРД не ушла (повторная отправка
|
||||
// идёт по ней же, а не новой заявкой).
|
||||
func (s *Service) unsentDeal(ctx context.Context, guid m2m.UUID) *m2mcore.Deal {
|
||||
d, err := s.repo.GetByGUID(ctx, guid)
|
||||
if err != nil || d == nil || d.State != m2mcore.StateValidated || d.Request == nil {
|
||||
return nil
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// ResendClaim — «Отправить повторно»: тот же запрос (тот же GUID) ещё раз в НРД. Для заявки
|
||||
// «Не отправлена» — обычный путь (переход в «Ожидает решение», BJ_SENT в DEPO); для заявки, которую
|
||||
// старая версия bj отметила отправленной без подтверждения ИШ, — только сама отправка.
|
||||
func (s *Service) ResendClaim(ctx context.Context, id string) error {
|
||||
d, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Request == nil {
|
||||
return errors.New("у заявки нет сохранённого запроса")
|
||||
}
|
||||
guid := string(d.Request.Header.GUID)
|
||||
switch {
|
||||
case d.State == m2mcore.StateValidated:
|
||||
_, err := s.SendBuiltRequest(ctx, d.Request, d.InvestorID, d.SignedClaim)
|
||||
return err
|
||||
case (d.State == m2mcore.StateAwaitingDecision || d.State == m2mcore.StateSubmittedToNSD) && sendFailGet(guid) != nil:
|
||||
if _, err := s.sender.Send(ctx, d.Request); err != nil {
|
||||
s.noteSendFailure(ctx, d, d.Request, err)
|
||||
return err
|
||||
}
|
||||
sendFailClear(guid)
|
||||
_ = s.repo.AppendEvent(ctx, d.ID, m2mcore.Event{Type: "resent", CreatedAt: time.Now().UTC(), Actor: "operator"})
|
||||
return nil
|
||||
}
|
||||
return errors.New("повторно отправить можно только заявку, которая не дошла до НРД")
|
||||
}
|
||||
|
||||
// quikHasRegistered — по переводу в QUIK МНП уже есть действующая блокировка.
|
||||
func quikHasRegistered(guid string) bool {
|
||||
for _, l := range globalQuikLinks.ForGUID(guid) {
|
||||
if l.State == "registered" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// sendFailText — причина неудачи понятными словами.
|
||||
func sendFailText(f *SendFailure) string {
|
||||
switch {
|
||||
case f.Code == "ERRORXML_UPLOAD_FAILED":
|
||||
return "контур НРД не принял пакет (недоступен)"
|
||||
case f.Code != "":
|
||||
return "НРД не принял пакет: " + f.Code
|
||||
}
|
||||
return f.Error
|
||||
}
|
||||
Reference in New Issue
Block a user