package lkgateway // lkhistory.go — история для кабинета клиента: движения по счетам депо и выплаты дохода // с удержанным налогом. Источник — учётная система депозитария (DEPO): // // операции: OD_DOCS + U_OP_INV_ZACH / U_OP_INV_SPIS / U_OP_INV_PEREVOD (связь по DEPO = OD_CLIENTS.ID); // доходы: U_OP_INF_INCOME + U_OP_INF_INCOME_DOL (связь по DEPO или INCOME_FACE); // налог: OD_NDFL по лицу. // // Кабинет ничего не считает сам: суммы и статусы показываются как в учёте. import ( "context" "database/sql" "fmt" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" "strings" "time" ) // LKOperation — движение по счёту депо. type LKOperation struct { Date string `json:"date"` // дата регистрации, ДД.ММ.ГГГГ DoneAt string `json:"done_at,omitempty"` // дата проведения Kind string `json:"kind"` // зачисление | списание | перевод между счетами Number string `json:"number,omitempty"` // номер документа депозитария Account string `json:"account,omitempty"` // счёт депо ISIN string `json:"isin,omitempty"` Security string `json:"security,omitempty"` Quantity float64 `json:"quantity"` // со знаком: списание отрицательное State string `json:"state"` // исполнено | в обработке | отменено Note string `json:"note,omitempty"` // комментарий или причина отказа } // LKIncome — выплата дохода клиенту. type LKIncome struct { Date string `json:"date"` Kind string `json:"kind"` // купон | дивиденды | доход ISIN string `json:"isin,omitempty"` Security string `json:"security,omitempty"` Quantity float64 `json:"quantity,omitempty"` // бумаг в расчёте Accrued float64 `json:"accrued"` // начислено Tax float64 `json:"tax"` // удержан налог Paid float64 `json:"paid"` // к выплате State string `json:"state,omitempty"` Account string `json:"account,omitempty"` } // depoClientIDs — внутренние идентификаторы счетов депо клиента и его лицо в DEPO. func depoClientIDs(ctx context.Context, s FancyMSSQLSettings, contract string) (ids []int64, face int64, accounts map[int64]string, err error) { conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil, 0, nil, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() rows, err := conn.QueryContext(cctx, fsql.Hist01, contract, likeEscape(contract)+" %") if err != nil { return nil, 0, nil, err } defer rows.Close() accounts = map[int64]string{} for rows.Next() { var id, f int64 var num string if rows.Scan(&id, &num, &f) != nil { continue } ids = append(ids, id) accounts[id] = num if face == 0 { face = f } } return ids, face, accounts, rows.Err() } // docStateRu — состояние документа депозитария словами. func docStateRu(state sql.NullInt64, proc sql.NullTime) string { switch { case state.Valid && state.Int64 == 2: return "исполнено" case state.Valid && state.Int64 == 3: return "отменено" case proc.Valid: return "исполнено" } return "в обработке" } // inClause — список идентификаторов для IN (...) — значения свои, не от пользователя. func inClause(ids []int64) string { parts := make([]string, 0, len(ids)) for _, id := range ids { parts = append(parts, fmt.Sprint(id)) } return strings.Join(parts, ",") } // parseRange — период запроса: пустые значения — последний год по сегодня. func parseRange(from, to string) (time.Time, time.Time) { fromT, err := time.Parse("2006-01-02", strings.TrimSpace(from)) if err != nil { fromT = time.Now().AddDate(-1, 0, 0) // по умолчанию — последний год } toT, err := time.Parse("2006-01-02", strings.TrimSpace(to)) if err != nil { toT = time.Now() } return time.Date(fromT.Year(), fromT.Month(), fromT.Day(), 0, 0, 0, 0, time.UTC), time.Date(toT.Year(), toT.Month(), toT.Day(), 0, 0, 0, 0, time.UTC) } // lkOperations — движения по счетам депо клиента за период (from/to — ГГГГ-ММ-ДД, можно пустыми). func lkOperations(ctx context.Context, s FancyMSSQLSettings, contract, from, to string, limit int) ([]LKOperation, error) { if strings.TrimSpace(s.Server) == "" { return nil, fmt.Errorf("учётная система депозитария не подключена") } ids, _, accounts, err := depoClientIDs(ctx, s, contract) if err != nil { return nil, err } if len(ids) == 0 { return nil, nil } conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() if limit <= 0 || limit > 500 { limit = 200 } fromT, toT := parseRange(from, to) // Три вида операций в одном списке: зачисление, списание, перевод между счетами. q := fmt.Sprintf(fsql.Hist02, limit, inClause(ids), inClause(ids), inClause(ids), inClause(ids)) rows, err := conn.QueryContext(cctx, q, fromT, toT) if err != nil { return nil, err } defer rows.Close() var out []LKOperation for rows.Next() { var rd, pd sql.NullTime var num, kind, isin, sec, note string var st sql.NullInt64 var acc int64 var qty float64 if rows.Scan(&rd, &pd, &num, &st, &kind, &acc, &isin, &sec, &qty, ¬e) != nil { continue } op := LKOperation{Kind: kind, Number: num, ISIN: isin, Security: sec, Quantity: qty, Account: accounts[acc], State: docStateRu(st, pd), Note: strings.TrimSpace(note)} if rd.Valid { op.Date = rd.Time.Format("02.01.2006") } if pd.Valid && pd.Time.Year() < 9000 { op.DoneAt = pd.Time.Format("02.01.2006") } out = append(out, op) } return out, rows.Err() } // incomeKindRu — вид дохода из учёта. func incomeKindRu(kind sql.NullInt64) string { switch { case !kind.Valid: return "доход" case kind.Int64 == 1: return "дивиденды" case kind.Int64 == 2: return "купон" case kind.Int64 == 3: return "погашение" } return "доход" } // lkIncome — выплаты дохода клиенту за период и удержанный налог по данным учёта. func lkIncome(ctx context.Context, s FancyMSSQLSettings, contract, from, to string) (rowsOut []LKIncome, taxTotal float64, err error) { if strings.TrimSpace(s.Server) == "" { return nil, 0, fmt.Errorf("учётная система депозитария не подключена") } ids, face, accounts, err := depoClientIDs(ctx, s, contract) if err != nil { return nil, 0, err } if len(ids) == 0 { return nil, 0, nil } conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil, 0, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() fromT, toT := parseRange(from, to) q := fmt.Sprintf(fsql.Hist03, inClause(ids)) rows, err := conn.QueryContext(cctx, q, fromT, toT, face) if err != nil { return nil, 0, err } defer rows.Close() for rows.Next() { var pd sql.NullTime var kind sql.NullInt64 var isin, sec, status string var qty, accrued, tax, paid float64 var acc int64 if rows.Scan(&pd, &kind, &isin, &sec, &qty, &accrued, &tax, &paid, &status, &acc) != nil { continue } in := LKIncome{Kind: incomeKindRu(kind), ISIN: isin, Security: sec, Quantity: qty, Accrued: accrued, Tax: tax, Paid: paid, Account: accounts[acc]} if st := strings.TrimSpace(status); st != "" && st != "0" { // «0» — служебное значение учёта in.State = st } if pd.Valid { in.Date = pd.Time.Format("02.01.2006") } rowsOut = append(rowsOut, in) } if err := rows.Err(); err != nil { return rowsOut, 0, err } // Удержанный налог по лицу за период — по журналу НДФЛ депозитария. if face > 0 { _ = conn.QueryRowContext(cctx, fsql.Hist04, sql.Named("p1", face), sql.Named("p2", fromT), sql.Named("p3", toT)).Scan(&taxTotal) } return rowsOut, taxTotal, nil }