-
Notifications
You must be signed in to change notification settings - Fork 195
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
fix: #448 #481
base: main
Are you sure you want to change the base?
fix: #448 #481
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { HttpModule } from '@nestjs/axios'; | ||
import { AffinityConnectionService } from './services/affinity/affinity.service'; | ||
import { CrmConnectionsService } from './services/crm.connection.service'; | ||
import { PrismaService } from '@@core/prisma/prisma.service'; | ||
import { LoggerService } from '@@core/logger/logger.service'; | ||
|
@@ -15,7 +17,7 @@ import { AttioConnectionService } from './services/attio/attio.service'; | |
import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; | ||
|
||
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. Consider removing the trailing comma after HttpModule for consistency. |
||
@Module({ | ||
imports: [WebhookModule], | ||
imports: [WebhookModule, HttpModule,], | ||
providers: [ | ||
CrmConnectionsService, | ||
PrismaService, | ||
|
@@ -31,6 +33,7 @@ import { ConnectionsStrategiesService } from '@@core/connections-strategies/conn | |
ZohoConnectionService, | ||
ZendeskConnectionService, | ||
PipedriveConnectionService, | ||
AffinityConnectionService, | ||
], | ||
exports: [CrmConnectionsService], | ||
}) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { HttpService } from '@nestjs/axios'; | ||
import { lastValueFrom } from 'rxjs'; | ||
import { Injectable } from '@nestjs/common'; | ||
import { | ||
CallbackParams, | ||
ICrmConnectionService, | ||
RefreshParams, | ||
} from '../../types'; | ||
import { PrismaService } from '@@core/prisma/prisma.service'; | ||
import axios from 'axios'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import { Action, handleServiceError } from '@@core/utils/errors'; | ||
import { EnvironmentService } from '@@core/environment/environment.service'; | ||
import { EncryptionService } from '@@core/encryption/encryption.service'; | ||
import { ServiceRegistry } from '../registry.service'; | ||
import { LoggerService } from '@@core/logger/logger.service'; | ||
import { | ||
OAuth2AuthData, | ||
CONNECTORS_METADATA, | ||
providerToType, | ||
} from '@panora/shared'; | ||
import { AuthStrategy } from '@panora/shared'; | ||
import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; | ||
import { ConnectionUtils } from '@@core/connections/@utils'; | ||
|
||
@Injectable() | ||
export class AffinityConnectionService implements ICrmConnectionService { | ||
constructor( | ||
private prisma: PrismaService, | ||
private logger: LoggerService, | ||
private env: EnvironmentService, | ||
private cryptoService: EncryptionService, | ||
private registry: ServiceRegistry, | ||
private httpService: HttpService, // Add HttpService for making HTTP requests | ||
) { | ||
this.logger.setContext(AffinityConnectionService.name); | ||
this.registry.registerService('affinity', this); | ||
} | ||
|
||
async handleCallback(opts: CallbackParams): Promise<Connection> { | ||
const { linkedUserId, projectId, code } = opts; | ||
const clientId = this.env.get('AFFINITY_CLIENT_ID'); | ||
const clientSecret = this.env.get('AFFINITY_CLIENT_SECRET'); | ||
const redirectUri = this.env.get('AFFINITY_REDIRECT_URI'); | ||
|
||
try { | ||
const tokenResponse = await lastValueFrom(this.httpService.post('https://api.affinity.co/oauth/token', { | ||
grant_type: 'authorization_code', | ||
code: code, | ||
redirect_uri: redirectUri, | ||
client_id: clientId, | ||
client_secret: clientSecret, | ||
})); | ||
|
||
const { access_token, refresh_token, expires_in } = tokenResponse.data; | ||
|
||
const connection = await this.prisma.connection.create({ | ||
data: { | ||
linkedUserId, | ||
projectId, | ||
accessToken: access_token, | ||
refreshToken: refresh_token, | ||
expiresAt: new Date(Date.now() + expires_in * 1000), | ||
provider: 'affinity', | ||
}, | ||
}); | ||
|
||
return connection; | ||
} catch (error) { | ||
this.logger.error('Error handling OAuth callback for Affinity', error); | ||
throw new Error('Failed to handle OAuth callback for Affinity'); | ||
} | ||
} | ||
|
||
async handleTokenRefresh(opts: RefreshParams): Promise<any> { | ||
const { connectionId, refreshToken } = opts; | ||
const clientId = this.env.get('AFFINITY_CLIENT_ID'); | ||
const clientSecret = this.env.get('AFFINITY_CLIENT_SECRET'); | ||
|
||
try { | ||
const tokenResponse = await lastValueFrom(this.httpService.post('https://api.affinity.co/oauth/token', { | ||
grant_type: 'refresh_token', | ||
refresh_token: refreshToken, | ||
client_id: clientId, | ||
client_secret: clientSecret, | ||
})); | ||
|
||
const { access_token, refresh_token, expires_in } = tokenResponse.data; | ||
|
||
await this.prisma.connection.update({ | ||
where: { id: connectionId }, | ||
data: { | ||
accessToken: access_token, | ||
refreshToken: refresh_token, | ||
expiresAt: new Date(Date.now() + expires_in * 1000), | ||
}, | ||
}); | ||
|
||
return { access_token, refresh_token, expires_in }; | ||
} catch (error) { | ||
this.logger.error('Error refreshing token for Affinity', error); | ||
throw new Error(`Failed to refresh token for Affinity: ${error.message}`); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
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. Consider adding a newline at the end of the file for POSIX compliance. |
||
import { IContactService } from '@crm/contact/types'; | ||
import { CrmObject } from '@crm/@lib/@types'; | ||
import axios from 'axios'; | ||
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 { ServiceRegistry } from '../registry.service'; | ||
import { AffinityContactInput, AffinityContactOutput } from './types'; | ||
|
||
@Injectable() | ||
export class AffinityService implements IContactService { | ||
constructor( | ||
private prisma: PrismaService, | ||
private logger: LoggerService, | ||
private cryptoService: EncryptionService, | ||
private registry: ServiceRegistry, | ||
private httpService: HttpService, | ||
) { | ||
this.logger.setContext( | ||
'CRM:Contact:' + AffinityService.name, | ||
); | ||
this.registry.registerService('affinity', this); | ||
} | ||
|
||
async addContact( | ||
contactData: AffinityContactInput, | ||
linkedUserId: string, | ||
): Promise<ApiResponse<AffinityContactOutput>> { | ||
try { | ||
const accessToken = await this.getAccessToken(linkedUserId); | ||
const response = await lastValueFrom( | ||
this.httpService.post('https://api.affinity.co/contacts', contactData, { | ||
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. Use |
||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
}, | ||
}) | ||
); | ||
|
||
return { | ||
success: true, | ||
data: response.data, | ||
}; | ||
} catch (error) { | ||
this.logger.error('Error adding contact to Affinity', error); | ||
return { | ||
success: false, | ||
message: 'Failed to add contact to Affinity', | ||
}; | ||
} | ||
} | ||
|
||
async syncContacts( | ||
linkedUserId: string, | ||
): Promise<ApiResponse<AffinityContactOutput[]>> { | ||
try { | ||
const accessToken = await this.getAccessToken(linkedUserId); | ||
const response = await lastValueFrom( | ||
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. Use |
||
this.httpService.get('https://api.affinity.co/contacts', { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
}, | ||
}) | ||
); | ||
|
||
return { | ||
success: true, | ||
data: response.data, | ||
}; | ||
} catch (error) { | ||
this.logger.error('Error syncing contacts from Affinity', error); | ||
return { | ||
success: false, | ||
message: 'Failed to sync contacts from Affinity', | ||
}; | ||
} | ||
} | ||
|
||
private async getAccessToken(linkedUserId: string): Promise<string> { | ||
const connection = await this.prisma.connection.findUnique({ | ||
where: { | ||
linkedUserId, | ||
provider: 'affinity', | ||
}, | ||
}); | ||
|
||
if (!connection) { | ||
throw new Error('No Affinity connection found for user'); | ||
} | ||
|
||
if (new Date(connection.expiresAt) < new Date()) { | ||
const refreshResponse = await lastValueFrom( | ||
this.httpService.post('https://api.affinity.co/oauth/token', { | ||
grant_type: 'refresh_token', | ||
refresh_token: connection.refreshToken, | ||
client_id: this.env.get('AFFINITY_CLIENT_ID'), | ||
client_secret: this.env.get('AFFINITY_CLIENT_SECRET'), | ||
}) | ||
); | ||
|
||
const { access_token, refresh_token, expires_in } = refreshResponse.data; | ||
|
||
await this.prisma.connection.update({ | ||
where: { id: connection.id }, | ||
data: { | ||
accessToken: access_token, | ||
refreshToken: refresh_token, | ||
expiresAt: new Date(Date.now() + expires_in * 1000), | ||
}, | ||
}); | ||
|
||
return access_token; | ||
} | ||
|
||
return connection.accessToken; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { Address } from '@crm/@lib/@types'; | ||
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. Consider adding a newline at the end of the file. |
||
import { | ||
UnifiedContactInput, | ||
UnifiedContactOutput, | ||
} from '@crm/contact/types/model.unified'; | ||
import { IContactMapper } from '@crm/contact/types'; | ||
import { Utils } from '@crm/@lib/@utils'; | ||
import { AffinityContactInput, AffinityContactOutput } from './types'; | ||
|
||
export class AffinityMapper implements IContactMapper { | ||
desunify( | ||
source: UnifiedContactInput, | ||
customFieldMappings?: { | ||
slug: string; | ||
remote_id: string; | ||
}[], | ||
): AffinityContactInput { | ||
// Mapping from unified contact to Affinity contact | ||
return { | ||
firstName: source.firstName, | ||
lastName: source.lastName, | ||
email: source.email, | ||
phone: source.phone, | ||
company: source.company, | ||
}; | ||
} | ||
|
||
unify( | ||
source: AffinityContactOutput | AffinityContactOutput[], | ||
customFieldMappings?: { | ||
slug: string; | ||
remote_id: string; | ||
}[], | ||
): UnifiedContactOutput | UnifiedContactOutput[] { | ||
if (Array.isArray(source)) { | ||
Comment on lines
+21
to
+35
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. Handle potential null or undefined values in |
||
return source.map(contact => this.unify(contact)); | ||
} | ||
|
||
return { | ||
id: source.id, | ||
firstName: source.firstName, | ||
lastName: source.lastName, | ||
email: source.email, | ||
phone: source.phone, | ||
company: source.company, | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export interface AffinityContact { | ||
id: string; | ||
firstName: string; | ||
lastName: string; | ||
email: string; | ||
phone?: string; | ||
company?: string; | ||
} | ||
|
||
export type AffinityContactInput = Partial<AffinityContact>; | ||
export type AffinityContactOutput = AffinityContact; | ||
|
||
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. Add a newline at the end of the file. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,43 @@ | ||
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/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 { IContactService, ApiResponse, DesunifyReturnType } from '../../types'; | ||
import { PrismaService } from '../../../prisma/prisma.service'; | ||
import { LoggerService } from '../../../logger/logger.service'; | ||
import { EncryptionService } from '../../../encryption/encryption.service'; | ||
import { ServiceRegistry } from '../../../registry/service.registry'; | ||
import { AffinityContactInput, AffinityContactOutput } from './types'; | ||
|
||
@Injectable() | ||
export class AffinityService implements IContactService { | ||
constructor( | ||
private prisma: PrismaService, | ||
private logger: LoggerService, | ||
private cryptoService: EncryptionService, | ||
private registry: ServiceRegistry, | ||
) { | ||
this.logger.setContext( | ||
CrmObject.contact.toUpperCase() + ':' + AffinityService.name, | ||
); | ||
this.registry.registerService('affinity', this); | ||
} | ||
|
||
async addContact( | ||
contactData: AffinityContactInput, | ||
linkedUserId: string, | ||
): Promise<ApiResponse<AffinityContactOutput>> { | ||
// Implementation for adding a contact to Affinity CRM | ||
// This should interact with Affinity CRM's API to add a contact | ||
return; | ||
} | ||
Comment on lines
+23
to
+30
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. The Please implement the interaction with Affinity CRM's API or provide a stub if this is a work in progress. |
||
|
||
async syncContacts( | ||
linkedUserId: string, | ||
): Promise<ApiResponse<AffinityContactOutput[]>> { | ||
// Implementation for syncing contacts from Affinity CRM | ||
// This should interact with Affinity CRM's API to fetch contacts | ||
return; | ||
} | ||
developerdhruv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
import { ServiceRegistry } from '../registry.service'; | ||
import { AttioContactInput, AttioContactOutput } from './types'; | ||
|
||
|
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.
Check the version of
nestjs
. The version^0.0.1
seems incorrect, possibly a typo or a placeholder.