Битрикс24 и Node.js в 2025: инструкция по настройке

Подробная инструкция по интеграции Битрикс24 с Node.js через REST API. Настройка, примеры кода, работа с данными и лучшие практики.

Интеграция Битрикс24 с Node.js открывает широкие возможности для автоматизации бизнес-процессов и создания пользовательских решений. В этой статье разберём пошаговую настройку взаимодействия между популярной CRM-системой и серверной платформой JavaScript, рассмотрим примеры работы с REST API и изучим лучшие практики разработки.

Содержание
  1. Зачем интегрировать Битрикс24 с Node.js
  2. Подготовка к интеграции
  3. Создание приложения в Битрикс24
  4. Настройка проекта Node.js
  5. Работа с REST API Битрикс24
  6. Базовый класс для работы с API
  7. Примеры использования API
  8. Создание веб-приложения с Express.js
  9. Настройка сервера
  10. Работа с различными сущностями CRM
  11. Работа с лидами
  12. Работа с компаниями
  13. Работа с задачами
  14. Обработка больших объёмов данных
  15. Пагинация и батчи
  16. Обработка ошибок и логирование
  17. Расширенная обработка ошибок
  18. Логирование операций
  19. Безопасность и лучшие практики
  20. Управление токенами доступа
  21. Валидация данных
  22. Тестирование интеграции
  23. Модульные тесты
  24. Развёртывание и мониторинг
  25. Докеризация приложения
  26. Мониторинг и метрики
  27. Примеры практических задач
  28. Синхронизация данных с внешней системой
  29. Автоматическое создание задач по событиям
  30. Оптимизация производительности
  31. Кэширование запросов
  32. Пул соединений и ограничение запросов

Зачем интегрировать Битрикс24 с Node.js

Node.js предоставляет разработчикам мощный инструмент для создания серверных приложений на JavaScript. Интеграция с Битрикс24 позволяет:

  • Автоматизировать рутинные задачи — создание сделок, обновление контактов, отправка уведомлений
  • Синхронизировать данные между Битрикс24 и внешними системами
  • Создавать пользовательские интерфейсы для работы с данными CRM
  • Разрабатывать боты и автоматические обработчики для различных событий
  • Интегрироваться с веб-сайтами для передачи лидов и заявок

Подготовка к интеграции

Создание приложения в Битрикс24

Перед началом работы необходимо создать приложение в вашем портале Битрикс24:

  1. Перейдите в раздел «Приложения» → «Разработчикам»
  2. Нажмите «Создать приложение»
  3. Выберите тип «Локальное приложение»
  4. Заполните основные данные приложения
  5. Получите ключи доступа: client_id и client_secret

Настройка проекта Node.js

Создайте новый проект Node.js и установите необходимые зависимости:

mkdir bitrix24-integration
cd bitrix24-integration
npm init -y
npm install axios express dotenv

Создайте файл .env для хранения конфиденциальных данных:

BITRIX24_DOMAIN=your-domain.bitrix24.ru
CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret
ACCESS_TOKEN=your_access_token

Работа с REST API Битрикс24

Базовый класс для работы с API

Создайте базовый класс для удобной работы с API Битрикс24:

const axios = require('axios');
require('dotenv').config();
class Bitrix24API {
constructor() {
this.domain = process.env.BITRIX24_DOMAIN;
this.accessToken = process.env.ACCESS_TOKEN;
this.baseURL = `https://${this.domain}/rest/`;
}
async makeRequest(method, data = {}) {
try {
const url = `${this.baseURL}${method}`;
const response = await axios.post(url, {
...data,
auth: this.accessToken
});
return response.data;
} catch (error) {
console.error('API Error:', error.response?.data || error.message);
throw error;
}
}
// Получение списка сделок
async getDeals(params = {}) {
return await this.makeRequest('crm.deal.list', params);
}
// Создание новой сделки
async createDeal(dealData) {
return await this.makeRequest('crm.deal.add', { fields: dealData });
}
// Обновление сделки
async updateDeal(dealId, dealData) {
return await this.makeRequest('crm.deal.update', {
id: dealId,
fields: dealData
});
}
// Получение контактов
async getContacts(params = {}) {
return await this.makeRequest('crm.contact.list', params);
}
// Создание контакта
async createContact(contactData) {
return await this.makeRequest('crm.contact.add', { fields: contactData });
}
}
module.exports = Bitrix24API;

Примеры использования API

Создайте файл examples.js с примерами работы:

const Bitrix24API = require('./bitrix24-api');
const bitrix = new Bitrix24API();
// Пример 1: Получение списка сделок
async function getDealsExample() {
try {
const deals = await bitrix.getDeals({
select: ['ID', 'TITLE', 'STAGE_ID', 'OPPORTUNITY'],
order: { DATE_CREATE: 'DESC' },
filter: { STAGE_ID: 'NEW' }
});
console.log('Новые сделки:', deals.result);
} catch (error) {
console.error('Ошибка при получении сделок:', error);
}
}
// Пример 2: Создание новой сделки
async function createDealExample() {
try {
const newDeal = await bitrix.createDeal({
STAGE_ID: 'NEW',
OPPORTUNITY: 50000,
CURRENCY_ID: 'RUB',
CONTACT_ID: 123
});
console.log('Создана сделка:', newDeal.result);
} catch (error) {
console.error('Ошибка при создании сделки:', error);
}
}
// Пример 3: Массовое обновление сделок
async function bulkUpdateDeals() {
try {
const deals = await bitrix.getDeals({
filter: { STAGE_ID: 'NEW' }
});
for (const deal of deals.result) {
await bitrix.updateDeal(deal.ID, {
COMMENTS: 'Обновлено через Node.js'
});
}
console.log('Обновлено сделок:', deals.result.length);
} catch (error) {
console.error('Ошибка при обновлении сделок:', error);
}
}
// Запуск примеров
getDealsExample();
createDealExample();
bulkUpdateDeals();

Создание веб-приложения с Express.js

Настройка сервера

Создайте файл server.js для веб-приложения:

const express = require('express');
const Bitrix24API = require('./bitrix24-api');
const app = express();
const port = process.env.PORT || 3000;
const bitrix = new Bitrix24API();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Главная страница
app.get('/', (req, res) => {
res.send(`
<h1>Битрикс24 Integration</h1>
<p>API для работы с Битрикс24 запущен</p>
<ul>
<li><a href="/api/deals">Получить сделки</a></li>
<li><a href="/api/contacts">Получить контакты</a></li>
</ul>
`);
});
// API для получения сделок
app.get('/api/deals', async (req, res) => {
try {
const deals = await bitrix.getDeals({
select: ['ID', 'TITLE', 'STAGE_ID', 'OPPORTUNITY', 'DATE_CREATE'],
order: { DATE_CREATE: 'DESC' }
});
res.json(deals.result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// API для создания сделки
app.post('/api/deals', async (req, res) => {
try {
const { title, opportunity, contactId } = req.body;
const newDeal = await bitrix.createDeal({
OPPORTUNITY: opportunity,
CONTACT_ID: contactId,
STAGE_ID: 'NEW',
CURRENCY_ID: 'RUB'
});
res.json(newDeal.result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// API для получения контактов
app.get('/api/contacts', async (req, res) => {
try {
const contacts = await bitrix.getContacts({
select: ['ID', 'NAME', 'LAST_NAME', 'EMAIL', 'PHONE'],
order: { DATE_CREATE: 'DESC' }
});
res.json(contacts.result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Webhook для обработки событий Битрикс24
app.post('/webhook/bitrix24', (req, res) => {
const { event, data } = req.body;
console.log('Получено событие:', event);
console.log('Данные:', data);
// Обработка различных событий
switch (event) {
case 'ONCRMDEALADD':
handleDealAdd(data);
break;
case 'ONCRMCONTACTADD':
handleContactAdd(data);
break;
default:
console.log('Неизвестное событие:', event);
}
res.status(200).send('OK');
});
async function handleDealAdd(data) {
console.log('Добавлена новая сделка:', data);
// Здесь можно добавить логику обработки
}
async function handleContactAdd(data) {
console.log('Добавлен новый контакт:', data);
// Здесь можно добавить логику обработки
}
app.listen(port, () => {
console.log(`Сервер запущен на порту ${port}`);
});

Работа с различными сущностями CRM

Работа с лидами

Расширьте класс API для работы с лидами:

// Добавьте эти методы в класс Bitrix24API
// Получение лидов
async getLeads(params = {}) {
return await this.makeRequest('crm.lead.list', params);
}
// Создание лида
async createLead(leadData) {
return await this.makeRequest('crm.lead.add', { fields: leadData });
}
// Конвертация лида в сделку
async convertLead(leadId, params = {}) {
return await this.makeRequest('crm.lead.convert', {
id: leadId,
...params
});
}

Работа с компаниями

// Получение компаний
async getCompanies(params = {}) {
return await this.makeRequest('crm.company.list', params);
}
// Создание компании
async createCompany(companyData) {
return await this.makeRequest('crm.company.add', { fields: companyData });
}
// Обновление компании
async updateCompany(companyId, companyData) {
return await this.makeRequest('crm.company.update', {
id: companyId,
fields: companyData
});
}

Работа с задачами

// Получение задач
async getTasks(params = {}) {
return await this.makeRequest('tasks.task.list', params);
}
// Создание задачи
async createTask(taskData) {
return await this.makeRequest('tasks.task.add', { fields: taskData });
}
// Обновление задачи
async updateTask(taskId, taskData) {
return await this.makeRequest('tasks.task.update', {
taskId: taskId,
fields: taskData
});
}

Обработка больших объёмов данных

Пагинация и батчи

Для работы с большими объёмами данных используйте пагинацию:

async getAllDeals() {
let allDeals = [];
let start = 0;
const limit = 50;
while (true) {
const response = await this.getDeals({
start: start,
limit: limit,
select: ['ID', 'TITLE', 'STAGE_ID', 'OPPORTUNITY']
});
if (!response.result || response.result.length === 0) {
break;
}
allDeals = allDeals.concat(response.result);
start += limit;
// Пауза между запросами для избежания лимитов
await new Promise(resolve => setTimeout(resolve, 100));
}
return allDeals;
}
// Batch-операции для множественных запросов
async batchRequest(commands) {
return await this.makeRequest('batch', { cmd: commands });
}

Обработка ошибок и логирование

Расширенная обработка ошибок

class Bitrix24Error extends Error {
constructor(message, code, details) {
super(message);
this.name = 'Bitrix24Error';
this.code = code;
this.details = details;
}
}
// Обновлённый метод makeRequest с улучшенной обработкой ошибок
async makeRequest(method, data = {}) {
try {
const url = `${this.baseURL}${method}`;
const response = await axios.post(url, {
...data,
auth: this.accessToken
});
if (response.data.error) {
throw new Bitrix24Error(
response.data.error_description || response.data.error,
response.data.error,
response.data
);
}
return response.data;
} catch (error) {
if (error instanceof Bitrix24Error) {
throw error;
}
if (error.response) {
throw new Bitrix24Error(
`HTTP Error: ${error.response.status}`,
error.response.status,
error.response.data
);
}
throw new Bitrix24Error(
error.message,
'NETWORK_ERROR',
{ originalError: error }
);
}
}

Логирование операций

const fs = require('fs');
const path = require('path');
class Logger {
constructor(logFile = 'bitrix24.log') {
this.logFile = path.join(__dirname, logFile);
}
log(level, message, data = null) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
message,
data
};
const logString = JSON.stringify(logEntry) + '\n';
fs.appendFileSync(this.logFile, logString);
console.log(`[${timestamp}] ${level}: ${message}`);
}
info(message, data) {
this.log('INFO', message, data);
}
error(message, data) {
this.log('ERROR', message, data);
}
warning(message, data) {
this.log('WARNING', message, data);
}
}
// Использование логгера в API классе
const logger = new Logger();
// В методе makeRequest добавьте логирование
async makeRequest(method, data = {}) {
logger.info(`API Request: ${method}`, { data });
try {
// ... код запроса
logger.info(`API Response: ${method}`, { result: response.data });
return response.data;
} catch (error) {
logger.error(`API Error: ${method}`, { error: error.message, data });
throw error;
}
}

Безопасность и лучшие практики

Управление токенами доступа

Создайте систему управления токенами с автоматическим обновлением:

class TokenManager {
constructor() {
this.tokenFile = path.join(__dirname, 'tokens.json');
this.loadTokens();
}
loadTokens() {
try {
const data = fs.readFileSync(this.tokenFile, 'utf8');
this.tokens = JSON.parse(data);
} catch (error) {
this.tokens = {};
}
}
saveTokens() {
fs.writeFileSync(this.tokenFile, JSON.stringify(this.tokens, null, 2));
}
async refreshToken(refreshToken) {
try {
const response = await axios.post('https://oauth.bitrix.info/oauth/token/', {
grant_type: 'refresh_token',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
refresh_token: refreshToken
});
this.tokens = response.data;
this.saveTokens();
return response.data;
} catch (error) {
throw new Error('Не удалось обновить токен: ' + error.message);
}
}
getValidToken() {
// Проверка срока действия токена и обновление при необходимости
const expiresAt = this.tokens.expires_at || 0;
const now = Date.now() / 1000;
if (now >= expiresAt - 300) { // Обновляем за 5 минут до истечения
return this.refreshToken(this.tokens.refresh_token);
}
return Promise.resolve(this.tokens.access_token);
}
}

Валидация данных

class DataValidator {
static validateDeal(dealData) {
const errors = [];
if (!dealData.TITLE || dealData.TITLE.trim() === '') {
errors.push('Название сделки обязательно');
}
if (dealData.OPPORTUNITY && isNaN(dealData.OPPORTUNITY)) {
errors.push('Сумма должна быть числом');
}
if (dealData.EMAIL && !this.isValidEmail(dealData.EMAIL)) {
errors.push('Некорректный email');
}
return errors;
}
static validateContact(contactData) {
const errors = [];
if (!contactData.NAME || contactData.NAME.trim() === '') {
errors.push('Имя контакта обязательно');
}
if (contactData.EMAIL && !this.isValidEmail(contactData.EMAIL)) {
errors.push('Некорректный email');
}
if (contactData.PHONE && !this.isValidPhone(contactData.PHONE)) {
errors.push('Некорректный номер телефона');
}
return errors;
}
static isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static isValidPhone(phone) {
const phoneRegex = /^\+?[\d\s\-\(\)]+$/;
return phoneRegex.test(phone);
}
}

Тестирование интеграции

Модульные тесты

Установите Jest для тестирования:

npm install --save-dev jest

Создайте файл tests/bitrix24-api.test.js:

const Bitrix24API = require('../bitrix24-api');
// Мокаем axios для тестирования
jest.mock('axios');
const axios = require('axios');
describe('Bitrix24API', () => {
let api;
beforeEach(() => {
api = new Bitrix24API();
jest.clearAllMocks();
});
test('should create deal successfully', async () => {
const mockResponse = {
data: {
result: 123,
time: { start: 1, finish: 2, duration: 1 }
}
};
axios.post.mockResolvedValue(mockResponse);
const dealData = {
OPPORTUNITY: 10000
};
const result = await api.createDeal(dealData);
expect(axios.post).toHaveBeenCalledWith(
expect.stringContaining('crm.deal.add'),
expect.objectContaining({
fields: dealData
})
);
expect(result.result).toBe(123);
});
test('should handle API errors', async () => {
const errorResponse = {
response: {
data: {
error: 'ACCESS_DENIED',
error_description: 'Access denied'
}
}
};
axios.post.mockRejectedValue(errorResponse);
await expect(api.getDeals()).rejects.toThrow('Access denied');
});
});
// Добавьте в package.json
{
"scripts": {
"test": "jest"
}
}

Развёртывание и мониторинг

Докеризация приложения

Создайте Dockerfile:

FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

И docker-compose.yml:

version: '3.8'
services:
bitrix24-integration:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
env_file:
- .env
volumes:
- ./logs:/app/logs
restart: unless-stopped
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
redis_data:

Мониторинг и метрики

Добавьте систему мониторинга:

const express = require('express');
class MetricsCollector {
constructor() {
this.metrics = {
requestCount: 0,
errorCount: 0,
responseTime: [],
lastRequest: null
};
}
recordRequest(method, duration, success = true) {
this.metrics.requestCount++;
this.metrics.responseTime.push(duration);
this.metrics.lastRequest = new Date().toISOString();
if (!success) {
this.metrics.errorCount++;
}
// Храним только последние 100 замеров времени ответа
if (this.metrics.responseTime.length > 100) {
this.metrics.responseTime.shift();
}
}
getStats() {
const responseTime = this.metrics.responseTime;
const avgResponseTime = responseTime.length > 0
? responseTime.reduce((a, b) => a + b, 0) / responseTime.length
: 0;
return {
totalRequests: this.metrics.requestCount,
totalErrors: this.metrics.errorCount,
errorRate: this.metrics.requestCount > 0
? (this.metrics.errorCount / this.metrics.requestCount * 100).toFixed(2) + '%'
: '0%',
averageResponseTime: avgResponseTime.toFixed(2) + 'ms',
lastRequest: this.metrics.lastRequest
};
}
}
const metrics = new MetricsCollector();
// Добавьте в ваш Express-сервер
app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
metrics: metrics.getStats()
});
});

Примеры практических задач

Синхронизация данных с внешней системой

class DataSync {
constructor(bitrix24Api) {
this.bitrix = bitrix24Api;
this.logger = new Logger('sync.log');
}
async syncContactsFromCSV(filePath) {
const csv = require('csv-parser');
const fs = require('fs');
const contacts = [];
return new Promise((resolve, reject) => {
fs.createReadStream(filePath)
.pipe(csv())
.on('data', (row) => {
contacts.push(row);
})
.on('end', async () => {
try {
const results = await this.processContacts(contacts);
resolve(results);
} catch (error) {
reject(error);
}
});
});
}
async processContacts(contacts) {
const results = {
created: 0,
updated: 0,
errors: 0
};
for (const contact of contacts) {
try {
// Проверяем, существует ли контакт
const existingContacts = await this.bitrix.getContacts({
filter: { EMAIL: contact.email }
});
if (existingContacts.result.length > 0) {
// Обновляем существующий контакт
await this.bitrix.updateContact(existingContacts.result[0].ID, {
NAME: contact.name,
LAST_NAME: contact.last_name,
PHONE: [{ VALUE: contact.phone, VALUE_TYPE: 'WORK' }]
});
results.updated++;
} else {
// Создаём новый контакт
await this.bitrix.createContact({
NAME: contact.name,
LAST_NAME: contact.last_name,
EMAIL: [{ VALUE: contact.email, VALUE_TYPE: 'WORK' }],
PHONE: [{ VALUE: contact.phone, VALUE_TYPE: 'WORK' }]
});
results.created++;
}
// Пауза между запросами
await new Promise(resolve => setTimeout(resolve, 200));
} catch (error) {
this.logger.error('Ошибка при обработке контакта', {
contact: contact,
error: error.message
});
results.errors++;
}
}
return results;
}
}
// Использование
const sync = new DataSync(bitrix);
sync.syncContactsFromCSV('./contacts.csv')
.then(results => {
console.log('Синхронизация завершена:', results);
})
.catch(error => {
console.error('Ошибка синхронизации:', error);
});

Автоматическое создание задач по событиям

class TaskAutomation {
constructor(bitrix24Api) {
this.bitrix = bitrix24Api;
}
async createTaskOnDealStageChange(dealId, newStage) {
const taskTemplates = {
'PREPARATION': {
title: 'Подготовить документы для сделки',
description: 'Подготовить все необходимые документы для заключения сделки'
},
'NEGOTIATION': {
title: 'Провести переговоры',
description: 'Связаться с клиентом для уточнения условий сделки'
},
'INVOICING': {
title: 'Выставить счёт',
description: 'Подготовить и отправить счёт клиенту'
}
};
const template = taskTemplates[newStage];
if (!template) return;
// Получаем данные сделки
const dealResponse = await this.bitrix.makeRequest('crm.deal.get', {
id: dealId
});
const deal = dealResponse.result;
// Создаём задачу
await this.bitrix.createTask({
DESCRIPTION: template.description,
RESPONSIBLE_ID: deal.ASSIGNED_BY_ID,
DEADLINE: this.getDeadlineForStage(newStage),
UF_CRM_TASK: [`D_${dealId}`] // Привязываем к сделке
});
}
getDeadlineForStage(stage) {
const deadlines = {
'PREPARATION': 3, // 3 дня
'NEGOTIATION': 7, // 7 дней
'INVOICING': 1    // 1 день
};
const days = deadlines[stage] || 5;
const deadline = new Date();
deadline.setDate(deadline.getDate() + days);
return deadline.toISOString().split('T')[0];
}
}
// Использование в webhook
app.post('/webhook/deal-stage-change', async (req, res) => {
const { dealId, newStage } = req.body;
const automation = new TaskAutomation(bitrix);
await automation.createTaskOnDealStageChange(dealId, newStage);
res.status(200).send('OK');
});

Оптимизация производительности

Кэширование запросов

Установите Redis для кэширования:

npm install redis
const redis = require('redis');
const client = redis.createClient();
class CachedBitrix24API extends Bitrix24API {
constructor() {
super();
this.cache = client;
this.cacheEnabled = true;
this.cacheTTL = 300; // 5 минут
}
async makeRequest(method, data = {}) {
if (!this.cacheEnabled) {
return super.makeRequest(method, data);
}
const cacheKey = this.getCacheKey(method, data);
try {
const cached = await this.cache.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
} catch (error) {
console.warn('Cache read error:', error);
}
const result = await super.makeRequest(method, data);
// Кэшируем только GET-запросы
if (this.isReadOnlyMethod(method)) {
try {
await this.cache.setex(cacheKey, this.cacheTTL, JSON.stringify(result));
} catch (error) {
console.warn('Cache write error:', error);
}
}
return result;
}
getCacheKey(method, data) {
return `bitrix24:${method}:${JSON.stringify(data)}`;
}
isReadOnlyMethod(method) {
return method.includes('.list') || method.includes('.get');
}
async clearCache(pattern = '*') {
try {
const keys = await this.cache.keys(`bitrix24:${pattern}`);
if (keys.length > 0) {
await this.cache.del(keys);
}
} catch (error) {
console.warn('Cache clear error:', error);
}
}
}

Пул соединений и ограничение запросов

class RateLimiter {
constructor(maxRequests = 50, timeWindow = 60000) {
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
this.requests = [];
}
async acquire() {
const now = Date.now();
// Удаляем старые запросы
this.requests = this.requests.filter(
time => now - time < this.timeWindow
);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = Math.min(...this.requests);
const waitTime = this.timeWindow - (now - oldestRequest);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
return this.acquire();
}
}
this.requests.push(now);
return true;
}
}
// Использование в API классе
class ThrottledBitrix24API extends CachedBitrix24API {
constructor() {
super();
this.rateLimiter = new RateLimiter(50, 60000); // 50 запросов в минуту
}
async makeRequest(method, data = {}) {
await this.rateLimiter.acquire();
return super.makeRequest(method, data);
}
}

Интеграция Битрикс24 с Node.js открывает множество возможностей для автоматизации бизнес-процессов и создания эффективных решений. Следуя рекомендациям из этой статьи, вы сможете создать надёжную и производительную интеграцию, которая будет соответствовать требованиям вашего бизнеса.

Наша компания предоставляет профессиональные услуги по настройке и внедрению Битрикс24. Мы поможем вам создать индивидуальные интеграции с Node.js, настроить автоматизацию бизнес-процессов и оптимизировать работу с CRM-системой. Обращайтесь к нам для получения консультации и разработки решений под ваши конкретные задачи.

Оцените статью
Битрикс24
Добавить комментарий