Рецепт: массовое обновление сделок

Переносит тысячи сделок CRM на новую стадию через BatchByChunkV2 — автоматическое разбиение на чанки, обработка частичных ошибок и одна строка прогресса.
Мы всё ещё дорабатываем эту страницу. Некоторых данных здесь может не хватать — скоро дополним.

Цель

Прочитать CSV-сопоставление dealId,newStageId и применить смену стадии к каждой строке. Может быть сотни сделок или десятки тысяч — BatchByChunkV2.make() разбивает вызовы на блоки по 50 на батч на стороне сервера, без ручной пагинации.

Стек и предпосылки

  • Node.js 20+, ESM
  • @bitrix24/b24jssdk@^1.0.0
  • Вебхук со scope crm, заданный как B24_HOOK.
  • CSV-файл migrations.csv:
    dealId,newStageId
    1234,WON
    1235,WON
    1240,LOSE
    
pnpm add @bitrix24/b24jssdk
export B24_HOOK="https://your-portal.bitrix24.com/rest/1/xxxxxxxxxxxxxxxx/"

Полный пример

scripts/migrate-deals.mjs:

import { readFile } from 'node:fs/promises'
import {
  B24Hook,
  EnumCrmEntityTypeId,
  ParamsFactory
} from '@bitrix24/b24jssdk'

const hookUrl = process.env.B24_HOOK
if (!hookUrl) {
  console.error('B24_HOOK is not set')
  process.exit(2)
}

const csv = await readFile(process.argv[2] ?? 'migrations.csv', 'utf8')
const rows = csv
  .split(/\r?\n/)
  .slice(1) // заголовок
  .filter(Boolean)
  .map((line) => {
    const [dealId, newStageId] = line.split(',')
    return { dealId: Number(dealId), newStageId }
  })

console.log(`planned updates: ${rows.length}`)

const b24 = B24Hook.fromWebhookUrl(hookUrl, {
  restrictionParams: ParamsFactory.getBatchProcessing()
})

const calls = rows.map(({ dealId, newStageId }) => [
  'crm.item.update',
  {
    entityTypeId: EnumCrmEntityTypeId.deal,
    id: dealId,
    fields: { stageId: newStageId }
  }
])

const response = await b24.actions.v2.batchByChunk.make({
  calls,
  options: {
    isHaltOnError: false,
    requestId: `deal-stage-migration-${Date.now()}`
  }
})

if (!response.isSuccess) {
  console.error('partial failure:')
  for (const message of response.getErrorMessages()) {
    console.error(`  - ${message}`)
  }
  process.exit(1)
}

console.log(`updated: ${response.getData()?.length ?? 0} of ${rows.length}`)

Запуск

node scripts/migrate-deals.mjs migrations.csv
# planned updates: 3450
# updated: 3450 of 3450

Если несколько строк не пройдут (удалённая сделка, отсутствующий scope, неверная стадия), isHaltOnError: false позволяет остальной части батча завершиться, и скрипт выходит с кодом 1, перечислив сбои.

Как это работает

  • BatchByChunkV2.make({ calls }) принимает произвольное число команд и нарезает их внутри на батчи по 50 — жёсткий лимит на запрос. Порядок вывода совпадает с порядком ввода.
  • Result.isSuccess становится false, если в любом чанке была ошибка. getErrorMessages() возвращает по одному сообщению на каждый неудавшийся вызов. Успешные вызовы всё равно в getData() — частичный прогресс сохраняется.
  • ParamsFactory.getBatchProcessing() повышает порог времени операций и снижает частоту запросов, что держит очередь портала отзывчивой, пока долгая миграция идёт рядом с живым UI-трафиком.
  • Именование запроса через options.requestId упрощает фильтрацию логов на стороне портала и дедупликацию случайных перезапусков.

Ограничения

  • Требуется форма calls из кортежей-двоек: BatchByChunkV2 намеренно отклоняет команды в виде именованных объектов, потому что границы чанков разорвали бы имена.
  • Каждая команда учитывается в дневном бюджете вызовов. Для десятков тысяч обновлений запускайте скрипт в непиковые часы и подумайте о разбиении по ответственному пользователю.
  • crm.item.update Bitrix24 валидирует stageId относительно категории сделки. Опечатка в newStageId возвращает ошибку конкретного вызова — видна в getErrorMessages() — но весь скрипт не прерывается благодаря isHaltOnError: false.
  • Атомарный откат не поддерживается. Если нужно транзакционное поведение, сначала выгрузите текущие значения в отдельный CSV и запустите вторую миграцию для восстановления.

См. также