d2024b44ca
- ручных отметок перед отправкой нет: автор виден по учётной записи, клиент/счёт/остаток/
бумага проверены сверкой с DEPO при сохранении; блокировка в QUIK — необязательно,
по умолчанию не требуется (модуль неторговых поручений сейчас не используется)
- «Удалить черновик» в карточке (POST /admin/manual/{id}/delete), только для черновиков
- количество не больше остатка на торговом разделе: проверка в форме (по выбранной бумаге)
и на сервере при сохранении (по счёту DEPO) — поручение с превышением не сохраняется
- отчёт MS010 с отказом: orderResponse.Rejected, OutgoingDeal.Rejected; шаг «Операция
завершена отказом» и бейдж — красные в карточках исходящего и входящего; онлайн-статус
«поручение 16 завершено отказом НРД»
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
166 lines
7.3 KiB
Go
166 lines
7.3 KiB
Go
package lkgateway
|
|
|
|
// livestatus.go — «чего ждёт заявка»: одна строка состояния для журналов и карточек.
|
|
//
|
|
// Статус сделки (черновик, отправлена, ожидает решение…) говорит, где заявка, но не говорит,
|
|
// чего мы ждём: черновика поручения от НРД, подачи, ответа MS010, решения оператора. Здесь это
|
|
// сводится в одну фразу с признаком «в работе». Пока заявка в работе, страницы обновляют
|
|
// строку по /admin/journal/live каждые несколько секунд; завершённые и отклонённые не
|
|
// опрашиваются — там ничего не изменится.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// LiveStatus — состояние для показа.
|
|
type LiveStatus struct {
|
|
Text string `json:"text"` // чего ждём / чем закончилось
|
|
Kind string `json:"kind"` // wait | warn | ok | bad
|
|
Active bool `json:"active"` // в работе: страница продолжает опрашивать
|
|
}
|
|
|
|
// form16Snapshot — черновики и подачи поручений 16 по всем сделкам одним обходом входящего
|
|
// каталога ИШ, с кэшем на несколько секунд: журнал на сотню строк и опрос состояния каждые
|
|
// 8 секунд не должны обходить каталог по разу на строку.
|
|
var form16Snap struct {
|
|
mu sync.Mutex
|
|
at time.Time
|
|
deal map[string]*OutgoingDeal
|
|
}
|
|
|
|
func form16Snapshot() map[string]*OutgoingDeal {
|
|
form16Snap.mu.Lock()
|
|
defer form16Snap.mu.Unlock()
|
|
if form16Snap.deal != nil && time.Since(form16Snap.at) < 5*time.Second {
|
|
return form16Snap.deal
|
|
}
|
|
m := map[string]*OutgoingDeal{}
|
|
if globalOutgoingStore != nil {
|
|
for _, d := range scanForm16DraftsOpt(globalOutgoingStore, false) { // ответы — только по сделкам в работе, ниже
|
|
m[d.GUID] = d
|
|
}
|
|
}
|
|
form16Snap.deal, form16Snap.at = m, time.Now()
|
|
return m
|
|
}
|
|
|
|
// liveOutgoing — состояние исходящего перевода: сделка M2M + депозитарное поручение 16.
|
|
func liveOutgoing(c ClaimView) LiveStatus {
|
|
guid := string(c.M2MGUID)
|
|
var depo *OutgoingDeal
|
|
if guid != "" && c.Status == "confirmed" { // каталог нужен только на этапе депо-исполнения
|
|
if d := form16Snapshot()[guid]; d != nil {
|
|
cp := *d
|
|
if cp.Submitted && len(cp.Responses) == 0 && !cp.Done {
|
|
cp.Responses, cp.Done = outgoingResponses(cp.ContrDIDs) // ответы ищем только для этой, живой сделки
|
|
_, cp.Rejected, _ = orderRespVerdict(cp.Responses)
|
|
}
|
|
depo = &cp
|
|
}
|
|
}
|
|
var watch *OrderWatch
|
|
if guid != "" && globalOrderWatch != nil {
|
|
watch = globalOrderWatch.Get(guid)
|
|
}
|
|
switch c.Status {
|
|
case "draft", "validated":
|
|
return LiveStatus{Text: "ожидает отправки в НРД", Kind: "wait", Active: true}
|
|
case "submitted_to_nsd":
|
|
return LiveStatus{Text: "отправлена в НРД, ожидает подтверждения приёма", Kind: "wait", Active: true}
|
|
case "awaiting_decision":
|
|
return LiveStatus{Text: "ожидает решения принимающего депозитария", Kind: "wait", Active: true}
|
|
case "manual_approval":
|
|
return LiveStatus{Text: "ожидает решения оператора", Kind: "warn", Active: true}
|
|
case "awaiting_sub16":
|
|
return LiveStatus{Text: "принята, ожидает депозитарного исполнения", Kind: "wait", Active: true}
|
|
case "rejected":
|
|
return LiveStatus{Text: "отклонена", Kind: "bad"}
|
|
case "timed_out":
|
|
return LiveStatus{Text: "истёк срок ответа контрагента", Kind: "bad"}
|
|
case "done":
|
|
return LiveStatus{Text: "исполнена", Kind: "ok"}
|
|
case "confirmed":
|
|
switch {
|
|
case depo == nil:
|
|
return LiveStatus{Text: "принята контрагентом, ожидает черновика поручения 16 от НРД", Kind: "wait", Active: true}
|
|
case !depo.Submitted:
|
|
return LiveStatus{Text: "черновик поручения 16 получен, ожидает подачи в НРД", Kind: "warn", Active: true}
|
|
case depo.Done && depo.Rejected:
|
|
return LiveStatus{Text: "поручение 16 завершено отказом НРД (MS010)", Kind: "bad"}
|
|
case depo.Done:
|
|
return LiveStatus{Text: "поручение 16 исполнено: MS010 получен", Kind: "ok"}
|
|
case watch != nil:
|
|
k := watch.Kind()
|
|
if k == "" {
|
|
k = "wait"
|
|
}
|
|
return LiveStatus{Text: "поручение 16 подано: " + watch.StatusRu(), Kind: k, Active: k == "wait" || k == "warn"}
|
|
default:
|
|
return LiveStatus{Text: "поручение 16 подано, ожидает ответа НРД (MS010)", Kind: "wait", Active: true}
|
|
}
|
|
}
|
|
return LiveStatus{Text: russianState(c.Status), Kind: "wait"}
|
|
}
|
|
|
|
// liveIncoming — состояние входящего перевода: наше решение, затем поручение 16/1.
|
|
func liveIncoming(it *IncomingTransfer) LiveStatus {
|
|
if it == nil {
|
|
return LiveStatus{}
|
|
}
|
|
if it.Status != "answered" {
|
|
return LiveStatus{Text: "ожидает нашего решения", Kind: "warn", Active: true}
|
|
}
|
|
_, _, outcome := incomingJournalStatus(it)
|
|
if outcome == "Отказано" {
|
|
return LiveStatus{Text: "отказано", Kind: "bad"}
|
|
}
|
|
if globalOrderWatch != nil {
|
|
if w := globalOrderWatch.Get(it.GUID); w != nil {
|
|
k := w.Kind()
|
|
return LiveStatus{Text: "поручение 16/1 подано: " + w.StatusRu(), Kind: k, Active: k == "wait" || k == "warn"}
|
|
}
|
|
}
|
|
if strings.HasPrefix(outcome, "Частично") {
|
|
return LiveStatus{Text: strings.ToLower(outcome) + "; ожидает поручения 16/1", Kind: "wait", Active: true}
|
|
}
|
|
return LiveStatus{Text: "принято, ожидает подачи поручения 16/1", Kind: "wait", Active: true}
|
|
}
|
|
|
|
// registerLiveStatus — GET /admin/journal/live?out=id,id&in=id,id → состояния для строк в работе.
|
|
func registerLiveStatus(mux *http.ServeMux, svc *Service, store *IncomingStore) {
|
|
mux.HandleFunc("/admin/journal/live", func(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
out := map[string]LiveStatus{}
|
|
for _, id := range splitIDs(r.URL.Query().Get("out")) {
|
|
if c, err := svc.GetClaim(ctx, id); err == nil {
|
|
out[id] = liveOutgoing(c)
|
|
}
|
|
}
|
|
in := map[string]LiveStatus{}
|
|
for _, id := range splitIDs(r.URL.Query().Get("in")) {
|
|
if it, ok := store.Get(id); ok {
|
|
in[id] = liveIncoming(it)
|
|
}
|
|
}
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"out": out, "in": in})
|
|
})
|
|
}
|
|
|
|
func splitIDs(s string) []string {
|
|
var out []string
|
|
for _, p := range strings.Split(s, ",") {
|
|
if p = strings.TrimSpace(p); p != "" && len(p) <= 64 && len(out) < 200 {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|