diff --git a/integrations/kommo/definitions/actions/contact.ts b/integrations/kommo/definitions/actions/contact.ts new file mode 100644 index 00000000..b86c17e7 --- /dev/null +++ b/integrations/kommo/definitions/actions/contact.ts @@ -0,0 +1,59 @@ +import {z} from '@bpinternal/zui' +import{ActionDefinition} from '@botpress/sdk' + + +export const contactSchema = z.object({ + id: z.number().describe('Contact ID'), + name: z.string().describe('Contact name'), + firstName: z.string().describe('First name'), + lastName: z.string().describe('Last name'), + responsibleUserId: z.number().describe('User responsible for this contact'), + groupId: z.number().describe('Group ID'), + updatedBy: z.number().describe('User who last updated this contact'), + createdAt: z.number().describe('When created (Unix timestamp)'), + updatedAt: z.number().describe('When last updated (Unix timestamp)'), + closestTaskAt: z.number().optional().describe('Closest task timestamp'), + isDeleted: z.boolean().describe('Whether contact is deleted'), + accountId: z.number().describe('Account ID'), +}) + +const createContact: ActionDefinition = { + title: 'Create Contact', + description: 'Creates a new contact', + input:{ + schema:z.object({ + name: z.string().optional().describe('Full contact name'), + firstName: z.string().optional().describe('First name'), + lastName: z.string().optional().describe('Last name'), + responsibleUserId: z.number().describe('User ID to assign this contact to'), + createdBy: z.number().describe('User ID who creates this contact'), + updatedBy: z.number().optional().describe('User ID who updates this contact'), + }), + }, + output:{ + schema:z.object({ + contact: contactSchema + }) + } + +} +const searchContacts: ActionDefinition = { + title: "Search Contacts", + description: 'Search for contacts by name, phone number, or email', + input:{ + schema: z.object({ + query: z.string().describe('Search query (name, phone number, or email)') + }), + }, + output: { + schema: z.object({ + contacts: z.array(contactSchema).describe('Array of matching contacts (empty if none found)') + }) + + } +} + +export const actions = { + createContact, + searchContacts, +} as const \ No newline at end of file diff --git a/integrations/kommo/definitions/actions/index.ts b/integrations/kommo/definitions/actions/index.ts new file mode 100644 index 00000000..9ec61ce3 --- /dev/null +++ b/integrations/kommo/definitions/actions/index.ts @@ -0,0 +1,8 @@ +import * as sdk from '@botpress/sdk' +import { actions as leadActions } from './lead' +import { actions as contactActions } from './contact' + +export const actions = { + ...leadActions, + ...contactActions, +} as const satisfies sdk.IntegrationDefinitionProps['actions'] \ No newline at end of file diff --git a/integrations/kommo/definitions/actions/lead.ts b/integrations/kommo/definitions/actions/lead.ts new file mode 100644 index 00000000..bcc7c0ba --- /dev/null +++ b/integrations/kommo/definitions/actions/lead.ts @@ -0,0 +1,77 @@ +import { z } from '@bpinternal/zui' +import { ActionDefinition } from '@botpress/sdk' + +// Lead Schema: defines what a Kommo lead looks like in Botpress +export const leadSchema = z.object({ + id: z.number().describe('Lead ID'), + name: z.string().describe('Lead name'), + price: z.number().optional().describe('Lead value in dollars'), + responsibleUserId: z.number().optional().describe('User responsible for this lead'), + pipelineId: z.number().optional().describe('Which sales pipeline'), + statusId: z.number().optional().describe('Which stage in the pipeline'), + createdAt: z.number().describe('When created (Unix timestamp)'), + updatedAt: z.number().describe('When last updated (Unix timestamp)'), +}) + + +const createLead: ActionDefinition = { + title: 'Create Lead', + description: 'Creates a new lead in Kommo CRM', + input: { + schema: z.object({ + name: z.string().describe('Lead name (required)'), + price: z.number().optional().describe('Lead value in dollars'), + responsibleUserId: z.number().optional().describe('User ID to assign this lead to'), + pipelineId: z.number().optional().describe('Pipeline ID (defaults to main pipeline)'), + statusId: z.number().optional().describe('Initial status/stage ID'), + }), + }, + output: { + schema: z.object({ + lead: leadSchema, + }), + }, +} + + +const updateLead: ActionDefinition = { + title: 'Update Lead', + description: 'Updates an existing lead in Kommo', + input: { + schema: z.object({ + leadId: z.number().describe('Lead ID to update'), + name: z.string().optional().describe('New name'), + price: z.number().optional().describe('New price'), + responsibleUserId: z.number().optional().describe('New responsible user'), + pipelineId: z.number().optional().describe('New pipeline ID'), + statusId: z.number().optional().describe('New status/stage ID'), + }), + }, + output: { + schema: z.object({ + lead: leadSchema, + }), + }, +} + +const searchLeads: ActionDefinition = { + title: 'Search Leads', + description: 'search for leads by name or other fields', + input:{ + schema: z.object({ + query: z.string().describe('Search query') + }), + }, + output: { + schema: z.object({ + leads: z.array(leadSchema).describe('Array of matching leads (empty if none found)') + }) + } +} + +// Export all lead actions +export const actions = { + createLead, + searchLeads, + updateLead, +} as const diff --git a/integrations/kommo/definitions/index.ts b/integrations/kommo/definitions/index.ts new file mode 100644 index 00000000..e964bf2a --- /dev/null +++ b/integrations/kommo/definitions/index.ts @@ -0,0 +1,2 @@ +// Re-export everything from definitions +export * from './actions' diff --git a/integrations/kommo/hub.md b/integrations/kommo/hub.md new file mode 100644 index 00000000..ec698743 --- /dev/null +++ b/integrations/kommo/hub.md @@ -0,0 +1,208 @@ +# Kommo Integration + +The Kommo integration allows you to connect your Botpress chatbot with Kommo. With this integration, your chatbot can create and update leads, manage contacts, and search through your CRM data. + +## Prerequisites + +Before using this integration, you need: + +1. A **Kommo account** with administrator access +2. A **private integration** created in your Kommo account +3. A **long-lived access token** from your private integration + +## Configuration + +To set up the Kommo integration in Botpress: + +### Step 1: Create a Private Integration in Kommo + +1. Log in to your Kommo account +2. Navigate to **Settings** → **Integrations** → **Private Integrations** +3. Click **Create Integration** +4. Give your integration a name (e.g., "Botpress Chatbot") +5. Set the required scopes: +6. Save the integration + +### Step 2: Generate an Access Token + +1. In your private integration settings, click **Generate Token** +2. Select the appropriate scopes +3. Copy the **long-lived access token** - you'll need this for Botpress + +### Step 3: Configure Botpress Integration + +In your Botpress integration settings, provide: + +- **Base Domain**: Your Kommo subdomain (e.g., `yourcompany.kommo.com`) + - Do not include `https://` or trailing slashes + - Example: `mycompany.kommo.com` +- **Access Token**: Paste the long-lived access token from Step 2 + +## Available Actions + +### Create Lead + +Create a new lead in your Kommo CRM. Leads represent potential sales opportunities and can be tracked through your sales pipeline. + +**Required Fields:** + +- **Name**: Lead name or title (e.g., "Website Inquiry - John Doe") + +**Optional Fields:** + +- **Price**: Lead value in dollars (e.g., 5000) +- **Responsible User ID**: Kommo user ID to assign this lead to +- **Pipeline ID**: Which sales pipeline to add this lead to (defaults to main pipeline) +- **Status ID**: Initial status/stage in the pipeline (e.g., "New Lead", "Contacted") + +**Output:** Returns the created lead with: +- Lead ID +- Name +- Price +- Responsible User ID +- Pipeline ID +- Status ID +- Created and Updated timestamps + +**Example Use Cases:** +- Capture leads from chatbot conversations +- Create sales opportunities from customer inquiries +- Track chatbot-generated leads in your CRM + +--- + +### Update Lead + +Update an existing lead in Kommo. This action allows you to modify lead information as the conversation progresses or circumstances change. + +**Required Fields:** + +- **Lead ID**: The unique identifier of the lead to update + +**Optional Fields:** + +- **Name**: New lead name +- **Price**: Updated lead value +- **Responsible User ID**: Reassign to a different user +- **Pipeline ID**: Move to a different pipeline +- **Status ID**: Update the lead's stage (e.g., move from "Contacted" to "Qualified") + +**Output:** Returns the updated lead with all current information. + +**Example Use Cases:** +- Update lead value based on customer responses +- Move leads through your sales pipeline automatically +- Reassign leads to different team members +- Update lead information as you gather more details + +--- + +### Search Leads + +Search for leads in your Kommo CRM using keywords. This action searches through lead names, prices, and other filled fields to find matching records. + +**Required Fields:** + +- **Query**: Search term (searches through lead names, prices, and other fields) + +**Output:** Returns an array of matching leads. Each lead includes: +- Lead ID, Name, Price +- Responsible User ID +- Pipeline and Status IDs +- Created and Updated timestamps + +**Example Use Cases:** +- Look up existing leads by name before creating duplicates +- Find leads by company name or keywords +- Check if a lead already exists in your CRM +- Search for leads by partial matches + +--- + +### Create Contact + +Create a new contact in your Kommo CRM. Contacts represent individual people or entities in your CRM database. + +**Required Fields:** + +- **Responsible User ID**: Kommo user ID to assign this contact to +- **Created By**: Kommo user ID of the person creating this contact + +**Optional Fields:** + +- **Name**: Full contact name +- **First Name**: Contact's first name +- **Last Name**: Contact's last name +- **Updated By**: Kommo user ID of the person updating this contact + +**Output:** Returns the created contact with: +- Contact ID +- Name, First Name, Last Name +- Responsible User ID +- Group ID, Account ID +- Created and Updated timestamps +- Deletion status + +**Example Use Cases:** +- Add new contacts from chatbot conversations +- Create CRM entries for new customers +- Store contact information collected during chat + +--- + +### Search Contacts + +Search for contacts in your Kommo CRM by name, phone number, or email address. This action helps you find existing contacts before creating duplicates. + +**Required Fields:** + +- **Query**: Search term (can be name, phone number, or email) + +**Output:** Returns an array of matching contacts. Each contact includes: +- Contact ID +- Name, First Name, Last Name +- Responsible User ID +- Group ID, Account ID +- Created and Updated timestamps +- Deletion status + +If no contacts are found, returns an empty array. + +**Example Use Cases:** +- Look up contacts by email before creating new ones +- Find contacts by phone number +- Search for contacts by name +- Verify if a contact exists in your CRM + +## Common Workflows + +### Lead Qualification Flow + +1. **Search Contacts** to check if the person exists +2. If not found, **Create Contact** with their information +3. **Create Lead** for the sales opportunity +4. As the conversation progresses, **Update Lead** with new information +5. **Search Leads** to find related opportunities + +### Customer Inquiry Handling + +1. **Search Contacts** by email or phone +2. If found, **Create Lead** and link to existing contact +3. **Update Lead** status as you qualify the opportunity +4. Assign lead to appropriate team member via **Update Lead** + +## Important Notes + +- **User IDs**: Responsible User IDs and Created By fields must be valid Kommo user IDs from your account +- **Pipeline and Status IDs**: These are specific to your Kommo account setup. Check your Kommo settings for valid IDs +- **Search**: Search queries are flexible and search across multiple fields +- **Phone Numbers**: When searching contacts by phone, use the same format stored in Kommo +- **Lead Values**: Price fields are in dollars (or your account's default currency) + +## Limitations + +- **Rate Limiting**: Kommo API has rate limits. Excessive requests may be throttled +- **Field Validation**: Some fields (like User IDs, Pipeline IDs) must exist in your Kommo account +- **Search Scope**: Searches are limited to fields populated in your CRM +- **Permissions**: The access token must have appropriate scopes (`crm`, `notifications`) + diff --git a/integrations/kommo/icon.svg b/integrations/kommo/icon.svg new file mode 100644 index 00000000..c7cb4c66 --- /dev/null +++ b/integrations/kommo/icon.svg @@ -0,0 +1,33 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + diff --git a/integrations/kommo/integration.definition.ts b/integrations/kommo/integration.definition.ts new file mode 100644 index 00000000..98802b42 --- /dev/null +++ b/integrations/kommo/integration.definition.ts @@ -0,0 +1,24 @@ +import { z, IntegrationDefinition } from '@botpress/sdk' +import { integrationName } from './package.json' +import { actions } from './definitions' + +export default new IntegrationDefinition({ + name: integrationName, + title: 'Kommo', + version: '0.1.0', + readme: 'hub.md', + icon: 'icon.svg', + + // User provides these when configuring the integration + configuration: { + schema: z.object({ + baseDomain: z + .string() + .describe('Your Kommo subdomain (e.g., yourcompany.kommo.com)'), + accessToken: z + .string() + .describe('Long-lived access token from your Kommo private integration'), + }), + }, + actions, +}) diff --git a/integrations/kommo/package.json b/integrations/kommo/package.json new file mode 100644 index 00000000..d36c6668 --- /dev/null +++ b/integrations/kommo/package.json @@ -0,0 +1,17 @@ +{ + "name": "kommo", + "integrationName": "kommo", + "scripts": { + "check:type": "tsc --noEmit" + }, + "private": true, + "dependencies": { + "@botpress/client": "1.29.0", + "@botpress/sdk": "5.1.1", + "axios": "^1.13.2" + }, + "devDependencies": { + "@types/node": "^22.16.4", + "typescript": "^5.6.3" + } +} diff --git a/integrations/kommo/src/actions/contact.ts b/integrations/kommo/src/actions/contact.ts new file mode 100644 index 00000000..17995b39 --- /dev/null +++ b/integrations/kommo/src/actions/contact.ts @@ -0,0 +1,77 @@ +import * as sdk from '@botpress/sdk' +import * as bp from '.botpress' +import { KommoClient, CreateContactRequest, KommoContact, getErrorMessage } from '../kommo-api' + +// mapping kommo to local schema +function mapKommoContactToBotpress(contact: KommoContact) { + return { + id: contact.id, + name: contact.name, + firstName: contact.first_name, + lastName: contact.last_name, + responsibleUserId: contact.responsible_user_id, + groupId: contact.group_id, + updatedBy: contact.updated_by, + createdAt: contact.created_at, + updatedAt: contact.updated_at, + closestTaskAt: contact.closest_task_at ?? undefined, + isDeleted: contact.is_deleted, + accountId: contact.account_id, + } +} + +export const createContact: bp.IntegrationProps['actions']['createContact'] = async ({ + ctx, + input, + logger, +}) => { + try { + logger.forBot().info('Creating contact with input:', input) + + const {baseDomain, accessToken} = ctx.configuration + const kommoClient = new KommoClient(accessToken, baseDomain, logger) + + const contactData: CreateContactRequest = { + name: input.name, + first_name: input.firstName, + last_name: input.lastName, + responsible_user_id: input.responsibleUserId, + created_by: input.createdBy, + updated_by: input.updatedBy, + } + + logger.forBot().info('Contact data to send:', contactData) + const kommoContact = await kommoClient.createContact(contactData) + logger.forBot().info('Contact created successfully:', { contactId: kommoContact.id }) + return { + contact: mapKommoContactToBotpress(kommoContact), + } + + } catch (error) { + logger.forBot().error('Failed to create contact', {error}) + throw new sdk.RuntimeError(getErrorMessage(error)) + } +} + +export const searchContacts: bp.IntegrationProps['actions']['searchContacts'] = async ({ + ctx, + input, + logger, +}) => { + try { + logger.forBot().info('Searching contacts:', {query: input.query}) + const {baseDomain, accessToken} = ctx.configuration + + const kommoClient = new KommoClient(accessToken, baseDomain, logger) + const kommoContacts = await kommoClient.searchContacts(input.query) + + const contacts = kommoContacts.map(mapKommoContactToBotpress) + + logger.forBot().info('Search complete:', {count: contacts.length}) + + return {contacts} + } catch (error) { + logger.forBot().error('Failed to search contacts', {error}) + throw new sdk.RuntimeError(getErrorMessage(error)) + } +} \ No newline at end of file diff --git a/integrations/kommo/src/actions/index.ts b/integrations/kommo/src/actions/index.ts new file mode 100644 index 00000000..4dc079a9 --- /dev/null +++ b/integrations/kommo/src/actions/index.ts @@ -0,0 +1,2 @@ +export * from './lead' +export * from './contact' \ No newline at end of file diff --git a/integrations/kommo/src/actions/lead.ts b/integrations/kommo/src/actions/lead.ts new file mode 100644 index 00000000..5053f9d5 --- /dev/null +++ b/integrations/kommo/src/actions/lead.ts @@ -0,0 +1,109 @@ +import * as sdk from '@botpress/sdk' +import * as bp from '.botpress' +import { KommoClient, CreateLeadRequest, UpdateLeadRequest, KommoLead, getErrorMessage } from '../kommo-api' + +// mapping kommo to local schema +function mapKommoLeadToBotpress(lead: KommoLead) { + return { + id: lead.id, + name: lead.name, + price: lead.price, + responsibleUserId: lead.responsible_user_id, + pipelineId: lead.pipeline_id, + statusId: lead.status_id, + createdAt: lead.created_at, + updatedAt: lead.updated_at, + } +} + +export const createLead: bp.IntegrationProps['actions']['createLead'] = async ({ + ctx, + input, + logger, +}) => { + try { + logger.forBot().info('Creating lead with input:', input) + logger.forBot().info('Configuration:', { baseDomain: ctx.configuration.baseDomain }) + + const { baseDomain, accessToken } = ctx.configuration + const kommoClient = new KommoClient(accessToken, baseDomain, logger) + + const leadData: CreateLeadRequest = { + name: input.name, + price: input.price, + responsible_user_id: input.responsibleUserId, + pipeline_id: input.pipelineId, + status_id: input.statusId, + } + + logger.forBot().info('Lead data to send:', leadData) + const kommoLead = await kommoClient.createLead(leadData) + logger.forBot().info('Lead created successfully:', { leadId: kommoLead.id }) + + return { + lead: mapKommoLeadToBotpress(kommoLead), + } + } catch (error) { + logger.forBot().error('Error creating lead:', { error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } +} + + +// handler for updating lead +export const updateLead: bp.IntegrationProps['actions']['updateLead'] = async ({ + ctx, + input, + logger, +}) => { + try { + logger.forBot().info('Updating lead:', input) + + const { baseDomain, accessToken } = ctx.configuration + const kommoClient = new KommoClient(accessToken, baseDomain, logger) + + const updateData: UpdateLeadRequest = { + name: input.name, + price: input.price, + responsible_user_id: input.responsibleUserId, + pipeline_id: input.pipelineId, + status_id: input.statusId, + } + + logger.forBot().info('Update data to send:', updateData) + const kommoLead = await kommoClient.updateLead(input.leadId, updateData) + logger.forBot().info('Lead updated successfully:', { leadId: kommoLead.id }) + + return { + lead: mapKommoLeadToBotpress(kommoLead), + } + } catch (error) { + logger.forBot().error('Error updating lead:', { error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } +} + +// search for lead action +export const searchLeads: bp.IntegrationProps['actions']['searchLeads'] = async ({ + ctx, + input, + logger, +}) => { + try { + logger.forBot().info('Searching leads:', { query: input.query }) + + const { baseDomain, accessToken } = ctx.configuration + const kommoClient = new KommoClient(accessToken, baseDomain, logger) + const kommoLeads = await kommoClient.searchLeads(input.query) + + const leads = kommoLeads.map(mapKommoLeadToBotpress) + + logger.forBot().info('Search complete:', { count: leads.length }) + + return { leads } + } catch (error) { + logger.forBot().error('Failed to search leads', { error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } +} + diff --git a/integrations/kommo/src/index.ts b/integrations/kommo/src/index.ts new file mode 100644 index 00000000..c14cccab --- /dev/null +++ b/integrations/kommo/src/index.ts @@ -0,0 +1,17 @@ +import * as bp from '.botpress' +import { createLead, updateLead, createContact, searchContacts, searchLeads} from './actions' + +export default new bp.Integration({ + register: async () => {}, + unregister: async () => {}, + + actions: { + createLead, + updateLead, + searchLeads, + createContact, + searchContacts, + }, + channels: {}, + handler: async () => {}, +}) diff --git a/integrations/kommo/src/kommo-api/error-handler.ts b/integrations/kommo/src/kommo-api/error-handler.ts new file mode 100644 index 00000000..62ea5704 --- /dev/null +++ b/integrations/kommo/src/kommo-api/error-handler.ts @@ -0,0 +1,65 @@ +import { isAxiosError } from 'axios' +import { ZodError, ZodIssue } from '@botpress/sdk' +import { KommoErrorResponse } from './types' + +const formatZodErrors = (issues: ZodIssue[]) => + 'Validation Error: ' + + issues + .map((issue) => { + const path = issue.path?.length ? `${issue.path.join('.')}: ` : '' + return path ? `${path}${issue.message}` : issue.message + }) + .join('\n') + +export const getErrorMessage = (err: unknown): string => { + if (isAxiosError(err)) { + // server dependent error + const status = err.response?.status + const data = err.response?.data + // always present + const message = err.message + + if (data && typeof data === 'object') { + const kommoError = data as KommoErrorResponse + + if (kommoError.detail) { + let errorMsg = kommoError.detail + + if (kommoError.validation_errors && kommoError.validation_errors.length > 0) { + const validationDetails = kommoError.validation_errors + .flatMap((ve) => ve.errors.map((e) => `${e.path}: ${e.detail}`)) + .join(', ') + errorMsg += ` - ${validationDetails}` + } + + return status ? `${errorMsg} (Status: ${status})` : errorMsg + } + } + + // Fallback for generic axios errors + if (typeof data === 'string' && data.trim()) { + return status ? `${data} (Status: ${status})` : data + } + return status ? `${message} (Status: ${status})` : message + } + + if (err instanceof ZodError) { + return formatZodErrors(err.errors) + } + + if (err instanceof Error) { + return err.message + } + + if (typeof err === 'string') { + return err + } + + if (err && typeof err === 'object' && 'message' in err) { + const message = (err as { message: unknown }).message + if (typeof message === 'string') { + return message + } + } + return 'An unexpected error occurred' +} diff --git a/integrations/kommo/src/kommo-api/index.ts b/integrations/kommo/src/kommo-api/index.ts new file mode 100644 index 00000000..93e96da3 --- /dev/null +++ b/integrations/kommo/src/kommo-api/index.ts @@ -0,0 +1,3 @@ +export * from './types' +export * from './kommo-client' +export * from './error-handler' diff --git a/integrations/kommo/src/kommo-api/kommo-client.ts b/integrations/kommo/src/kommo-api/kommo-client.ts new file mode 100644 index 00000000..77377af6 --- /dev/null +++ b/integrations/kommo/src/kommo-api/kommo-client.ts @@ -0,0 +1,189 @@ +import axios, { AxiosInstance, AxiosError } from 'axios' +import * as sdk from '@botpress/sdk' +import * as bp from '../../.botpress' +import { CreateLeadRequest, KommoLead, KommoCreateResponse, UpdateLeadRequest } from './types' +import { CreateContactRequest, KommoContact, KommoCreateContactResponse, KommoSearchContactsResponse, KommoSearchLeadResponse } from './types' +import { getErrorMessage } from './error-handler' + + +export class KommoClient { + private _axios: AxiosInstance + private _logger: bp.Logger + + constructor(accessToken: string, baseDomain: string, logger: bp.Logger) { + this._logger = logger + + // Ensure basedomain doesn't have protocol prefix or trailing slash + const cleanDomain = baseDomain.replace(/^https?:\/\//, '').replace(/\/$/, '') + + // Create axios instance with Kommo API configuration + this._axios = axios.create({ + baseURL: `https://${cleanDomain}/api/v4`, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + this._logger.forBot().debug('KommoClient initialized', { + baseURL: `https://${cleanDomain}/api/v4` + }) + } + +// -----LEADS------ + async createLead(data: CreateLeadRequest): Promise { + try { + this._logger.forBot().debug('Creating lead in Kommo', { name: data.name }) + const response = await this._axios.post('/leads', [data]) + const createdLeadId = response.data._embedded.leads[0]?.id + + if (!createdLeadId) { + throw new sdk.RuntimeError('No lead ID returned from Kommo') + } + + const lead = await this.getLead(createdLeadId) + + if (!lead) { + throw new sdk.RuntimeError('Failed to fetch created lead') + } + + this._logger.forBot().info('Lead created successfully', { leadId: lead.id }) + return lead + } catch (error) { + this._logger.forBot().error('Failed to create lead', { error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } + } + + + // gets a lead by id + async getLead(leadId: number): Promise { + try { + this._logger.forBot().debug('Fetching lead from Kommo', { leadId }) + + const response = await this._axios.get(`/leads/${leadId}`) + const lead = response.data + + return lead + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + this._logger.forBot().info('Lead not found', { leadId }) + return undefined + } + + this._logger.forBot().error('Failed to fetch lead', { leadId, error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } + } + + // update a single lead + async updateLead(leadId: number, data: UpdateLeadRequest): Promise { + try { + this._logger.forBot().debug('Updating lead in Kommo', { leadId, data }) + await this._axios.patch(`/leads/${leadId}`, data) + const lead = await this.getLead(leadId) + + if (!lead) { + throw new sdk.RuntimeError('Failed to fetch updated lead') + } + + this._logger.forBot().info('Lead updated successfully', { leadId: lead.id }) + return lead + } catch (error) { + this._logger.forBot().error('Failed to update lead', { leadId, error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } + } + + // search leads by query + async searchLeads(query: string): Promise { + try { + this._logger.forBot().debug('Searching for leads', { query }) + const response = await this._axios.get('/leads', { + params: { query } + }) + + const leads = response.data._embedded?.leads || [] + + this._logger.forBot().info('Leads found:', { count: leads.length }) + return leads + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + this._logger.forBot().info('No leads found', { query }) + return [] + } + this._logger.forBot().error('Failed to search leads', { query, error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } + } +// -----Contacts----- + async createContact(data: CreateContactRequest): Promise { + try { + this._logger.forBot().debug('Creating contact in Kommo', { name: data.name }) + // contacts sent to kommo as an array + const response = await this._axios.post('/contacts', [data]) + + // get the ID from the response + const createdContactId = response.data._embedded.contacts[0]?.id + if (!createdContactId) { + throw new sdk.RuntimeError('No contact ID returned from Kommo') + } + // fetch full contact details to return to user + const contact = await this.getContact(createdContactId) + if (!contact) { + throw new sdk.RuntimeError('Failed to fetch created contact') + } + this._logger.forBot().info('Contact created successfully', { contactId: contact.id }) + + return contact + } catch (error) { + this._logger.forBot().error('Failed to create contact', { error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } + } + + // internal method for create contact + async getContact(contactId: number): Promise { + try { + this._logger.forBot().debug('Fetching contact from Kommo', { contactId }) + + const response = await this._axios.get(`/contacts/${contactId}`) + const contact = response.data + return contact + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + this._logger.forBot().info('Contact not found', { contactId }) + return undefined + } + + this._logger.forBot().error('Failed to fetch contact', { contactId, error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } + } + + // method to search contacts by phone number, name or email + async searchContacts(query: string): Promise { + try { + this._logger.forBot().debug('Searching contacts in Kommo', { query }) + + const response = await this._axios.get('/contacts', { + params: { query } + }) + + const contacts = response.data._embedded?.contacts || [] + + this._logger.forBot().info('Contacts found:', { count: contacts.length }) + return contacts + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + this._logger.forBot().info('No contacts found', { query }) + return [] + } + + this._logger.forBot().error('Failed to search contacts', { query, error }) + throw new sdk.RuntimeError(getErrorMessage(error)) + } + } +} + + diff --git a/integrations/kommo/src/kommo-api/types.ts b/integrations/kommo/src/kommo-api/types.ts new file mode 100644 index 00000000..666713f8 --- /dev/null +++ b/integrations/kommo/src/kommo-api/types.ts @@ -0,0 +1,245 @@ + +// -----LEADS------ + +export type KommoLead = { + id: number + name: string + price: number + responsible_user_id: number + group_id: number + status_id: number + pipeline_id: number + loss_reason_id: number | null + created_by: number + updated_by: number + created_at: number + updated_at: number + closed_at: number | null + closest_task_at: number | null + is_deleted: boolean + score: number | null + account_id: number + labor_cost: number | null + is_price_computed: boolean + + custom_fields_values?: Array<{ + field_id: number + field_name: string + field_code?: string | null + field_type: string + values: Array<{ + value: string | number + enum_id?: number + }> + }> + + // Embedded relationships (contacts, companies, tags) + _embedded?: { + tags?: Array<{ + id: number + name: string + }> + companies?: Array<{ + id: number + _links: { + self: { + href: string + } + } + }> + contacts?: Array<{ + id: number + is_main: boolean + _links: { + self: { + href: string + } + } + }> + } + _links?: { + self: { + href: string + } + } +} + + +export type CreateLeadRequest = { + name: string // REQUIRED - only required field! + price?: number + responsible_user_id?: number + pipeline_id?: number + status_id?: number + created_by?: number + updated_by?: number + created_at?: number + updated_at?: number + closed_at?: number + + // Custom fields + custom_fields_values?: Array<{ + field_id: number + values: Array<{ + value: string | number + }> + }> + + // Embedded data (tags, contacts, companies) + _embedded?: { + tags?: Array<{ + id?: number + name?: string + }> + contacts?: Array<{ + id: number + is_main?: boolean + }> + companies?: Array<{ + id: number + }> + } +} + + +export type UpdateLeadRequest = { + id?: number + name?: string + price?: number + responsible_user_id?: number + status_id?: number + pipeline_id?: number + custom_fields_values?: Array<{ + field_id: number + values: Array<{ + value: string | number + }> + }> +} + + +export type KommoCreateResponse = { + _links: { + self: { + href: string + } + } + _embedded: { + leads: Array<{ + id: number + request_id: string + _links: { + self: { + href: string + } + } + }> + } +} + + +export type KommoUpdateResponse = { + _links: { + self: { + href: string + } + } + _embedded: { + leads: KommoLead[] + } +} + +export type KommoSearchLeadResponse = { + _page: number + _links: { + self: { + href: string + } + } + _embedded: { + leads: KommoLead[] + } +} + + +// -------------CONTACTS------------- + +// full contact with all details +export type KommoContact = { + id: number; + name: string; + first_name: string; + last_name: string; + responsible_user_id: number; + group_id: number; + updated_by: number; + created_at: number; + updated_at: number; + closest_task_at: number | null; + is_deleted: boolean; + account_id: number; + +}; + + +// sends data to kommo +export type CreateContactRequest = { + name?: string; + first_name?: string + last_name?: string; + responsible_user_id: number; + created_by: number; + updated_by?: number; +}; + +// what kommo returns after creating contats +export type KommoCreateContactResponse = { + _links:{ + self:{ + href:string; + }; + }; + _embedded:{ + contacts: Array<{ + id:number; + request_id: string; + _links:{ + self:{ + href:string; + }; + }; + }>; + }; +} + +// for searching contacts by phone number +export type KommoSearchContactsResponse = { + _page: number; + _links: { + self: { + href: string; + }; + next?: { + href: string; + }; + }; + _embedded: { + contacts: KommoContact[]; + }; +} + +//----General----- +export type KommoErrorResponse = { + title: string + type: string + status: number + detail: string + validation_errors?: Array<{ + request_id: string + errors: Array<{ + code: string + path: string + detail: string + }> + }> +} \ No newline at end of file diff --git a/integrations/kommo/tsconfig.json b/integrations/kommo/tsconfig.json new file mode 100644 index 00000000..462fc49c --- /dev/null +++ b/integrations/kommo/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["es2022"], + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedParameters": true, + "target": "es2017", + "baseUrl": ".", + "outDir": "dist", + "checkJs": false, + "exactOptionalPropertyTypes": false, + "resolveJsonModule": true, + "noPropertyAccessFromIndexSignature": false, + "noUnusedLocals": false + }, + "include": [".botpress/**/*", "src/**/*", "*.ts", "*.json"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 779fd1d9..2375e1a0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,4 @@ packages: - 'integrations/apify' - 'integrations/zoom' - 'integrations/hubspot-help-desk-hitl' + - 'integrations/kommo'