diff --git a/packages/api/.env.example b/packages/api/.env.example index 04570920f..a7c386947 100644 --- a/packages/api/.env.example +++ b/packages/api/.env.example @@ -18,6 +18,8 @@ FRESHSALES_CLIENT_ID= FRESHSALES_CLIENT_SECRET= ZENDESK_SELL_CLIENT_ID= ZENDESK_SELL_CLIENT_SECRET= +ATTIO_CLIENT_ID= +ATTIO_CLIENT_SECRET= # TICKETING ZENDESK_TICKETING_SUBDOMAIN= @@ -25,6 +27,18 @@ ZENDESK_TICKETING_CLIENT_ID= ZENDESK_TICKETING_CLIENT_SECRET= FRONT_CLIENT_ID= FRONT_CLIENT_SECRET= +GORGIAS_CLIENT_ID= +GORGIAS_CLIENT_SECRET= +JIRA_CLIENT_ID= +JIRA_CLIENT_SECRET= +JIRA_SERVICE_MGMT_CLIENT_ID= +JIRA_SERVICE_MGMT_CLIENT_SECRET= +LINEAR_CLIENT_ID= +LINEAR_CLIENT_SECRET= +GITLAB_CLIENT_ID= +GITLAB_CLIENT_SECRET= +CLICKUP_CLIENT_ID= +CLICKUP_CLIENT_SECRET= OAUTH_REDIRECT_BASE='https://api-staging.panora.dev/' diff --git a/packages/api/src/@core/connections/crm/services/attio/attio.service.ts b/packages/api/src/@core/connections/crm/services/attio/attio.service.ts index 8d110fbb3..ed54a8ac6 100644 --- a/packages/api/src/@core/connections/crm/services/attio/attio.service.ts +++ b/packages/api/src/@core/connections/crm/services/attio/attio.service.ts @@ -1,10 +1,10 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable } from '@nestjs/common'; import { - AttioOAuthResponse, - CallbackParams, - ICrmConnectionService, - RefreshParams, -} from "../../types"; + AttioOAuthResponse, + CallbackParams, + ICrmConnectionService, + RefreshParams, +} from '../../types'; import { PrismaService } from '@@core/prisma/prisma.service'; import axios from 'axios'; import { v4 as uuidv4 } from 'uuid'; @@ -14,104 +14,99 @@ import { EncryptionService } from '@@core/encryption/encryption.service'; import { ServiceConnectionRegistry } from '../registry.service'; import { LoggerService } from '@@core/logger/logger.service'; - @Injectable() export class AttioConnectionService implements ICrmConnectionService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private env: EnvironmentService, - private cryptoService: EncryptionService, - private registry: ServiceConnectionRegistry - ) { - this.logger.setContext(AttioConnectionService.name); - this.registry.registerService("attio", this); - } - - async handleCallback(opts: CallbackParams) { - try { - console.log("Linked User iD : ; + avatarUrl: string; +} + export interface ZendeskTicketingOAuthResponse { access_token: string; token_type: string; @@ -21,6 +29,43 @@ export interface GithubOAuthResponse { scope: string; } +export interface GorgiasOAuthResponse { + access_token: string; + expires_in: 0; + id_token: string; + refresh_token: string; + scope: string; + token_type: string; +} + +export interface JiraOAuthResponse { + access_token: string; + refresh_token: string; + expires_in: number | Date; + scope: string; +} + +export interface ClickupOAuthResponse { + access_token: string; +} + +export interface GitlabOAuthResponse { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + created_at: number; +} +export interface LinearOAuthResponse { + access_token: string; + token_type: string; + expires_in: number; + scope: string; +} +export interface JiraServiceManagementOAuthResponse { + access_token: string; +} + export type CallbackParams = { linkedUserId: string; projectId: string; diff --git a/packages/api/src/@core/environment/environment.service.ts b/packages/api/src/@core/environment/environment.service.ts index 77131d9e7..8e23dacfc 100644 --- a/packages/api/src/@core/environment/environment.service.ts +++ b/packages/api/src/@core/environment/environment.service.ts @@ -13,7 +13,7 @@ export type RateLimit = { @Injectable() export class EnvironmentService { - constructor(private configService: ConfigService) { } + constructor(private configService: ConfigService) {} getEnvMode(): string { return this.configService.get('ENV'); @@ -47,11 +47,11 @@ export class EnvironmentService { }; } - getAttioAuth(): OAuth { + getAttioSecret(): OAuth { return { CLIENT_ID: this.configService.get('ATTIO_CLIENT_ID'), CLIENT_SECRET: this.configService.get('ATTIO_CLIENT_SECRET'), - } + }; } getZohoSecret(): OAuth { @@ -111,6 +111,48 @@ export class EnvironmentService { }; } + getGorgiasSecret(): OAuth { + return { + CLIENT_ID: this.configService.get('GORGIAS_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('GORGIAS_CLIENT_SECRET'), + }; + } + + getJiraSecret(): OAuth { + return { + CLIENT_ID: this.configService.get('JIRA_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('JIRA_CLIENT_SECRET'), + }; + } + + getGitlabSecret(): OAuth { + return { + CLIENT_ID: this.configService.get('GITLAB_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('GITLAB_CLIENT_SECRET'), + }; + } + + getJiraServiceManagementSecret(): OAuth { + return { + CLIENT_ID: this.configService.get('JIRA_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('JIRA_CLIENT_SECRET'), + }; + } + + getLinearSecret(): OAuth { + return { + CLIENT_ID: this.configService.get('LINEAR_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('LINEAR_CLIENT_SECRET'), + }; + } + + getClickupSecret(): OAuth { + return { + CLIENT_ID: this.configService.get('CLICKUP_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('CLICKUP_CLIENT_SECRET'), + }; + } + getThrottleConfig(): RateLimit { return { ttl: this.configService.get('THROTTLER_TTL'), diff --git a/packages/api/src/@core/utils/types/original/original.ticketing.ts b/packages/api/src/@core/utils/types/original/original.ticketing.ts index b9ce99b62..80e1e1148 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -83,6 +83,53 @@ import { GithubUserOutput, } from '@ticketing/user/services/github/types'; +import { + GorgiasTicketInput, + GorgiasTicketOutput, +} from '@ticketing/ticket/services/gorgias/types'; +import { + JiraTicketInput, + JiraTicketOutput, +} from '@ticketing/ticket/services/jira/types'; +import { + GorgiasCommentInput, + GorgiasCommentOutput, +} from '@ticketing/comment/services/gorgias/types'; +import { + JiraCommentInput, + JiraCommentOutput, +} from '@ticketing/comment/services/jira/types'; +import { + GorgiasUserInput, + GorgiasUserOutput, +} from '@ticketing/user/services/gorgias/types'; +import { + JiraUserInput, + JiraUserOutput, +} from '@ticketing/user/services/jira/types'; +import { + GorgiasContactInput, + GorgiasContactOutput, +} from '@ticketing/contact/services/gorgias/types'; +import { + GorgiasTagInput, + GorgiasTagOutput, +} from '@ticketing/tag/services/gorgias/types'; +import { + GorgiasTeamInput, + GorgiasTeamOutput, +} from '@ticketing/team/services/gorgias/types'; +import { GorgiasAttachmentOutput } from '@ticketing/attachment/services/gorgias/types'; +import { JiraAttachmentOutput } from '@ticketing/attachment/services/jira/types'; +import { + JiraTeamInput, + JiraTeamOutput, +} from '@ticketing/team/services/JIRA/types'; +import { + JiraTagInput, + JiraTagOutput, +} from '@ticketing/tag/services/jira/types'; + /* INPUT */ /* ticket */ @@ -90,19 +137,28 @@ export type OriginalTicketInput = | ZendeskTicketInput | FrontTicketInput | GithubTicketInput - | HubspotTicketInput; + | HubspotTicketInput + | GorgiasTicketInput + | JiraTicketInput; +//| JiraServiceMgmtTicketInput; /* comment */ export type OriginalCommentInput = | ZendeskCommentInput | FrontCommentInput | GithubCommentInput - | HubspotCommentInput; + | HubspotCommentInput + | GorgiasCommentInput + | JiraCommentInput; +//| JiraCommentServiceMgmtInput; /* user */ export type OriginalUserInput = | ZendeskUserInput | GithubUserInput - | FrontUserInput; + | FrontUserInput + | GorgiasUserInput + | JiraUserInput; +//| JiraServiceMgmtUserInput; /* account */ export type OriginalAccountInput = | ZendeskAccountInput @@ -112,15 +168,23 @@ export type OriginalAccountInput = export type OriginalContactInput = | ZendeskContactInput | GithubContactInput - | FrontContactInput; + | FrontContactInput + | GorgiasContactInput; /* tag */ -export type OriginalTagInput = ZendeskTagInput | GithubTagInput | FrontTagInput; +export type OriginalTagInput = + | ZendeskTagInput + | GithubTagInput + | FrontTagInput + | GorgiasTagInput + | JiraTagInput; /* team */ export type OriginalTeamInput = | ZendeskTeamInput | GithubTeamInput - | FrontTeamInput; + | FrontTeamInput + | GorgiasTeamInput + | JiraTeamInput; /* attachment */ export type OriginalAttachmentInput = null; @@ -142,19 +206,25 @@ export type OriginalTicketOutput = | ZendeskTicketOutput | FrontTicketOutput | GithubTicketOutput - | HubspotTicketOutput; + | HubspotTicketOutput + | GorgiasTicketOutput + | JiraTicketOutput; + /* comment */ export type OriginalCommentOutput = | ZendeskCommentOutput | FrontCommentOutput | GithubCommentOutput - | HubspotCommentOutput; + | HubspotCommentOutput + | GorgiasCommentOutput + | JiraCommentOutput; /* user */ export type OriginalUserOutput = | ZendeskUserOutput | GithubUserOutput - | FrontUserOutput; - + | FrontUserOutput + | GorgiasUserOutput + | JiraUserOutput; /* account */ export type OriginalAccountOutput = | ZendeskAccountOutput @@ -164,24 +234,32 @@ export type OriginalAccountOutput = export type OriginalContactOutput = | ZendeskContactOutput | GithubContactOutput - | FrontContactOutput; + | FrontContactOutput + | GorgiasContactOutput; /* tag */ export type OriginalTagOutput = | ZendeskTagOutput | GithubTagOutput - | FrontTagOutput; + | FrontTagOutput + | GorgiasTagOutput + | JiraTagOutput; + /* team */ export type OriginalTeamOutput = | ZendeskTeamOutput | GithubTeamOutput - | FrontTeamOutput; + | FrontTeamOutput + | GorgiasTeamOutput + | JiraTeamOutput; /* attachment */ export type OriginalAttachmentOutput = | ZendeskAttachmentOutput | FrontAttachmentOutput - | GithubAttachmentOutput; + | GithubAttachmentOutput + | GorgiasAttachmentOutput + | JiraAttachmentOutput; export type TicketingObjectOutput = | OriginalTicketOutput diff --git a/packages/api/src/ticketing/account/services/github/index.ts b/packages/api/src/ticketing/account/services/github/index.ts index f9164f231..38a4c20c9 100644 --- a/packages/api/src/ticketing/account/services/github/index.ts +++ b/packages/api/src/ticketing/account/services/github/index.ts @@ -10,7 +10,6 @@ import { ServiceRegistry } from '../registry.service'; import { IAccountService } from '@ticketing/account/types'; import { GithubAccountOutput } from './types'; -//TODO @Injectable() export class GithubService implements IAccountService { constructor( @@ -36,7 +35,7 @@ export class GithubService implements IAccountService { provider_slug: 'github', }, }); - const resp = await axios.get(`https://api.github.com/accounts`, { + const resp = await axios.get(`https://api.github.com/user/orgs`, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cryptoService.decrypt( @@ -44,7 +43,7 @@ export class GithubService implements IAccountService { )}`, }, }); - this.logger.log(`Synced github accounts !`); + this.logger.log(`Synced github accounts (organizations) !`); return { data: resp.data, diff --git a/packages/api/src/ticketing/account/services/github/mappers.ts b/packages/api/src/ticketing/account/services/github/mappers.ts index c666bb63f..b7ec4e143 100644 --- a/packages/api/src/ticketing/account/services/github/mappers.ts +++ b/packages/api/src/ticketing/account/services/github/mappers.ts @@ -23,6 +23,32 @@ export class GithubAccountMapper implements IAccountMapper { remote_id: string; }[], ): UnifiedAccountOutput | UnifiedAccountOutput[] { - return; + if (!Array.isArray(source)) { + return this.mapSingleAccountToUnified(source, customFieldMappings); + } + return source.map((ticket) => + this.mapSingleAccountToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleAccountToUnified( + account: GithubAccountOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAccountOutput { + const unifiedAccount: UnifiedAccountOutput = { + name: account.login, + domains: [ + account.events_url, + account.hooks_url, + account.issues_url, + account.members_url, + account.public_members_url, + account.repos_url, + ], + }; + return unifiedAccount; } } diff --git a/packages/api/src/ticketing/account/services/github/types.ts b/packages/api/src/ticketing/account/services/github/types.ts index 7ad3a458d..7531eb14e 100644 --- a/packages/api/src/ticketing/account/services/github/types.ts +++ b/packages/api/src/ticketing/account/services/github/types.ts @@ -1,6 +1,16 @@ export type GithubAccountInput = { - name: string; + login: string; + id: number; + node_id: string; + url: string; + repos_url: string; + events_url: string; + hooks_url: string; + issues_url: string; + members_url: string; + public_members_url: string; + avatar_url: string; + description: string | null; }; -//TODO export type GithubAccountOutput = GithubAccountInput; diff --git a/packages/api/src/ticketing/attachment/services/gorgias/mappers.ts b/packages/api/src/ticketing/attachment/services/gorgias/mappers.ts new file mode 100644 index 000000000..940c6d799 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/gorgias/mappers.ts @@ -0,0 +1,46 @@ +import { IAttachmentMapper } from '@ticketing/attachment/types'; +import { + UnifiedAttachmentInput, + UnifiedAttachmentOutput, +} from '@ticketing/attachment/types/model.unified'; +import { GorgiasAttachmentOutput } from './types'; + +export class GorgiasAttachmentMapper implements IAttachmentMapper { + async desunify( + source: UnifiedAttachmentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + unify( + source: GorgiasAttachmentOutput | GorgiasAttachmentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput | UnifiedAttachmentOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleAttachmentToUnified(source, customFieldMappings); + } + return source.map((attachment) => + this.mapSingleAttachmentToUnified(attachment, customFieldMappings), + ); + } + + private mapSingleAttachmentToUnified( + attachment: GorgiasAttachmentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput { + return { + file_name: attachment.name, + file_url: attachment.url, + }; + } +} diff --git a/packages/api/src/ticketing/attachment/services/gorgias/types.ts b/packages/api/src/ticketing/attachment/services/gorgias/types.ts new file mode 100644 index 000000000..2aa430d29 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/gorgias/types.ts @@ -0,0 +1,10 @@ +export type GorgiasAttachmentOutput = { + url: string; + name: string; + size: number | null; + content_type: string; + public: boolean; // Assuming this field indicates if the attachment is public or not + extra?: string; // Optional field for extra information +}; + +export type GorgiasAttachmentInput = Partial; diff --git a/packages/api/src/ticketing/attachment/services/jira/mappers.ts b/packages/api/src/ticketing/attachment/services/jira/mappers.ts new file mode 100644 index 000000000..8151f0cb9 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/jira/mappers.ts @@ -0,0 +1,44 @@ +import { IAttachmentMapper } from '@ticketing/attachment/types'; +import { + UnifiedAttachmentInput, + UnifiedAttachmentOutput, +} from '@ticketing/attachment/types/model.unified'; +import { JiraAttachmentOutput } from './types'; + +//TODO: +export class JiraAttachmentMapper implements IAttachmentMapper { + async desunify( + source: UnifiedAttachmentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + unify( + source: JiraAttachmentOutput | JiraAttachmentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput | UnifiedAttachmentOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleAttachmentToUnified(source, customFieldMappings); + } + return source.map((attachment) => + this.mapSingleAttachmentToUnified(attachment, customFieldMappings), + ); + } + + private mapSingleAttachmentToUnified( + attachment: JiraAttachmentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput { + return; + } +} diff --git a/packages/api/src/ticketing/attachment/services/jira/types.ts b/packages/api/src/ticketing/attachment/services/jira/types.ts new file mode 100644 index 000000000..3500a20d9 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/jira/types.ts @@ -0,0 +1,4 @@ +//TODO: attachment couldnt be queried with the api without its id +export type JiraAttachmentOutput = { + id: string; +}; diff --git a/packages/api/src/ticketing/attachment/types/mappingsTypes.ts b/packages/api/src/ticketing/attachment/types/mappingsTypes.ts index d985c38ca..319eb084b 100644 --- a/packages/api/src/ticketing/attachment/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/attachment/types/mappingsTypes.ts @@ -1,10 +1,12 @@ import { FrontAttachmentMapper } from '../services/front/mappers'; import { GithubAttachmentMapper } from '../services/github/mappers'; +import { GorgiasAttachmentMapper } from '../services/gorgias/mappers'; import { ZendeskAttachmentMapper } from '../services/zendesk/mappers'; const zendeskAttachmentMapper = new ZendeskAttachmentMapper(); const githubAttachmentMapper = new GithubAttachmentMapper(); const frontAttachmentMapper = new FrontAttachmentMapper(); +const gorgiasAttachmentMapper = new GorgiasAttachmentMapper(); export const attachmentUnificationMapping = { zendesk_tcg: { @@ -19,4 +21,8 @@ export const attachmentUnificationMapping = { unify: githubAttachmentMapper.unify.bind(githubAttachmentMapper), desunify: githubAttachmentMapper.desunify, }, + gorgias: { + unify: gorgiasAttachmentMapper.unify.bind(gorgiasAttachmentMapper), + desunify: gorgiasAttachmentMapper.desunify, + }, }; diff --git a/packages/api/src/ticketing/comment/services/gorgias/index.ts b/packages/api/src/ticketing/comment/services/gorgias/index.ts new file mode 100644 index 000000000..c9eae6b0b --- /dev/null +++ b/packages/api/src/ticketing/comment/services/gorgias/index.ts @@ -0,0 +1,154 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ICommentService } from '@ticketing/comment/types'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { GorgiasCommentInput, GorgiasCommentOutput } from './types'; +import { ServiceRegistry } from '../registry.service'; +import { Utils } from '@ticketing/comment/utils'; +import * as fs from 'fs'; + +@Injectable() +export class GorgiasService implements ICommentService { + private readonly utils: Utils; + + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.comment.toUpperCase() + ':' + GorgiasService.name, + ); + this.registry.registerService('gorgias', this); + this.utils = new Utils(); + } + + async addComment( + commentData: GorgiasCommentInput, + linkedUserId: string, + remoteIdTicket: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gorgias', + }, + }); + + let uploads = []; + const uuids = commentData.attachments as any[]; + if (uuids && uuids.length > 0) { + const attachmentPromises = uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid.extra, + }, + }); + if (!res) { + throw new Error(`tcg_attachment not found for uuid ${uuid}`); + } + // Assuming you want to construct the right binary attachment here + // For now, we'll just return the URL + const stats = fs.statSync(res.file_url); + return { + url: res.file_url, + name: res.file_name, + size: stats.size, + content_type: 'application/pdf', //todo + }; + }); + uploads = await Promise.all(attachmentPromises); + } + + // Assuming you want to modify the comment object here + // For now, we'll just add the uploads to the comment + const data = { + ...commentData, + attachments: uploads, + }; + + const resp = await axios.post( + `${connection.account_url}/api/tickets/${remoteIdTicket}/messages`, + JSON.stringify(data), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + return { + data: resp.data, + message: 'Gorgias comment created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Gorgias', + TicketingObject.comment, + ActionType.POST, + ); + } + } + async syncComments( + linkedUserId: string, + id_ticket: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gorgias', + }, + }); + //retrieve ticket remote id so we can retrieve the comments in the original software + const ticket = await this.prisma.tcg_tickets.findUnique({ + where: { + id_tcg_ticket: id_ticket, + }, + select: { + remote_id: true, + }, + }); + + const resp = await axios.get( + `https://api2.gorgiasapp.com/conversations/${ticket.remote_id}/comments`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced gorgias comments !`); + + return { + data: resp.data._results, + message: 'Gorgias comments retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Gorgias', + TicketingObject.comment, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/comment/services/gorgias/mappers.ts b/packages/api/src/ticketing/comment/services/gorgias/mappers.ts new file mode 100644 index 000000000..1056d0910 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/gorgias/mappers.ts @@ -0,0 +1,111 @@ +import { ICommentMapper } from '@ticketing/comment/types'; +import { + UnifiedCommentInput, + UnifiedCommentOutput, +} from '@ticketing/comment/types/model.unified'; +import { GorgiasCommentInput, GorgiasCommentOutput } from './types'; +import { UnifiedAttachmentOutput } from '@ticketing/attachment/types/model.unified'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { unify } from '@@core/utils/unification/unify'; +import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; +import { Utils } from '@ticketing/comment/utils'; + +export class GorgiasCommentMapper implements ICommentMapper { + private readonly utils: Utils; + + constructor() { + this.utils = new Utils(); + } + + async desunify( + source: UnifiedCommentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: GorgiasCommentInput = { + sender: { + id: + Number(await this.utils.getUserRemoteIdFromUuid(source.user_id)) || + Number( + await this.utils.getContactRemoteIdFromUuid( + source.user_id || source.contact_id, + ), + ), + }, + via: 'chat', + from_agent: false, + channel: 'chat', + body_html: source.html_body, + body_text: source.body, + attachments: source.attachments, + }; + return result; + } + + async unify( + source: GorgiasCommentOutput | GorgiasCommentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleCommentToUnified(source, customFieldMappings); + } + return Promise.all( + source.map((comment) => + this.mapSingleCommentToUnified(comment, customFieldMappings), + ), + ); + } + + private async mapSingleCommentToUnified( + comment: GorgiasCommentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + let opts; + + if (comment.attachments && comment.attachments.length > 0) { + const unifiedObject = (await unify({ + sourceObject: comment.attachments, + targetType: TicketingObject.attachment, + providerName: 'gorgias', + customFieldMappings: [], + })) as UnifiedAttachmentOutput[]; + + opts = { ...opts, attachments: unifiedObject }; + } + + if (comment.sender.id) { + const user_id = await this.utils.getUserUuidFromRemoteId( + String(comment.sender.id), + 'gorgias', + ); + + if (user_id) { + opts = { user_id: user_id, creator_type: 'user' }; + } else { + const contact_id = await this.utils.getContactUuidFromRemoteId( + String(comment.sender.id), + 'gorgias', + ); + if (contact_id) { + opts = { creator_type: 'contact', contact_id: contact_id }; + } + } + } + + const res = { + body: comment.body_text || '', + html_body: comment.body_html || '', + ...opts, + }; + + return res; + } +} diff --git a/packages/api/src/ticketing/comment/services/gorgias/types.ts b/packages/api/src/ticketing/comment/services/gorgias/types.ts new file mode 100644 index 000000000..f061e0bb9 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/gorgias/types.ts @@ -0,0 +1,59 @@ +export interface GorgiasCommentOutput { + id: number; + attachments: Attachment[]; + body_html: string; + body_text: string; + channel: string; + created_datetime: string; // ISO 8601 datetime format + external_id: string; + failed_datetime: string | null; // ISO 8601 datetime format, nullable for successful messages + from_agent: boolean; + integration_id: number; + last_sending_error?: string; // Assuming this can be undefined or string + message_id: string; + receiver: Receiver | null; // Optional, based on source type + rule_id: number | null; // Assuming it can be null when no rule sent the message + sender: Sender; + sent_datetime: string; // ISO 8601 datetime format + source: Source; + stripped_html: string; + stripped_text: string; + subject: string; + ticket_id: number; + via: string; + uri: string; +} + +export type GorgiasCommentInput = Partial; + +interface Attachment { + url: string; + name: string; + size: number | null; + content_type: string; + public: boolean; // Assuming this field indicates if the attachment is public or not + extra?: string; // Optional field for extra information +} + +interface Receiver { + id: number; + email?: string; +} + +interface Sender { + id: number; + email?: string; +} + +interface Source { + type: string; + from: Participant; + to: Participant[]; + cc?: Participant[]; + bcc?: Participant[]; +} + +interface Participant { + name: string; + address: string; +} diff --git a/packages/api/src/ticketing/comment/services/jira/index.ts b/packages/api/src/ticketing/comment/services/jira/index.ts new file mode 100644 index 000000000..2d71be023 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/jira/index.ts @@ -0,0 +1,171 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ICommentService } from '@ticketing/comment/types'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { JiraCommentInput, JiraCommentOutput } from './types'; +import { ServiceRegistry } from '../registry.service'; +import { Utils } from '@ticketing/comment/utils'; +import * as fs from 'fs'; + +@Injectable() +export class JiraService implements ICommentService { + private readonly utils: Utils; + + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.comment.toUpperCase() + ':' + JiraService.name, + ); + this.registry.registerService('jira', this); + this.utils = new Utils(); + } + + async addComment( + commentData: JiraCommentInput, + linkedUserId: string, + remoteIdTicket: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'jira', + }, + }); + + // Send request without attachments + const resp = await axios.post( + `${connection.account_url}/rest/api/3/issue/${remoteIdTicket}/comment`, + JSON.stringify(commentData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + //add attachments + // Process attachments + let uploads = []; + const uuids = commentData.attachments; + if (uuids && uuids.length > 0) { + uploads = await Promise.all( + uuids.map(async (uuid) => { + const attachment = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!attachment) { + throw new Error(`tcg_attachment not found for uuid ${uuid}`); + } + // TODO: Construct the right binary attachment + // Get the AWS S3 right file + // TODO: Check how to send a stream of a URL + return await this.utils.fetchFileStreamFromURL(attachment.file_url); + }), + ); + } + + if (uploads.length > 0) { + const formData = new FormData(); + + uploads.forEach((fileStream, index) => { + //const stats = fs.statSync(fileStream); + //const fileSizeInBytes = stats.size; + formData.append('file', fileStream); //, { knownLength: fileSizeInBytes }); + }); + + // Send request with attachments + const resp_ = await axios.post( + `${connection.account_url}/rest/api/3/issue/${remoteIdTicket}/attachments`, + formData, + { + headers: { + 'Content-Type': 'application/json', + 'X-Atlassian-Token': 'no-check', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + } + + // Return response + return { + data: resp.data, + message: 'Jira comment created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Jira', + TicketingObject.comment, + ActionType.POST, + ); + } + } + async syncComments( + linkedUserId: string, + id_ticket: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'jira', + }, + }); + //retrieve ticket remote id so we can retrieve the comments in the original software + const ticket = await this.prisma.tcg_tickets.findUnique({ + where: { + id_tcg_ticket: id_ticket, + }, + select: { + remote_id: true, + }, + }); + const resp = await axios.get( + `${connection.account_url}/rest/api/3/issue/${ticket.remote_id}/comment`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced jira comments !`); + + return { + data: resp.data.comments, + message: 'Jira comments retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Jira', + TicketingObject.comment, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/comment/services/jira/mappers.ts b/packages/api/src/ticketing/comment/services/jira/mappers.ts new file mode 100644 index 000000000..ffe3f3a23 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/jira/mappers.ts @@ -0,0 +1,85 @@ +import { ICommentMapper } from '@ticketing/comment/types'; +import { + UnifiedCommentInput, + UnifiedCommentOutput, +} from '@ticketing/comment/types/model.unified'; +import { JiraCommentInput, JiraCommentOutput } from './types'; +import { UnifiedAttachmentOutput } from '@ticketing/attachment/types/model.unified'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { unify } from '@@core/utils/unification/unify'; +import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; +import { Utils } from '@ticketing/comment/utils'; + +export class JiraCommentMapper implements ICommentMapper { + private readonly utils: Utils; + + constructor() { + this.utils = new Utils(); + } + + async desunify( + source: UnifiedCommentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: JiraCommentInput = { + body: source.body, + attachments: source.attachments, + }; + return result; + } + + async unify( + source: JiraCommentOutput | JiraCommentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleCommentToUnified(source, customFieldMappings); + } + return Promise.all( + source.map((comment) => + this.mapSingleCommentToUnified(comment, customFieldMappings), + ), + ); + } + + private async mapSingleCommentToUnified( + comment: JiraCommentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + //map the jira attachment to our unified version of attachment + //unifying the original attachment object coming from Jira + + let opts; + + //TODO: find a way to retrieve attachments for jira + // issue: attachments are tied to issues not comments in Jira + + if (comment.author.accountId) { + const user_id = await this.utils.getUserUuidFromRemoteId( + comment.author.accountId, + 'jira', + ); + + if (user_id) { + // we must always fall here for Jira + opts = { user_id: user_id, creator_type: 'user' }; + } + } + + const res = { + body: comment.body, + ...opts, + }; + + return res; + } +} diff --git a/packages/api/src/ticketing/comment/services/jira/types.ts b/packages/api/src/ticketing/comment/services/jira/types.ts new file mode 100644 index 000000000..5658676a7 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/jira/types.ts @@ -0,0 +1,17 @@ +export type JiraCommentInput = { + body: any; + [key: string]: any; +}; + +export type JiraCommentOutput = JiraCommentInput & { + id: string; + created: string; + updated: string; + self: string; + author: { + accountId: string; + active: boolean; + displayName: string; + self: string; + }; +}; diff --git a/packages/api/src/ticketing/comment/types/mappingsTypes.ts b/packages/api/src/ticketing/comment/types/mappingsTypes.ts index a01408950..ea82a4fc8 100644 --- a/packages/api/src/ticketing/comment/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/comment/types/mappingsTypes.ts @@ -1,10 +1,12 @@ import { FrontCommentMapper } from '../services/front/mappers'; import { GithubCommentMapper } from '../services/github/mappers'; +import { GorgiasCommentMapper } from '../services/gorgias/mappers'; import { ZendeskCommentMapper } from '../services/zendesk/mappers'; const zendeskCommentMapper = new ZendeskCommentMapper(); const githubCommentMapper = new GithubCommentMapper(); const frontCommentMapper = new FrontCommentMapper(); +const gorgiasCommentMapper = new GorgiasCommentMapper(); export const commentUnificationMapping = { zendesk_tcg: { @@ -19,4 +21,8 @@ export const commentUnificationMapping = { unify: githubCommentMapper.unify.bind(githubCommentMapper), desunify: githubCommentMapper.desunify, }, + gorgias: { + unify: gorgiasCommentMapper.unify.bind(gorgiasCommentMapper), + desunify: gorgiasCommentMapper.desunify, + }, }; diff --git a/packages/api/src/ticketing/comment/utils/index.ts b/packages/api/src/ticketing/comment/utils/index.ts index b4048b952..25be8326a 100644 --- a/packages/api/src/ticketing/comment/utils/index.ts +++ b/packages/api/src/ticketing/comment/utils/index.ts @@ -1,14 +1,20 @@ +import { EncryptionService } from '@@core/encryption/encryption.service'; import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; export class Utils { private readonly prisma: PrismaClient; + private readonly cryptoService: EncryptionService; + constructor() { this.prisma = new PrismaClient(); + /*this.cryptoService = new EncryptionService( + new EnvironmentService(new ConfigService()), + );*/ } async fetchFileStreamFromURL(file_url: string) { - //TODO; - return; + return fs.createReadStream(file_url); } async getUserUuidFromRemoteId(remote_id: string, remote_platform: string) { @@ -28,6 +34,7 @@ export class Utils { throw new Error(error); } } + async getUserRemoteIdFromUuid(uuid: string) { try { const res = await this.prisma.tcg_users.findFirst({ @@ -41,6 +48,7 @@ export class Utils { throw new Error(error); } } + async getContactUuidFromRemoteId(remote_id: string, remote_platform: string) { try { const res = await this.prisma.tcg_contacts.findFirst({ @@ -58,6 +66,7 @@ export class Utils { throw new Error(error); } } + async getContactRemoteIdFromUuid(uuid: string) { try { const res = await this.prisma.tcg_contacts.findFirst({ diff --git a/packages/api/src/ticketing/contact/services/gorgias/index.ts b/packages/api/src/ticketing/contact/services/gorgias/index.ts new file mode 100644 index 000000000..da829a318 --- /dev/null +++ b/packages/api/src/ticketing/contact/services/gorgias/index.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { IContactService } from '@ticketing/contact/types'; +import { GorgiasContactOutput } from './types'; + +@Injectable() +export class GorgiasService implements IContactService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.contact.toUpperCase() + ':' + GorgiasService.name, + ); + this.registry.registerService('gorgias', this); + } + + async syncContacts( + linkedUserId: string, + unused_, + remote_account_id: string, + ): Promise> { + try { + if (!remote_account_id) throw new Error('remote account id not found'); + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gorgias', + }, + }); + + const resp = await axios.get(`${connection.account_url}/api/customers`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced gorgias contacts !`); + + return { + data: resp.data._results, + message: 'Gorgias contacts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Gorgias', + TicketingObject.contact, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/contact/services/gorgias/mappers.ts b/packages/api/src/ticketing/contact/services/gorgias/mappers.ts new file mode 100644 index 000000000..7d987b644 --- /dev/null +++ b/packages/api/src/ticketing/contact/services/gorgias/mappers.ts @@ -0,0 +1,48 @@ +import { IContactMapper } from '@ticketing/contact/types'; +import { GorgiasContactInput, GorgiasContactOutput } from './types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from '@ticketing/contact/types/model.unified'; + +export class GorgiasContactMapper implements IContactMapper { + desunify( + source: UnifiedContactInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GorgiasContactInput { + return; + } + + unify( + source: GorgiasContactOutput | GorgiasContactOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedContactOutput | UnifiedContactOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((contact) => + this.mapSingleContactToUnified(contact, customFieldMappings), + ); + } + + private mapSingleContactToUnified( + contact: GorgiasContactOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedContactOutput { + const unifiedContact: UnifiedContactOutput = { + name: contact.name, + email_address: contact.email, + }; + + return unifiedContact; + } +} diff --git a/packages/api/src/ticketing/contact/services/gorgias/types.ts b/packages/api/src/ticketing/contact/services/gorgias/types.ts new file mode 100644 index 000000000..3c971bee7 --- /dev/null +++ b/packages/api/src/ticketing/contact/services/gorgias/types.ts @@ -0,0 +1,14 @@ +export type GorgiasContactOutput = { + id: number; + created_datetime: string; // ISO 8601 datetime format + email: string; // Assuming email validation occurs elsewhere + external_id: string; + firstname: string; + language: string; // ISO_639-1 format + lastname: string; + name: string; + timezone: string; // IANA timezone name + updated_datetime: string; // ISO 8601 datetime format +}; + +export type GorgiasContactInput = Partial; diff --git a/packages/api/src/ticketing/contact/types/mappingsTypes.ts b/packages/api/src/ticketing/contact/types/mappingsTypes.ts index 2063bd6c5..b0a4a156f 100644 --- a/packages/api/src/ticketing/contact/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/contact/types/mappingsTypes.ts @@ -1,10 +1,12 @@ import { FrontContactMapper } from '../services/front/mappers'; import { GithubContactMapper } from '../services/github/mappers'; +import { GorgiasContactMapper } from '../services/gorgias/mappers'; import { ZendeskContactMapper } from '../services/zendesk/mappers'; const zendeskContactMapper = new ZendeskContactMapper(); const frontContactMapper = new FrontContactMapper(); const githubContactMapper = new GithubContactMapper(); +const gorgiasContactMapper = new GorgiasContactMapper(); export const contactUnificationMapping = { zendesk_tcg: { @@ -19,4 +21,8 @@ export const contactUnificationMapping = { unify: githubContactMapper.unify.bind(githubContactMapper), desunify: githubContactMapper.desunify, }, + gorgias: { + unify: gorgiasContactMapper.unify.bind(gorgiasContactMapper), + desunify: gorgiasContactMapper.desunify, + }, }; diff --git a/packages/api/src/ticketing/tag/services/gorgias/index.ts b/packages/api/src/ticketing/tag/services/gorgias/index.ts new file mode 100644 index 000000000..fda2ea1c0 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/gorgias/index.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { ITagService } from '@ticketing/tag/types'; +import { GorgiasTagOutput } from './types'; + +@Injectable() +export class GorgiasService implements ITagService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.tag.toUpperCase() + ':' + GorgiasService.name, + ); + this.registry.registerService('gorgias', this); + } + + async syncTags( + linkedUserId: string, + id_ticket: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gorgias', + }, + }); + + const ticket = await this.prisma.tcg_tickets.findUnique({ + where: { + id_tcg_ticket: id_ticket, + }, + select: { + remote_id: true, + }, + }); + + const resp = await axios.get(`${connection.account_url}/api/tags`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced gorgias tags !`); + + const conversation = resp.data._results.find( + (c) => c.id === ticket.remote_id, + ); + + return { + data: conversation.tags, + message: 'Gorgias tags retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Gorgias', + TicketingObject.tag, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/tag/services/gorgias/mappers.ts b/packages/api/src/ticketing/tag/services/gorgias/mappers.ts new file mode 100644 index 000000000..965c39e06 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/gorgias/mappers.ts @@ -0,0 +1,47 @@ +import { ITagMapper } from '@ticketing/tag/types'; +import { GorgiasTagInput, GorgiasTagOutput } from './types'; +import { + UnifiedTagInput, + UnifiedTagOutput, +} from '@ticketing/tag/types/model.unified'; + +export class GorgiasTagMapper implements ITagMapper { + desunify( + source: UnifiedTagInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GorgiasTagInput { + return; + } + + unify( + source: GorgiasTagOutput | GorgiasTagOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTagOutput | UnifiedTagOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((tag) => + this.mapSingleTagToUnified(tag, customFieldMappings), + ); + } + + private mapSingleTagToUnified( + tag: GorgiasTagOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTagOutput { + const unifiedTag: UnifiedTagOutput = { + name: tag.name, + }; + + return unifiedTag; + } +} diff --git a/packages/api/src/ticketing/tag/services/gorgias/types.ts b/packages/api/src/ticketing/tag/services/gorgias/types.ts new file mode 100644 index 000000000..30ff8ebad --- /dev/null +++ b/packages/api/src/ticketing/tag/services/gorgias/types.ts @@ -0,0 +1,14 @@ +export type GorgiasTagOutput = { + id: number; + created_datetime: string; // ISO 8601 datetime format + decoration: { + color: string; + }; + deleted_datetime: string | null; // ISO 8601 datetime format, nullable since a tag may not be deleted + description: string; + name: string; + usage: number; + uri: string; +}; + +export type GorgiasTagInput = Partial; diff --git a/packages/api/src/ticketing/tag/services/jira/index.ts b/packages/api/src/ticketing/tag/services/jira/index.ts new file mode 100644 index 000000000..f7009a87c --- /dev/null +++ b/packages/api/src/ticketing/tag/services/jira/index.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { ITagService } from '@ticketing/tag/types'; +import { JiraTagOutput } from './types'; + +@Injectable() +export class JiraService implements ITagService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.tag.toUpperCase() + ':' + JiraService.name, + ); + this.registry.registerService('jira', this); + } + + async syncTags( + linkedUserId: string, + id_ticket: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'jira', + }, + }); + + const ticket = await this.prisma.tcg_tickets.findUnique({ + where: { + id_tcg_ticket: id_ticket, + }, + select: { + remote_id: true, + }, + }); + + //todo: TAGS + const resp = await axios.get('https://api2.jiraapp.com/conversations', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced jira tags !`); + + const conversation = resp.data._results.find( + (c) => c.id === ticket.remote_id, + ); + + return { + data: conversation.tags, + message: 'Jira tags retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Jira', + TicketingObject.tag, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/tag/services/jira/mappers.ts b/packages/api/src/ticketing/tag/services/jira/mappers.ts new file mode 100644 index 000000000..593dd4e20 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/jira/mappers.ts @@ -0,0 +1,47 @@ +import { ITagMapper } from '@ticketing/tag/types'; +import { JiraTagInput, JiraTagOutput } from './types'; +import { + UnifiedTagInput, + UnifiedTagOutput, +} from '@ticketing/tag/types/model.unified'; + +export class JiraTagMapper implements ITagMapper { + desunify( + source: UnifiedTagInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): JiraTagInput { + return; + } + + unify( + source: JiraTagOutput | JiraTagOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTagOutput | UnifiedTagOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((tag) => + this.mapSingleTagToUnified(tag, customFieldMappings), + ); + } + + private mapSingleTagToUnified( + tag: JiraTagOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTagOutput { + const unifiedTag: UnifiedTagOutput = { + name: tag.name, + }; + + return unifiedTag; + } +} diff --git a/packages/api/src/ticketing/tag/services/jira/types.ts b/packages/api/src/ticketing/tag/services/jira/types.ts new file mode 100644 index 000000000..c1c08665a --- /dev/null +++ b/packages/api/src/ticketing/tag/services/jira/types.ts @@ -0,0 +1,25 @@ +export type JiraTagInput = { + id: string; +}; + +export type JiraTagOutput = { + _links: TagLink; + id: string; + name: string; + description: string; + highlight: string | null; + is_private: boolean; + is_visible_in_conversation_lists: boolean; + created_at: number; + updated_at: number; +}; + +interface TagLink { + self: string; + related: { + conversations: string; + owner: string; + parent_tag: string; + children: string; + }; +} diff --git a/packages/api/src/ticketing/tag/types/mappingsTypes.ts b/packages/api/src/ticketing/tag/types/mappingsTypes.ts index 0c3c5456e..23ee3b9fc 100644 --- a/packages/api/src/ticketing/tag/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/tag/types/mappingsTypes.ts @@ -1,10 +1,12 @@ import { FrontTagMapper } from '../services/front/mappers'; import { GithubTagMapper } from '../services/github/mappers'; +import { GorgiasTagMapper } from '../services/gorgias/mappers'; import { ZendeskTagMapper } from '../services/zendesk/mappers'; const zendeskTagMapper = new ZendeskTagMapper(); const frontTagMapper = new FrontTagMapper(); const githubTagMapper = new GithubTagMapper(); +const gorgiasTagMapper = new GorgiasTagMapper(); export const tagUnificationMapping = { zendesk_tcg: { @@ -19,4 +21,8 @@ export const tagUnificationMapping = { unify: githubTagMapper.unify.bind(githubTagMapper), desunify: githubTagMapper.desunify, }, + gorgias: { + unify: gorgiasTagMapper.unify.bind(gorgiasTagMapper), + desunify: gorgiasTagMapper.desunify, + }, }; diff --git a/packages/api/src/ticketing/team/services/gorgias/index.ts b/packages/api/src/ticketing/team/services/gorgias/index.ts new file mode 100644 index 000000000..70029689c --- /dev/null +++ b/packages/api/src/ticketing/team/services/gorgias/index.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { ITeamService } from '@ticketing/team/types'; +import { GorgiasTeamOutput } from './types'; + +@Injectable() +export class GorgiasService implements ITeamService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.team.toUpperCase() + ':' + GorgiasService.name, + ); + this.registry.registerService('gorgias', this); + } + + async syncTeams( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gorgias', + }, + }); + + const resp = await axios.get(`${connection.account_url}/api/teams`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced gorgias teams !`); + + return { + data: resp.data, + message: 'Gorgias teams retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Gorgias', + TicketingObject.team, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/team/services/gorgias/mappers.ts b/packages/api/src/ticketing/team/services/gorgias/mappers.ts new file mode 100644 index 000000000..e1ee76358 --- /dev/null +++ b/packages/api/src/ticketing/team/services/gorgias/mappers.ts @@ -0,0 +1,47 @@ +import { ITeamMapper } from '@ticketing/team/types'; +import { GorgiasTeamInput, GorgiasTeamOutput } from './types'; +import { + UnifiedTeamInput, + UnifiedTeamOutput, +} from '@ticketing/team/types/model.unified'; + +export class GorgiasTeamMapper implements ITeamMapper { + desunify( + source: UnifiedTeamInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GorgiasTeamInput { + return; + } + + unify( + source: GorgiasTeamOutput | GorgiasTeamOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput | UnifiedTeamOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((team) => + this.mapSingleTeamToUnified(team, customFieldMappings), + ); + } + + private mapSingleTeamToUnified( + team: GorgiasTeamOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput { + const unifiedTeam: UnifiedTeamOutput = { + name: team.name, + }; + + return unifiedTeam; + } +} diff --git a/packages/api/src/ticketing/team/services/gorgias/types.ts b/packages/api/src/ticketing/team/services/gorgias/types.ts new file mode 100644 index 000000000..d170e24e0 --- /dev/null +++ b/packages/api/src/ticketing/team/services/gorgias/types.ts @@ -0,0 +1,24 @@ +export type GorgiasTeamOutput = { + id: number; + uri: string; + name: string; + description: string; + decoration: { + emoji: string; + }; + members: User[]; + created_datetime: string; +}; + +export type GorgiasTeamInput = Partial; + +interface User { + id: number; + name: string; + email: string; + meta: Meta; +} + +interface Meta { + [key: string]: any; +} diff --git a/packages/api/src/ticketing/team/services/jira/index.ts b/packages/api/src/ticketing/team/services/jira/index.ts new file mode 100644 index 000000000..2fd49ce46 --- /dev/null +++ b/packages/api/src/ticketing/team/services/jira/index.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { ITeamService } from '@ticketing/team/types'; +import { JiraTeamOutput } from './types'; + +@Injectable() +export class JiraService implements ITeamService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.team.toUpperCase() + ':' + JiraService.name, + ); + this.registry.registerService('jira', this); + } + + async syncTeams( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'jira', + }, + }); + + const resp = await axios.get( + `${connection.account_url}/rest/api/3/user/groups`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced jira teams !`); + + return { + data: resp.data._results, + message: 'Jira teams retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Jira', + TicketingObject.team, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/team/services/jira/mappers.ts b/packages/api/src/ticketing/team/services/jira/mappers.ts new file mode 100644 index 000000000..f73a597a1 --- /dev/null +++ b/packages/api/src/ticketing/team/services/jira/mappers.ts @@ -0,0 +1,47 @@ +import { ITeamMapper } from '@ticketing/team/types'; +import { JiraTeamInput, JiraTeamOutput } from './types'; +import { + UnifiedTeamInput, + UnifiedTeamOutput, +} from '@ticketing/team/types/model.unified'; + +export class JiraTeamMapper implements ITeamMapper { + desunify( + source: UnifiedTeamInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): JiraTeamInput { + return; + } + + unify( + source: JiraTeamOutput | JiraTeamOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput | UnifiedTeamOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((team) => + this.mapSingleTeamToUnified(team, customFieldMappings), + ); + } + + private mapSingleTeamToUnified( + team: JiraTeamOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput { + const unifiedTeam: UnifiedTeamOutput = { + name: team.name, + }; + + return unifiedTeam; + } +} diff --git a/packages/api/src/ticketing/team/services/jira/types.ts b/packages/api/src/ticketing/team/services/jira/types.ts new file mode 100644 index 000000000..76e1cadab --- /dev/null +++ b/packages/api/src/ticketing/team/services/jira/types.ts @@ -0,0 +1,9 @@ +export type JiraTeamInput = { + groupId: string; +}; + +export type JiraTeamOutput = { + groupId: string; + name: string; + self: string; +}; diff --git a/packages/api/src/ticketing/team/types/mappingsTypes.ts b/packages/api/src/ticketing/team/types/mappingsTypes.ts index 8d1936b98..2e8f2e22c 100644 --- a/packages/api/src/ticketing/team/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/team/types/mappingsTypes.ts @@ -1,10 +1,12 @@ import { FrontTeamMapper } from '../services/front/mappers'; import { GithubTeamMapper } from '../services/github/mappers'; +import { GorgiasTeamMapper } from '../services/gorgias/mappers'; import { ZendeskTeamMapper } from '../services/zendesk/mappers'; const zendeskTeamMapper = new ZendeskTeamMapper(); const frontTeamMapper = new FrontTeamMapper(); const githubTeamMapper = new GithubTeamMapper(); +const gorgiasTeamMapper = new GorgiasTeamMapper(); export const teamUnificationMapping = { zendesk_tcg: { @@ -19,4 +21,8 @@ export const teamUnificationMapping = { unify: githubTeamMapper.unify.bind(githubTeamMapper), desunify: githubTeamMapper.desunify, }, + gorgias: { + unify: gorgiasTeamMapper.unify.bind(gorgiasTeamMapper), + desunify: gorgiasTeamMapper.desunify, + }, }; diff --git a/packages/api/src/ticketing/ticket/services/github/mappers.ts b/packages/api/src/ticketing/ticket/services/github/mappers.ts index ae908aae2..835348cc3 100644 --- a/packages/api/src/ticketing/ticket/services/github/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/github/mappers.ts @@ -98,9 +98,6 @@ export class GithubTicketMapper implements ITicketMapper { ...opts, }; - // Additional properties like due_date, priority, etc., can be mapped based on your application's logic. - // GitHub API may not directly provide equivalents for all UnifiedTicketInput fields. - return unifiedTicket; }), ); diff --git a/packages/api/src/ticketing/ticket/services/gorgias/index.ts b/packages/api/src/ticketing/ticket/services/gorgias/index.ts new file mode 100644 index 000000000..2b5ddcccb --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/gorgias/index.ts @@ -0,0 +1,142 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ITicketService } from '@ticketing/ticket/types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { GorgiasTicketInput, GorgiasTicketOutput } from './types'; +import * as fs from 'fs'; + +@Injectable() +export class GorgiasService implements ITicketService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.ticket.toUpperCase() + ':' + GorgiasService.name, + ); + this.registry.registerService('gorgias', this); + } + + async addTicket( + ticketData: GorgiasTicketInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gorgias', + }, + }); + + const comments = ticketData.messages; + const modifiedComments = await Promise.all( + comments.map(async (comment) => { + let uploads = []; + const uuids = comment.attachments as any[]; + if (uuids && uuids.length > 0) { + const attachmentPromises = uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid.extra, + }, + }); + if (!res) { + throw new Error(`tcg_attachment not found for uuid ${uuid}`); + } + // Assuming you want to construct the right binary attachment here + // For now, we'll just return the URL + const stats = fs.statSync(res.file_url); + return { + url: res.file_url, + name: res.file_name, + size: stats.size, + content_type: 'application/pdf', //todo + }; + }); + uploads = await Promise.all(attachmentPromises); + } + + // Assuming you want to modify the comment object here + // For now, we'll just add the uploads to the comment + return { + ...comment, + attachments: uploads, + }; + }), + ); + + const resp = await axios.post( + `${connection.account_url}/api/tickets`, + JSON.stringify({ ...ticketData, messages: modifiedComments }), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + return { + data: resp.data, + message: 'Gorgias ticket created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Gorgias', + TicketingObject.ticket, + ActionType.POST, + ); + } + } + + async syncTickets( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gorgias', + }, + }); + + const resp = await axios.get(`${connection.account_url}/api/tickets`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced gorgias tickets !`); + + return { + data: resp.data, + message: 'Gorgias tickets retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Gorgias', + TicketingObject.ticket, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/ticket/services/gorgias/mappers.ts b/packages/api/src/ticketing/ticket/services/gorgias/mappers.ts new file mode 100644 index 000000000..cea0ba8aa --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/gorgias/mappers.ts @@ -0,0 +1,135 @@ +import { ITicketMapper } from '@ticketing/ticket/types'; +import { GorgiasTicketInput, GorgiasTicketOutput } from './types'; +import { + UnifiedTicketInput, + UnifiedTicketOutput, +} from '@ticketing/ticket/types/model.unified'; +import { Utils } from '@ticketing/ticket/utils'; + +export class GorgiasTicketMapper implements ITicketMapper { + private readonly utils: Utils; + + constructor() { + this.utils = new Utils(); + } + + async desunify( + source: UnifiedTicketInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: GorgiasTicketInput = { + channel: source.type ?? 'email', // Assuming 'email' as default channel + subject: source.name, + status: source.status, + created_datetime: source.due_date?.toISOString(), + messages: [ + { + via: source.type ?? 'email', + from_agent: false, + channel: source.type ?? 'email', + body_html: source.comment.html_body, + body_text: source.comment.body, + attachments: source.comment.attachments.map((att) => ({ + extra: att, + })), + sender: + source.comment.creator_type === 'user' + ? { + id: Number( + await this.utils.getAsigneeRemoteIdFromUserUuid( + source.comment.user_id, + ), + ), + } + : undefined, + }, + ], + }; + + if (source.assigned_to && source.assigned_to.length > 0) { + const data = await this.utils.getAsigneeRemoteIdFromUserUuid( + source.assigned_to[0], + ); + result.assignee_user = { + id: Number(data), + }; + } + + if (source.tags) { + result.tags = source.tags.map((tag) => ({ name: tag })); + } + + // Map custom fields if applicable + if (customFieldMappings && source.field_mappings) { + result.meta = {}; // Ensure meta exists + source.field_mappings.forEach((fieldMapping) => { + customFieldMappings.forEach((mapping) => { + if (fieldMapping.hasOwnProperty(mapping.slug)) { + result.meta[mapping.remote_id] = fieldMapping[mapping.slug]; + } + }); + }); + } + + return result; + } + + async unify( + source: GorgiasTicketOutput | GorgiasTicketOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const sourcesArray = Array.isArray(source) ? source : [source]; + return Promise.all( + sourcesArray.map(async (ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ), + ); + } + + private async mapSingleTicketToUnified( + ticket: GorgiasTicketOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings = + customFieldMappings?.map((mapping) => ({ + [mapping.slug]: ticket.meta + ? ticket.meta[mapping.remote_id] + : undefined, + })) ?? []; + + let opts: any; + + if (ticket.assignee_user) { + //fetch the right assignee uuid from remote id + const user_id = await this.utils.getUserUuidFromRemoteId( + String(ticket.assignee_user.id), + 'gorgias', + ); + if (user_id) { + opts = { assigned_to: [user_id] }; + } + } + + // Assuming additional processing is needed to fully populate the UnifiedTicketOutput + const unifiedTicket: UnifiedTicketOutput = { + name: ticket.subject, + status: ticket.status, + description: ticket.subject, // Assuming the description is similar to the subject + field_mappings, + due_date: new Date(ticket.created_datetime), + tags: ticket.tags?.map((tag) => tag.name), + ...opts, + }; + + return unifiedTicket; + } +} diff --git a/packages/api/src/ticketing/ticket/services/gorgias/types.ts b/packages/api/src/ticketing/ticket/services/gorgias/types.ts new file mode 100644 index 000000000..542b9f6c3 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/gorgias/types.ts @@ -0,0 +1,120 @@ +export type GorgiasTicketOutput = { + id: number; + assignee_user: User | null; + channel: string; + closed_datetime: string | null; // ISO 8601 datetime + created_datetime: string; // ISO 8601 datetime + customer: Customer; + external_id: string; + from_agent: boolean; + is_unread: boolean; + language: string; + last_message_datetime: string | null; // ISO 8601 datetime + last_received_message_datetime: string | null; // ISO 8601 datetime + messages: Partial[]; + meta: Meta; + opened_datetime: string | null; // ISO 8601 datetime + reply_options?: any; // Undefined type, could be specified more if structure is known + satisfaction_survey: SatisfactionSurvey | null; + snooze_datetime: string | null; // ISO 8601 datetime + spam: boolean; + status: string; + subject: string; + tags: Tag[]; + trashed_datetime: string | null; // ISO 8601 datetime + updated_datetime: string | null; // ISO 8601 datetime + via: string; + uri: string; +}; + +export type GorgiasTicketInput = Partial; + +interface User { + id: number; +} + +interface Customer { + id: number; + // Additional customer properties as needed +} + +interface Meta { + [key: string]: any; // key-value storage +} + +interface SatisfactionSurvey { + id: number; + body_text: string; + created_datetime: string; // ISO 8601 datetime + customer_id: number; + meta: Meta; // Assuming Meta is a key-value store + score: number; // Range from 1 to 5 + scored_datetime: string; // ISO 8601 datetime + sent_datetime: string | null; // ISO 8601 datetime, nullable if not sent + should_send_datetime: string | null; // ISO 8601 datetime, nullable if there's no schedule for sending + ticket_id: number; + uri: string; +} + +interface Tag { + name: string; +} + +interface Message { + id: number; + attachments: Partial[] | string[]; + body_html: string; + body_text: string; + channel: string; + created_datetime: string; // ISO 8601 datetime format + external_id: string; + failed_datetime: string | null; // ISO 8601 datetime format, nullable for successful messages + from_agent: boolean; + integration_id: number; + last_sending_error?: string; // Assuming this can be undefined or string + message_id: string; + receiver: Receiver | null; // Optional, based on source type + rule_id: number | null; // Assuming it can be null when no rule sent the message + sender: Sender; + sent_datetime: string; // ISO 8601 datetime format + source: Source; + stripped_html: string; + stripped_text: string; + subject: string; + ticket_id: number; + via: string; + uri: string; +} + +interface Attachment { + // Assuming structure for Attachment objects, add properties as needed + url: string; + name: string; + size: number | null; + content_type: string; + public: boolean; // Assuming this field indicates if the attachment is public or not + extra?: string; // Optional field for extra information +} + +interface Receiver { + id: number; + email?: string; +} + +interface Sender { + id: number; + email?: string; +} + +interface Source { + type: string; + from: Participant; + to: Participant[]; + cc?: Participant[]; + bcc?: Participant[]; +} + +interface Participant { + name: string; + address: string; +} diff --git a/packages/api/src/ticketing/ticket/services/jira/index.ts b/packages/api/src/ticketing/ticket/services/jira/index.ts new file mode 100644 index 000000000..6ed338882 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/jira/index.ts @@ -0,0 +1,113 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ITicketService } from '@ticketing/ticket/types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { JiraTicketInput, JiraTicketOutput } from './types'; +import { Utils } from '@ticketing/comment/utils'; + +@Injectable() +export class JiraService implements ITicketService { + private readonly utils: Utils; + + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.ticket.toUpperCase() + ':' + JiraService.name, + ); + this.registry.registerService('jira', this); + this.utils = new Utils(); + } + + async addTicket( + ticketData: JiraTicketInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'jira', + }, + }); + + //Add comment by calling the unified comment function but first insert the ticket base data + + const resp = await axios.post( + `${connection.account_url}/rest/api/3/issue`, + JSON.stringify(ticketData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + // Add comment if someone wants to add one when creation of the ticket + + return { + data: resp.data, + message: 'Jira ticket created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Jira', + TicketingObject.ticket, + ActionType.POST, + ); + } + } + async syncTickets( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'jira', + }, + }); + const resp = await axios.get( + `${connection.account_url}/rest/api/3/search`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced jira tickets !`); + + return { + data: resp.data.issues, + message: 'Jira tickets retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Jira', + TicketingObject.ticket, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/ticket/services/jira/mappers.ts b/packages/api/src/ticketing/ticket/services/jira/mappers.ts new file mode 100644 index 000000000..6ec7bf279 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/jira/mappers.ts @@ -0,0 +1,117 @@ +import { ITicketMapper } from '@ticketing/ticket/types'; +import { JiraTicketInput, JiraTicketOutput } from './types'; +import { + UnifiedTicketInput, + UnifiedTicketOutput, +} from '@ticketing/ticket/types/model.unified'; +import { Utils } from '@ticketing/ticket/utils'; + +export class JiraTicketMapper implements ITicketMapper { + private readonly utils: Utils; + + constructor() { + this.utils = new Utils(); + } + + async desunify( + source: UnifiedTicketInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!source.project_id) { + throw new Error('a project key/id is mandatory for Jira ticket creation'); + } + const result: JiraTicketInput = { + fields: { + project: { + key: source.project_id, + }, + description: source.description, + issuetype: { + name: source.type, + }, + }, + }; + + if (source.assigned_to && source.assigned_to.length > 0) { + const data = await this.utils.getAsigneeRemoteIdFromUserUuid( + source.assigned_to[0], + ); + result.fields.assignee = { + id: data, + }; + } + + if (source.tags) { + result.fields.labels = source.tags; + } + + // Map custom fields if applicable + /*TODO if (customFieldMappings && source.field_mappings) { + result.meta = {}; // Ensure meta exists + source.field_mappings.forEach((fieldMapping) => { + customFieldMappings.forEach((mapping) => { + if (fieldMapping.hasOwnProperty(mapping.slug)) { + result.meta[mapping.remote_id] = fieldMapping[mapping.slug]; + } + }); + }); + }*/ + + return result; + } + + async unify( + source: JiraTicketOutput | JiraTicketOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return Promise.all( + sourcesArray.map((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ), + ); + } + + private async mapSingleTicketToUnified( + ticket: JiraTicketOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + /*TODO: const field_mappings = customFieldMappings?.map((mapping) => ({ + [mapping.slug]: ticket.custom_fields?.[mapping.remote_id], + }));*/ + + let opts: any; + + const assigneeId = ticket.fields.assignee.id; + const user_id = await this.utils.getUserUuidFromRemoteId( + assigneeId, + 'jira', + ); + if (user_id) { + opts = { assigned_to: [user_id] }; + } + + const unifiedTicket: UnifiedTicketOutput = { + name: ticket.fields.description, + status: ticket.fields.status.name, + description: ticket.fields.description, + due_date: new Date(ticket.fields.duedate), + tags: ticket.fields.labels, + field_mappings: [], //TODO + ...opts, + }; + + return unifiedTicket; + } +} diff --git a/packages/api/src/ticketing/ticket/services/jira/types.ts b/packages/api/src/ticketing/ticket/services/jira/types.ts new file mode 100644 index 000000000..19ff47c93 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/jira/types.ts @@ -0,0 +1,141 @@ +export type JiraTicketInput = Partial; +export type JiraTicketOutput = Issue; //Partial; + +interface AvatarUrls { + '16x16': string; + '24x24': string; + '32x32': string; + '48x48': string; +} + +interface User { + accountId: string; + accountType?: string; + active: boolean; + avatarUrls?: AvatarUrls; + displayName: string; + key?: string; + name?: string; + self: string; +} + +interface Comment { + author: User; + body: any; + created: string; + id: string; + self: string; + updateAuthor: User; + updated: string; + visibility: Visibility; +} + +interface Visibility { + identifier: string; + type: string; + value: string; +} + +interface Attachment { + author: User; + content: string; + created: string; + filename: string; + id: string; + mimeType: string; + self: string; + size: number; + thumbnail?: string; +} + +interface IssueLink { + id: string; + inwardIssue?: Issue; + outwardIssue?: Issue; + type: LinkType; +} + +interface LinkType { + id: string; + inward: string; + name: string; + outward: string; +} + +interface Worklog { + author: User; + comment: any; + id: string; + issueId: string; + self: string; + started: string; + timeSpent: string; + timeSpentSeconds: number; + updateAuthor: User; + updated: string; + visibility: Visibility; +} + +interface Project { + avatarUrls: AvatarUrls; + id: string; + insight?: ProjectInsight; + key: string; + name: string; + projectCategory?: ProjectCategory; + self: string; + simplified: boolean; + style: string; +} + +interface ProjectInsight { + lastIssueUpdateTime: string; + totalIssueCount: number; +} + +interface ProjectCategory { + description: string; + id: string; + name: string; + self: string; +} + +interface Issue { + expand: string; + fields: Partial<{ + assignee: Partial<{ id: string }>; + watcher: Partial; + attachment: Partial[]; + 'sub-tasks': Partial[]; + description: any; + project: Partial; + comment: Partial[]; + issuelinks: Partial[]; + worklog: Partial[]; + issuetype: Partial<{ id: string; name: string }>; + summary: string; + labels: string[]; + status: Partial<{ + name: string; + description: string; + [key: string]: any; + }>; + duedate: string; + }>; + id: string; + key: string; + self: string; +} + +interface Watcher { + isWatching: boolean; + self: string; + watchCount: number; + watchers: User[]; +} + +interface SubTask { + id: string; + outwardIssue?: Issue; + type: LinkType; +} diff --git a/packages/api/src/ticketing/ticket/types/mappingsTypes.ts b/packages/api/src/ticketing/ticket/types/mappingsTypes.ts index 91d90131b..abe07e2e5 100644 --- a/packages/api/src/ticketing/ticket/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/ticket/types/mappingsTypes.ts @@ -1,5 +1,6 @@ import { FrontTicketMapper } from '../services/front/mappers'; import { GithubTicketMapper } from '../services/github/mappers'; +import { GorgiasTicketMapper } from '../services/gorgias/mappers'; import { HubspotTicketMapper } from '../services/hubspot/mappers'; import { ZendeskTicketMapper } from '../services/zendesk/mappers'; @@ -7,6 +8,7 @@ const zendeskTicketMapper = new ZendeskTicketMapper(); const frontTicketMapper = new FrontTicketMapper(); const githubTicketMapper = new GithubTicketMapper(); const hubspotTicketMapper = new HubspotTicketMapper(); +const gorgiasTicketMapper = new GorgiasTicketMapper(); export const ticketUnificationMapping = { zendesk_tcg: { @@ -25,4 +27,8 @@ export const ticketUnificationMapping = { unify: hubspotTicketMapper.unify.bind(hubspotTicketMapper), desunify: hubspotTicketMapper.desunify, }, + gorgias: { + unify: gorgiasTicketMapper.unify.bind(gorgiasTicketMapper), + desunify: gorgiasTicketMapper.desunify, + }, }; diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index c600ec23c..f0e8999a5 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -34,6 +34,12 @@ export class UnifiedTicketInput { }) parent_ticket?: string; + @ApiPropertyOptional({ + type: String, + description: 'The uuid of the project the ticket belongs to', + }) + project_id?: string; + @ApiPropertyOptional({ type: [String], description: 'The tags names of the ticket', @@ -59,7 +65,7 @@ export class UnifiedTicketInput { @ApiPropertyOptional({ type: [String], - description: 'The users uuids the ticket is assigned to', + description: 'The comments of the ticket', }) comment?: UnifiedCommentInput; diff --git a/packages/api/src/ticketing/user/services/github/index.ts b/packages/api/src/ticketing/user/services/github/index.ts index 4e995e168..80758cae0 100644 --- a/packages/api/src/ticketing/user/services/github/index.ts +++ b/packages/api/src/ticketing/user/services/github/index.ts @@ -36,7 +36,7 @@ export class GithubService implements IUserService { provider_slug: 'github', }, }); - const resp = await axios.get(`https://api.github.com/users`, { + const resp = await axios.get(`https://api.github.com/user`, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cryptoService.decrypt( diff --git a/packages/api/src/ticketing/user/services/gorgias/index.ts b/packages/api/src/ticketing/user/services/gorgias/index.ts new file mode 100644 index 000000000..3b7da40c6 --- /dev/null +++ b/packages/api/src/ticketing/user/services/gorgias/index.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { IUserService } from '@ticketing/user/types'; +import { GorgiasUserOutput } from './types'; + +@Injectable() +export class GorgiasService implements IUserService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.user.toUpperCase() + ':' + GorgiasService.name, + ); + this.registry.registerService('gorgias', this); + } + + async syncUsers( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gorgias', + }, + }); + + const resp = await axios.get(`${connection.account_url}/api/users`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced gorgias users !`); + + return { + data: resp.data._results, + message: 'Gorgias users retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Gorgias', + TicketingObject.user, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/user/services/gorgias/mappers.ts b/packages/api/src/ticketing/user/services/gorgias/mappers.ts new file mode 100644 index 000000000..2cf695578 --- /dev/null +++ b/packages/api/src/ticketing/user/services/gorgias/mappers.ts @@ -0,0 +1,57 @@ +import { IUserMapper } from '@ticketing/user/types'; +import { + UnifiedUserInput, + UnifiedUserOutput, +} from '@ticketing/user/types/model.unified'; +import { GorgiasUserInput, GorgiasUserOutput } from './types'; + +export class GorgiasUserMapper implements IUserMapper { + desunify( + source: UnifiedUserInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GorgiasUserInput { + return; + } + + unify( + source: GorgiasUserOutput | GorgiasUserOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedUserOutput | UnifiedUserOutput[] { + const sourcesArray = Array.isArray(source) ? source : [source]; + return sourcesArray.map((user) => + this.mapSingleUserToUnified(user, customFieldMappings), + ); + } + + private mapSingleUserToUnified( + user: GorgiasUserOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedUserOutput { + // Initialize field_mappings array from customFields, if provided + const field_mappings = customFieldMappings + ? customFieldMappings + .map((mapping) => ({ + key: mapping.slug, + value: user.meta ? user.meta[mapping.remote_id] : undefined, + })) + .filter((mapping) => mapping.value !== undefined) + : []; + + const unifiedUser: UnifiedUserOutput = { + name: `${user.firstname} ${user.lastname}`, + email_address: user.email, + field_mappings, + }; + + return unifiedUser; + } +} diff --git a/packages/api/src/ticketing/user/services/gorgias/types.ts b/packages/api/src/ticketing/user/services/gorgias/types.ts new file mode 100644 index 000000000..209f525e3 --- /dev/null +++ b/packages/api/src/ticketing/user/services/gorgias/types.ts @@ -0,0 +1,30 @@ +export type GorgiasUserOutput = { + id: number; + active: boolean; + bio: string; + created_datetime: string; // ISO 8601 datetime format + country: string; // ISO 3166-1 alpha-2 country code + deactivated_datetime: string | null; // ISO 8601 datetime, nullable if the user is active + email: string; // Assuming validation is handled elsewhere + external_id: string; + firstname: string; + lastname: string; + language: string; // ISO language code + meta: Meta; // Key-value storage for additional user data + name: string; + role: UserRole; + timezone: string; // IANA timezone name + updated_datetime: string; +}; + +export type GorgiasUserInput = Partial; + +interface Meta { + [key: string]: any; // Flexible key-value store for additional structured information +} + +interface UserRole { + id: number; + name: string; + permissions: string[]; // Example: Array of permission identifiers +} diff --git a/packages/api/src/ticketing/user/services/jira/index.ts b/packages/api/src/ticketing/user/services/jira/index.ts new file mode 100644 index 000000000..bc348e6ab --- /dev/null +++ b/packages/api/src/ticketing/user/services/jira/index.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { IUserService } from '@ticketing/user/types'; +import { JiraUserOutput } from './types'; + +@Injectable() +export class JiraService implements IUserService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.user.toUpperCase() + ':' + JiraService.name, + ); + this.registry.registerService('jira', this); + } + + async syncUsers( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'jira', + }, + }); + // TODO: sync projects first inside /projects + // /rest/api/3/project/search + + const resp = await axios.get( + `${connection.account_url}/rest/api/3/users/search`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced jira users !`); + + return { + data: resp.data, + message: 'Jira users retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Jira', + TicketingObject.user, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/user/services/jira/mappers.ts b/packages/api/src/ticketing/user/services/jira/mappers.ts new file mode 100644 index 000000000..0e35533a8 --- /dev/null +++ b/packages/api/src/ticketing/user/services/jira/mappers.ts @@ -0,0 +1,49 @@ +import { IUserMapper } from '@ticketing/user/types'; +import { + UnifiedUserInput, + UnifiedUserOutput, +} from '@ticketing/user/types/model.unified'; +import { JiraUserInput, JiraUserOutput } from './types'; + +export class JiraUserMapper implements IUserMapper { + desunify( + source: UnifiedUserInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): JiraUserInput { + return; + } + + unify( + source: JiraUserOutput | JiraUserOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedUserOutput | UnifiedUserOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((user) => + this.mapSingleUserToUnified(user, customFieldMappings), + ); + } + + private mapSingleUserToUnified( + user: JiraUserOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedUserOutput { + const unifiedUser: UnifiedUserOutput = { + name: `${user.displayName}`, + email_address: '', + field_mappings: [], + }; + + return unifiedUser; + } +} diff --git a/packages/api/src/ticketing/user/services/jira/types.ts b/packages/api/src/ticketing/user/services/jira/types.ts new file mode 100644 index 000000000..44a974755 --- /dev/null +++ b/packages/api/src/ticketing/user/services/jira/types.ts @@ -0,0 +1,21 @@ +export type JiraUserInput = { + id: string; +}; + +export type JiraUserOutput = { + accountId: string; + accountType: string; + active: boolean; + avatarUrls: AvatarUrls; + displayName: string; + key: string; + name: string; + self: string; +}; + +type AvatarUrls = { + '16x16': string; + '24x24': string; + '32x32': string; + '48x48': string; +}; diff --git a/packages/api/src/ticketing/user/sync/sync.service.ts b/packages/api/src/ticketing/user/sync/sync.service.ts index b312d519d..7831b198e 100644 --- a/packages/api/src/ticketing/user/sync/sync.service.ts +++ b/packages/api/src/ticketing/user/sync/sync.service.ts @@ -135,7 +135,11 @@ export class SyncService implements OnModuleInit { //TODO const userIds = sourceObject.map((user) => - 'id' in user ? String(user.id) : undefined, + 'id' in user + ? String(user.id) + : 'accountId' in user + ? String(user.accountId) + : undefined, ); //insert the data in the DB with the fieldMappings (value table) @@ -208,6 +212,7 @@ export class SyncService implements OnModuleInit { email_address: user.email_address, teams: user.teams || [], modified_at: new Date(), + //TODO: id_tcg_account: user.account_id || '', }, }); unique_ticketing_user_id = res.id_tcg_user; @@ -223,6 +228,7 @@ export class SyncService implements OnModuleInit { created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, + id_tcg_account: user.account_id || '', remote_id: originId, remote_platform: originSource, }; diff --git a/packages/api/src/ticketing/user/types/model.unified.ts b/packages/api/src/ticketing/user/types/model.unified.ts index c71328e03..d80f85b61 100644 --- a/packages/api/src/ticketing/user/types/model.unified.ts +++ b/packages/api/src/ticketing/user/types/model.unified.ts @@ -17,6 +17,13 @@ export class UnifiedUserInput { }) teams?: string[]; + //TODO + @ApiPropertyOptional({ + type: [String], + description: 'The account or organization the user is part of', + }) + account_id?: string[]; + @ApiProperty({ type: [{}], description: diff --git a/packages/shared/src/authUrl.ts b/packages/shared/src/authUrl.ts index c22422db1..7d4fdfcc3 100644 --- a/packages/shared/src/authUrl.ts +++ b/packages/shared/src/authUrl.ts @@ -44,6 +44,14 @@ export const constructAuthUrl = ({ projectId, linkedUserId, providerName, return finalAuth = `${baseUrl}?client_id=${encodeURIComponent(clientId)}&response_type=code&redirect_uri=${encodedRedirectUrl}&state=${state}` } else if (providerName == "zendesk_tcg" || providerName == "front") { finalAuth = `${baseUrl}?client_id=${encodeURIComponent(clientId)}&response_type=code&redirect_uri=${encodedRedirectUrl}&scope=${encodeURIComponent(scopes)}&state=${state}` + } else if (providerName == "gorgias" || providerName == "linear") { + finalAuth = `${baseUrl}?client_id=${encodeURIComponent(clientId)}&response_type=code&redirect_uri=${encodedRedirectUrl}&scope=${encodeURIComponent(scopes)}&state=${state}` + } else if (providerName == "jira" || providerName == "jira_service_mgmt") { + finalAuth = `${baseUrl}?audience=api.atlassian.com&client_id=${encodeURIComponent(clientId)}&response_type=code&prompt=consent&redirect_uri=${encodedRedirectUrl}&scope=${encodeURIComponent(scopes)}&state=${state}` + } else if(providerName == "gitlab") { + finalAuth = `${baseUrl}?client_id=${encodeURIComponent(clientId)}&response_type=code&redirect_uri=${encodedRedirectUrl}&scope=${encodeURIComponent(scopes)}&state=${state}&code_challenge=&code_challenge_method=` + } else if(providerName == "clickup"){ + finalAuth = `${baseUrl}?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodedRedirectUrl}&state=${state}` } else { finalAuth = addScope ? `${baseUrl}?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodedRedirectUrl}&scope=${encodeURIComponent(scopes)}&state=${state}` diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index e92c9fe0a..4f844ab4e 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -82,6 +82,48 @@ export const providersConfig: ProvidersConfig = { logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRNKVceZGVM7PbARp_2bjdOICUxlpS5B29UYlurvh6Z2Q&s', description: "Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users" }, + 'gorgias': { + clientId: '', //TODO + scopes: 'write:all openid email profile offline', + authBaseUrl: 'https://panora.gorgias.com/oauth/authorize', + logoPath: '', + description: "Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users" + }, + 'jira': { + clientId: '1Xy0XSajM28HG7n9gufEyU0RO72SqEHW', + scopes: 'read:jira-work manage:jira-project manage:jira-data-provider manage:jira-webhook write:jira-work manage:jira-configuration read:jira-user offline_access', + authBaseUrl: 'https://auth.atlassian.com/authorize', + logoPath: '', + description: "Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users" + }, + 'jira_service_mgmt': { + clientId: '1Xy0XSajM28HG7n9gufEyU0RO72SqEHW', + scopes: 'read:servicedesk-request manage:servicedesk-customer read:servicemanagement-insight-objects write:servicedesk-request offline_access', + authBaseUrl: 'https://auth.atlassian.com/authorize', + logoPath: '', + description: "Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users" + }, + 'linear': { + clientId: '', + scopes: 'read,write', + authBaseUrl: 'https://linear.app/oauth/authorize', + logoPath: '', + description: "Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users" + }, + 'gitlab': { + clientId: '', + scopes: '', + authBaseUrl: 'https://gitlab.example.com/oauth/authorize', + logoPath: '', + description: "Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users" + }, + 'clickup': { + clientId: '', + scopes: '', + authBaseUrl: 'https://app.clickup.com/api', + logoPath: '', + description: "Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users" + }, }, 'accounting': { 'pennylane': {