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 }