package lkgateway import ( "context" "errors" "fmt" "log" "net/http" "sort" "strings" "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" ) // Service — бизнес-логика lk-gateway: преобразует DTO в доменные сущности // m2mcore, оркестрирует FSM сделки, эмитит callback'и в ЛК. type Service struct { repo m2mcore.Repository sender m2mcore.NSDSender store m2mcore.FansyStore recorder *m2mcore.MemoryRecorder defaultSender m2m.DeponentCode defaultReceiver m2m.DeponentCode callbackURL string httpClient *http.Client mu sync.RWMutex claimToID map[string]string // claim public ID -> internal deal ID } // Config — параметры сервиса. type Config struct { Repository m2mcore.Repository Sender m2mcore.NSDSender Store m2mcore.FansyStore Recorder *m2mcore.MemoryRecorder DefaultSender m2m.DeponentCode DefaultReceiver m2m.DeponentCode CallbackURL string } // NewService собирает сервис. func NewService(cfg Config) *Service { if cfg.Recorder == nil { cfg.Recorder = m2mcore.NewMemoryRecorder() } return &Service{ repo: cfg.Repository, sender: cfg.Sender, store: cfg.Store, recorder: cfg.Recorder, defaultSender: cfg.DefaultSender, defaultReceiver: cfg.DefaultReceiver, callbackURL: cfg.CallbackURL, httpClient: &http.Client{Timeout: 5 * time.Second}, claimToID: make(map[string]string), } } // CreateClaim принимает DTO заявки, формирует M2MTransferRequest, // создаёт сделку и отправляет в НРД. // quikSendHook — вызывается перед отправкой M2MTR в НРД (регистрация «Вывод ЦБ (M2M)» в QUIK МНП). // Ошибка хука отменяет отправку. Назначается в server.go. var quikSendHook func(ctx context.Context, req *m2m.M2MTransferRequest) error // sentHook — вызывается после успешной отправки M2MTR (запись в трансферные таблицы DEPO). var sentHook func(req *m2m.M2MTransferRequest) func (s *Service) CreateClaim(ctx context.Context, in CreateClaimRequest) (CreateClaimResponse, error) { req, err := s.BuildRequest(ctx, in) if err != nil { return CreateClaimResponse{}, err } return s.SendBuiltRequest(ctx, req, in.Investor.ID, []byte(in.SignedDocument)) } // BuildRequest формирует обогащённый M2MTransferRequest из DTO (GUID, // ReferenceID, реквизиты) БЕЗ отправки и без создания заявки. Нужно, чтобы // сначала положить запрос в БД Fancy на проверку, а отправить позже. func (s *Service) BuildRequest(ctx context.Context, in CreateClaimRequest) (*m2m.M2MTransferRequest, error) { domainClaim, err := dtoToClaim(in) if err != nil { return nil, fmt.Errorf("lkgateway: dtoToClaim: %w", err) } receiver := s.defaultReceiver if in.ReceiverCodeOverride != "" { receiver = m2m.DeponentCode(in.ReceiverCodeOverride) } req, err := m2mcore.EnrichRequest(ctx, s.store, domainClaim, m2mcore.SenderReceiver{ SenderCode: s.defaultSender, ReceiverCode: receiver, }) if err != nil { return nil, fmt.Errorf("lkgateway: EnrichRequest: %w", err) } return req, nil } // BuildRequestUnchecked — BuildRequest без финальной проверки формата НРД: пакет нужен // для журнала и отказа через Fancy даже при некорректных данных заявки (lkemulate.go). func (s *Service) BuildRequestUnchecked(ctx context.Context, in CreateClaimRequest) (*m2m.M2MTransferRequest, error) { domainClaim, err := dtoToClaim(in) if err != nil { return nil, fmt.Errorf("lkgateway: dtoToClaim: %w", err) } receiver := s.defaultReceiver if in.ReceiverCodeOverride != "" { receiver = m2m.DeponentCode(in.ReceiverCodeOverride) } req, err := m2mcore.EnrichRequestUnchecked(ctx, s.store, domainClaim, m2mcore.SenderReceiver{ SenderCode: s.defaultSender, ReceiverCode: receiver, }) if err != nil { return nil, fmt.Errorf("lkgateway: EnrichRequest: %w", err) } return req, nil } // SendBuiltRequest создаёт заявку (Deal) по готовому запросу, отправляет его в // НРД и сохраняет ответ. Используется и обычным CreateClaim, и поллером Fancy // (когда «кран» одобрил запрос в БД). func (s *Service) SendBuiltRequest(ctx context.Context, req *m2m.M2MTransferRequest, investorID string, signedDoc []byte) (CreateClaimResponse, error) { return s.SendBuiltRequestVia(ctx, s.sender, req, investorID, signedDoc) } // SendBuiltRequestVia — SendBuiltRequest через указанный отправитель (имитатор НРД // для заявок ЛК в тестовом режиме, lkemulate.go). Хуки QUIK и DEPO срабатывают так же. func (s *Service) SendBuiltRequestVia(ctx context.Context, sender m2mcore.NSDSender, req *m2m.M2MTransferRequest, investorID string, signedDoc []byte) (CreateClaimResponse, error) { deal, err := m2mcore.NewDeal(req.Header.GUID, investorID, signedDoc) if err != nil { return CreateClaimResponse{}, fmt.Errorf("lkgateway: NewDeal: %w", err) } saved, err := s.repo.Create(ctx, deal) if err != nil { return CreateClaimResponse{}, fmt.Errorf("lkgateway: repo.Create: %w", err) } if err := saved.Validate(ctx, req); err != nil { return CreateClaimResponse{}, fmt.Errorf("lkgateway: deal.Validate: %w", err) } // QUIK МНП: блокировка бумаг клиента до отправки в НРД (если цикл включён). if quikSendHook != nil { if err := quikSendHook(ctx, req); err != nil { return CreateClaimResponse{}, fmt.Errorf("lkgateway: %w", err) } } resp, err := sender.Send(ctx, req) if err != nil { return CreateClaimResponse{}, fmt.Errorf("lkgateway: sender.Send: %w", err) } saved.Response = resp if err := saved.Submit(ctx); err != nil { return CreateClaimResponse{}, fmt.Errorf("lkgateway: deal.Submit: %w", err) } if err := s.repo.Update(ctx, saved); err != nil { return CreateClaimResponse{}, fmt.Errorf("lkgateway: repo.Update: %w", err) } s.recorder.IncDeal(saved.State) s.mu.Lock() s.claimToID[saved.ID] = saved.ID s.mu.Unlock() if sentHook != nil { go sentHook(req) } return CreateClaimResponse{ ID: saved.ID, Status: string(saved.State), CreatedAt: saved.CreatedAt, Success: true, }, nil } // GetClaim возвращает полную карточку заявки. func (s *Service) GetClaim(ctx context.Context, id string) (ClaimView, error) { deal, err := s.repo.GetByID(ctx, id) if err != nil { return ClaimView{}, err } return dealToView(deal), nil } // ClaimRawXML возвращает ТОЧНЫЕ байты (windows-1251) отправленного запроса и // ответа НРД по заявке — для скачивания и пересылки в поддержку M2MOST@nsd.ru. // kind: "request" (M2MTransferRequest) или "response" (M2MTransferResponse). func (s *Service) ClaimRawXML(ctx context.Context, id, kind string) ([]byte, error) { deal, err := s.repo.GetByID(ctx, id) if err != nil { return nil, err } switch kind { case "request": if deal.Request == nil { return nil, errors.New("lkgateway: у заявки нет отправленного запроса") } return nsdxml.Marshal(deal.Request) case "response": if len(deal.RawResponse) == 0 { return nil, errors.New("lkgateway: по заявке ещё нет ответа НРД") } return deal.RawResponse, nil case "decision": if deal.Decision == nil { return nil, errors.New("lkgateway: по заявке нет решения НРД (Decision)") } return nsdxml.Marshal(deal.Decision) default: return nil, fmt.Errorf("lkgateway: неизвестный тип XML %q", kind) } } // ListClaims возвращает страницу заявок. func (s *Service) ListClaims(ctx context.Context, filter m2mcore.Filter) (ClaimsPage, error) { if filter.Limit == 0 { filter.Limit = 50 } deals, err := s.repo.List(ctx, filter) if err != nil { return ClaimsPage{}, err } sort.Slice(deals, func(i, j int) bool { return deals[i].CreatedAt.After(deals[j].CreatedAt) }) items := make([]ClaimView, 0, len(deals)) for _, d := range deals { items = append(items, dealToView(d)) } return ClaimsPage{Items: items, Total: len(items), Limit: filter.Limit, Offset: filter.Offset}, nil } // ApplyDecision принимает Decision (из mock-NSDSender или реального адаптера), // обновляет соответствующую сделку и шлёт callback в ЛК. func (s *Service) ApplyDecision(ctx context.Context, decision *m2m.M2MTransferDecision) error { if decision == nil { return errors.New("lkgateway: ApplyDecision: decision=nil") } deal, err := s.repo.GetByGUID(ctx, decision.Header.GUID) if err != nil { return fmt.Errorf("lkgateway: GetByGUID: %w", err) } if err := deal.ReceiveDecision(ctx, decision); err != nil { return fmt.Errorf("lkgateway: ReceiveDecision: %w", err) } if err := s.repo.Update(ctx, deal); err != nil { return fmt.Errorf("lkgateway: repo.Update: %w", err) } s.recorder.IncDeal(deal.State) if s.callbackURL != "" { s.sendCallback(ctx, deal) } return nil } // ApplyServiceResponse применяет M2MTransferResponse (ответ сервиса МОСТ) к // сделке: сохраняет ответ, при ERROR переводит сделку в Rejected и шлёт // callback в ЛК. Сделку ищем по GUID ответа. func (s *Service) ApplyServiceResponse(ctx context.Context, resp *m2m.M2MTransferResponse, raw []byte) error { if resp == nil { return errors.New("lkgateway: ApplyServiceResponse: resp=nil") } deal, err := s.findDealForResponse(ctx, resp) if err != nil { return fmt.Errorf("lkgateway: поиск сделки для ответа: %w", err) } prev := deal.State if err := deal.ReceiveServiceResponse(ctx, resp, raw); err != nil { return fmt.Errorf("lkgateway: ReceiveServiceResponse: %w", err) } if err := s.repo.Update(ctx, deal); err != nil { return fmt.Errorf("lkgateway: repo.Update: %w", err) } // Состояние сменилось (ERROR → Rejected) — учитываем в метриках и шлём // callback. На INFO состояние не меняется — callback не нужен. if deal.State != prev { s.recorder.IncDeal(deal.State) if s.callbackURL != "" { s.sendCallback(ctx, deal) } } return nil } // zeroGUID — нулевой UUID, который НРД присылает в сервисных ошибках // (напр. M2M14), когда не идентифицирует исходный запрос. const zeroGUID = "00000000-0000-0000-0000-000000000000" // findDealForResponse сопоставляет ответ МОСТ со сделкой. Сначала по GUID; // если GUID нулевой/пустой или сделка по нему не найдена (реальное поведение // НРД при M2M14 — ответ без нашего GUID и без ReferenceID), применяем // эвристику: берём самую раннюю ожидающую решение заявку без ответа. Для // тестового сценария «одна заявка в полёте» это однозначно; при множестве // заявок in-flight такие сервисные ошибки в принципе неразличимы на стороне // НРД, и FIFO — наилучшее доступное приближение. func (s *Service) findDealForResponse(ctx context.Context, resp *m2m.M2MTransferResponse) (*m2mcore.Deal, error) { guid := string(resp.GUID) if guid != "" && guid != zeroGUID { deal, err := s.repo.GetByGUID(ctx, resp.GUID) if err == nil { return deal, nil } if !errors.Is(err, m2mcore.ErrNotFound) { return nil, err } } // Fallback: ответ без идентифицируемого GUID. st := m2mcore.StateAwaitingDecision deals, err := s.repo.List(ctx, m2mcore.Filter{State: &st, Limit: 200}) if err != nil { return nil, err } var cand *m2mcore.Deal for _, d := range deals { if d.Response != nil { continue // этой заявке ответ уже сопоставлен } if cand == nil || d.CreatedAt.Before(cand.CreatedAt) { cand = d } } if cand == nil { return nil, m2mcore.ErrNotFound } log.Printf("lkgateway: ответ МОСТ с GUID=%s сопоставлен по эвристике (FIFO) заявке id=%s status=%s", guid, cand.ID, resp.StatusCode) return cand, nil } // sendCallback отправляет PATCH в ЛК с обновлением статуса. func (s *Service) sendCallback(ctx context.Context, deal *m2mcore.Deal) { cb := callbackForDeal(deal) if err := postJSON(ctx, s.httpClient, s.callbackURL+"/api/v1/back_office/claims/"+deal.ID, "PATCH", cb); err != nil { log.Printf("lkgateway: callback в ЛК упал: %v", err) } } // Recorder возвращает экспонируемый Recorder для admin-страницы. func (s *Service) Recorder() *m2mcore.MemoryRecorder { return s.recorder } // Repo возвращает Repository (для админских проверок). func (s *Service) Repo() m2mcore.Repository { return s.repo } // Внутренние преобразования и хелперы. func dtoToClaim(in CreateClaimRequest) (m2mcore.ClaimInput, error) { out := m2mcore.ClaimInput{ InvestorClientID: in.Investor.ID, TransferringDepositoryINN: m2m.OrganizationINN(in.TransferringDepositoryINN), ReceivingDepositoryINN: m2m.OrganizationINN(in.ReceivingDepositoryINN), } // Переопределение документа инвестора (тест с роботом: серия = сценарий). if d := in.InvestorDocumentOverride; d != nil { out.InvestorDocument = &m2mcore.ClientDocument{ DocumentType: m2m.IdentityDocumentCode(d.DocumentType), Series: d.Series, Number: d.Number, } } // CostInfo if in.CostInfo.Yes != nil { out.CostInfo = m2m.CostInfo{Yes: &m2m.CostInfoYes{Code: m2m.DeponentCode(in.CostInfo.Yes.Code)}} } else { out.CostInfo = m2m.CostInfo{No: &m2m.CostInfoNo{}} } // IIA if in.IIAAgreement != nil { out.IIAAgreement = &m2m.IIAAgreementDetails{ AgreementType: m2m.IIAContractType(in.IIAAgreement.AgreementType), AgreementNumber: in.IIAAgreement.AgreementNumber, AgreementDate: in.IIAAgreement.AgreementDate, BrokerINN: m2m.OrganizationINN(in.IIAAgreement.BrokerINN), } } // Securities for _, sec := range in.Securities { ds, err := dtoSecurityDetails(sec.SecurityDetails) if err != nil { return m2mcore.ClaimInput{}, err } q, err := dtoQuantity(sec.Quantity) if err != nil { return m2mcore.ClaimInput{}, err } out.Securities = append(out.Securities, m2mcore.ClaimSecurityInput{ SecurityCode: m2m.SecurityCode(sec.SecurityCode), Details: ds, Quantity: q, }) } return out, nil } func dtoSecurityDetails(in SecurityDetails) (m2m.SecurityDetails, error) { if in.ISIN != "" { isin := m2m.ISIN(in.ISIN) return m2m.SecurityDetails{ISIN: &isin}, nil } if in.SecurityInfo != nil { si := &m2m.SecurityDescription{ SecurityClassification: m2m.SecurityClassification(in.SecurityInfo.Classification), SecurityCategory: m2m.SecurityCategory(in.SecurityInfo.Category), SecurityType: in.SecurityInfo.SecurityType, SecuritySeries: in.SecurityInfo.SecuritySeries, } if in.SecurityInfo.IdentificationDetails.RegNumber != "" { rn := in.SecurityInfo.IdentificationDetails.RegNumber si.IdentificationDetails = m2m.IdentificationDetails{RegNumber: &rn} } if in.SecurityInfo.IdentificationDetails.FundShares != nil { si.IdentificationDetails = m2m.IdentificationDetails{ FundShares: &m2m.FundShares{ RegNumber: in.SecurityInfo.IdentificationDetails.FundShares.RegNumber, Class: in.SecurityInfo.IdentificationDetails.FundShares.Class, }, } } return m2m.SecurityDetails{SecurityInfo: si}, nil } return m2m.SecurityDetails{}, errors.New("lkgateway: SecurityDetails — задайте isin или security_info") } func dtoQuantity(in Quantity) (m2m.Quantity, error) { if in.Whole > 0 { w := in.Whole return m2m.Quantity{Whole: &w}, nil } if in.Fractional != "" { f := m2m.Decimal16(in.Fractional) return m2m.Quantity{Fractional: &f}, nil } return m2m.Quantity{}, errors.New("lkgateway: Quantity — задайте whole или fractional") } func dealToView(d *m2mcore.Deal) ClaimView { out := ClaimView{ ID: d.ID, Status: string(d.State), CreatedAt: d.CreatedAt, UpdatedAt: d.UpdatedAt, M2MGUID: d.GUID, } // Идентификатор инвестора (UUID) — для выгрузки всех заявок клиента. out.Investor.ID = d.InvestorID for _, st := range d.Stages { out.Stages = append(out.Stages, StageView{ State: string(st.State), EnteredAt: st.EnteredAt, LeftAt: st.LeftAt, Reason: st.Reason, }) } if d.Request != nil { // Точный отправленный пакет (windows-1251 → UTF-8) для просмотра в карточке. if b, err := nsdxml.Marshal(d.Request); err == nil { out.RequestRawXML = string(nsdxml.DecodeWindows1251(b)) } out.TransferringDepositoryINN = string(d.Request.Data.TransferringDepository.INN) out.ReceivingDepositoryINN = string(d.Request.Data.ReceivingDepository.INN) ii := d.Request.Data.InvestorInformation out.Investor = Investor{ ID: d.InvestorID, LastName: ii.LastName, FirstName: ii.FirstName, MiddleName: ii.MiddleName, Document: Document{ DocumentType: string(ii.IdentityDocument.DocumentType), Number: string(ii.IdentityDocument.DocumentNumber), }, } if ii.IdentityDocument.DocumentSeries != nil { out.Investor.Document.Series = string(*ii.IdentityDocument.DocumentSeries) } if d.Request.Header.CostInfo.Yes != nil { out.CostInfo = CostInfo{Yes: &CostInfoYes{Code: string(d.Request.Header.CostInfo.Yes.Code)}} } else if d.Request.Header.CostInfo.No != nil { empty := struct{}{} out.CostInfo = CostInfo{No: &empty} } if d.Request.Header.IIAAgreementDetails != nil { out.IIAAgreement = &IIAAgreement{ AgreementType: string(d.Request.Header.IIAAgreementDetails.AgreementType), AgreementNumber: d.Request.Header.IIAAgreementDetails.AgreementNumber, AgreementDate: d.Request.Header.IIAAgreementDetails.AgreementDate, BrokerINN: string(d.Request.Header.IIAAgreementDetails.BrokerINN), } } // Ценные бумаги из отправленного запроса — для блока «ЦБ» в карточке // и колонки ЦБ в журнале (раньше не заполнялось → показывало 0). for _, sec := range d.Request.Data.TransferredSecurities.Securities { cs := ClaimSec{SecurityCode: string(sec.SecurityCode)} if sec.SecurityDetails.ISIN != nil { cs.SecurityDetails.ISIN = string(*sec.SecurityDetails.ISIN) } // Количество: целое (Whole) или дробное (Fractional, у паёв). Раньше // читали только Whole → паи показывались пустыми/«0». if sec.Quantity.Whole != nil { cs.Quantity.Whole = *sec.Quantity.Whole } else if sec.Quantity.Fractional != nil { cs.Quantity.Fractional = string(*sec.Quantity.Fractional) } for _, sa := range sec.SettlementAccount { cs.SettlementAccounts = append(cs.SettlementAccounts, SettlementAccount{ SettlementRequisitesINN: string(sa.SettlementRequisites.INN), SettlementLocation: SettlementLocation{ DeponentCode: sa.SettlementLocation.DeponentCode, AccountID: string(sa.SettlementLocation.AccountID), SectionID: sa.SettlementLocation.SectionID, }, }) } out.Securities = append(out.Securities, cs) } } if d.Response != nil { out.M2MResponse = responseToView(d.Response) if len(d.RawResponse) > 0 { // Ответ НРД в windows-1251 — декодируем в UTF-8 для показа. out.M2MResponse.RawXML = string(nsdxml.DecodeWindows1251(d.RawResponse)) } } if d.Decision != nil { out.M2MDecision = decisionToView(d.Decision) } if d.State != m2mcore.StateDraft { cb := callbackForDeal(d) out.LastCallback = &cb } // Самопроверка тестовой заявки роботу: серия документа = код сценария. out.TestResult = buildTestResult(out.Investor.Document.Series, out.M2MDecision, out.M2MResponse) return out } // buildTestResult формирует «ожидали/получили/вердикт» для тестовой заявки // роботу НРД. Сценарий определяется по серии документа инвестора (= код // сценария 1111/2001/2002/3333). Для нетестовых заявок возвращает nil. func buildTestResult(series string, dec *NSDDecisionView, resp *NSDResponseView) *TestResultView { expected := map[string][2]string{ "1111": {"Ответ с отказом", "M2MTransferDecision: все бумаги отклонены (Rejection) с кодом ошибки из номера документа"}, "2001": {"Принять все бумаги", "M2MTransferDecision: все бумаги приняты (Confirmation)"}, "2002": {"Принять бумаги частично", "M2MTransferDecision: часть бумаг принята, часть отклонена (M2M05)"}, "3333": {"Робот — принимающая сторона", "M2MTransferDecision: отказ M2M05 + отдельное сообщение — новый перевод от робота"}, } exp, ok := expected[series] if !ok { return nil // не тестовая заявка } tr := &TestResultView{Scenario: series, Title: exp[0], Expected: exp[1]} // Сначала смотрим решение (Decision) — целевой ответ для всех сценариев. if dec != nil && len(dec.Securities) > 0 { conf, rej := 0, 0 codes := map[string]bool{} for _, s := range dec.Securities { if s.Outcome == "confirmed" { conf++ } else { rej++ } for _, c := range s.RejectCodes { codes[c] = true } } codeList := make([]string, 0, len(codes)) for c := range codes { codeList = append(codeList, c) } sort.Strings(codeList) tr.Got = fmt.Sprintf("M2MTransferDecision: принято %d, отклонено %d", conf, rej) if len(codeList) > 0 { tr.Got += " (коды отказа: " + strings.Join(codeList, ", ") + ")" } // Оценка по сценарию. switch series { case "2001": tr.statusFromBool(conf > 0 && rej == 0, "все бумаги приняты — как и ожидалось", "ожидался приём всех бумаг") case "1111": tr.statusFromBool(rej > 0 && conf == 0, "все бумаги отклонены — как и ожидалось", "ожидался отказ по всем бумагам") case "2002": tr.statusFromBool(conf > 0 && rej > 0, "часть принята, часть отклонена — как и ожидалось", "ожидался частичный приём") case "3333": tr.statusFromBool(rej > 0, "робот отклонил перевод (M2M05) и должен прислать свой перевод отдельным сообщением", "ожидался отказ M2M05") } return tr } // Решения нет — но мог прийти ответ сервиса МОСТ (ошибка формата/бумаг). if resp != nil { var codes []string for _, e := range resp.Responses { if e.Code != "" { codes = append(codes, e.Code) } } tr.Got = "Ответ сервиса МОСТ: " + resp.StatusCode if len(codes) > 0 { tr.Got += " (" + strings.Join(codes, ", ") + ")" } tr.Status = "error" tr.Note = "решение не получено — сервис НРД вернул ошибку до приёмки (см. коды)" return tr } tr.Got = "ответ робота ещё не получен" tr.Status = "waiting" tr.Note = "ждём ответ робота (обычно 30–60 сек)" return tr } func responseToView(r *m2m.M2MTransferResponse) *NSDResponseView { v := &NSDResponseView{ GUID: string(r.GUID), StatusCode: string(r.StatusCode), } for _, e := range r.Responses { ent := NSDResponseEntry{Code: e.Code, Text: e.Text} if e.ReferenceID != nil { ent.ReferenceID = string(*e.ReferenceID) } v.Responses = append(v.Responses, ent) } return v } func decisionToView(d *m2m.M2MTransferDecision) *NSDDecisionView { v := &NSDDecisionView{GUID: string(d.Header.GUID)} for _, sec := range d.Data.Securities { entry := NSDDecisionSecurity{ReferenceID: string(sec.ReferenceID)} if sec.TransferDecision.Confirmation != nil { entry.Outcome = "confirmed" } else if sec.TransferDecision.Rejection != nil { entry.Outcome = "rejected" entry.RejectCodes = sec.TransferDecision.Rejection.Codes } v.Securities = append(v.Securities, entry) } return v } func callbackForDeal(d *m2mcore.Deal) StatusCallback { cb := StatusCallback{ ClaimID: d.ID, NewStatus: string(d.State), UpdatedAt: d.UpdatedAt, } if d.Decision != nil { cb.NSDResponse = nsdResponseFromDecision(d.Decision) if d.State == m2mcore.StateRejected { for _, sec := range d.Decision.Data.Securities { if sec.TransferDecision.Rejection != nil && len(sec.TransferDecision.Rejection.Codes) > 0 { cb.ReasonCode = sec.TransferDecision.Rejection.Codes[0] cb.ReasonText = "Отказ принимающей стороны (mock)" break } } } } else if d.Response != nil { cb.NSDResponse = responseToView(d.Response) } return cb } func nsdResponseFromDecision(d *m2m.M2MTransferDecision) *NSDResponseView { v := &NSDResponseView{GUID: string(d.Header.GUID), StatusCode: string(m2m.StatusInfo)} for _, sec := range d.Data.Securities { ref := string(sec.ReferenceID) ent := NSDResponseEntry{ReferenceID: ref} if sec.TransferDecision.Confirmation != nil { ent.Code = "01" ent.Text = "Подтверждение принимающей стороны." } else if sec.TransferDecision.Rejection != nil { ent.Code = "07" ent.Text = "Отказ принимающей стороны." } v.Responses = append(v.Responses, ent) } return v }