Skip to content
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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,199 changes: 1,118 additions & 81 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
},
"dependencies": {
"@changesets/cli": "^2.26.2",
"@nestjs/axios": "^3.0.2",
"axios": "^1.7.2",
"gitmoji-cli": "^9.0.0",
"nestjs": "^0.0.1",
Copy link
Contributor

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.

"optional": "^0.1.4",
"sharp": "^0.33.2",
"turbo": "^1.10.16"
Expand Down
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';
Expand All @@ -15,7 +17,7 @@ import { AttioConnectionService } from './services/attio/attio.service';
import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service';

Copy link

Choose a reason for hiding this comment

The 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,
Expand All @@ -31,6 +33,7 @@ import { ConnectionsStrategiesService } from '@@core/connections-strategies/conn
ZohoConnectionService,
ZendeskConnectionService,
PipedriveConnectionService,
AffinityConnectionService,
],
exports: [CrmConnectionsService],
})
Expand Down
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}`);
}
}
}
3 changes: 3 additions & 0 deletions packages/api/src/crm/contact/contact.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { WebhookService } from '@@core/webhook/webhook.service';
import { BullModule } from '@nestjs/bull';
import { EncryptionService } from '@@core/encryption/encryption.service';
import { ServiceRegistry } from './services/registry.service';
import { AffinityService } from './services/affinity';


@Module({
imports: [
Expand All @@ -40,6 +42,7 @@ import { ServiceRegistry } from './services/registry.service';
ZohoService,
PipedriveService,
HubspotService,
AffinityService,
],
exports: [
SyncService,
Expand Down
119 changes: 119 additions & 0 deletions packages/api/src/crm/contact/services/affinity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Injectable } from '@nestjs/common';
Copy link

Choose a reason for hiding this comment

The 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, {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use handleServiceError to manage errors consistently across services.

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(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use handleServiceError to manage errors consistently across services.

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;
}
}
48 changes: 48 additions & 0 deletions packages/api/src/crm/contact/services/affinity/mappers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Address } from '@crm/@lib/@types';
Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle potential null or undefined values in source properties.

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,
};
}
}
12 changes: 12 additions & 0 deletions packages/api/src/crm/contact/services/affinity/types.ts
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;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a newline at the end of the file.

47 changes: 39 additions & 8 deletions packages/api/src/crm/contact/services/attio/index.ts
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addContact method lacks implementation details. Ensure that the method interacts with the Affinity CRM's API as intended.

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';

Expand Down
Loading