@@ -35,6 +35,9 @@ type OrderWatch struct {
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" `
// Lines — состояние по каждой строке пакета (одно депо-поручение = одна бумага),
// ключ — contr_d_id. По договорённости с Fancy (18.09.2026) исходы пишутся в DEPO
// по строкам с REFERENCE и сразу по MS010 этой строки, не дожидаясь остальных:
@@ -44,11 +47,20 @@ type OrderWatch struct {
// 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" `
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 по этой строке
}
// 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).
@@ -110,9 +122,14 @@ func (w *OrderWatch) StatusRu() string {
if w . MS010At . Sub ( w . OrderAt ) > orderMS010Within {
late = fmt . Sprintf ( ", с опозданием %s" , w . MS010At . Sub ( w . OrderAt ) . Round ( time . Minute ) )
}
if ! 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 . 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 :
@@ -259,6 +276,10 @@ func orderWatchCheck(ctx context.Context, rc *RuntimeConfig, guid, direction str
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 // финальное состояние уже зафиксировано по всем строкам
}
@@ -281,7 +302,11 @@ func orderWatchCheck(ctx context.Context, rc *RuntimeConfig, guid, direction str
if w . Lines == nil {
w . Lines = map [ string ] * OrderLineWatch { }
}
w . Lines [ ln . ContrDID ] = & OrderLineWatch { Ref : ln . Ref , ISIN : ln . ISIN , MS010At : at , Rejected : rejected }
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
}
@@ -315,9 +340,13 @@ func orderWatchCheck(ctx context.Context, rc *RuntimeConfig, guid, direction str
}
}
if fresh {
why := "отчёт MS010 не получен"
if ! w . NoCounterAt . IsZero ( ) {
why = "НРД сообщил, что контрагент ещё не подал встречное " + w . counterName ( ) + " — наше поручение принято, исполнение ждёт контрагента"
}
raiseSystemAlert ( rc , "НРД: депо-поручение не исполнено в срок 5 минут" ,
fmt . Sprintf ( "%s (перевод %s): подано %s, отчёт MS010 не получен . Ожидаем исполнения до %s, после этого поручение считается отменённым." ,
label , shortGUID ( guid ) , orderAt . Local ( ) . Format ( "02.01.2006 15:04" ) , orderAt . Add ( orderExecWithin ) . Local ( ) . Format ( "02.01.2006" ) ) )
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" ) ) )
}
}
@@ -426,3 +455,59 @@ func (s *Server) orderWatchLoop(ctx context.Context) {
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 ]
// уведомление «нет встречного поручения» (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" ) )
}
}
}
}