Перейти к основному контенту

Справочники в Pyrus

Из Метабот можно передавать справочники в Pyrus. Для этого необходимо сделать интеграцию с PyrusApi.

Настройка таблиц

Для работы интеграции в боте необходимо создать таблицу pyrus_task_bindings:

image.png

В плагине для интеграции будут использоваться поля pyrus_task_id и channel_id.

Настройка интеграции

Шаги подключения интеграции:

  • Добавьте в ваш бизнес плагин Business.Integrations.PyrusApi со следующим кодом:
{
    _apiBase: 'https://api.pyrus.com/v4',
    _authUrl: 'https://accounts.pyrus.com/api/v4/auth',
    set apiBase(value) {
      this._apiBase = value
    },
    get apiBase() {
      return this._apiBase
    },
    set authUrl(value) {
      this._authUrl = value
    },
    get authUrl() {
      return this._authUrl
    },
  
    /**
     * HTTP-запрос к Pyrus API с Bearer-токеном.
     * @param {string} method - GET, POST, etc.
     * @param {string} uri - полный URI или путь относительно API_BASE
     * @param {Object|string|null} [body=null] - тело запроса (объект будет JSON.stringify)
     * @returns {Object|null} - разобранный JSON ответ или null при ошибке
     */
    request: function(method, uri, body = null) {
      const url = uri.startsWith('http') ? uri : this._apiBase + (uri.startsWith('/') ? uri : '/' + uri)
      const token = this.getToken()
      if (!token) return null
      const headers = {
        'Authorization': 'Bearer ' + token,
        'Content-Type': 'application/json'
      }
      
      try {
        
        api.request(method, url, [], [], null, body, headers)
        const code = api.getLastResponseCode()
        if (code === 200) {
          const content = api.getLastResponseContent()
          return content ? JSON.parse(content) : null
        }

        return false
      } catch (e) {
        debug(e.stack)
        return null
      }
    },
  
    /**
     * Получает токен из атрибутов бота (при необходимости можно вызвать refreshToken).
     * @returns {string|null}
     */
    getToken: function() {
      return bot.getAttr('pyrus_api_token') || null
    },
  
    /**
     * Обновляет токен через POST /api/v4/auth, сохраняет в bot.setAttr('pyrus_api_token', ...).
     * @returns {boolean} - успех
     */
    refreshToken: function() {
      const login = bot.getAttr('pyrus_api_login')
      const securityKey = bot.getAttr('pyrus_api_security_key')
      if (!login || !securityKey) {
        debug("Нет токенов")
        return false
      }
      const headers = { 'Content-Type': 'application/json' }
      try {
        api.request('POST', this._authUrl, [], [], null, { login: login, security_key: securityKey }, headers)
        if (api.getLastResponseCode() !== 200) return false
        const content = api.getLastResponseContent()
        const data = content ? JSON.parse(content) : null
        const token = data?.access_token || data?.token
        if (token) {
          bot.setAttr('pyrus_api_token', token)
          return true
        }
        return false
      } catch (e) {
        
        debug(e.stack)
        return false
      }
    },
  
    /**
     * Ищет task_id в таблице pyrus_task_bindings по channel_id.
     * @param {string} channelId
     * @returns {number|null} - pyrus_task_id или null
     */
    getTaskIdByChannelId: function(channelId) {
      if (!channelId) return null
      const rows = table.find('pyrus_task_bindings', [], [
        ['channel_id', '=', channelId]
      ])
      const first = rows?.[0]
      const id = first?.pyrus_task_id
      return id != null ? Number(id) : null
    },

    /**
     * Достаёт lead id из маппинга полей (поле MetabotLeadId).
     * @param {Array} mappings - массив { code, value }
     * @returns {number|null}
     */
    getLeadIdByMapFields: function(mappings) {
      if (!Array.isArray(mappings)) return null
      const item = mappings.find(m => m?.code === 'MetabotLeadId')
      if (!item || item.value == null) return null
      const raw = String(item.value).split(',')[0].trim()
      const id = parseInt(raw, 10)
      return Number.isNaN(id) ? null : id
    },

    /**
     * Определяет lead id по channel_id (формат "lead_<id>_dialog_...") или из маппинга.
     * @param {string} channelIdStr - строка channel_id
     * @param {Array} [mappingsFallback] - маппинг полей для fallback (getLeadIdByMapFields)
     * @returns {number|null}
     */
    getLeadIdByChannelId: function(channelIdStr, mappingsFallback) {
      if (channelIdStr && typeof channelIdStr === 'string') {
        const parts = channelIdStr.split('_')
        if (parts.length >= 2) {
          const id = parseInt(parts[1], 10)
          if (!Number.isNaN(id)) return id
        }
      }
      return this.getLeadIdByMapFields(mappingsFallback || [])
    },

    /**
     * GET /v4/tasks/{taskId}
     * @param {number} taskId
     * @returns {Object|null} - задача с form_id и т.д.
     */
    getTask: function(taskId) {
      return this.request('GET', '/tasks/' + taskId)
    },
  
    /**
     * GET /v4/forms/{formId}
     * @param {number} formId
     * @returns {Object|null} - форма с полями (fields)
     */
    getForm: function(formId) {
      return this.request('GET', '/forms/' + formId)
    },
  
    /**
     * GET /v4/catalogs/{catalogId}
     * @param {number} catalogId
     * @returns {Object|null} - справочник с items
     */
    getCatalog: function(catalogId) {
      return this.request('GET', '/catalogs/' + catalogId)
    },
  
    /**
     * Рекурсивный поиск поля по коду (info.code) или имени (name) в структуре формы.
     * @param {Array|Object} fields - массив полей или объект с полем fields
     * @param {string} fieldName - код поля (info.code) или название (name)
     * @returns {Object|null} - поле с id, name, type, info.catalog_id
     */
    getFieldByName: function(fields, fieldName) {
      if (!fields || !fieldName) return null
      const list = Array.isArray(fields) ? fields : (fields.fields || fields.items || [])
      for (let i = 0; i < list.length; i++) {
        const f = list[i]
        if (f?.info?.code === fieldName || f?.name === fieldName) return f
        if (f?.type === 'approval' && f?.steps) {
          const inSteps = this.getFieldByName(f.steps, fieldName)
          if (inSteps) return inSteps
        }
        if (f?.fields?.length) {
          const nested = this.getFieldByName(f.fields, fieldName)
          if (nested) return nested
        }
      }
      return null
    },
  
    /**
     * Ищет item_id в справочнике по значению в указанном столбце.
     * @param {number} catalogId
     * @param {string} value - искомое значение
     * @param {string} [columnName] - имя столбца для сравнения (например "Name", "Наименование"). Если не задано, ищем по первому текстовому столбцу.
     * @returns {number|null} - item_id
     */
    getCatalogItemId: function(catalogId, value, columnName) {
      if (value == null || value === '') return null
      const catalog = this.getCatalog(catalogId)
      const items = catalog?.items || catalog?.catalog?.items
      if (!Array.isArray(items) || items.length === 0) return null
      const columns = catalog?.columns || catalog?.catalog?.columns || []
      const colName = columnName || (columns[0]?.name)
      const valueStr = String(value).trim().toLowerCase()
      for (let i = 0; i < items.length; i++) {
        const item = items[i]
        const itemId = item?.item_id ?? item?.id
        const cells = item?.cells || item?.values || []
        for (let c = 0; c < columns.length; c++) {
          const col = columns[c]
          if (colName && col?.name !== colName) continue
          const cell = cells[c] ?? cells[col?.id]
          const cellVal = (cell?.value != null ? cell.value : cell)
          if (cellVal != null && String(cellVal).trim().toLowerCase() === valueStr) {
            return itemId != null ? Number(itemId) : null
          }
        }
        if (!colName && cells?.length) {
          const firstVal = cells[0]?.value != null ? cells[0].value : cells[0]
          if (firstVal != null && String(firstVal).trim().toLowerCase() === valueStr) {
            return itemId != null ? Number(itemId) : null
          }
        }
      }
      return null
    },
  
    /**
     * POST /v4/tasks/{taskId}/comments с field_updates.
     * @param {number} taskId
     * @param {string} text - текст комментария
     * @param {Array} fieldUpdates - [{ id, value }, ...], value для справочника: { item_id: number }
     * @returns {Object|null}
     */
    updateTaskFields: function(taskId, text, fieldUpdates) {
      
      return this.request('POST', '/tasks/' + taskId + '/comments', {
        text: text || '',
        field_updates: Array.isArray(fieldUpdates) ? fieldUpdates : []
      })
    },
  
    /**
     * Строит массив field_updates по маппингу полей формы.
     * catalogMappings: [{ field_name|code, value, catalog_column? }, ...]
     * Для полей-справочников ищет item_id по value в каталоге, для остальных подставляет value как есть.
     * @param {Object|number} formOrFormId - объект формы с полем fields или id формы
     * @param {Array} catalogMappings
     * @returns {Array} - field_updates для updateTaskFields
     */
    buildFieldUpdates: function(formOrFormId, catalogMappings) {
      if (!Array.isArray(catalogMappings) || !catalogMappings.length) return []
      const isFormObject = formOrFormId != null && typeof formOrFormId === 'object' && Array.isArray(formOrFormId.fields)
      const formFields = isFormObject ? formOrFormId.fields : (() => {
        const form = this.getForm(formOrFormId)
        return form?.fields || form?.form?.fields || []
      })()
      const updates = []
      for (const m of catalogMappings) {
        const fieldName = m?.field_name || m?.code
        const value = m?.value
        if (fieldName == null || value == null) continue
        const field = this.getFieldByName(formFields, fieldName)
        if (!field?.id) continue
        const fieldType = (field.type || '').toLowerCase()
        const catalogId = field.info?.catalog_id ?? field.catalog_id
        if (catalogId && ['catalog', 'choice', 'lookup'].includes(fieldType)) {
          const col = m?.catalog_column
          if (Array.isArray(value)) {
            const itemIds = value
              .map(v => this.getCatalogItemId(catalogId, v, col))
              .filter(id => id != null)
            if (itemIds.length) updates.push({ id: field.id, value: { item_ids: itemIds } })
          } else {
            const itemId = this.getCatalogItemId(catalogId, value, col)
            if (itemId != null) updates.push({ id: field.id, value: { item_id: itemId } })
          }
        } else {
          updates.push({ id: field.id, value: value })
        }
      }
      return updates
    }
  }
  • В Атрибуты бота добавьте следующие системные переменные:
    • pyrus_api_login - логин аккаунта Pyrus
    • pyrus_api_security_key - ключ доступа к АПИ
    • pyrus_api_token - токен авторизации АПИ
    • refreshTokenTime - время для обновления pyrus_api_token (по-умолчанию поставьте 0)

image.png

  • В место где хотите передавать справочник добавьте код обновления заявки:
let pyrusApi = snippet("Business.Integrations.PyrusApi")
let task_id = pyrusApi.getTaskIdByChannelId(channelId)

let mappings = [{
              "code": "CustomCatalog1",
              "value": [lead.getAttr('Подотдел'), lead.getAttr('Структура')]
           }]

let now = new Date();
let refreshTokenTime = now.getTime() + (1000*60*60*24);

if (task_id) {
  if(bot.getIntAttr('refreshTokenTime') <= now.getTime()){
    bot.setAttr('refreshTokenTime', refreshTokenTime)
    pyrusApi.refreshToken()
  }
  let task = pyrusApi.getTask(task_id)
  let formId = task?.task?.form_id
  
  if (formId) {
    let form = pyrusApi.getForm(formId)
    // Получаем под капотом все данные полей и самостоятельно маппим
    let fieldUpdates = pyrusApi.buildFieldUpdates(form, mappings)
    if (fieldUpdates?.length) {
      pyrusApi.updateTaskFields(task_id, "Поля формы обновлены", fieldUpdates)
    }
  }
}

Разберем код:

  • В первой строке обращаемся к плагину PyrusApi.
  • Во второй забираем из таблицы заявок id заявки по id канала.
  • Объявляем передаваемые параметры в mappings. Поле code должно соответствовать коду поля справочника в форме в Pyrus, а value данным из справочника.

Второй способ передачи справочников:

{
  "code": "Тарифы", - По названию
  "value": "Стартовый" - Единичный выбор
}
  • Далее вычисляем пришло ли время обновления токена, если да, то обновляем и записываем новые данные в атрибуты бота.
  • В 17 строке находим задачу в Pyrus через АПИ.
  • В 21 находим форму в Pyrus через АПИ.
  • В 23-25 обновляем поля задачи.

Данный функционал работает для передачи любых полей формы