package lkgateway // manual.go — ручной ввод ИСХОДЯЩЕГО перевода M2M по бумажному поручению клиента. // // Клиент приносит поручение на бумаге, сотрудник депозитария вводит его в bj так, как клиент // заполнил бы заявку в личном кабинете. Дальше перевод идёт тем же путём, что и заявка ЛК: // пакет M2MTransferRequest в НРД → запись в DEPO (sentHook) → блокировка в QUIK (quikSendHook, // если сотрудник не заблокировал вручную) → ответ контрагента → черновик формы 16 → поручение 16 // (вручную кнопкой или автоматическим режимом) → отчёт НРД MS010. // // Отправить нельзя, пока не заполнены реквизиты, не выполнена сверка с DEPO (если учётная система // подключена) и не проставлены обязательные отметки сверки с учётной и торговой системами. Каждая // отметка хранит, кто и когда её поставил. Поручения — в manual-orders.json. import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "log" "os" "regexp" "sort" "strconv" "strings" "sync" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore" ) const manualFile = "/var/lib/bj/.bj/manual-orders.json" // ManualSec — бумага в поручении. type ManualSec struct { ISIN string `json:"isin"` Code string `json:"code,omitempty"` // код НРД (необязательно; пусто — ISIN) Name string `json:"name,omitempty"` Qty string `json:"qty"` } // ManualMark — отметка сотрудника (кто и когда). type ManualMark struct { Done bool `json:"done"` By string `json:"by,omitempty"` At time.Time `json:"at,omitempty"` } // ManualDepoCheck — результат сверки с DEPO на момент Hash (данные поручения). type ManualDepoCheck struct { At time.Time `json:"at"` By string `json:"by,omitempty"` Hash string `json:"hash"` Ran bool `json:"ran"` Err string `json:"err,omitempty"` Recommend string `json:"recommend,omitempty"` Summary string `json:"summary,omitempty"` Investor string `json:"investor,omitempty"` Account string `json:"account,omitempty"` Secs []depoSecResult `json:"secs,omitempty"` } // ManualOrder — поручение, введённое вручную. type ManualOrder struct { ID int `json:"id"` CreatedAt time.Time `json:"created_at"` CreatedBy string `json:"created_by,omitempty"` UpdatedAt time.Time `json:"updated_at"` // Основание PaperNo string `json:"paper_no"` PaperDate string `json:"paper_date"` // ГГГГ-ММ-ДД ReceivedVia string `json:"received_via"` // Клиент LastName string `json:"last_name"` FirstName string `json:"first_name"` MiddleName string `json:"middle_name,omitempty"` BirthDate string `json:"birth_date"` // ГГГГ-ММ-ДД DocType string `json:"doc_type"` DocSeries string `json:"doc_series,omitempty"` DocNumber string `json:"doc_number"` DocIssueDate string `json:"doc_issue_date,omitempty"` DocIssuer string `json:"doc_issuer,omitempty"` Contract string `json:"contract,omitempty"` Account string `json:"account"` QuikCode string `json:"quik_code,omitempty"` Phone string `json:"phone,omitempty"` // Принимающая сторона RecvINN string `json:"recv_inn"` RecvCode string `json:"recv_code,omitempty"` RecvName string `json:"recv_name,omitempty"` RecvAccount string `json:"recv_account,omitempty"` // счёт клиента у принимающего (для сведения) SettlePlace string `json:"settle_place"` // nrd (СПБ Банк пока не используется) CostInfo bool `json:"cost_info"` Securities []ManualSec `json:"securities"` Comment string `json:"comment,omitempty"` // Сверка и отметки до отправки Depo *ManualDepoCheck `json:"depo,omitempty"` Checks map[string]ManualMark `json:"checks,omitempty"` QuikMode string `json:"quik_mode,omitempty"` // auto | manual | none QuikInstr string `json:"quik_instr,omitempty"` // Отправка Status string `json:"status"` // draft | sent | declined GUID string `json:"guid,omitempty"` DealID string `json:"deal_id,omitempty"` SentAt time.Time `json:"sent_at,omitempty"` SentBy string `json:"sent_by,omitempty"` SendErr string `json:"send_err,omitempty"` DeclineReason string `json:"decline_reason,omitempty"` DeclinedBy string `json:"declined_by,omitempty"` DeclinedAt time.Time `json:"declined_at,omitempty"` // Отметки после ответа Post map[string]ManualMark `json:"post,omitempty"` } // FullName — ФИО клиента. func (o *ManualOrder) FullName() string { return strings.TrimSpace(strings.Join([]string{o.LastName, o.FirstName, o.MiddleName}, " ")) } // Number — номер поручения в bj. func (o *ManualOrder) Number() string { return fmt.Sprintf("Р-%05d", o.ID) } // dataHash — отпечаток данных, от которых зависят сверка и отметки: изменили данные — сверку и // отметки нужно пройти заново. func (o *ManualOrder) dataHash() string { b, _ := json.Marshal([]any{o.LastName, o.FirstName, o.MiddleName, o.BirthDate, o.DocType, o.DocSeries, o.DocNumber, o.Account, o.RecvINN, o.SettlePlace, o.CostInfo, o.Securities}) sum := sha256.Sum256(b) return hex.EncodeToString(sum[:8]) } // manualCheckDef — обязательная отметка. type manualCheckDef struct{ Key, Group, Title, Hint string } // manualPreChecks — отметки перед отправкой (все обязательны). var manualPreChecks = []manualCheckDef{ {"paper", "Поручение клиента", "Поручение оформлено правильно", "подпись клиента или представителя, дата, реквизиты бумаг и принимающего депозитария; полномочия представителя (доверенность) проверены"}, {"client", "Учётная система депозитария", "Клиент идентифицирован", "ФИО, документ, удостоверяющий личность, и дата рождения совпадают с анкетой депонента"}, {"account", "Учётная система депозитария", "Счёт депо открыт, принадлежит клиенту и не является ИИС", "перевод с индивидуального инвестиционного счёта через M2M не выполняется"}, {"balance", "Учётная система депозитария", "Остаток достаточен, бумаги свободны", "на торговом разделе не меньше количества в поручении; нет обременений, ареста и блокировок"}, {"security", "Учётная система депозитария", "Бумаги допускаются к переводу M2M", "российские выпуски (ISIN начинается с RU), приняты на обслуживание"}, {"qual", "Учётная система депозитария", "Ограничения для квалифицированных инвесторов проверены", "если бумага только для квалифицированных инвесторов — статус клиента проверен, клиент предупреждён о возможном отказе"}, {"fees", "Учётная система депозитария", "Задолженности по оплате услуг нет", "или задолженность урегулирована по правилам депозитария"}, {"counterparty", "Принимающая сторона", "Принимающий депозитарий — участник M2M", "ИНН и наименование сверены со справочником участников НРД"}, } // manualPostDef — отметки после ответа: when — done (исполнено) | rejected (отказ); quik — только при // ручной блокировке в торговой системе. type manualPostDef struct { Key, When, Title string Quik bool } var manualPostChecks = []manualPostDef{ {"quik_debit", "done", "Списание в торговой системе проведено (лимит уменьшен окончательно)", true}, {"depo_done", "done", "Операция списания отражена в учётной системе депозитария", false}, {"client_done", "done", "Клиент уведомлён об исполнении", false}, {"paper_done", "done", "На поручении проставлена отметка об исполнении", false}, {"quik_unblock", "rejected", "Блокировка в торговой системе снята (лимит восстановлен)", true}, {"client_rejected", "rejected", "Клиент уведомлён об отказе и его причине", false}, {"paper_rejected", "rejected", "На поручении проставлена отметка об отказе", false}, } // manualStore — поручения ручного ввода (переживают перезапуск). type manualStore struct { mu sync.Mutex path string items map[int]*ManualOrder next int } var globalManual = loadManualStore(manualFile) func loadManualStore(path string) *manualStore { s := &manualStore{path: path, items: map[int]*ManualOrder{}, next: 1} if b, err := os.ReadFile(path); err == nil { var list []*ManualOrder if json.Unmarshal(b, &list) == nil { for _, o := range list { if o.Status == "sending" { // отправка прервана перезапуском o.Status, o.SendErr = "draft", "отправка прервана перезапуском сервиса — перед повтором проверьте журнал исходящих переводов" } s.items[o.ID] = o if o.ID >= s.next { s.next = o.ID + 1 } } } } return s } func (s *manualStore) saveLocked() error { list := make([]*ManualOrder, 0, len(s.items)) for _, o := range s.items { list = append(list, o) } sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID }) b, err := json.MarshalIndent(list, "", " ") if err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, b, 0o600); err != nil { return err } return os.Rename(tmp, s.path) } func (s *manualStore) Get(id int) (*ManualOrder, bool) { s.mu.Lock() defer s.mu.Unlock() o, ok := s.items[id] if !ok { return nil, false } b, _ := json.Marshal(o) // глубокая копия var cp ManualOrder _ = json.Unmarshal(b, &cp) return &cp, true } func (s *manualStore) List() []*ManualOrder { s.mu.Lock() ids := make([]int, 0, len(s.items)) for id := range s.items { ids = append(ids, id) } s.mu.Unlock() sort.Sort(sort.Reverse(sort.IntSlice(ids))) out := make([]*ManualOrder, 0, len(ids)) for _, id := range ids { if o, ok := s.Get(id); ok { out = append(out, o) } } return out } // Create — новое поручение (ID присваивается). func (s *manualStore) Create(o *ManualOrder) error { s.mu.Lock() defer s.mu.Unlock() o.ID = s.next s.next++ o.CreatedAt, o.UpdatedAt = time.Now(), time.Now() s.items[o.ID] = o return s.saveLocked() } // Update — изменение под замком: fn получает текущую запись. // Delete — удалить черновик. Отправленные и отклонённые поручения не удаляются: они след. func (s *manualStore) Delete(id int) error { s.mu.Lock() defer s.mu.Unlock() o, ok := s.items[id] if !ok { return fmt.Errorf("поручение не найдено") } if o.Status != "draft" { return fmt.Errorf("удалить можно только черновик, а поручение уже %s", manualStatusRu(o.Status)) } delete(s.items, id) return s.saveLocked() } func (s *manualStore) Update(id int, fn func(o *ManualOrder) error) error { s.mu.Lock() defer s.mu.Unlock() o, ok := s.items[id] if !ok { return errors.New("поручение не найдено") } b, _ := json.Marshal(o) var cp ManualOrder _ = json.Unmarshal(b, &cp) if err := fn(&cp); err != nil { return err } cp.UpdatedAt = time.Now() s.items[id] = &cp return s.saveLocked() } var ( manualINNRe = regexp.MustCompile(`^\d{10}$`) manualISINRe = regexp.MustCompile(`^[A-Z]{2}[A-Z0-9]{9}\d$`) manualDateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) ) // manualQty — количество: целое или дробное (паи), > 0. func manualQty(s string) (Quantity, bool) { s = strings.ReplaceAll(strings.TrimSpace(s), ",", ".") f, err := strconv.ParseFloat(s, 64) if err != nil || f <= 0 { return Quantity{}, false } if f == float64(uint64(f)) { return Quantity{Whole: uint64(f)}, true } return Quantity{Fractional: strconv.FormatFloat(f, 'f', -1, 64)}, true } // manualDataProblems — что не заполнено или заполнено неверно. func manualDataProblems(o *ManualOrder) []string { var p []string need := func(v, what string) { if strings.TrimSpace(v) == "" { p = append(p, "не заполнено: "+what) } } // Номер поручению присваивает учётная система при регистрации — здесь он не нужен. if !manualDateRe.MatchString(o.PaperDate) { p = append(p, "не заполнена дата поручения клиента") } need(o.LastName, "фамилия клиента") need(o.FirstName, "имя клиента") if !manualDateRe.MatchString(o.BirthDate) { p = append(p, "не заполнена дата рождения клиента") } need(o.DocType, "вид документа") need(o.DocNumber, "номер документа") if o.DocType == "21" && len(strings.ReplaceAll(o.DocSeries, " ", "")) != 4 { p = append(p, "серия паспорта РФ — 4 цифры") } need(o.Account, "счёт депо клиента") if !manualINNRe.MatchString(strings.TrimSpace(o.RecvINN)) { p = append(p, "ИНН принимающего депозитария — 10 цифр") } switch { case len(o.Securities) == 0: p = append(p, "не указана бумага") case len(o.Securities) > 1: p = append(p, "в одном поручении — одна бумага: на каждую бумагу оформите отдельное поручение") } for i, s := range o.Securities { isin := strings.ToUpper(strings.TrimSpace(s.ISIN)) switch { case !manualISINRe.MatchString(isin): p = append(p, fmt.Sprintf("бумага %d: неверный ISIN «%s»", i+1, s.ISIN)) case !isRussianISIN(isin): p = append(p, fmt.Sprintf("бумага %d: %s — иностранный выпуск, через M2M не переводится", i+1, isin)) } if _, ok := manualQty(s.Qty); !ok { p = append(p, fmt.Sprintf("бумага %d: количество должно быть больше нуля", i+1)) } } return p } // manualDepoConfigured — подключена ли учётная система (сверка обязательна). func manualDepoConfigured(rc *RuntimeConfig) bool { return rc != nil && strings.TrimSpace(rc.Snapshot().Fancy.Server) != "" } // manualQuikAutoAvailable — может ли bj сам заблокировать бумаги в торговой системе при отправке. func manualQuikAutoAvailable(rc *RuntimeConfig) bool { if rc == nil { return false } q := rc.Snapshot().Quik return quikActive(q) && q.AutoOut } // manualSendBlockers — почему поручение пока нельзя отправить (пусто — можно). func manualSendBlockers(rc *RuntimeConfig, o *ManualOrder) []string { if o.Status != "draft" { return []string{"поручение уже " + manualStatusRu(o.Status)} } b := manualDataProblems(o) if manualDepoConfigured(rc) { switch { case o.Depo == nil: b = append(b, "не выполнена сверка с учётной системой (DEPO)") case o.Depo.Hash != o.dataHash(): b = append(b, "данные изменены после сверки с DEPO — выполните сверку заново") case !o.Depo.Ran: b = append(b, "сверка с DEPO не выполнена: "+o.Depo.Err) case o.Depo.Recommend != "confirm": b = append(b, "сверка с DEPO не пройдена: "+o.Depo.Summary) } } // Ручных отметок перед отправкой нет: кто создал поручение — видно по учётной записи, // а клиент, счёт, остаток и бумага проверены сверкой с учётной системой при сохранении. switch o.QuikMode { case "auto": if !manualQuikAutoAvailable(rc) { b = append(b, "автоматическая блокировка в торговой системе не включена — заблокируйте бумаги вручную") } case "manual": if strings.TrimSpace(o.QuikInstr) == "" { b = append(b, "укажите номер поручения на блокировку в торговой системе") } case "none", "": // модуль неторговых поручений QUIK не используется — блокировка не требуется } return b } func manualStatusRu(s string) string { switch s { case "draft": return "черновик" case "sent": return "отправлено в НРД" case "sending": return "отправляется" case "declined": return "отклонено депозитарием" } return s } // manualClaim — заявка в том же виде, что из личного кабинета. func manualClaim(rc *RuntimeConfig, o *ManualOrder) (CreateClaimRequest, error) { nsd := rc.Snapshot().NSD var secs []ClaimSec for _, s := range o.Securities { isin := strings.ToUpper(strings.TrimSpace(s.ISIN)) q, ok := manualQty(s.Qty) if !ok { return CreateClaimRequest{}, fmt.Errorf("количество по %s", isin) } secs = append(secs, ClaimSec{ SecurityCode: pick(strings.TrimSpace(s.Code), isin), SecurityDetails: SecurityDetails{ISIN: isin}, Quantity: q, SettlementAccounts: []SettlementAccount{{ SettlementRequisitesINN: transferringINN(rc.Snapshot()), SettlementLocation: SettlementLocation{DeponentCode: nsd.DeponentCode, AccountID: nsd.AccountID, SectionID: nsd.SectionID}, }}, }) } cost := CostInfo{No: &struct{}{}} if o.CostInfo { cost = CostInfo{Yes: &CostInfoYes{Code: nsd.DeponentCode}} } ser := strings.ReplaceAll(strings.TrimSpace(o.DocSeries), " ", "") return CreateClaimRequest{ Investor: Investor{ ID: lkInvestorUUID("manual:" + ser + strings.TrimSpace(o.DocNumber)), LastName: strings.TrimSpace(o.LastName), FirstName: strings.TrimSpace(o.FirstName), MiddleName: strings.TrimSpace(o.MiddleName), Document: Document{DocumentType: o.DocType, Series: ser, Number: strings.TrimSpace(o.DocNumber)}, BirthDate: o.BirthDate, }, TransferringDepositoryINN: transferringINN(rc.Snapshot()), ReceivingDepositoryINN: strings.TrimSpace(o.RecvINN), CostInfo: cost, Securities: secs, SignatureFormat: "PAPER", ClientAccount: strings.ToUpper(strings.TrimSpace(o.Account)), }, nil } // manualDepoRun — сверка поручения с DEPO теми же проверками, что и заявка ЛК. func manualDepoRun(rc *RuntimeConfig, o *ManualOrder) *ManualDepoCheck { nsd := rc.Snapshot().NSD var secs []depoOutSec for i, s := range o.Securities { secs = append(secs, depoOutSec{ReferenceID: strconv.Itoa(i + 1), ISIN: strings.ToUpper(strings.TrimSpace(s.ISIN)), Code: s.Code, Qty: s.Qty}) } res := runDepoCheckOutAccount(o.LastName, o.FirstName, o.MiddleName, strings.ReplaceAll(o.DocSeries, " ", ""), strings.TrimSpace(o.DocNumber), o.BirthDate, secs, nsd.AccountID, nsd.SectionID, strings.ToUpper(strings.TrimSpace(o.Account))) return &ManualDepoCheck{At: time.Now(), Hash: o.dataHash(), Ran: res.Ran, Err: res.Err, Recommend: res.Recommend, Summary: res.Summary, Investor: res.Investor, Account: res.Account, Secs: res.Secs} } // manualDepoClient — клиент по документу из DEPO: ФИО, дата рождения, открытый торговый счёт. type manualDepoClient struct { Found bool Name, Birth, Account string Err string } func manualDepoLookup(rc *RuntimeConfig, ser, num string) manualDepoClient { var out manualDepoClient if !manualDepoConfigured(rc) { out.Err = "учётная система (DEPO) не подключена" return out } fs := rc.Snapshot().Fancy db, ctx, cancel, errS := depoOpen(fs) if errS != "" { out.Err = errS return out } defer cancel() defer db.Close() p := depoResolveParty(ctx, db, fs, strings.ReplaceAll(ser, " ", ""), strings.TrimSpace(num), "") if !p.faceOK { out.Err = "клиент с таким документом в DEPO не найден" return out } out.Found, out.Name, out.Account = true, p.faceName, p.accOpen if p.birth.Valid { out.Birth = p.birth.Time.Format("2006-01-02") } return out } // manualDepoSecNames — наименования бумаг из DEPO по ISIN (не найденные — без записи). func manualDepoSecNames(rc *RuntimeConfig, isins []string) map[string]string { out := map[string]string{} if !manualDepoConfigured(rc) || len(isins) == 0 { return out } fs := rc.Snapshot().Fancy db, ctx, cancel, errS := depoOpen(fs) if errS != "" { return out } defer cancel() defer db.Close() for _, isin := range isins { if _, name, found, _ := depoSecurity(ctx, db, isin); found { out[isin] = name } } return out } // manualQuikSkipKey — в контексте отправки: бумаги в торговой системе заблокированы вручную // (или торговая система не используется) — хук QUIK не вызывается. type manualQuikSkipKey struct{} func manualQuikSkipped(ctx context.Context) bool { v, _ := ctx.Value(manualQuikSkipKey{}).(bool) return v } // manualSend — отправка поручения в НРД тем же путём, что и заявка из личного кабинета. func manualSend(ctx context.Context, rc *RuntimeConfig, svc *Service, o *ManualOrder) (guid, dealID string, err error) { claim, err := manualClaim(rc, o) if err != nil { return "", "", err } if ss, ok := svc.store.(*SeedStore); ok { nsd := rc.Snapshot().NSD ss.EnsureLKClient(LKUser{ Login: "manual:" + o.Number(), Contract: claim.Investor.ID, LastName: claim.Investor.LastName, FirstName: claim.Investor.FirstName, MiddleName: claim.Investor.MiddleName, DocType: claim.Investor.Document.DocumentType, DocSeries: claim.Investor.Document.Series, DocNumber: claim.Investor.Document.Number, Active: true, }, nil, m2mcore.DepoAccount{ DeponentCode: nsd.DeponentCode, AccountID: m2m.AccountID(nsd.AccountID), SectionID: nsd.SectionID, DepositoryINN: m2m.OrganizationINN(settlementPlaceINN(o.SettlePlace)), }) } req, err := svc.BuildRequest(ctx, claim) if err != nil { return "", "", fmt.Errorf("пакет не прошёл проверку формата НРД: %w", err) } // Выбор отправителя по режиму эмуляции НРД (как в ЛК-потоке, lkemulate.go): // imitate — внутренний имитатор, в НРД ничего не уходит; // substitute — реальная отправка в НРД; клиент из списка подмены идёт с заменой блока // инвестора на тест-инвестора, остальные — с настоящими данными; // off — реальная отправка в НРД с настоящими данными. sender := svc.sender switch nsdEmuMode(rc) { case "imitate": if lkEmuSender != nil { sender = lkEmuSender } case "substitute": inv := claim.Investor switch { case svc.sender == nil: // шлюз НРД не настроен — реально отправить нельзя, падаем на имитатор if lkEmuSender != nil { sender = lkEmuSender } case realTestMatch(rc, inv.Document.Series, inv.Document.Number): asInv, asText := realTestInvestor(rc, req.Data.InvestorInformation.IdentityDocument.DocumentType) realTestPut(RealTestRec{GUID: string(req.Header.GUID), 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} log.Printf("lk-gateway: ручной ввод %s: подмена личности — в НРД (%s) как %s", o.Number(), rc.Snapshot().NSD.Profile, asText) default: log.Printf("lk-gateway: ручной ввод %s: в НРД (%s) с настоящими данными (вне списка подмены)", o.Number(), rc.Snapshot().NSD.Profile) } } if o.QuikMode != "auto" { ctx = context.WithValue(ctx, manualQuikSkipKey{}, true) } resp, err := svc.SendBuiltRequestVia(ctx, sender, req, claim.Investor.ID, nil) if err != nil { return string(req.Header.GUID), "", err } return string(req.Header.GUID), resp.ID, nil } // ---- ход исполнения (для карточки) ---- // manualStep — шаг исполнения: State — done | wait | todo | fail | skip. type manualStep struct { Title, Detail, State string At time.Time } // manualProgress — ход исполнения отправленного поручения и итог: outcome — "" (в работе) | // done | rejected. type manualProgress struct { Steps []manualStep Outcome string Claim *ClaimView Depo *OutgoingDeal Quik []*QuikLink } func manualProgressFor(ctx context.Context, svc *Service, o *ManualOrder) manualProgress { var pr manualProgress add := func(title, detail, state string, at time.Time) { pr.Steps = append(pr.Steps, manualStep{Title: title, Detail: detail, State: state, At: at}) } add("Поручение введено", o.CreatedBy, "done", o.CreatedAt) if o.Depo != nil { st := "done" if !o.Depo.Ran || o.Depo.Recommend != "confirm" { st = "fail" } add("Сверка с учётной системой", pick(o.Depo.Summary, o.Depo.Err), st, o.Depo.At) } if o.Status == "declined" { add("Отклонено депозитарием", o.DeclineReason, "fail", o.DeclinedAt) pr.Outcome = "rejected" return pr } if o.Status != "sent" { add("Отправка в НРД", "после сверки и отметок", "todo", time.Time{}) return pr } add("Пакет отправлен в НРД", "GUID "+o.GUID, "done", o.SentAt) if svc != nil && o.DealID != "" { if v, err := svc.GetClaim(ctx, o.DealID); err == nil { pr.Claim = &v } } if o.GUID != "" { pr.Quik = globalQuikLinks.ForGUID(o.GUID) if globalOutgoingStore != nil { if d, ok := scanForm16Deal(globalOutgoingStore, o.GUID); ok { pr.Depo = d } } } c := pr.Claim switch { case c == nil: add("Ответ сервиса MOEX МОСТ", "нет данных о переводе", "wait", time.Time{}) case c.M2MResponse != nil && c.M2MResponse.StatusCode == "ERROR": var codes []string for _, r := range c.M2MResponse.Responses { codes = append(codes, strings.TrimSpace(r.Code+" "+r.Text)) } add("Ответ сервиса MOEX МОСТ: ошибка", strings.Join(codes, "; "), "fail", c.UpdatedAt) pr.Outcome = "rejected" return pr case c.M2MResponse != nil: add("Пакет принят сервисом MOEX МОСТ", c.M2MResponse.StatusCode, "done", time.Time{}) default: add("Ответ сервиса MOEX МОСТ", "ждём квитанцию", "wait", time.Time{}) } if c != nil && c.M2MDecision != nil { rejected := false var codes []string for _, s := range c.M2MDecision.Securities { if s.Outcome == "rejected" { rejected = true codes = append(codes, s.RejectCodes...) } } if rejected { add("Принимающий депозитарий отказал", rejectCodesLabel(codes), "fail", time.Time{}) pr.Outcome = "rejected" return pr } add("Принимающий депозитарий согласился", "", "done", time.Time{}) } else if c != nil && (c.Status == "rejected" || c.Status == "timed_out") { add("Перевод не состоялся", russianState(c.Status), "fail", c.UpdatedAt) pr.Outcome = "rejected" return pr } else if pr.Depo == nil { add("Решение принимающего депозитария", "ждём ответ контрагента", "wait", time.Time{}) } d := pr.Depo switch { case d == nil: add("Черновик поручения 16 от НРД", "придёт после согласия контрагента", "todo", time.Time{}) add("Поручение 16 подано", "", "todo", time.Time{}) add("Отчёт НРД об исполнении (MS010)", "", "todo", time.Time{}) return pr case !d.Submitted: add("Черновик поручения 16 получен", "", "done", d.DraftAt) add("Поручение 16 подано", "подайте поручение кнопкой ниже (в автоматическом режиме bj подаёт сам)", "wait", time.Time{}) add("Отчёт НРД об исполнении (MS010)", "", "todo", time.Time{}) return pr } add("Черновик поручения 16 получен", "", "done", d.DraftAt) add("Поручение 16 подано", "№ "+strings.Join(d.ContrDIDs, ", "), "done", d.OrderAt) if d.Done { add("Отчёт НРД об исполнении (MS010)", "бумаги списаны", "done", time.Time{}) pr.Outcome = "done" } else { add("Отчёт НРД об исполнении (MS010)", "ждём встречное поручение контрагента и отчёт НРД", "wait", time.Time{}) } return pr } // manualDocTypes — виды документов (справочник НРД). var manualDocTypes = []struct{ Code, Title string }{ {"21", "Паспорт гражданина РФ"}, {"03", "Свидетельство о рождении"}, {"10", "Паспорт иностранного гражданина"}, {"12", "Вид на жительство в РФ"}, {"22", "Заграничный паспорт гражданина РФ"}, {"07", "Военный билет"}, {"91", "Иной документ"}, }