Комплексное решение по входящим и исходящим звонкам

Данный сценарий интеграци работает комплексно. Сценарии подключаются по очереди. Представленный ниже код необходимо добавлять в указанные этапы функций. Логика работы следующая:

  1. Поиск контакта с ответственным менеджером сопоставленным со схемой( в дальнейшем Пользователь№1=Схема№1) наличие активной сделки у данного контакта
    • Нет контакта с Пользователем№1=схема№1, создаем новый контакт и сделку с ответственным Пользователем№1
    • Есть контакт но ответственный Пользователем№2 а не Пользователем№1 который привязан схеме создаем (дубль) новый контакт и новую сделку с Пользователем№1
    • Если есть контакт с Пользователем№1=схема№1 но нет активной сделки, создаем новую сделку с ответственным Пользователем№1
  2. Добавляем запись звонка в этот контакт (который подходит по условиям в пункте 1, вновь созданный или найденный с нужным ответственным)
  3. Если звонок был пропущен, задачу на перезвон добавляем в этот контакт (который подходит по условиям в пункте 1, вновь созданный или найденный с нужным ответственным)
  4. По Завершению звонка добавляем запись звонка.
  5. При исходящем такая же логика проверки как в 1 пункте.

Завершен исходящий разговор

const axios = require('axios');

module.exports = async (args) => {
    const contactId = args.context.contactId;
    const contactName = args.context.contactName;
    const contactResponsibleId = args.context.contactResponsibleId;

    // пишем звонок
    const url = `https://${args.settings.domain}/api/v2/notes`;
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${args.crm_auth}`,
    };
    const data = {
	    add: [{
		    element_id: parseInt(contactId),
		    element_type: 1, // 1 - Contact, 2 - Lead, 3 - Company
		    note_type: 11, // 10 - Incoming call, 11 - Outgoing call
		    params: {
		        UNIQ: args.call_args.call_id,
		        LINK: args.call_args.call_record_link,
		        PHONE: contactName,
		        DURATION: parseInt(args.call_args.timestamp) - parseInt(args.call_args.call_answer_timestamp),
		        SRC: 'sipuni',
		        call_status: 4, //1 Left a voice message, 2 Call back later, 3 Not available, 4 The conversation took place, 5 Wrong number, 6 Did not get through, 7 Number is busy
		        call_result: '',
		    },
		    created_by: contactResponsibleId,
		    responsible_user_id: contactResponsibleId,
	    }]
    };
    const call = await axios.post(url, data, { headers });
    const noteId = call.data._embedded.items[0].id;

    return { noteId };
};

Исходящий звонок

const SipuniAmoCrm = require('@sipuni/amocrm');

const LEAD_CLOSED_STATUSES = [142 /*успешная*/, 143 /*неуспешная*/];
const CONTACT_LIMIT = 10;
const CONTACT_PHONE_FIELD_ID = 000001; //id телефонного поля
const SCHEME_TO_RESPONSIBLE = { // сопоставление названия схемы и ID ответственного в амо по ней
    "Пользователем№1": 111000,
    "Пользователем№2": 000222,
    "Пользователем№1": 333000,
};
const MAIN_TREE_TAMES = [
    'Загород',
];

// запрос информации о сделке по айди, проверка статуса сделки
async function findOpenLead(api, lead_id, reponsible_id = 0) {
    const lead = await api.leads.get(lead_id);
    if (reponsible_id) {
        if (lead && !LEAD_CLOSED_STATUSES.includes(lead.status_id) && lead.responsible_user_id === reponsible_id) {
            return lead_id;
        }
    } else {
        if (lead && !LEAD_CLOSED_STATUSES.includes(lead.status_id)) {
            return lead_id;
        }
    }
    return 0;
}

// перебор всех найденных контактов, поиск нужного кастомного поля, если нашли, забираем информацию о окнтакте и ищем у него открытую сделку, если не наши возвращаем 0
async function findContactAndLead(amoApi, contacts, tree_name) {
    const reponsibleId = SCHEME_TO_RESPONSIBLE[tree_name];
    let leads;
    const promises = [];
    if (MAIN_TREE_TAMES.includes(tree_name)) {
        leads = contacts[0]._embedded.leads;
        for(let leadIndex=0; leadIndex < leads.length; leadIndex++) { 
            promises.push(findOpenLead(amoApi, leads[leadIndex].id));
        }
        const results = await Promise.all(promises);
        const leadId = results.find(item => item > 0)
        if (leadId) {
            return {
                contactId: contacts[0].id,
                contactName: contacts[0].name,
                contactResponsibleId : contacts[0].responsible_user_id,
                leadId,
            }
        } else {
            return {
                contactId: contacts[0].id,
                contactName: contacts[0].name,
                contactResponsibleId : contacts[0].responsible_user_id,
                leadId: 0,
            }
        }
    }
    for(let contactIndex=0; contactIndex < contacts.length; contactIndex++) {
        leads = contacts[contactIndex]._embedded.leads;
        if (contacts[contactIndex].responsible_user_id === reponsibleId) {
            for(let leadIndex=0; leadIndex < leads.length; leadIndex++) { 
                promises.push(findOpenLead(amoApi, leads[leadIndex].id, reponsibleId));
            }
            const results = await Promise.all(promises);
            const leadId = results.find(item => item > 0)
            if (leadId) {
                return {
                    contactId: contacts[contactIndex].id,
                    contactName: contacts[contactIndex].name,
                    contactResponsibleId : contacts[contactIndex].responsible_user_id,
                    leadId,
                }
            } else {
                return {
                    contactId: contacts[contactIndex].id,
                    contactName: contacts[contactIndex].name,
                    contactResponsibleId : contacts[contactIndex].responsible_user_id,
                    leadId: 0,
                }
            }
        }
	}
    return {
        contactId: 0,
        contactName: '',
        contactResponsibleId: 0,
        leadId: 0,
    };
}

module.exports = async (args) => {
    const amoApi = new SipuniAmoCrm({ domain: args.settings.domain, accessToken: args.crm_auth });
	
    let contacts = {};
    let contactId = 0;
    let contactName = '';
    let contactResponsibleId = 0;
    let leadId = 0;
    const reponsibleId = SCHEME_TO_RESPONSIBLE[args.call_args.tree_name];

    // Поиск контактов по номеру с айдишниками сделок
    // result._embedded.contacts[i]._embedded.leads[j].id
    const result = await amoApi.request('GET', '/contacts', {
        'query': args.call_args.dst_num.slice(1),
        'limit': CONTACT_LIMIT,
        'with': 'leads'
    });

    if (result) {
        contacts = result._embedded.contacts;
    }

    // проверка соответствия доп поля и схемы
    const findResult = await findContactAndLead(amoApi, contacts, args.call_args.tree_name);
    contactId = findResult.contactId;
    contactName = findResult.contactName;
    contactResponsibleId = findResult.contactResponsibleId;
    leadId = findResult.leadId;

    console.log(contactId);
    console.log(leadId);

    // создание контакта, если не найден существующий
    if (!contactId) {
        contactName = `Контакт ${args.call_args.dst_num}`;
        const contact = await amoApi.contacts.create({
            name: contactName,
            responsible_user_id: reponsibleId,
            custom_fields_values: [
				{
					field_id: CONTACT_PHONE_FIELD_ID,
					values: [{
						value: `+${args.call_args.dst_num}`,
						enum_code: 'WORK'
					}],
				},
			],
        });
        contactId = contact.id;
        contactResponsibleId = reponsibleId;
    }

    // создание сделки, если не найдена существующая
    if (!leadId) {
        const leadName = `Сделка ${args.call_args.dst_num}`;
        const lead = await amoApi.leads.create({
            name: leadName,
            responsible_user_id: reponsibleId,
            _embedded: {
                contacts: [{
                    id: contactId,
                    is_main: true,
                }],
            },
        });
        leadId = lead.id;
    }

    return {
        contactId,
        contactName,
        contactResponsibleId,
        leadId
    };
};

Исходящий вызов не отвечен

const axios = require('axios');

module.exports = async (args) => {
    const contactId = args.context.contactId;
    const contactName = args.context.contactName;
    const contactResponsibleId = args.context.contactResponsibleId;

    // создание заметки о пропущенном
    const url = `https://${args.settings.domain}/api/v2/notes`;
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${args.crm_auth}`,
    };
    const data = {
	    add: [{
		    element_id: parseInt(contactId),
		    element_type: 1, // 1 - Contact, 2 - Lead, 3 - Company
		    note_type: 11, // 10 - Incoming call, 11 - Outgoing call
		    params: {
		        UNIQ: args.call_args.call_id,
		        PHONE: contactName,
		        DURATION: 0,
		        SRC: 'sipuni',
		        call_status: 6, //1 Left a voice message, 2 Call back later, 3 Not available, 4 The conversation took place, 5 Wrong number, 6 Did not get through, 7 Number is busy
		        call_result: 'Не дозвонились',
		    },
		    created_by: contactResponsibleId,
		    responsible_user_id: contactResponsibleId,
	    }]
    };
    const call = await axios.post(url, data, { headers });
    const noteId = call.data._embedded.items[0].id;

    return { noteId };
};

Пропущен входящий вызов

const axios = require('axios');
const SipuniAmoCrm = require('@sipuni/amocrm');

const TASK_DUE = 60*60*3600; // 1 час в секундах

module.exports = async (args) => {
    const amoApi = new SipuniAmoCrm({ domain: args.settings.domain, accessToken: args.crm_auth });
    const contactId = args.context.contactId;
    const contactName = args.context.contactName;
    const contactResponsibleId = args.context.contactResponsibleId;
    const leadId = args.context.leadId;
    let task;
    let taskId;

    // создание заметки о пропущенном
    const url = `https://${args.settings.domain}/api/v2/notes`;
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${args.crm_auth}`,
    };
    const data = {
	    add: [{
		    element_id: parseInt(contactId),
		    element_type: 1, // 1 - Contact, 2 - Lead, 3 - Company
		    note_type: 10, // 10 - Incoming call, 11 - Outgoing call
		    params: {
		        UNIQ: args.call_args.call_id,
		        PHONE: contactName,
		        DURATION: 0,
		        SRC: 'sipuni',
		        call_status: 6, //1 Left a voice message, 2 Call back later, 3 Not available, 4 The conversation took place, 5 Wrong number, 6 Did not get through, 7 Number is busy
		        call_result: 'Пропущенный звонок',
		    },
		    created_by: contactResponsibleId,
		    responsible_user_id: contactResponsibleId,
	    }]
    };
    const call = await axios.post(url, data, { headers });
    const noteId = call.data._embedded.items[0].id;

    // создание задачи
    const existingTasks = await amoApi.tasks.list({
        'filter[responsible_user_id]': contactResponsibleId,
        'filter[is_completed]': 0,
        'filter[entity_type]': 'leads',
        'filter[entity_id]': leadId,
    });
    if (existingTasks) {
        taskId = existingTasks.map(function(task) {return task.id;});
    } else {
        const timestamp = Math.ceil((new Date()).getTime()/1000);
        task = await amoApi.tasks.create({
            entity_id: leadId,
            entity_type: 'leads',
            text: 'Перезвонить',
            complete_till: timestamp + TASK_DUE,
            responsible_user_id: contactResponsibleId,
        });
    }
    if (task) {
        taskId = task.id;
        return {
            noteId,
            createdTaskId: taskId,
        };
    } else {
        return {
            noteId,
            findedTaskId: taskId,
        };
    }
};

Входящий звонок на внешний номер

const SipuniAmoCrm = require('@sipuni/amocrm');

const LEAD_CLOSED_STATUSES = [142 /*успешная*/, 143 /*неуспешная*/];
const CONTACT_LIMIT = 10;
const CONTACT_PHONE_FIELD_ID = 000001; // id телефонного поля
const SCHEME_TO_RESPONSIBLE = { // сопоставление названия схемы и ID ответственного в амо по ней
    "Пользователем№1": 111000,
    "Пользователем№2": 000222,
    "Пользователем№1": 333000,
};
const MAIN_TREE_TAMES = [
    'Общая',
];

// запрос информации о сделке по айди, проверка статуса сделки
async function findOpenLead(api, lead_id, reponsible_id = 0) {
    const lead = await api.leads.get(lead_id);
    if (reponsible_id) {
        if (lead && !LEAD_CLOSED_STATUSES.includes(lead.status_id) && lead.responsible_user_id === reponsible_id) {
            return lead_id;
        }
    } else {
        if (lead && !LEAD_CLOSED_STATUSES.includes(lead.status_id)) {
            return lead_id;
        }
    }
    return 0;
}

// перебор всех найденных контактов, поиск нужного кастомного поля, если нашли, забираем информацию о окнтакте и ищем у него открытую сделку, если не наши возвращаем 0
async function findContactAndLead(amoApi, contacts, tree_name) {
    const reponsibleId = SCHEME_TO_RESPONSIBLE[tree_name];
    let leads;
    const promises = [];
    if (MAIN_TREE_TAMES.includes(tree_name)) {
        leads = contacts[0]._embedded.leads;
        for(let leadIndex=0; leadIndex < leads.length; leadIndex++) { 
            promises.push(findOpenLead(amoApi, leads[leadIndex].id));
        }
        const results = await Promise.all(promises);
        const leadId = results.find(item => item > 0)
        if (leadId) {
            return {
                contactId: contacts[0].id,
                contactName: contacts[0].name,
                contactResponsibleId : contacts[0].responsible_user_id,
                leadId,
            }
        } else {
            return {
                contactId: contacts[0].id,
                contactName: contacts[0].name,
                contactResponsibleId : contacts[0].responsible_user_id,
                leadId: 0,
            }
        }
    }
    for(let contactIndex=0; contactIndex < contacts.length; contactIndex++) {
        leads = contacts[contactIndex]._embedded.leads;
        if (contacts[contactIndex].responsible_user_id === reponsibleId) {
            for(let leadIndex=0; leadIndex < leads.length; leadIndex++) { 
                promises.push(findOpenLead(amoApi, leads[leadIndex].id, reponsibleId));
            }
            const results = await Promise.all(promises);
            const leadId = results.find(item => item > 0)
            if (leadId) {
                return {
                    contactId: contacts[contactIndex].id,
                    contactName: contacts[contactIndex].name,
                    contactResponsibleId : contacts[contactIndex].responsible_user_id,
                    leadId,
                }
            } else {
                return {
                    contactId: contacts[contactIndex].id,
                    contactName: contacts[contactIndex].name,
                    contactResponsibleId : contacts[contactIndex].responsible_user_id,
                    leadId: 0,
                }
            }
        }
	}
    return {
        contactId: 0,
        contactName: '',
        contactResponsibleId: 0,
        leadId: 0,
    };
}

module.exports = async (args) => {
    const amoApi = new SipuniAmoCrm({ domain: args.settings.domain, accessToken: args.crm_auth });
	
    let contacts = {};
    let contactId = 0;
    let contactName = '';
    let contactResponsibleId = 0;
    let leadId = 0;
    const reponsibleId = SCHEME_TO_RESPONSIBLE[args.call_args.tree_name];

    // Поиск контактов по номеру с айдишниками сделок
    // result._embedded.contacts[i]._embedded.leads[j].id
    const result = await amoApi.request('GET', '/contacts', {
        'query': args.call_args.src_num.slice(1),
        'limit': CONTACT_LIMIT,
        'with': 'leads'
    });

    if (result) {
        contacts = result._embedded.contacts;
    }

    // проверка соответствия доп поля и схемы
    const findResult = await findContactAndLead(amoApi, contacts, args.call_args.tree_name);
    contactId = findResult.contactId;
    contactName = findResult.contactName;
    contactResponsibleId = findResult.contactResponsibleId;
    leadId = findResult.leadId;

    // создание контакта, если не найден существующий
    if (!contactId) {
        contactName = `Контакт ${args.call_args.src_num}`;
        const contact = await amoApi.contacts.create({
            name: contactName,
            responsible_user_id: reponsibleId,
            custom_fields_values: [
				{
					field_id: CONTACT_PHONE_FIELD_ID,
					values: [{
						value: `+${args.call_args.src_num}`,
						enum_code: 'WORK'
					}],
				},
			],
        });
        contactId = contact.id;
        contactResponsibleId = reponsibleId;
    }

    // создание сделки, если не найдена существующая
    if (!leadId) {
        const leadName = `Сделка ${args.call_args.src_num}`;
        const lead = await amoApi.leads.create({
            name: leadName,
            responsible_user_id: reponsibleId,
            _embedded: {
                contacts: [{
                    id: contactId,
                    is_main: true,
                }],
            },
        });
        leadId = lead.id;
    }

    return {
        contactId,
        contactName,
        contactResponsibleId,
        leadId
    };
};

Завершен входящий разговор

const axios = require('axios');

module.exports = async (args) => {
    const contactId = args.context.contactId;
    const contactName = args.context.contactName;
    const contactResponsibleId = args.context.contactResponsibleId;

    // пишем звонок
    const url = `https://${args.settings.domain}/api/v2/notes`;
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${args.crm_auth}`,
    };
    const data = {
	    add: [{
		    element_id: parseInt(contactId),
		    element_type: 1, // 1 - Contact, 2 - Lead, 3 - Company
		    note_type: 10, // 10 - Incoming call, 11 - Outgoing call
		    params: {
		        UNIQ: args.call_args.call_id,
		        LINK: args.call_args.call_record_link,
		        PHONE: contactName,
		        DURATION: parseInt(args.call_args.timestamp) - parseInt(args.call_args.call_answer_timestamp),
		        SRC: 'sipuni',
		        call_status: 4, //1 Left a voice message, 2 Call back later, 3 Not available, 4 The conversation took place, 5 Wrong number, 6 Did not get through, 7 Number is busy
		        call_result: '',
		    },
		    created_by: contactResponsibleId,
		    responsible_user_id: contactResponsibleId,
	    }]
    };
    const call = await axios.post(url, data, { headers });
    const noteId = call.data._embedded.items[0].id;

    return { noteId };
};