diff --git a/packages/api/src/crm/@utils/@types/index.ts b/packages/api/src/crm/@utils/@types/index.ts index ce2507a85..67c78c36b 100644 --- a/packages/api/src/crm/@utils/@types/index.ts +++ b/packages/api/src/crm/@utils/@types/index.ts @@ -159,7 +159,7 @@ export * from '../../company/services/zendesk/types'; export * from '../../company/services/hubspot/types'; export * from '../../company/services/zoho/types'; export * from '../../company/services/pipedrive/types'; - +export * from '../../company/services/attio/types' /* engagementType */ export class Email { diff --git a/packages/api/src/crm/company/company.module.ts b/packages/api/src/crm/company/company.module.ts index f9d34bf19..c7eab994c 100644 --- a/packages/api/src/crm/company/company.module.ts +++ b/packages/api/src/crm/company/company.module.ts @@ -14,6 +14,7 @@ import { HubspotService } from './services/hubspot'; import { PipedriveService } from './services/pipedrive'; import { ZendeskService } from './services/zendesk'; import { ZohoService } from './services/zoho'; +import { AttioService } from './services/attio' @Module({ imports: [ @@ -37,7 +38,8 @@ import { ZohoService } from './services/zoho'; ZohoService, PipedriveService, HubspotService, + AttioService ], exports: [SyncService], }) -export class CompanyModule {} +export class CompanyModule { } diff --git a/packages/api/src/crm/company/services/attio/index.ts b/packages/api/src/crm/company/services/attio/index.ts new file mode 100644 index 000000000..0d091dbc9 --- /dev/null +++ b/packages/api/src/crm/company/services/attio/index.ts @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { + CrmObject, + AttioCompanyInput, + AttioCompanyOutput, +} from '@crm/@utils/@types'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { ApiResponse } from '@@core/utils/types'; +import { ICompanyService } from '@crm/company/types'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class AttioService implements ICompanyService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.company.toUpperCase() + ':' + AttioService.name, + ); + this.registry.registerService('attio', this); + } + + async addCompany( + companyData: AttioCompanyInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'attio', + }, + }); + + const resp = await axios.post( + 'https://api.attio.com/v2/objects/companies/records', + JSON.stringify({ + data: companyData + }), + { + headers: { + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + 'Content-Type': 'application/json', + }, + }, + ); + return { + data: resp.data.data, + message: 'Attio company created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Attio', + CrmObject.company, + ActionType.POST, + ); + } + } + + async syncCompanies( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'attio', + }, + }); + const resp = await axios.post( + `https://api.attio.com/v2/objects/companies/records/query`, {}, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data.data, + message: 'Attio companies retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Attio', + CrmObject.company, + ActionType.POST, + ); + } + } +} diff --git a/packages/api/src/crm/company/services/attio/mappers.ts b/packages/api/src/crm/company/services/attio/mappers.ts new file mode 100644 index 000000000..af858e958 --- /dev/null +++ b/packages/api/src/crm/company/services/attio/mappers.ts @@ -0,0 +1,167 @@ +import { AttioCompanyInput, AttioCompanyOutput } from '@crm/@utils/@types'; +import { + UnifiedCompanyInput, + UnifiedCompanyOutput, +} from '@crm/company/types/model.unified'; +import { ICompanyMapper } from '@crm/company/types'; +import { Utils } from '@crm/deal/utils'; + +export class AttioCompanyMapper implements ICompanyMapper { + private readonly utils: Utils; + + constructor() { + this.utils = new Utils(); + } + async desunify( + source: UnifiedCompanyInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: AttioCompanyInput = { + values: { + name: [{ + value: source.name + }], + categories: [{ + option: source.industry + }] + } + } + // const result: AttioCompanyInput = { + // city: '', + // name: source.name, + // phone: '', + // state: '', + // domain: '', + // industry: source.industry, + // }; + + // Assuming 'phone_numbers' array contains at least one phone number + // const primaryPhone = source.phone_numbers?.[0]?.phone_number; + // if (primaryPhone) { + // result.values = primaryPhone; + // } + if (source.addresses) { + const address = source.addresses[0]; + if (address) { + // result.city = address.city; + // result.state = address.state; + result.values.primary_location = [{ + locality: address.city, + line_1: address.street_1, + line_2: address.street_2, + line_3: null, + line_4: null, + region: address.state + "," + address.country, + postcode: address.postal_code, + latitude: null, + longitude: null, + country_code: null, + }] + } + } + + if (source.user_id) { + const owner_id = await this.utils.getRemoteIdFromUserUuid(source.user_id); + if (owner_id) { + result.values.team = [{ + target_object: 'people', + target_record_id: owner_id, + }]; + } + } + + // Attio company does not have attribute for email address + // Attio Company doest not have direct mapping of number of employees + + + + if (customFieldMappings && source.field_mappings) { + for (const fieldMapping of source.field_mappings) { + for (const key in fieldMapping) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === key, + ); + if (mapping) { + result.values[mapping.remote_id] = fieldMapping[key]; + } + } + } + } + + return result; + } + + async unify( + source: AttioCompanyOutput | AttioCompanyOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleCompanyToUnified(source, customFieldMappings); + } + // Handling array of AttioCompanyOutput + return Promise.all( + source.map((company) => + this.mapSingleCompanyToUnified(company, customFieldMappings), + ), + ); + } + + private async mapSingleCompanyToUnified( + company: AttioCompanyOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings = + customFieldMappings?.map((mapping) => ({ + [mapping.slug]: company.values[mapping.remote_id], + })) || []; + + let opts: any = {}; + + if (company.values.team[0]?.target_record_id) { + const owner_id = await this.utils.getUserUuidFromRemoteId( + company.values.team[0].target_record_id, + 'attio', + ); + if (owner_id) { + opts = { + user_id: owner_id, + }; + } + } + + return { + name: company.values.name[0]?.value, + industry: typeof company.values.categories[0]?.option === "string" ? company.values.categories[0]?.option : company.values.categories[0]?.option.title, + number_of_employees: 0, // Placeholder, as there's no direct mapping provided + addresses: [ + { + street_1: company.values.primary_location[0]?.line_1, + city: company.values.primary_location[0]?.locality, + state: company.values.primary_location[0]?.region, + postal_code: company.values.primary_location[0]?.postcode, + country: company.values.primary_location[0]?.country_code, + address_type: 'primary', + owner_type: 'company', + }, + ], // Assuming 'street', 'city', 'state', 'postal_code', 'country' are properties in company.properties + phone_numbers: [ + { + phone_number: '', + phone_type: 'primary', + owner_type: 'company', + }, + ], + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/company/services/attio/types.ts b/packages/api/src/crm/company/services/attio/types.ts new file mode 100644 index 000000000..8584caf5c --- /dev/null +++ b/packages/api/src/crm/company/services/attio/types.ts @@ -0,0 +1,133 @@ +interface WorkspaceId { + workspace_id: string; + object_id: string; + record_id: string; +} + +interface CreatedByActor { + type: string; + id: string | null; +} + +interface ValueItemBase { + active_from?: string; + active_until?: string | null; + created_by_actor?: CreatedByActor; + attribute_type?: string; +} + +interface NumberValueItem extends ValueItemBase { + value: number; +} + +interface TextValueItem extends ValueItemBase { + value: string; +} + +interface DomainValueItem extends ValueItemBase { + domain: string; + root_domain?: string; +} + +interface LocationValueItem extends ValueItemBase { + line_1: string | null; + line_2: string | null; + line_3: string | null; + line_4: string | null; + locality: string | null; + region: string | null; + postcode: string | null; + latitude: string | null; + longitude: string | null; + country_code: string | null; +} + +interface TeamValueItemOption1 extends ValueItemBase { + target_object: string; + target_record_id: string; +} + +interface TeamValueItemOption2 extends ValueItemBase { + target_object: string; + [key: string]: any; +} + +interface CategoryValueItem extends ValueItemBase { + option: string | Option; +} + +interface StrongConnectionValueItem extends ValueItemBase { + referenced_actor_type: string; + referenced_actor_id: string; +} + +interface OptionId { + workspace_id: string; + object_id: string; + attribute_id: string; + option_id: string; +} + +interface Option { + id: OptionId; + title: string; + is_archived: boolean; +} + +interface ActorReference extends ValueItemBase { + referenced_actor_type: string; + referenced_actor_id: string; +} + +interface OwnerActor { + type: string; + id: string; +} + +interface Interaction extends ValueItemBase { + interaction_type: string; + interacted_at: string; + owner_actor: OwnerActor; +} + + +export interface AttioCompany { + id: WorkspaceId; + created_at: string; + values: { + domains?: DomainValueItem[]; + name?: TextValueItem[]; + description?: TextValueItem[]; + + team?: TeamValueItemOption1[] | TeamValueItemOption2[]; + + primary_location?: LocationValueItem[]; + categories?: CategoryValueItem[]; + logo_url?: TextValueItem[]; + twitter_follower_count?: NumberValueItem[]; + foundation_date?: TextValueItem[]; + strongest_connection_user?: StrongConnectionValueItem[]; + estimated_arr_usd?: CategoryValueItem[]; + strongest_connection_strength_legacy?: NumberValueItem[]; + employee_range?: CategoryValueItem[]; + twitter?: TeamValueItemOption1[]; + angellist?: TextValueItem[]; + facebook?: TextValueItem[]; + linkedin?: TextValueItem[]; + instagram?: TextValueItem[]; + strongest_connection_strength?: CategoryValueItem[]; + last_calendar_interaction?: Interaction[]; + last_email_interaction?: Interaction[]; + first_interaction?: Interaction[]; + first_email_interaction?: Interaction[]; + first_calendar_interaction?: Interaction[]; + next_calendar_interaction?: Interaction[]; + last_interaction?: Interaction[]; + created_at?: TextValueItem[]; + created_by?: ActorReference[] + }; +} + + +export type AttioCompanyInput = Partial; +export type AttioCompanyOutput = AttioCompanyInput; diff --git a/packages/api/src/crm/company/types/mappingsTypes.ts b/packages/api/src/crm/company/types/mappingsTypes.ts index b06e30fc7..1264de266 100644 --- a/packages/api/src/crm/company/types/mappingsTypes.ts +++ b/packages/api/src/crm/company/types/mappingsTypes.ts @@ -3,12 +3,15 @@ import { HubspotCompanyMapper } from '@crm/company/services/hubspot/mappers'; import { PipedriveCompanyMapper } from '@crm/company/services/pipedrive/mappers'; import { ZendeskCompanyMapper } from '@crm/company/services/zendesk/mappers'; import { ZohoCompanyMapper } from '@crm/company/services/zoho/mappers'; +import { AttioCompanyMapper } from '@crm/company/services/attio/mappers'; + const hubspotCompanyMapper = new HubspotCompanyMapper(); const zendeskCompanyMapper = new ZendeskCompanyMapper(); const zohoCompanyMapper = new ZohoCompanyMapper(); const pipedriveCompanyMapper = new PipedriveCompanyMapper(); const freshSalesCompanyMapper = new FreshsalesCompanyMapper(); +const attioCompanyMapper = new AttioCompanyMapper(); export const companyUnificationMapping = { hubspot: { @@ -31,4 +34,8 @@ export const companyUnificationMapping = { unify: freshSalesCompanyMapper.unify.bind(freshSalesCompanyMapper), desunify: freshSalesCompanyMapper.desunify.bind(freshSalesCompanyMapper), }, + attio: { + unify: attioCompanyMapper.unify.bind(attioCompanyMapper), + desunify: attioCompanyMapper.desunify.bind(attioCompanyMapper) + } }; diff --git a/packages/api/src/crm/contact/services/attio/index.ts b/packages/api/src/crm/contact/services/attio/index.ts index 2e9f8ecb1..7c5df6103 100644 --- a/packages/api/src/crm/contact/services/attio/index.ts +++ b/packages/api/src/crm/contact/services/attio/index.ts @@ -80,8 +80,8 @@ export class AttioService implements IContactService { provider_slug: 'attio', }, }); - console.log('Before Axios'); - console.log(this.cryptoService.decrypt(connection.access_token)); + // console.log('Before Axios'); + // console.log(this.cryptoService.decrypt(connection.access_token)); const resp = await axios.post( `https://api.attio.com/v2/objects/people/records/query`, @@ -110,7 +110,7 @@ export class AttioService implements IContactService { this.logger, 'Attio', CrmObject.contact, - ActionType.GET, + ActionType.POST, ); } } diff --git a/packages/api/src/crm/contact/services/attio/mappers.ts b/packages/api/src/crm/contact/services/attio/mappers.ts index 7ece91954..0fa7c84a1 100644 --- a/packages/api/src/crm/contact/services/attio/mappers.ts +++ b/packages/api/src/crm/contact/services/attio/mappers.ts @@ -106,7 +106,7 @@ export class AttioContactMapper implements IContactMapper { }[], ): Promise { const field_mappings = customFieldMappings.map((mapping) => ({ - [mapping.slug]: contact[mapping.remote_id], + [mapping.slug]: contact.values[mapping.remote_id], })); const address: Address = { street_1: '', @@ -118,13 +118,14 @@ export class AttioContactMapper implements IContactMapper { const opts: any = {}; return { - first_name: contact.values.name[0].first_name, - last_name: contact.values.name[0].last_name, - email_addresses: contact.values.email_addresses.map((e) => ({ + first_name: contact.values.name[0]?.first_name, + last_name: contact.values.name[0]?.last_name, + // user_id: contact.values.created_by[0]?.referenced_actor_id, + email_addresses: contact.values.email_addresses?.map((e) => ({ email_address: e.email_address, email_address_type: e.attribute_type ? e.attribute_type : '', })), // Map each email - phone_numbers: contact.values.phone_numbers.map((p) => ({ + phone_numbers: contact.values.phone_numbers?.map((p) => ({ phone_number: p.original_phone_number, phone_type: p.attribute_type ? p.attribute_type : '', })), // Map each phone number, diff --git a/packages/api/src/crm/contact/services/attio/types.ts b/packages/api/src/crm/contact/services/attio/types.ts index aab95c9d6..950da6ac9 100644 --- a/packages/api/src/crm/contact/services/attio/types.ts +++ b/packages/api/src/crm/contact/services/attio/types.ts @@ -40,12 +40,12 @@ interface LocationValueItem extends ValueItemBase { line_2: string | null; line_3: string | null; line_4: string | null; - locality: string; - region: string; - postcode: string; - country_code: string; - latitude: string; - longitude: string; + locality: string | null; + region: string | null; + postcode: string | null; + country_code: string | null; + latitude: string | null; + longitude: string | null; } interface RecordReferenceValueItem extends ValueItemBase { @@ -53,6 +53,11 @@ interface RecordReferenceValueItem extends ValueItemBase { target_record_id: string; } +interface RecordReferenceValueItem2 extends ValueItemBase { + target_object: string; + [key: string]: any; +} + interface ActorReferenceValueItem extends ValueItemBase { referenced_actor_type: string; referenced_actor_id: string; @@ -69,6 +74,7 @@ interface EmailAddressValueItem extends ValueItemBase { interface PhoneValueItem extends ValueItemBase { country_code?: string; original_phone_number: string; + phone_number?: string; } interface PersonalNameValueItem extends ValueItemBase { @@ -95,7 +101,7 @@ export interface AttioContact { avatar_url?: TextValueItem[]; job_title?: TextValueItem[]; next_calendar_interaction?: InteractionValueItem[]; - company?: RecordReferenceValueItem[]; + company?: RecordReferenceValueItem[] | RecordReferenceValueItem2[]; primary_location?: LocationValueItem[]; angellist?: TextValueItem[]; description?: TextValueItem[]; @@ -111,6 +117,7 @@ export interface AttioContact { facebook?: TextValueItem[]; name?: PersonalNameValueItem[]; first_calendar_interaction?: InteractionValueItem[]; + twitter_follower_count?: NumberValueItem[]; instagram?: TextValueItem[]; first_email_interaction?: InteractionValueItem[]; phone_numbers?: PhoneValueItem[];