package lkgateway import ( "context" "database/sql" "errors" "fmt" "strings" "time" ) // Коннектор bj → QUIK МНП, ПИШУЩАЯ сторона M2M (регистрация/подпись/снятие). // Читающая часть (Export, справочники) — в quikdb.go. // // Жизненный цикл вывода ЦБ (M2M), по «Перевод ЦБ в MOEX МОСТ» v1.1: // // 1. quikRegisterWithdrawal — sp_NewInstrSec (@InstructionType=9, @InstrTypeID=100, // @CheckType=1): QUIK проверяет достаточность и БЛОКИРУЕТ бумаги, возвращает @ActionID. // 2. quikActionResult — GetActionResult(@ActionID): QueryStatus (2 исполнено / 3 отвергнуто / // 4 не обработано / 5 проверяется), ReplyText, InstrNo (= Number поручения). // 3. читаем Export (quikListM2MInstructions) → Number активного поручения → в НРД. // 4а. НРД подтвердил → quikSignWithdrawal (sp_SignInstrSec, @SignFlag=16 «Депозитарий», // @Volume=NULL): статус «Исполнено», блокировка снята, бумаги списаны. // 4б. НРД отказал → quikDenyInstruction (sp_DenyInstr, @CancelText=причина): снятие, // блокировка снимается автоматически. // 5. приём от др. брокера → quikRegisterDeposit (sp_NewInstrSec, @InstructionType=8, // @InstrTypeID=101, @InSecCode=NULL, @ISIN): регистрируется И сразу исполняется. // // БЕЗОПАСНОСТЬ: БД БОЕВАЯ. Все пишущие вызовы идут с @CheckType=1 (реальная // транзакция на сервер QUIK) и защищены предохранителем quikWritesAllowed — // пока типы 100/101 выключены (Disabled=y) и нет тестового клиента, писать нельзя. // Идентификаторы M2M на стороне QUIK (фиксированы инструкцией + наши данные). const ( quikTypeWithdrawM2M = 100 // InstrTypeID «Вывод ЦБ М2М» quikTypeDepositM2M = 101 // InstrTypeID «Ввод ЦБ М2М» quikBaseTypeWithdraw = 9 // базовый Type для вывода (Поставка ЦБ из ДКК) quikBaseTypeDeposit = 8 // базовый Type для ввода (Получение ЦБ в ДКК) quikSecWithdraw = "RUROUTM2M" // МНП-инструмент вывода quikSecDeposit = "RURINM2M" // МНП-инструмент ввода quikSignFlagDepo = 16 // FLG_DEPO — подпись «Депозитарий» quikCheckViaAPI = 1 // @CheckType=1 — через QuikAPI (проверка+блокировка) ) // errQuikWritesDisabled — предохранитель: пишущие операции запрещены, пока // администратор не завершил настройку в QAdmin (классы/типы) и не выделен // явное разрешение записи. Снимается через quikWritesAllowed. var errQuikWritesDisabled = errors.New("quik: пишущие операции M2M отключены (нужны: включение типов 100/101 в QAdmin, классы INSTROUTM2M/INSTRINM2M, разрешение записи в МНП в настройках bj)") // quikWritesAllowed — глобальный предохранитель пишущих вызовов к боевой БД QUIK. // По умолчанию false. Включается осознанно (флаг/настройка), когда контур готов. var quikWritesAllowed = false // QuikWithdrawParams — параметры регистрации поручения «Вывод ЦБ (M2M)». type QuikWithdrawParams struct { ExtID string // наш идентификатор заявки (связка с bj_transfer_requests.guid) ClientID string // код клиента в QUIK ISIN string // ISIN выводимой бумаги Quantity float64 // количество (в ШТУКАХ — с 21.4.0) Account string // торговый счёт клиента DepoAccount string // счёт депо клиента (составной, при необходимости) RecipientINN string // ИНН депозитария-получателя (из M2MDepoRecipients) PassExpenses bool // передавать информацию о расходах (наш CostInfo) QtyScale int // масштаб дробного количества (0 = целые) } // QuikActionResult — итог обработки команды Модулем (GetActionResult / Actions). type QuikActionResult struct { QueryStatus int // 1 новая, 2 исполнена, 3 отвергнута, 4 не обработана, 5 проверяется ReplyText string // текст ответа/ошибки InstrNo string // номер поручения (= Export.Number) } // quikStatusResultName — расшифровка QueryStatus (GetActionResult). func quikStatusResultName(v int) string { switch v { case 1: return "новая" case 2: return "исполнена" case 3: return "отвергнута" case 4: return "не может быть обработана" case 5: return "проверяется на сервере QUIK" } return fmt.Sprintf("код %d", v) } // quikRegisterWithdrawal регистрирует «Вывод ЦБ (M2M)» — QUIK проверяет // достаточность и блокирует бумаги. Возвращает @ActionID для GetActionResult. // НЕ вызывать на боевой БД, пока не снят предохранитель quikWritesAllowed. func quikRegisterWithdrawal(ctx context.Context, s QuikMNPSettings, p QuikWithdrawParams) (int64, error) { if !quikWritesAllowed { return 0, errQuikWritesDisabled } if strings.TrimSpace(p.ClientID) == "" || strings.TrimSpace(p.ISIN) == "" || p.Quantity <= 0 { return 0, errors.New("quik: не заданы ClientID/ISIN/Quantity для вывода") } conn, err := quikOpen(s) if err != nil { return 0, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() var actionID int64 // @QuikClassCode=NULL (обязательно при @CheckType=1 для чужой фирмы) → поиск // инструмента по @ISIN; @InSecCode=NULL; количество в штуках (@QtyScale). // @QuikClassCode и @QtyScale вместе задавать НЕЛЬЗЯ — при непустом ISIN оставляем QtyScale. _, err = conn.ExecContext(cctx, "sp_NewInstrSec", sql.Named("ActionID", sql.Out{Dest: &actionID}), sql.Named("ExtID", nullStr(p.ExtID)), sql.Named("ClientID", p.ClientID), sql.Named("InstructionType", quikBaseTypeWithdraw), sql.Named("InstrTypeID", quikTypeWithdrawM2M), sql.Named("Volume", p.Quantity), sql.Named("OutSecCode", quikSecWithdraw), sql.Named("InSecCode", nil), sql.Named("QuikClassCode", nil), sql.Named("ISIN", p.ISIN), sql.Named("QtyScale", nullInt(p.QtyScale)), sql.Named("Account", nullStr(p.Account)), sql.Named("QuikFirm", quikFirmID()), sql.Named("FirmID", quikFirmID()), sql.Named("CanSplit", 0), sql.Named("BlockS", p.Quantity), sql.Named("M2MDepoRecipientINN", nullStr(p.RecipientINN)), sql.Named("PassExpensesInfo", p.PassExpenses), sql.Named("DepoAccount_1", nullStr(p.DepoAccount)), sql.Named("CheckType", quikCheckViaAPI), ) if err != nil { return 0, fmt.Errorf("quik: sp_NewInstrSec (вывод): %s", quikErrorDetail(err)) } return actionID, nil } // quikRegisterDeposit регистрирует «Ввод ЦБ (M2M)» — зачисление бумаг от другого // брокера. Поиск инструмента по @ISIN (@InSecCode=NULL). Регистрируется и сразу // исполняется (у типа «Ввод M2M» подписей нет). func quikRegisterDeposit(ctx context.Context, s QuikMNPSettings, clientID, isin, account string, qty float64) (int64, error) { if !quikWritesAllowed { return 0, errQuikWritesDisabled } if strings.TrimSpace(clientID) == "" || strings.TrimSpace(isin) == "" || qty <= 0 { return 0, errors.New("quik: не заданы ClientID/ISIN/qty для ввода") } conn, err := quikOpen(s) if err != nil { return 0, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() var actionID int64 _, err = conn.ExecContext(cctx, "sp_NewInstrSec", sql.Named("ActionID", sql.Out{Dest: &actionID}), sql.Named("ClientID", clientID), sql.Named("InstructionType", quikBaseTypeDeposit), sql.Named("InstrTypeID", quikTypeDepositM2M), sql.Named("Volume", qty), sql.Named("OutSecCode", quikSecDeposit), sql.Named("InSecCode", nil), sql.Named("QuikClassCode", nil), sql.Named("ISIN", isin), sql.Named("Account", nullStr(account)), sql.Named("QuikFirm", quikFirmID()), sql.Named("FirmID", quikFirmID()), sql.Named("CanSplit", 0), sql.Named("BlockS", qty), sql.Named("CheckType", quikCheckViaAPI), ) if err != nil { return 0, fmt.Errorf("quik: sp_NewInstrSec (ввод): %s", quikErrorDetail(err)) } return actionID, nil } // quikSignWithdrawal подписывает (исполняет) поручение на вывод после подтверждения // НРД: статус → «Исполнено», блокировка снимается, бумаги списываются. // @Volume ВСЕГДА NULL при подписи; @SignFlag=16 (подпись «Депозитарий»). func quikSignWithdrawal(ctx context.Context, s QuikMNPSettings, queryNo float64, clientID string) (int64, error) { if !quikWritesAllowed { return 0, errQuikWritesDisabled } conn, err := quikOpen(s) if err != nil { return 0, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() var actionID int64 _, err = conn.ExecContext(cctx, "sp_SignInstrSec", sql.Named("ActionID", sql.Out{Dest: &actionID}), sql.Named("QueryNo", queryNo), sql.Named("ClientID", nullStr(clientID)), sql.Named("Volume", nil), // при подписи всегда NULL sql.Named("IntSecCode", quikSecWithdraw), sql.Named("FirmID", quikFirmID()), sql.Named("SignFlag", quikSignFlagDepo), sql.Named("CheckType", quikCheckViaAPI), ) if err != nil { return 0, fmt.Errorf("quik: sp_SignInstrSec: %s", quikErrorDetail(err)) } return actionID, nil } // quikDenyInstruction снимает поручение с указанием причины (отказ НРД): блокировка // снимается автоматически, клиент видит причину в терминале. func quikDenyInstruction(ctx context.Context, s QuikMNPSettings, queryNo float64, cancelText string) (int64, error) { if !quikWritesAllowed { return 0, errQuikWritesDisabled } if strings.TrimSpace(cancelText) == "" { cancelText = "Отказ по результату перевода M2M" } conn, err := quikOpen(s) if err != nil { return 0, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() var actionID int64 _, err = conn.ExecContext(cctx, "sp_DenyInstr", sql.Named("ActionID", sql.Out{Dest: &actionID}), sql.Named("QueryNo", queryNo), sql.Named("CancelText", cancelText), sql.Named("CheckType", quikCheckViaAPI), ) if err != nil { return 0, fmt.Errorf("quik: sp_DenyInstr: %s", quikErrorDetail(err)) } return actionID, nil } // quikActionResult читает итог обработки команды Модулем (read-only). Безопасно // на боевой БД — только SELECT из результата GetActionResult / таблицы Actions. func quikActionResult(ctx context.Context, s QuikMNPSettings, actionID int64) (QuikActionResult, error) { var r QuikActionResult conn, err := quikOpen(s) if err != nil { return r, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() // Читаем из таблицы Actions по ActionID (эквивалент GetActionResult). err = conn.QueryRowContext(cctx, `SELECT ISNULL(QueryStatus,0), ISNULL(ReplyText,''), ISNULL(CAST(InstrNo AS varchar(32)),'') FROM Actions WHERE ActionID=@p1`, actionID). Scan(&r.QueryStatus, &r.ReplyText, &r.InstrNo) if err != nil { return r, fmt.Errorf("quik: чтение Actions #%d: %s", actionID, quikErrorDetail(err)) } return r, nil } // nullStr → NULL для пустой строки (иначе значение). func nullStr(v string) any { if strings.TrimSpace(v) == "" { return nil } return v } // nullInt → NULL для нуля (иначе значение). Для @QtyScale: 0 = не задавать. func nullInt(v int) any { if v == 0 { return nil } return v } // quikFirmID — код фирмы в QUIK (таблица Firms) = код участника нашей организации. func quikFirmID() string { if globalRC == nil { return "" } return orgCode(globalRC.Snapshot()) }