Files
Bridge-and-Join-s/internal/lkgateway/orderwatch.go
T
fontvielle 32d88fc42a ЛК: итог «Исполнена» по MS010, GUID без учёта регистра, заявка при отказе МНП берётся один раз; тестовый контур не трогает боевой ЛК
Перевод 97EAD00C (заявка ЛК #71741, 23.10.2026) исполнен в QUIK и НРД (MS010),
но в ЛК статус не изменился, а сделка осталась «Подтверждена»:
- связки guid → заявка ЛК искались с учётом регистра: мы шлём GUID строчными,
  НРД отвечает заглавными — OnLKOutcome молча не находил заявку. Ключи —
  строчными (lkGUIDKey), старые записи приводятся при загрузке; итог модуля
  ЛК (lk.transfer_requests) — lower(guid);
- после MS010 по всем строкам поручения 16 сделка → «Исполнена» (CompleteSUB16),
  итог — в ЛК (outOnExecuted, каждый проход order-watch, повтор безвреден);
  в бою согласие контрагента — ещё не «Исполнена»: статус ждёт MS010, как в
  тесте «боевой ЛК → TEST3»; имитатор НРД — итог сразу.

Тестовый 28 с боевым ЛК взял ту же заявку и после отказа МНП («запрещен для
операции шорт» — бумагу уже заблокировал 9) брал её каждые 10 секунд: 108
сделок с новыми GUID и столько же писем.
- Сделка заведена, а отправка не прошла (МНП не заблокировал) — заявка
  помечается взятой и связывается с переводом: дальше — оператор в карточке.
- Боевой ЛК при тестовом профиле ИШ (lkProdOnTestNSD): заявки клиентов не
  берутся (кроме теста «боевой ЛК → тестовый НРД»), портфели не заполняются,
  предупреждение в настройках ЛК.
- Письма: в теме и первой строке — сервер bj и контур НРД («[bj ТЕСТ …]»
  у тестового); письма о входящих — метка только у тестового.
- История FSM — по МСК с датой, «текущее состояние» вместо «сейчас».
- Карточка теста «боевой ЛК → тестовый НРД» — только в режиме разработки
  (включённый тест виден всегда, чтобы его выключить).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-23 16:04:44 +03:00

628 lines
25 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
// Контроль сроков депо-поручений 16 / 16/1 (регламент депозитария, 09.09.2026):
// - отчёт MS010 ожидается в течение 5 минут после подачи; нет — оповещение
// «не исполнено в срок»;
// - далее исполнение ожидается до 30 дней; по истечении — поручение считается
// отменённым (оповещение, по исходящему снимается блокировка в QUIK).
// Состояние — /var/lib/bj/.bj/order-watch.json (чтобы оповещать один раз).
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
)
const (
orderWatchFile = "/var/lib/bj/.bj/order-watch.json"
orderMS010Within = 5 * time.Minute
orderExecWithin = 30 * 24 * time.Hour
)
type OrderWatch struct {
GUID string `json:"guid"`
Direction string `json:"direction"` // out (16) | in (16/1)
OrderAt time.Time `json:"order_at"`
MS010At time.Time `json:"ms010_at,omitempty"` // все строки получили MS010
Rejected bool `json:"rejected,omitempty"` // хотя бы одна строка отвергнута
LateAt time.Time `json:"late_at,omitempty"` // когда зафиксировали просрочку 5 мин
ExpiredAt time.Time `json:"expired_at,omitempty"` // все незакрытые строки истекли (30 дней)
// NoCounterAt — НРД прислал уведомление GS036 «об отсутствии встречного поручения»: контрагент
// ещё не подал своё поручение (к нашей 16 — его 16/1, к нашей 16/1 — его 16). Сбрасывается MS010.
NoCounterAt time.Time `json:"no_counter_at,omitempty"`
// MatchedAt — НРД сквитовал поручения со встречными (GS116, MACH) по всем строкам: дальше — исполнение.
MatchedAt time.Time `json:"matched_at,omitempty"`
// Lines — состояние по каждой строке пакета (одно депо-поручение = одна бумага),
// ключ — contr_d_id. По договорённости с Fancy (18.09.2026) исходы пишутся в DEPO
// по строкам с REFERENCE и сразу по MS010 этой строки, не дожидаясь остальных:
// пакет может исполниться частично (Сбер — да, Т-Технологии — отказ).
Lines map[string]*OrderLineWatch `json:"lines,omitempty"`
}
// OrderLineWatch — одна строка поручения: какая бумага (REFERENCE/ISIN) и её исход.
type OrderLineWatch struct {
Ref string `json:"ref,omitempty"` // REFERENCE строки в FTV_M2M_LINES
ISIN string `json:"isin,omitempty"`
MS010At time.Time `json:"ms010_at,omitempty"`
Rejected bool `json:"rejected,omitempty"`
ExpiredAt time.Time `json:"expired_at,omitempty"`
NoCounterAt time.Time `json:"no_counter_at,omitempty"` // GS036 по этой строке
MatchedAt time.Time `json:"matched_at,omitempty"` // GS116 MACH по этой строке
}
// counterName — чьё встречное поручение ждём: к нашей 16 — 16/1 принимающего, к нашей 16/1 — 16 передающего.
func (w *OrderWatch) counterName() string {
if w != nil && w.Direction == "in" {
return "поручение 16 передающего депозитария"
}
return "поручение 16/1 принимающего депозитария"
}
// orderLine — строка поручения для наблюдателя: contr_d_id ↔ бумага (REFERENCE, ISIN).
type orderLine struct {
ContrDID string
Ref string
ISIN string
}
// lineDone — строка в финальном состоянии (MS010 получен или срок истёк).
func (l *OrderLineWatch) lineDone() bool {
return l != nil && (!l.MS010At.IsZero() || !l.ExpiredAt.IsZero())
}
// linesSummary — сколько строк закрыто MS010 / отвергнуто / истекло из общего числа.
// Нужно для частичного исполнения: пакет может быть исполнен не целиком.
func (w *OrderWatch) linesSummary() (total, executed, rejected, expired int) {
if w == nil {
return
}
for _, l := range w.Lines {
total++
switch {
case !l.MS010At.IsZero() && l.Rejected:
rejected++
case !l.MS010At.IsZero():
executed++
case !l.ExpiredAt.IsZero():
expired++
}
}
return
}
// Состояние по-русски для карточек.
func (w *OrderWatch) StatusRu() string {
if w == nil {
return ""
}
// Частичное исполнение: часть строк закрыта, часть ещё ждёт / отвергнута / истекла.
if total, executed, rejected, expired := w.linesSummary(); total > 1 && executed > 0 && executed < total {
s := fmt.Sprintf("исполнено частично: %d из %d бумаг", executed, total)
if rejected > 0 {
s += fmt.Sprintf(", отвергнуто %d", rejected)
}
if expired > 0 {
s += fmt.Sprintf(", истёк срок по %d", expired)
}
if executed+rejected+expired < total {
s += fmt.Sprintf(", ожидаем MS010 по %d", total-executed-rejected-expired)
}
return s
}
switch {
case !w.MS010At.IsZero() && w.Rejected:
return "НРД отверг поручение (MS010 " + w.MS010At.Local().Format("02.01 15:04") + ")"
case !w.MS010At.IsZero():
late := ""
if w.MS010At.Sub(w.OrderAt) > orderMS010Within {
late = ", с опозданием " + humanDur(w.MS010At.Sub(w.OrderAt))
}
switch {
case !w.MatchedAt.IsZero() && w.MS010At.Sub(w.MatchedAt) > orderMS010Within:
late += "; сквитовано со встречным в " + w.MatchedAt.Local().Format("15:04") + ", исполнение НРД — через " + humanDur(w.MS010At.Sub(w.MatchedAt))
case w.MatchedAt.IsZero() && !w.NoCounterAt.IsZero():
late += "; встречное " + w.counterName() + " подано не сразу (уведомление НРД " + w.NoCounterAt.Local().Format("15:04") + ")"
}
return "исполнено: MS010 получен " + w.MS010At.Local().Format("02.01.2006 15:04") + late
case !w.ExpiredAt.IsZero():
return "СРОК 30 ДНЕЙ ИСТЁК " + w.ExpiredAt.Local().Format("02.01.2006") + " — поручение считается отменённым"
case !w.MatchedAt.IsZero():
return "поручение сквитовано со встречным (НРД " + w.MatchedAt.Local().Format("02.01 15:04") + "), ждём исполнения НРД (MS010)"
case !w.NoCounterAt.IsZero():
return "НРД: контрагент ещё не подал встречное " + w.counterName() + " (уведомление НРД " + w.NoCounterAt.Local().Format("02.01 15:04") + "); наше поручение принято, ждём исполнения"
case !w.LateAt.IsZero():
return "НЕ ИСПОЛНЕНО В СРОК 5 МИН (подано " + w.OrderAt.Local().Format("02.01 15:04") + "); ожидаем исполнения до " + w.OrderAt.Add(orderExecWithin).Local().Format("02.01.2006")
default:
return "ожидаем MS010 до " + w.OrderAt.Add(orderMS010Within).Local().Format("15:04")
}
}
func (w *OrderWatch) Kind() string {
if w != nil {
// частично исполнено — предупреждение, а не «отказ» и не «ок»
if total, executed, _, _ := w.linesSummary(); total > 1 && executed > 0 && executed < total {
return "warn"
}
}
switch {
case w == nil:
return ""
case !w.MS010At.IsZero() && w.Rejected:
return "bad"
case !w.MS010At.IsZero():
return "ok"
case !w.ExpiredAt.IsZero():
return "bad"
case !w.LateAt.IsZero():
return "warn"
}
return "wait"
}
type orderWatchStore struct {
mu sync.Mutex
items map[string]*OrderWatch
}
var globalOrderWatch = loadOrderWatch()
func loadOrderWatch() *orderWatchStore {
s := &orderWatchStore{items: map[string]*OrderWatch{}}
if b, err := os.ReadFile(orderWatchFile); err == nil {
var list []*OrderWatch
if json.Unmarshal(b, &list) == nil {
for _, w := range list {
s.items[w.GUID] = w
}
}
}
return s
}
func (s *orderWatchStore) saveLocked() {
list := make([]*OrderWatch, 0, len(s.items))
for _, w := range s.items {
list = append(list, w)
}
b, _ := json.MarshalIndent(list, "", " ")
_ = os.MkdirAll(filepath.Dir(orderWatchFile), 0o755)
tmp := orderWatchFile + ".tmp"
if os.WriteFile(tmp, b, 0o640) == nil {
_ = os.Rename(tmp, orderWatchFile)
}
}
// Get — состояние контроля по GUID (nil, если поручение не подавалось).
func (s *orderWatchStore) Get(guid string) *OrderWatch {
s.mu.Lock()
defer s.mu.Unlock()
if w, ok := s.items[guid]; ok {
cp := *w
return &cp
}
return nil
}
func (s *orderWatchStore) update(guid string, fn func(w *OrderWatch)) *OrderWatch {
s.mu.Lock()
defer s.mu.Unlock()
w, ok := s.items[guid]
if !ok {
w = &OrderWatch{GUID: guid}
s.items[guid] = w
}
fn(w)
s.saveLocked()
cp := *w
return &cp
}
var orderWatchStarted = time.Now()
// outgoingLineRefsCache — REFERENCE по ISIN для исходящей сделки: берётся из
// M2MTR-запроса в репозитории; после отправки не меняется, поэтому кэшируем по GUID.
var outgoingLineRefsCache = struct {
mu sync.Mutex
m map[string]map[string]string // guid → ISIN → ref
}{m: map[string]map[string]string{}}
// outgoingLineRefs — строки исходящего поручения 16: contr_d_id ↔ ISIN ↔ REFERENCE.
// Один черновик MF010 = одна бумага; REFERENCE берём из бумаг M2MTR по ISIN.
func outgoingLineRefs(ctx context.Context, svc *Service, od *OutgoingDeal) []orderLine {
if od == nil {
return nil
}
outgoingLineRefsCache.mu.Lock()
byISIN, ok := outgoingLineRefsCache.m[od.GUID]
outgoingLineRefsCache.mu.Unlock()
if !ok && svc != nil {
byISIN = map[string]string{}
if deals, err := svc.Repo().List(ctx, m2mcore.Filter{Limit: 500}); err == nil {
for _, d := range deals {
if d == nil || d.Request == nil || !strings.EqualFold(string(d.GUID), od.GUID) {
continue
}
for _, sec := range d.Request.Data.TransferredSecurities.Securities {
byISIN[strings.ToUpper(quikSecISIN(sec))] = string(sec.ReferenceID)
}
break
}
}
if len(byISIN) > 0 {
outgoingLineRefsCache.mu.Lock()
outgoingLineRefsCache.m[od.GUID] = byISIN
outgoingLineRefsCache.mu.Unlock()
}
}
var out []orderLine
for _, sec := range od.Secs {
if sec.ContrDID == "" {
continue
}
isin := strings.ToUpper(sec.ISIN)
out = append(out, orderLine{ContrDID: sec.ContrDID, Ref: byISIN[isin], ISIN: isin})
}
return out
}
// orderWatchCheck — один объект (перевод): подано когда, ответы НРД ПО КАЖДОЙ СТРОКЕ →
// фиксируем MS010/просрочку/истечение и пишем строчные события в DEPO сразу по
// MS010 этой бумаги, не дожидаясь остальных (частичное исполнение пакета).
func orderWatchCheck(ctx context.Context, rc *RuntimeConfig, guid, direction string, orderAt time.Time, lines []orderLine, label string) {
if guid == "" || orderAt.IsZero() || len(lines) == 0 {
return
}
w := globalOrderWatch.Get(guid)
if w == nil {
w = globalOrderWatch.update(guid, func(w *OrderWatch) { w.Direction, w.OrderAt = direction, orderAt })
}
if time.Since(orderAt) < 48*time.Hour { // GS036 ищем и после исполнения — карточка объясняет задержку
orderNoteNoCounter(guid, w, lines)
w = globalOrderWatch.Get(guid)
}
if !w.MS010At.IsZero() || !w.ExpiredAt.IsZero() {
return // финальное состояние уже зафиксировано по всем строкам
}
now := time.Now()
fresh := orderAt.After(orderWatchStarted) // по старым поручениям при первом запуске не шумим
// 1) исходы по строкам: у каждой бумаги свой MS010
for _, ln := range lines {
st := w.Lines[ln.ContrDID]
if st.lineDone() {
continue
}
resps := findOrderResponses(ln.ContrDID)
done, rejected, text := orderRespVerdict(resps)
if !done {
continue
}
at := orderRespAt(resps)
globalOrderWatch.update(guid, func(w *OrderWatch) {
if w.Lines == nil {
w.Lines = map[string]*OrderLineWatch{}
}
nc := time.Time{}
if old := w.Lines[ln.ContrDID]; old != nil {
nc = old.NoCounterAt
}
w.Lines[ln.ContrDID] = &OrderLineWatch{Ref: ln.Ref, ISIN: ln.ISIN, MS010At: at, Rejected: rejected, NoCounterAt: nc}
if rejected {
w.Rejected = true
}
})
no := orderRespNo(resps)
if rejected {
go depoOnOrderLineResult(rc, guid, ln.Ref, "rejected", text, no)
} else {
go depoOnOrderLineResult(rc, guid, ln.Ref, "accepted", "", no)
}
}
w = globalOrderWatch.Get(guid)
// 2) все строки закрыты MS010 → финальное состояние объекта
if allLinesMS010(w, lines) {
latest := time.Time{}
for _, ln := range lines {
if st := w.Lines[ln.ContrDID]; st != nil && st.MS010At.After(latest) {
latest = st.MS010At
}
}
w = globalOrderWatch.update(guid, func(w *OrderWatch) { w.MS010At = latest })
if direction == "out" { // тест «боевой ЛК → TEST3»: итог в ЛК — по исполнению НРД (realtest.go)
partial := ""
if total, executed, rejected, _ := w.linesSummary(); total > 1 && executed > 0 && rejected > 0 {
partial = fmt.Sprintf("исполнено %d из %d бумаг", executed, total)
}
go realTestOnExecuted(context.Background(), rc, guid, w.Rejected, partial)
}
return
}
// 3) просрочка 5 минут — один сигнал на объект, событие по каждой незакрытой строке
if w.LateAt.IsZero() && now.Sub(orderAt) > orderMS010Within {
globalOrderWatch.update(guid, func(w *OrderWatch) { w.LateAt = now })
for _, ln := range lines {
if !w.Lines[ln.ContrDID].lineDone() {
go depoOnOrderLineResult(rc, guid, ln.Ref, "late", "", "")
}
}
if fresh {
why := "отчёт MS010 не получен"
if !w.MatchedAt.IsZero() {
why = "поручения сквитованы со встречными (" + w.MatchedAt.Local().Format("15:04") + "), ждём исполнения НРД"
} else if !w.NoCounterAt.IsZero() {
why = "НРД сообщил, что контрагент ещё не подал встречное " + w.counterName() + " — наше поручение принято, исполнение ждёт контрагента"
}
raiseSystemAlert(rc, "НРД: депо-поручение не исполнено в срок 5 минут",
fmt.Sprintf("%s (перевод %s): подано %s, %s. Ожидаем исполнения до %s, после этого поручение считается отменённым.",
label, shortGUID(guid), orderAt.Local().Format("02.01.2006 15:04"), why, orderAt.Add(orderExecWithin).Local().Format("02.01.2006")))
}
}
// 4) 30 дней — по каждой незакрытой строке BJ_CANCELLED с REFERENCE; QUIK-блокировка снимается по ISIN
if now.Sub(orderAt) > orderExecWithin {
expiredAny := false
for _, ln := range lines {
if w.Lines[ln.ContrDID].lineDone() {
continue
}
expiredAny = true
globalOrderWatch.update(guid, func(w *OrderWatch) {
if w.Lines == nil {
w.Lines = map[string]*OrderLineWatch{}
}
w.Lines[ln.ContrDID] = &OrderLineWatch{Ref: ln.Ref, ISIN: ln.ISIN, ExpiredAt: now}
})
go depoOnOrderLineResult(rc, guid, ln.Ref, "expired", "", "")
if direction == "out" {
for _, l := range globalQuikLinks.ForGUID(guid) {
if l.State == "registered" && (ln.ISIN == "" || strings.EqualFold(l.ISIN, ln.ISIN)) {
_, _ = QuikOutDeny(ctx, rc, guid, l.ISIN, "Истёк срок исполнения поручения в НРД (30 дней) — перевод отменён")
}
}
}
}
if expiredAny {
globalOrderWatch.update(guid, func(w *OrderWatch) { w.ExpiredAt = now })
raiseSystemAlert(rc, "НРД: истёк срок 30 дней по депо-поручению — считается отменённым",
fmt.Sprintf("%s (перевод %s): подано %s, отчёт MS010 так и не получен. Поручение считается отменённым; проверьте перевод в НРД и позицию клиента.",
label, shortGUID(guid), orderAt.Local().Format("02.01.2006 15:04")))
}
}
}
// allLinesMS010 — по всем строкам получен MS010.
func allLinesMS010(w *OrderWatch, lines []orderLine) bool {
if w == nil || len(lines) == 0 {
return false
}
for _, ln := range lines {
st := w.Lines[ln.ContrDID]
if st == nil || st.MS010At.IsZero() {
return false
}
}
return true
}
func orderWatchTick(ctx context.Context, rc *RuntimeConfig, svc *Service) {
if globalOutgoingStore != nil {
for _, od := range scanForm16Drafts(globalOutgoingStore) {
if !od.Submitted {
continue
}
orderWatchCheck(ctx, rc, od.GUID, "out", od.OrderAt, outgoingLineRefs(ctx, svc, od), "Поручение 16 (списание)")
if w := globalOrderWatch.Get(od.GUID); w != nil && !w.MS010At.IsZero() && time.Since(w.MS010At) < 7*24*time.Hour {
outOnExecuted(ctx, rc, svc, od.GUID, w)
}
}
}
if globalIncomingStore != nil {
for _, it := range globalIncomingStore.List() {
if !it.OrderSubmitted {
continue
}
orderWatchCheck(ctx, rc, it.GUID, "in", it.OrderAt, incomingOrderLines(it), "Поручение 16/1 (зачисление)")
}
}
}
// outOnExecuted — НРД закрыл MS010 все строки нашего поручения 16: сделка → «Исполнена» (если списана
// хоть одна бумага), итог — в ЛК ЕСИА Финанс. Вызывается каждый проход, повтор ничего не меняет
// (23.10.2026: после MS010 сделка 97EAD00C оставалась «Подтверждена», в ЛК статус не менялся).
// Тест «боевой ЛК → TEST3» — свой итог (realTestOnExecuted).
func outOnExecuted(ctx context.Context, rc *RuntimeConfig, svc *Service, guid string, w *OrderWatch) {
total, executed, rejected, _ := w.linesSummary()
if total == 0 && !w.Rejected { // старые записи без строк — одна строка
total, executed = 1, 1
}
if svc != nil && executed > 0 {
d, err := svc.Repo().GetByGUID(ctx, m2m.UUID(guid))
if err != nil {
d, err = svc.Repo().GetByGUID(ctx, m2m.UUID(strings.ToLower(guid)))
}
if err == nil && (d.State == m2mcore.StateConfirmed || d.State == m2mcore.StateAwaitingSUB16) {
if err := d.CompleteSUB16(ctx); err != nil {
log.Printf("order-watch: %s: перевод не отмечен исполненным: %v", shortGUID(guid), err)
} else if err := svc.Repo().Update(ctx, d); err != nil {
log.Printf("order-watch: %s: сохранение «Исполнена»: %v", shortGUID(guid), err)
} else {
log.Printf("order-watch: %s: поручение 16 исполнено (MS010) — перевод «Исполнена»", shortGUID(guid))
}
}
}
if realTestGet(guid) != nil || !lkLinked(guid) {
return
}
status, reason := "done", "Перевод исполнен: бумаги списаны"
switch {
case executed == 0:
status, reason = "rejected", "НРД не исполнил поручение на списание"
case rejected > 0:
reason = fmt.Sprintf("Перевод исполнен частично: исполнено %d из %d бумаг", executed, total)
}
globalLKFlows.stepIfExists(guid, pick3(status == "done", "response", "reject"), "Итог НРД (MS010) → статус в ЛК", reason)
OnLKOutcome(ctx, rc, guid, status, reason)
log.Printf("order-watch: %s: итог в ЛК — %s (%s)", shortGUID(guid), status, reason)
}
// incomingOrderLines — строки поданных 16/1. Старые записи (до строчных событий) знали
// только первый contr_d_id — для них одна строка без REFERENCE.
func incomingOrderLines(it *IncomingTransfer) []orderLine {
if it == nil {
return nil
}
if len(it.OrderLines) > 0 {
out := make([]orderLine, 0, len(it.OrderLines))
for _, l := range it.OrderLines {
out = append(out, orderLine{ContrDID: l.ContrDID, Ref: l.Ref, ISIN: l.ISIN})
}
return out
}
if it.OrderContrDID != "" {
return []orderLine{{ContrDID: it.OrderContrDID}}
}
return nil
}
// orderWatchLoop — фоновый контроль сроков (раз в минуту).
func (s *Server) orderWatchLoop(ctx context.Context) {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
}
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("order-watch: panic: %v", r)
}
}()
orderWatchTick(ctx, s.rc, s.svc)
}()
healthBeat("order-watch")
}
}
// findNoCounterNotice — уведомление НРД GS036 «об отсутствии встречного поручения» по REFERENCE строки
// (номера нашего поручения в нём нет, есть референс сделки M2M). Возвращает время получения.
func findNoCounterNotice(ref string) (time.Time, bool) {
if len(ref) < 8 {
return time.Time{}, false
}
for _, pk := range pkgIndex() {
if strings.Contains(pk.body, ref) && strings.Contains(strings.ToLower(pk.body), "отсутствии встречного поручения") {
return pk.mt, true
}
}
return time.Time{}, false
}
// orderWatchRefs — референсы строк поручения по переводу (для поиска уведомлений НРД без номера поручения).
func orderWatchRefs(guid string) []string {
w := globalOrderWatch.Get(guid)
if w == nil {
return nil
}
var out []string
for _, l := range w.Lines {
if len(l.Ref) >= 8 {
out = append(out, l.Ref)
}
}
return out
}
// orderNoteNoCounter — отметить уведомления GS036 «нет встречного поручения» по строкам поручения.
func orderNoteNoCounter(guid string, w *OrderWatch, lines []orderLine) {
for _, ln := range lines {
st := w.Lines[ln.ContrDID]
// отчёт GS116 «сквитовано» (MACH) — по номеру поручения
if st == nil || st.MatchedAt.IsZero() {
if at, ok := findMatchedNotice(ln.ContrDID); ok {
globalOrderWatch.update(guid, func(w *OrderWatch) {
if w.Lines == nil {
w.Lines = map[string]*OrderLineWatch{}
}
l := w.Lines[ln.ContrDID]
if l == nil {
l = &OrderLineWatch{Ref: ln.Ref, ISIN: ln.ISIN}
w.Lines[ln.ContrDID] = l
}
l.MatchedAt = at
all, last := true, time.Time{}
for _, x := range lines {
lx := w.Lines[x.ContrDID]
if lx == nil || lx.MatchedAt.IsZero() {
all = false
break
}
if lx.MatchedAt.After(last) {
last = lx.MatchedAt
}
}
if all {
w.MatchedAt = last
}
})
}
}
// уведомление «нет встречного поручения» (GS036) — по REFERENCE строки
if (st == nil || st.NoCounterAt.IsZero()) && ln.Ref != "" {
if at, ok := findNoCounterNotice(ln.Ref); ok {
globalOrderWatch.update(guid, func(w *OrderWatch) {
if w.Lines == nil {
w.Lines = map[string]*OrderLineWatch{}
}
l := w.Lines[ln.ContrDID]
if l == nil {
l = &OrderLineWatch{Ref: ln.Ref, ISIN: ln.ISIN}
w.Lines[ln.ContrDID] = l
}
l.NoCounterAt = at
if w.NoCounterAt.IsZero() || at.Before(w.NoCounterAt) {
w.NoCounterAt = at
}
})
log.Printf("order-watch: перевод %s, %s: НРД сообщил об отсутствии встречного поручения (%s)", shortGUID(guid), ln.Ref, at.Local().Format("15:04:05"))
}
}
}
}
// findMatchedNotice — отчёт НРД GS116 «сквитовано со встречным» (<Mtchd>) по номеру поручения.
func findMatchedNotice(contrDID string) (time.Time, bool) {
if contrDID == "" {
return time.Time{}, false
}
for _, pk := range pkgIndex() {
if strings.Contains(pk.body, "<Mtchd>") && strings.Contains(pk.body, "GS116") && strings.Contains(pk.body, ">"+contrDID+"<") {
return pk.mt, true
}
}
return time.Time{}, false
}
// orderRespNo — номер ответа НРД (BizMsgIdr) из отчёта MS010 по строке.
func orderRespNo(resps []orderResponse) string {
for _, r := range resps {
if r.Done && r.No != "" {
return r.No
}
}
return ""
}