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

266 lines
10 KiB
Go

package lkgateway
import (
"context"
"database/sql"
"encoding/json"
"net/http"
"net/url"
"path"
"strings"
"time"
)
// registerIncoming вешает маршруты «Входящие переводы» (сценарий 3333):
// список, карточка с выбором по бумагам, отправка решения роботу.
func registerIncoming(mux *http.ServeMux, a *admin, deps incomingDeps) {
mux.HandleFunc("/admin/incoming", func(w http.ResponseWriter, r *http.Request) {
data := struct {
page
Items []*IncomingTransfer
}{nowPage("Входящие переводы", "incoming"), deps.store.List()}
render(w, a.incoming, data)
})
// JSON-фид для браузерных уведомлений: список входящих, ждущих решения.
// Фронтенд (layout.html) опрашивает его и показывает Notification на новые.
mux.HandleFunc("/admin/incoming/feed", func(w http.ResponseWriter, r *http.Request) {
type feedItem struct {
ID string `json:"id"`
GUID string `json:"guid"`
Sender string `json:"sender"`
Count int `json:"count"`
Received string `json:"received"`
}
var pending []feedItem
for _, it := range deps.store.List() {
if it.Status != "pending" {
continue
}
pending = append(pending, feedItem{
ID: it.ID, GUID: it.GUID, Sender: it.SenderCode,
Count: len(it.Securities), Received: it.ReceivedAt.In(time.FixedZone("МСК", 3*3600)).Format("02.01 15:04:05"),
})
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(map[string]any{"count": len(pending), "pending": pending})
})
// Тестовый прогон входящего в DEPO с реальным клиентом (без отправки в НРД).
mux.HandleFunc("/admin/incoming/depo-sim", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
id := r.FormValue("id")
it, ok := deps.store.Get(id)
if !ok {
http.NotFound(w, r)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
defer cancel()
guid, err := depoSimulateIncoming(ctx, deps.rc, it, r.FormValue("ser"), r.FormValue("num"))
if err != nil {
http.Redirect(w, r, "/admin/incoming/"+id+"?flash="+urlQ("Прогон в DEPO: "+err.Error()), http.StatusSeeOther)
return
}
// ждём, создаст ли джоба документ
doc := ""
if db, e := depoEventsDB(deps.rc); e == nil {
for i := 0; i < 8; i++ {
time.Sleep(5 * time.Second)
var d sql.NullString
_ = db.QueryRowContext(ctx, "SELECT CAST(DOC AS varchar(32)) FROM dbo.FTV_M2M WHERE GUID=@g", sql.Named("g", guid)).Scan(&d)
if d.Valid && d.String != "" && d.String != "0" {
doc = d.String
break
}
}
db.Close()
}
msg := "Прогон в DEPO записан: новый GUID " + guid + ", клиент по паспорту " + r.FormValue("ser") + " " + r.FormValue("num")
if doc != "" {
msg += ", джоба создала документ DEPO №" + doc
} else {
msg += "; документ DEPO пока не создан (джоба не ответила за 40 с)"
}
http.Redirect(w, r, "/admin/incoming/"+id+"?flash="+urlQ(msg), http.StatusSeeOther)
})
mux.HandleFunc("/admin/incoming/", func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimPrefix(r.URL.Path, "/admin/incoming/")
// POST /admin/incoming/{id}/answer
if strings.HasSuffix(p, "/answer") {
id := path.Base(strings.TrimSuffix(p, "/answer"))
handleIncomingAnswer(w, r, deps, id)
return
}
// GET /admin/incoming/{id}/export.zip — выгрузка для регулятора
if strings.HasSuffix(p, "/export.zip") {
id := path.Base(strings.TrimSuffix(p, "/export.zip"))
exportIncoming(w, r, deps, id)
return
}
// GET /admin/incoming/{id}/draft.zip — черновик поручения 16/1 (MF010) для учётной системы
if strings.HasSuffix(p, "/draft.zip") {
id := path.Base(strings.TrimSuffix(p, "/draft.zip"))
downloadIncomingDraft(w, r, deps, id)
return
}
// POST /admin/incoming/{id}/submit-order — подать поручение 16/1 (ORDXM) в НРД
if strings.HasSuffix(p, "/submit-order") {
id := path.Base(strings.TrimSuffix(p, "/submit-order"))
submitDepoOrder(w, r, deps, id)
return
}
// GET /admin/incoming/{id}/order.xml — скачать отправленную форму 16/1
if strings.HasSuffix(p, "/order.xml") {
id := path.Base(strings.TrimSuffix(p, "/order.xml"))
downloadSentOrder(w, r, deps, id)
return
}
// GET /admin/incoming/{id}/request.xml — исходный запрос контрагента
if strings.HasSuffix(p, "/request.xml") {
id := path.Base(strings.TrimSuffix(p, "/request.xml"))
it, ok := deps.store.Get(id)
if !ok || it.RawXML == "" {
http.Error(w, "запрос не найден", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="request-`+shortID(id)+`.xml"`)
_, _ = w.Write(faithfulXMLBytes(it.RawXML))
return
}
// GET /admin/incoming/{id}/decision.xml — наш ответ-решение
if strings.HasSuffix(p, "/decision.xml") {
id := path.Base(strings.TrimSuffix(p, "/decision.xml"))
it, ok := deps.store.Get(id)
if !ok || it.DecisionXML == "" {
http.Error(w, "решение не найдено", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="decision-`+shortID(id)+`.xml"`)
_, _ = w.Write(faithfulXMLBytes(it.DecisionXML))
return
}
// GET /admin/incoming/{id}/ms010.zip — ответ НРД (отчёт MS010) по поручению
if strings.HasSuffix(p, "/ms010.zip") {
id := path.Base(strings.TrimSuffix(p, "/ms010.zip"))
it, ok := deps.store.Get(id)
if !ok {
http.Error(w, "не найден", http.StatusNotFound)
return
}
writeOrderResponseZip(w, it.OrderContrDID, "otvet-nrd-ms010-"+shortID(id))
return
}
// GET /admin/incoming/{id}
id := path.Base(p)
it, ok := deps.store.Get(id)
if !ok {
http.Error(w, "входящий перевод не найден", http.StatusNotFound)
return
}
var orderResps []orderResponse
var orderDone bool
var orderDoneAt string
if it.OrderSubmitted {
orderResps = findOrderResponses(it.OrderContrDID)
for _, rp := range orderResps {
if rp.Done { // отчёт MS010 — операция проведена
orderDone = true
orderDoneAt = rp.Time
}
}
}
rejected, codes := incomingRejectedAll(it)
data := incomingDetailData{page: nowPage("Входящий перевод", "incoming"), It: it, Flash: r.URL.Query().Get("flash"),
OrderResponses: orderResps, OrderDone: orderDone, OrderDoneAt: orderDoneAt, DraftReceived: draftReceivedTime(it.GUID),
Quik: globalQuikLinks.ForGUID(it.GUID), QuikOn: quikActive(deps.rc.Snapshot().Quik), Watch: globalOrderWatch.Get(it.GUID), DepoL: globalDepoLinks.Get(it.GUID),
Rejected: rejected, RejectCodes: codes, AutoMode: deps.rc.Snapshot().M2MAuto.Enabled}
// Сверка выполняется при открытии карточки; итог сохраняем для бейджа в списке.
data.Depo = runDepoCheck(it)
if data.Depo.Ran {
deps.store.SetDepo(it.ID, data.Depo.Recommend, data.Depo.Summary)
}
render(w, a.incomingDetail, data)
})
}
// handleIncomingAnswer обрабатывает форму решения: по каждой бумаге radio
// confirm/reject (+ код отказа), собирает решение и отправляет роботу.
func handleIncomingAnswer(w http.ResponseWriter, r *http.Request, deps incomingDeps, id string) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
it, ok := deps.store.Get(id)
if !ok {
http.Error(w, "не найден", http.StatusNotFound)
return
}
var decisions []SecurityDecision
for _, sec := range it.Securities {
// поле decision_<ReferenceID> = "confirm" | "reject"
val := r.FormValue("decision_" + sec.ReferenceID)
d := SecurityDecision{ReferenceID: sec.ReferenceID, Confirm: val == "confirm"}
if !d.Confirm {
d.RejectCode = strings.TrimSpace(r.FormValue("reject_" + sec.ReferenceID))
if d.RejectCode == "" {
d.RejectCode = "M2M05"
}
}
decisions = append(decisions, d)
}
msg := "Решение отправлено контрагенту через НРД"
if devMode(deps.rc) {
msg = "Решение отправлено роботу"
}
if err := answerIncoming(r.Context(), deps, id, decisions); err != nil {
msg = "Ошибка отправки решения: " + err.Error()
}
http.Redirect(w, r, "/admin/incoming/"+id+"?flash="+url.QueryEscape(msg), http.StatusSeeOther)
}
// incomingDetailData — карточка входящего перевода.
type incomingDetailData struct {
page
It *IncomingTransfer
Flash string
OrderResponses []orderResponse
OrderDone bool
OrderDoneAt string
DraftReceived string
Depo depoResult // сверка с DEPO — рекомендация по ответу, ничего не отправляется
Quik []*QuikLink
QuikOn bool
Watch *OrderWatch
DepoL *DepoLink
Rejected bool // решение отправлено и ни одна бумага не принята — перевод завершён отказом
RejectCodes string // коды отказа через запятую
AutoMode bool // автоматический режим M2M (поручение 16/1 подаст bj)
}
// incomingRejectedAll — перевод отвечен и ни одна бумага не принята (поручение 16/1 не нужно).
func incomingRejectedAll(it *IncomingTransfer) (bool, string) {
if it.Status != "answered" || incomingHasConfirmed(it) {
return false, ""
}
var codes []string
seen := map[string]bool{}
for _, s := range it.Securities {
if s.RejectCode != "" && !seen[s.RejectCode] {
seen[s.RejectCode] = true
codes = append(codes, s.RejectCode)
}
}
return true, strings.Join(codes, ", ")
}