package lkgateway // lkdocs.go — корпоративные действия по бумагам клиента и документы депозитария: // готовые выписки из учётной системы (U_OP_INF_EXTRACT) плюс заявки клиента на // документ, которые кабинет кладёт в схему lk и показывает сотрудникам. // // Кабинет не создаёт документы в учётной системе: заявка — это поручение сотруднику, // он готовит документ у себя и выкладывает клиенту. import ( "context" "database/sql" "fmt" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" "strings" "time" ) // LKCorpAction — корпоративное действие по бумаге клиента. type LKCorpAction struct { FixDate string `json:"fix_date,omitempty"` // дата фиксации OperDate string `json:"oper_date,omitempty"` // дата операции Kind string `json:"kind"` // расшифровка вида Code string `json:"code,omitempty"` // код вида в учёте ISIN string `json:"isin,omitempty"` Security string `json:"security,omitempty"` Note string `json:"note,omitempty"` } // caKindRu — вид корпоративного действия по коду учёта (коды ISO 20022). func caKindRu(code string) string { switch strings.ToUpper(strings.TrimSpace(code)) { case "DVCA": return "дивиденды" case "INTR": return "выплата купона" case "REDM": return "погашение" case "MCAL": return "досрочное погашение" case "PCAL": return "частичное погашение" case "BPUT": return "оферта: предъявление к выкупу" case "TEND": return "добровольное предложение о выкупе" case "MEET", "XMET", "OMET": return "собрание владельцев" case "CONV": return "конвертация" case "SPLF", "SPLR": return "дробление или консолидация" case "MRGR": return "реорганизация" case "EXOF": return "обмен бумаг" case "DRCA": return "выплата по депозитарной расписке" } return "корпоративное действие" } // lkCorpActions — ближайшие корпоративные действия по бумагам клиента. func lkCorpActions(ctx context.Context, s FancyMSSQLSettings, isins []string) ([]LKCorpAction, error) { if strings.TrimSpace(s.Server) == "" || len(isins) == 0 { return nil, nil } conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 25*time.Second) defer cancel() // Список ISIN подставляем параметрами: значения приходят из портфеля клиента. names := make([]string, 0, len(isins)) args := make([]any, 0, len(isins)) for i, isin := range isins { names = append(names, fmt.Sprintf("@p%d", i+1)) args = append(args, sql.Named(fmt.Sprintf("p%d", i+1), strings.ToUpper(strings.TrimSpace(isin)))) } q := fmt.Sprintf(fsql.Doc01, strings.Join(names, ",")) rows, err := conn.QueryContext(cctx, q, args...) if err != nil { return nil, err } defer rows.Close() var out []LKCorpAction for rows.Next() { var code, isin, name, offer string var fix, oper sql.NullTime if rows.Scan(&code, &isin, &name, &fix, &oper, &offer) != nil { continue } a := LKCorpAction{Code: strings.TrimSpace(code), Kind: caKindRu(code), ISIN: isin, Security: name, Note: strings.TrimSpace(offer)} if fix.Valid && fix.Time.Year() < 9000 { a.FixDate = fix.Time.Format("02.01.2006") } if oper.Valid && oper.Time.Year() > 2000 && oper.Time.Year() < 9000 { a.OperDate = oper.Time.Format("02.01.2006") } out = append(out, a) } return out, rows.Err() } // LKDocument — документ депозитария, готовый или заказанный клиентом. type LKDocument struct { ID int64 `json:"id,omitempty"` Date string `json:"date"` // дата готовности или заказа Title string `json:"title"` // что за документ Period string `json:"period,omitempty"` // за какой период Account string `json:"account,omitempty"` State string `json:"state"` // готов | в работе | отклонён Note string `json:"note,omitempty"` // комментарий депозитария Ordered bool `json:"ordered"` // заявка клиента (а не готовый документ учёта) Kind string `json:"kind,omitempty"` // вид заявки } // lkDocSchema — таблица заявок на документы (создаётся вместе со схемой lk). var lkDocSchema = fsql.Doc02 // lkDocKinds — виды документов, которые клиент может заказать в кабинете. var lkDocKinds = []string{ "Выписка по счёту депо", "Справка об остатках на дату", "Отчёт об операциях за период", "Справка о доходах", } // lkDocKindAllowed — вид заявки из списка (чужие значения не принимаем). func lkDocKindAllowed(kind string) bool { for _, k := range lkDocKinds { if k == kind { return true } } return false } // lkDocRequestAdd — заявка клиента на документ. func lkDocRequestAdd(ctx context.Context, rc *RuntimeConfig, contract, clientName, kind, from, to, account, comment string) error { if !lkDocKindAllowed(kind) { return fmt.Errorf("неизвестный вид документа") } db, err := lkDB(rc) if err != nil { return err } var fromP, toP any if t, err := time.Parse("2006-01-02", strings.TrimSpace(from)); err == nil { fromP = t } if t, err := time.Parse("2006-01-02", strings.TrimSpace(to)); err == nil { toP = t } _, err = db.ExecContext(ctx, fsql.Doc03, contract, clientName, kind, fromP, toP, strings.TrimSpace(account), strings.TrimSpace(comment)) return err } // lkDocRequests — заявки клиента на документы. func lkDocRequests(ctx context.Context, rc *RuntimeConfig, contract string) ([]LKDocument, error) { db, err := lkDB(rc) if err != nil { return nil, err } rows, err := db.QueryContext(ctx, fsql.Doc04, contract) if err != nil { return nil, err } defer rows.Close() var out []LKDocument for rows.Next() { var d LKDocument var created time.Time var from, to *time.Time var status string if err := rows.Scan(&d.ID, &created, &d.Kind, &from, &to, &d.Account, &status, &d.Note); err != nil { continue } d.Ordered, d.Title, d.Date = true, d.Kind, created.Format("02.01.2006") switch status { case "done": d.State = "готов" case "rejected": d.State = "отклонён" default: d.State = "в работе" } if from != nil && to != nil { d.Period = from.Format("02.01.2006") + " — " + to.Format("02.01.2006") } out = append(out, d) } return out, rows.Err() } // lkReadyDocuments — готовые выписки по счетам клиента из учётной системы. func lkReadyDocuments(ctx context.Context, s FancyMSSQLSettings, contract string) ([]LKDocument, error) { if strings.TrimSpace(s.Server) == "" { return nil, nil } ids, _, accounts, err := depoClientIDs(ctx, s, contract) if err != nil || len(ids) == 0 { return nil, err } conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 25*time.Second) defer cancel() q := fmt.Sprintf(fsql.Doc05, inClause(ids)) rows, err := conn.QueryContext(cctx, q) if err != nil { return nil, err } defer rows.Close() var out []LKDocument for rows.Next() { var rd, bd, ed sql.NullTime var num string var acc int64 var state, turns sql.NullInt64 if rows.Scan(&rd, &num, &bd, &ed, &acc, &state, &turns) != nil { continue } d := LKDocument{Title: "Выписка по счёту депо", Account: accounts[acc], State: docStateRu(state, sql.NullTime{}), Note: num} if turns.Valid && turns.Int64 == 1 { d.Title = "Выписка по счёту депо с оборотами" } if rd.Valid { d.Date = rd.Time.Format("02.01.2006") } if bd.Valid && ed.Valid { d.Period = bd.Time.Format("02.01.2006") + " — " + ed.Time.Format("02.01.2006") } else if ed.Valid { d.Period = "на " + ed.Time.Format("02.01.2006") } out = append(out, d) } return out, rows.Err() } // notifyDocRequest — сообщить сотрудникам депозитария, что клиент заказал документ. func notifyDocRequest(rc *RuntimeConfig, u LKUser, kind, from, to string) { period := "" if from != "" || to != "" { period = fmt.Sprintf(" за период %s — %s", pick(from, "…"), pick(to, "…")) } go raiseSystemAlert(rc, "Кабинет: заказан документ", fmt.Sprintf("Клиент %s (договор %s) заказал документ «%s»%s. Заявка видна в кабинете клиента и в журнале lk.doc_requests.", pick(u.FullName, u.Login), u.Contract, kind, period)) }