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

456 lines
15 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_handlers.go — страницы раздела «Ручной ввод M2M» (/admin/manual).
import (
"context"
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type manualPageData struct {
page
Mode string // list | edit | card | print
Flash string
List []manualListRow
O *ManualOrder
Handbook []HandbookEntry
DocTypes []struct{ Code, Title string }
PreChecks []manualCheckDef
PostChecks []manualPostDef
Problems []string // незаполненные реквизиты (для формы)
Blockers []string // почему нельзя отправить
DepoOn bool // учётная система подключена — сверка обязательна
DepoStale bool // данные изменены после сверки
QuikAuto bool // bj может сам заблокировать бумаги в торговой системе
Progress manualProgress
NSD NSDSettings
Auto bool // автоматический режим M2M включён (поручение 16 подаётся само)
}
// manualListRow — строка списка.
type manualListRow struct {
O *ManualOrder
State string // draft | sent | declined | done | rejected
StateRu string
}
func manualActor(w http.ResponseWriter) string {
if u := currentUser(w); u != nil {
if u.Name != "" {
return u.Name + " (" + u.Login + ")"
}
return u.Login
}
return "без входа"
}
func registerManual(mux *http.ServeMux, rc *RuntimeConfig, svc *Service) {
tpl, err := template.New("layout.html").Funcs(templateFuncs).ParseFS(templatesFS,
"web/templates/layout.html", "web/templates/admin_manual.html")
if err != nil {
log.Fatalf("lk-gateway: шаблон admin_manual: %v", err)
}
help, err := template.New("layout.html").Funcs(templateFuncs).ParseFS(templatesFS,
"web/templates/layout.html", "web/templates/admin_help_manual.html")
if err != nil {
log.Fatalf("lk-gateway: шаблон admin_help_manual: %v", err)
}
mux.HandleFunc("/admin/help/manual", func(w http.ResponseWriter, r *http.Request) {
render(w, help, nowPage("Ручной ввод M2M: инструкция", "help"))
})
base := func(title string) manualPageData {
return manualPageData{
page: nowPage(title, "manual"), DocTypes: manualDocTypes, PreChecks: manualPreChecks, PostChecks: manualPostChecks,
DepoOn: manualDepoConfigured(rc), QuikAuto: manualQuikAutoAvailable(rc), NSD: rc.Snapshot().NSD,
}
}
back := func(w http.ResponseWriter, r *http.Request, to, msg, anchor string) {
if msg != "" {
to += "?flash=" + url.QueryEscape(msg)
}
http.Redirect(w, r, to+anchor, http.StatusSeeOther)
}
mux.HandleFunc("/admin/manual", func(w http.ResponseWriter, r *http.Request) {
d := base("Ручной ввод M2M")
d.Mode, d.Flash = "list", r.URL.Query().Get("flash")
var drafts map[string]*OutgoingDeal
for _, o := range globalManual.List() {
row := manualListRow{O: o, State: o.Status, StateRu: manualStatusRu(o.Status)}
if o.Status == "sent" {
if drafts == nil { // INBOX читаем один раз на список
drafts = map[string]*OutgoingDeal{}
for _, dd := range scanForm16Drafts(globalOutgoingStore) {
drafts[dd.GUID] = dd
}
}
row.StateRu = "в работе"
if dd := drafts[o.GUID]; dd != nil && dd.Done {
row.State, row.StateRu = "done", "исполнено"
} else if o.DealID != "" && svc != nil {
if v, err := svc.GetClaim(r.Context(), o.DealID); err == nil && (v.Status == "rejected" || v.Status == "timed_out") {
row.State, row.StateRu = "rejected", "отказ"
}
}
}
d.List = append(d.List, row)
}
render(w, tpl, d)
})
registerManualDepo(mux, rc)
mux.HandleFunc("/admin/manual/new", func(w http.ResponseWriter, r *http.Request) {
d := base("Новое поручение")
d.Mode = "edit"
d.O = &ManualOrder{Status: "draft", DocType: "21", SettlePlace: "nrd", PaperDate: time.Now().Format("2006-01-02"),
ReceivedVia: "лично в офисе", Securities: []ManualSec{{}}}
d.Handbook = handbookParticipants()
render(w, tpl, d)
})
// Сохранение формы (новое или черновик). do: save | lookup (заполнить из DEPO) | depo (сверка).
mux.HandleFunc("/admin/manual/save", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
_ = r.ParseForm()
in := manualFromForm(r)
id, _ := strconv.Atoi(r.FormValue("id"))
actor := manualActor(w)
msg := "Поручение сохранено"
if id == 0 {
in.Status, in.CreatedBy = "draft", actor
if err := globalManual.Create(in); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
id = in.ID
} else {
reset := false
err := globalManual.Update(id, func(o *ManualOrder) error {
if o.Status != "draft" {
return fmt.Errorf("поручение уже %s — изменить нельзя", manualStatusRu(o.Status))
}
old := o.dataHash()
manualCopyData(o, in)
if o.dataHash() != old && (o.Depo != nil || len(o.Checks) > 0) {
o.Depo, o.Checks, reset = nil, nil, true
}
return nil
})
if err != nil {
back(w, r, fmt.Sprintf("/admin/manual/%d", id), err.Error(), "")
return
}
if reset {
msg = "Данные изменены — сверка и отметки сброшены, пройдите их заново"
}
}
switch r.FormValue("do") {
case "lookup":
msg = manualDoLookup(rc, id)
case "depo":
msg = manualDoDepo(rc, id, actor)
}
anchor := ""
if r.FormValue("do") == "depo" {
anchor = "#depo"
}
back(w, r, fmt.Sprintf("/admin/manual/%d", id), msg, anchor)
})
mux.HandleFunc("/admin/manual/", func(w http.ResponseWriter, r *http.Request) {
rest := strings.TrimPrefix(r.URL.Path, "/admin/manual/")
parts := strings.SplitN(rest, "/", 2)
id, err := strconv.Atoi(parts[0])
if err != nil {
http.NotFound(w, r)
return
}
action := ""
if len(parts) == 2 {
action = parts[1]
}
o, ok := globalManual.Get(id)
if !ok {
http.NotFound(w, r)
return
}
card := fmt.Sprintf("/admin/manual/%d", id)
if action != "" && action != "print" && r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
actor := manualActor(w)
switch action {
case "", "print":
d := base("Поручение " + o.Number())
d.O, d.Flash = o, r.URL.Query().Get("flash")
d.Mode = "card"
if action == "print" {
d.Mode = "print"
}
if o.Status == "draft" {
d.Handbook = handbookParticipants()
d.Problems = manualDataProblems(o)
d.Blockers = manualSendBlockers(rc, o)
d.DepoStale = o.Depo != nil && o.Depo.Hash != o.dataHash()
if len(o.Securities) == 0 { // форма показывает строку бумаги всегда
o.Securities = []ManualSec{{}}
}
}
d.Progress = manualProgressFor(r.Context(), svc, o)
d.Auto = rc.Snapshot().M2MAuto.Enabled
render(w, tpl, d)
case "checks":
_ = r.ParseForm()
err := globalManual.Update(id, func(o *ManualOrder) error {
if o.Status != "draft" {
return fmt.Errorf("поручение уже %s", manualStatusRu(o.Status))
}
if o.Checks == nil {
o.Checks = map[string]ManualMark{}
}
for _, c := range manualPreChecks {
manualMark(o.Checks, c.Key, r.FormValue("check_"+c.Key) != "", actor)
}
switch m := r.FormValue("quik_mode"); m {
case "auto", "manual", "none":
o.QuikMode = m
}
o.QuikInstr = strings.TrimSpace(r.FormValue("quik_instr"))
return nil
})
msg := ""
if err != nil {
msg = err.Error()
}
back(w, r, card, msg, "#checks")
case "send":
var blockers []string
err := globalManual.Update(id, func(o *ManualOrder) error {
if blockers = manualSendBlockers(rc, o); len(blockers) > 0 {
return fmt.Errorf("отправить нельзя: %s", strings.Join(blockers, "; "))
}
o.Status, o.SendErr = "sending", ""
return nil
})
if err != nil {
back(w, r, card, err.Error(), "")
return
}
o, _ = globalManual.Get(id)
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
guid, dealID, serr := manualSend(ctx, rc, svc, o)
cancel()
msg := ""
_ = globalManual.Update(id, func(o *ManualOrder) error {
if serr != nil {
o.Status, o.SendErr = "draft", serr.Error()
msg = "Поручение не отправлено: " + serr.Error()
return nil
}
o.Status, o.GUID, o.DealID, o.SentAt, o.SentBy = "sent", guid, dealID, time.Now(), actor
msg = "Поручение отправлено в НРД. Ход исполнения — ниже на этой странице."
return nil
})
log.Printf("lk-gateway: ручной ввод %s (%s): отправка — %s", o.Number(), o.FullName(), msg)
back(w, r, card, msg, "")
case "decline":
reason := strings.TrimSpace(r.FormValue("reason"))
err := globalManual.Update(id, func(o *ManualOrder) error {
if o.Status != "draft" {
return fmt.Errorf("поручение уже %s", manualStatusRu(o.Status))
}
if reason == "" {
return fmt.Errorf("укажите причину отказа")
}
o.Status, o.DeclineReason, o.DeclinedBy, o.DeclinedAt = "declined", reason, actor, time.Now()
return nil
})
msg := "Поручение отклонено депозитарием"
if err != nil {
msg = err.Error()
}
back(w, r, card, msg, "")
case "post":
_ = r.ParseForm()
err := globalManual.Update(id, func(o *ManualOrder) error {
if o.Post == nil {
o.Post = map[string]ManualMark{}
}
for _, c := range manualPostChecks {
if _, shown := r.Form["shown_"+c.Key]; shown {
manualMark(o.Post, c.Key, r.FormValue("post_"+c.Key) != "", actor)
}
}
return nil
})
msg := ""
if err != nil {
msg = err.Error()
}
back(w, r, card, msg, "#post")
default:
http.NotFound(w, r)
}
})
}
// manualMark — поставить или снять отметку (время и автор — при постановке).
func manualMark(m map[string]ManualMark, key string, on bool, actor string) {
cur := m[key]
switch {
case on && !cur.Done:
m[key] = ManualMark{Done: true, By: actor, At: time.Now()}
case !on && cur.Done:
delete(m, key)
}
}
// manualFromForm — данные поручения из формы.
func manualFromForm(r *http.Request) *ManualOrder {
f := func(k string) string { return strings.TrimSpace(r.FormValue(k)) }
o := &ManualOrder{
PaperNo: f("paper_no"), PaperDate: f("paper_date"), ReceivedVia: f("received_via"),
LastName: f("last_name"), FirstName: f("first_name"), MiddleName: f("middle_name"), BirthDate: f("birth_date"),
DocType: f("doc_type"), DocSeries: strings.ReplaceAll(f("doc_series"), " ", ""), DocNumber: strings.ReplaceAll(f("doc_number"), " ", ""),
DocIssueDate: f("doc_issue_date"), DocIssuer: f("doc_issuer"),
Contract: f("contract"), Account: strings.ToUpper(f("account")), QuikCode: f("quik_code"), Phone: f("phone"),
RecvINN: f("recv_inn"), RecvName: f("recv_name"), RecvAccount: f("recv_account"), SettlePlace: f("settle_place"),
CostInfo: r.FormValue("cost_info") != "", Comment: f("comment"),
}
if o.SettlePlace != "spb" {
o.SettlePlace = "nrd"
}
isins, codes, names, qtys := r.Form["sec_isin"], r.Form["sec_code"], r.Form["sec_name"], r.Form["sec_qty"]
at := func(a []string, i int) string {
if i < len(a) {
return strings.TrimSpace(a[i])
}
return ""
}
for i := range isins {
s := ManualSec{ISIN: strings.ToUpper(at(isins, i)), Code: at(codes, i), Name: at(names, i), Qty: at(qtys, i)}
if s.ISIN == "" && s.Qty == "" && s.Code == "" {
continue
}
o.Securities = append(o.Securities, s)
}
if o.RecvINN != "" {
o.RecvCode = handbookCodeByINN(o.RecvINN)
if o.RecvName == "" {
for _, p := range handbookParticipants() {
if p.INN == o.RecvINN && (o.RecvCode == "" || p.Code == o.RecvCode) {
o.RecvName = p.Name
break
}
}
}
}
return o
}
// manualCopyData — переносит данные формы в сохранённое поручение (служебные поля не трогает).
func manualCopyData(dst, src *ManualOrder) {
keep := *dst
*dst = *src
dst.ID, dst.CreatedAt, dst.CreatedBy, dst.UpdatedAt = keep.ID, keep.CreatedAt, keep.CreatedBy, keep.UpdatedAt
dst.Depo, dst.Checks, dst.QuikMode, dst.QuikInstr = keep.Depo, keep.Checks, keep.QuikMode, keep.QuikInstr
dst.Status, dst.GUID, dst.DealID, dst.SentAt, dst.SentBy, dst.SendErr = keep.Status, keep.GUID, keep.DealID, keep.SentAt, keep.SentBy, keep.SendErr
dst.DeclineReason, dst.DeclinedBy, dst.DeclinedAt, dst.Post = keep.DeclineReason, keep.DeclinedBy, keep.DeclinedAt, keep.Post
}
// manualDoLookup — заполнить из DEPO пустые ФИО, дату рождения, счёт и наименования бумаг.
func manualDoLookup(rc *RuntimeConfig, id int) string {
o, ok := globalManual.Get(id)
if !ok {
return "поручение не найдено"
}
if strings.TrimSpace(o.DocNumber) == "" {
return "Для поиска в DEPO укажите серию и номер документа"
}
c := manualDepoLookup(rc, o.DocSeries, o.DocNumber)
if !c.Found {
return "Поиск в DEPO: " + c.Err
}
var isins []string
for _, s := range o.Securities {
if s.Name == "" && s.ISIN != "" {
isins = append(isins, s.ISIN)
}
}
names := manualDepoSecNames(rc, isins)
var filled []string
_ = globalManual.Update(id, func(o *ManualOrder) error {
old := o.dataHash()
if o.LastName == "" && o.FirstName == "" {
if p := strings.Fields(c.Name); len(p) >= 2 {
o.LastName, o.FirstName = p[0], p[1]
if len(p) > 2 {
o.MiddleName = strings.Join(p[2:], " ")
}
filled = append(filled, "ФИО")
}
}
if o.BirthDate == "" && c.Birth != "" {
o.BirthDate = c.Birth
filled = append(filled, "дата рождения")
}
if o.Account == "" && c.Account != "" {
o.Account = c.Account
filled = append(filled, "счёт "+c.Account)
}
for i := range o.Securities {
if n := names[o.Securities[i].ISIN]; n != "" && o.Securities[i].Name == "" {
o.Securities[i].Name = n
filled = append(filled, "наименование "+o.Securities[i].ISIN)
}
}
if o.dataHash() != old {
o.Depo, o.Checks = nil, nil
}
return nil
})
msg := "Клиент в DEPO: " + c.Name
if c.Account == "" {
msg += " (открытого торгового счёта нет)"
}
if len(filled) > 0 {
msg += ". Заполнено: " + strings.Join(filled, ", ") + " — проверьте с поручением клиента"
} else {
msg += ". Пустых полей для заполнения нет"
}
return msg
}
// manualDoDepo — сверка с DEPO.
func manualDoDepo(rc *RuntimeConfig, id int, actor string) string {
o, ok := globalManual.Get(id)
if !ok {
return "поручение не найдено"
}
if p := manualDataProblems(o); len(p) > 0 {
return "Сверка не выполнена — сначала заполните реквизиты: " + strings.Join(p, "; ")
}
chk := manualDepoRun(rc, o)
chk.By = actor
_ = globalManual.Update(id, func(o *ManualOrder) error {
o.Depo = chk
return nil
})
if !chk.Ran {
return "Сверка с DEPO не выполнена: " + chk.Err
}
return "Сверка с DEPO: " + chk.Summary
}