// Package mock — заглушка NSDSender для локальных стендов без реального // Интеграционного шлюза НРД. Возвращает синтетические M2MTransferResponse // (синхронно, сразу) и эмитит M2MTransferDecision (асинхронно, через // настраиваемую задержку) для каждой отправленной заявки. // // Подходит для: // - сквозного дев-теста (ЛК → m2m-core → mock → callback в ЛК); // - демонстраций «увидеть как оно работает» без подключения к НРД; // - юнит-тестов компонентов выше уровня транспорта. package mock import ( "context" "errors" "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/nsdxml" ) // DecisionOutcome — что должен вернуть mock после задержки. type DecisionOutcome int const ( OutcomeConfirm DecisionOutcome = iota OutcomeReject OutcomeTimeout ) // Config — настройки mock-сендера. type Config struct { // DecisionDelay — задержка между Send и эмиссией Decision в канал. DecisionDelay time.Duration // DefaultOutcome — каким будет Decision, если не задан per-Request override. DefaultOutcome DecisionOutcome // RejectionCode — какой код отказа возвращать в OutcomeReject (6 chars max). RejectionCode string // SenderCode/ReceiverCode для эмитированной Decision Header (берётся из // исходного Request обменом местами). NSDSenderCode m2m.DeponentCode } // DefaultConfig — разумные дефолты: подтверждение через 3 секунды. func DefaultConfig() Config { return Config{ DecisionDelay: 3 * time.Second, DefaultOutcome: OutcomeConfirm, RejectionCode: "07", } } // Sender — mock реализация m2mcore.NSDSender. type Sender struct { cfg Config decisions chan *m2m.M2MTransferDecision outcomes sync.Map // map[m2m.UUID]DecisionOutcome — override per GUID mu sync.Mutex stats stats lifeCtx context.Context // независимый от HTTP-запроса контекст для эмиссии Decision lifeCancel context.CancelFunc } type stats struct { Sent uint64 Confirmed uint64 Rejected uint64 TimedOut uint64 } // NewSender создаёт mock с указанной конфигурацией. func NewSender(cfg Config) *Sender { ctx, cancel := context.WithCancel(context.Background()) return &Sender{ cfg: cfg, decisions: make(chan *m2m.M2MTransferDecision, 64), lifeCtx: ctx, lifeCancel: cancel, } } // Stop отменяет внутренний контекст mock'а, останавливая все запущенные // emit-горутины. func (s *Sender) Stop() { s.lifeCancel() } // SetOutcome задаёт исход для конкретной заявки по GUID (вызывается до Send). func (s *Sender) SetOutcome(guid m2m.UUID, out DecisionOutcome) { s.outcomes.Store(guid, out) } // Decisions — канал эмитированных от имени принимающей стороны Decision'ов. // Подписаться, считать в горутине, передавать в m2mcore.Deal.ReceiveDecision. func (s *Sender) Decisions() <-chan *m2m.M2MTransferDecision { return s.decisions } // Stats — текущие счётчики (для UI). func (s *Sender) Stats() (sent, confirmed, rejected, timedOut uint64) { s.mu.Lock() defer s.mu.Unlock() return s.stats.Sent, s.stats.Confirmed, s.stats.Rejected, s.stats.TimedOut } // Send имитирует отправку в НРД. Возвращает синтетический Response // с StatusCode=INFO. Через DecisionDelay в канал прилетает Decision. func (s *Sender) Send(ctx context.Context, req *m2m.M2MTransferRequest) (*m2m.M2MTransferResponse, error) { if req == nil { return nil, errors.New("nsdadapter/mock: Send: req=nil") } if err := req.Validate(); err != nil { return nil, err } s.mu.Lock() s.stats.Sent++ s.mu.Unlock() // Синтетический Response: принимаем заявку, по каждой ЦБ — INFO 01. resp := &m2m.M2MTransferResponse{ GUID: req.Header.GUID, StatusCode: m2m.StatusInfo, } for _, sec := range req.Data.TransferredSecurities.Securities { refID := sec.ReferenceID resp.Responses = append(resp.Responses, m2m.Response{ ReferenceID: &refID, Code: "01", Text: "Запрос на перевод принят НРД и направлен принимающей стороне (mock).", }) } // Эмитим Decision в отдельной горутине. Используем lifeCtx, чтобы // HTTP-таймауты вызывающего не прерывали эмиссию. outcome := s.cfg.DefaultOutcome if v, ok := s.outcomes.Load(req.Header.GUID); ok { outcome = v.(DecisionOutcome) } go s.emitDecision(s.lifeCtx, req, outcome) _ = ctx return resp, nil } // SendDecision имитирует отправку нашего Decision принимающей стороне (мы — реципиент). // Mock просто фиксирует статистику. func (s *Sender) SendDecision(_ context.Context, decision *m2m.M2MTransferDecision) error { if decision == nil { return errors.New("nsdadapter/mock: SendDecision: decision=nil") } if err := decision.Validate(); err != nil { return err } s.mu.Lock() s.stats.Sent++ s.mu.Unlock() return nil } func (s *Sender) emitDecision(ctx context.Context, req *m2m.M2MTransferRequest, outcome DecisionOutcome) { select { case <-ctx.Done(): return case <-time.After(s.cfg.DecisionDelay): } if outcome == OutcomeTimeout { s.mu.Lock() s.stats.TimedOut++ s.mu.Unlock() // При таймауте Decision не приходит вообще — это и эмулируем. return } decision := &m2m.M2MTransferDecision{ Header: m2m.DecisionHeader{ GUID: req.Header.GUID, CreationTimestamp: nsdxml.Now(), SenderCode: req.Header.ReceiverCode, ReceiverCode: req.Header.SenderCode, CostInfo: m2m.CostInfo{No: &m2m.CostInfoNo{}}, }, Data: m2m.DecisionData{ ReceivingDepository: req.Data.ReceivingDepository, }, } for _, sec := range req.Data.TransferredSecurities.Securities { ds := m2m.DecisionSecurity{ReferenceID: sec.ReferenceID} if outcome == OutcomeConfirm { ds.TransferDecision = m2m.DecisionTransfer{ Confirmation: &m2m.Confirmation{ SettlementAccount: sec.SettlementAccount[0], }, } } else { ds.TransferDecision = m2m.DecisionTransfer{ Rejection: &m2m.Rejection{ Codes: []string{s.cfg.RejectionCode}, }, } } decision.Data.Securities = append(decision.Data.Securities, ds) } s.mu.Lock() switch outcome { case OutcomeConfirm: s.stats.Confirmed++ case OutcomeReject: s.stats.Rejected++ } s.mu.Unlock() select { case <-ctx.Done(): case s.decisions <- decision: } } // Verify тип Sender удовлетворяет m2mcore.NSDSender. var _ m2mcore.NSDSender = (*Sender)(nil)