Files
Bridge-and-Join-s/internal/lkgateway/manual.go
T

702 lines
29 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (СПБ Банк пока не используется)
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 цифр")
}
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)
}
}
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", "Иной документ"},
}