-
Notifications
You must be signed in to change notification settings - Fork 185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add integration with Redtail (#554) #643
base: main
Are you sure you want to change the base?
Changes from 4 commits
8c611a8
9f52eb2
bb62b11
1b1c061
f78d00d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,97 @@ | ||||||||
import { Injectable } from '@nestjs/common'; | ||||||||
import { IContactService } from '@crm/contact/types'; | ||||||||
import { CrmObject } from '@crm/@lib/@types'; | ||||||||
import axios from 'axios'; | ||||||||
import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; | ||||||||
import { LoggerService } from '@@core/@core-services/logger/logger.service'; | ||||||||
import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; | ||||||||
import { ApiResponse } from '@@core/utils/types'; | ||||||||
import { ServiceRegistry } from '../registry.service'; | ||||||||
import { RedtailContactInput, RedtailContactOutput } from './types'; | ||||||||
import { SyncParam } from '@@core/utils/types/interface'; | ||||||||
|
||||||||
@Injectable() | ||||||||
export class RedtailService implements IContactService { | ||||||||
constructor( | ||||||||
private prisma: PrismaService, | ||||||||
private logger: LoggerService, | ||||||||
private cryptoService: EncryptionService, | ||||||||
private registry: ServiceRegistry, | ||||||||
) { | ||||||||
this.logger.setContext( | ||||||||
CrmObject.contact.toUpperCase() + ':' + RedtailService.name, | ||||||||
); | ||||||||
this.registry.registerService('redtail', this); | ||||||||
} | ||||||||
|
||||||||
async addContact( | ||||||||
contactData: RedtailContactInput, | ||||||||
linkedUserId: string, | ||||||||
): Promise<ApiResponse<RedtailContactOutput>> { | ||||||||
try { | ||||||||
const connection = await this.prisma.connections.findFirst({ | ||||||||
where: { | ||||||||
id_linked_user: linkedUserId, | ||||||||
provider_slug: 'redtail', | ||||||||
vertical: 'crm', | ||||||||
}, | ||||||||
}); | ||||||||
|
||||||||
const authHeader = this.createAuthHeader(connection.api_key, connection.user_key); | ||||||||
|
||||||||
const resp = await axios.post( | ||||||||
`${connection.account_url}/contacts`, | ||||||||
JSON.stringify(contactData), | ||||||||
{ | ||||||||
headers: { | ||||||||
'Content-Type': 'application/json', | ||||||||
Authorization: authHeader, | ||||||||
}, | ||||||||
}, | ||||||||
); | ||||||||
return { | ||||||||
data: resp.data.data, | ||||||||
message: 'Redtail contact created', | ||||||||
statusCode: 201, | ||||||||
}; | ||||||||
} catch (error) { | ||||||||
throw error; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove redundant catch clause. The catch clause that only rethrows the original error is unnecessary and can be removed to simplify the code. - } catch (error) {
- throw error;
+ } Committable suggestion
Suggested change
ToolsBiome
|
||||||||
} | ||||||||
} | ||||||||
|
||||||||
async sync(data: SyncParam): Promise<ApiResponse<RedtailContactOutput[]>> { | ||||||||
try { | ||||||||
const { linkedUserId } = data; | ||||||||
|
||||||||
const connection = await this.prisma.connections.findFirst({ | ||||||||
where: { | ||||||||
id_linked_user: linkedUserId, | ||||||||
provider_slug: 'redtail', | ||||||||
vertical: 'crm', | ||||||||
}, | ||||||||
}); | ||||||||
|
||||||||
const authHeader = this.createAuthHeader(connection.api_key, connection.user_key); | ||||||||
|
||||||||
const resp = await axios.get(`${connection.account_url}/contacts`, { | ||||||||
headers: { | ||||||||
'Content-Type': 'application/json', | ||||||||
Authorization: authHeader, | ||||||||
}, | ||||||||
}); | ||||||||
|
||||||||
return { | ||||||||
data: resp.data.data, | ||||||||
message: 'Redtail contacts retrieved', | ||||||||
statusCode: 200, | ||||||||
}; | ||||||||
} catch (error) { | ||||||||
throw error; | ||||||||
} | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove redundant catch clause. The catch clause that only rethrows the original error is unnecessary and can be removed to simplify the code. - } catch (error) {
- throw error;
+ } Committable suggestion
Suggested change
ToolsBiome
|
||||||||
} | ||||||||
|
||||||||
private createAuthHeader(apiKey: string, userKey: string): string { | ||||||||
const credentials = `${apiKey}:${userKey}`; | ||||||||
return `Userkeyauth ${Buffer.from(credentials).toString('base64')}`; | ||||||||
} | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { | ||
UnifiedContactInput, | ||
UnifiedContactOutput, | ||
} from '@crm/contact/types/model.unified'; | ||
import { IContactMapper } from '@crm/contact/types'; | ||
import { RedtailContactInput, RedtailContactOutput } from './types'; | ||
import { Utils } from '@crm/lib/utils'; | ||
import { MappersRegistry } from '@core/core-services/registries/mappers.registry'; | ||
import { Injectable } from '@nestjs/common'; | ||
|
||
@Injectable() | ||
export class RedtailContactMapper implements IContactMapper { | ||
constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { | ||
this.mappersRegistry.registerService('crm', 'contact', 'redtail', this); | ||
} | ||
|
||
async desunify( | ||
source: UnifiedContactInput, | ||
customFieldMappings?: { | ||
slug: string; | ||
remote_id: string; | ||
}[], | ||
): Promise<RedtailContactInput> { | ||
const primaryEmail = source.email_addresses?.[0]?.email_address; | ||
const primaryPhone = source.phone_numbers?.[0]?.phone_number; | ||
|
||
const emailObject = primaryEmail | ||
? [{ value: primaryEmail, primary: true, label: '' }] | ||
: []; | ||
const phoneObject = primaryPhone | ||
? [ | ||
{ | ||
value: primaryPhone, | ||
primary: true, | ||
label: | ||
source.phone_numbers?.[0]?.phone_type == 'MOBILE' | ||
? 'Mobile' | ||
: '', | ||
}, | ||
] | ||
: []; | ||
const result: RedtailContactInput = { | ||
name: `${source.first_name} ${source.last_name}`, | ||
email: emailObject, | ||
phone: phoneObject, | ||
}; | ||
|
||
if (source.user_id) { | ||
const owner = await this.utils.getUser(source.user_id); | ||
if (owner) { | ||
result.owner_id = { | ||
id: Number(owner.remote_id), | ||
name: owner.name, | ||
email: owner.email, | ||
has_pic: 0, | ||
pic_hash: "", | ||
active_flag: false, | ||
value: 0, | ||
}; | ||
} | ||
} | ||
|
||
if (customFieldMappings && source.field_mappings) { | ||
for (const [k, v] of Object.entries(source.field_mappings)) { | ||
const mapping = customFieldMappings.find( | ||
(mapping) => mapping.slug === k, | ||
); | ||
if (mapping) { | ||
result[mapping.remote_id] = v; | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
async unify( | ||
source: RedtailContactOutput | RedtailContactOutput[], | ||
connectionId: string, | ||
customFieldMappings?: { | ||
slug: string; | ||
remote_id: string; | ||
}[], | ||
): Promise<UnifiedContactOutput | UnifiedContactOutput[]> { | ||
if (!Array.isArray(source)) { | ||
return await this.mapSingleContactToUnified( | ||
source, | ||
connectionId, | ||
customFieldMappings, | ||
); | ||
} | ||
|
||
return Promise.all( | ||
source.map((contact) => | ||
this.mapSingleContactToUnified( | ||
contact, | ||
connectionId, | ||
customFieldMappings, | ||
), | ||
), | ||
); | ||
} | ||
|
||
private async mapSingleContactToUnified( | ||
contact: RedtailContactOutput, | ||
connectionId: string, | ||
customFieldMappings?: { | ||
slug: string; | ||
remote_id: string; | ||
}[], | ||
): Promise<UnifiedContactOutput> { | ||
const field_mappings: { [key: string]: any } = {}; | ||
if (customFieldMappings) { | ||
for (const mapping of customFieldMappings) { | ||
field_mappings[mapping.slug] = contact[mapping.remote_id]; | ||
} | ||
} | ||
let opts: any = {}; | ||
if (contact.owner_id?.id) { | ||
const user_id = await this.utils.getUserUuidFromRemoteId( | ||
String(contact.owner_id.id), | ||
connectionId, | ||
); | ||
if (user_id) { | ||
opts = { | ||
user_id: user_id, | ||
}; | ||
} | ||
} | ||
|
||
return { | ||
remote_id: String(contact.id), | ||
remote_data: contact, | ||
first_name: contact.first_name, | ||
last_name: contact.last_name, | ||
email_addresses: contact.email?.map((e) => ({ | ||
email_address: e.value, | ||
email_address_type: e.label ? e.label.toUpperCase() : null, | ||
})), // Map each email | ||
phone_numbers: contact.phone?.map((p) => ({ | ||
phone_number: p.value, | ||
phone_type: p.label ? p.label.toUpperCase() : null, | ||
})), // Map each phone number, | ||
field_mappings, | ||
...opts, | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
export interface RedtailContact { | ||
id: string; | ||
company_id: number; | ||
owner_id: { | ||
id: number; | ||
name: string; | ||
email: string; | ||
has_pic: number; | ||
pic_hash: string; | ||
active_flag: boolean; | ||
value: number; | ||
}; | ||
org_id: { | ||
name: string; | ||
people_count: number; | ||
owner_id: number; | ||
address: string; | ||
active_flag: boolean; | ||
cc_email: string; | ||
value: number; | ||
}; | ||
name: string; | ||
first_name: string; | ||
last_name: string; | ||
open_deals_count: number; | ||
related_open_deals_count: number; | ||
closed_deals_count: number; | ||
related_closed_deals_count: number; | ||
participant_open_deals_count: number; | ||
participant_closed_deals_count: number; | ||
email_messages_count: number; | ||
activities_count: number; | ||
done_activities_count: number; | ||
undone_activities_count: number; | ||
files_count: number; | ||
notes_count: number; | ||
followers_count: number; | ||
won_deals_count: number; | ||
related_won_deals_count: number; | ||
lost_deals_count: number; | ||
related_lost_deals_count: number; | ||
active_flag: boolean; | ||
phone: { value: string; primary: boolean; label: string }[]; | ||
email: { value: string; primary: boolean; label: string }[]; | ||
primary_email: string; | ||
first_char: string; | ||
update_time: Date; | ||
add_time: Date; | ||
visible_to: string; | ||
marketing_status: string; | ||
picture_id: { | ||
item_type: string; | ||
item_id: number; | ||
active_flag: boolean; | ||
add_time: string; | ||
update_time: string; | ||
added_by_user_id: number; | ||
pictures: { | ||
'128': string; | ||
'512': string; | ||
}; | ||
value: number; | ||
}; | ||
next_activity_date: string; | ||
next_activity_time: string; | ||
next_activity_id: number; | ||
last_activity_id: number; | ||
last_activity_date: string; | ||
last_incoming_mail_time: string; | ||
last_outgoing_mail_time: string; | ||
label: number; | ||
org_name: string; | ||
owner_name: string; | ||
cc_email: string; | ||
[key: string]: any; | ||
} | ||
|
||
export type RedtailContactInput = Partial<RedtailContact>; | ||
export type RedtailContactOutput = RedtailContactInput; | ||
Comment on lines
+78
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify the purpose of The |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use template literals for string concatenation.
Instead of using string concatenation, prefer template literals for better readability.
Committable suggestion
Tools
Biome