#Синхронизация с 1С/ERP

🔄 Сложность: advanced | Скоупы: crm | Стек: Node.js, node-cron

#Что делаем

Реализуем двустороннюю синхронизацию контактов между Bitrix24 CRM и внешней ERP-системой (1С или аналог). Скрипт сравнивает данные по ключевому полю (ИНН или email), создаёт недостающие записи в обоих системах и обновляет изменённые поля. Синхронизация запускается по расписанию и использует Entity API для оптимальной работы. Это обеспечивает единую базу клиентов без ручного ввода.

#Необходимо

  • API-ключ Вайбкод с правами crm
  • Node.js 18+
  • Пакет: node-cron
  • Доступ к API внешней ERP-системы (в примере используется mock-заглушка)

#Полный код

javascript
// erp-sync.js
// Двусторонняя синхронизация контактов Bitrix24 <-> 1С/ERP

import cron from 'node-cron';

const API_KEY = process.env.VIBE_API_KEY;
const BASE_URL = process.env.VIBE_BASE_URL;

const HEADERS = {
  'X-Api-Key': API_KEY,
  'Content-Type': 'application/json',
};

// ============================================================
// Mock: имитация данных из 1С/ERP
// В реальном проекте замените на запросы к API вашей ERP-системы
// ============================================================
function fetchErpContacts() {
  return [
    {
      id: 'erp-001',
      name: 'Иван',
      lastName: 'Петров',
      email: 'petrov@example.com',
      phone: '+79001234567',
      inn: '7701234567',
      company: 'ООО Ромашка',
      updatedAt: new Date('2026-03-15'),
    },
    {
      id: 'erp-002',
      name: 'Мария',
      lastName: 'Сидорова',
      email: 'sidorova@example.com',
      phone: '+79009876543',
      inn: '7709876543',
      company: 'ИП Сидорова',
      updatedAt: new Date('2026-03-16'),
    },
    {
      id: 'erp-003',
      name: 'Алексей',
      lastName: 'Козлов',
      email: 'kozlov@example.com',
      phone: '+79005551234',
      inn: '7705551234',
      company: 'АО Берёзка',
      updatedAt: new Date('2026-03-17'),
    },
  ];
}

function createErpContact(contact) {
  console.log(`   [1С] Создан контакт: ${contact.name} ${contact.lastName}`);
  return { id: `erp-${Date.now()}` };
}

function updateErpContact(erpId, fields) {
  console.log(`   [1С] Обновлён контакт ${erpId}: ${JSON.stringify(fields)}`);
}
// ============================================================

// Загружаем все контакты из Bitrix24 через Entity API
async function fetchBitrixContacts() {
  const allContacts = [];
  let offset = 0;
  const limit = 200;

  while (true) {
    const response = await fetch(
      `${BASE_URL}/v1/contacts?limit=${limit}&offset=${offset}&select=id,name,lastName,email,phone,ufCrmInn,companyTitle,updatedAt`,
      { headers: { 'X-Api-Key': API_KEY } }
    );

    const { data } = await response.json();
    allContacts.push(...(data || []));

    if (!data || data.length < limit) break;
    offset += limit;
  }

  return allContacts;
}

// Сопоставляем контакты по ИНН (основной ключ) или email (запасной)
function matchContacts(bitrixContacts, erpContacts) {
  const matched = [];      // совпавшие пары
  const onlyInBitrix = []; // есть только в Bitrix24
  const onlyInErp = [];    // есть только в 1С

  const bitrixByInn = new Map();
  const bitrixByEmail = new Map();

  for (const bc of bitrixContacts) {
    const inn = bc.ufCrmInn;
    const email = bc.email?.[0]?.VALUE;
    if (inn) bitrixByInn.set(inn, bc);
    if (email) bitrixByEmail.set(email.toLowerCase(), bc);
  }

  const matchedBitrixIds = new Set();

  for (const erp of erpContacts) {
    const bitrixMatch = bitrixByInn.get(erp.inn) || bitrixByEmail.get(erp.email?.toLowerCase());

    if (bitrixMatch) {
      matched.push({ bitrix: bitrixMatch, erp });
      matchedBitrixIds.add(bitrixMatch.id);
    } else {
      onlyInErp.push(erp);
    }
  }

  for (const bc of bitrixContacts) {
    if (!matchedBitrixIds.has(bc.id)) {
      onlyInBitrix.push(bc);
    }
  }

  return { matched, onlyInBitrix, onlyInErp };
}

// Создаём контакт в Bitrix24 через Entity API
async function createBitrixContact(erpContact) {
  const response = await fetch(`${BASE_URL}/v1/contacts`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({
      name: erpContact.name,
      lastName: erpContact.lastName,
      email: [{ VALUE: erpContact.email, VALUE_TYPE: 'WORK' }],
      phone: [{ VALUE: erpContact.phone, VALUE_TYPE: 'WORK' }],
      ufCrmInn: erpContact.inn,
      companyTitle: erpContact.company,
      sourceId: 'OTHER',
      sourceDescription: 'Импорт из 1С/ERP',
    }),
  });

  const { data } = await response.json();
  console.log(`   [B24] Создан контакт: ${erpContact.name} ${erpContact.lastName} → ID: ${data.id}`);
  return data;
}

// Обновляем изменённые поля через Entity API
async function syncMatchedContact(bitrix, erp) {
  const updates = {};
  let hasChanges = false;

  if (bitrix.name !== erp.name) {
    updates.name = erp.name;
    hasChanges = true;
  }
  if (bitrix.lastName !== erp.lastName) {
    updates.lastName = erp.lastName;
    hasChanges = true;
  }

  if (hasChanges) {
    await fetch(`${BASE_URL}/v1/contacts/${bitrix.id}`, {
      method: 'PATCH',
      headers: HEADERS,
      body: JSON.stringify(updates),
    });
    console.log(`   [B24] Обновлён контакт #${bitrix.id}: ${JSON.stringify(updates)}`);
  }

  return hasChanges;
}

// Основная функция синхронизации
async function synchronize() {
  const startTime = Date.now();
  console.log(`\n${'='.repeat(60)}`);
  console.log(`🔄 Синхронизация: ${new Date().toLocaleString('ru-RU')}`);
  console.log('='.repeat(60));

  // Загружаем данные из обоих источников
  console.log('\n📥 Загрузка данных...');
  const [bitrixContacts, erpContacts] = await Promise.all([
    fetchBitrixContacts(),
    Promise.resolve(fetchErpContacts()),
  ]);

  console.log(`   Bitrix24: ${bitrixContacts.length} контактов`);
  console.log(`   1С/ERP:   ${erpContacts.length} контактов`);

  // Сопоставляем
  console.log('\n🔍 Сопоставление...');
  const { matched, onlyInBitrix, onlyInErp } = matchContacts(bitrixContacts, erpContacts);

  console.log(`   Совпадений: ${matched.length}`);
  console.log(`   Только в B24: ${onlyInBitrix.length}`);
  console.log(`   Только в 1С: ${onlyInErp.length}`);

  let created = 0;
  let updated = 0;

  // Создаём недостающие контакты в Bitrix24
  if (onlyInErp.length > 0) {
    console.log('\n➕ Создание в Bitrix24...');
    for (const erp of onlyInErp) {
      await createBitrixContact(erp);
      created++;
    }
  }

  // Создаём недостающие контакты в 1С
  if (onlyInBitrix.length > 0) {
    console.log('\n➕ Создание в 1С/ERP...');
    for (const bc of onlyInBitrix) {
      createErpContact(bc);
      created++;
    }
  }

  // Обновляем совпавшие записи
  if (matched.length > 0) {
    console.log('\n🔄 Обновление совпавших...');
    for (const { bitrix, erp } of matched) {
      const changed = await syncMatchedContact(bitrix, erp);
      if (changed) updated++;
    }
  }

  // Итоги
  const duration = ((Date.now() - startTime) / 1000).toFixed(1);
  console.log(`\n${'─'.repeat(60)}`);
  console.log(`📊 Итоги: создано ${created}, обновлено ${updated}, время ${duration}с`);
  console.log('─'.repeat(60));
}

// Запуск по расписанию: каждый час
console.log('🔄 Сервис синхронизации с 1С/ERP запущен');
console.log('   Расписание: каждый час');

// Первый запуск сразу
synchronize().catch(console.error);

// Далее по крону
cron.schedule('0 * * * *', () => {
  synchronize().catch(console.error);
});

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

  1. Сервис параллельно загружает контакты из Bitrix24 через Entity API (GET /v1/contacts с авто-пагинацией через limit/offset) и из внешней ERP-системы.
  2. Контакты сопоставляются по ИНН как основному ключу и по email как запасному. В результате формируются три группы: совпавшие, только в Bitrix24, только в ERP.
  3. Контакты, которые есть только в ERP, создаются в Bitrix24 через Entity API (POST /v1/contacts) с пометкой об источнике.
  4. Контакты, которые есть только в Bitrix24, создаются во внешней системе (в примере — mock-функция).
  5. Для совпавших записей сравниваются поля, и при расхождении обновляется Bitrix24 через Entity API (PATCH /v1/contacts/:id).
  6. Синхронизация запускается каждый час через node-cron и при старте сервиса.

#Что можно улучшить

  • Заменить mock-функции ERP на реальные запросы к API 1С (например, через OData)
  • Добавить журнал синхронизации в базу данных для отслеживания истории изменений
  • Реализовать conflict resolution: кто главнее при расхождении — Bitrix24 или ERP
  • Использовать POST /v1/contacts/batch для создания/обновления контактов пачками по 500 штук