Раздел «Ручной ввод M2M» по бумажным поручениям; тестовые тексты и имитации убраны из клиентского интерфейса

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-17 18:26:59 +03:00
parent 627dbca6af
commit e4de1ab4d8
38 changed files with 2014 additions and 132 deletions
+698
View File
@@ -0,0 +1,698 @@
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"
"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 | spb
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 получает текущую запись.
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)
}
}
need(o.PaperNo, "номер поручения клиента")
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 цифр")
}
if len(o.Securities) == 0 {
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)
}
}
for _, c := range manualPreChecks {
if !o.Checks[c.Key].Done {
b = append(b, "нет отметки: "+c.Title)
}
}
switch o.QuikMode {
case "auto":
if !manualQuikAutoAvailable(rc) {
b = append(b, "автоматическая блокировка в торговой системе не включена — заблокируйте бумаги вручную")
}
case "manual":
if strings.TrimSpace(o.QuikInstr) == "" {
b = append(b, "укажите номер поручения на блокировку в торговой системе")
}
case "none":
default:
b = append(b, "не выбрано, как заблокированы бумаги в торговой системе")
}
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)
}
sender := svc.sender
if emu, _ := lkEmulateNSD(rc); emu && lkEmuSender != nil {
sender = lkEmuSender
}
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", "Иной документ"},
}