From 0bbbf2a406f75be1520f0be0abc78ff6a9852ba6 Mon Sep 17 00:00:00 2001 From: nael Date: Tue, 2 Jan 2024 18:00:21 +0100 Subject: [PATCH 01/25] :sparkles: Integrations for ticket WIP --- .../connections/connections.controller.ts | 8 + .../connections/crm/crm.connection.module.ts | 1 + .../ticketing/services/front/front.service.ts | 151 ++++++++++++++++++ .../ticketing/services/registry.service.ts | 14 +- .../services/ticketing.connection.service.ts | 4 +- .../services/zendesk/zendesk.service.ts | 12 +- .../ticketing/ticketing.connection.module.ts | 8 +- .../connections/ticketing/types/index.ts | 6 + .../@core/environment/environment.service.ts | 30 ++-- .../types/original/original.ticketing.ts | 8 +- .../ticketing/ticket/services/front/index.ts | 105 ++++++++++++ .../ticket/services/front/mappers.ts | 102 ++++++++++++ .../ticketing/ticket/services/front/types.ts | 102 ++++++++++++ .../ticket/services/zendesk/index.ts | 48 +++++- .../ticketing/ticket/types/model.unified.ts | 4 +- 15 files changed, 568 insertions(+), 35 deletions(-) create mode 100644 packages/api/src/@core/connections/ticketing/services/front/front.service.ts create mode 100644 packages/api/src/ticketing/ticket/services/front/index.ts create mode 100644 packages/api/src/ticketing/ticket/services/front/mappers.ts create mode 100644 packages/api/src/ticketing/ticket/services/front/types.ts diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 4a7dc8bd3..bb04720ed 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -6,12 +6,14 @@ import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { PrismaService } from '@@core/prisma/prisma.service'; import { ProviderVertical, getProviderVertical } from '@@core/utils/types'; import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { TicketingConnectionsService } from './ticketing/services/ticketing.connection.service'; @ApiTags('connections') @Controller('connections') export class ConnectionsController { constructor( private readonly crmConnectionsService: CrmConnectionsService, + private readonly ticketingConnectionsService: TicketingConnectionsService, private logger: LoggerService, private prisma: PrismaService, ) { @@ -67,6 +69,12 @@ export class ConnectionsController { case ProviderVertical.MarketingAutomation: break; case ProviderVertical.Ticketing: + this.ticketingConnectionsService.handleTicketingCallBack( + projectId, + linkedUserId, + providerName, + code, + ); break; case ProviderVertical.Unknown: break; diff --git a/packages/api/src/@core/connections/crm/crm.connection.module.ts b/packages/api/src/@core/connections/crm/crm.connection.module.ts index d0a3c4c04..8794afd28 100644 --- a/packages/api/src/@core/connections/crm/crm.connection.module.ts +++ b/packages/api/src/@core/connections/crm/crm.connection.module.ts @@ -23,6 +23,7 @@ import { PipedriveConnectionService } from './services/pipedrive/pipedrive.servi WebhookService, EnvironmentService, EncryptionService, + // PROVIDERS SERVICES FreshsalesConnectionService, HubspotConnectionService, ZohoConnectionService, diff --git a/packages/api/src/@core/connections/ticketing/services/front/front.service.ts b/packages/api/src/@core/connections/ticketing/services/front/front.service.ts new file mode 100644 index 000000000..5a984357e --- /dev/null +++ b/packages/api/src/@core/connections/ticketing/services/front/front.service.ts @@ -0,0 +1,151 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { Action, handleServiceError } from '@@core/utils/errors'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { + CallbackParams, + RefreshParams, + ITicketingConnectionService, + FrontOAuthResponse, +} from '../../types'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class FrontConnectionService implements ITicketingConnectionService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private env: EnvironmentService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext(FrontConnectionService.name); + this.registry.registerService('front', this); + } + + async handleCallback(opts: CallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'front', //TODO + }, + }); + + //reconstruct the redirect URI that was passed in the frontend it must be the same + const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + + const formData = new URLSearchParams({ + grant_type: 'authorization_code', + redirect_uri: REDIRECT_URI, + code: code, + }); + const res = await axios.post( + `https://app.frontapp.com/oauth/token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Basic ${Buffer.from( + `${this.env.getFrontSecret().CLIENT_ID}:${ + this.env.getFrontSecret().CLIENT_SECRET + }`, + ).toString('base64')}`, + }, + }, + ); + const data: FrontOAuthResponse = res.data; + this.logger.log( + 'OAuth credentials : front ticketing ' + JSON.stringify(data), + ); + + let db_res; + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_at) * 1000, + ), + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + provider_slug: 'front', + token_type: 'oauth', + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_at) * 1000, + ), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { id_linked_user: linkedUserId }, + }, + }, + }); + } + return db_res; + } catch (error) { + handleServiceError(error, this.logger, 'front', Action.oauthCallback); + } + } + + async handleTokenRefresh(opts: RefreshParams) { + try { + const { connectionId, refreshToken } = opts; + const formData = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: this.cryptoService.decrypt(refreshToken), + }); + const res = await axios.post( + `https://app.frontapp.com/oauth/token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Basic ${Buffer.from( + `${this.env.getFrontSecret().CLIENT_ID}:${ + this.env.getFrontSecret().CLIENT_SECRET + }`, + ).toString('base64')}`, + }, + }, + ); + const data: FrontOAuthResponse = res.data; + await this.prisma.connections.update({ + where: { + id_connection: connectionId, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_at) * 1000, + ), + }, + }); + this.logger.log('OAuth credentials updated : front '); + } catch (error) { + handleServiceError(error, this.logger, 'front', Action.oauthRefresh); + } + } +} diff --git a/packages/api/src/@core/connections/ticketing/services/registry.service.ts b/packages/api/src/@core/connections/ticketing/services/registry.service.ts index df10c5f73..0de0f679d 100644 --- a/packages/api/src/@core/connections/ticketing/services/registry.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/registry.service.ts @@ -1,22 +1,22 @@ import { Injectable } from '@nestjs/common'; import { ITicketingConnectionService } from '../types'; -import { ZendeskConnectionService } from './zendesk/zendesk.service'; @Injectable() -export class ServiceConnectionRegistry { +export class ServiceRegistry { private serviceMap: Map; - constructor(zendesk: ZendeskConnectionService) { + constructor() { this.serviceMap = new Map(); - this.serviceMap.set('zendesk_t', zendesk); + } + + registerService(serviceKey: string, service: ITicketingConnectionService) { + this.serviceMap.set(serviceKey, service); } getService(integrationId: string): ITicketingConnectionService { const service = this.serviceMap.get(integrationId); if (!service) { - throw new Error( - `Connection Service not found for integration ID: ${integrationId}`, - ); + throw new Error(`Service not found for integration ID: ${integrationId}`); } return service; } diff --git a/packages/api/src/@core/connections/ticketing/services/ticketing.connection.service.ts b/packages/api/src/@core/connections/ticketing/services/ticketing.connection.service.ts index e18158ee9..572496db2 100644 --- a/packages/api/src/@core/connections/ticketing/services/ticketing.connection.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/ticketing.connection.service.ts @@ -6,12 +6,12 @@ import { connections as Connection } from '@prisma/client'; import { PrismaService } from '@@core/prisma/prisma.service'; import { v4 as uuidv4 } from 'uuid'; import { CallbackParams, RefreshParams } from '../types'; -import { ServiceConnectionRegistry } from './registry.service'; +import { ServiceRegistry } from './registry.service'; @Injectable() export class TicketingConnectionsService { constructor( - private serviceRegistry: ServiceConnectionRegistry, + private serviceRegistry: ServiceRegistry, private webhook: WebhookService, private logger: LoggerService, private prisma: PrismaService, diff --git a/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts b/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts index d2b6caf88..2f7e03066 100644 --- a/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts @@ -12,6 +12,7 @@ import { ITicketingConnectionService, ZendeskTicketingOAuthResponse, } from '../../types'; +import { ServiceRegistry } from '../registry.service'; @Injectable() export class ZendeskConnectionService implements ITicketingConnectionService { @@ -20,8 +21,10 @@ export class ZendeskConnectionService implements ITicketingConnectionService { private logger: LoggerService, private env: EnvironmentService, private cryptoService: EncryptionService, + private registry: ServiceRegistry, ) { this.logger.setContext(ZendeskConnectionService.name); + this.registry.registerService('zendesk_t', this); } async handleCallback(opts: CallbackParams) { @@ -30,7 +33,7 @@ export class ZendeskConnectionService implements ITicketingConnectionService { const isNotUnique = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk', //TODO + provider_slug: 'zendesk_t', //TODO }, }); @@ -78,7 +81,7 @@ export class ZendeskConnectionService implements ITicketingConnectionService { db_res = await this.prisma.connections.create({ data: { id_connection: uuidv4(), - provider_slug: 'zendesk', + provider_slug: 'zendesk_t', token_type: 'oauth', access_token: this.cryptoService.encrypt(data.access_token), refresh_token: '', @@ -96,11 +99,12 @@ export class ZendeskConnectionService implements ITicketingConnectionService { } return db_res; } catch (error) { - handleServiceError(error, this.logger, 'zendesk', Action.oauthCallback); + handleServiceError(error, this.logger, 'zendesk_t', Action.oauthCallback); } } - //TODO + //todo: revoke ? + //ZENDESK TICKETING OAUTH TOKENS DONT EXPIRE BUT THEY MAY BE REVOKED async handleTokenRefresh(opts: RefreshParams): Promise { throw new Error('Method not implemented.'); } diff --git a/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts b/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts index dcccd026c..84b217a07 100644 --- a/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts +++ b/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts @@ -7,20 +7,22 @@ import { WebhookModule } from '@@core/webhook/webhook.module'; import { EnvironmentService } from '@@core/environment/environment.service'; import { EncryptionService } from '@@core/encryption/encryption.service'; import { TicketingConnectionsService } from './services/ticketing.connection.service'; -import { ServiceConnectionRegistry } from './services/registry.service'; +import { ServiceRegistry } from './services/registry.service'; +import { FrontConnectionService } from './services/front/front.service'; @Module({ imports: [WebhookModule], providers: [ TicketingConnectionsService, PrismaService, - ZendeskConnectionService, LoggerService, WebhookService, EnvironmentService, EncryptionService, - ServiceConnectionRegistry, + ServiceRegistry, + //PROVIDERS SERVICES ZendeskConnectionService, + FrontConnectionService, ], exports: [TicketingConnectionsService], }) diff --git a/packages/api/src/@core/connections/ticketing/types/index.ts b/packages/api/src/@core/connections/ticketing/types/index.ts index 9d39a9f06..a53a00acf 100644 --- a/packages/api/src/@core/connections/ticketing/types/index.ts +++ b/packages/api/src/@core/connections/ticketing/types/index.ts @@ -5,6 +5,12 @@ export interface ZendeskTicketingOAuthResponse { token_type: string; scope: string; } +export interface FrontOAuthResponse { + access_token: string; + refresh_token: string; + expires_at: string; + token_type: string; +} export type CallbackParams = { linkedUserId: string; diff --git a/packages/api/src/@core/environment/environment.service.ts b/packages/api/src/@core/environment/environment.service.ts index 80909fc69..18bb82f6a 100644 --- a/packages/api/src/@core/environment/environment.service.ts +++ b/packages/api/src/@core/environment/environment.service.ts @@ -37,6 +37,9 @@ export class EnvironmentService { getCryptoKey(): string { return this.configService.get('ENCRYPT_CRYPTO_SECRET_KEY'); } + + /* CRM */ + getHubspotAuth(): OAuth { return { CLIENT_ID: this.configService.get('HUBSPOT_CLIENT_ID'), @@ -58,6 +61,21 @@ export class EnvironmentService { }; } + getFreshsalesSecret(): OAuth { + return { + CLIENT_ID: this.configService.get('FRESHSALES_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('FRESHSALES_CLIENT_SECRET'), + }; + } + getPipedriveSecret(): OAuth { + return { + CLIENT_ID: this.configService.get('PIPEDRIVE_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('PIPEDRIVE_CLIENT_SECRET'), + }; + } + + /* TICKETING */ + getZendeskTicketingSecret(): OAuth { return { CLIENT_ID: this.configService.get('ZENDESK_TICKETING_CLIENT_ID'), @@ -71,16 +89,10 @@ export class EnvironmentService { return this.configService.get('ZENDESK_TICKETING_SUBDOMAIN'); } - getFreshsalesSecret(): OAuth { - return { - CLIENT_ID: this.configService.get('FRESHSALES_CLIENT_ID'), - CLIENT_SECRET: this.configService.get('FRESHSALES_CLIENT_SECRET'), - }; - } - getPipedriveSecret(): OAuth { + getFrontSecret(): OAuth { return { - CLIENT_ID: this.configService.get('PIPEDRIVE_CLIENT_ID'), - CLIENT_SECRET: this.configService.get('PIPEDRIVE_CLIENT_SECRET'), + CLIENT_ID: this.configService.get('FRONT_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('FRONT_CLIENT_SECRET'), }; } 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 1be81d418..e8ac2f34f 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -8,11 +8,15 @@ import { ZendeskAttachmentOutput, ZendeskAttachmentInput, } from '@ticketing/@utils/@types'; +import { + FrontTicketInput, + FrontTicketOutput, +} from '@ticketing/ticket/services/front/types'; /* INPUT */ /* ticket */ -export type OriginalTicketInput = ZendeskTicketInput; +export type OriginalTicketInput = ZendeskTicketInput | FrontTicketInput; /* comment */ export type OriginalCommentInput = ZendeskCommentInput; @@ -32,7 +36,7 @@ export type TicketingObjectInput = /* OUTPUT */ /* ticket */ -export type OriginalTicketOutput = ZendeskTicketOutput; +export type OriginalTicketOutput = ZendeskTicketOutput | FrontTicketOutput; /* comment */ export type OriginalCommentOutput = ZendeskCommentOutput; diff --git a/packages/api/src/ticketing/ticket/services/front/index.ts b/packages/api/src/ticketing/ticket/services/front/index.ts new file mode 100644 index 000000000..5d013378b --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/front/index.ts @@ -0,0 +1,105 @@ +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 { EnvironmentService } from '@@core/environment/environment.service'; +import { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; +import { ServiceRegistry } from '../registry.service'; +import { FrontTicketInput, FrontTicketOutput } from './types'; + +@Injectable() +export class FrontService implements ITicketService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.ticket.toUpperCase() + ':' + FrontService.name, + ); + this.registry.registerService('front', this); + } + async addTicket( + ticketData: FrontTicketInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'front', + }, + }); + const dataBody = ticketData; + const resp = await axios.post( + `https://api2.frontapp.com/conversations`, + JSON.stringify(dataBody), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data, + message: 'Front ticket created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + TicketingObject.ticket, + ActionType.POST, + ); + } + } + async syncTickets( + linkedUserId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'front', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/conversations', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced front tickets !`); + + return { + data: resp.data._results, + message: 'Front tickets retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + TicketingObject.ticket, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts new file mode 100644 index 000000000..fd644fd41 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -0,0 +1,102 @@ +import { ITicketMapper } from '@ticketing/ticket/types'; +import { FrontTicketInput, FrontTicketOutput } from './types'; +import { + UnifiedTicketInput, + UnifiedTicketOutput, +} from '@ticketing/ticket/types/model.unified'; + +export class FrontTicketMapper implements ITicketMapper { + desunify( + source: UnifiedTicketInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): FrontTicketInput { + // For simplicity, we're assuming that the attachments are provided as string URLs. + // In a real implementation, we would need to handle binary data for attachments. + //TODO: handle attachments + /*const attachments = source.comments?.[0]?.attachments?.map( + (attachment) => attachment.url, + );*/ + + const result: FrontTicketInput = { + type: 'discussion', // Assuming 'discussion' as a default type for Front conversations + subject: source.name, + inbox_id: source.assigned_to?.[0], // TODO + comment: { + body: source.comments[0].body || '', //TODO: handle where a lot of comments must be added + //TODO: attachments: [''], + }, + }; + + //TODO: custom fields => https://dev.frontapp.com/reference/patch_conversations-conversation-id + + // Custom fields mapping logic + /*if (customFieldMappings && source.field_mappings) { + result.custom_fields = {}; + customFieldMappings.forEach((mapping) => { + const fieldMapping = source.field_mappings?.find( + (fm) => fm[mapping.slug], + ); + if (fieldMapping && fieldMapping[mapping.slug]) { + result.custom_fields[mapping.remote_id] = fieldMapping[mapping.slug]; + } + }); + }*/ + + return result; + } + + unify( + source: FrontTicketOutput | FrontTicketOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketOutput | UnifiedTicketOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTicketToUnified( + ticket: FrontTicketOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketOutput { + //TODO: retrieve comments for the conv id: https://dev.frontapp.com/reference/get_conversations-conversation-id-comments + const comments = []; + /*ticket.comments?.map((comment) => ({ + body: comment.body, + html_body: comment.html_body, + is_private: comment.is_private, + remote_id: comment.id.toString(), + // Assuming attachments are URLs in string format + attachments: comment.attachments?.map((att) => ({ url: att })), + }));*/ + + const field_mappings = customFieldMappings?.map((mapping) => ({ + [mapping.slug]: ticket.custom_fields?.[mapping.remote_id], + })); + + const unifiedTicket: UnifiedTicketOutput = { + id: ticket.id, + name: ticket.subject, + status: ticket.status, + description: ticket.subject, // todo: ? + due_date: new Date(ticket.created_at), // todo ? + tags: JSON.stringify(ticket.tags?.map((tag) => tag.name)), + assigned_to: ticket.assignee ? [ticket.assignee.email] : undefined, + comments: comments, + field_mappings: field_mappings, + }; + + return unifiedTicket; + } +} diff --git a/packages/api/src/ticketing/ticket/services/front/types.ts b/packages/api/src/ticketing/ticket/services/front/types.ts new file mode 100644 index 000000000..3ba849398 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/front/types.ts @@ -0,0 +1,102 @@ +export type FrontTicketInput = { + type: 'discussion'; + inbox_id?: string; + teammate_ids?: string[]; + subject: string; + comment: Comment; +}; + +type Comment = { + author_id?: string; + body: string; + attachments?: string[]; //TODO: maybe wrong type +}; + +export type FrontTicketOutput = Partial; + +type Conversation = { + _links: Link; + id: string; + subject: string; + status: string; + assignee: Assignee; + recipient: Recipient; + tags: Tag[]; + links: LinkItem[]; + custom_fields: CustomFields; + created_at: number; + is_private: boolean; + scheduled_reminders: ScheduledReminder[]; + metadata: Metadata; +}; + +type Link = { + self: string; + related?: { + [key: string]: string; + }; +}; + +type CustomFields = { + [key: string]: string | boolean | number | null; +}; + +type Assignee = { + _links: Link; + id: string; + email: string; + username: string; + first_name: string; + last_name: string; + is_admin: boolean; + is_available: boolean; + is_blocked: boolean; + custom_fields: CustomFields; +}; + +type Recipient = { + _links: { + related: { + contact: string; + }; + }; + name: string; + handle: string; + role: string; +}; + +type Tag = { + _links: Link; + id: string; + name: string; + description: string; + highlight: null | string; + is_private: boolean; + is_visible_in_conversation_lists: boolean; + created_at: number; + updated_at: number; +}; + +type LinkItem = { + _links: Link; + id: string; + name: string; + type: string; + external_url: string; + custom_fields: CustomFields; +}; + +type ScheduledReminder = { + _links: { + related: { + owner: string; + }; + }; + created_at: number; + scheduled_at: number; + updated_at: number; +}; + +type Metadata = { + external_conversation_ids: string[]; +}; diff --git a/packages/api/src/ticketing/ticket/services/zendesk/index.ts b/packages/api/src/ticketing/ticket/services/zendesk/index.ts index 61454b754..c0b375ce3 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/index.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/index.ts @@ -2,10 +2,13 @@ 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, ZendeskTicketOutput } from '@ticketing/@utils/@types'; +import { + TicketingObject, + ZendeskTicketInput, + ZendeskTicketOutput, +} from '@ticketing/@utils/@types'; import { ITicketService } from '@ticketing/ticket/types'; import { ApiResponse } from '@@core/utils/types'; -import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; import { EnvironmentService } from '@@core/environment/environment.service'; @@ -27,11 +30,10 @@ export class ZendeskService implements ITicketService { this.registry.registerService('zendesk_t', this); } async addTicket( - ticketData: DesunifyReturnType, + ticketData: ZendeskTicketInput, linkedUserId: string, ): Promise> { try { - //TODO: check required scope => crm.objects.contacts.write const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, @@ -68,10 +70,44 @@ export class ZendeskService implements ITicketService { ); } } - syncTickets( + async syncTickets( linkedUserId: string, custom_properties?: string[], ): Promise> { - throw new Error('Method not implemented.'); + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'zendesk_t', + }, + }); + + const resp = await axios.get( + `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tickets.json`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced zendesk tickets !`); + + return { + data: resp.data.tickets, + message: 'Zendesk tickets retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zendesk', + TicketingObject.ticket, + ActionType.GET, + ); + } } } diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index e6ca0ca16..b20493260 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -8,11 +8,11 @@ export class UnifiedTicketInput { due_date?: Date; type?: string; parent_ticket?: string; - tags?: string; + tags?: string; // TODO: create a real Tag object here completed_at?: Date; priority?: string; assigned_to?: string[]; - comments?: Comment[]; + comments: Comment[]; @ApiPropertyOptional({ type: [{}] }) field_mappings?: Record[]; } From 0f7317470d9268bce0469817c57fb01649ec218f Mon Sep 17 00:00:00 2001 From: nael Date: Tue, 2 Jan 2024 18:29:40 +0100 Subject: [PATCH 02/25] :construction: Added github oauth service --- .../services/github/github.service.ts | 148 ++++++++++++++++++ .../ticketing/ticketing.connection.module.ts | 2 + .../connections/ticketing/types/index.ts | 9 ++ .../@core/environment/environment.service.ts | 7 + 4 files changed, 166 insertions(+) create mode 100644 packages/api/src/@core/connections/ticketing/services/github/github.service.ts diff --git a/packages/api/src/@core/connections/ticketing/services/github/github.service.ts b/packages/api/src/@core/connections/ticketing/services/github/github.service.ts new file mode 100644 index 000000000..bef9c245a --- /dev/null +++ b/packages/api/src/@core/connections/ticketing/services/github/github.service.ts @@ -0,0 +1,148 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { Action, handleServiceError } from '@@core/utils/errors'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { + CallbackParams, + RefreshParams, + ITicketingConnectionService, + GithubOAuthResponse, +} from '../../types'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class GithubConnectionService implements ITicketingConnectionService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private env: EnvironmentService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext(GithubConnectionService.name); + this.registry.registerService('github', this); + } + + async handleCallback(opts: CallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'github', //TODO + }, + }); + + //reconstruct the redirect URI that was passed in the githubend it must be the same + const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + + const formData = new URLSearchParams({ + client_id: this.env.getGithubSecret().CLIENT_ID, + client_secret: this.env.getGithubSecret().CLIENT_SECRET, + redirect_uri: REDIRECT_URI, + code: code, + //repository_id: todo + }); + const res = await axios.post( + `https://github.com/login/oauth/access_token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: GithubOAuthResponse = res.data; + this.logger.log( + 'OAuth credentials : github ticketing ' + JSON.stringify(data), + ); + + let db_res; + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + provider_slug: 'github', + token_type: 'oauth', + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { id_linked_user: linkedUserId }, + }, + }, + }); + } + return db_res; + } catch (error) { + handleServiceError(error, this.logger, 'github', Action.oauthCallback); + } + } + + async handleTokenRefresh(opts: RefreshParams) { + try { + const { connectionId, refreshToken } = opts; + const formData = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: this.cryptoService.decrypt(refreshToken), + }); + const res = await axios.post( + `https://app.githubapp.com/oauth/token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Basic ${Buffer.from( + `${this.env.getGithubSecret().CLIENT_ID}:${ + this.env.getGithubSecret().CLIENT_SECRET + }`, + ).toString('base64')}`, + }, + }, + ); + const data: GithubOAuthResponse = res.data; + await this.prisma.connections.update({ + where: { + id_connection: connectionId, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + }, + }); + this.logger.log('OAuth credentials updated : github '); + } catch (error) { + handleServiceError(error, this.logger, 'github', Action.oauthRefresh); + } + } +} diff --git a/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts b/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts index 84b217a07..67e1b1455 100644 --- a/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts +++ b/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts @@ -9,6 +9,7 @@ import { EncryptionService } from '@@core/encryption/encryption.service'; import { TicketingConnectionsService } from './services/ticketing.connection.service'; import { ServiceRegistry } from './services/registry.service'; import { FrontConnectionService } from './services/front/front.service'; +import { GithubConnectionService } from './services/github/github.service'; @Module({ imports: [WebhookModule], @@ -23,6 +24,7 @@ import { FrontConnectionService } from './services/front/front.service'; //PROVIDERS SERVICES ZendeskConnectionService, FrontConnectionService, + GithubConnectionService, ], exports: [TicketingConnectionsService], }) diff --git a/packages/api/src/@core/connections/ticketing/types/index.ts b/packages/api/src/@core/connections/ticketing/types/index.ts index a53a00acf..c2e5a2ab4 100644 --- a/packages/api/src/@core/connections/ticketing/types/index.ts +++ b/packages/api/src/@core/connections/ticketing/types/index.ts @@ -12,6 +12,15 @@ export interface FrontOAuthResponse { token_type: string; } +export interface GithubOAuthResponse { + access_token: string; + refresh_token: string; + expires_in: string; + refresh_token_expires_in: string; //TODO + token_type: string; + scope: 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 18bb82f6a..480a429d6 100644 --- a/packages/api/src/@core/environment/environment.service.ts +++ b/packages/api/src/@core/environment/environment.service.ts @@ -96,6 +96,13 @@ export class EnvironmentService { }; } + getGithubSecret(): OAuth { + return { + CLIENT_ID: this.configService.get('GITHUB_CLIENT_ID'), + CLIENT_SECRET: this.configService.get('GITHUB_CLIENT_SECRET'), + }; + } + getThrottleConfig(): RateLimit { return { ttl: this.configService.get('THROTTLER_TTL'), From 572158dc0a96aff25ed7c5b6261f619cc7cf34d0 Mon Sep 17 00:00:00 2001 From: nael Date: Wed, 3 Jan 2024 16:42:08 +0100 Subject: [PATCH 03/25] :construction: WIP for integrations in ticket --- .../types/original/original.ticketing.ts | 47 ++++- .../ticketing/comment/comment.controller.ts | 14 +- .../comment/services/comment.service.ts | 197 +++++++++++++++++- .../ticketing/comment/services/front/index.ts | 118 +++++++++++ .../comment/services/front/mappers.ts | 47 +++++ .../ticketing/comment/services/front/types.ts | 50 +++++ .../comment/services/github/index.ts | 120 +++++++++++ .../comment/services/github/mappers.ts | 47 +++++ .../comment/services/github/types.ts | 5 + .../comment/services/hubspot/index.ts | 120 +++++++++++ .../comment/services/hubspot/mappers.ts | 47 +++++ .../comment/services/hubspot/types.ts | 5 + .../comment/services/zendesk/index.ts | 70 +++++-- .../comment/services/zendesk/types.ts | 191 ++++++++++++++++- .../ticketing/comment/sync/sync.service.ts | 118 ++++++++++- .../api/src/ticketing/comment/types/index.ts | 2 + .../ticketing/comment/types/mappingsTypes.ts | 12 ++ .../ticketing/comment/types/model.unified.ts | 18 +- .../ticket/services/front/mappers.ts | 14 +- .../ticketing/ticket/services/github/index.ts | 103 +++++++++ .../ticket/services/github/mappers.ts | 28 +++ .../ticketing/ticket/services/github/types.ts | 5 + .../ticket/services/hubspot/index.ts | 103 +++++++++ .../ticket/services/hubspot/mappers.ts | 43 ++++ .../ticket/services/hubspot/types.ts | 5 + .../ticket/services/ticket.service.ts | 32 --- .../ticket/services/zendesk/mappers.ts | 15 +- .../src/ticketing/ticket/sync/sync.service.ts | 20 -- .../ticketing/ticket/types/mappingsTypes.ts | 18 ++ .../ticketing/ticket/types/model.unified.ts | 8 +- .../api/src/ticketing/ticket/utils/index.ts | 14 -- 31 files changed, 1494 insertions(+), 142 deletions(-) create mode 100644 packages/api/src/ticketing/comment/services/front/index.ts create mode 100644 packages/api/src/ticketing/comment/services/front/mappers.ts create mode 100644 packages/api/src/ticketing/comment/services/front/types.ts create mode 100644 packages/api/src/ticketing/comment/services/github/index.ts create mode 100644 packages/api/src/ticketing/comment/services/github/mappers.ts create mode 100644 packages/api/src/ticketing/comment/services/github/types.ts create mode 100644 packages/api/src/ticketing/comment/services/hubspot/index.ts create mode 100644 packages/api/src/ticketing/comment/services/hubspot/mappers.ts create mode 100644 packages/api/src/ticketing/comment/services/hubspot/types.ts create mode 100644 packages/api/src/ticketing/ticket/services/github/index.ts create mode 100644 packages/api/src/ticketing/ticket/services/github/mappers.ts create mode 100644 packages/api/src/ticketing/ticket/services/github/types.ts create mode 100644 packages/api/src/ticketing/ticket/services/hubspot/index.ts create mode 100644 packages/api/src/ticketing/ticket/services/hubspot/mappers.ts create mode 100644 packages/api/src/ticketing/ticket/services/hubspot/types.ts 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 e8ac2f34f..c0511fa76 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -8,19 +8,46 @@ import { ZendeskAttachmentOutput, ZendeskAttachmentInput, } from '@ticketing/@utils/@types'; +import { + FrontCommentInput, + FrontCommentOutput, +} from '@ticketing/comment/services/front/types'; +import { + GithubCommentInput, + GithubCommentOutput, +} from '@ticketing/comment/services/github/types'; +import { + HubspotCommentInput, + HubspotCommentOutput, +} from '@ticketing/comment/services/hubspot/types'; import { FrontTicketInput, FrontTicketOutput, } from '@ticketing/ticket/services/front/types'; +import { + GithubTicketInput, + GithubTicketOutput, +} from '@ticketing/ticket/services/github/types'; +import { + HubspotTicketInput, + HubspotTicketOutput, +} from '@ticketing/ticket/services/hubspot/types'; /* INPUT */ /* ticket */ -export type OriginalTicketInput = ZendeskTicketInput | FrontTicketInput; +export type OriginalTicketInput = + | ZendeskTicketInput + | FrontTicketInput + | GithubTicketInput + | HubspotTicketInput; /* comment */ -export type OriginalCommentInput = ZendeskCommentInput; - +export type OriginalCommentInput = + | ZendeskCommentInput + | FrontCommentInput + | GithubCommentInput + | HubspotCommentInput; /* user */ export type OriginalUserInput = ZendeskUserInput; @@ -36,11 +63,17 @@ export type TicketingObjectInput = /* OUTPUT */ /* ticket */ -export type OriginalTicketOutput = ZendeskTicketOutput | FrontTicketOutput; - +export type OriginalTicketOutput = + | ZendeskTicketOutput + | FrontTicketOutput + | GithubTicketOutput + | HubspotTicketOutput; /* comment */ -export type OriginalCommentOutput = ZendeskCommentOutput; - +export type OriginalCommentOutput = + | ZendeskCommentOutput + | FrontCommentOutput + | GithubCommentOutput + | HubspotCommentOutput; /* user */ export type OriginalUserOutput = ZendeskUserOutput; diff --git a/packages/api/src/ticketing/comment/comment.controller.ts b/packages/api/src/ticketing/comment/comment.controller.ts index b1e49e3a6..c670842a0 100644 --- a/packages/api/src/ticketing/comment/comment.controller.ts +++ b/packages/api/src/ticketing/comment/comment.controller.ts @@ -60,13 +60,13 @@ export class CommentController { @ApiOperation({ operationId: 'getComment', summary: 'Retrieve a Comment', - description: 'Retrieve a ticket from any connected Ticketing software', + description: 'Retrieve a comment from any connected Ticketing software', }) @ApiParam({ name: 'id', required: true, type: String, - description: 'id of the `ticket` you want to retrive.', + description: 'id of the `comment` you want to retrive.', }) @ApiQuery({ name: 'remoteData', @@ -87,7 +87,7 @@ export class CommentController { @ApiOperation({ operationId: 'addComment', summary: 'Create a Comment', - description: 'Create a ticket in any supported Ticketing software', + description: 'Create a comment in any supported Ticketing software', }) @ApiHeader({ name: 'integrationId', @@ -112,13 +112,13 @@ export class CommentController { //@ApiCustomResponse(CommentResponse) @Post() addComment( - @Body() unfiedContactData: UnifiedCommentInput, + @Body() unfiedCommentData: UnifiedCommentInput, @Headers('integrationId') integrationId: string, @Headers('linkedUserId') linkedUserId: string, @Query('remoteData') remote_data?: boolean, ) { return this.commentService.addComment( - unfiedContactData, + unfiedCommentData, integrationId, linkedUserId, remote_data, @@ -142,13 +142,13 @@ export class CommentController { //@ApiCustomResponse(CommentResponse) @Post('batch') addComments( - @Body() unfiedContactData: UnifiedCommentInput[], + @Body() unfiedCommentData: UnifiedCommentInput[], @Headers('integrationId') integrationId: string, @Headers('linkedUserId') linkedUserId: string, @Query('remoteData') remote_data?: boolean, ) { return this.commentService.batchAddComments( - unfiedContactData, + unfiedCommentData, integrationId, linkedUserId, remote_data, diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index 6ba8ab2f1..ebe409bcf 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; -import { ZendeskService } from './zendesk'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; @@ -10,21 +9,19 @@ import { UnifiedCommentInput, UnifiedCommentOutput, } from '../types/model.unified'; -import { CommentResponse } from '../types'; +import { CommentResponse, ICommentService } from '../types'; import { desunify } from '@@core/utils/unification/desunify'; import { TicketingObject } from '@ticketing/@utils/@types'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { unify } from '@@core/utils/unification/unify'; import { ServiceRegistry } from './registry.service'; +import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; @Injectable() export class CommentService { constructor( private prisma: PrismaService, - private zendesk: ZendeskService, private logger: LoggerService, private webhook: WebhookService, - private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, ) { this.logger.setContext(CommentService.name); @@ -36,7 +33,36 @@ export class CommentService { linkedUserId: string, remote_data?: boolean, ): Promise> { - return; + try { + const responses = await Promise.all( + unifiedCommentData.map((unifiedData) => + this.addComment( + unifiedData, + integrationId.toLowerCase(), + linkedUserId, + remote_data, + ), + ), + ); + + const allComments = responses.flatMap( + (response) => response.data.comments, + ); + const allRemoteData = responses.flatMap( + (response) => response.data.remote_data || [], + ); + + return { + data: { + comments: allComments, + remote_data: allRemoteData, + }, + message: 'All comments inserted successfully', + statusCode: 201, + }; + } catch (error) { + handleServiceError(error, this.logger); + } } async addComment( @@ -45,7 +71,164 @@ export class CommentService { linkedUserId: string, remote_data?: boolean, ): Promise> { - return; + try { + const linkedUser = await this.prisma.linked_users.findUnique({ + where: { + id_linked_user: linkedUserId, + }, + }); + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.comment.created', //sync, push or pull + method: 'POST', + url: '/ticketing/comment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + const job_id = job_resp_create.id_event; + + //desunify the data according to the target obj wanted + const desunifiedObject = await desunify({ + sourceObject: unifiedCommentData, + targetType: TicketingObject.comment, + providerName: integrationId, + customFieldMappings: [], + }); + + const service: ICommentService = + this.serviceRegistry.getService(integrationId); + //get remote_id of the ticket so the comment is inserted successfully + const ticket = await this.prisma.tcg_tickets.findUnique({ + where: { + id_tcg_ticket: unifiedCommentData.ticket_id, + }, + select: { + remote_id: true, + }, + }); + const resp: ApiResponse = await service.addComment( + desunifiedObject, + linkedUserId, + ticket.remote_id, + ); + + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject: [resp.data], + targetType: TicketingObject.comment, + providerName: integrationId, + customFieldMappings: [], + })) as UnifiedCommentOutput[]; + + // add the comment inside our db + const source_comment = resp.data; + const target_comment = unifiedObject[0]; + const originId = + 'id' in source_comment ? String(source_comment.id) : undefined; //TODO + + const existingComment = await this.prisma.tcg_comments.findFirst({ + where: { + remote_id: originId, + remote_platform: integrationId, + events: { + id_linked_user: linkedUserId, + }, + }, + }); + + let unique_ticketing_comment_id: string; + + if (existingComment) { + // Update the existing comment + const res = await this.prisma.tcg_comments.update({ + where: { + id_tcg_comment: existingComment.id_tcg_comment, + }, + data: { + body: target_comment.body, + html_body: target_comment.html_body, + is_private: target_comment.is_private, + author_type: target_comment.author_type, + id_tcg_ticket: target_comment.ticket_id, + id_event: job_id, + modified_at: new Date(), + }, + }); + unique_ticketing_comment_id = res.id_tcg_comment; + } else { + // Create a new comment + this.logger.log('comment not exists'); + const data = { + id_tcg_comment: uuidv4(), + body: target_comment.body, + html_body: target_comment.html_body, + is_private: target_comment.is_private, + created_at: new Date(), + modified_at: new Date(), + author_type: target_comment.author_type, + id_tcg_ticket: target_comment.ticket_id, + id_event: job_id, + remote_id: originId, + remote_platform: integrationId, + //TODO; id_tcg_contact String? @db.Uuid + //TODO; id_tcg_user String? @db.Uuid + }; + const res = await this.prisma.tcg_comments.create({ + data: data, + }); + unique_ticketing_comment_id = res.id_tcg_comment; + } + + if (remote_data) { + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_comment_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_comment_id, + format: 'json', + data: JSON.stringify(source_comment), + created_at: new Date(), + }, + update: { + data: JSON.stringify(source_comment), + created_at: new Date(), + }, + }); + } + + ///// + const result_comment = await this.getComment( + unique_ticketing_comment_id, + remote_data, + ); + + const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: status_resp, + }, + }); + await this.webhook.handleWebhook( + result_comment.data.comments, + 'ticketing.comment.created', + linkedUser.id_project, + job_id, + ); + return { ...resp, data: result_comment.data }; + } catch (error) { + handleServiceError(error, this.logger); + } } async getComment( diff --git a/packages/api/src/ticketing/comment/services/front/index.ts b/packages/api/src/ticketing/comment/services/front/index.ts new file mode 100644 index 000000000..a0663be98 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/front/index.ts @@ -0,0 +1,118 @@ +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 { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ICommentService } from '@ticketing/comment/types'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { FrontCommentOutput } from './types'; +import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class FrontService implements ICommentService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.comment.toUpperCase() + ':' + FrontService.name, + ); + this.registry.registerService('front', this); + } + async addComment( + commentData: DesunifyReturnType, + linkedUserId: string, + remoteIdTicket: string, + ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.write + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'front', + }, + }); + const dataBody = commentData; + const resp = await axios.post( + `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, + JSON.stringify(dataBody), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data, + message: 'Front comment created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + 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: 'front', + }, + }); + //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.frontapp.com/conversations/${ticket.remote_id}/comments`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced front comments !`); + + return { + data: resp.data._results, + message: 'Front comments retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + TicketingObject.comment, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts new file mode 100644 index 000000000..26c3a9a8c --- /dev/null +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -0,0 +1,47 @@ +import { ICommentMapper } from '@ticketing/comment/types'; +import { + UnifiedCommentInput, + UnifiedCommentOutput, +} from '@ticketing/comment/types/model.unified'; +import { FrontCommentInput, FrontCommentOutput } from './types'; + +export class FrontCommentMapper implements ICommentMapper { + desunify( + source: UnifiedCommentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): FrontCommentInput { + //TODO + return; + } + + unify( + source: FrontCommentOutput | FrontCommentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedCommentOutput | UnifiedCommentOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleCommentToUnified(source, customFieldMappings); + } + return source.map((comment) => + this.mapSingleCommentToUnified(comment, customFieldMappings), + ); + } + + private mapSingleCommentToUnified( + comment: FrontCommentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedCommentOutput { + /*TODO const field_mappings = customFieldMappings.map((mapping) => ({ + [mapping.slug]: comment.custom_fields[mapping.remote_id], + }));*/ + return; + } +} diff --git a/packages/api/src/ticketing/comment/services/front/types.ts b/packages/api/src/ticketing/comment/services/front/types.ts new file mode 100644 index 000000000..83298fada --- /dev/null +++ b/packages/api/src/ticketing/comment/services/front/types.ts @@ -0,0 +1,50 @@ +export type FrontCommentInput = { + author_id?: string; + body: string; + attachments?: string[]; +}; + +export type FrontCommentOutput = { + _links: Links; + id: string; + author?: Author; + body: string; + posted_at: number; + attachments?: Attachment[]; +}; + +type Links = { + self: string; + related?: Record; +}; + +type CustomFields = { + [key: string]: string | boolean | number; +}; + +type Author = { + _links: Links; + id: string; + email: string; + username: string; + first_name: string; + last_name: string; + is_admin: boolean; + is_available: boolean; + is_blocked: boolean; + custom_fields: CustomFields; +}; + +type AttachmentMetadata = { + is_inline: boolean; + cid: string; +}; + +type Attachment = { + id: string; + filename: string; + url: string; + content_type: string; + size: number; + metadata: AttachmentMetadata; +}; diff --git a/packages/api/src/ticketing/comment/services/github/index.ts b/packages/api/src/ticketing/comment/services/github/index.ts new file mode 100644 index 000000000..0ce4de98b --- /dev/null +++ b/packages/api/src/ticketing/comment/services/github/index.ts @@ -0,0 +1,120 @@ +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 { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ICommentService } from '@ticketing/comment/types'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; +import { ServiceRegistry } from '../registry.service'; +import { GithubCommentOutput } from './types'; + +@Injectable() +export class GithubService implements ICommentService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.comment.toUpperCase() + ':' + GithubService.name, + ); + this.registry.registerService('github', this); + } + async addComment( + commentData: DesunifyReturnType, + linkedUserId: string, + remoteIdTicket: string, + ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.write + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'github', + }, + }); + const dataBody = { + comment: commentData, + }; + const resp = await axios.post( + `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, + JSON.stringify(dataBody), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data, + message: 'Github comment created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + 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: 'github', + }, + }); + //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.frontapp.com/conversations/${ticket.remote_id}/comments`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced github comments !`); + + return { + data: resp.data._results, + message: 'Front github retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + TicketingObject.comment, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/comment/services/github/mappers.ts b/packages/api/src/ticketing/comment/services/github/mappers.ts new file mode 100644 index 000000000..e2127e808 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/github/mappers.ts @@ -0,0 +1,47 @@ +import { ICommentMapper } from '@ticketing/comment/types'; +import { + UnifiedCommentInput, + UnifiedCommentOutput, +} from '@ticketing/comment/types/model.unified'; +import { GithubCommentInput, GithubCommentOutput } from './types'; + +export class GithubCommentMapper implements ICommentMapper { + desunify( + source: UnifiedCommentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GithubCommentInput { + //TODO + return; + } + + unify( + source: GithubCommentOutput | GithubCommentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedCommentOutput | UnifiedCommentOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleCommentToUnified(source, customFieldMappings); + } + return source.map((comment) => + this.mapSingleCommentToUnified(comment, customFieldMappings), + ); + } + + private mapSingleCommentToUnified( + comment: GithubCommentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedCommentOutput { + /*TODO const field_mappings = customFieldMappings.map((mapping) => ({ + [mapping.slug]: comment.custom_fields[mapping.remote_id], + }));*/ + return; + } +} diff --git a/packages/api/src/ticketing/comment/services/github/types.ts b/packages/api/src/ticketing/comment/services/github/types.ts new file mode 100644 index 000000000..bcc9d9ec5 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/github/types.ts @@ -0,0 +1,5 @@ +export type GithubCommentInput = { + id: string; +}; + +export type GithubCommentOutput = GithubCommentInput; diff --git a/packages/api/src/ticketing/comment/services/hubspot/index.ts b/packages/api/src/ticketing/comment/services/hubspot/index.ts new file mode 100644 index 000000000..d443341a8 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/hubspot/index.ts @@ -0,0 +1,120 @@ +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 { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ICommentService } from '@ticketing/comment/types'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; +import { ServiceRegistry } from '../registry.service'; +import { HubspotCommentOutput } from './types'; + +@Injectable() +export class HubspotService implements ICommentService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.comment.toUpperCase() + ':' + HubspotService.name, + ); + this.registry.registerService('hubspot_t', this); + } + async addComment( + commentData: DesunifyReturnType, + linkedUserId: string, + remoteIdTicket: string, + ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.write + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'hubspot_t', + }, + }); + const dataBody = { + comment: commentData, + }; + const resp = await axios.post( + `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, + JSON.stringify(dataBody), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data, + message: 'Hubspot comment created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Hubspot', + 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: 'hubspot_t', + }, + }); + //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.frontapp.com/conversations/${ticket.remote_id}/comments`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced hubspot comments !`); + + return { + data: resp.data._results, + message: 'Hubspot comments retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Hubspot', + TicketingObject.comment, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/comment/services/hubspot/mappers.ts b/packages/api/src/ticketing/comment/services/hubspot/mappers.ts new file mode 100644 index 000000000..f9f82953b --- /dev/null +++ b/packages/api/src/ticketing/comment/services/hubspot/mappers.ts @@ -0,0 +1,47 @@ +import { ICommentMapper } from '@ticketing/comment/types'; +import { + UnifiedCommentInput, + UnifiedCommentOutput, +} from '@ticketing/comment/types/model.unified'; +import { HubspotCommentInput, HubspotCommentOutput } from './types'; + +export class HubspotCommentMapper implements ICommentMapper { + desunify( + source: UnifiedCommentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): HubspotCommentInput { + //TODO + return; + } + + unify( + source: HubspotCommentOutput | HubspotCommentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedCommentOutput | UnifiedCommentOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleCommentToUnified(source, customFieldMappings); + } + return source.map((comment) => + this.mapSingleCommentToUnified(comment, customFieldMappings), + ); + } + + private mapSingleCommentToUnified( + comment: HubspotCommentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedCommentOutput { + /*TODO const field_mappings = customFieldMappings.map((mapping) => ({ + [mapping.slug]: comment.custom_fields[mapping.remote_id], + }));*/ + return; + } +} diff --git a/packages/api/src/ticketing/comment/services/hubspot/types.ts b/packages/api/src/ticketing/comment/services/hubspot/types.ts new file mode 100644 index 000000000..d3ec77b6f --- /dev/null +++ b/packages/api/src/ticketing/comment/services/hubspot/types.ts @@ -0,0 +1,5 @@ +export type HubspotCommentInput = { + id: string; +}; + +export type HubspotCommentOutput = HubspotCommentInput; diff --git a/packages/api/src/ticketing/comment/services/zendesk/index.ts b/packages/api/src/ticketing/comment/services/zendesk/index.ts index df5ab8ff6..1a94351c2 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/index.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/index.ts @@ -6,20 +6,19 @@ import { ApiResponse } from '@@core/utils/types'; import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { EnvironmentService } from '@@core/environment/environment.service'; import { ICommentService } from '@ticketing/comment/types'; import { TicketingObject } from '@ticketing/@utils/@types'; -import { ZendeskCommentOutput } from './types'; import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; - +import { ZendeskCommentOutput } from './types'; +import { EnvironmentService } from '@@core/environment/environment.service'; @Injectable() export class ZendeskService implements ICommentService { constructor( private prisma: PrismaService, private logger: LoggerService, - private cryptoService: EncryptionService, private env: EnvironmentService, + private cryptoService: EncryptionService, private registry: ServiceRegistry, ) { this.logger.setContext( @@ -30,9 +29,9 @@ export class ZendeskService implements ICommentService { async addComment( commentData: DesunifyReturnType, linkedUserId: string, + remoteIdTicket: string, ): Promise> { try { - //TODO: check required scope => crm.objects.contacts.write const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, @@ -40,11 +39,13 @@ export class ZendeskService implements ICommentService { }, }); const dataBody = { - comment: commentData, + ticket: { + comment: commentData, + }, }; - const ticketOriginalId = 0; //TODO: check if it exists first and retrieve it properly - const resp = await axios.post( - `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tickets/${ticketOriginalId}.json`, + //to add a comment on Zendesk you must update a ticket using the Ticket API + const resp = await axios.put( + `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tickets/${remoteIdTicket}.json`, JSON.stringify(dataBody), { headers: { @@ -70,10 +71,55 @@ export class ZendeskService implements ICommentService { ); } } - syncComments( + async syncComments( linkedUserId: string, - custom_properties?: string[], + id_ticket: string, ): Promise> { - throw new Error('Method not implemented.'); + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'zendesk_t', + }, + }); + //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://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tickets/${ + ticket.remote_id + }/comments.json`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced zendesk comments !`); + + return { + data: resp.data.comments, + message: 'Zendesk comments retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zendesk', + TicketingObject.comment, + ActionType.GET, + ); + } } } diff --git a/packages/api/src/ticketing/comment/services/zendesk/types.ts b/packages/api/src/ticketing/comment/services/zendesk/types.ts index f404340b1..3f248a54d 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/types.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/types.ts @@ -1,5 +1,192 @@ -export type ZendeskCommentInput = { +export type ZendeskCommentInput = BaseComment; + +export type ZendeskCommentOutput = ZendeskCommentInput & { + id: number; +}; + +type BaseComment = { + attachments: Attachment[]; // Read-only. Attachments, if any. + audit_id: number; // Read-only. The id of the ticket audit record. + author_id?: number; // The id of the comment author. + body?: string; // The comment string. + created_at: string; // Read-only. The time the comment was created. + html_body?: string; // The comment formatted as HTML. + metadata: Metadata; // Read-only. System information and comment flags. + plain_body: string; // Read-only. The comment presented as plain text. + public?: boolean; // true if a public comment; false if an internal note. + type: 'Comment' | 'VoiceComment'; // Read-only. Either 'Comment' or 'VoiceComment'. + uploads?: string[]; // List of tokens for comment attachments. + via?: Via; // Describes how the object was created. +}; + +type Attachment = { + content_type: string; // The content type of the image, e.g., "image/png". + content_url: string; // A full URL where the attachment image file can be downloaded. + deleted: boolean; // If true, the attachment has been deleted. + file_name: string; // The name of the image file. + height: string | null; // The height of the image file in pixels, or null if unknown. + id: number; // Automatically assigned when created. + inline: boolean; // If true, the attachment is excluded from the attachment list. + malware_access_override: boolean; // If true, you can download an attachment flagged as malware. + malware_scan_result: + | 'malware_found' + | 'malware_not_found' + | 'failed_to_scan' + | 'not_scanned'; // The result of the malware scan. + mapped_content_url: string; // The URL the attachment image file has been mapped to. + size: number; // The size of the image file in bytes. + thumbnails: Attachment[]; // An array of attachment objects. + url: string; // A URL to access the attachment details. + width: string | null; // The width of the image file in pixels, or null if unknown. +}; + +export type CustomField = { id: string; + value: any; +}; + +//48 ccs MAX otherwise 404 error +type EmailCc = + | { + user_email: string; + user_name?: string; + user_id?: never; + action?: 'put' | 'delete'; + } + | { + user_email?: never; + user_name?: never; + user_id: string; + action?: 'put' | 'delete'; + }; + +type Metadata = Record; + +type Via = { + channel: string; + source: + | EmailSource + | WebSource + | ZendeskWidgetSource + | FeedbackTabSource + | MobileSource + | ApiSource + | FollowUpSource + | BusinessRuleTriggerSource + | BusinessRuleAutomationSource + | ForumTopicSource + | SocialMediaSource + | ChatSource + | ChatOfflineMessageSource + | CallSource + | FacebookSource + | SystemMergedSource + | SystemFollowUpSource + | SystemSuspendedTicketSource + | SystemProblemTicketSolvedSource + | AnyChannelSource; + rel?: string; +}; + +type EmailSource = { + from: EmailDetails; + to: EmailDetails; + original_recipients?: string[]; +}; + +type EmailDetails = { + address: string; + name?: string; + email_ccs?: EmailCc[]; +}; + +type WebSource = any; // "Submit a request" on website has no additional details + +type ZendeskWidgetSource = { + zendesk_widget: any; // Replace 'any' with actual structure if available +}; + +type FeedbackTabSource = { + feedback_tab: any; // Replace 'any' with actual structure if available +}; + +type MobileSource = { + mobile: any; // Replace 'any' with actual structure if available +}; + +type ApiSource = { + api: any; // Replace 'any' with actual structure if available +}; + +type FollowUpSource = { + ticket_id: number; + subject: string; +}; + +type BusinessRuleTriggerSource = { + id: number; + title: string; + deleted?: boolean; + revision_id?: number; // Enterprise +}; + +type BusinessRuleAutomationSource = { + id: number; + title: string; + deleted?: boolean; +}; + +type ForumTopicSource = { + topic_id: number; + topic_name: string; }; -export type ZendeskCommentOutput = ZendeskCommentInput; +type SocialMediaSource = { + profile_url: string; + username: string; + name?: string; +}; + +type ChatSource = any; // Chat has no additional details + +type ChatOfflineMessageSource = { + chat_offline_message: any; // Replace 'any' with actual structure if available +}; + +type CallSource = { + phone: string; + formatted_phone: string; + name?: string; +}; + +type FacebookSource = { + name: string; + profile_url: string; + facebook_id: string; +}; + +type SystemMergedSource = { + ticket_id: number; + subject: string; +}; + +type SystemFollowUpSource = { + ticket_id: number; + subject: string; +}; + +type SystemSuspendedTicketSource = { + suspended_ticket_id: number; +}; + +type SystemProblemTicketSolvedSource = { + ticket_id: number; + subject: string; +}; + +type AnyChannelSource = { + service_info: any; // Replace 'any' with actual structure if available + supports_channelback: boolean; + supports_clickthrough: boolean; + registered_integration_service_name: string; +}; diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index 3a5998844..a61b849c2 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -64,11 +64,23 @@ export class SyncService implements OnModuleInit { const providers = TICKETING_PROVIDERS; for (const provider of providers) { try { - await this.syncCommentsForLinkedUser( - provider, - linkedUser.id_linked_user, - id_project, - ); + //call the sync comments for every ticket of the linkedUser (a comment is tied to a ticket) + const tickets = await this.prisma.tcg_tickets.findMany({ + where: { + remote_platform: provider, + events: { + id_linked_user: linkedUser.id_linked_user, + }, + }, + }); + for (const ticket of tickets) { + await this.syncCommentsForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ticket.id_tcg_ticket, + ); + } } catch (error) { handleServiceError(error, this.logger); } @@ -87,6 +99,7 @@ export class SyncService implements OnModuleInit { integrationId: string, linkedUserId: string, id_project: string, + id_ticket: string, ) { try { this.logger.log( @@ -129,7 +142,7 @@ export class SyncService implements OnModuleInit { const service: ICommentService = this.serviceRegistry.getService(integrationId); const resp: ApiResponse = - await service.syncComments(linkedUserId, remoteProperties); + await service.syncComments(linkedUserId, id_ticket, remoteProperties); const sourceObject: OriginalCommentOutput[] = resp.data; //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); @@ -145,13 +158,13 @@ export class SyncService implements OnModuleInit { const commentsIds = sourceObject.map((comment) => 'id' in comment ? String(comment.id) : undefined, ); - //insert the data in the DB with the fieldMappings (value table) const comments_data = await this.saveCommentsInDb( linkedUserId, unifiedObject, commentsIds, integrationId, + id_ticket, job_id, sourceObject, ); @@ -176,12 +189,99 @@ export class SyncService implements OnModuleInit { async saveCommentsInDb( linkedUserId: string, - tickets: UnifiedCommentOutput[], + comments: UnifiedCommentOutput[], originIds: string[], originSource: string, + id_ticket: string, jobId: string, remote_data: Record[], ): Promise { - return; + try { + let comments_results: TicketingComment[] = []; + for (let i = 0; i < comments.length; i++) { + const comment = comments[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingComment = await this.prisma.tcg_comments.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + events: { + id_linked_user: linkedUserId, + }, + }, + }); + + let unique_ticketing_comment_id: string; + + if (existingComment) { + // Update the existing comment + const res = await this.prisma.tcg_comments.update({ + where: { + id_tcg_comment: existingComment.id_tcg_comment, + }, + data: { + body: comment.body, + html_body: comment.html_body, + is_private: comment.is_private, + author_type: comment.author_type, + id_tcg_ticket: id_ticket, + id_event: jobId, + modified_at: new Date(), + }, + }); + unique_ticketing_comment_id = res.id_tcg_comment; + comments_results = [...comments_results, res]; + } else { + // Create a new comment + this.logger.log('comment not exists'); + const data = { + id_tcg_comment: uuidv4(), + body: comment.body, + html_body: comment.html_body, + is_private: comment.is_private, + created_at: new Date(), + modified_at: new Date(), + author_type: comment.author_type, + id_tcg_ticket: id_ticket, + id_event: jobId, + remote_id: originId, + remote_platform: originSource, + //TODO; id_tcg_contact String? @db.Uuid + //TODO; id_tcg_user String? @db.Uuid + }; + const res = await this.prisma.tcg_comments.create({ + data: data, + }); + comments_results = [...comments_results, res]; + unique_ticketing_comment_id = res.id_tcg_comment; + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_comment_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_comment_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return comments_results; + } catch (error) { + handleServiceError(error, this.logger); + } } } diff --git a/packages/api/src/ticketing/comment/types/index.ts b/packages/api/src/ticketing/comment/types/index.ts index 546489de2..2bdfe5264 100644 --- a/packages/api/src/ticketing/comment/types/index.ts +++ b/packages/api/src/ticketing/comment/types/index.ts @@ -8,10 +8,12 @@ export interface ICommentService { addComment( commentData: DesunifyReturnType, linkedUserId: string, + remoteIdTicket: string, ): Promise>; syncComments( linkedUserId: string, + idTicket: string, custom_properties?: string[], ): Promise>; } diff --git a/packages/api/src/ticketing/comment/types/mappingsTypes.ts b/packages/api/src/ticketing/comment/types/mappingsTypes.ts index 5e12ec50a..609276e10 100644 --- a/packages/api/src/ticketing/comment/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/comment/types/mappingsTypes.ts @@ -1,10 +1,22 @@ +import { FrontCommentMapper } from '../services/front/mappers'; +import { GithubCommentMapper } from '../services/github/mappers'; import { ZendeskCommentMapper } from '../services/zendesk/mappers'; const zendeskCommentMapper = new ZendeskCommentMapper(); +const githubCommentMapper = new GithubCommentMapper(); +const frontCommentMapper = new FrontCommentMapper(); export const commentUnificationMapping = { zendesk: { unify: zendeskCommentMapper.unify, desunify: zendeskCommentMapper.desunify, }, + front: { + unify: frontCommentMapper.unify, + desunify: frontCommentMapper.desunify, + }, + github: { + unify: githubCommentMapper.unify, + desunify: githubCommentMapper.desunify, + }, }; diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index f2dc53fcc..28be251aa 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -1,6 +1,18 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { Comment } from '@ticketing/comment/types'; -export class UnifiedCommentInput {} +export class UnifiedCommentInput { + body: string; + html_body: string; + is_private: boolean; + created_at: Date; + modified_at: Date; + author_type: string; + ticket_id: string; + contact_id?: string; + user_id?: string; +} -export class UnifiedCommentOutput extends UnifiedCommentInput {} +//TODO: add remote_id +export class UnifiedCommentOutput extends UnifiedCommentInput { + id: string; +} diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index fd644fd41..f7426591e 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -25,7 +25,7 @@ export class FrontTicketMapper implements ITicketMapper { subject: source.name, inbox_id: source.assigned_to?.[0], // TODO comment: { - body: source.comments[0].body || '', //TODO: handle where a lot of comments must be added + body: '', //source.comments[0].body || '', //TODO: handle where a lot of comments must be added //TODO: attachments: [''], }, }; @@ -70,17 +70,6 @@ export class FrontTicketMapper implements ITicketMapper { remote_id: string; }[], ): UnifiedTicketOutput { - //TODO: retrieve comments for the conv id: https://dev.frontapp.com/reference/get_conversations-conversation-id-comments - const comments = []; - /*ticket.comments?.map((comment) => ({ - body: comment.body, - html_body: comment.html_body, - is_private: comment.is_private, - remote_id: comment.id.toString(), - // Assuming attachments are URLs in string format - attachments: comment.attachments?.map((att) => ({ url: att })), - }));*/ - const field_mappings = customFieldMappings?.map((mapping) => ({ [mapping.slug]: ticket.custom_fields?.[mapping.remote_id], })); @@ -93,7 +82,6 @@ export class FrontTicketMapper implements ITicketMapper { due_date: new Date(ticket.created_at), // todo ? tags: JSON.stringify(ticket.tags?.map((tag) => tag.name)), assigned_to: ticket.assignee ? [ticket.assignee.email] : undefined, - comments: comments, field_mappings: field_mappings, }; diff --git a/packages/api/src/ticketing/ticket/services/github/index.ts b/packages/api/src/ticketing/ticket/services/github/index.ts new file mode 100644 index 000000000..88c9f16c0 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/github/index.ts @@ -0,0 +1,103 @@ +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 { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; +import { ServiceRegistry } from '../registry.service'; +import { GithubTicketInput, GithubTicketOutput } from './types'; + +@Injectable() +export class GithubService implements ITicketService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.ticket.toUpperCase() + ':' + GithubService.name, + ); + this.registry.registerService('github', this); + } + async addTicket( + ticketData: GithubTicketInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'github', + }, + }); + const dataBody = ticketData; + const resp = await axios.post( + `https://api2.frontapp.com/conversations`, + JSON.stringify(dataBody), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data, + message: 'Github ticket created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + TicketingObject.ticket, + ActionType.POST, + ); + } + } + async syncTickets( + linkedUserId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'github', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/conversations', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced github tickets !`); + + return { + data: resp.data._results, + message: 'Github tickets retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + TicketingObject.ticket, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/ticket/services/github/mappers.ts b/packages/api/src/ticketing/ticket/services/github/mappers.ts new file mode 100644 index 000000000..c4149c025 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/github/mappers.ts @@ -0,0 +1,28 @@ +import { ITicketMapper } from '@ticketing/ticket/types'; +import { GithubTicketInput, GithubTicketOutput } from './types'; +import { + UnifiedTicketInput, + UnifiedTicketOutput, +} from '@ticketing/ticket/types/model.unified'; + +export class GithubTicketMapper implements ITicketMapper { + desunify( + source: UnifiedTicketInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GithubTicketInput { + return; + } + + unify( + source: GithubTicketOutput | GithubTicketOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketOutput | UnifiedTicketOutput[] { + return; + } +} diff --git a/packages/api/src/ticketing/ticket/services/github/types.ts b/packages/api/src/ticketing/ticket/services/github/types.ts new file mode 100644 index 000000000..f28626572 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/github/types.ts @@ -0,0 +1,5 @@ +export type GithubTicketInput = { + id: string; +}; + +export type GithubTicketOutput = GithubTicketInput; diff --git a/packages/api/src/ticketing/ticket/services/hubspot/index.ts b/packages/api/src/ticketing/ticket/services/hubspot/index.ts new file mode 100644 index 000000000..86d4665f4 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/hubspot/index.ts @@ -0,0 +1,103 @@ +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 { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; +import { ServiceRegistry } from '../registry.service'; +import { HubspotTicketInput, HubspotTicketOutput } from './types'; + +@Injectable() +export class HubspotService implements ITicketService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.ticket.toUpperCase() + ':' + HubspotService.name, + ); + this.registry.registerService('hubspot_t', this); + } + async addTicket( + ticketData: HubspotTicketInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'hubspot_t', + }, + }); + const dataBody = ticketData; + const resp = await axios.post( + `https://api2.frontapp.com/conversations`, + JSON.stringify(dataBody), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data, + message: 'Hubspot ticket created', + statusCode: 201, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Hubspot', + TicketingObject.ticket, + ActionType.POST, + ); + } + } + async syncTickets( + linkedUserId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'hubspot_t', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/conversations', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced hubspot tickets !`); + + return { + data: resp.data._results, + message: 'Hubspot tickets retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Hubspot', + TicketingObject.ticket, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts new file mode 100644 index 000000000..f027c588d --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts @@ -0,0 +1,43 @@ +import { ITicketMapper } from '@ticketing/ticket/types'; +import { HubspotTicketInput, HubspotTicketOutput } from './types'; +import { + UnifiedTicketInput, + UnifiedTicketOutput, +} from '@ticketing/ticket/types/model.unified'; + +export class HubspotTicketMapper implements ITicketMapper { + desunify( + source: UnifiedTicketInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): HubspotTicketInput { + return; + } + + unify( + source: HubspotTicketOutput | HubspotTicketOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketOutput | UnifiedTicketOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTicketToUnified( + ticket: HubspotTicketOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketOutput { + return; + } +} diff --git a/packages/api/src/ticketing/ticket/services/hubspot/types.ts b/packages/api/src/ticketing/ticket/services/hubspot/types.ts new file mode 100644 index 000000000..2c1bbfcbf --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/hubspot/types.ts @@ -0,0 +1,5 @@ +export type HubspotTicketInput = { + id: string; +}; + +export type HubspotTicketOutput = HubspotTicketInput; diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index af7b5e754..af756bfbc 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -14,16 +14,13 @@ import { desunify } from '@@core/utils/unification/desunify'; import { TicketingObject } from '@ticketing/@utils/@types'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { unify } from '@@core/utils/unification/unify'; -import { normalizeComments } from '../utils'; import { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from './registry.service'; -import { ZendeskService } from './zendesk'; @Injectable() export class TicketService { constructor( private prisma: PrismaService, - private zendesk: ZendeskService, private logger: LoggerService, private webhook: WebhookService, private fieldMappingService: FieldMappingService, @@ -143,11 +140,8 @@ export class TicketService { id_linked_user: linkedUserId, }, }, - include: { tcg_comments: true }, }); - const { normalizedComments } = normalizeComments(target_ticket.comments); - let unique_ticketing_ticket_id: string; if (existingTicket) { @@ -168,17 +162,6 @@ export class TicketService { priority: target_ticket.priority || '', assigned_to: target_ticket.assigned_to || [], modified_at: new Date(), - tcg_comments: { - update: normalizedComments.map((comment, index) => ({ - where: { - id_tcg_ticket: - existingTicket.tcg_comments[index].id_tcg_ticket, - id_tcg_comment: - existingTicket.tcg_comments[index].id_tcg_comment, - }, - data: comment, - })), - }, }, }); unique_ticketing_ticket_id = res.id_tcg_ticket; @@ -204,12 +187,6 @@ export class TicketService { remote_platform: integrationId, }; - if (normalizedComments) { - data['tcg_comments'] = { - create: normalizedComments, - }; - } - const res = await this.prisma.tcg_tickets.create({ data: data, }); @@ -313,9 +290,6 @@ export class TicketService { where: { id_tcg_ticket: id_ticketing_ticket, }, - include: { - tcg_comments: true, - }, }); // Fetch field mappings for the ticket @@ -355,12 +329,6 @@ export class TicketService { completed_at: ticket.completed_at || null, priority: ticket.priority || '', assigned_to: ticket.assigned_to || [], - comments: ticket.tcg_comments.map((comment) => ({ - remote_id: comment.remote_id, - body: comment.body, - html_body: comment.html_body, - is_private: comment.is_private, - })), field_mappings: field_mappings, }; diff --git a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts index 08c18bf6f..9c2ad615c 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts @@ -32,10 +32,13 @@ export class ZendeskTicketMapper implements ITicketMapper { type: source.type as 'problem' | 'incident' | 'question' | 'task', updated_at: source.completed_at?.toISOString(), comment: { + body: '', //TODO + }, + /*comment: { body: source.comments[0].body, html_body: source.comments[0].html_body, public: !source.comments[0].is_private, - }, + },*/ }; if (customFieldMappings && source.field_mappings) { @@ -94,16 +97,6 @@ export class ZendeskTicketMapper implements ITicketMapper { completed_at: undefined, // If available, add logic to determine the completed date priority: ticket.priority, assigned_to: undefined, // If available, add logic to map assigned users - comments: ticket.comment - ? [ - { - remote_id: ticket.comment.id.toString(), - body: ticket.comment.body, - html_body: ticket.comment.html_body, - is_private: !ticket.comment.public, - }, - ] - : undefined, field_mappings: undefined, // Add logic to map custom fields if available id: ticket.id.toString(), }; diff --git a/packages/api/src/ticketing/ticket/sync/sync.service.ts b/packages/api/src/ticketing/ticket/sync/sync.service.ts index bf2f048fc..8620bd299 100644 --- a/packages/api/src/ticketing/ticket/sync/sync.service.ts +++ b/packages/api/src/ticketing/ticket/sync/sync.service.ts @@ -11,7 +11,6 @@ import { TicketingObject } from '@ticketing/@utils/@types'; import { UnifiedTicketOutput } from '../types/model.unified'; import { WebhookService } from '@@core/webhook/webhook.service'; import { tcg_tickets as TicketingTicket } from '@prisma/client'; -import { normalizeComments } from '../utils'; import { ITicketService } from '../types'; import { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../services/registry.service'; @@ -204,8 +203,6 @@ export class SyncService implements OnModuleInit { include: { tcg_comments: true }, }); - const { normalizedComments } = normalizeComments(ticket.comments); - let unique_ticketing_ticket_id: string; if (existingTicket) { @@ -226,17 +223,6 @@ export class SyncService implements OnModuleInit { priority: ticket.priority || '', assigned_to: ticket.assigned_to || [], modified_at: new Date(), - tcg_comments: { - update: normalizedComments.map((comment, index) => ({ - where: { - id_tcg_ticket: - existingTicket.tcg_comments[index].id_tcg_ticket, - id_tcg_comment: - existingTicket.tcg_comments[index].id_tcg_comment, - }, - data: comment, - })), - }, }, }); unique_ticketing_ticket_id = res.id_tcg_ticket; @@ -262,12 +248,6 @@ export class SyncService implements OnModuleInit { remote_id: originId, remote_platform: originSource, }; - - if (normalizedComments) { - data['tcg_comments'] = { - create: normalizedComments, - }; - } const res = await this.prisma.tcg_tickets.create({ data: data, }); diff --git a/packages/api/src/ticketing/ticket/types/mappingsTypes.ts b/packages/api/src/ticketing/ticket/types/mappingsTypes.ts index ce6600804..08e8b6c24 100644 --- a/packages/api/src/ticketing/ticket/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/ticket/types/mappingsTypes.ts @@ -1,10 +1,28 @@ +import { FrontTicketMapper } from '../services/front/mappers'; +import { GithubTicketMapper } from '../services/github/mappers'; +import { HubspotTicketMapper } from '../services/hubspot/mappers'; import { ZendeskTicketMapper } from '../services/zendesk/mappers'; const zendeskTicketMapper = new ZendeskTicketMapper(); +const frontTicketMapper = new FrontTicketMapper(); +const githubTicketMapper = new GithubTicketMapper(); +const hubspotTicketMapper = new HubspotTicketMapper(); export const ticketUnificationMapping = { zendesk: { unify: zendeskTicketMapper.unify, desunify: zendeskTicketMapper.desunify, }, + front: { + unify: frontTicketMapper.unify, + desunify: frontTicketMapper.desunify, + }, + github: { + unify: githubTicketMapper.unify, + desunify: githubTicketMapper.desunify, + }, + hubspot: { + unify: hubspotTicketMapper.unify, + desunify: hubspotTicketMapper.desunify, + }, }; diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index b20493260..7a854922d 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -1,5 +1,4 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { Comment } from '@ticketing/ticket/types'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class UnifiedTicketInput { name: string; @@ -12,12 +11,11 @@ export class UnifiedTicketInput { completed_at?: Date; priority?: string; assigned_to?: string[]; - comments: Comment[]; @ApiPropertyOptional({ type: [{}] }) field_mappings?: Record[]; } export class UnifiedTicketOutput extends UnifiedTicketInput { - @ApiPropertyOptional() - id?: string; + @ApiProperty() + id: string; } diff --git a/packages/api/src/ticketing/ticket/utils/index.ts b/packages/api/src/ticketing/ticket/utils/index.ts index e50c7be29..e69de29bb 100644 --- a/packages/api/src/ticketing/ticket/utils/index.ts +++ b/packages/api/src/ticketing/ticket/utils/index.ts @@ -1,14 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; -import { Comment } from '@ticketing/ticket/types'; - -export function normalizeComments(comments: Comment[]) { - const normalizedComments = comments.map((comment) => ({ - ...comment, - created_at: new Date(), - modified_at: new Date(), - id_tcg_comment: uuidv4(), - })); - return { - normalizedComments, - }; -} From 5cc8221c671acda2edb5ac2db918f6bf36371e68 Mon Sep 17 00:00:00 2001 From: nael Date: Wed, 3 Jan 2024 17:17:28 +0100 Subject: [PATCH 04/25] :construction: Fixed build --- .../src/crm/contact/services/hubspot/types.ts | 15 +- .../src/ticketing/comment/comment.module.ts | 6 + .../comment/services/hubspot/index.ts | 10 +- .../comment/services/zendesk/types.ts | 4 +- .../ticketing/ticket/services/front/index.ts | 3 +- .../ticketing/ticket/services/github/index.ts | 3 +- .../ticket/services/hubspot/index.ts | 26 ++- .../ticket/services/hubspot/types.ts | 33 ++- .../ticket/services/zendesk/index.ts | 3 +- .../api/src/ticketing/ticket/ticket.module.ts | 6 + packages/api/swagger/swagger-spec.json | 214 +++++++++++++++++- 11 files changed, 293 insertions(+), 30 deletions(-) diff --git a/packages/api/src/crm/contact/services/hubspot/types.ts b/packages/api/src/crm/contact/services/hubspot/types.ts index 7e9de18ef..208850ca0 100644 --- a/packages/api/src/crm/contact/services/hubspot/types.ts +++ b/packages/api/src/crm/contact/services/hubspot/types.ts @@ -16,6 +16,14 @@ export interface HubspotContactInput { [key: string]: any; } +export interface HubspotContactOutput { + id: string; + properties: HubspotPropertiesOuput; + createdAt: string; + updatedAt: string; + archived: boolean; +} + type HubspotPropertiesOuput = { createdate: string; email: string; @@ -35,10 +43,3 @@ export const commonHubspotProperties = { lastname: '', // Add any other common properties here }; -export interface HubspotContactOutput { - id: string; - properties: HubspotPropertiesOuput; - createdAt: string; - updatedAt: string; - archived: boolean; -} diff --git a/packages/api/src/ticketing/comment/comment.module.ts b/packages/api/src/ticketing/comment/comment.module.ts index 0e9f8e6c7..e33d3d1ae 100644 --- a/packages/api/src/ticketing/comment/comment.module.ts +++ b/packages/api/src/ticketing/comment/comment.module.ts @@ -10,6 +10,9 @@ import { CommentController } from './comment.controller'; import { CommentService } from './services/comment.service'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './services/registry.service'; +import { GithubService } from './services/github'; +import { FrontService } from './services/front'; +import { HubspotService } from './services/hubspot'; @Module({ imports: [ @@ -29,6 +32,9 @@ import { ServiceRegistry } from './services/registry.service'; ServiceRegistry, /* PROVIDERS SERVICES */ ZendeskService, + HubspotService, + FrontService, + GithubService, ], exports: [SyncService], }) diff --git a/packages/api/src/ticketing/comment/services/hubspot/index.ts b/packages/api/src/ticketing/comment/services/hubspot/index.ts index d443341a8..ccab00f34 100644 --- a/packages/api/src/ticketing/comment/services/hubspot/index.ts +++ b/packages/api/src/ticketing/comment/services/hubspot/index.ts @@ -32,7 +32,7 @@ export class HubspotService implements ICommentService { ): Promise> { try { //TODO: check required scope => crm.objects.contacts.write - const connection = await this.prisma.connections.findFirst({ + /*const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, provider_slug: 'hubspot_t', @@ -57,7 +57,8 @@ export class HubspotService implements ICommentService { data: resp.data, message: 'Hubspot comment created', statusCode: 201, - }; + };*/ + return; } catch (error) { handleServiceError( error, @@ -73,7 +74,7 @@ export class HubspotService implements ICommentService { id_ticket: string, ): Promise> { try { - const connection = await this.prisma.connections.findFirst({ + /*const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, provider_slug: 'hubspot_t', @@ -106,7 +107,8 @@ export class HubspotService implements ICommentService { data: resp.data._results, message: 'Hubspot comments retrieved', statusCode: 200, - }; + };*/ + return; } catch (error) { handleServiceError( error, diff --git a/packages/api/src/ticketing/comment/services/zendesk/types.ts b/packages/api/src/ticketing/comment/services/zendesk/types.ts index 3f248a54d..2115f377c 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/types.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/types.ts @@ -19,7 +19,7 @@ type BaseComment = { via?: Via; // Describes how the object was created. }; -type Attachment = { +export type Attachment = { content_type: string; // The content type of the image, e.g., "image/png". content_url: string; // A full URL where the attachment image file can be downloaded. deleted: boolean; // If true, the attachment has been deleted. @@ -40,7 +40,7 @@ type Attachment = { width: string | null; // The width of the image file in pixels, or null if unknown. }; -export type CustomField = { +export type CustomField_ = { id: string; value: any; }; diff --git a/packages/api/src/ticketing/ticket/services/front/index.ts b/packages/api/src/ticketing/ticket/services/front/index.ts index 5d013378b..b85718a78 100644 --- a/packages/api/src/ticketing/ticket/services/front/index.ts +++ b/packages/api/src/ticketing/ticket/services/front/index.ts @@ -8,7 +8,6 @@ import { ApiResponse } from '@@core/utils/types'; import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; import { EnvironmentService } from '@@core/environment/environment.service'; -import { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; import { FrontTicketInput, FrontTicketOutput } from './types'; @@ -68,7 +67,7 @@ export class FrontService implements ITicketService { async syncTickets( linkedUserId: string, custom_properties?: string[], - ): Promise> { + ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { diff --git a/packages/api/src/ticketing/ticket/services/github/index.ts b/packages/api/src/ticketing/ticket/services/github/index.ts index 88c9f16c0..3113bb301 100644 --- a/packages/api/src/ticketing/ticket/services/github/index.ts +++ b/packages/api/src/ticketing/ticket/services/github/index.ts @@ -7,7 +7,6 @@ import { ITicketService } from '@ticketing/ticket/types'; import { ApiResponse } from '@@core/utils/types'; import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; import { GithubTicketInput, GithubTicketOutput } from './types'; @@ -66,7 +65,7 @@ export class GithubService implements ITicketService { async syncTickets( linkedUserId: string, custom_properties?: string[], - ): Promise> { + ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { diff --git a/packages/api/src/ticketing/ticket/services/hubspot/index.ts b/packages/api/src/ticketing/ticket/services/hubspot/index.ts index 86d4665f4..c566eb1a1 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/index.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/index.ts @@ -7,9 +7,12 @@ import { ITicketService } from '@ticketing/ticket/types'; import { ApiResponse } from '@@core/utils/types'; import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; -import { HubspotTicketInput, HubspotTicketOutput } from './types'; +import { + HubspotTicketInput, + HubspotTicketOutput, + commonHubspotProperties, +} from './types'; @Injectable() export class HubspotService implements ITicketService { @@ -35,9 +38,9 @@ export class HubspotService implements ITicketService { provider_slug: 'hubspot_t', }, }); - const dataBody = ticketData; + const dataBody = { properties: ticketData }; const resp = await axios.post( - `https://api2.frontapp.com/conversations`, + `https://api.hubapi.com/crm/v3/objects/tickets`, JSON.stringify(dataBody), { headers: { @@ -66,7 +69,7 @@ export class HubspotService implements ITicketService { async syncTickets( linkedUserId: string, custom_properties?: string[], - ): Promise> { + ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { @@ -75,7 +78,16 @@ export class HubspotService implements ITicketService { }, }); - const resp = await axios.get('https://api2.frontapp.com/conversations', { + const commonPropertyNames = Object.keys(commonHubspotProperties); + const allProperties = [...commonPropertyNames, ...custom_properties]; + const baseURL = 'https://api.hubapi.com/crm/v3/objects/tickets/'; + + const queryString = allProperties + .map((prop) => `properties=${encodeURIComponent(prop)}`) + .join('&'); + + const url = `${baseURL}?${queryString}`; + const resp = await axios.get(url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cryptoService.decrypt( @@ -86,7 +98,7 @@ export class HubspotService implements ITicketService { this.logger.log(`Synced hubspot tickets !`); return { - data: resp.data._results, + data: resp.data.results, message: 'Hubspot tickets retrieved', statusCode: 200, }; diff --git a/packages/api/src/ticketing/ticket/services/hubspot/types.ts b/packages/api/src/ticketing/ticket/services/hubspot/types.ts index 2c1bbfcbf..806e39651 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/types.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/types.ts @@ -1,5 +1,36 @@ export type HubspotTicketInput = { + subject: string; + hs_pipeline: string; + hubspot_owner_id: string; + hs_pipeline_stage: string; + hs_ticket_priority: string; +}; + +export type HubspotTicketOutput = { id: string; + properties: TicketProperties; + createdAt: string; + updatedAt: string; + archived: boolean; }; -export type HubspotTicketOutput = HubspotTicketInput; +type TicketProperties = { + createdate: string; + hs_lastmodifieddate: string; + hs_pipeline: string; + hs_pipeline_stage: string; + hs_ticket_priority: string; + hubspot_owner_id: string; + subject: string; + [key: string]: string; +}; + +export const commonHubspotProperties = { + createdate: '', + hs_lastmodifieddate: '', + hs_pipeline: '', + hs_pipeline_stage: '', + hs_ticket_priority: '', + hubspot_owner_id: '', + subject: '', +}; diff --git a/packages/api/src/ticketing/ticket/services/zendesk/index.ts b/packages/api/src/ticketing/ticket/services/zendesk/index.ts index c0b375ce3..947ed3bd4 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/index.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/index.ts @@ -12,7 +12,6 @@ import { ApiResponse } from '@@core/utils/types'; import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; import { EnvironmentService } from '@@core/environment/environment.service'; -import { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; @Injectable() @@ -73,7 +72,7 @@ export class ZendeskService implements ITicketService { async syncTickets( linkedUserId: string, custom_properties?: string[], - ): Promise> { + ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { diff --git a/packages/api/src/ticketing/ticket/ticket.module.ts b/packages/api/src/ticketing/ticket/ticket.module.ts index 9fd39e2f2..fbe503b50 100644 --- a/packages/api/src/ticketing/ticket/ticket.module.ts +++ b/packages/api/src/ticketing/ticket/ticket.module.ts @@ -10,6 +10,9 @@ import { ZendeskService } from './services/zendesk'; import { BullModule } from '@nestjs/bull'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './services/registry.service'; +import { HubspotService } from './services/hubspot'; +import { FrontService } from './services/front'; +import { GithubService } from './services/github'; @Module({ imports: [ @@ -29,6 +32,9 @@ import { ServiceRegistry } from './services/registry.service'; ServiceRegistry, /* PROVIDERS SERVICES */ ZendeskService, + HubspotService, + FrontService, + GithubService, ], exports: [SyncService], }) diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index 4e46c94c1..6ecb1c092 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -848,6 +848,210 @@ ] } }, + "/crm/deal": { + "get": { + "operationId": "getDeals", + "summary": "List a batch of Deals", + "parameters": [ + { + "name": "integrationId", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "linkedUserId", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "remoteData", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Crm software.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "crm/deal" + ] + }, + "post": { + "operationId": "addDeal", + "summary": "Create a Deal", + "description": "Create a deal in any supported Crm software", + "parameters": [ + { + "name": "integrationId", + "required": true, + "in": "header", + "description": "The integration ID", + "schema": { + "type": "string" + } + }, + { + "name": "linkedUserId", + "required": true, + "in": "header", + "description": "The linked user ID", + "schema": { + "type": "string" + } + }, + { + "name": "remoteData", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Crm software.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnifiedDealInput" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "crm/deal" + ] + }, + "patch": { + "operationId": "updateDeal", + "summary": "Update a Deal", + "parameters": [ + { + "name": "id", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "crm/deal" + ] + } + }, + "/crm/deal/{id}": { + "get": { + "operationId": "getDeal", + "summary": "Retrieve a Deal", + "description": "Retrieve a deal from any connected Crm software", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "id of the you want to retrive.", + "schema": { + "type": "string" + } + }, + { + "name": "remoteData", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Crm software.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "crm/deal" + ] + } + }, + "/crm/deal/batch": { + "post": { + "operationId": "addDeals", + "summary": "Add a batch of Deals", + "parameters": [ + { + "name": "integrationId", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "linkedUserId", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "remoteData", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Crm software.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnifiedDealInput" + } + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "crm/deal" + ] + } + }, "/ticketing/ticket": { "get": { "operationId": "getTickets", @@ -1095,7 +1299,7 @@ "post": { "operationId": "addComment", "summary": "Create a Comment", - "description": "Create a ticket in any supported Ticketing software", + "description": "Create a comment in any supported Ticketing software", "parameters": [ { "name": "integrationId", @@ -1171,13 +1375,13 @@ "get": { "operationId": "getComment", "summary": "Retrieve a Comment", - "description": "Retrieve a ticket from any connected Ticketing software", + "description": "Retrieve a comment from any connected Ticketing software", "parameters": [ { "name": "id", "required": true, "in": "path", - "description": "id of the `ticket` you want to retrive.", + "description": "id of the `comment` you want to retrive.", "schema": { "type": "string" } @@ -1596,6 +1800,10 @@ "field_mappings" ] }, + "UnifiedDealInput": { + "type": "object", + "properties": {} + }, "UnifiedTicketInput": { "type": "object", "properties": { From d499efea84444e4c13cf2347d496544d1b3aa845 Mon Sep 17 00:00:00 2001 From: nael Date: Wed, 3 Jan 2024 19:19:43 +0100 Subject: [PATCH 05/25] :construction: Partial mapping done --- .../comment/services/front/mappers.ts | 23 ++++++--- .../comment/services/zendesk/mappers.ts | 26 +++++++--- .../comment/services/zendesk/types.ts | 2 +- .../ticketing/comment/types/model.unified.ts | 4 +- .../ticket/services/front/mappers.ts | 4 +- .../ticketing/ticket/services/github/index.ts | 25 +++++---- .../ticketing/ticket/services/github/types.ts | 8 ++- .../ticket/services/hubspot/mappers.ts | 51 +++++++++++++++++-- .../ticket/services/hubspot/types.ts | 2 +- .../ticket/services/zendesk/mappers.ts | 9 ++-- .../ticketing/ticket/types/model.unified.ts | 3 +- 11 files changed, 119 insertions(+), 38 deletions(-) diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts index 26c3a9a8c..830bbc428 100644 --- a/packages/api/src/ticketing/comment/services/front/mappers.ts +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -13,8 +13,12 @@ export class FrontCommentMapper implements ICommentMapper { remote_id: string; }[], ): FrontCommentInput { - //TODO - return; + const result: FrontCommentInput = { + body: source.body, + author_id: source.user_id || source.contact_id, //TODO: + }; + + return result; } unify( @@ -39,9 +43,16 @@ export class FrontCommentMapper implements ICommentMapper { remote_id: string; }[], ): UnifiedCommentOutput { - /*TODO const field_mappings = customFieldMappings.map((mapping) => ({ - [mapping.slug]: comment.custom_fields[mapping.remote_id], - }));*/ - return; + return { + id: comment.id, + body: comment.body, + html_body: '', + created_at: new Date(comment.posted_at * 1000), // Convert UNIX timestamp to Date + modified_at: new Date(), // Placeholder, as modified_at is not available + author_type: comment.author ? 'user' : '', + ticket_id: '', // TODO: Need to be determined from related data + contact_id: '', // TODO: Need to be determined from related data + user_id: '', //TODO + }; } } diff --git a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts index 4d464cf33..85fd2f606 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts @@ -13,8 +13,16 @@ export class ZendeskCommentMapper implements ICommentMapper { remote_id: string; }[], ): ZendeskCommentInput { - //TODO - return; + const result: ZendeskCommentInput = { + body: source.body, + html_body: source.html_body, + public: !source.is_private, + author_id: source.user_id ? parseInt(source.user_id) : undefined, + created_at: source.created_at.toISOString(), + type: 'Comment', + }; + + return result; } unify( @@ -39,9 +47,15 @@ export class ZendeskCommentMapper implements ICommentMapper { remote_id: string; }[], ): UnifiedCommentOutput { - /*TODO const field_mappings = customFieldMappings.map((mapping) => ({ - [mapping.slug]: comment.custom_fields[mapping.remote_id], - }));*/ - return; + return { + id: comment.id.toString(), + body: comment.body || '', + html_body: comment.html_body || '', + is_private: !comment.public, + created_at: new Date(comment.created_at), + modified_at: new Date(comment.created_at), // Assuming the creation date for modification as well + author_type: '', //TODO + ticket_id: '', //TODO + }; } } diff --git a/packages/api/src/ticketing/comment/services/zendesk/types.ts b/packages/api/src/ticketing/comment/services/zendesk/types.ts index 2115f377c..7d7f647d2 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/types.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/types.ts @@ -1,4 +1,4 @@ -export type ZendeskCommentInput = BaseComment; +export type ZendeskCommentInput = Partial; export type ZendeskCommentOutput = ZendeskCommentInput & { id: number; diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index 28be251aa..d3b5effa4 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -2,8 +2,8 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; export class UnifiedCommentInput { body: string; - html_body: string; - is_private: boolean; + html_body?: string; + is_private?: boolean; created_at: Date; modified_at: Date; author_type: string; diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index f7426591e..22065c84d 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -25,8 +25,8 @@ export class FrontTicketMapper implements ITicketMapper { subject: source.name, inbox_id: source.assigned_to?.[0], // TODO comment: { - body: '', //source.comments[0].body || '', //TODO: handle where a lot of comments must be added - //TODO: attachments: [''], + body: source.comment.body, + author_id: source.comment.contact_id || source.comment.user_id, }, }; diff --git a/packages/api/src/ticketing/ticket/services/github/index.ts b/packages/api/src/ticketing/ticket/services/github/index.ts index 3113bb301..6b919459f 100644 --- a/packages/api/src/ticketing/ticket/services/github/index.ts +++ b/packages/api/src/ticketing/ticket/services/github/index.ts @@ -10,6 +10,7 @@ import { ActionType, handleServiceError } from '@@core/utils/errors'; import { ServiceRegistry } from '../registry.service'; import { GithubTicketInput, GithubTicketOutput } from './types'; +//TODO @Injectable() export class GithubService implements ITicketService { constructor( @@ -35,8 +36,10 @@ export class GithubService implements ITicketService { }, }); const dataBody = ticketData; + const owner = ''; + const repo = ''; const resp = await axios.post( - `https://api2.frontapp.com/conversations`, + `https://api.github.com/repos/${owner}/${repo}/issues`, JSON.stringify(dataBody), { headers: { @@ -73,15 +76,19 @@ export class GithubService implements ITicketService { provider_slug: 'github', }, }); - - const resp = await axios.get('https://api2.frontapp.com/conversations', { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, + const owner = ''; + const repo = ''; + const resp = await axios.get( + `https://api.github.com/repos/${owner}/${repo}/issues`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, }, - }); + ); this.logger.log(`Synced github tickets !`); return { diff --git a/packages/api/src/ticketing/ticket/services/github/types.ts b/packages/api/src/ticketing/ticket/services/github/types.ts index f28626572..8faa96c83 100644 --- a/packages/api/src/ticketing/ticket/services/github/types.ts +++ b/packages/api/src/ticketing/ticket/services/github/types.ts @@ -1,5 +1,11 @@ export type GithubTicketInput = { - id: string; + title: string; + body?: string; + assignee?: string | null; + milestone?: string | number | null; + labels?: string[]; + assignees?: string[]; }; +//TODO export type GithubTicketOutput = GithubTicketInput; diff --git a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts index f027c588d..e1ccbbb97 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts @@ -1,5 +1,10 @@ import { ITicketMapper } from '@ticketing/ticket/types'; -import { HubspotTicketInput, HubspotTicketOutput } from './types'; +import { + HubspotTicketInput, + HubspotTicketOutput, + TicketProperties, + commonHubspotProperties, +} from './types'; import { UnifiedTicketInput, UnifiedTicketOutput, @@ -13,7 +18,30 @@ export class HubspotTicketMapper implements ITicketMapper { remote_id: string; }[], ): HubspotTicketInput { - return; + const properties: TicketProperties = { ...commonHubspotProperties }; + + properties.subject = source.name; + properties.hs_ticket_priority = source.priority || 'MEDIUM'; // Assuming 'MEDIUM' as a default + + // Map the custom fields + if (customFieldMappings && source.field_mappings) { + source.field_mappings.forEach((fieldMapping) => { + const mapping = customFieldMappings.find((m) => + fieldMapping.hasOwnProperty(m.slug), + ); + if (mapping && fieldMapping[mapping.slug]) { + properties[mapping.remote_id] = fieldMapping[mapping.slug]; + } + }); + } + + return { + subject: source.name, + hs_pipeline: 'default', // Replace 'default' with actual pipeline ID + hubspot_owner_id: '', // TODO Replace 'default' with actual owner ID + hs_pipeline_stage: '', // TODO Replace 'default' with actual pipeline stage + hs_ticket_priority: properties.hs_ticket_priority, + }; } unify( @@ -38,6 +66,23 @@ export class HubspotTicketMapper implements ITicketMapper { remote_id: string; }[], ): UnifiedTicketOutput { - return; + const field_mappings = customFieldMappings.map((mapping) => ({ + [mapping.slug]: ticket.properties[mapping.remote_id], + })); + + return { + id: ticket.id, + name: ticket.properties.subject, + status: ticket.properties.hs_pipeline_stage, // Map to your unified status + description: ticket.properties.subject, + due_date: new Date(ticket.properties.createdate), + type: '', // Define how you determine the type + parent_ticket: '', // Define how you determine the parent ticket + tags: '', // Define how you map or store tags + completed_at: new Date(ticket.properties.hs_lastmodifieddate), + priority: ticket.properties.hs_ticket_priority, + assigned_to: [], // Define how you determine assigned users + field_mappings: field_mappings, + }; } } diff --git a/packages/api/src/ticketing/ticket/services/hubspot/types.ts b/packages/api/src/ticketing/ticket/services/hubspot/types.ts index 806e39651..bc42e76d8 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/types.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/types.ts @@ -14,7 +14,7 @@ export type HubspotTicketOutput = { archived: boolean; }; -type TicketProperties = { +export type TicketProperties = { createdate: string; hs_lastmodifieddate: string; hs_pipeline: string; diff --git a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts index 9c2ad615c..e734ce075 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts @@ -32,13 +32,10 @@ export class ZendeskTicketMapper implements ITicketMapper { type: source.type as 'problem' | 'incident' | 'question' | 'task', updated_at: source.completed_at?.toISOString(), comment: { - body: '', //TODO + body: source.comment.body, + html_body: source.comment.html_body, + public: !source.comment.is_private, }, - /*comment: { - body: source.comments[0].body, - html_body: source.comments[0].html_body, - public: !source.comments[0].is_private, - },*/ }; if (customFieldMappings && source.field_mappings) { diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index 7a854922d..fdd518de0 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -1,4 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { UnifiedCommentInput } from '@ticketing/comment/types/model.unified'; export class UnifiedTicketInput { name: string; @@ -13,8 +14,8 @@ export class UnifiedTicketInput { assigned_to?: string[]; @ApiPropertyOptional({ type: [{}] }) field_mappings?: Record[]; + comment?: UnifiedCommentInput; } - export class UnifiedTicketOutput extends UnifiedTicketInput { @ApiProperty() id: string; From c2a3e60fd6c76aefe595a1c1eae102c7c7ddaf60 Mon Sep 17 00:00:00 2001 From: nael Date: Thu, 4 Jan 2024 14:53:42 +0100 Subject: [PATCH 06/25] :construction: Added user object boilerplate --- packages/api/scripts/commonObject.sh | 6 +- .../types/original/original.ticketing.ts | 18 +- .../comment/services/front/mappers.ts | 2 +- .../comment/services/hubspot/mappers.ts | 1 + .../comment/services/zendesk/mappers.ts | 3 +- .../ticketing/comment/types/model.unified.ts | 2 +- .../ticketing/ticket/services/front/index.ts | 3 - .../ticket/services/front/mappers.ts | 40 ++-- .../ticketing/ticket/services/front/types.ts | 1 + .../ticketing/ticket/services/github/index.ts | 19 +- .../ticketing/ticket/services/github/types.ts | 188 +++++++++++++++++- .../ticket/services/hubspot/mappers.ts | 47 ++--- .../ticket/services/hubspot/types.ts | 1 + .../ticket/services/zendesk/mappers.ts | 22 +- .../src/ticketing/ticket/ticket.controller.ts | 4 +- .../ticketing/ticket/types/model.unified.ts | 4 +- .../api/src/ticketing/ticketing.module.ts | 4 +- .../ticketing/user/services/front/index.ts | 63 ++++++ .../ticketing/user/services/front/mappers.ts | 43 ++++ .../ticketing/user/services/front/types.ts | 5 + .../ticketing/user/services/github/index.ts | 64 ++++++ .../ticketing/user/services/github/mappers.ts | 28 +++ .../ticketing/user/services/github/types.ts | 6 + .../user/services/registry.service.ts | 2 +- .../ticketing/user/services/user.service.ts | 18 +- .../ticketing/user/services/zendesk/index.ts | 60 ++++-- .../user/services/zendesk/mappers.ts | 43 ++++ .../ticketing/user/services/zendesk/types.ts | 6 +- .../src/ticketing/user/sync/sync.service.ts | 4 +- .../api/src/ticketing/user/types/index.ts | 7 +- .../src/ticketing/user/types/mappingsTypes.ts | 23 ++- .../src/ticketing/user/types/model.unified.ts | 9 +- .../api/src/ticketing/user/user.controller.ts | 69 ++++++- .../api/src/ticketing/user/user.module.ts | 14 +- .../api/src/ticketing/user/utils/index.ts | 1 + 35 files changed, 703 insertions(+), 127 deletions(-) create mode 100644 packages/api/src/ticketing/user/services/front/index.ts create mode 100644 packages/api/src/ticketing/user/services/front/mappers.ts create mode 100644 packages/api/src/ticketing/user/services/front/types.ts create mode 100644 packages/api/src/ticketing/user/services/github/index.ts create mode 100644 packages/api/src/ticketing/user/services/github/mappers.ts create mode 100644 packages/api/src/ticketing/user/services/github/types.ts diff --git a/packages/api/scripts/commonObject.sh b/packages/api/scripts/commonObject.sh index b9dcf9a72..0b9347dcd 100755 --- a/packages/api/scripts/commonObject.sh +++ b/packages/api/scripts/commonObject.sh @@ -352,7 +352,7 @@ export class ${ObjectCap}Controller { name: 'id', required: true, type: String, - description: 'id of the `${objectType}` you want to retrive.', + description: 'id of the ${objectType} you want to retrieve.', }) @ApiQuery({ name: 'remoteData', @@ -398,13 +398,13 @@ export class ${ObjectCap}Controller { //@ApiCustomResponse(${ObjectCap}Response) @Post() add${ObjectCap}( - @Body() unfied${ObjectCap}Data: Unified${ObjectCap}Input, + @Body() unified${ObjectCap}Data: Unified${ObjectCap}Input, @Headers('integrationId') integrationId: string, @Headers('linkedUserId') linkedUserId: string, @Query('remoteData') remote_data?: boolean, ) { return this.${objectType}Service.add${ObjectCap}( - unfied${ObjectCap}Data, + unified${ObjectCap}Data, integrationId, linkedUserId, remote_data, 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 c0511fa76..46f95f596 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -32,6 +32,14 @@ import { HubspotTicketInput, HubspotTicketOutput, } from '@ticketing/ticket/services/hubspot/types'; +import { + FrontUserInput, + FrontUserOutput, +} from '@ticketing/user/services/front/types'; +import { + GithubUserInput, + GithubUserOutput, +} from '@ticketing/user/services/github/types'; /* INPUT */ @@ -49,7 +57,10 @@ export type OriginalCommentInput = | GithubCommentInput | HubspotCommentInput; /* user */ -export type OriginalUserInput = ZendeskUserInput; +export type OriginalUserInput = + | ZendeskUserInput + | GithubUserInput + | FrontUserInput; /* attachment */ export type OriginalAttachmentInput = ZendeskAttachmentInput; @@ -75,7 +86,10 @@ export type OriginalCommentOutput = | GithubCommentOutput | HubspotCommentOutput; /* user */ -export type OriginalUserOutput = ZendeskUserOutput; +export type OriginalUserOutput = + | ZendeskUserOutput + | GithubUserOutput + | FrontUserOutput; /* attachment */ export type OriginalAttachmentOutput = ZendeskAttachmentOutput; diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts index 830bbc428..8ba7414bb 100644 --- a/packages/api/src/ticketing/comment/services/front/mappers.ts +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -49,7 +49,7 @@ export class FrontCommentMapper implements ICommentMapper { html_body: '', created_at: new Date(comment.posted_at * 1000), // Convert UNIX timestamp to Date modified_at: new Date(), // Placeholder, as modified_at is not available - author_type: comment.author ? 'user' : '', + author_type: comment.author ? 'user' : null, ticket_id: '', // TODO: Need to be determined from related data contact_id: '', // TODO: Need to be determined from related data user_id: '', //TODO diff --git a/packages/api/src/ticketing/comment/services/hubspot/mappers.ts b/packages/api/src/ticketing/comment/services/hubspot/mappers.ts index f9f82953b..92a262771 100644 --- a/packages/api/src/ticketing/comment/services/hubspot/mappers.ts +++ b/packages/api/src/ticketing/comment/services/hubspot/mappers.ts @@ -5,6 +5,7 @@ import { } from '@ticketing/comment/types/model.unified'; import { HubspotCommentInput, HubspotCommentOutput } from './types'; +//TODO: HUBSPOT DOES NOT HAVE A COMMENT ENDPOINT export class HubspotCommentMapper implements ICommentMapper { desunify( source: UnifiedCommentInput, diff --git a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts index 85fd2f606..ff695fa29 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts @@ -18,7 +18,6 @@ export class ZendeskCommentMapper implements ICommentMapper { html_body: source.html_body, public: !source.is_private, author_id: source.user_id ? parseInt(source.user_id) : undefined, - created_at: source.created_at.toISOString(), type: 'Comment', }; @@ -54,7 +53,7 @@ export class ZendeskCommentMapper implements ICommentMapper { is_private: !comment.public, created_at: new Date(comment.created_at), modified_at: new Date(comment.created_at), // Assuming the creation date for modification as well - author_type: '', //TODO + author_type: null, //TODO ticket_id: '', //TODO }; } diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index d3b5effa4..50fde361c 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -6,7 +6,7 @@ export class UnifiedCommentInput { is_private?: boolean; created_at: Date; modified_at: Date; - author_type: string; + author_type: 'user' | 'contact' | null; ticket_id: string; contact_id?: string; user_id?: string; diff --git a/packages/api/src/ticketing/ticket/services/front/index.ts b/packages/api/src/ticketing/ticket/services/front/index.ts index b85718a78..59311a1d3 100644 --- a/packages/api/src/ticketing/ticket/services/front/index.ts +++ b/packages/api/src/ticketing/ticket/services/front/index.ts @@ -7,7 +7,6 @@ import { ITicketService } from '@ticketing/ticket/types'; import { ApiResponse } from '@@core/utils/types'; import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { EnvironmentService } from '@@core/environment/environment.service'; import { ServiceRegistry } from '../registry.service'; import { FrontTicketInput, FrontTicketOutput } from './types'; @@ -17,7 +16,6 @@ export class FrontService implements ITicketService { private prisma: PrismaService, private logger: LoggerService, private cryptoService: EncryptionService, - private env: EnvironmentService, private registry: ServiceRegistry, ) { this.logger.setContext( @@ -66,7 +64,6 @@ export class FrontService implements ITicketService { } async syncTickets( linkedUserId: string, - custom_properties?: string[], ): Promise> { try { const connection = await this.prisma.connections.findFirst({ diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index 22065c84d..72547dc50 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -13,37 +13,33 @@ export class FrontTicketMapper implements ITicketMapper { remote_id: string; }[], ): FrontTicketInput { - // For simplicity, we're assuming that the attachments are provided as string URLs. - // In a real implementation, we would need to handle binary data for attachments. - //TODO: handle attachments - /*const attachments = source.comments?.[0]?.attachments?.map( - (attachment) => attachment.url, - );*/ - const result: FrontTicketInput = { type: 'discussion', // Assuming 'discussion' as a default type for Front conversations subject: source.name, - inbox_id: source.assigned_to?.[0], // TODO + teammate_ids: source.assigned_to, comment: { body: source.comment.body, - author_id: source.comment.contact_id || source.comment.user_id, + author_id: + source.comment.author_type === 'user' + ? source.comment.user_id + : source.comment.contact_id, + attachments: source.attachments, }, }; //TODO: custom fields => https://dev.frontapp.com/reference/patch_conversations-conversation-id - - // Custom fields mapping logic - /*if (customFieldMappings && source.field_mappings) { - result.custom_fields = {}; - customFieldMappings.forEach((mapping) => { - const fieldMapping = source.field_mappings?.find( - (fm) => fm[mapping.slug], - ); - if (fieldMapping && fieldMapping[mapping.slug]) { - result.custom_fields[mapping.remote_id] = fieldMapping[mapping.slug]; + if (customFieldMappings && source.field_mappings) { + for (const fieldMapping of source.field_mappings) { + for (const key in fieldMapping) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === key, + ); + if (mapping) { + result[mapping.remote_id] = fieldMapping[key]; + } } - }); - }*/ + } + } return result; } @@ -81,7 +77,7 @@ export class FrontTicketMapper implements ITicketMapper { description: ticket.subject, // todo: ? due_date: new Date(ticket.created_at), // todo ? tags: JSON.stringify(ticket.tags?.map((tag) => tag.name)), - assigned_to: ticket.assignee ? [ticket.assignee.email] : undefined, + assigned_to: ticket.assignee ? [ticket.assignee.email] : undefined, //TODO: it must be a uuid of a user object field_mappings: field_mappings, }; diff --git a/packages/api/src/ticketing/ticket/services/front/types.ts b/packages/api/src/ticketing/ticket/services/front/types.ts index 3ba849398..ef1520a85 100644 --- a/packages/api/src/ticketing/ticket/services/front/types.ts +++ b/packages/api/src/ticketing/ticket/services/front/types.ts @@ -4,6 +4,7 @@ export type FrontTicketInput = { teammate_ids?: string[]; subject: string; comment: Comment; + custom_fields?: CustomFields; }; type Comment = { diff --git a/packages/api/src/ticketing/ticket/services/github/index.ts b/packages/api/src/ticketing/ticket/services/github/index.ts index 6b919459f..d95aacaf5 100644 --- a/packages/api/src/ticketing/ticket/services/github/index.ts +++ b/packages/api/src/ticketing/ticket/services/github/index.ts @@ -78,21 +78,18 @@ export class GithubService implements ITicketService { }); const owner = ''; const repo = ''; - const resp = await axios.get( - `https://api.github.com/repos/${owner}/${repo}/issues`, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, + const resp = await axios.get(`https://api.github.com/repos/issues`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, }, - ); + }); this.logger.log(`Synced github tickets !`); return { - data: resp.data._results, + data: resp.data, message: 'Github tickets retrieved', statusCode: 200, }; diff --git a/packages/api/src/ticketing/ticket/services/github/types.ts b/packages/api/src/ticketing/ticket/services/github/types.ts index 8faa96c83..de468bb89 100644 --- a/packages/api/src/ticketing/ticket/services/github/types.ts +++ b/packages/api/src/ticketing/ticket/services/github/types.ts @@ -7,5 +7,189 @@ export type GithubTicketInput = { assignees?: string[]; }; -//TODO -export type GithubTicketOutput = GithubTicketInput; +export type GithubTicketOutput = { + id: number; + node_id: string; + url: string; + repository_url: string; + labels_url: string; + comments_url: string; + events_url: string; + html_url: string; + number: number; + state: string; + title: string; + body: string; + user: GitHubUser; + labels: GitHubLabel[]; + assignee: GitHubUser; + assignees: GitHubUser[]; + milestone: GitHubMilestone; + locked: boolean; + active_lock_reason: string; + comments: number; + pull_request: GitHubPullRequest; + closed_at: string | null; + created_at: string; + updated_at: string; + repository: GitHubRepository; + author_association: string; +}; + +type GitHubUser = { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; +}; + +type GitHubLabel = { + id: number; + node_id: string; + url: string; + name: string; + description: string; + color: string; + default: boolean; +}; + +type GitHubMilestone = { + url: string; + html_url: string; + labels_url: string; + id: number; + node_id: string; + number: number; + state: string; + title: string; + description: string; + creator: GitHubUser; + open_issues: number; + closed_issues: number; + created_at: string; + updated_at: string; + closed_at: string | null; + due_on: string | null; +}; + +type GitHubPullRequest = { + url: string; + html_url: string; + diff_url: string; + patch_url: string; +}; + +type GitHubRepository = { + id: number; + node_id: string; + name: string; + full_name: string; + owner: GitHubUser; + private: boolean; + html_url: string; + description: string; + fork: boolean; + url: string; + archive_url: string; + assignees_url: string; + blobs_url: string; + branches_url: string; + collaborators_url: string; + comments_url: string; + commits_url: string; + compare_url: string; + contents_url: string; + contributors_url: string; + deployments_url: string; + downloads_url: string; + events_url: string; + forks_url: string; + git_commits_url: string; + git_refs_url: string; + git_tags_url: string; + git_url: string; + issue_comment_url: string; + issue_events_url: string; + issues_url: string; + keys_url: string; + labels_url: string; + languages_url: string; + merges_url: string; + milestones_url: string; + notifications_url: string; + pulls_url: string; + releases_url: string; + ssh_url: string; + stargazers_url: string; + statuses_url: string; + subscribers_url: string; + subscription_url: string; + tags_url: string; + teams_url: string; + trees_url: string; + clone_url: string; + mirror_url: string | null; + hooks_url: string; + svn_url: string; + homepage: string | null; + language: string | null; + forks_count: number; + stargazers_count: number; + watchers_count: number; + size: number; + default_branch: string; + open_issues_count: number; + is_template: boolean; + topics: string[]; + has_issues: boolean; + has_projects: boolean; + has_wiki: boolean; + has_pages: boolean; + has_downloads: boolean; + archived: boolean; + disabled: boolean; + visibility: string; + pushed_at: string; + created_at: string; + updated_at: string; + permissions: GitHubPermissions; + allow_rebase_merge: boolean; + template_repository: any; // Replace 'any' with the actual structure, if available. + temp_clone_token: string; + allow_squash_merge: boolean; + allow_auto_merge: boolean; + delete_branch_on_merge: boolean; + allow_merge_commit: boolean; + subscribers_count: number; + network_count: number; + license: GitHubLicense; + forks: number; +}; +type GitHubLicense = { + key: string; + name: string; + url: string | null; + spdx_id: string | null; + node_id: string; + html_url: string; +}; + +type GitHubPermissions = { + admin: boolean; + push: boolean; + pull: boolean; +}; diff --git a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts index e1ccbbb97..e0e8cd8a6 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts @@ -1,10 +1,5 @@ import { ITicketMapper } from '@ticketing/ticket/types'; -import { - HubspotTicketInput, - HubspotTicketOutput, - TicketProperties, - commonHubspotProperties, -} from './types'; +import { HubspotTicketInput, HubspotTicketOutput } from './types'; import { UnifiedTicketInput, UnifiedTicketOutput, @@ -18,30 +13,28 @@ export class HubspotTicketMapper implements ITicketMapper { remote_id: string; }[], ): HubspotTicketInput { - const properties: TicketProperties = { ...commonHubspotProperties }; - - properties.subject = source.name; - properties.hs_ticket_priority = source.priority || 'MEDIUM'; // Assuming 'MEDIUM' as a default + const result = { + subject: source.name, + hs_pipeline: source.type, + hubspot_owner_id: '', // TODO Replace 'default' with actual owner ID + hs_pipeline_stage: source.status, + hs_ticket_priority: source.priority || 'MEDIUM', + }; - // Map the custom fields if (customFieldMappings && source.field_mappings) { - source.field_mappings.forEach((fieldMapping) => { - const mapping = customFieldMappings.find((m) => - fieldMapping.hasOwnProperty(m.slug), - ); - if (mapping && fieldMapping[mapping.slug]) { - properties[mapping.remote_id] = fieldMapping[mapping.slug]; + for (const fieldMapping of source.field_mappings) { + for (const key in fieldMapping) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === key, + ); + if (mapping) { + result[mapping.remote_id] = fieldMapping[key]; + } } - }); + } } - return { - subject: source.name, - hs_pipeline: 'default', // Replace 'default' with actual pipeline ID - hubspot_owner_id: '', // TODO Replace 'default' with actual owner ID - hs_pipeline_stage: '', // TODO Replace 'default' with actual pipeline stage - hs_ticket_priority: properties.hs_ticket_priority, - }; + return result; } unify( @@ -73,10 +66,10 @@ export class HubspotTicketMapper implements ITicketMapper { return { id: ticket.id, name: ticket.properties.subject, - status: ticket.properties.hs_pipeline_stage, // Map to your unified status + status: ticket.properties.hs_pipeline_stage, description: ticket.properties.subject, due_date: new Date(ticket.properties.createdate), - type: '', // Define how you determine the type + type: ticket.properties.hs_pipeline, parent_ticket: '', // Define how you determine the parent ticket tags: '', // Define how you map or store tags completed_at: new Date(ticket.properties.hs_lastmodifieddate), diff --git a/packages/api/src/ticketing/ticket/services/hubspot/types.ts b/packages/api/src/ticketing/ticket/services/hubspot/types.ts index bc42e76d8..20585ae86 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/types.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/types.ts @@ -4,6 +4,7 @@ export type HubspotTicketInput = { hubspot_owner_id: string; hs_pipeline_stage: string; hs_ticket_priority: string; + [key: string]: any; }; export type HubspotTicketOutput = { diff --git a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts index e734ce075..ca5b54154 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts @@ -14,9 +14,7 @@ export class ZendeskTicketMapper implements ITicketMapper { }[], ): ZendeskTicketInput { const result: ZendeskTicketInput = { - assignee_email: source.assigned_to?.[0], // Assuming the first assigned_to is the assignee email - created_at: source.completed_at?.toISOString(), - custom_fields: undefined, // Custom field mapping logic needed TODO + assignee_email: source.assigned_to?.[0], //TODO; get the mail of the uuid description: source.description, due_at: source.due_date?.toISOString(), priority: source.priority as 'urgent' | 'high' | 'normal' | 'low', @@ -30,7 +28,6 @@ export class ZendeskTicketMapper implements ITicketMapper { subject: source.name, tags: [source.tags], type: source.type as 'problem' | 'incident' | 'question' | 'task', - updated_at: source.completed_at?.toISOString(), comment: { body: source.comment.body, html_body: source.comment.html_body, @@ -46,8 +43,7 @@ export class ZendeskTicketMapper implements ITicketMapper { (mapping) => mapping.slug === key, ); if (mapping) { - const obj = { id: mapping.remote_id, value: fieldMapping[key] }; //TODO - //result[custom_fields][mapping.remote_id] = fieldMapping[key]; + const obj = { id: mapping.remote_id, value: fieldMapping[key] }; res = [...res, obj]; } } @@ -79,9 +75,15 @@ export class ZendeskTicketMapper implements ITicketMapper { remote_id: string; }[], ): UnifiedTicketOutput { - /*TODO const field_mappings = customFieldMappings.map((mapping) => ({ - [mapping.slug]: ticket.custom_fields[mapping.remote_id], - }));*/ + const field_mappings = customFieldMappings.reduce((acc, mapping) => { + const customField = ticket.custom_fields.find( + (field) => field.id === mapping.remote_id, + ); + if (customField) { + acc.push({ [mapping.slug]: customField.value }); + } + return acc; + }, [] as Record[]); const unifiedTicket: UnifiedTicketOutput = { name: ticket.subject, @@ -94,7 +96,7 @@ export class ZendeskTicketMapper implements ITicketMapper { completed_at: undefined, // If available, add logic to determine the completed date priority: ticket.priority, assigned_to: undefined, // If available, add logic to map assigned users - field_mappings: undefined, // Add logic to map custom fields if available + field_mappings: field_mappings, id: ticket.id.toString(), }; diff --git a/packages/api/src/ticketing/ticket/ticket.controller.ts b/packages/api/src/ticketing/ticket/ticket.controller.ts index fb5e972b5..7fbfd0579 100644 --- a/packages/api/src/ticketing/ticket/ticket.controller.ts +++ b/packages/api/src/ticketing/ticket/ticket.controller.ts @@ -114,13 +114,13 @@ export class TicketController { //@ApiCustomResponse(TicketResponse) @Post() addTicket( - @Body() unfiedContactData: UnifiedTicketInput, + @Body() unfiedTicketData: UnifiedTicketInput, @Headers('integrationId') integrationId: string, @Headers('linkedUserId') linkedUserId: string, @Query('remoteData') remote_data?: boolean, ) { return this.ticketService.addTicket( - unfiedContactData, + unfiedTicketData, integrationId, linkedUserId, remote_data, diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index fdd518de0..a1bd4480f 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -12,9 +12,11 @@ export class UnifiedTicketInput { completed_at?: Date; priority?: string; assigned_to?: string[]; - @ApiPropertyOptional({ type: [{}] }) field_mappings?: Record[]; comment?: UnifiedCommentInput; + attachments?: string[]; + account_id?: string; + contact_id?: string; } export class UnifiedTicketOutput extends UnifiedTicketInput { @ApiProperty() diff --git a/packages/api/src/ticketing/ticketing.module.ts b/packages/api/src/ticketing/ticketing.module.ts index ee4575e4d..5d8d533c3 100644 --- a/packages/api/src/ticketing/ticketing.module.ts +++ b/packages/api/src/ticketing/ticketing.module.ts @@ -4,6 +4,7 @@ import { CommentModule } from './comment/comment.module'; import { UserModule } from './user/user.module'; import { AttachmentModule } from './attachment/attachment.module'; import { ContactModule } from './contact/contact.module'; +import { UserModule } from './user/user.module'; @Module({ imports: [ @@ -15,8 +16,7 @@ import { ContactModule } from './contact/contact.module'; ], providers: [], controllers: [], - exports: [ - TicketModule, + exports: [ TicketModule, CommentModule, UserModule, AttachmentModule, diff --git a/packages/api/src/ticketing/user/services/front/index.ts b/packages/api/src/ticketing/user/services/front/index.ts new file mode 100644 index 000000000..a2b5148df --- /dev/null +++ b/packages/api/src/ticketing/user/services/front/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 { FrontUserOutput } from './types'; + +@Injectable() +export class FrontService implements IUserService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.user.toUpperCase() + ':' + FrontService.name, + ); + this.registry.registerService('front', this); + } + + async syncUsers( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'front', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/teammates', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced front users !`); + + return { + data: resp.data._results, + message: 'Front users retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + TicketingObject.user, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/user/services/front/mappers.ts b/packages/api/src/ticketing/user/services/front/mappers.ts new file mode 100644 index 000000000..9e37057cf --- /dev/null +++ b/packages/api/src/ticketing/user/services/front/mappers.ts @@ -0,0 +1,43 @@ +import { IUserMapper } from '@ticketing/user/types'; +import { + UnifiedUserInput, + UnifiedUserOutput, +} from '@ticketing/user/types/model.unified'; +import { FrontUserInput, FrontUserOutput } from './types'; + +export class FrontUserMapper implements IUserMapper { + desunify( + source: UnifiedUserInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): FrontUserInput { + return; + } + + unify( + source: FrontUserOutput | FrontUserOutput[], + 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((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTicketToUnified( + ticket: FrontUserOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedUserOutput { + return; + } +} diff --git a/packages/api/src/ticketing/user/services/front/types.ts b/packages/api/src/ticketing/user/services/front/types.ts new file mode 100644 index 000000000..6323ca120 --- /dev/null +++ b/packages/api/src/ticketing/user/services/front/types.ts @@ -0,0 +1,5 @@ +export type FrontUserInput = { + id: string; +}; + +export type FrontUserOutput = FrontUserInput; diff --git a/packages/api/src/ticketing/user/services/github/index.ts b/packages/api/src/ticketing/user/services/github/index.ts new file mode 100644 index 000000000..4e995e168 --- /dev/null +++ b/packages/api/src/ticketing/user/services/github/index.ts @@ -0,0 +1,64 @@ +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 { GithubUserOutput } from './types'; + +//TODO +@Injectable() +export class GithubService implements IUserService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.user.toUpperCase() + ':' + GithubService.name, + ); + this.registry.registerService('github', this); + } + + async syncUsers( + linkedUserId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'github', + }, + }); + const resp = await axios.get(`https://api.github.com/users`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced github users !`); + + return { + data: resp.data, + message: 'Github users retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + TicketingObject.user, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/user/services/github/mappers.ts b/packages/api/src/ticketing/user/services/github/mappers.ts new file mode 100644 index 000000000..653d692ea --- /dev/null +++ b/packages/api/src/ticketing/user/services/github/mappers.ts @@ -0,0 +1,28 @@ +import { IUserMapper } from '@ticketing/user/types'; +import { GithubUserInput, GithubUserOutput } from './types'; +import { + UnifiedUserInput, + UnifiedUserOutput, +} from '@ticketing/user/types/model.unified'; + +export class GithubUserMapper implements IUserMapper { + desunify( + source: UnifiedUserInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GithubUserInput { + return; + } + + unify( + source: GithubUserOutput | GithubUserOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedUserOutput | UnifiedUserOutput[] { + return; + } +} diff --git a/packages/api/src/ticketing/user/services/github/types.ts b/packages/api/src/ticketing/user/services/github/types.ts new file mode 100644 index 000000000..1f644a57b --- /dev/null +++ b/packages/api/src/ticketing/user/services/github/types.ts @@ -0,0 +1,6 @@ +export type GithubUserInput = { + name: string; +}; + +//TODO +export type GithubUserOutput = GithubUserInput; diff --git a/packages/api/src/ticketing/user/services/registry.service.ts b/packages/api/src/ticketing/user/services/registry.service.ts index 342dc3645..2959ac489 100644 --- a/packages/api/src/ticketing/user/services/registry.service.ts +++ b/packages/api/src/ticketing/user/services/registry.service.ts @@ -16,7 +16,7 @@ export class ServiceRegistry { getService(integrationId: string): IUserService { const service = this.serviceMap.get(integrationId); if (!service) { - throw new Error(`Service not found for integration ID: ${integrationId}`); + throw new Error(); } return service; } diff --git a/packages/api/src/ticketing/user/services/user.service.ts b/packages/api/src/ticketing/user/services/user.service.ts index c34125d25..0f597aff3 100644 --- a/packages/api/src/ticketing/user/services/user.service.ts +++ b/packages/api/src/ticketing/user/services/user.service.ts @@ -10,8 +10,9 @@ import { UserResponse } from '../types'; import { desunify } from '@@core/utils/unification/desunify'; import { TicketingObject } from '@ticketing/@utils/@types'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { unify } from '@@core/utils/unification/unify'; import { ServiceRegistry } from './registry.service'; +import { OriginalUserOutput } from '@@core/utils/types/original/original.ticketing'; +import { unify } from '@@core/utils/unification/unify'; @Injectable() export class UserService { @@ -25,5 +26,18 @@ export class UserService { this.logger.setContext(UserService.name); } - // Additional methods and logic + async getUser( + id_ticketing_user: string, + remote_data?: boolean, + ): Promise> { + return; + } + + async getUsers( + integrationId: string, + linkedUserId: string, + remote_data?: boolean, + ): Promise> { + return; + } } diff --git a/packages/api/src/ticketing/user/services/zendesk/index.ts b/packages/api/src/ticketing/user/services/zendesk/index.ts index fa222dadd..532981e03 100644 --- a/packages/api/src/ticketing/user/services/zendesk/index.ts +++ b/packages/api/src/ticketing/user/services/zendesk/index.ts @@ -1,13 +1,14 @@ -import { EncryptionService } from '@@core/encryption/encryption.service'; -import { EnvironmentService } from '@@core/environment/environment.service'; +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, ZendeskUserOutput } from '@ticketing/@utils/@types'; import { ApiResponse } from '@@core/utils/types'; -import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { Injectable } from '@nestjs/common'; -import { TicketingObject, ZendeskUserInput } from '@ticketing/@utils/@types'; -import { IUserService } from '@ticketing/user/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { EnvironmentService } from '@@core/environment/environment.service'; import { ServiceRegistry } from '../registry.service'; +import { IUserService } from '@ticketing/user/types'; @Injectable() export class ZendeskService implements IUserService { @@ -23,16 +24,45 @@ export class ZendeskService implements IUserService { ); this.registry.registerService('zendesk_t', this); } - addUser( - userData: DesunifyReturnType, - linkedUserId: string, - ): Promise> { - throw new Error('Method not implemented.'); - } - syncUsers( + + async syncUsers( linkedUserId: string, custom_properties?: string[], - ): Promise> { - throw new Error('Method not implemented.'); + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'zendesk_t', + }, + }); + + const resp = await axios.get( + `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/users`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced zendesk users !`); + + return { + data: resp.data.users, + message: 'Zendesk users retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zendesk', + TicketingObject.user, + ActionType.GET, + ); + } } } diff --git a/packages/api/src/ticketing/user/services/zendesk/mappers.ts b/packages/api/src/ticketing/user/services/zendesk/mappers.ts index e69de29bb..223be54ef 100644 --- a/packages/api/src/ticketing/user/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/user/services/zendesk/mappers.ts @@ -0,0 +1,43 @@ +import { IUserMapper } from '@ticketing/user/types'; +import { ZendeskUserInput, ZendeskUserOutput } from './types'; +import { + UnifiedUserInput, + UnifiedUserOutput, +} from '@ticketing/user/types/model.unified'; + +export class ZendeskUserMapper implements IUserMapper { + desunify( + source: UnifiedUserInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): ZendeskUserInput { + return; + } + + unify( + source: ZendeskUserOutput | ZendeskUserOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedUserOutput | UnifiedUserOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleUserToUnified(source, customFieldMappings); + } + return source.map((ticket) => + this.mapSingleUserToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleUserToUnified( + ticket: ZendeskUserOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedUserOutput { + return; + } +} diff --git a/packages/api/src/ticketing/user/services/zendesk/types.ts b/packages/api/src/ticketing/user/services/zendesk/types.ts index fa5442b53..15356640c 100644 --- a/packages/api/src/ticketing/user/services/zendesk/types.ts +++ b/packages/api/src/ticketing/user/services/zendesk/types.ts @@ -1,5 +1,7 @@ export type ZendeskUserInput = { - id: string; + _: string; }; -export type ZendeskUserOutput = ZendeskUserInput; +export type ZendeskUserOutput = ZendeskUserInput & { + id: number; // Read-only. Automatically assigned when the ticket is created. +}; diff --git a/packages/api/src/ticketing/user/sync/sync.service.ts b/packages/api/src/ticketing/user/sync/sync.service.ts index e61d3d900..0a12a2b3e 100644 --- a/packages/api/src/ticketing/user/sync/sync.service.ts +++ b/packages/api/src/ticketing/user/sync/sync.service.ts @@ -3,15 +3,15 @@ import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse, TICKETING_PROVIDERS } from '@@core/utils/types'; +import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { ServiceRegistry } from '../services/registry.service'; import { unify } from '@@core/utils/unification/unify'; import { TicketingObject } from '@ticketing/@utils/@types'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedUserOutput } from '../types/model.unified'; import { IUserService } from '../types'; -import { ServiceRegistry } from '../services/registry.service'; @Injectable() export class SyncService implements OnModuleInit { diff --git a/packages/api/src/ticketing/user/types/index.ts b/packages/api/src/ticketing/user/types/index.ts index 19cb5adc3..dff14f804 100644 --- a/packages/api/src/ticketing/user/types/index.ts +++ b/packages/api/src/ticketing/user/types/index.ts @@ -1,15 +1,10 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; import { UnifiedUserInput, UnifiedUserOutput } from './model.unified'; +import { OriginalUserOutput } from '@@core/utils/types/original/original.ticketing'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiResponse } from '@@core/utils/types'; -import { OriginalUserOutput } from '@@core/utils/types/original/original.ticketing'; export interface IUserService { - addUser( - userData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - syncUsers( linkedUserId: string, custom_properties?: string[], diff --git a/packages/api/src/ticketing/user/types/mappingsTypes.ts b/packages/api/src/ticketing/user/types/mappingsTypes.ts index d08c20aaa..9a7bf0f37 100644 --- a/packages/api/src/ticketing/user/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/user/types/mappingsTypes.ts @@ -1 +1,22 @@ -// Content for mappingsTypes.ts +import { FrontUserMapper } from '../services/front/mappers'; +import { GithubUserMapper } from '../services/github/mappers'; +import { ZendeskUserMapper } from '../services/zendesk/mappers'; + +const zendeskUserMapper = new ZendeskUserMapper(); +const frontUserMapper = new FrontUserMapper(); +const githubUserMapper = new GithubUserMapper(); + +export const userUnificationMapping = { + zendesk: { + unify: zendeskUserMapper.unify, + desunify: zendeskUserMapper.desunify, + }, + front: { + unify: frontUserMapper.unify, + desunify: frontUserMapper.desunify, + }, + github: { + unify: githubUserMapper.unify, + desunify: githubUserMapper.desunify, + }, +}; diff --git a/packages/api/src/ticketing/user/types/model.unified.ts b/packages/api/src/ticketing/user/types/model.unified.ts index fe03f0afd..d97644198 100644 --- a/packages/api/src/ticketing/user/types/model.unified.ts +++ b/packages/api/src/ticketing/user/types/model.unified.ts @@ -1,3 +1,8 @@ -export class UnifiedUserInput {} +export class UnifiedUserInput { + name: string; + email_address: string; +} -export class UnifiedUserOutput extends UnifiedUserInput {} +export class UnifiedUserOutput extends UnifiedUserInput { + id: string; +} diff --git a/packages/api/src/ticketing/user/user.controller.ts b/packages/api/src/ticketing/user/user.controller.ts index 46f3ff6eb..fb47cd2ca 100644 --- a/packages/api/src/ticketing/user/user.controller.ts +++ b/packages/api/src/ticketing/user/user.controller.ts @@ -1,4 +1,69 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Query, Get, Param, Headers } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, + ApiHeader, +} from '@nestjs/swagger'; +import { ApiCustomResponse } from '@@core/utils/types'; +import { UserService } from './services/user.service'; +@ApiTags('ticketing/user') @Controller('ticketing/user') -export class UserController {} +export class UserController { + constructor( + private readonly userService: UserService, + private logger: LoggerService, + ) { + this.logger.setContext(UserController.name); + } + + @ApiOperation({ + operationId: 'getUsers', + summary: 'List a batch of Users', + }) + @ApiHeader({ name: 'integrationId', required: true }) + @ApiHeader({ name: 'linkedUserId', required: true }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(UserResponse) + @Get() + getUsers( + @Headers('integrationId') integrationId: string, + @Headers('linkedUserId') linkedUserId: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.userService.getUsers(integrationId, linkedUserId, remote_data); + } + + @ApiOperation({ + operationId: 'getUser', + summary: 'Retrieve a User', + description: 'Retrieve a user from any connected Ticketing software', + }) + @ApiParam({ + name: 'id', + required: true, + type: String, + description: 'id of the user you want to retrieve.', + }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(UserResponse) + @Get(':id') + getUser(@Param('id') id: string, @Query('remoteData') remote_data?: boolean) { + return this.userService.getUser(id, remote_data); + } +} diff --git a/packages/api/src/ticketing/user/user.module.ts b/packages/api/src/ticketing/user/user.module.ts index 9b1fd29ed..833733636 100644 --- a/packages/api/src/ticketing/user/user.module.ts +++ b/packages/api/src/ticketing/user/user.module.ts @@ -1,15 +1,17 @@ import { Module } from '@nestjs/common'; import { UserController } from './user.controller'; -import { BullModule } from '@nestjs/bull'; +import { SyncService } from './sync/sync.service'; +import { LoggerService } from '@@core/logger/logger.service'; import { UserService } from './services/user.service'; import { ServiceRegistry } from './services/registry.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { WebhookService } from '@@core/webhook/webhook.service'; import { EncryptionService } from '@@core/encryption/encryption.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { PrismaService } from '@@core/prisma/prisma.service'; +import { WebhookService } from '@@core/webhook/webhook.service'; +import { BullModule } from '@nestjs/bull'; import { ZendeskService } from './services/zendesk'; -import { LoggerService } from '@@core/logger/logger.service'; -import { SyncService } from './sync/sync.service'; +import { FrontService } from './services/front'; +import { GithubService } from './services/github'; @Module({ imports: [ @@ -29,6 +31,8 @@ import { SyncService } from './sync/sync.service'; ServiceRegistry, /* PROVIDERS SERVICES */ ZendeskService, + FrontService, + GithubService, ], exports: [SyncService], }) diff --git a/packages/api/src/ticketing/user/utils/index.ts b/packages/api/src/ticketing/user/utils/index.ts index e69de29bb..f849788c1 100644 --- a/packages/api/src/ticketing/user/utils/index.ts +++ b/packages/api/src/ticketing/user/utils/index.ts @@ -0,0 +1 @@ +/* PUT ALL UTILS FUNCTIONS USED IN YOUR OBJECT METHODS HERE */ From 0250d812618ab5b7e3a4615fd61d1f1ed46f169f Mon Sep 17 00:00:00 2001 From: nael Date: Thu, 4 Jan 2024 18:44:06 +0100 Subject: [PATCH 07/25] :construction: Added boilerplate for account and user --- packages/api/prisma/schema.prisma | 50 ++- .../types/original/original.ticketing.ts | 40 +++ .../api/src/ticketing/@utils/@types/index.ts | 38 ++- .../ticketing/account/account.controller.ts | 76 +++++ .../src/ticketing/account/account.module.ts | 35 ++ .../account/services/account.service.ts | 193 +++++++++++ .../ticketing/account/services/front/index.ts | 63 ++++ .../account/services/front/mappers.ts | 43 +++ .../ticketing/account/services/front/types.ts | 5 + .../account/services/github/index.ts | 64 ++++ .../account/services/github/mappers.ts | 28 ++ .../account/services/github/types.ts | 6 + .../account/services/registry.service.ts | 23 ++ .../account/services/zendesk/index.ts | 71 ++++ .../account/services/zendesk/mappers.ts | 43 +++ .../account/services/zendesk/types.ts | 7 + .../ticketing/account/sync/sync.service.ts | 305 ++++++++++++++++++ .../api/src/ticketing/account/types/index.ts | 38 +++ .../ticketing/account/types/mappingsTypes.ts | 22 ++ .../ticketing/account/types/model.unified.ts | 9 + .../api/src/ticketing/account/utils/index.ts | 1 + .../comment/services/comment.service.ts | 4 +- .../comment/services/front/mappers.ts | 2 +- .../comment/services/zendesk/mappers.ts | 2 +- .../ticketing/comment/sync/sync.service.ts | 4 +- .../ticketing/comment/types/model.unified.ts | 2 +- .../ticket/services/front/mappers.ts | 2 +- .../ticket/services/ticket.service.ts | 2 +- .../api/src/ticketing/ticketing.module.ts | 3 +- .../ticketing/user/services/user.service.ts | 184 ++++++++++- .../src/ticketing/user/sync/sync.service.ts | 284 +++++++++++++++- .../src/ticketing/user/types/model.unified.ts | 2 + 32 files changed, 1615 insertions(+), 36 deletions(-) create mode 100644 packages/api/src/ticketing/account/account.controller.ts create mode 100644 packages/api/src/ticketing/account/account.module.ts create mode 100644 packages/api/src/ticketing/account/services/account.service.ts create mode 100644 packages/api/src/ticketing/account/services/front/index.ts create mode 100644 packages/api/src/ticketing/account/services/front/mappers.ts create mode 100644 packages/api/src/ticketing/account/services/front/types.ts create mode 100644 packages/api/src/ticketing/account/services/github/index.ts create mode 100644 packages/api/src/ticketing/account/services/github/mappers.ts create mode 100644 packages/api/src/ticketing/account/services/github/types.ts create mode 100644 packages/api/src/ticketing/account/services/registry.service.ts create mode 100644 packages/api/src/ticketing/account/services/zendesk/index.ts create mode 100644 packages/api/src/ticketing/account/services/zendesk/mappers.ts create mode 100644 packages/api/src/ticketing/account/services/zendesk/types.ts create mode 100644 packages/api/src/ticketing/account/sync/sync.service.ts create mode 100644 packages/api/src/ticketing/account/types/index.ts create mode 100644 packages/api/src/ticketing/account/types/mappingsTypes.ts create mode 100644 packages/api/src/ticketing/account/types/model.unified.ts create mode 100644 packages/api/src/ticketing/account/utils/index.ts diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 579d94ada..2b4c55f5e 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -385,9 +385,9 @@ model tcg_comments { is_private Boolean? created_at DateTime? @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) - author_type String? @db.Uuid remote_id String? remote_platform String? + creator_type String? id_tcg_ticket String? @db.Uuid id_tcg_contact String? @db.Uuid id_tcg_user String? @db.Uuid @@ -414,10 +414,12 @@ model tcg_contacts { remote_id String? remote_platform String? id_event String? @db.Uuid + id_tcg_account String? @db.Uuid tcg_comments tcg_comments[] events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_43") @@index([id_event], map: "fk_tcg_contact_event_id") + @@index([id_tcg_account], map: "fk_tcg_contact_tcg_account_id") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments @@ -438,10 +440,13 @@ model tcg_tickets { remote_id String? remote_platform String? id_event String? @db.Uuid + creator_type String? + id_tcg_user String? @db.Uuid tcg_comments tcg_comments[] events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_44") @@index([id_event], map: "fk_tcg_tickets_eventid") + @@index([id_tcg_user], map: "fk_tcg_ticket_tcg_user") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments @@ -449,11 +454,12 @@ model tcg_users { id_tcg_user String @id(map: "pk_tcg_users") @db.Uuid name String? email_address String? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) remote_id String? remote_platform String? id_event String? @db.Uuid + teams String[] + created_at DateTime? @db.Timestamp(6) + modified_at DateTime? @db.Timestamp(6) tcg_comments tcg_comments[] events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_45") @@ -538,3 +544,41 @@ model webhooks_reponses { http_status_code String webhook_delivery_attempts webhook_delivery_attempts[] } + +/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model tcg_accounts { + id_tcg_account String @db.Uuid + created_at DateTime? @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) + remote_id String? + name String? + domains String[] + + @@ignore +} + +/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. +model tcg_tags { + id_tcg_tag String @db.Uuid + remote_id String? + name String? + created_at DateTime? @db.Timestamp(6) + modified_at DateTime? @db.Timestamp(6) + id_tcg_ticket String? @db.Uuid + + @@index([id_tcg_ticket], map: "fk_tcg_tag_tcg_ticketid") + @@ignore +} + +/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. +model tcg_teams { + id_tcg_team String @db.Uuid + remote_id String? + name String? + description String? + created_at DateTime? @db.Timestamp(6) + modified_at DateTime? @db.Timestamp(6) + + @@ignore +} 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 46f95f596..3d0f5fd07 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -61,6 +61,24 @@ export type OriginalUserInput = | ZendeskUserInput | GithubUserInput | FrontUserInput; +/* account */ +export type OriginalAccountInput = + | ZendeskAccountInput + | GithubAccountInput + | FrontAccountInput; +/* contact */ +export type OriginalContactInput = + | ZendeskContactInput + | GithubContactInput + | FrontContactInput; + +/* tag */ +export type OriginalTagInput = ZendeskTagInput | GithubTagInput | FrontTagInput; +/* team */ +export type OriginalTeamInput = + | ZendeskTeamInput + | GithubTeamInput + | FrontTeamInput; /* attachment */ export type OriginalAttachmentInput = ZendeskAttachmentInput; @@ -91,6 +109,28 @@ export type OriginalUserOutput = | GithubUserOutput | FrontUserOutput; +/* account */ +export type OriginalAccountOutput = + | ZendeskAccountOutput + | GithubAccountOutput + | FrontAccountOutput; +/* contact */ +export type OriginalContactOutput = + | ZendeskContactOutput + | GithubContactOutput + | FrontContactOutput; + +/* tag */ +export type OriginalTagOutput = + | ZendeskTagOutput + | GithubTagOutput + | FrontTagOutput; +/* team */ +export type OriginalTeamOutput = + | ZendeskTeamOutput + | GithubTeamOutput + | FrontTeamOutput; + /* attachment */ export type OriginalAttachmentOutput = ZendeskAttachmentOutput; diff --git a/packages/api/src/ticketing/@utils/@types/index.ts b/packages/api/src/ticketing/@utils/@types/index.ts index 09c40be41..d9f8f46b5 100644 --- a/packages/api/src/ticketing/@utils/@types/index.ts +++ b/packages/api/src/ticketing/@utils/@types/index.ts @@ -1,3 +1,5 @@ +import { IAccountService } from '@ticketing/account/types'; +import { UnifiedAccountInput, UnifiedAccountOutput } from '@ticketing/account/types/model.unified'; import { IAttachmentService } from '@ticketing/attachment/types'; import { ICommentService } from '@ticketing/comment/types'; import { commentUnificationMapping } from '@ticketing/comment/types/mappingsTypes'; @@ -13,6 +15,11 @@ import { UnifiedTicketOutput, } from '@ticketing/ticket/types/model.unified'; import { IUserService } from '@ticketing/user/types'; +import { userUnificationMapping } from '@ticketing/user/types/mappingsTypes'; +import { + UnifiedUserInput, + UnifiedUserOutput, +} from '@ticketing/user/types/model.unified'; export enum TicketingObject { ticket = 'ticket', @@ -20,17 +27,38 @@ export enum TicketingObject { user = 'user', attachment = 'attachement', contact = 'contact', + account = 'account', + tag = 'tag', + team = 'team', } export type UnifiedTicketing = | UnifiedTicketInput | UnifiedTicketOutput | UnifiedCommentInput - | UnifiedCommentOutput; + | UnifiedCommentOutput + | UnifiedUserInput + | UnifiedUserOutput + | UnifiedAccountInput + | UnifiedAccountOutput; + | UnifiedContactInput + | UnifiedContactOutput; + | UnifiedTeamInput + | UnifiedTeamOutput + | UnifiedTagInput + | UnifiedTagOutput + | UnifiedAttachmentInput + | UnifiedAttachmentOutput; export const unificationMapping = { [TicketingObject.ticket]: ticketUnificationMapping, [TicketingObject.comment]: commentUnificationMapping, + [TicketingObject.user]: userUnificationMapping, + [TicketingObject.account]: accountUnificationMapping, + [TicketingObject.contact]: contactUnificationMapping, + [TicketingObject.team]: teamUnificationMapping, + [TicketingObject.tag]: tagUnificationMapping, + [TicketingObject.attachment]: attachmentUnificationMapping, }; export type ITicketingService = @@ -38,10 +66,16 @@ export type ITicketingService = | ICommentService | IUserService | IAttachmentService - | IContactService; + | IContactService + | ITeamService + | ITagService; +; export * from '../../ticket/services/zendesk/types'; export * from '../../comment/services/zendesk/types'; export * from '../../user/services/zendesk/types'; export * from '../../contact/services/zendesk/types'; export * from '../../attachment/services/zendesk/types'; +export * from '../../account/services/zendesk/types'; +export * from '../../team/services/zendesk/types'; +export * from '../../tag/services/zendesk/types'; diff --git a/packages/api/src/ticketing/account/account.controller.ts b/packages/api/src/ticketing/account/account.controller.ts new file mode 100644 index 000000000..ec414eac3 --- /dev/null +++ b/packages/api/src/ticketing/account/account.controller.ts @@ -0,0 +1,76 @@ +import { Controller, Query, Get, Param, Headers } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, + ApiHeader, +} from '@nestjs/swagger'; +import { ApiCustomResponse } from '@@core/utils/types'; +import { AccountService } from './services/account.service'; + +@ApiTags('ticketing/account') +@Controller('ticketing/account') +export class AccountController { + constructor( + private readonly accountService: AccountService, + private logger: LoggerService, + ) { + this.logger.setContext(AccountController.name); + } + + @ApiOperation({ + operationId: 'getAccounts', + summary: 'List a batch of Accounts', + }) + @ApiHeader({ name: 'integrationId', required: true }) + @ApiHeader({ name: 'linkedUserId', required: true }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(AccountResponse) + @Get() + getAccounts( + @Headers('integrationId') integrationId: string, + @Headers('linkedUserId') linkedUserId: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.accountService.getAccounts( + integrationId, + linkedUserId, + remote_data, + ); + } + + @ApiOperation({ + operationId: 'getAccount', + summary: 'Retrieve an Account', + description: 'Retrieve an account from any connected Ticketing software', + }) + @ApiParam({ + name: 'id', + required: true, + type: String, + description: 'id of the account you want to retrieve.', + }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(AccountResponse) + @Get(':id') + getAccount( + @Param('id') id: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.accountService.getAccount(id, remote_data); + } +} diff --git a/packages/api/src/ticketing/account/account.module.ts b/packages/api/src/ticketing/account/account.module.ts new file mode 100644 index 000000000..48f0d779b --- /dev/null +++ b/packages/api/src/ticketing/account/account.module.ts @@ -0,0 +1,35 @@ +import { Module } from '@nestjs/common'; +import { AccountController } from './account.controller'; +import { SyncService } from './sync/sync.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { AccountService } from './services/account.service'; +import { ServiceRegistry } from './services/registry.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { WebhookService } from '@@core/webhook/webhook.service'; +import { BullModule } from '@nestjs/bull'; + +@Module({ + imports: [ + BullModule.registerQueue({ + name: 'webhookDelivery', + }), + ], + controllers: [AccountController], + providers: [ + AccountService, + PrismaService, + LoggerService, + SyncService, + WebhookService, + EncryptionService, + FieldMappingService, + ServiceRegistry, + /* PROVIDERS SERVICES */ + + ], + exports: [SyncService], +}) +export class AccountModule {} + diff --git a/packages/api/src/ticketing/account/services/account.service.ts b/packages/api/src/ticketing/account/services/account.service.ts new file mode 100644 index 000000000..4b60f5f06 --- /dev/null +++ b/packages/api/src/ticketing/account/services/account.service.ts @@ -0,0 +1,193 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { ApiResponse } from '@@core/utils/types'; +import { handleServiceError } from '@@core/utils/errors'; +import { UnifiedAccountOutput } from '../types/model.unified'; +import { AccountResponse } from '../types'; + +@Injectable() +export class AccountService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(AccountService.name); + } + + async getAccount( + id_ticketing_account: string, + remote_data?: boolean, + ): Promise> { + try { + const account = await this.prisma.tcg_accounts.findUnique({ + where: { + id_tcg_account: id_ticketing_account, + }, + }); + + // Fetch field mappings for the ticket + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: account.id_tcg_account, + }, + }, + include: { + attribute: true, + }, + }); + + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from(fieldMappingsMap, ([key, value]) => ({ + [key]: value, + })); + + // Transform to UnifiedAccountOutput format + const unifiedAccount: UnifiedAccountOutput = { + id: account.id_tcg_account, + email_address: account.email_address, + name: account.name, + teams: account.teams, + field_mappings: field_mappings, + }; + + let res: AccountResponse = { + accounts: [unifiedAccount], + }; + + if (remote_data) { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: account.id_tcg_account, + }, + }); + const remote_data = JSON.parse(resp.data); + + res = { + ...res, + remote_data: [remote_data], + }; + } + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async getAccounts( + integrationId: string, + linkedAccountId: string, + remote_data?: boolean, + ): Promise> { + try { + //TODO: handle case where data is not there (not synced) or old synced + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.account.pull', + method: 'GET', + url: '/ticketing/account', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedAccountId, + }, + }); + const job_id = job_resp_create.id_event; + const accounts = await this.prisma.tcg_accounts.findMany({ + where: { + remote_id: integrationId.toLowerCase(), + events: { + id_linked_user: linkedAccountId, + }, + }, + }); + + const unifiedAccounts: UnifiedAccountOutput[] = await Promise.all( + accounts.map(async (account) => { + // Fetch field mappings for the account + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: account.id_tcg_account, + }, + }, + include: { + attribute: true, + }, + }); + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from( + fieldMappingsMap, + ([key, value]) => ({ [key]: value }), + ); + + // Transform to UnifiedAccountOutput format + return { + id: account.id_tcg_account, + email_address: account.email_address, + name: account.name, + teams: account.teams, + field_mappings: field_mappings, + }; + }), + ); + + let res: AccountResponse = { + accounts: unifiedAccounts, + }; + + if (remote_data) { + const remote_array_data: Record[] = await Promise.all( + accounts.map(async (account) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: account.id_tcg_account, + }, + }); + const remote_data = JSON.parse(resp.data); + return remote_data; + }), + ); + + res = { + ...res, + remote_data: remote_array_data, + }; + } + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } +} diff --git a/packages/api/src/ticketing/account/services/front/index.ts b/packages/api/src/ticketing/account/services/front/index.ts new file mode 100644 index 000000000..279a64e66 --- /dev/null +++ b/packages/api/src/ticketing/account/services/front/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 { IAccountService } from '@ticketing/account/types'; +import { FrontAccountOutput } from './types'; + +@Injectable() +export class FrontService implements IAccountService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.account.toUpperCase() + ':' + FrontService.name, + ); + this.registry.registerService('front', this); + } + + async syncAccounts( + linkedAccountId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedAccountId, + provider_slug: 'front', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/teammates', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced front accounts !`); + + return { + data: resp.data._results, + message: 'Front accounts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + TicketingObject.account, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/account/services/front/mappers.ts b/packages/api/src/ticketing/account/services/front/mappers.ts new file mode 100644 index 000000000..60eacfc43 --- /dev/null +++ b/packages/api/src/ticketing/account/services/front/mappers.ts @@ -0,0 +1,43 @@ +import { IAccountMapper } from '@ticketing/account/types'; +import { FrontAccountInput, FrontAccountOutput } from './types'; +import { + UnifiedAccountInput, + UnifiedAccountOutput, +} from '@ticketing/account/types/model.unified'; + +export class FrontAccountMapper implements IAccountMapper { + desunify( + source: UnifiedAccountInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): FrontAccountInput { + return; + } + + unify( + source: FrontAccountOutput | FrontAccountOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAccountOutput | UnifiedAccountOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTicketToUnified( + ticket: FrontAccountOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAccountOutput { + return; + } +} diff --git a/packages/api/src/ticketing/account/services/front/types.ts b/packages/api/src/ticketing/account/services/front/types.ts new file mode 100644 index 000000000..ab8889055 --- /dev/null +++ b/packages/api/src/ticketing/account/services/front/types.ts @@ -0,0 +1,5 @@ +export type FrontAccountInput = { + id: string; +}; + +export type FrontAccountOutput = FrontAccountInput; diff --git a/packages/api/src/ticketing/account/services/github/index.ts b/packages/api/src/ticketing/account/services/github/index.ts new file mode 100644 index 000000000..eb138c149 --- /dev/null +++ b/packages/api/src/ticketing/account/services/github/index.ts @@ -0,0 +1,64 @@ +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 { IAccountService } from '@ticketing/account/types'; +import { GithubAccountOutput } from './types'; + +//TODO +@Injectable() +export class GithubService implements IAccountService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.account.toUpperCase() + ':' + GithubService.name, + ); + this.registry.registerService('github', this); + } + + async syncAccounts( + linkedAccountId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedAccountId, + provider_slug: 'github', + }, + }); + const resp = await axios.get(`https://api.github.com/accounts`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced github accounts !`); + + return { + data: resp.data, + message: 'Github accounts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + TicketingObject.account, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/account/services/github/mappers.ts b/packages/api/src/ticketing/account/services/github/mappers.ts new file mode 100644 index 000000000..c666bb63f --- /dev/null +++ b/packages/api/src/ticketing/account/services/github/mappers.ts @@ -0,0 +1,28 @@ +import { IAccountMapper } from '@ticketing/account/types'; +import { GithubAccountInput, GithubAccountOutput } from './types'; +import { + UnifiedAccountInput, + UnifiedAccountOutput, +} from '@ticketing/account/types/model.unified'; + +export class GithubAccountMapper implements IAccountMapper { + desunify( + source: UnifiedAccountInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GithubAccountInput { + return; + } + + unify( + source: GithubAccountOutput | GithubAccountOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAccountOutput | UnifiedAccountOutput[] { + return; + } +} diff --git a/packages/api/src/ticketing/account/services/github/types.ts b/packages/api/src/ticketing/account/services/github/types.ts new file mode 100644 index 000000000..7ad3a458d --- /dev/null +++ b/packages/api/src/ticketing/account/services/github/types.ts @@ -0,0 +1,6 @@ +export type GithubAccountInput = { + name: string; +}; + +//TODO +export type GithubAccountOutput = GithubAccountInput; diff --git a/packages/api/src/ticketing/account/services/registry.service.ts b/packages/api/src/ticketing/account/services/registry.service.ts new file mode 100644 index 000000000..c7c0928fa --- /dev/null +++ b/packages/api/src/ticketing/account/services/registry.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { IAccountService } from '../types'; + +@Injectable() +export class ServiceRegistry { + private serviceMap: Map; + + constructor() { + this.serviceMap = new Map(); + } + + registerService(serviceKey: string, service: IAccountService) { + this.serviceMap.set(serviceKey, service); + } + + getService(integrationId: string): IAccountService { + const service = this.serviceMap.get(integrationId); + if (!service) { + throw new Error(); + } + return service; + } +} diff --git a/packages/api/src/ticketing/account/services/zendesk/index.ts b/packages/api/src/ticketing/account/services/zendesk/index.ts new file mode 100644 index 000000000..e968dd06a --- /dev/null +++ b/packages/api/src/ticketing/account/services/zendesk/index.ts @@ -0,0 +1,71 @@ +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, + ZendeskAccountOutput, +} from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { ServiceRegistry } from '../registry.service'; +import { IAccountService } from '@ticketing/account/types'; + +@Injectable() +export class ZendeskService implements IAccountService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.account.toUpperCase() + ':' + ZendeskService.name, + ); + this.registry.registerService('zendesk_t', this); + } + + async syncAccounts( + linkedAccountId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedAccountId, + provider_slug: 'zendesk_t', + }, + }); + + const resp = await axios.get( + `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/accounts`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced zendesk accounts !`); + + return { + data: resp.data.accounts, + message: 'Zendesk accounts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zendesk', + TicketingObject.account, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/account/services/zendesk/mappers.ts b/packages/api/src/ticketing/account/services/zendesk/mappers.ts new file mode 100644 index 000000000..47a524a92 --- /dev/null +++ b/packages/api/src/ticketing/account/services/zendesk/mappers.ts @@ -0,0 +1,43 @@ +import { IAccountMapper } from '@ticketing/account/types'; +import { ZendeskAccountInput, ZendeskAccountOutput } from './types'; +import { + UnifiedAccountInput, + UnifiedAccountOutput, +} from '@ticketing/account/types/model.unified'; + +export class ZendeskAccountMapper implements IAccountMapper { + desunify( + source: UnifiedAccountInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): ZendeskAccountInput { + return; + } + + unify( + source: ZendeskAccountOutput | ZendeskAccountOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAccountOutput | UnifiedAccountOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleAccountToUnified(source, customFieldMappings); + } + return source.map((ticket) => + this.mapSingleAccountToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleAccountToUnified( + ticket: ZendeskAccountOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAccountOutput { + return; + } +} diff --git a/packages/api/src/ticketing/account/services/zendesk/types.ts b/packages/api/src/ticketing/account/services/zendesk/types.ts new file mode 100644 index 000000000..928814ea2 --- /dev/null +++ b/packages/api/src/ticketing/account/services/zendesk/types.ts @@ -0,0 +1,7 @@ +export type ZendeskAccountInput = { + _: string; +}; + +export type ZendeskAccountOutput = ZendeskAccountInput & { + id: number; // Read-only. Automatically assigned when the ticket is created. +}; diff --git a/packages/api/src/ticketing/account/sync/sync.service.ts b/packages/api/src/ticketing/account/sync/sync.service.ts new file mode 100644 index 000000000..747e15b5b --- /dev/null +++ b/packages/api/src/ticketing/account/sync/sync.service.ts @@ -0,0 +1,305 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; +import { Cron } from '@nestjs/schedule'; +import { ApiResponse, TICKETING_PROVIDERS } from '@@core/utils/types'; +import { v4 as uuidv4 } from 'uuid'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { ServiceRegistry } from '../services/registry.service'; +import { unify } from '@@core/utils/unification/unify'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { WebhookService } from '@@core/webhook/webhook.service'; +import { UnifiedAccountOutput } from '../types/model.unified'; +import { IAccountService } from '../types'; +import { OriginalAccountOutput } from '@@core/utils/types/original/original.ticketing'; +import { tcg_accounts as TicketingAccount } from '@prisma/client'; + +@Injectable() +export class SyncService implements OnModuleInit { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private webhook: WebhookService, + private fieldMappingService: FieldMappingService, + private serviceRegistry: ServiceRegistry, + ) { + this.logger.setContext(SyncService.name); + } + + async onModuleInit() { + try { + await this.syncAccounts(); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + @Cron('*/20 * * * *') + //function used by sync worker which populate our tcg_accounts table + //its role is to fetch all accounts from providers 3rd parties and save the info inside our db + async syncAccounts() { + try { + this.logger.log(`Syncing accounts....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedAccounts = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedAccounts.map(async (linkedAccount) => { + try { + const providers = TICKETING_PROVIDERS; + for (const provider of providers) { + try { + await this.syncAccountsForLinkedAccount( + provider, + linkedAccount.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncAccountsForLinkedAccount( + integrationId: string, + linkedAccountId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} accounts for linkedAccount ${linkedAccountId}`, + ); + // check if linkedAccount has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedAccountId, + provider_slug: integrationId, + }, + }); + if (!connection) return; + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.account.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedAccountId, + }, + }); + const job_id = job_resp_create.id_event; + + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedAccountId, + 'account', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: IAccountService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = + await service.syncAccounts(linkedAccountId, remoteProperties); + + const sourceObject: OriginalAccountOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: TicketingObject.account, + providerName: integrationId, + customFieldMappings, + })) as UnifiedAccountOutput[]; + + //TODO + const accountIds = sourceObject.map((account) => + 'id' in account ? String(account.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const account_data = await this.saveAccountsInDb( + linkedAccountId, + unifiedObject, + accountIds, + integrationId, + job_id, + sourceObject, + ); + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + await this.webhook.handleWebhook( + account_data, + 'ticketing.account.pulled', + id_project, + job_id, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveAccountsInDb( + linkedAccountId: string, + accounts: UnifiedAccountOutput[], + originIds: string[], + originSource: string, + jobId: string, + remote_data: Record[], + ): Promise { + try { + let accounts_results: TicketingAccount[] = []; + for (let i = 0; i < accounts.length; i++) { + const account = accounts[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingAccount = await this.prisma.tcg_accounts.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + events: { + id_linked_account: linkedAccountId, + }, + }, + }); + + let unique_ticketing_account_id: string; + + if (existingAccount) { + // Update the existing ticket + const res = await this.prisma.tcg_accounts.update({ + where: { + id_tcg_account: existingAccount.id_tcg_account, + }, + data: { + name: existingAccount.name, + domains: existingAccount.domains, + modified_at: new Date(), + }, + }); + unique_ticketing_account_id = res.id_tcg_account; + accounts_results = [...accounts_results, res]; + } else { + // Create a new account + this.logger.log('not existing account ' + account.name); + const data = { + id_tcg_account: uuidv4(), + name: account.name, + domains: account.domains, + created_at: new Date(), + modified_at: new Date(), + id_event: jobId, + remote_id: originId, + remote_platform: originSource, + }; + const res = await this.prisma.tcg_accounts.create({ + data: data, + }); + accounts_results = [...accounts_results, res]; + unique_ticketing_account_id = res.id_tcg_account; + } + + // check duplicate or existing values + if (account.field_mappings && account.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_ticketing_account_id, + }, + }); + + for (const mapping of account.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedAccountId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_account_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_account_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return accounts_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } +} diff --git a/packages/api/src/ticketing/account/types/index.ts b/packages/api/src/ticketing/account/types/index.ts new file mode 100644 index 000000000..8a1b4fbde --- /dev/null +++ b/packages/api/src/ticketing/account/types/index.ts @@ -0,0 +1,38 @@ +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { UnifiedAccountInput, UnifiedAccountOutput } from './model.unified'; +import { OriginalAccountOutput } from '@@core/utils/types/original/original.ticketing'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiResponse } from '@@core/utils/types'; + +export interface IAccountService { + syncAccounts( + linkedUserId: string, + custom_properties?: string[], + ): Promise>; +} + +export interface IAccountMapper { + desunify( + source: UnifiedAccountInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): DesunifyReturnType; + + unify( + source: OriginalAccountOutput | OriginalAccountOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAccountOutput | UnifiedAccountOutput[]; +} + +export class AccountResponse { + @ApiProperty({ type: [UnifiedAccountOutput] }) + accounts: UnifiedAccountOutput[]; + + @ApiPropertyOptional({ type: [{}] }) + remote_data?: Record[]; // Data in original format +} diff --git a/packages/api/src/ticketing/account/types/mappingsTypes.ts b/packages/api/src/ticketing/account/types/mappingsTypes.ts new file mode 100644 index 000000000..7d68374e2 --- /dev/null +++ b/packages/api/src/ticketing/account/types/mappingsTypes.ts @@ -0,0 +1,22 @@ +import { FrontAccountMapper } from '../services/front/mappers'; +import { GithubAccountMapper } from '../services/github/mappers'; +import { ZendeskAccountMapper } from '../services/zendesk/mappers'; + +const zendeskAccountMapper = new ZendeskAccountMapper(); +const frontAccountMapper = new FrontAccountMapper(); +const githubAccountMapper = new GithubAccountMapper(); + +export const accountUnificationMapping = { + zendesk: { + unify: zendeskAccountMapper.unify, + desunify: zendeskAccountMapper.desunify, + }, + front: { + unify: frontAccountMapper.unify, + desunify: frontAccountMapper.desunify, + }, + github: { + unify: githubAccountMapper.unify, + desunify: githubAccountMapper.desunify, + }, +}; diff --git a/packages/api/src/ticketing/account/types/model.unified.ts b/packages/api/src/ticketing/account/types/model.unified.ts new file mode 100644 index 000000000..943753498 --- /dev/null +++ b/packages/api/src/ticketing/account/types/model.unified.ts @@ -0,0 +1,9 @@ +export class UnifiedAccountInput { + name: string; + domains: string[]; + field_mappings?: Record[]; +} + +export class UnifiedAccountOutput extends UnifiedAccountInput { + id: string; +} diff --git a/packages/api/src/ticketing/account/utils/index.ts b/packages/api/src/ticketing/account/utils/index.ts new file mode 100644 index 000000000..f849788c1 --- /dev/null +++ b/packages/api/src/ticketing/account/utils/index.ts @@ -0,0 +1 @@ +/* PUT ALL UTILS FUNCTIONS USED IN YOUR OBJECT METHODS HERE */ diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index ebe409bcf..89e655693 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -153,7 +153,7 @@ export class CommentService { body: target_comment.body, html_body: target_comment.html_body, is_private: target_comment.is_private, - author_type: target_comment.author_type, + creator_type: target_comment.creator_type, id_tcg_ticket: target_comment.ticket_id, id_event: job_id, modified_at: new Date(), @@ -170,7 +170,7 @@ export class CommentService { is_private: target_comment.is_private, created_at: new Date(), modified_at: new Date(), - author_type: target_comment.author_type, + creator_type: target_comment.creator_type, id_tcg_ticket: target_comment.ticket_id, id_event: job_id, remote_id: originId, diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts index 8ba7414bb..f3482d576 100644 --- a/packages/api/src/ticketing/comment/services/front/mappers.ts +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -49,7 +49,7 @@ export class FrontCommentMapper implements ICommentMapper { html_body: '', created_at: new Date(comment.posted_at * 1000), // Convert UNIX timestamp to Date modified_at: new Date(), // Placeholder, as modified_at is not available - author_type: comment.author ? 'user' : null, + creator_type: comment.author ? 'user' : null, ticket_id: '', // TODO: Need to be determined from related data contact_id: '', // TODO: Need to be determined from related data user_id: '', //TODO diff --git a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts index ff695fa29..56061a7c0 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts @@ -53,7 +53,7 @@ export class ZendeskCommentMapper implements ICommentMapper { is_private: !comment.public, created_at: new Date(comment.created_at), modified_at: new Date(comment.created_at), // Assuming the creation date for modification as well - author_type: null, //TODO + creator_type: null, //TODO ticket_id: '', //TODO }; } diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index a61b849c2..0079b4cde 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -228,7 +228,7 @@ export class SyncService implements OnModuleInit { body: comment.body, html_body: comment.html_body, is_private: comment.is_private, - author_type: comment.author_type, + creator_type: comment.creator_type, id_tcg_ticket: id_ticket, id_event: jobId, modified_at: new Date(), @@ -246,7 +246,7 @@ export class SyncService implements OnModuleInit { is_private: comment.is_private, created_at: new Date(), modified_at: new Date(), - author_type: comment.author_type, + creator_type: comment.creator_type, id_tcg_ticket: id_ticket, id_event: jobId, remote_id: originId, diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index 50fde361c..1439fa56d 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -6,7 +6,7 @@ export class UnifiedCommentInput { is_private?: boolean; created_at: Date; modified_at: Date; - author_type: 'user' | 'contact' | null; + creator_type: 'user' | 'contact' | null; ticket_id: string; contact_id?: string; user_id?: string; diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index 72547dc50..e2dc6c75b 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -20,7 +20,7 @@ export class FrontTicketMapper implements ITicketMapper { comment: { body: source.comment.body, author_id: - source.comment.author_type === 'user' + source.comment.creator_type === 'user' ? source.comment.user_id : source.comment.contact_id, attachments: source.attachments, diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index af756bfbc..6d5ed894a 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -422,7 +422,7 @@ export class TicketService { return { id: ticket.id_tcg_ticket, name: ticket.name || '', - remote_id: ticket.remote_id || '', + //TODO: remote_id: ticket.remote_id || '', status: ticket.status || '', description: ticket.description || '', due_date: ticket.due_date || null, diff --git a/packages/api/src/ticketing/ticketing.module.ts b/packages/api/src/ticketing/ticketing.module.ts index 5d8d533c3..4c6bedb63 100644 --- a/packages/api/src/ticketing/ticketing.module.ts +++ b/packages/api/src/ticketing/ticketing.module.ts @@ -4,7 +4,7 @@ import { CommentModule } from './comment/comment.module'; import { UserModule } from './user/user.module'; import { AttachmentModule } from './attachment/attachment.module'; import { ContactModule } from './contact/contact.module'; -import { UserModule } from './user/user.module'; +import { AccountModule } from './account/account.module'; @Module({ imports: [ @@ -13,6 +13,7 @@ import { UserModule } from './user/user.module'; UserModule, AttachmentModule, ContactModule, + AccountModule, ], providers: [], controllers: [], diff --git a/packages/api/src/ticketing/user/services/user.service.ts b/packages/api/src/ticketing/user/services/user.service.ts index 0f597aff3..d788e4306 100644 --- a/packages/api/src/ticketing/user/services/user.service.ts +++ b/packages/api/src/ticketing/user/services/user.service.ts @@ -4,25 +4,12 @@ import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; import { handleServiceError } from '@@core/utils/errors'; -import { WebhookService } from '@@core/webhook/webhook.service'; -import { UnifiedUserInput, UnifiedUserOutput } from '../types/model.unified'; +import { UnifiedUserOutput } from '../types/model.unified'; import { UserResponse } from '../types'; -import { desunify } from '@@core/utils/unification/desunify'; -import { TicketingObject } from '@ticketing/@utils/@types'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { ServiceRegistry } from './registry.service'; -import { OriginalUserOutput } from '@@core/utils/types/original/original.ticketing'; -import { unify } from '@@core/utils/unification/unify'; @Injectable() export class UserService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private webhook: WebhookService, - private fieldMappingService: FieldMappingService, - private serviceRegistry: ServiceRegistry, - ) { + constructor(private prisma: PrismaService, private logger: LoggerService) { this.logger.setContext(UserService.name); } @@ -30,7 +17,71 @@ export class UserService { id_ticketing_user: string, remote_data?: boolean, ): Promise> { - return; + try { + const user = await this.prisma.tcg_users.findUnique({ + where: { + id_tcg_user: id_ticketing_user, + }, + }); + + // Fetch field mappings for the ticket + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: user.id_tcg_user, + }, + }, + include: { + attribute: true, + }, + }); + + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from(fieldMappingsMap, ([key, value]) => ({ + [key]: value, + })); + + // Transform to UnifiedUserOutput format + const unifiedUser: UnifiedUserOutput = { + id: user.id_tcg_user, + email_address: user.email_address, + name: user.name, + teams: user.teams, + field_mappings: field_mappings, + }; + + let res: UserResponse = { + users: [unifiedUser], + }; + + if (remote_data) { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: user.id_tcg_user, + }, + }); + const remote_data = JSON.parse(resp.data); + + res = { + ...res, + remote_data: [remote_data], + }; + } + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } } async getUsers( @@ -38,6 +89,105 @@ export class UserService { linkedUserId: string, remote_data?: boolean, ): Promise> { - return; + try { + //TODO: handle case where data is not there (not synced) or old synced + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.user.pull', + method: 'GET', + url: '/ticketing/user', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + const job_id = job_resp_create.id_event; + const users = await this.prisma.tcg_users.findMany({ + where: { + remote_id: integrationId.toLowerCase(), + events: { + id_linked_user: linkedUserId, + }, + }, + }); + + const unifiedUsers: UnifiedUserOutput[] = await Promise.all( + users.map(async (user) => { + // Fetch field mappings for the user + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: user.id_tcg_user, + }, + }, + include: { + attribute: true, + }, + }); + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from( + fieldMappingsMap, + ([key, value]) => ({ [key]: value }), + ); + + // Transform to UnifiedUserOutput format + return { + id: user.id_tcg_user, + email_address: user.email_address, + name: user.name, + teams: user.teams, + field_mappings: field_mappings, + }; + }), + ); + + let res: UserResponse = { + users: unifiedUsers, + }; + + if (remote_data) { + const remote_array_data: Record[] = await Promise.all( + users.map(async (user) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: user.id_tcg_user, + }, + }); + const remote_data = JSON.parse(resp.data); + return remote_data; + }), + ); + + res = { + ...res, + remote_data: remote_array_data, + }; + } + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } } } diff --git a/packages/api/src/ticketing/user/sync/sync.service.ts b/packages/api/src/ticketing/user/sync/sync.service.ts index 0a12a2b3e..ce46d9e01 100644 --- a/packages/api/src/ticketing/user/sync/sync.service.ts +++ b/packages/api/src/ticketing/user/sync/sync.service.ts @@ -3,15 +3,17 @@ import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, TICKETING_PROVIDERS } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; import { unify } from '@@core/utils/unification/unify'; import { TicketingObject } from '@ticketing/@utils/@types'; import { WebhookService } from '@@core/webhook/webhook.service'; -import { UnifiedUserOutput } from '../types/model.unified'; import { IUserService } from '../types'; +import { OriginalUserOutput } from '@@core/utils/types/original/original.ticketing'; +import { tcg_users as TicketingUser } from '@prisma/client'; +import { UnifiedUserOutput } from '../types/model.unified'; @Injectable() export class SyncService implements OnModuleInit { @@ -26,8 +28,282 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncUsers(); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + @Cron('*/20 * * * *') + //function used by sync worker which populate our tcg_users table + //its role is to fetch all users from providers 3rd parties and save the info inside our db + async syncUsers() { + try { + this.logger.log(`Syncing users....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedUsers.map(async (linkedUser) => { + try { + const providers = TICKETING_PROVIDERS; + for (const provider of providers) { + try { + await this.syncUsersForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncUsersForLinkedUser( + integrationId: string, + linkedUserId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} users for linkedUser ${linkedUserId}`, + ); + // check if linkedUser has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: integrationId, + }, + }); + if (!connection) return; + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.user.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + const job_id = job_resp_create.id_event; + + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'user', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: IUserService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = await service.syncUsers( + linkedUserId, + remoteProperties, + ); + + const sourceObject: OriginalUserOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: TicketingObject.user, + providerName: integrationId, + customFieldMappings, + })) as UnifiedUserOutput[]; + + //TODO + const userIds = sourceObject.map((user) => + 'id' in user ? String(user.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const user_data = await this.saveUsersInDb( + linkedUserId, + unifiedObject, + userIds, + integrationId, + job_id, + sourceObject, + ); + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + await this.webhook.handleWebhook( + user_data, + 'ticketing.user.pulled', + id_project, + job_id, + ); + } catch (error) { + handleServiceError(error, this.logger); + } } - // Additional methods and logic + async saveUsersInDb( + linkedUserId: string, + users: UnifiedUserOutput[], + originIds: string[], + originSource: string, + jobId: string, + remote_data: Record[], + ): Promise { + try { + let users_results: TicketingUser[] = []; + for (let i = 0; i < users.length; i++) { + const user = users[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingUser = await this.prisma.tcg_users.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + events: { + id_linked_user: linkedUserId, + }, + }, + }); + + let unique_ticketing_user_id: string; + + if (existingUser) { + // Update the existing ticket + const res = await this.prisma.tcg_users.update({ + where: { + id_tcg_user: existingUser.id_tcg_user, + }, + data: { + name: user.name, + email_address: user.email_address, + teams: user.teams || [], + modified_at: new Date(), + }, + }); + unique_ticketing_user_id = res.id_tcg_user; + users_results = [...users_results, res]; + } else { + // Create a new user + this.logger.log('not existing user ' + user.name); + const data = { + id_tcg_user: uuidv4(), + name: user.name, + email_address: user.email_address, + teams: user.teams || [], + created_at: new Date(), + modified_at: new Date(), + id_event: jobId, + remote_id: originId, + remote_platform: originSource, + }; + const res = await this.prisma.tcg_users.create({ + data: data, + }); + users_results = [...users_results, res]; + unique_ticketing_user_id = res.id_tcg_user; + } + + // check duplicate or existing values + if (user.field_mappings && user.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_ticketing_user_id, + }, + }); + + for (const mapping of user.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedUserId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_user_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_user_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return users_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/ticketing/user/types/model.unified.ts b/packages/api/src/ticketing/user/types/model.unified.ts index d97644198..801abbed0 100644 --- a/packages/api/src/ticketing/user/types/model.unified.ts +++ b/packages/api/src/ticketing/user/types/model.unified.ts @@ -1,6 +1,8 @@ export class UnifiedUserInput { name: string; email_address: string; + teams?: string[]; + field_mappings?: Record[]; } export class UnifiedUserOutput extends UnifiedUserInput { From bd10468984c7befb0fa8b389550e00076e946936 Mon Sep 17 00:00:00 2001 From: nael Date: Thu, 4 Jan 2024 19:36:31 +0100 Subject: [PATCH 08/25] :construction: All objects have their boilerplate: TODO are the attachment services --- .../types/original/original.ticketing.ts | 46 +- .../api/src/ticketing/@utils/@types/index.ts | 35 +- .../attachment/services/attachment.service.ts | 411 +++++++++++++++++- .../ticketing/attachment/sync/sync.service.ts | 252 ++++++++++- .../attachment/types/mappingsTypes.ts | 23 +- .../attachment/types/model.unified.ts | 8 +- .../src/ticketing/contact/contact.module.ts | 4 + .../contact/services/contact.service.ts | 199 ++++++++- .../ticketing/contact/services/front/index.ts | 63 +++ .../contact/services/front/mappers.ts | 43 ++ .../ticketing/contact/services/front/types.ts | 5 + .../contact/services/github/index.ts | 64 +++ .../contact/services/github/mappers.ts | 28 ++ .../contact/services/github/types.ts | 6 + .../contact/services/zendesk/index.ts | 66 ++- .../contact/services/zendesk/mappers.ts | 43 ++ .../contact/services/zendesk/types.ts | 6 +- .../ticketing/contact/sync/sync.service.ts | 280 +++++++++++- .../api/src/ticketing/contact/types/index.ts | 5 - .../ticketing/contact/types/mappingsTypes.ts | 23 +- .../ticketing/contact/types/model.unified.ts | 12 +- .../src/ticketing/tag/services/front/index.ts | 61 +++ .../ticketing/tag/services/front/mappers.ts | 43 ++ .../src/ticketing/tag/services/front/types.ts | 5 + .../ticketing/tag/services/github/index.ts | 64 +++ .../ticketing/tag/services/github/mappers.ts | 28 ++ .../ticketing/tag/services/github/types.ts | 6 + .../tag/services/registry.service.ts | 23 + .../src/ticketing/tag/services/tag.service.ts | 189 ++++++++ .../ticketing/tag/services/zendesk/index.ts | 68 +++ .../ticketing/tag/services/zendesk/mappers.ts | 43 ++ .../ticketing/tag/services/zendesk/types.ts | 7 + .../src/ticketing/tag/sync/sync.service.ts | 305 +++++++++++++ .../api/src/ticketing/tag/tag.controller.ts | 81 ++++ packages/api/src/ticketing/tag/tag.module.ts | 39 ++ packages/api/src/ticketing/tag/types/index.ts | 38 ++ .../src/ticketing/tag/types/mappingsTypes.ts | 22 + .../src/ticketing/tag/types/model.unified.ts | 8 + packages/api/src/ticketing/tag/utils/index.ts | 1 + .../ticketing/team/services/front/index.ts | 63 +++ .../ticketing/team/services/front/mappers.ts | 43 ++ .../ticketing/team/services/front/types.ts | 5 + .../ticketing/team/services/github/index.ts | 64 +++ .../ticketing/team/services/github/mappers.ts | 28 ++ .../ticketing/team/services/github/types.ts | 6 + .../team/services/registry.service.ts | 23 + .../ticketing/team/services/team.service.ts | 191 ++++++++ .../ticketing/team/services/zendesk/index.ts | 68 +++ .../team/services/zendesk/mappers.ts | 43 ++ .../ticketing/team/services/zendesk/types.ts | 7 + .../src/ticketing/team/sync/sync.service.ts | 307 +++++++++++++ .../api/src/ticketing/team/team.controller.ts | 69 +++ .../api/src/ticketing/team/team.module.ts | 39 ++ .../api/src/ticketing/team/types/index.ts | 38 ++ .../src/ticketing/team/types/mappingsTypes.ts | 22 + .../src/ticketing/team/types/model.unified.ts | 9 + .../api/src/ticketing/team/utils/index.ts | 1 + .../api/src/ticketing/ticketing.module.ts | 8 +- 58 files changed, 3626 insertions(+), 61 deletions(-) create mode 100644 packages/api/src/ticketing/contact/services/front/index.ts create mode 100644 packages/api/src/ticketing/contact/services/front/mappers.ts create mode 100644 packages/api/src/ticketing/contact/services/front/types.ts create mode 100644 packages/api/src/ticketing/contact/services/github/index.ts create mode 100644 packages/api/src/ticketing/contact/services/github/mappers.ts create mode 100644 packages/api/src/ticketing/contact/services/github/types.ts create mode 100644 packages/api/src/ticketing/tag/services/front/index.ts create mode 100644 packages/api/src/ticketing/tag/services/front/mappers.ts create mode 100644 packages/api/src/ticketing/tag/services/front/types.ts create mode 100644 packages/api/src/ticketing/tag/services/github/index.ts create mode 100644 packages/api/src/ticketing/tag/services/github/mappers.ts create mode 100644 packages/api/src/ticketing/tag/services/github/types.ts create mode 100644 packages/api/src/ticketing/tag/services/registry.service.ts create mode 100644 packages/api/src/ticketing/tag/services/tag.service.ts create mode 100644 packages/api/src/ticketing/tag/services/zendesk/index.ts create mode 100644 packages/api/src/ticketing/tag/services/zendesk/mappers.ts create mode 100644 packages/api/src/ticketing/tag/services/zendesk/types.ts create mode 100644 packages/api/src/ticketing/tag/sync/sync.service.ts create mode 100644 packages/api/src/ticketing/tag/tag.controller.ts create mode 100644 packages/api/src/ticketing/tag/tag.module.ts create mode 100644 packages/api/src/ticketing/tag/types/index.ts create mode 100644 packages/api/src/ticketing/tag/types/mappingsTypes.ts create mode 100644 packages/api/src/ticketing/tag/types/model.unified.ts create mode 100644 packages/api/src/ticketing/tag/utils/index.ts create mode 100644 packages/api/src/ticketing/team/services/front/index.ts create mode 100644 packages/api/src/ticketing/team/services/front/mappers.ts create mode 100644 packages/api/src/ticketing/team/services/front/types.ts create mode 100644 packages/api/src/ticketing/team/services/github/index.ts create mode 100644 packages/api/src/ticketing/team/services/github/mappers.ts create mode 100644 packages/api/src/ticketing/team/services/github/types.ts create mode 100644 packages/api/src/ticketing/team/services/registry.service.ts create mode 100644 packages/api/src/ticketing/team/services/team.service.ts create mode 100644 packages/api/src/ticketing/team/services/zendesk/index.ts create mode 100644 packages/api/src/ticketing/team/services/zendesk/mappers.ts create mode 100644 packages/api/src/ticketing/team/services/zendesk/types.ts create mode 100644 packages/api/src/ticketing/team/sync/sync.service.ts create mode 100644 packages/api/src/ticketing/team/team.controller.ts create mode 100644 packages/api/src/ticketing/team/team.module.ts create mode 100644 packages/api/src/ticketing/team/types/index.ts create mode 100644 packages/api/src/ticketing/team/types/mappingsTypes.ts create mode 100644 packages/api/src/ticketing/team/types/model.unified.ts create mode 100644 packages/api/src/ticketing/team/utils/index.ts 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 3d0f5fd07..5578365dd 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -7,7 +7,23 @@ import { ZendeskUserOutput, ZendeskAttachmentOutput, ZendeskAttachmentInput, + ZendeskAccountInput, + ZendeskAccountOutput, + ZendeskContactInput, + ZendeskContactOutput, + ZendeskTagInput, + ZendeskTagOutput, + ZendeskTeamInput, + ZendeskTeamOutput, } from '@ticketing/@utils/@types'; +import { + FrontAccountInput, + FrontAccountOutput, +} from '@ticketing/account/services/front/types'; +import { + GithubAccountInput, + GithubAccountOutput, +} from '@ticketing/account/services/github/types'; import { FrontCommentInput, FrontCommentOutput, @@ -20,6 +36,30 @@ import { HubspotCommentInput, HubspotCommentOutput, } from '@ticketing/comment/services/hubspot/types'; +import { + FrontContactInput, + FrontContactOutput, +} from '@ticketing/contact/services/front/types'; +import { + GithubContactInput, + GithubContactOutput, +} from '@ticketing/contact/services/github/types'; +import { + FrontTagInput, + FrontTagOutput, +} from '@ticketing/tag/services/front/types'; +import { + GithubTagInput, + GithubTagOutput, +} from '@ticketing/tag/services/github/types'; +import { + FrontTeamInput, + FrontTeamOutput, +} from '@ticketing/team/services/front/types'; +import { + GithubTeamInput, + GithubTeamOutput, +} from '@ticketing/team/services/github/types'; import { FrontTicketInput, FrontTicketOutput, @@ -138,4 +178,8 @@ export type TicketingObjectOutput = | OriginalTicketOutput | OriginalCommentOutput | OriginalUserOutput - | OriginalAttachmentOutput; + | OriginalAttachmentOutput + | OriginalTeamOutput + | OriginalTagOutput + | OriginalContactOutput + | OriginalAccountOutput; diff --git a/packages/api/src/ticketing/@utils/@types/index.ts b/packages/api/src/ticketing/@utils/@types/index.ts index d9f8f46b5..079ceee92 100644 --- a/packages/api/src/ticketing/@utils/@types/index.ts +++ b/packages/api/src/ticketing/@utils/@types/index.ts @@ -1,6 +1,16 @@ +import { contactUnificationMapping } from '@crm/contact/types/mappingsTypes'; import { IAccountService } from '@ticketing/account/types'; -import { UnifiedAccountInput, UnifiedAccountOutput } from '@ticketing/account/types/model.unified'; +import { accountUnificationMapping } from '@ticketing/account/types/mappingsTypes'; +import { + UnifiedAccountInput, + UnifiedAccountOutput, +} from '@ticketing/account/types/model.unified'; import { IAttachmentService } from '@ticketing/attachment/types'; +import { attachmentUnificationMapping } from '@ticketing/attachment/types/mappingsTypes'; +import { + UnifiedAttachmentInput, + UnifiedAttachmentOutput, +} from '@ticketing/attachment/types/model.unified'; import { ICommentService } from '@ticketing/comment/types'; import { commentUnificationMapping } from '@ticketing/comment/types/mappingsTypes'; import { @@ -8,6 +18,22 @@ import { UnifiedCommentOutput, } from '@ticketing/comment/types/model.unified'; import { IContactService } from '@ticketing/contact/types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from '@ticketing/contact/types/model.unified'; +import { ITagService } from '@ticketing/tag/types'; +import { tagUnificationMapping } from '@ticketing/tag/types/mappingsTypes'; +import { + UnifiedTagInput, + UnifiedTagOutput, +} from '@ticketing/tag/types/model.unified'; +import { ITeamService } from '@ticketing/team/types'; +import { teamUnificationMapping } from '@ticketing/team/types/mappingsTypes'; +import { + UnifiedTeamInput, + UnifiedTeamOutput, +} from '@ticketing/team/types/model.unified'; import { ITicketService } from '@ticketing/ticket/types'; import { ticketUnificationMapping } from '@ticketing/ticket/types/mappingsTypes'; import { @@ -40,9 +66,9 @@ export type UnifiedTicketing = | UnifiedUserInput | UnifiedUserOutput | UnifiedAccountInput - | UnifiedAccountOutput; + | UnifiedAccountOutput | UnifiedContactInput - | UnifiedContactOutput; + | UnifiedContactOutput | UnifiedTeamInput | UnifiedTeamOutput | UnifiedTagInput @@ -67,10 +93,9 @@ export type ITicketingService = | IUserService | IAttachmentService | IContactService + | IAccountService | ITeamService | ITagService; -; - export * from '../../ticket/services/zendesk/types'; export * from '../../comment/services/zendesk/types'; export * from '../../user/services/zendesk/types'; diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index 2624418dd..e1bf59e09 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -9,12 +9,13 @@ import { UnifiedAttachmentInput, UnifiedAttachmentOutput, } from '../types/model.unified'; -import { AttachmentResponse } from '../types'; +import { AttachmentResponse, IAttachmentService } from '../types'; import { desunify } from '@@core/utils/unification/desunify'; import { TicketingObject } from '@ticketing/@utils/@types'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { unify } from '@@core/utils/unification/unify'; import { ServiceRegistry } from './registry.service'; +import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; @Injectable() export class AttachmentService { @@ -28,5 +29,411 @@ export class AttachmentService { this.logger.setContext(AttachmentService.name); } - // Additional methods and logic + async batchAddAttachments( + unifiedAttachmentData: UnifiedAttachmentInput[], + integrationId: string, + linkedUserId: string, + remote_data?: boolean, + ): Promise> { + try { + const responses = await Promise.all( + unifiedAttachmentData.map((unifiedData) => + this.addAttachment( + unifiedData, + integrationId.toLowerCase(), + linkedUserId, + remote_data, + ), + ), + ); + + const allAttachments = responses.flatMap( + (response) => response.data.attachments, + ); + const allRemoteData = responses.flatMap( + (response) => response.data.remote_data || [], + ); + + return { + data: { + attachments: allAttachments, + remote_data: allRemoteData, + }, + message: 'All attachments inserted successfully', + statusCode: 201, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async addAttachment( + unifiedAttachmentData: UnifiedAttachmentInput, + integrationId: string, + linkedUserId: string, + remote_data?: boolean, + ): Promise> { + try { + const linkedUser = await this.prisma.linked_users.findUnique({ + where: { + id_linked_user: linkedUserId, + }, + }); + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.attachment.created', //sync, push or pull + method: 'POST', + url: '/ticketing/attachment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + const job_id = job_resp_create.id_event; + + //TODO + // Retrieve custom field mappings + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'attachment', + ); + //desunify the data according to the target obj wanted + const desunifiedObject = await desunify({ + sourceObject: unifiedAttachmentData, + targetType: TicketingObject.attachment, + providerName: integrationId, + customFieldMappings: unifiedAttachmentData.field_mappings + ? customFieldMappings + : [], + }); + + const service: IAttachmentService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = + await service.addAttachment(desunifiedObject, linkedUserId); + + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject: [resp.data], + targetType: TicketingObject.attachment, + providerName: integrationId, + customFieldMappings: customFieldMappings, + })) as UnifiedAttachmentOutput[]; + + // add the attachment inside our db + const source_attachment = resp.data; + const target_attachment = unifiedObject[0]; + const originId = + 'id' in source_attachment ? String(source_attachment.id) : undefined; //TODO + + const existingAttachment = await this.prisma.tcg_attachments.findFirst({ + where: { + remote_id: originId, + remote_platform: integrationId, + events: { + id_linked_user: linkedUserId, + }, + }, + }); + + let unique_ticketing_attachment_id: string; + + if (existingAttachment) { + // Update the existing attachment + const res = await this.prisma.tcg_attachments.update({ + where: { + id_tcg_attachment: existingAttachment.id_tcg_attachment, + }, + data: { + //TODO + modified_at: new Date(), + }, + }); + unique_ticketing_attachment_id = res.id_tcg_attachment; + } else { + // Create a new attachment + this.logger.log('not existing attachment ' + target_attachment); + const data = { + id_tcg_attachment: uuidv4(), + //TODO + created_at: new Date(), + modified_at: new Date(), + id_event: job_id, + remote_id: originId, + remote_platform: integrationId, + }; + + const res = await this.prisma.tcg_attachments.create({ + data: data, + }); + unique_ticketing_attachment_id = res.id_tcg_attachment; + } + + // check duplicate or existing values + if ( + target_attachment.field_mappings && + target_attachment.field_mappings.length > 0 + ) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_ticketing_attachment_id, + }, + }); + + for (const mapping of target_attachment.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: integrationId, + id_consumer: linkedUserId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] || 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + if (remote_data) { + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_attachment_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_attachment_id, + format: 'json', + data: JSON.stringify(source_attachment), + created_at: new Date(), + }, + update: { + data: JSON.stringify(source_attachment), + created_at: new Date(), + }, + }); + } + + ///// + const result_attachment = await this.getAttachment( + unique_ticketing_attachment_id, + remote_data, + ); + + const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: status_resp, + }, + }); + await this.webhook.handleWebhook( + result_attachment.data.attachments, + 'ticketing.attachment.created', + linkedUser.id_project, + job_id, + ); + return { ...resp, data: result_attachment.data }; + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async getAttachment( + id_ticketing_attachment: string, + remote_data?: boolean, + ): Promise> { + try { + const attachment = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: id_ticketing_attachment, + }, + }); + + // Fetch field mappings for the attachment + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: attachment.id_tcg_attachment, + }, + }, + include: { + attribute: true, + }, + }); + + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from(fieldMappingsMap, ([key, value]) => ({ + [key]: value, + })); + + // Transform to UnifiedAttachmentOutput format + const unifiedAttachment: UnifiedAttachmentOutput = { + id: attachment.id_tcg_attachment, + //TODO + field_mappings: field_mappings, + }; + + let res: AttachmentResponse = { + attachments: [unifiedAttachment], + }; + + if (remote_data) { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: attachment.id_tcg_attachment, + }, + }); + const remote_data = JSON.parse(resp.data); + + res = { + ...res, + remote_data: [remote_data], + }; + } + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async getAttachments( + integrationId: string, + linkedUserId: string, + remote_data?: boolean, + ): Promise> { + try { + //TODO: handle case where data is not there (not synced) or old synced + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.attachment.pull', + method: 'GET', + url: '/ticketing/attachment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + const job_id = job_resp_create.id_event; + const attachments = await this.prisma.tcg_attachments.findMany({ + where: { + remote_id: integrationId.toLowerCase(), + events: { + id_linked_user: linkedUserId, + }, + }, + }); + + const unifiedAttachments: UnifiedAttachmentOutput[] = await Promise.all( + attachments.map(async (attachment) => { + // Fetch field mappings for the attachment + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: attachment.id_tcg_attachment, + }, + }, + include: { + attribute: true, + }, + }); + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from( + fieldMappingsMap, + ([key, value]) => ({ [key]: value }), + ); + + // Transform to UnifiedAttachmentOutput format + return { + id: attachment.id_tcg_attachment, + //TODO + field_mappings: field_mappings, + }; + }), + ); + + let res: AttachmentResponse = { + attachments: unifiedAttachments, + }; + + if (remote_data) { + const remote_array_data: Record[] = await Promise.all( + attachments.map(async (attachment) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: attachment.id_tcg_attachment, + }, + }); + const remote_data = JSON.parse(resp.data); + return remote_data; + }), + ); + + res = { + ...res, + remote_data: remote_array_data, + }; + } + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/ticketing/attachment/sync/sync.service.ts b/packages/api/src/ticketing/attachment/sync/sync.service.ts index 5caadfabc..d6a9b1afc 100644 --- a/packages/api/src/ticketing/attachment/sync/sync.service.ts +++ b/packages/api/src/ticketing/attachment/sync/sync.service.ts @@ -12,6 +12,8 @@ import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedAttachmentOutput } from '../types/model.unified'; import { IAttachmentService } from '../types'; import { ServiceRegistry } from '../services/registry.service'; +import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; +import { tcg_attachments as TicketingAttachment } from '@prisma/client'; @Injectable() export class SyncService implements OnModuleInit { @@ -26,8 +28,254 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncAttachments(); + } catch (error) { + handleServiceError(error, this.logger); + } } - // Additional methods and logic + @Cron('*/20 * * * *') + //function used by sync worker which populate our tcg_attachments table + //its role is to fetch all attachments from providers 3rd parties and save the info inside our db + async syncAttachments() { + try { + this.logger.log(`Syncing attachments....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedUsers.map(async (linkedUser) => { + try { + const providers = TICKETING_PROVIDERS; + for (const provider of providers) { + try { + //call the sync attachments for every ticket of the linkedUser (a attachment is tied to a ticket) + const tickets = await this.prisma.tcg_tickets.findMany({ + where: { + remote_platform: provider, + events: { + id_linked_user: linkedUser.id_linked_user, + }, + }, + }); + for (const ticket of tickets) { + await this.syncAttachmentsForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ticket.id_tcg_ticket, + ); + } + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncAttachmentsForLinkedUser( + integrationId: string, + linkedUserId: string, + id_project: string, + id_ticket: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} attachments for linkedUser ${linkedUserId}`, + ); + // check if linkedUser has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: integrationId, + }, + }); + if (!connection) return; + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.attachment.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + const job_id = job_resp_create.id_event; + + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'attachment', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: IAttachmentService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = + await service.syncAttachments(linkedUserId, remoteProperties); + + const sourceObject: OriginalAttachmentOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: TicketingObject.attachment, + providerName: integrationId, + customFieldMappings, + })) as UnifiedAttachmentOutput[]; + + //TODO + const attachmentsIds = sourceObject.map((attachment) => + 'id' in attachment ? String(attachment.id) : undefined, + ); + //insert the data in the DB with the fieldMappings (value table) + const attachments_data = await this.saveAttachmentsInDb( + linkedUserId, + unifiedObject, + attachmentsIds, + integrationId, + id_ticket, + job_id, + sourceObject, + ); + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + await this.webhook.handleWebhook( + attachments_data, + 'ticketing.attachment.pulled', + id_project, + job_id, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveAttachmentsInDb( + linkedUserId: string, + attachments: UnifiedAttachmentOutput[], + originIds: string[], + originSource: string, + id_ticket: string, + jobId: string, + remote_data: Record[], + ): Promise { + try { + let attachments_results: TicketingAttachment[] = []; + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingAttachment = await this.prisma.tcg_attachments.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + events: { + id_linked_user: linkedUserId, + }, + }, + }); + + let unique_ticketing_attachment_id: string; + + if (existingAttachment) { + // Update the existing attachment + const res = await this.prisma.tcg_attachments.update({ + where: { + id_tcg_attachment: existingAttachment.id_tcg_attachment, + }, + data: { + //TODO + id_tcg_ticket: id_ticket, + id_event: jobId, + modified_at: new Date(), + }, + }); + unique_ticketing_attachment_id = res.id_tcg_attachment; + attachments_results = [...attachments_results, res]; + } else { + // Create a new attachment + this.logger.log('attachment not exists'); + const data = { + id_tcg_attachment: uuidv4(), + //TODO + created_at: new Date(), + modified_at: new Date(), + id_tcg_ticket: id_ticket, + id_event: jobId, + remote_id: originId, + remote_platform: originSource, + //TODO; id_tcg_contact String? @db.Uuid + //TODO; id_tcg_user String? @db.Uuid + }; + const res = await this.prisma.tcg_attachments.create({ + data: data, + }); + attachments_results = [...attachments_results, res]; + unique_ticketing_attachment_id = res.id_tcg_attachment; + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_attachment_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_attachment_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return attachments_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/ticketing/attachment/types/mappingsTypes.ts b/packages/api/src/ticketing/attachment/types/mappingsTypes.ts index d08c20aaa..267326725 100644 --- a/packages/api/src/ticketing/attachment/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/attachment/types/mappingsTypes.ts @@ -1 +1,22 @@ -// Content for mappingsTypes.ts +import { FrontAttachmentMapper } from '../services/front/mappers'; +import { GithubAttachmentMapper } from '../services/github/mappers'; +import { ZendeskAttachmentMapper } from '../services/zendesk/mappers'; + +const zendeskAttachmentMapper = new ZendeskAttachmentMapper(); +const frontAttachmentMapper = new FrontAttachmentMapper(); +const githubAttachmentMapper = new GithubAttachmentMapper(); + +export const attachmentUnificationMapping = { + zendesk: { + unify: zendeskAttachmentMapper.unify, + desunify: zendeskAttachmentMapper.desunify, + }, + front: { + unify: frontAttachmentMapper.unify, + desunify: frontAttachmentMapper.desunify, + }, + github: { + unify: githubAttachmentMapper.unify, + desunify: githubAttachmentMapper.desunify, + }, +}; diff --git a/packages/api/src/ticketing/attachment/types/model.unified.ts b/packages/api/src/ticketing/attachment/types/model.unified.ts index b452a5c8d..0151979d3 100644 --- a/packages/api/src/ticketing/attachment/types/model.unified.ts +++ b/packages/api/src/ticketing/attachment/types/model.unified.ts @@ -1,3 +1,7 @@ -export class UnifiedAttachmentInput {} +export class UnifiedAttachmentInput { + field_mappings?: Record[]; +} -export class UnifiedAttachmentOutput extends UnifiedAttachmentInput {} +export class UnifiedAttachmentOutput extends UnifiedAttachmentInput { + id: string; +} diff --git a/packages/api/src/ticketing/contact/contact.module.ts b/packages/api/src/ticketing/contact/contact.module.ts index cba0c9133..a6c3a9c79 100644 --- a/packages/api/src/ticketing/contact/contact.module.ts +++ b/packages/api/src/ticketing/contact/contact.module.ts @@ -10,6 +10,8 @@ import { FieldMappingService } from '@@core/field-mapping/field-mapping.service' import { ServiceRegistry } from './services/registry.service'; import { ContactService } from './services/contact.service'; import { ContactController } from './contact.controller'; +import { FrontService } from './services/front'; +import { GithubService } from './services/github'; @Module({ imports: [ @@ -29,6 +31,8 @@ import { ContactController } from './contact.controller'; ServiceRegistry, /* PROVIDERS SERVICES */ ZendeskService, + FrontService, + GithubService, ], exports: [SyncService], }) diff --git a/packages/api/src/ticketing/contact/services/contact.service.ts b/packages/api/src/ticketing/contact/services/contact.service.ts index a03b682b5..b10dce39c 100644 --- a/packages/api/src/ticketing/contact/services/contact.service.ts +++ b/packages/api/src/ticketing/contact/services/contact.service.ts @@ -4,27 +4,192 @@ import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; import { handleServiceError } from '@@core/utils/errors'; -import { WebhookService } from '@@core/webhook/webhook.service'; -import { - UnifiedContactInput, - UnifiedContactOutput, -} from '../types/model.unified'; +import { UnifiedContactOutput } from '../types/model.unified'; import { ContactResponse } from '../types'; -import { desunify } from '@@core/utils/unification/desunify'; -import { TicketingObject } from '@ticketing/@utils/@types'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { unify } from '@@core/utils/unification/unify'; -import { ServiceRegistry } from './registry.service'; @Injectable() export class ContactService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private webhook: WebhookService, - private fieldMappingService: FieldMappingService, - private serviceRegistry: ServiceRegistry, - ) { + constructor(private prisma: PrismaService, private logger: LoggerService) { this.logger.setContext(ContactService.name); } + + async getContact( + id_ticketing_contact: string, + remote_data?: boolean, + ): Promise> { + try { + const contact = await this.prisma.tcg_contacts.findUnique({ + where: { + id_tcg_contact: id_ticketing_contact, + }, + }); + + // Fetch field mappings for the ticket + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: contact.id_tcg_contact, + }, + }, + include: { + attribute: true, + }, + }); + + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from(fieldMappingsMap, ([key, value]) => ({ + [key]: value, + })); + + // Transform to UnifiedContactOutput format + const unifiedContact: UnifiedContactOutput = { + id: contact.id_tcg_contact, + email_address: contact.email_address, + name: contact.name, + details: contact.details, + phone_number: contact.phone_number, + field_mappings: field_mappings, + }; + + let res: ContactResponse = { + contacts: [unifiedContact], + }; + + if (remote_data) { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: contact.id_tcg_contact, + }, + }); + const remote_data = JSON.parse(resp.data); + + res = { + ...res, + remote_data: [remote_data], + }; + } + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async getContacts( + integrationId: string, + linkedContactId: string, + remote_data?: boolean, + ): Promise> { + try { + //TODO: handle case where data is not there (not synced) or old synced + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.contact.pull', + method: 'GET', + url: '/ticketing/contact', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedContactId, + }, + }); + const job_id = job_resp_create.id_event; + const contacts = await this.prisma.tcg_contacts.findMany({ + where: { + remote_id: integrationId.toLowerCase(), + events: { + id_linked_user: linkedContactId, + }, + }, + }); + + const unifiedContacts: UnifiedContactOutput[] = await Promise.all( + contacts.map(async (contact) => { + // Fetch field mappings for the contact + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: contact.id_tcg_contact, + }, + }, + include: { + attribute: true, + }, + }); + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from( + fieldMappingsMap, + ([key, value]) => ({ [key]: value }), + ); + + // Transform to UnifiedContactOutput format + return { + id: contact.id_tcg_contact, + email_address: contact.email_address, + name: contact.name, + details: contact.details, + phone_number: contact.phone_number, + field_mappings: field_mappings, + }; + }), + ); + + let res: ContactResponse = { + contacts: unifiedContacts, + }; + + if (remote_data) { + const remote_array_data: Record[] = await Promise.all( + contacts.map(async (contact) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: contact.id_tcg_contact, + }, + }); + const remote_data = JSON.parse(resp.data); + return remote_data; + }), + ); + + res = { + ...res, + remote_data: remote_array_data, + }; + } + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/ticketing/contact/services/front/index.ts b/packages/api/src/ticketing/contact/services/front/index.ts new file mode 100644 index 000000000..65c60e3fb --- /dev/null +++ b/packages/api/src/ticketing/contact/services/front/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 { IContactService } from '@ticketing/contact/types'; +import { FrontContactOutput } from './types'; + +@Injectable() +export class FrontService implements IContactService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.contact.toUpperCase() + ':' + FrontService.name, + ); + this.registry.registerService('front', this); + } + + async syncContacts( + linkedContactId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedContactId, + provider_slug: 'front', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/teammates', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced front contacts !`); + + return { + data: resp.data._results, + message: 'Front contacts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + TicketingObject.contact, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/contact/services/front/mappers.ts b/packages/api/src/ticketing/contact/services/front/mappers.ts new file mode 100644 index 000000000..1e9bd9119 --- /dev/null +++ b/packages/api/src/ticketing/contact/services/front/mappers.ts @@ -0,0 +1,43 @@ +import { IContactMapper } from '@ticketing/contact/types'; +import { FrontContactInput, FrontContactOutput } from './types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from '@ticketing/contact/types/model.unified'; + +export class FrontContactMapper implements IContactMapper { + desunify( + source: UnifiedContactInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): FrontContactInput { + return; + } + + unify( + source: FrontContactOutput | FrontContactOutput[], + 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((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTicketToUnified( + ticket: FrontContactOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedContactOutput { + return; + } +} diff --git a/packages/api/src/ticketing/contact/services/front/types.ts b/packages/api/src/ticketing/contact/services/front/types.ts new file mode 100644 index 000000000..f744e464a --- /dev/null +++ b/packages/api/src/ticketing/contact/services/front/types.ts @@ -0,0 +1,5 @@ +export type FrontContactInput = { + id: string; +}; + +export type FrontContactOutput = FrontContactInput; diff --git a/packages/api/src/ticketing/contact/services/github/index.ts b/packages/api/src/ticketing/contact/services/github/index.ts new file mode 100644 index 000000000..baf87f328 --- /dev/null +++ b/packages/api/src/ticketing/contact/services/github/index.ts @@ -0,0 +1,64 @@ +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 { GithubContactOutput } from './types'; + +//TODO +@Injectable() +export class GithubService implements IContactService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.contact.toUpperCase() + ':' + GithubService.name, + ); + this.registry.registerService('github', this); + } + + async syncContacts( + linkedContactId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedContactId, + provider_slug: 'github', + }, + }); + const resp = await axios.get(`https://api.github.com/contacts`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced github contacts !`); + + return { + data: resp.data, + message: 'Github contacts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + TicketingObject.contact, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/contact/services/github/mappers.ts b/packages/api/src/ticketing/contact/services/github/mappers.ts new file mode 100644 index 000000000..cd5d22f33 --- /dev/null +++ b/packages/api/src/ticketing/contact/services/github/mappers.ts @@ -0,0 +1,28 @@ +import { IContactMapper } from '@ticketing/contact/types'; +import { GithubContactInput, GithubContactOutput } from './types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from '@ticketing/contact/types/model.unified'; + +export class GithubContactMapper implements IContactMapper { + desunify( + source: UnifiedContactInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GithubContactInput { + return; + } + + unify( + source: GithubContactOutput | GithubContactOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedContactOutput | UnifiedContactOutput[] { + return; + } +} diff --git a/packages/api/src/ticketing/contact/services/github/types.ts b/packages/api/src/ticketing/contact/services/github/types.ts new file mode 100644 index 000000000..422136439 --- /dev/null +++ b/packages/api/src/ticketing/contact/services/github/types.ts @@ -0,0 +1,6 @@ +export type GithubContactInput = { + name: string; +}; + +//TODO +export type GithubContactOutput = GithubContactInput; diff --git a/packages/api/src/ticketing/contact/services/zendesk/index.ts b/packages/api/src/ticketing/contact/services/zendesk/index.ts index 8d00a3c68..b2001713f 100644 --- a/packages/api/src/ticketing/contact/services/zendesk/index.ts +++ b/packages/api/src/ticketing/contact/services/zendesk/index.ts @@ -1,14 +1,17 @@ -import { EncryptionService } from '@@core/encryption/encryption.service'; -import { EnvironmentService } from '@@core/environment/environment.service'; +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, + ZendeskContactOutput, +} from '@ticketing/@utils/@types'; import { ApiResponse } from '@@core/utils/types'; -import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { OriginalContactOutput } from '@@core/utils/types/original/original.crm'; -import { Injectable } from '@nestjs/common'; -import { TicketingObject } from '@ticketing/@utils/@types'; -import { IContactService } from '@ticketing/contact/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { EnvironmentService } from '@@core/environment/environment.service'; import { ServiceRegistry } from '../registry.service'; +import { IContactService } from '@ticketing/contact/types'; @Injectable() export class ZendeskService implements IContactService { @@ -24,16 +27,45 @@ export class ZendeskService implements IContactService { ); this.registry.registerService('zendesk_t', this); } - addContact( - contactData: DesunifyReturnType, - linkedUserId: string, - ): Promise> { - throw new Error('Method not implemented.'); - } - syncContacts( - linkedUserId: string, + + async syncContacts( + linkedContactId: string, custom_properties?: string[], - ): Promise> { - throw new Error('Method not implemented.'); + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedContactId, + provider_slug: 'zendesk_t', + }, + }); + + const resp = await axios.get( + `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/contacts`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced zendesk contacts !`); + + return { + data: resp.data.contacts, + message: 'Zendesk contacts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zendesk', + TicketingObject.contact, + ActionType.GET, + ); + } } } diff --git a/packages/api/src/ticketing/contact/services/zendesk/mappers.ts b/packages/api/src/ticketing/contact/services/zendesk/mappers.ts index e69de29bb..516b69efe 100644 --- a/packages/api/src/ticketing/contact/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/contact/services/zendesk/mappers.ts @@ -0,0 +1,43 @@ +import { IContactMapper } from '@ticketing/contact/types'; +import { ZendeskContactInput, ZendeskContactOutput } from './types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from '@ticketing/contact/types/model.unified'; + +export class ZendeskContactMapper implements IContactMapper { + desunify( + source: UnifiedContactInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): ZendeskContactInput { + return; + } + + unify( + source: ZendeskContactOutput | ZendeskContactOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedContactOutput | UnifiedContactOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleContactToUnified(source, customFieldMappings); + } + return source.map((ticket) => + this.mapSingleContactToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleContactToUnified( + ticket: ZendeskContactOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedContactOutput { + return; + } +} diff --git a/packages/api/src/ticketing/contact/services/zendesk/types.ts b/packages/api/src/ticketing/contact/services/zendesk/types.ts index fea981f1c..093356801 100644 --- a/packages/api/src/ticketing/contact/services/zendesk/types.ts +++ b/packages/api/src/ticketing/contact/services/zendesk/types.ts @@ -1,5 +1,7 @@ export type ZendeskContactInput = { - id: string; + _: string; }; -export type ZendeskContactOutput = ZendeskContactInput; +export type ZendeskContactOutput = ZendeskContactInput & { + id: number; // Read-only. Automatically assigned when the ticket is created. +}; diff --git a/packages/api/src/ticketing/contact/sync/sync.service.ts b/packages/api/src/ticketing/contact/sync/sync.service.ts index 6e094ef7f..0e2bdbb09 100644 --- a/packages/api/src/ticketing/contact/sync/sync.service.ts +++ b/packages/api/src/ticketing/contact/sync/sync.service.ts @@ -12,6 +12,8 @@ import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedContactOutput } from '../types/model.unified'; import { IContactService } from '../types'; import { ServiceRegistry } from '../services/registry.service'; +import { tcg_contacts as TicketingContact } from '@prisma/client'; +import { OriginalContactOutput } from '@@core/utils/types/original/original.ticketing'; @Injectable() export class SyncService implements OnModuleInit { @@ -26,6 +28,282 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncContacts(); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + @Cron('*/20 * * * *') + //function used by sync worker which populate our tcg_contacts table + //its role is to fetch all contacts from providers 3rd parties and save the info inside our db + async syncContacts() { + try { + this.logger.log(`Syncing contacts....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedContacts = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedContacts.map(async (linkedContact) => { + try { + const providers = TICKETING_PROVIDERS; + for (const provider of providers) { + try { + await this.syncContactsForLinkedContact( + provider, + linkedContact.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncContactsForLinkedContact( + integrationId: string, + linkedContactId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} contacts for linkedContact ${linkedContactId}`, + ); + // check if linkedContact has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedContactId, + provider_slug: integrationId, + }, + }); + if (!connection) return; + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.contact.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedContactId, + }, + }); + const job_id = job_resp_create.id_event; + + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedContactId, + 'contact', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: IContactService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = + await service.syncContacts(linkedContactId, remoteProperties); + + const sourceObject: OriginalContactOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: TicketingObject.contact, + providerName: integrationId, + customFieldMappings, + })) as UnifiedContactOutput[]; + + //TODO + const contactIds = sourceObject.map((contact) => + 'id' in contact ? String(contact.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const contact_data = await this.saveContactsInDb( + linkedContactId, + unifiedObject, + contactIds, + integrationId, + job_id, + sourceObject, + ); + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + await this.webhook.handleWebhook( + contact_data, + 'ticketing.contact.pulled', + id_project, + job_id, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveContactsInDb( + linkedContactId: string, + contacts: UnifiedContactOutput[], + originIds: string[], + originSource: string, + jobId: string, + remote_data: Record[], + ): Promise { + try { + let contacts_results: TicketingContact[] = []; + for (let i = 0; i < contacts.length; i++) { + const contact = contacts[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingContact = await this.prisma.tcg_contacts.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + events: { + id_linked_user: linkedContactId, + }, + }, + }); + + let unique_ticketing_contact_id: string; + + if (existingContact) { + // Update the existing ticket + const res = await this.prisma.tcg_contacts.update({ + where: { + id_tcg_contact: existingContact.id_tcg_contact, + }, + data: { + name: existingContact.name, + email_address: existingContact.email_address, + phone_number: existingContact.phone_number, + details: existingContact.details, + modified_at: new Date(), + }, + }); + unique_ticketing_contact_id = res.id_tcg_contact; + contacts_results = [...contacts_results, res]; + } else { + // Create a new contact + this.logger.log('not existing contact ' + contact.name); + const data = { + id_tcg_contact: uuidv4(), + name: contact.name, + email_address: contact.email_address, + phone_number: contact.phone_number, + details: contact.details, + created_at: new Date(), + modified_at: new Date(), + id_event: jobId, + remote_id: originId, + remote_platform: originSource, + }; + const res = await this.prisma.tcg_contacts.create({ + data: data, + }); + contacts_results = [...contacts_results, res]; + unique_ticketing_contact_id = res.id_tcg_contact; + } + + // check duplicate or existing values + if (contact.field_mappings && contact.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_ticketing_contact_id, + }, + }); + + for (const mapping of contact.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedContactId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_contact_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_contact_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return contacts_results; + } catch (error) { + handleServiceError(error, this.logger); + } } } diff --git a/packages/api/src/ticketing/contact/types/index.ts b/packages/api/src/ticketing/contact/types/index.ts index 0b5848f0c..332b645b5 100644 --- a/packages/api/src/ticketing/contact/types/index.ts +++ b/packages/api/src/ticketing/contact/types/index.ts @@ -5,11 +5,6 @@ import { ApiResponse } from '@@core/utils/types'; import { OriginalContactOutput } from '@@core/utils/types/original/original.crm'; export interface IContactService { - addContact( - contactData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - syncContacts( linkedUserId: string, custom_properties?: string[], diff --git a/packages/api/src/ticketing/contact/types/mappingsTypes.ts b/packages/api/src/ticketing/contact/types/mappingsTypes.ts index d08c20aaa..7bd112f89 100644 --- a/packages/api/src/ticketing/contact/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/contact/types/mappingsTypes.ts @@ -1 +1,22 @@ -// Content for mappingsTypes.ts +import { FrontContactMapper } from '../services/front/mappers'; +import { GithubContactMapper } from '../services/github/mappers'; +import { ZendeskContactMapper } from '../services/zendesk/mappers'; + +const zendeskContactMapper = new ZendeskContactMapper(); +const frontContactMapper = new FrontContactMapper(); +const githubContactMapper = new GithubContactMapper(); + +export const accountUnificationMapping = { + zendesk: { + unify: zendeskContactMapper.unify, + desunify: zendeskContactMapper.desunify, + }, + front: { + unify: frontContactMapper.unify, + desunify: frontContactMapper.desunify, + }, + github: { + unify: githubContactMapper.unify, + desunify: githubContactMapper.desunify, + }, +}; diff --git a/packages/api/src/ticketing/contact/types/model.unified.ts b/packages/api/src/ticketing/contact/types/model.unified.ts index ba42f6609..a59aa7cdb 100644 --- a/packages/api/src/ticketing/contact/types/model.unified.ts +++ b/packages/api/src/ticketing/contact/types/model.unified.ts @@ -1,3 +1,11 @@ -export class UnifiedContactInput {} +export class UnifiedContactInput { + name: string; + email_address: string; + phone_number?: string; + details?: string; + field_mappings?: Record[]; +} -export class UnifiedContactOutput extends UnifiedContactInput {} +export class UnifiedContactOutput extends UnifiedContactInput { + id: string; +} diff --git a/packages/api/src/ticketing/tag/services/front/index.ts b/packages/api/src/ticketing/tag/services/front/index.ts new file mode 100644 index 000000000..730023a82 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/front/index.ts @@ -0,0 +1,61 @@ +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 { FrontTagOutput } from './types'; + +@Injectable() +export class FrontService implements ITagService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.tag.toUpperCase() + ':' + FrontService.name, + ); + this.registry.registerService('front', this); + } + + async syncTags(linkedTagId: string): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedTagId, + provider_slug: 'front', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/teammates', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced front tags !`); + + return { + data: resp.data._results, + message: 'Front tags retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + TicketingObject.tag, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/tag/services/front/mappers.ts b/packages/api/src/ticketing/tag/services/front/mappers.ts new file mode 100644 index 000000000..965723b22 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/front/mappers.ts @@ -0,0 +1,43 @@ +import { ITagMapper } from '@ticketing/tag/types'; +import { FrontTagInput, FrontTagOutput } from './types'; +import { + UnifiedTagInput, + UnifiedTagOutput, +} from '@ticketing/tag/types/model.unified'; + +export class FrontTagMapper implements ITagMapper { + desunify( + source: UnifiedTagInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): FrontTagInput { + return; + } + + unify( + source: FrontTagOutput | FrontTagOutput[], + 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((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTicketToUnified( + ticket: FrontTagOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTagOutput { + return; + } +} diff --git a/packages/api/src/ticketing/tag/services/front/types.ts b/packages/api/src/ticketing/tag/services/front/types.ts new file mode 100644 index 000000000..38e26e603 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/front/types.ts @@ -0,0 +1,5 @@ +export type FrontTagInput = { + id: string; +}; + +export type FrontTagOutput = FrontTagInput; diff --git a/packages/api/src/ticketing/tag/services/github/index.ts b/packages/api/src/ticketing/tag/services/github/index.ts new file mode 100644 index 000000000..bb02c0a6d --- /dev/null +++ b/packages/api/src/ticketing/tag/services/github/index.ts @@ -0,0 +1,64 @@ +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 { GithubTagOutput } from './types'; + +//TODO +@Injectable() +export class GithubService implements ITagService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.tag.toUpperCase() + ':' + GithubService.name, + ); + this.registry.registerService('github', this); + } + + async syncTags( + linkedTagId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedTagId, + provider_slug: 'github', + }, + }); + const resp = await axios.get(`https://api.github.com/tags`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced github tags !`); + + return { + data: resp.data, + message: 'Github tags retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + TicketingObject.tag, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/tag/services/github/mappers.ts b/packages/api/src/ticketing/tag/services/github/mappers.ts new file mode 100644 index 000000000..bea26ee1a --- /dev/null +++ b/packages/api/src/ticketing/tag/services/github/mappers.ts @@ -0,0 +1,28 @@ +import { ITagMapper } from '@ticketing/tag/types'; +import { GithubTagInput, GithubTagOutput } from './types'; +import { + UnifiedTagInput, + UnifiedTagOutput, +} from '@ticketing/tag/types/model.unified'; + +export class GithubTagMapper implements ITagMapper { + desunify( + source: UnifiedTagInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GithubTagInput { + return; + } + + unify( + source: GithubTagOutput | GithubTagOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTagOutput | UnifiedTagOutput[] { + return; + } +} diff --git a/packages/api/src/ticketing/tag/services/github/types.ts b/packages/api/src/ticketing/tag/services/github/types.ts new file mode 100644 index 000000000..9598acae2 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/github/types.ts @@ -0,0 +1,6 @@ +export type GithubTagInput = { + name: string; +}; + +//TODO +export type GithubTagOutput = GithubTagInput; diff --git a/packages/api/src/ticketing/tag/services/registry.service.ts b/packages/api/src/ticketing/tag/services/registry.service.ts new file mode 100644 index 000000000..c68895086 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/registry.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { ITagService } from '../types'; + +@Injectable() +export class ServiceRegistry { + private serviceMap: Map; + + constructor() { + this.serviceMap = new Map(); + } + + registerService(serviceKey: string, service: ITagService) { + this.serviceMap.set(serviceKey, service); + } + + getService(integrationId: string): ITagService { + const service = this.serviceMap.get(integrationId); + if (!service) { + throw new Error(); + } + return service; + } +} diff --git a/packages/api/src/ticketing/tag/services/tag.service.ts b/packages/api/src/ticketing/tag/services/tag.service.ts new file mode 100644 index 000000000..6c1c99444 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/tag.service.ts @@ -0,0 +1,189 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { ApiResponse } from '@@core/utils/types'; +import { handleServiceError } from '@@core/utils/errors'; +import { UnifiedTagOutput } from '../types/model.unified'; +import { TagResponse } from '../types'; + +@Injectable() +export class TagService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(TagService.name); + } + + async getTag( + id_ticketing_tag: string, + remote_data?: boolean, + ): Promise> { + try { + const tag = await this.prisma.tcg_tags.findUnique({ + where: { + id_tcg_tag: id_ticketing_tag, + }, + }); + + // Fetch field mappings for the ticket + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: tag.id_tcg_tag, + }, + }, + include: { + attribute: true, + }, + }); + + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from(fieldMappingsMap, ([key, value]) => ({ + [key]: value, + })); + + // Transform to UnifiedTagOutput format + const unifiedTag: UnifiedTagOutput = { + id: tag.id_tcg_tag, + name: tag.name, + field_mappings: field_mappings, + }; + + let res: TagResponse = { + tags: [unifiedTag], + }; + + if (remote_data) { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: tag.id_tcg_tag, + }, + }); + const remote_data = JSON.parse(resp.data); + + res = { + ...res, + remote_data: [remote_data], + }; + } + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async getTags( + integrationId: string, + linkedTagId: string, + remote_data?: boolean, + ): Promise> { + try { + //TODO: handle case where data is not there (not synced) or old synced + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.tag.pull', + method: 'GET', + url: '/ticketing/tag', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedTagId, + }, + }); + const job_id = job_resp_create.id_event; + const tags = await this.prisma.tcg_tags.findMany({ + where: { + remote_id: integrationId.toLowerCase(), + events: { + id_linked_user: linkedTagId, + }, + }, + }); + + const unifiedTags: UnifiedTagOutput[] = await Promise.all( + tags.map(async (tag) => { + // Fetch field mappings for the tag + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: tag.id_tcg_tag, + }, + }, + include: { + attribute: true, + }, + }); + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from( + fieldMappingsMap, + ([key, value]) => ({ [key]: value }), + ); + + // Transform to UnifiedTagOutput format + return { + id: tag.id_tcg_tag, + name: tag.name, + field_mappings: field_mappings, + }; + }), + ); + + let res: TagResponse = { + tags: unifiedTags, + }; + + if (remote_data) { + const remote_array_data: Record[] = await Promise.all( + tags.map(async (tag) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: tag.id_tcg_tag, + }, + }); + const remote_data = JSON.parse(resp.data); + return remote_data; + }), + ); + + res = { + ...res, + remote_data: remote_array_data, + }; + } + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } +} diff --git a/packages/api/src/ticketing/tag/services/zendesk/index.ts b/packages/api/src/ticketing/tag/services/zendesk/index.ts new file mode 100644 index 000000000..cc43752df --- /dev/null +++ b/packages/api/src/ticketing/tag/services/zendesk/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, ZendeskTagOutput } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { ServiceRegistry } from '../registry.service'; +import { ITagService } from '@ticketing/tag/types'; + +@Injectable() +export class ZendeskService implements ITagService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.tag.toUpperCase() + ':' + ZendeskService.name, + ); + this.registry.registerService('zendesk_t', this); + } + + async syncTags( + linkedTagId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedTagId, + provider_slug: 'zendesk_t', + }, + }); + + const resp = await axios.get( + `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tags`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced zendesk tags !`); + + return { + data: resp.data.tags, + message: 'Zendesk tags retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zendesk', + TicketingObject.tag, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/tag/services/zendesk/mappers.ts b/packages/api/src/ticketing/tag/services/zendesk/mappers.ts new file mode 100644 index 000000000..40d81ea4d --- /dev/null +++ b/packages/api/src/ticketing/tag/services/zendesk/mappers.ts @@ -0,0 +1,43 @@ +import { ITagMapper } from '@ticketing/tag/types'; +import { ZendeskTagInput, ZendeskTagOutput } from './types'; +import { + UnifiedTagInput, + UnifiedTagOutput, +} from '@ticketing/tag/types/model.unified'; + +export class ZendeskTagMapper implements ITagMapper { + desunify( + source: UnifiedTagInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): ZendeskTagInput { + return; + } + + unify( + source: ZendeskTagOutput | ZendeskTagOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTagOutput | UnifiedTagOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleTagToUnified(source, customFieldMappings); + } + return source.map((ticket) => + this.mapSingleTagToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTagToUnified( + ticket: ZendeskTagOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTagOutput { + return; + } +} diff --git a/packages/api/src/ticketing/tag/services/zendesk/types.ts b/packages/api/src/ticketing/tag/services/zendesk/types.ts new file mode 100644 index 000000000..e8891b6d8 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/zendesk/types.ts @@ -0,0 +1,7 @@ +export type ZendeskTagInput = { + _: string; +}; + +export type ZendeskTagOutput = ZendeskTagInput & { + id: number; // Read-only. Automatically assigned when the ticket is created. +}; diff --git a/packages/api/src/ticketing/tag/sync/sync.service.ts b/packages/api/src/ticketing/tag/sync/sync.service.ts new file mode 100644 index 000000000..9d6f775c2 --- /dev/null +++ b/packages/api/src/ticketing/tag/sync/sync.service.ts @@ -0,0 +1,305 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; +import { Cron } from '@nestjs/schedule'; +import { ApiResponse, TICKETING_PROVIDERS } from '@@core/utils/types'; +import { v4 as uuidv4 } from 'uuid'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { ServiceRegistry } from '../services/registry.service'; +import { unify } from '@@core/utils/unification/unify'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { WebhookService } from '@@core/webhook/webhook.service'; +import { UnifiedTagOutput } from '../types/model.unified'; +import { ITagService } from '../types'; +import { OriginalTagOutput } from '@@core/utils/types/original/original.ticketing'; +import { tcg_tags as TicketingTag } from '@prisma/client'; + +@Injectable() +export class SyncService implements OnModuleInit { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private webhook: WebhookService, + private fieldMappingService: FieldMappingService, + private serviceRegistry: ServiceRegistry, + ) { + this.logger.setContext(SyncService.name); + } + + async onModuleInit() { + try { + await this.syncTags(); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + @Cron('*/20 * * * *') + //function used by sync worker which populate our tcg_tags table + //its role is to fetch all tags from providers 3rd parties and save the info inside our db + async syncTags() { + try { + this.logger.log(`Syncing tags....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedTags = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedTags.map(async (linkedTag) => { + try { + const providers = TICKETING_PROVIDERS; + for (const provider of providers) { + try { + await this.syncTagsForLinkedTag( + provider, + linkedTag.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncTagsForLinkedTag( + integrationId: string, + linkedTagId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} tags for linkedTag ${linkedTagId}`, + ); + // check if linkedTag has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedTagId, + provider_slug: integrationId, + }, + }); + if (!connection) return; + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.tag.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedTagId, + }, + }); + const job_id = job_resp_create.id_event; + + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedTagId, + 'tag', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: ITagService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = await service.syncTags( + linkedTagId, + remoteProperties, + ); + + const sourceObject: OriginalTagOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: TicketingObject.tag, + providerName: integrationId, + customFieldMappings, + })) as UnifiedTagOutput[]; + + //TODO + const tagIds = sourceObject.map((tag) => + 'id' in tag ? String(tag.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const tag_data = await this.saveTagsInDb( + linkedTagId, + unifiedObject, + tagIds, + integrationId, + job_id, + sourceObject, + ); + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + await this.webhook.handleWebhook( + tag_data, + 'ticketing.tag.pulled', + id_project, + job_id, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveTagsInDb( + linkedTagId: string, + tags: UnifiedTagOutput[], + originIds: string[], + originSource: string, + jobId: string, + remote_data: Record[], + ): Promise { + try { + let tags_results: TicketingTag[] = []; + for (let i = 0; i < tags.length; i++) { + const tag = tags[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingTag = await this.prisma.tcg_tags.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + events: { + id_linked_user: linkedTagId, + }, + }, + }); + + let unique_ticketing_tag_id: string; + + if (existingTag) { + // Update the existing ticket + const res = await this.prisma.tcg_tags.update({ + where: { + id_tcg_tag: existingTag.id_tcg_tag, + }, + data: { + name: existingTag.name, + modified_at: new Date(), + }, + }); + unique_ticketing_tag_id = res.id_tcg_tag; + tags_results = [...tags_results, res]; + } else { + // Create a new tag + this.logger.log('not existing tag ' + tag.name); + const data = { + id_tcg_tag: uuidv4(), + name: tag.name, + created_at: new Date(), + modified_at: new Date(), + id_event: jobId, + remote_id: originId, + remote_platform: originSource, + }; + const res = await this.prisma.tcg_tags.create({ + data: data, + }); + tags_results = [...tags_results, res]; + unique_ticketing_tag_id = res.id_tcg_tag; + } + + // check duplicate or existing values + if (tag.field_mappings && tag.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_ticketing_tag_id, + }, + }); + + for (const mapping of tag.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedTagId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_tag_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_tag_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return tags_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } +} diff --git a/packages/api/src/ticketing/tag/tag.controller.ts b/packages/api/src/ticketing/tag/tag.controller.ts new file mode 100644 index 000000000..2514b2b03 --- /dev/null +++ b/packages/api/src/ticketing/tag/tag.controller.ts @@ -0,0 +1,81 @@ +import { + Controller, + Post, + Body, + Query, + Get, + Patch, + Param, + Headers, +} from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { + ApiBody, + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, + ApiHeader, +} from '@nestjs/swagger'; +import { ApiCustomResponse } from '@@core/utils/types'; +import { TagService } from './services/tag.service'; +import { TagResponse } from './types'; +import { UnifiedTagInput } from './types/model.unified'; + +@ApiTags('ticketing/tag') +@Controller('ticketing/tag') +export class TagController { + constructor( + private readonly tagService: TagService, + private logger: LoggerService, + ) { + this.logger.setContext(TagController.name); + } + + @ApiOperation({ + operationId: 'getTags', + summary: 'List a batch of Tags', + }) + @ApiHeader({ name: 'integrationId', required: true }) + @ApiHeader({ name: 'linkedUserId', required: true }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(TagResponse) + @Get() + getTags( + @Headers('integrationId') integrationId: string, + @Headers('linkedUserId') linkedUserId: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.tagService.getTags(integrationId, linkedUserId, remote_data); + } + + @ApiOperation({ + operationId: 'getTag', + summary: 'Retrieve a Tag', + description: 'Retrieve a tag from any connected Ticketing software', + }) + @ApiParam({ + name: 'id', + required: true, + type: String, + description: 'id of the tag you want to retrieve.', + }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(TagResponse) + @Get(':id') + getTag(@Param('id') id: string, @Query('remoteData') remote_data?: boolean) { + return this.tagService.getTag(id, remote_data); + } +} diff --git a/packages/api/src/ticketing/tag/tag.module.ts b/packages/api/src/ticketing/tag/tag.module.ts new file mode 100644 index 000000000..03e41f0f9 --- /dev/null +++ b/packages/api/src/ticketing/tag/tag.module.ts @@ -0,0 +1,39 @@ +import { Module } from '@nestjs/common'; +import { TagController } from './tag.controller'; +import { SyncService } from './sync/sync.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { TagService } from './services/tag.service'; +import { ServiceRegistry } from './services/registry.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { WebhookService } from '@@core/webhook/webhook.service'; +import { BullModule } from '@nestjs/bull'; +import { FrontService } from './services/front'; +import { GithubService } from './services/github'; +import { ZendeskService } from './services/zendesk'; + +@Module({ + imports: [ + BullModule.registerQueue({ + name: 'webhookDelivery', + }), + ], + controllers: [TagController], + providers: [ + TagService, + PrismaService, + LoggerService, + SyncService, + WebhookService, + EncryptionService, + FieldMappingService, + ServiceRegistry, + /* PROVIDERS SERVICES */ + ZendeskService, + FrontService, + GithubService, + ], + exports: [SyncService], +}) +export class TagModule {} diff --git a/packages/api/src/ticketing/tag/types/index.ts b/packages/api/src/ticketing/tag/types/index.ts new file mode 100644 index 000000000..52abd91f0 --- /dev/null +++ b/packages/api/src/ticketing/tag/types/index.ts @@ -0,0 +1,38 @@ +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { UnifiedTagInput, UnifiedTagOutput } from './model.unified'; +import { OriginalTagOutput } from '@@core/utils/types/original/original.ticketing'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiResponse } from '@@core/utils/types'; + +export interface ITagService { + syncTags( + linkedUserId: string, + custom_properties?: string[], + ): Promise>; +} + +export interface ITagMapper { + desunify( + source: UnifiedTagInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): DesunifyReturnType; + + unify( + source: OriginalTagOutput | OriginalTagOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTagOutput | UnifiedTagOutput[]; +} + +export class TagResponse { + @ApiProperty({ type: [UnifiedTagOutput] }) + tags: UnifiedTagOutput[]; + + @ApiPropertyOptional({ type: [{}] }) + remote_data?: Record[]; // Data in original format +} diff --git a/packages/api/src/ticketing/tag/types/mappingsTypes.ts b/packages/api/src/ticketing/tag/types/mappingsTypes.ts new file mode 100644 index 000000000..010647ab5 --- /dev/null +++ b/packages/api/src/ticketing/tag/types/mappingsTypes.ts @@ -0,0 +1,22 @@ +import { FrontTagMapper } from '../services/front/mappers'; +import { GithubTagMapper } from '../services/github/mappers'; +import { ZendeskTagMapper } from '../services/zendesk/mappers'; + +const zendeskTagMapper = new ZendeskTagMapper(); +const frontTagMapper = new FrontTagMapper(); +const githubTagMapper = new GithubTagMapper(); + +export const tagUnificationMapping = { + zendesk: { + unify: zendeskTagMapper.unify, + desunify: zendeskTagMapper.desunify, + }, + front: { + unify: frontTagMapper.unify, + desunify: frontTagMapper.desunify, + }, + github: { + unify: githubTagMapper.unify, + desunify: githubTagMapper.desunify, + }, +}; diff --git a/packages/api/src/ticketing/tag/types/model.unified.ts b/packages/api/src/ticketing/tag/types/model.unified.ts new file mode 100644 index 000000000..5af56978f --- /dev/null +++ b/packages/api/src/ticketing/tag/types/model.unified.ts @@ -0,0 +1,8 @@ +export class UnifiedTagInput { + name: string; + field_mappings?: Record[]; +} + +export class UnifiedTagOutput extends UnifiedTagInput { + id: string; +} diff --git a/packages/api/src/ticketing/tag/utils/index.ts b/packages/api/src/ticketing/tag/utils/index.ts new file mode 100644 index 000000000..f849788c1 --- /dev/null +++ b/packages/api/src/ticketing/tag/utils/index.ts @@ -0,0 +1 @@ +/* PUT ALL UTILS FUNCTIONS USED IN YOUR OBJECT METHODS HERE */ diff --git a/packages/api/src/ticketing/team/services/front/index.ts b/packages/api/src/ticketing/team/services/front/index.ts new file mode 100644 index 000000000..23aa639e4 --- /dev/null +++ b/packages/api/src/ticketing/team/services/front/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 { FrontTeamOutput } from './types'; + +@Injectable() +export class FrontService implements ITeamService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.team.toUpperCase() + ':' + FrontService.name, + ); + this.registry.registerService('front', this); + } + + async syncTeams( + linkedTeamId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedTeamId, + provider_slug: 'front', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/teammates', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced front teams !`); + + return { + data: resp.data._results, + message: 'Front teams retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + TicketingObject.team, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/team/services/front/mappers.ts b/packages/api/src/ticketing/team/services/front/mappers.ts new file mode 100644 index 000000000..e3879bf8f --- /dev/null +++ b/packages/api/src/ticketing/team/services/front/mappers.ts @@ -0,0 +1,43 @@ +import { ITeamMapper } from '@ticketing/team/types'; +import { FrontTeamInput, FrontTeamOutput } from './types'; +import { + UnifiedTeamInput, + UnifiedTeamOutput, +} from '@ticketing/team/types/model.unified'; + +export class FrontTeamMapper implements ITeamMapper { + desunify( + source: UnifiedTeamInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): FrontTeamInput { + return; + } + + unify( + source: FrontTeamOutput | FrontTeamOutput[], + 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((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTicketToUnified( + ticket: FrontTeamOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput { + return; + } +} diff --git a/packages/api/src/ticketing/team/services/front/types.ts b/packages/api/src/ticketing/team/services/front/types.ts new file mode 100644 index 000000000..04b193f85 --- /dev/null +++ b/packages/api/src/ticketing/team/services/front/types.ts @@ -0,0 +1,5 @@ +export type FrontTeamInput = { + id: string; +}; + +export type FrontTeamOutput = FrontTeamInput; diff --git a/packages/api/src/ticketing/team/services/github/index.ts b/packages/api/src/ticketing/team/services/github/index.ts new file mode 100644 index 000000000..a1cc32f77 --- /dev/null +++ b/packages/api/src/ticketing/team/services/github/index.ts @@ -0,0 +1,64 @@ +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 { GithubTeamOutput } from './types'; + +//TODO +@Injectable() +export class GithubService implements ITeamService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.team.toUpperCase() + ':' + GithubService.name, + ); + this.registry.registerService('github', this); + } + + async syncTeams( + linkedTeamId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedTeamId, + provider_slug: 'github', + }, + }); + const resp = await axios.get(`https://api.github.com/teams`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced github teams !`); + + return { + data: resp.data, + message: 'Github teams retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + TicketingObject.team, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/team/services/github/mappers.ts b/packages/api/src/ticketing/team/services/github/mappers.ts new file mode 100644 index 000000000..69bd3ea70 --- /dev/null +++ b/packages/api/src/ticketing/team/services/github/mappers.ts @@ -0,0 +1,28 @@ +import { ITeamMapper } from '@ticketing/team/types'; +import { GithubTeamInput, GithubTeamOutput } from './types'; +import { + UnifiedTeamInput, + UnifiedTeamOutput, +} from '@ticketing/team/types/model.unified'; + +export class GithubTeamMapper implements ITeamMapper { + desunify( + source: UnifiedTeamInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GithubTeamInput { + return; + } + + unify( + source: GithubTeamOutput | GithubTeamOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput | UnifiedTeamOutput[] { + return; + } +} diff --git a/packages/api/src/ticketing/team/services/github/types.ts b/packages/api/src/ticketing/team/services/github/types.ts new file mode 100644 index 000000000..f92dda4f8 --- /dev/null +++ b/packages/api/src/ticketing/team/services/github/types.ts @@ -0,0 +1,6 @@ +export type GithubTeamInput = { + name: string; +}; + +//TODO +export type GithubTeamOutput = GithubTeamInput; diff --git a/packages/api/src/ticketing/team/services/registry.service.ts b/packages/api/src/ticketing/team/services/registry.service.ts new file mode 100644 index 000000000..b895071ae --- /dev/null +++ b/packages/api/src/ticketing/team/services/registry.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { ITeamService } from '../types'; + +@Injectable() +export class ServiceRegistry { + private serviceMap: Map; + + constructor() { + this.serviceMap = new Map(); + } + + registerService(serviceKey: string, service: ITeamService) { + this.serviceMap.set(serviceKey, service); + } + + getService(integrationId: string): ITeamService { + const service = this.serviceMap.get(integrationId); + if (!service) { + throw new Error(); + } + return service; + } +} diff --git a/packages/api/src/ticketing/team/services/team.service.ts b/packages/api/src/ticketing/team/services/team.service.ts new file mode 100644 index 000000000..32f966fa2 --- /dev/null +++ b/packages/api/src/ticketing/team/services/team.service.ts @@ -0,0 +1,191 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { ApiResponse } from '@@core/utils/types'; +import { handleServiceError } from '@@core/utils/errors'; +import { UnifiedTeamOutput } from '../types/model.unified'; +import { TeamResponse } from '../types'; + +@Injectable() +export class TeamService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(TeamService.name); + } + + async getTeam( + id_ticketing_team: string, + remote_data?: boolean, + ): Promise> { + try { + const team = await this.prisma.tcg_teams.findUnique({ + where: { + id_tcg_team: id_ticketing_team, + }, + }); + + // Fetch field mappings for the ticket + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: team.id_tcg_team, + }, + }, + include: { + attribute: true, + }, + }); + + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from(fieldMappingsMap, ([key, value]) => ({ + [key]: value, + })); + + // Transform to UnifiedTeamOutput format + const unifiedTeam: UnifiedTeamOutput = { + id: team.id_tcg_team, + name: team.name, + description: team.description, + field_mappings: field_mappings, + }; + + let res: TeamResponse = { + teams: [unifiedTeam], + }; + + if (remote_data) { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: team.id_tcg_team, + }, + }); + const remote_data = JSON.parse(resp.data); + + res = { + ...res, + remote_data: [remote_data], + }; + } + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async getTeams( + integrationId: string, + linkedTeamId: string, + remote_data?: boolean, + ): Promise> { + try { + //TODO: handle case where data is not there (not synced) or old synced + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.team.pull', + method: 'GET', + url: '/ticketing/team', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedTeamId, + }, + }); + const job_id = job_resp_create.id_event; + const teams = await this.prisma.tcg_teams.findMany({ + where: { + remote_id: integrationId.toLowerCase(), + events: { + id_linked_user: linkedTeamId, + }, + }, + }); + + const unifiedTeams: UnifiedTeamOutput[] = await Promise.all( + teams.map(async (team) => { + // Fetch field mappings for the team + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: team.id_tcg_team, + }, + }, + include: { + attribute: true, + }, + }); + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from( + fieldMappingsMap, + ([key, value]) => ({ [key]: value }), + ); + + // Transform to UnifiedTeamOutput format + return { + id: team.id_tcg_team, + name: team.name, + description: team.description, + field_mappings: field_mappings, + }; + }), + ); + + let res: TeamResponse = { + teams: unifiedTeams, + }; + + if (remote_data) { + const remote_array_data: Record[] = await Promise.all( + teams.map(async (team) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: team.id_tcg_team, + }, + }); + const remote_data = JSON.parse(resp.data); + return remote_data; + }), + ); + + res = { + ...res, + remote_data: remote_array_data, + }; + } + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } +} diff --git a/packages/api/src/ticketing/team/services/zendesk/index.ts b/packages/api/src/ticketing/team/services/zendesk/index.ts new file mode 100644 index 000000000..f9199edd9 --- /dev/null +++ b/packages/api/src/ticketing/team/services/zendesk/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, ZendeskTeamOutput } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { ServiceRegistry } from '../registry.service'; +import { ITeamService } from '@ticketing/team/types'; + +@Injectable() +export class ZendeskService implements ITeamService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.team.toUpperCase() + ':' + ZendeskService.name, + ); + this.registry.registerService('zendesk_t', this); + } + + async syncTeams( + linkedTeamId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedTeamId, + provider_slug: 'zendesk_t', + }, + }); + + const resp = await axios.get( + `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/teams`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced zendesk teams !`); + + return { + data: resp.data.teams, + message: 'Zendesk teams retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zendesk', + TicketingObject.team, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/team/services/zendesk/mappers.ts b/packages/api/src/ticketing/team/services/zendesk/mappers.ts new file mode 100644 index 000000000..83f8b1688 --- /dev/null +++ b/packages/api/src/ticketing/team/services/zendesk/mappers.ts @@ -0,0 +1,43 @@ +import { ITeamMapper } from '@ticketing/team/types'; +import { ZendeskTeamInput, ZendeskTeamOutput } from './types'; +import { + UnifiedTeamInput, + UnifiedTeamOutput, +} from '@ticketing/team/types/model.unified'; + +export class ZendeskTeamMapper implements ITeamMapper { + desunify( + source: UnifiedTeamInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): ZendeskTeamInput { + return; + } + + unify( + source: ZendeskTeamOutput | ZendeskTeamOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput | UnifiedTeamOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleTeamToUnified(source, customFieldMappings); + } + return source.map((ticket) => + this.mapSingleTeamToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTeamToUnified( + ticket: ZendeskTeamOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput { + return; + } +} diff --git a/packages/api/src/ticketing/team/services/zendesk/types.ts b/packages/api/src/ticketing/team/services/zendesk/types.ts new file mode 100644 index 000000000..2a62d1e26 --- /dev/null +++ b/packages/api/src/ticketing/team/services/zendesk/types.ts @@ -0,0 +1,7 @@ +export type ZendeskTeamInput = { + _: string; +}; + +export type ZendeskTeamOutput = ZendeskTeamInput & { + id: number; // Read-only. Automatically assigned when the ticket is created. +}; diff --git a/packages/api/src/ticketing/team/sync/sync.service.ts b/packages/api/src/ticketing/team/sync/sync.service.ts new file mode 100644 index 000000000..1b74d7123 --- /dev/null +++ b/packages/api/src/ticketing/team/sync/sync.service.ts @@ -0,0 +1,307 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; +import { Cron } from '@nestjs/schedule'; +import { ApiResponse, TICKETING_PROVIDERS } from '@@core/utils/types'; +import { v4 as uuidv4 } from 'uuid'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { ServiceRegistry } from '../services/registry.service'; +import { unify } from '@@core/utils/unification/unify'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { WebhookService } from '@@core/webhook/webhook.service'; +import { UnifiedTeamOutput } from '../types/model.unified'; +import { ITeamService } from '../types'; +import { tcg_teams as TicketingTeam } from '@prisma/client'; +import { OriginalTeamOutput } from '@@core/utils/types/original/original.ticketing'; + +@Injectable() +export class SyncService implements OnModuleInit { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private webhook: WebhookService, + private fieldMappingService: FieldMappingService, + private serviceRegistry: ServiceRegistry, + ) { + this.logger.setContext(SyncService.name); + } + + async onModuleInit() { + try { + await this.syncTeams(); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + @Cron('*/20 * * * *') + //function used by sync worker which populate our tcg_teams table + //its role is to fetch all teams from providers 3rd parties and save the info inside our db + async syncTeams() { + try { + this.logger.log(`Syncing teams....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedTeams = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedTeams.map(async (linkedTeam) => { + try { + const providers = TICKETING_PROVIDERS; + for (const provider of providers) { + try { + await this.syncTeamsForLinkedTeam( + provider, + linkedTeam.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncTeamsForLinkedTeam( + integrationId: string, + linkedTeamId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} teams for linkedTeam ${linkedTeamId}`, + ); + // check if linkedTeam has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedTeamId, + provider_slug: integrationId, + }, + }); + if (!connection) return; + const job_resp_create = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.team.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedTeamId, + }, + }); + const job_id = job_resp_create.id_event; + + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedTeamId, + 'team', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: ITeamService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = await service.syncTeams( + linkedTeamId, + remoteProperties, + ); + + const sourceObject: OriginalTeamOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: TicketingObject.team, + providerName: integrationId, + customFieldMappings, + })) as UnifiedTeamOutput[]; + + //TODO + const teamIds = sourceObject.map((team) => + 'id' in team ? String(team.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const team_data = await this.saveTeamsInDb( + linkedTeamId, + unifiedObject, + teamIds, + integrationId, + job_id, + sourceObject, + ); + await this.prisma.events.update({ + where: { + id_event: job_id, + }, + data: { + status: 'success', + }, + }); + await this.webhook.handleWebhook( + team_data, + 'ticketing.team.pulled', + id_project, + job_id, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveTeamsInDb( + linkedTeamId: string, + teams: UnifiedTeamOutput[], + originIds: string[], + originSource: string, + jobId: string, + remote_data: Record[], + ): Promise { + try { + let teams_results: TicketingTeam[] = []; + for (let i = 0; i < teams.length; i++) { + const team = teams[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingTeam = await this.prisma.tcg_teams.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + events: { + id_linked_user: linkedTeamId, + }, + }, + }); + + let unique_ticketing_team_id: string; + + if (existingTeam) { + // Update the existing ticket + const res = await this.prisma.tcg_teams.update({ + where: { + id_tcg_team: existingTeam.id_tcg_team, + }, + data: { + name: existingTeam.name, + description: team.description, + modified_at: new Date(), + }, + }); + unique_ticketing_team_id = res.id_tcg_team; + teams_results = [...teams_results, res]; + } else { + // Create a new team + this.logger.log('not existing team ' + team.name); + const data = { + id_tcg_team: uuidv4(), + name: team.name, + description: team.description, + created_at: new Date(), + modified_at: new Date(), + id_event: jobId, + remote_id: originId, + remote_platform: originSource, + }; + const res = await this.prisma.tcg_teams.create({ + data: data, + }); + teams_results = [...teams_results, res]; + unique_ticketing_team_id = res.id_tcg_team; + } + + // check duplicate or existing values + if (team.field_mappings && team.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_ticketing_team_id, + }, + }); + + for (const mapping of team.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedTeamId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_team_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_team_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return teams_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } +} diff --git a/packages/api/src/ticketing/team/team.controller.ts b/packages/api/src/ticketing/team/team.controller.ts new file mode 100644 index 000000000..30c75ee59 --- /dev/null +++ b/packages/api/src/ticketing/team/team.controller.ts @@ -0,0 +1,69 @@ +import { Controller, Query, Get, Param, Headers } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, + ApiHeader, +} from '@nestjs/swagger'; +import { ApiCustomResponse } from '@@core/utils/types'; +import { TeamService } from './services/team.service'; + +@ApiTags('ticketing/team') +@Controller('ticketing/team') +export class TeamController { + constructor( + private readonly teamService: TeamService, + private logger: LoggerService, + ) { + this.logger.setContext(TeamController.name); + } + + @ApiOperation({ + operationId: 'getTeams', + summary: 'List a batch of Teams', + }) + @ApiHeader({ name: 'integrationId', required: true }) + @ApiHeader({ name: 'linkedUserId', required: true }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(TeamResponse) + @Get() + getTeams( + @Headers('integrationId') integrationId: string, + @Headers('linkedUserId') linkedUserId: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.teamService.getTeams(integrationId, linkedUserId, remote_data); + } + + @ApiOperation({ + operationId: 'getTeam', + summary: 'Retrieve a Team', + description: 'Retrieve a team from any connected Ticketing software', + }) + @ApiParam({ + name: 'id', + required: true, + type: String, + description: 'id of the team you want to retrieve.', + }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(TeamResponse) + @Get(':id') + getTeam(@Param('id') id: string, @Query('remoteData') remote_data?: boolean) { + return this.teamService.getTeam(id, remote_data); + } +} diff --git a/packages/api/src/ticketing/team/team.module.ts b/packages/api/src/ticketing/team/team.module.ts new file mode 100644 index 000000000..5aada8f6e --- /dev/null +++ b/packages/api/src/ticketing/team/team.module.ts @@ -0,0 +1,39 @@ +import { Module } from '@nestjs/common'; +import { TeamController } from './team.controller'; +import { SyncService } from './sync/sync.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { TeamService } from './services/team.service'; +import { ServiceRegistry } from './services/registry.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { WebhookService } from '@@core/webhook/webhook.service'; +import { BullModule } from '@nestjs/bull'; +import { FrontService } from './services/front'; +import { GithubService } from './services/github'; +import { ZendeskService } from './services/zendesk'; + +@Module({ + imports: [ + BullModule.registerQueue({ + name: 'webhookDelivery', + }), + ], + controllers: [TeamController], + providers: [ + TeamService, + PrismaService, + LoggerService, + SyncService, + WebhookService, + EncryptionService, + FieldMappingService, + ServiceRegistry, + /* PROVIDERS SERVICES */ + ZendeskService, + FrontService, + GithubService, + ], + exports: [SyncService], +}) +export class TeamModule {} diff --git a/packages/api/src/ticketing/team/types/index.ts b/packages/api/src/ticketing/team/types/index.ts new file mode 100644 index 000000000..c96093e61 --- /dev/null +++ b/packages/api/src/ticketing/team/types/index.ts @@ -0,0 +1,38 @@ +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { UnifiedTeamInput, UnifiedTeamOutput } from './model.unified'; +import { OriginalTeamOutput } from '@@core/utils/types/original/original.ticketing'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiResponse } from '@@core/utils/types'; + +export interface ITeamService { + syncTeams( + linkedUserId: string, + custom_properties?: string[], + ): Promise>; +} + +export interface ITeamMapper { + desunify( + source: UnifiedTeamInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): DesunifyReturnType; + + unify( + source: OriginalTeamOutput | OriginalTeamOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput | UnifiedTeamOutput[]; +} + +export class TeamResponse { + @ApiProperty({ type: [UnifiedTeamOutput] }) + teams: UnifiedTeamOutput[]; + + @ApiPropertyOptional({ type: [{}] }) + remote_data?: Record[]; // Data in original format +} diff --git a/packages/api/src/ticketing/team/types/mappingsTypes.ts b/packages/api/src/ticketing/team/types/mappingsTypes.ts new file mode 100644 index 000000000..06240c4f8 --- /dev/null +++ b/packages/api/src/ticketing/team/types/mappingsTypes.ts @@ -0,0 +1,22 @@ +import { FrontTeamMapper } from '../services/front/mappers'; +import { GithubTeamMapper } from '../services/github/mappers'; +import { ZendeskTeamMapper } from '../services/zendesk/mappers'; + +const zendeskTeamMapper = new ZendeskTeamMapper(); +const frontTeamMapper = new FrontTeamMapper(); +const githubTeamMapper = new GithubTeamMapper(); + +export const teamUnificationMapping = { + zendesk: { + unify: zendeskTeamMapper.unify, + desunify: zendeskTeamMapper.desunify, + }, + front: { + unify: frontTeamMapper.unify, + desunify: frontTeamMapper.desunify, + }, + github: { + unify: githubTeamMapper.unify, + desunify: githubTeamMapper.desunify, + }, +}; diff --git a/packages/api/src/ticketing/team/types/model.unified.ts b/packages/api/src/ticketing/team/types/model.unified.ts new file mode 100644 index 000000000..9745c704c --- /dev/null +++ b/packages/api/src/ticketing/team/types/model.unified.ts @@ -0,0 +1,9 @@ +export class UnifiedTeamInput { + name: string; + description?: string; + field_mappings?: Record[]; +} + +export class UnifiedTeamOutput extends UnifiedTeamInput { + id: string; +} diff --git a/packages/api/src/ticketing/team/utils/index.ts b/packages/api/src/ticketing/team/utils/index.ts new file mode 100644 index 000000000..f849788c1 --- /dev/null +++ b/packages/api/src/ticketing/team/utils/index.ts @@ -0,0 +1 @@ +/* PUT ALL UTILS FUNCTIONS USED IN YOUR OBJECT METHODS HERE */ diff --git a/packages/api/src/ticketing/ticketing.module.ts b/packages/api/src/ticketing/ticketing.module.ts index 4c6bedb63..c293ff4aa 100644 --- a/packages/api/src/ticketing/ticketing.module.ts +++ b/packages/api/src/ticketing/ticketing.module.ts @@ -5,6 +5,8 @@ import { UserModule } from './user/user.module'; import { AttachmentModule } from './attachment/attachment.module'; import { ContactModule } from './contact/contact.module'; import { AccountModule } from './account/account.module'; +import { TagModule } from './tag/tag.module'; +import { TeamModule } from './team/team.module'; @Module({ imports: [ @@ -14,12 +16,12 @@ import { AccountModule } from './account/account.module'; AttachmentModule, ContactModule, AccountModule, + TagModule, + TeamModule, ], providers: [], controllers: [], - exports: [ TicketModule, - CommentModule, - UserModule, + exports: [ UserModule, AttachmentModule, ContactModule, ], From 369c6029925966720710c70bed73c71a2c4acc31 Mon Sep 17 00:00:00 2001 From: nael Date: Fri, 5 Jan 2024 01:04:19 +0100 Subject: [PATCH 09/25] :construction: WIP on ticketing objects --- packages/api/prisma/schema.prisma | 65 ++++++++++------- .../types/original/original.ticketing.ts | 24 ++++++- .../account/services/account.service.ts | 12 ++-- .../ticketing/account/services/front/index.ts | 4 +- .../account/services/github/index.ts | 4 +- .../account/services/zendesk/index.ts | 4 +- .../ticketing/account/sync/sync.service.ts | 20 +++--- .../ticketing/attachment/attachment.module.ts | 4 ++ .../attachment/services/front/index.ts | 70 +++++++++++++++++++ .../attachment/services/front/mappers.ts | 43 ++++++++++++ .../attachment/services/front/types.ts | 5 ++ .../attachment/services/github/index.ts | 69 ++++++++++++++++++ .../attachment/services/github/mappers.ts | 28 ++++++++ .../attachment/services/github/types.ts | 6 ++ .../attachment/services/zendesk/index.ts | 63 +++++++++++++---- .../attachment/services/zendesk/mappers.ts | 43 ++++++++++++ .../attachment/services/zendesk/types.ts | 6 +- .../contact/services/contact.service.ts | 6 +- .../ticketing/contact/services/front/index.ts | 4 +- .../contact/services/github/index.ts | 4 +- .../contact/services/zendesk/index.ts | 4 +- .../ticketing/contact/sync/sync.service.ts | 20 +++--- .../api/src/ticketing/contact/types/index.ts | 2 +- .../src/ticketing/tag/services/front/index.ts | 4 +- .../ticketing/tag/services/github/index.ts | 4 +- .../src/ticketing/tag/services/tag.service.ts | 6 +- .../ticketing/tag/services/zendesk/index.ts | 4 +- .../src/ticketing/tag/sync/sync.service.ts | 20 +++--- .../ticketing/team/services/front/index.ts | 4 +- .../ticketing/team/services/github/index.ts | 4 +- .../ticketing/team/services/team.service.ts | 6 +- .../ticketing/team/services/zendesk/index.ts | 4 +- .../src/ticketing/team/sync/sync.service.ts | 20 +++--- 33 files changed, 461 insertions(+), 125 deletions(-) create mode 100644 packages/api/src/ticketing/attachment/services/front/index.ts create mode 100644 packages/api/src/ticketing/attachment/services/front/mappers.ts create mode 100644 packages/api/src/ticketing/attachment/services/front/types.ts create mode 100644 packages/api/src/ticketing/attachment/services/github/index.ts create mode 100644 packages/api/src/ticketing/attachment/services/github/mappers.ts create mode 100644 packages/api/src/ticketing/attachment/services/github/types.ts diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 2b4c55f5e..e8a309ac8 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -383,10 +383,10 @@ model tcg_comments { body String? html_body String? is_private Boolean? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) remote_id String? remote_platform String? + created_at DateTime? @db.Timestamp(6) + modified_at DateTime? @db.Timestamp(6) creator_type String? id_tcg_ticket String? @db.Uuid id_tcg_contact String? @db.Uuid @@ -409,10 +409,10 @@ model tcg_contacts { email_address String? phone_number String? details String? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) remote_id String? remote_platform String? + created_at DateTime? @db.Timestamp(6) + modified_at DateTime? @db.Timestamp(6) id_event String? @db.Uuid id_tcg_account String? @db.Uuid tcg_comments tcg_comments[] @@ -548,37 +548,48 @@ model webhooks_reponses { /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model tcg_accounts { - id_tcg_account String @db.Uuid - created_at DateTime? @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - remote_id String? - name String? - domains String[] - - @@ignore + id_tcg_account String @id(map: "pk_tcg_account") @db.Uuid + remote_id String? + name String? + domains String[] + remote_platform String? + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) } /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. model tcg_tags { - id_tcg_tag String @db.Uuid - remote_id String? - name String? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) - id_tcg_ticket String? @db.Uuid + id_tcg_tag String @id(map: "pk_tcg_tags") @db.Uuid + name String? + remote_id String? + remote_platform String? + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) + id_tcg_ticket String? @db.Uuid @@index([id_tcg_ticket], map: "fk_tcg_tag_tcg_ticketid") - @@ignore } /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. model tcg_teams { - id_tcg_team String @db.Uuid - remote_id String? - name String? - description String? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) - - @@ignore + id_tcg_team String @id(map: "pk_tcg_teams") @db.Uuid + remote_id String? + remote_platform String? + name String? + description String? + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model tcg_attachments { + id_tcg_attachment String @id(map: "pk_tcg_attachments") @db.Uuid + remote_id String? + remote_platform String? + file_name String? + id_tcg_ticket String? @db.Uuid + file_url String? + uploader String @db.Uuid + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) } 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 5578365dd..032d5bcaa 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -24,6 +24,14 @@ import { GithubAccountInput, GithubAccountOutput, } from '@ticketing/account/services/github/types'; +import { + FrontAttachmentInput, + FrontAttachmentOutput, +} from '@ticketing/attachment/services/front/types'; +import { + GithubAttachmentInput, + GithubAttachmentOutput, +} from '@ticketing/attachment/services/github/types'; import { FrontCommentInput, FrontCommentOutput, @@ -121,13 +129,20 @@ export type OriginalTeamInput = | FrontTeamInput; /* attachment */ -export type OriginalAttachmentInput = ZendeskAttachmentInput; +export type OriginalAttachmentInput = + | ZendeskAttachmentInput + | FrontAttachmentInput + | GithubAttachmentInput; export type TicketingObjectInput = | OriginalTicketInput | OriginalCommentInput | OriginalUserInput - | OriginalAttachmentInput; + | OriginalAttachmentInput + | OriginalTagInput + | OriginalTeamInput + | OriginalContactInput + | OriginalAccountInput; /* OUTPUT */ @@ -172,7 +187,10 @@ export type OriginalTeamOutput = | FrontTeamOutput; /* attachment */ -export type OriginalAttachmentOutput = ZendeskAttachmentOutput; +export type OriginalAttachmentOutput = + | ZendeskAttachmentOutput + | FrontAttachmentOutput + | GithubAttachmentOutput; export type TicketingObjectOutput = | OriginalTicketOutput diff --git a/packages/api/src/ticketing/account/services/account.service.ts b/packages/api/src/ticketing/account/services/account.service.ts index 4b60f5f06..d4c529c94 100644 --- a/packages/api/src/ticketing/account/services/account.service.ts +++ b/packages/api/src/ticketing/account/services/account.service.ts @@ -51,9 +51,8 @@ export class AccountService { // Transform to UnifiedAccountOutput format const unifiedAccount: UnifiedAccountOutput = { id: account.id_tcg_account, - email_address: account.email_address, name: account.name, - teams: account.teams, + domains: account.domains, field_mappings: field_mappings, }; @@ -86,7 +85,7 @@ export class AccountService { async getAccounts( integrationId: string, - linkedAccountId: string, + linkedUserId: string, remote_data?: boolean, ): Promise> { try { @@ -101,7 +100,7 @@ export class AccountService { provider: integrationId, direction: '0', timestamp: new Date(), - id_linked_user: linkedAccountId, + id_linked_user: linkedUserId, }, }); const job_id = job_resp_create.id_event; @@ -109,7 +108,7 @@ export class AccountService { where: { remote_id: integrationId.toLowerCase(), events: { - id_linked_user: linkedAccountId, + id_linked_user: linkedUserId, }, }, }); @@ -143,9 +142,8 @@ export class AccountService { // Transform to UnifiedAccountOutput format return { id: account.id_tcg_account, - email_address: account.email_address, name: account.name, - teams: account.teams, + domains: account.domains, field_mappings: field_mappings, }; }), diff --git a/packages/api/src/ticketing/account/services/front/index.ts b/packages/api/src/ticketing/account/services/front/index.ts index 279a64e66..15ab6f74a 100644 --- a/packages/api/src/ticketing/account/services/front/index.ts +++ b/packages/api/src/ticketing/account/services/front/index.ts @@ -25,12 +25,12 @@ export class FrontService implements IAccountService { } async syncAccounts( - linkedAccountId: string, + linkedUserId: string, ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedAccountId, + id_linked_user: linkedUserId, provider_slug: 'front', }, }); diff --git a/packages/api/src/ticketing/account/services/github/index.ts b/packages/api/src/ticketing/account/services/github/index.ts index eb138c149..f9164f231 100644 --- a/packages/api/src/ticketing/account/services/github/index.ts +++ b/packages/api/src/ticketing/account/services/github/index.ts @@ -26,13 +26,13 @@ export class GithubService implements IAccountService { } async syncAccounts( - linkedAccountId: string, + linkedUserId: string, custom_properties?: string[], ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedAccountId, + id_linked_user: linkedUserId, provider_slug: 'github', }, }); diff --git a/packages/api/src/ticketing/account/services/zendesk/index.ts b/packages/api/src/ticketing/account/services/zendesk/index.ts index e968dd06a..9f5908ef0 100644 --- a/packages/api/src/ticketing/account/services/zendesk/index.ts +++ b/packages/api/src/ticketing/account/services/zendesk/index.ts @@ -29,13 +29,13 @@ export class ZendeskService implements IAccountService { } async syncAccounts( - linkedAccountId: string, + linkedUserId: string, custom_properties?: string[], ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedAccountId, + id_linked_user: linkedUserId, provider_slug: 'zendesk_t', }, }); diff --git a/packages/api/src/ticketing/account/sync/sync.service.ts b/packages/api/src/ticketing/account/sync/sync.service.ts index 747e15b5b..e543f0378 100644 --- a/packages/api/src/ticketing/account/sync/sync.service.ts +++ b/packages/api/src/ticketing/account/sync/sync.service.ts @@ -85,17 +85,17 @@ export class SyncService implements OnModuleInit { //todo: HANDLE DATA REMOVED FROM PROVIDER async syncAccountsForLinkedAccount( integrationId: string, - linkedAccountId: string, + linkedUserId: string, id_project: string, ) { try { this.logger.log( - `Syncing ${integrationId} accounts for linkedAccount ${linkedAccountId}`, + `Syncing ${integrationId} accounts for linkedAccount ${linkedUserId}`, ); // check if linkedAccount has a connection if not just stop sync const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedAccountId, + id_linked_user: linkedUserId, provider_slug: integrationId, }, }); @@ -110,7 +110,7 @@ export class SyncService implements OnModuleInit { provider: integrationId, direction: '0', timestamp: new Date(), - id_linked_user: linkedAccountId, + id_linked_user: linkedUserId, }, }); const job_id = job_resp_create.id_event; @@ -119,7 +119,7 @@ export class SyncService implements OnModuleInit { const customFieldMappings = await this.fieldMappingService.getCustomFieldMappings( integrationId, - linkedAccountId, + linkedUserId, 'account', ); const remoteProperties: string[] = customFieldMappings.map( @@ -129,7 +129,7 @@ export class SyncService implements OnModuleInit { const service: IAccountService = this.serviceRegistry.getService(integrationId); const resp: ApiResponse = - await service.syncAccounts(linkedAccountId, remoteProperties); + await service.syncAccounts(linkedUserId, remoteProperties); const sourceObject: OriginalAccountOutput[] = resp.data; //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); @@ -148,7 +148,7 @@ export class SyncService implements OnModuleInit { //insert the data in the DB with the fieldMappings (value table) const account_data = await this.saveAccountsInDb( - linkedAccountId, + linkedUserId, unifiedObject, accountIds, integrationId, @@ -175,7 +175,7 @@ export class SyncService implements OnModuleInit { } async saveAccountsInDb( - linkedAccountId: string, + linkedUserId: string, accounts: UnifiedAccountOutput[], originIds: string[], originSource: string, @@ -197,7 +197,7 @@ export class SyncService implements OnModuleInit { remote_id: originId, remote_platform: originSource, events: { - id_linked_account: linkedAccountId, + id_linked_account: linkedUserId, }, }, }); @@ -252,7 +252,7 @@ export class SyncService implements OnModuleInit { where: { slug: Object.keys(mapping)[0], source: originSource, - id_consumer: linkedAccountId, + id_consumer: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/attachment/attachment.module.ts b/packages/api/src/ticketing/attachment/attachment.module.ts index 5cdf80a80..e28533cea 100644 --- a/packages/api/src/ticketing/attachment/attachment.module.ts +++ b/packages/api/src/ticketing/attachment/attachment.module.ts @@ -10,6 +10,8 @@ import { FieldMappingService } from '@@core/field-mapping/field-mapping.service' import { PrismaService } from '@@core/prisma/prisma.service'; import { WebhookService } from '@@core/webhook/webhook.service'; import { BullModule } from '@nestjs/bull'; +import { GithubService } from './services/github'; +import { FrontService } from './services/front'; @Module({ imports: [ @@ -29,6 +31,8 @@ import { BullModule } from '@nestjs/bull'; ServiceRegistry, /* PROVIDERS SERVICES */ ZendeskService, + FrontService, + GithubService, ], exports: [SyncService], }) diff --git a/packages/api/src/ticketing/attachment/services/front/index.ts b/packages/api/src/ticketing/attachment/services/front/index.ts new file mode 100644 index 000000000..0714ce137 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/front/index.ts @@ -0,0 +1,70 @@ +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 { IAttachmentService } from '@ticketing/attachment/types'; +import { FrontAttachmentInput, FrontAttachmentOutput } from './types'; + +@Injectable() +export class FrontService implements IAttachmentService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.attachment.toUpperCase() + ':' + FrontService.name, + ); + this.registry.registerService('front', this); + } + + async addAttachment( + attachmentData: FrontAttachmentInput, + linkedUserId: string, + ): Promise> { + return; + } + + async syncAttachments( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'front', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/teammates', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced front attachments !`); + + return { + data: resp.data._results, + message: 'Front attachments retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Front', + TicketingObject.attachment, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/attachment/services/front/mappers.ts b/packages/api/src/ticketing/attachment/services/front/mappers.ts new file mode 100644 index 000000000..164cd3ef4 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/front/mappers.ts @@ -0,0 +1,43 @@ +import { IAttachmentMapper } from '@ticketing/attachment/types'; +import { FrontAttachmentInput, FrontAttachmentOutput } from './types'; +import { + UnifiedAttachmentInput, + UnifiedAttachmentOutput, +} from '@ticketing/attachment/types/model.unified'; + +export class FrontAttachmentMapper implements IAttachmentMapper { + desunify( + source: UnifiedAttachmentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): FrontAttachmentInput { + return; + } + + unify( + source: FrontAttachmentOutput | FrontAttachmentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput | UnifiedAttachmentOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleTicketToUnified( + ticket: FrontAttachmentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput { + return; + } +} diff --git a/packages/api/src/ticketing/attachment/services/front/types.ts b/packages/api/src/ticketing/attachment/services/front/types.ts new file mode 100644 index 000000000..6bdd606aa --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/front/types.ts @@ -0,0 +1,5 @@ +export type FrontAttachmentInput = { + id: string; +}; + +export type FrontAttachmentOutput = FrontAttachmentInput; diff --git a/packages/api/src/ticketing/attachment/services/github/index.ts b/packages/api/src/ticketing/attachment/services/github/index.ts new file mode 100644 index 000000000..63e842989 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/github/index.ts @@ -0,0 +1,69 @@ +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 { IAttachmentService } from '@ticketing/attachment/types'; +import { GithubAttachmentInput, GithubAttachmentOutput } from './types'; + +//TODO +@Injectable() +export class GithubService implements IAttachmentService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.attachment.toUpperCase() + ':' + GithubService.name, + ); + this.registry.registerService('github', this); + } + async addAttachment( + attachmentData: GithubAttachmentInput, + linkedUserId: string, + ): Promise> { + return; + } + async syncAttachments( + linkedUserId: string, + custom_properties?: string[], + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'github', + }, + }); + const resp = await axios.get(`https://api.github.com/attachments`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced github attachments !`); + + return { + data: resp.data, + message: 'Github attachments retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Github', + TicketingObject.attachment, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/attachment/services/github/mappers.ts b/packages/api/src/ticketing/attachment/services/github/mappers.ts new file mode 100644 index 000000000..e90772e67 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/github/mappers.ts @@ -0,0 +1,28 @@ +import { IAttachmentMapper } from '@ticketing/attachment/types'; +import { GithubAttachmentInput, GithubAttachmentOutput } from './types'; +import { + UnifiedAttachmentInput, + UnifiedAttachmentOutput, +} from '@ticketing/attachment/types/model.unified'; + +export class GithubAttachmentMapper implements IAttachmentMapper { + desunify( + source: UnifiedAttachmentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): GithubAttachmentInput { + return; + } + + unify( + source: GithubAttachmentOutput | GithubAttachmentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput | UnifiedAttachmentOutput[] { + return; + } +} diff --git a/packages/api/src/ticketing/attachment/services/github/types.ts b/packages/api/src/ticketing/attachment/services/github/types.ts new file mode 100644 index 000000000..690d2fb75 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/github/types.ts @@ -0,0 +1,6 @@ +export type GithubAttachmentInput = { + name: string; +}; + +//TODO +export type GithubAttachmentOutput = GithubAttachmentInput; diff --git a/packages/api/src/ticketing/attachment/services/zendesk/index.ts b/packages/api/src/ticketing/attachment/services/zendesk/index.ts index 2dbf562fc..af9af88b4 100644 --- a/packages/api/src/ticketing/attachment/services/zendesk/index.ts +++ b/packages/api/src/ticketing/attachment/services/zendesk/index.ts @@ -1,16 +1,20 @@ -import { EncryptionService } from '@@core/encryption/encryption.service'; -import { EnvironmentService } from '@@core/environment/environment.service'; +import { Injectable } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; -import { ApiResponse } from '@@core/utils/types'; -import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { Injectable } from '@nestjs/common'; +import { EncryptionService } from '@@core/encryption/encryption.service'; import { TicketingObject, ZendeskAttachmentInput, + ZendeskAttachmentOutput, } from '@ticketing/@utils/@types'; -import { IAttachmentService } from '@ticketing/attachment/types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { EnvironmentService } from '@@core/environment/environment.service'; import { ServiceRegistry } from '../registry.service'; +import { IAttachmentService } from '@ticketing/attachment/types'; +import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; @Injectable() export class ZendeskService implements IAttachmentService { @@ -26,16 +30,51 @@ export class ZendeskService implements IAttachmentService { ); this.registry.registerService('zendesk_t', this); } + addAttachment( - attachmentData: DesunifyReturnType, + attachmentData: ZendeskAttachmentInput, linkedUserId: string, - ): Promise> { - throw new Error('Method not implemented.'); + ): Promise> { + return; } - syncAttachments( + async syncAttachments( linkedUserId: string, custom_properties?: string[], - ): Promise> { - throw new Error('Method not implemented.'); + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'zendesk_t', + }, + }); + + const resp = await axios.get( + `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/attachments`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced zendesk attachments !`); + + return { + data: resp.data.attachments, + message: 'Zendesk attachments retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zendesk', + TicketingObject.attachment, + ActionType.GET, + ); + } } } diff --git a/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts b/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts index e69de29bb..8e9e402b8 100644 --- a/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts @@ -0,0 +1,43 @@ +import { IAttachmentMapper } from '@ticketing/attachment/types'; +import { ZendeskAttachmentInput, ZendeskAttachmentOutput } from './types'; +import { + UnifiedAttachmentInput, + UnifiedAttachmentOutput, +} from '@ticketing/attachment/types/model.unified'; + +export class ZendeskAttachmentMapper implements IAttachmentMapper { + desunify( + source: UnifiedAttachmentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): ZendeskAttachmentInput { + return; + } + + unify( + source: ZendeskAttachmentOutput | ZendeskAttachmentOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput | UnifiedAttachmentOutput[] { + if (!Array.isArray(source)) { + return this.mapSingleAttachmentToUnified(source, customFieldMappings); + } + return source.map((ticket) => + this.mapSingleAttachmentToUnified(ticket, customFieldMappings), + ); + } + + private mapSingleAttachmentToUnified( + ticket: ZendeskAttachmentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput { + return; + } +} diff --git a/packages/api/src/ticketing/attachment/services/zendesk/types.ts b/packages/api/src/ticketing/attachment/services/zendesk/types.ts index 417c82d83..97083238d 100644 --- a/packages/api/src/ticketing/attachment/services/zendesk/types.ts +++ b/packages/api/src/ticketing/attachment/services/zendesk/types.ts @@ -1,5 +1,7 @@ export type ZendeskAttachmentInput = { - id: string; + _: string; }; -export type ZendeskAttachmentOutput = ZendeskAttachmentInput; +export type ZendeskAttachmentOutput = ZendeskAttachmentInput & { + id: number; // Read-only. Automatically assigned when the ticket is created. +}; diff --git a/packages/api/src/ticketing/contact/services/contact.service.ts b/packages/api/src/ticketing/contact/services/contact.service.ts index b10dce39c..374b861bf 100644 --- a/packages/api/src/ticketing/contact/services/contact.service.ts +++ b/packages/api/src/ticketing/contact/services/contact.service.ts @@ -87,7 +87,7 @@ export class ContactService { async getContacts( integrationId: string, - linkedContactId: string, + linkedUserId: string, remote_data?: boolean, ): Promise> { try { @@ -102,7 +102,7 @@ export class ContactService { provider: integrationId, direction: '0', timestamp: new Date(), - id_linked_user: linkedContactId, + id_linked_user: linkedUserId, }, }); const job_id = job_resp_create.id_event; @@ -110,7 +110,7 @@ export class ContactService { where: { remote_id: integrationId.toLowerCase(), events: { - id_linked_user: linkedContactId, + id_linked_user: linkedUserId, }, }, }); diff --git a/packages/api/src/ticketing/contact/services/front/index.ts b/packages/api/src/ticketing/contact/services/front/index.ts index 65c60e3fb..3fb172356 100644 --- a/packages/api/src/ticketing/contact/services/front/index.ts +++ b/packages/api/src/ticketing/contact/services/front/index.ts @@ -25,12 +25,12 @@ export class FrontService implements IContactService { } async syncContacts( - linkedContactId: string, + linkedUserId: string, ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedContactId, + id_linked_user: linkedUserId, provider_slug: 'front', }, }); diff --git a/packages/api/src/ticketing/contact/services/github/index.ts b/packages/api/src/ticketing/contact/services/github/index.ts index baf87f328..54dff8476 100644 --- a/packages/api/src/ticketing/contact/services/github/index.ts +++ b/packages/api/src/ticketing/contact/services/github/index.ts @@ -26,13 +26,13 @@ export class GithubService implements IContactService { } async syncContacts( - linkedContactId: string, + linkedUserId: string, custom_properties?: string[], ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedContactId, + id_linked_user: linkedUserId, provider_slug: 'github', }, }); diff --git a/packages/api/src/ticketing/contact/services/zendesk/index.ts b/packages/api/src/ticketing/contact/services/zendesk/index.ts index b2001713f..06e4c2581 100644 --- a/packages/api/src/ticketing/contact/services/zendesk/index.ts +++ b/packages/api/src/ticketing/contact/services/zendesk/index.ts @@ -29,13 +29,13 @@ export class ZendeskService implements IContactService { } async syncContacts( - linkedContactId: string, + linkedUserId: string, custom_properties?: string[], ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedContactId, + id_linked_user: linkedUserId, provider_slug: 'zendesk_t', }, }); diff --git a/packages/api/src/ticketing/contact/sync/sync.service.ts b/packages/api/src/ticketing/contact/sync/sync.service.ts index 0e2bdbb09..3d78975f8 100644 --- a/packages/api/src/ticketing/contact/sync/sync.service.ts +++ b/packages/api/src/ticketing/contact/sync/sync.service.ts @@ -85,17 +85,17 @@ export class SyncService implements OnModuleInit { //todo: HANDLE DATA REMOVED FROM PROVIDER async syncContactsForLinkedContact( integrationId: string, - linkedContactId: string, + linkedUserId: string, id_project: string, ) { try { this.logger.log( - `Syncing ${integrationId} contacts for linkedContact ${linkedContactId}`, + `Syncing ${integrationId} contacts for linkedContact ${linkedUserId}`, ); // check if linkedContact has a connection if not just stop sync const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedContactId, + id_linked_user: linkedUserId, provider_slug: integrationId, }, }); @@ -110,7 +110,7 @@ export class SyncService implements OnModuleInit { provider: integrationId, direction: '0', timestamp: new Date(), - id_linked_user: linkedContactId, + id_linked_user: linkedUserId, }, }); const job_id = job_resp_create.id_event; @@ -119,7 +119,7 @@ export class SyncService implements OnModuleInit { const customFieldMappings = await this.fieldMappingService.getCustomFieldMappings( integrationId, - linkedContactId, + linkedUserId, 'contact', ); const remoteProperties: string[] = customFieldMappings.map( @@ -129,7 +129,7 @@ export class SyncService implements OnModuleInit { const service: IContactService = this.serviceRegistry.getService(integrationId); const resp: ApiResponse = - await service.syncContacts(linkedContactId, remoteProperties); + await service.syncContacts(linkedUserId, remoteProperties); const sourceObject: OriginalContactOutput[] = resp.data; //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); @@ -148,7 +148,7 @@ export class SyncService implements OnModuleInit { //insert the data in the DB with the fieldMappings (value table) const contact_data = await this.saveContactsInDb( - linkedContactId, + linkedUserId, unifiedObject, contactIds, integrationId, @@ -175,7 +175,7 @@ export class SyncService implements OnModuleInit { } async saveContactsInDb( - linkedContactId: string, + linkedUserId: string, contacts: UnifiedContactOutput[], originIds: string[], originSource: string, @@ -197,7 +197,7 @@ export class SyncService implements OnModuleInit { remote_id: originId, remote_platform: originSource, events: { - id_linked_user: linkedContactId, + id_linked_user: linkedUserId, }, }, }); @@ -256,7 +256,7 @@ export class SyncService implements OnModuleInit { where: { slug: Object.keys(mapping)[0], source: originSource, - id_consumer: linkedContactId, + id_consumer: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/contact/types/index.ts b/packages/api/src/ticketing/contact/types/index.ts index 332b645b5..05de6b4ac 100644 --- a/packages/api/src/ticketing/contact/types/index.ts +++ b/packages/api/src/ticketing/contact/types/index.ts @@ -2,7 +2,7 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; import { UnifiedContactInput, UnifiedContactOutput } from './model.unified'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiResponse } from '@@core/utils/types'; -import { OriginalContactOutput } from '@@core/utils/types/original/original.crm'; +import { OriginalContactOutput } from '@@core/utils/types/original/original.ticketing'; export interface IContactService { syncContacts( diff --git a/packages/api/src/ticketing/tag/services/front/index.ts b/packages/api/src/ticketing/tag/services/front/index.ts index 730023a82..33bd415a2 100644 --- a/packages/api/src/ticketing/tag/services/front/index.ts +++ b/packages/api/src/ticketing/tag/services/front/index.ts @@ -24,11 +24,11 @@ export class FrontService implements ITagService { this.registry.registerService('front', this); } - async syncTags(linkedTagId: string): Promise> { + async syncTags(linkedUserId: string): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedTagId, + id_linked_user: linkedUserId, provider_slug: 'front', }, }); diff --git a/packages/api/src/ticketing/tag/services/github/index.ts b/packages/api/src/ticketing/tag/services/github/index.ts index bb02c0a6d..030b03358 100644 --- a/packages/api/src/ticketing/tag/services/github/index.ts +++ b/packages/api/src/ticketing/tag/services/github/index.ts @@ -26,13 +26,13 @@ export class GithubService implements ITagService { } async syncTags( - linkedTagId: string, + linkedUserId: string, custom_properties?: string[], ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedTagId, + id_linked_user: linkedUserId, provider_slug: 'github', }, }); diff --git a/packages/api/src/ticketing/tag/services/tag.service.ts b/packages/api/src/ticketing/tag/services/tag.service.ts index 6c1c99444..ba4bc4438 100644 --- a/packages/api/src/ticketing/tag/services/tag.service.ts +++ b/packages/api/src/ticketing/tag/services/tag.service.ts @@ -84,7 +84,7 @@ export class TagService { async getTags( integrationId: string, - linkedTagId: string, + linkedUserId: string, remote_data?: boolean, ): Promise> { try { @@ -99,7 +99,7 @@ export class TagService { provider: integrationId, direction: '0', timestamp: new Date(), - id_linked_user: linkedTagId, + id_linked_user: linkedUserId, }, }); const job_id = job_resp_create.id_event; @@ -107,7 +107,7 @@ export class TagService { where: { remote_id: integrationId.toLowerCase(), events: { - id_linked_user: linkedTagId, + id_linked_user: linkedUserId, }, }, }); diff --git a/packages/api/src/ticketing/tag/services/zendesk/index.ts b/packages/api/src/ticketing/tag/services/zendesk/index.ts index cc43752df..5e2d83dc9 100644 --- a/packages/api/src/ticketing/tag/services/zendesk/index.ts +++ b/packages/api/src/ticketing/tag/services/zendesk/index.ts @@ -26,13 +26,13 @@ export class ZendeskService implements ITagService { } async syncTags( - linkedTagId: string, + linkedUserId: string, custom_properties?: string[], ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedTagId, + id_linked_user: linkedUserId, provider_slug: 'zendesk_t', }, }); diff --git a/packages/api/src/ticketing/tag/sync/sync.service.ts b/packages/api/src/ticketing/tag/sync/sync.service.ts index 9d6f775c2..3d40e23e2 100644 --- a/packages/api/src/ticketing/tag/sync/sync.service.ts +++ b/packages/api/src/ticketing/tag/sync/sync.service.ts @@ -85,17 +85,17 @@ export class SyncService implements OnModuleInit { //todo: HANDLE DATA REMOVED FROM PROVIDER async syncTagsForLinkedTag( integrationId: string, - linkedTagId: string, + linkedUserId: string, id_project: string, ) { try { this.logger.log( - `Syncing ${integrationId} tags for linkedTag ${linkedTagId}`, + `Syncing ${integrationId} tags for linkedTag ${linkedUserId}`, ); // check if linkedTag has a connection if not just stop sync const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedTagId, + id_linked_user: linkedUserId, provider_slug: integrationId, }, }); @@ -110,7 +110,7 @@ export class SyncService implements OnModuleInit { provider: integrationId, direction: '0', timestamp: new Date(), - id_linked_user: linkedTagId, + id_linked_user: linkedUserId, }, }); const job_id = job_resp_create.id_event; @@ -119,7 +119,7 @@ export class SyncService implements OnModuleInit { const customFieldMappings = await this.fieldMappingService.getCustomFieldMappings( integrationId, - linkedTagId, + linkedUserId, 'tag', ); const remoteProperties: string[] = customFieldMappings.map( @@ -129,7 +129,7 @@ export class SyncService implements OnModuleInit { const service: ITagService = this.serviceRegistry.getService(integrationId); const resp: ApiResponse = await service.syncTags( - linkedTagId, + linkedUserId, remoteProperties, ); @@ -150,7 +150,7 @@ export class SyncService implements OnModuleInit { //insert the data in the DB with the fieldMappings (value table) const tag_data = await this.saveTagsInDb( - linkedTagId, + linkedUserId, unifiedObject, tagIds, integrationId, @@ -177,7 +177,7 @@ export class SyncService implements OnModuleInit { } async saveTagsInDb( - linkedTagId: string, + linkedUserId: string, tags: UnifiedTagOutput[], originIds: string[], originSource: string, @@ -199,7 +199,7 @@ export class SyncService implements OnModuleInit { remote_id: originId, remote_platform: originSource, events: { - id_linked_user: linkedTagId, + id_linked_user: linkedUserId, }, }, }); @@ -252,7 +252,7 @@ export class SyncService implements OnModuleInit { where: { slug: Object.keys(mapping)[0], source: originSource, - id_consumer: linkedTagId, + id_consumer: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/team/services/front/index.ts b/packages/api/src/ticketing/team/services/front/index.ts index 23aa639e4..a5913cc11 100644 --- a/packages/api/src/ticketing/team/services/front/index.ts +++ b/packages/api/src/ticketing/team/services/front/index.ts @@ -25,12 +25,12 @@ export class FrontService implements ITeamService { } async syncTeams( - linkedTeamId: string, + linkedUserId: string, ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedTeamId, + id_linked_user: linkedUserId, provider_slug: 'front', }, }); diff --git a/packages/api/src/ticketing/team/services/github/index.ts b/packages/api/src/ticketing/team/services/github/index.ts index a1cc32f77..620741056 100644 --- a/packages/api/src/ticketing/team/services/github/index.ts +++ b/packages/api/src/ticketing/team/services/github/index.ts @@ -26,13 +26,13 @@ export class GithubService implements ITeamService { } async syncTeams( - linkedTeamId: string, + linkedUserId: string, custom_properties?: string[], ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedTeamId, + id_linked_user: linkedUserId, provider_slug: 'github', }, }); diff --git a/packages/api/src/ticketing/team/services/team.service.ts b/packages/api/src/ticketing/team/services/team.service.ts index 32f966fa2..af79317d0 100644 --- a/packages/api/src/ticketing/team/services/team.service.ts +++ b/packages/api/src/ticketing/team/services/team.service.ts @@ -85,7 +85,7 @@ export class TeamService { async getTeams( integrationId: string, - linkedTeamId: string, + linkedUserId: string, remote_data?: boolean, ): Promise> { try { @@ -100,7 +100,7 @@ export class TeamService { provider: integrationId, direction: '0', timestamp: new Date(), - id_linked_user: linkedTeamId, + id_linked_user: linkedUserId, }, }); const job_id = job_resp_create.id_event; @@ -108,7 +108,7 @@ export class TeamService { where: { remote_id: integrationId.toLowerCase(), events: { - id_linked_user: linkedTeamId, + id_linked_user: linkedUserId, }, }, }); diff --git a/packages/api/src/ticketing/team/services/zendesk/index.ts b/packages/api/src/ticketing/team/services/zendesk/index.ts index f9199edd9..de82ebf62 100644 --- a/packages/api/src/ticketing/team/services/zendesk/index.ts +++ b/packages/api/src/ticketing/team/services/zendesk/index.ts @@ -26,13 +26,13 @@ export class ZendeskService implements ITeamService { } async syncTeams( - linkedTeamId: string, + linkedUserId: string, custom_properties?: string[], ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedTeamId, + id_linked_user: linkedUserId, provider_slug: 'zendesk_t', }, }); diff --git a/packages/api/src/ticketing/team/sync/sync.service.ts b/packages/api/src/ticketing/team/sync/sync.service.ts index 1b74d7123..414956acf 100644 --- a/packages/api/src/ticketing/team/sync/sync.service.ts +++ b/packages/api/src/ticketing/team/sync/sync.service.ts @@ -85,17 +85,17 @@ export class SyncService implements OnModuleInit { //todo: HANDLE DATA REMOVED FROM PROVIDER async syncTeamsForLinkedTeam( integrationId: string, - linkedTeamId: string, + linkedUserId: string, id_project: string, ) { try { this.logger.log( - `Syncing ${integrationId} teams for linkedTeam ${linkedTeamId}`, + `Syncing ${integrationId} teams for linkedTeam ${linkedUserId}`, ); // check if linkedTeam has a connection if not just stop sync const connection = await this.prisma.connections.findFirst({ where: { - id_linked_user: linkedTeamId, + id_linked_user: linkedUserId, provider_slug: integrationId, }, }); @@ -110,7 +110,7 @@ export class SyncService implements OnModuleInit { provider: integrationId, direction: '0', timestamp: new Date(), - id_linked_user: linkedTeamId, + id_linked_user: linkedUserId, }, }); const job_id = job_resp_create.id_event; @@ -119,7 +119,7 @@ export class SyncService implements OnModuleInit { const customFieldMappings = await this.fieldMappingService.getCustomFieldMappings( integrationId, - linkedTeamId, + linkedUserId, 'team', ); const remoteProperties: string[] = customFieldMappings.map( @@ -129,7 +129,7 @@ export class SyncService implements OnModuleInit { const service: ITeamService = this.serviceRegistry.getService(integrationId); const resp: ApiResponse = await service.syncTeams( - linkedTeamId, + linkedUserId, remoteProperties, ); @@ -150,7 +150,7 @@ export class SyncService implements OnModuleInit { //insert the data in the DB with the fieldMappings (value table) const team_data = await this.saveTeamsInDb( - linkedTeamId, + linkedUserId, unifiedObject, teamIds, integrationId, @@ -177,7 +177,7 @@ export class SyncService implements OnModuleInit { } async saveTeamsInDb( - linkedTeamId: string, + linkedUserId: string, teams: UnifiedTeamOutput[], originIds: string[], originSource: string, @@ -199,7 +199,7 @@ export class SyncService implements OnModuleInit { remote_id: originId, remote_platform: originSource, events: { - id_linked_user: linkedTeamId, + id_linked_user: linkedUserId, }, }, }); @@ -254,7 +254,7 @@ export class SyncService implements OnModuleInit { where: { slug: Object.keys(mapping)[0], source: originSource, - id_consumer: linkedTeamId, + id_consumer: linkedUserId, }, }); From 13ef887ed05cf88dfea39e7bd8a91773a2ab0a01 Mon Sep 17 00:00:00 2001 From: nael Date: Fri, 5 Jan 2024 16:26:40 +0100 Subject: [PATCH 10/25] :construction: Updated ticket object --- packages/api/src/@core/utils/errors.ts | 1 + .../attachment/attachment.controller.ts | 186 +++++++++++++++++- .../attachment/services/attachment.service.ts | 7 + .../attachment/types/model.unified.ts | 4 + .../ticketing/comment/types/model.unified.ts | 4 +- .../ticketing/contact/contact.controller.ts | 69 +++++-- .../ticket/services/front/mappers.ts | 13 +- .../ticket/services/hubspot/mappers.ts | 6 +- .../ticket/services/ticket.service.ts | 106 +++++----- .../ticket/services/zendesk/mappers.ts | 20 +- .../src/ticketing/ticket/sync/sync.service.ts | 41 ++-- .../ticketing/ticket/types/model.unified.ts | 5 +- .../api/src/ticketing/ticket/utils/index.ts | 56 ++++++ 13 files changed, 415 insertions(+), 103 deletions(-) diff --git a/packages/api/src/@core/utils/errors.ts b/packages/api/src/@core/utils/errors.ts index 22052e6c5..bd7868373 100644 --- a/packages/api/src/@core/utils/errors.ts +++ b/packages/api/src/@core/utils/errors.ts @@ -4,6 +4,7 @@ import axios, { AxiosError } from 'axios'; import { Prisma } from '@prisma/client'; import { TargetObject } from './types'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import { PinoLogger } from 'nestjs-pino'; type ServiceError = AxiosError | PrismaClientKnownRequestError | Error; diff --git a/packages/api/src/ticketing/attachment/attachment.controller.ts b/packages/api/src/ticketing/attachment/attachment.controller.ts index 6b16a04ef..2b19ef2ab 100644 --- a/packages/api/src/ticketing/attachment/attachment.controller.ts +++ b/packages/api/src/ticketing/attachment/attachment.controller.ts @@ -1,4 +1,184 @@ -import { Controller } from '@nestjs/common'; +import { + Controller, + Post, + Body, + Query, + Get, + Param, + Headers, +} from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { + ApiBody, + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, + ApiHeader, +} from '@nestjs/swagger'; +import { ApiCustomResponse } from '@@core/utils/types'; +import { AttachmentService } from './services/attachment.service'; +import { UnifiedAttachmentInput } from './types/model.unified'; -@Controller('attachment') -export class AttachmentController {} +@ApiTags('ticketing/attachment') +@Controller('ticketing/attachment') +export class AttachmentController { + constructor( + private readonly attachmentService: AttachmentService, + private logger: LoggerService, + ) { + this.logger.setContext(AttachmentController.name); + } + + @ApiOperation({ + operationId: 'getAttachments', + summary: 'List a batch of Attachments', + }) + @ApiHeader({ name: 'integrationId', required: true }) + @ApiHeader({ name: 'linkedUserId', required: true }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(AttachmentResponse) + @Get() + getAttachments( + @Headers('integrationId') integrationId: string, + @Headers('linkedUserId') linkedUserId: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.attachmentService.getAttachments( + integrationId, + linkedUserId, + remote_data, + ); + } + + @ApiOperation({ + operationId: 'getAttachment', + summary: 'Retrieve a Attachment', + description: 'Retrieve a attachment from any connected Ticketing software', + }) + @ApiParam({ + name: 'id', + required: true, + type: String, + description: 'id of the attachment you want to retrive.', + }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(AttachmentResponse) + @Get(':id') + getAttachment( + @Param('id') id: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.attachmentService.getAttachment(id, remote_data); + } + + @ApiOperation({ + operationId: 'downloadAttachment', + summary: 'Download a Attachment', + description: 'Download a attachment from any connected Ticketing software', + }) + @ApiParam({ + name: 'id', + required: true, + type: String, + description: 'id of the attachment you want to retrive.', + }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(AttachmentResponse) + @Get(':id/download') + downloadAttachment( + @Param('id') id: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.attachmentService.downloadAttachment(id, remote_data); + } + + @ApiOperation({ + operationId: 'addAttachment', + summary: 'Create a Attachment', + description: 'Create a attachment in any supported Ticketing software', + }) + @ApiHeader({ + name: 'integrationId', + required: true, + description: 'The integration ID', + example: '6aa2acf3-c244-4f85-848b-13a57e7abf55', + }) + @ApiHeader({ + name: 'linkedUserId', + required: true, + description: 'The linked user ID', + example: 'b008e199-eda9-4629-bd41-a01b6195864a', + }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + @ApiBody({ type: UnifiedAttachmentInput }) + //@ApiCustomResponse(AttachmentResponse) + @Post() + addAttachment( + @Body() unfiedAttachmentData: UnifiedAttachmentInput, + @Headers('integrationId') integrationId: string, + @Headers('linkedUserId') linkedUserId: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.attachmentService.addAttachment( + unfiedAttachmentData, + integrationId, + linkedUserId, + remote_data, + ); + } + + @ApiOperation({ + operationId: 'addAttachments', + summary: 'Add a batch of Attachments', + }) + @ApiHeader({ name: 'integrationId', required: true }) + @ApiHeader({ name: 'linkedUserId', required: true }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + @ApiBody({ type: UnifiedAttachmentInput, isArray: true }) + //@ApiCustomResponse(AttachmentResponse) + @Post('batch') + addAttachments( + @Body() unfiedAttachmentData: UnifiedAttachmentInput[], + @Headers('integrationId') integrationId: string, + @Headers('linkedUserId') linkedUserId: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.attachmentService.batchAddAttachments( + unfiedAttachmentData, + integrationId, + linkedUserId, + remote_data, + ); + } +} diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index e1bf59e09..3d2ad0ba0 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -436,4 +436,11 @@ export class AttachmentService { handleServiceError(error, this.logger); } } + + async downloadAttachmentt( + id_ticketing_attachment: string, + remote_data?: boolean, + ): Promise> { + return; + } } diff --git a/packages/api/src/ticketing/attachment/types/model.unified.ts b/packages/api/src/ticketing/attachment/types/model.unified.ts index 0151979d3..90785cde2 100644 --- a/packages/api/src/ticketing/attachment/types/model.unified.ts +++ b/packages/api/src/ticketing/attachment/types/model.unified.ts @@ -1,4 +1,8 @@ export class UnifiedAttachmentInput { + file_name: string; + ticket_id?: string; + file_url: string; + uploader?: string; field_mappings?: Record[]; } diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index 1439fa56d..52c4d6625 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -1,4 +1,5 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; +import { UnifiedAttachmentInput } from '@ticketing/attachment/types/model.unified'; export class UnifiedCommentInput { body: string; @@ -7,9 +8,10 @@ export class UnifiedCommentInput { created_at: Date; modified_at: Date; creator_type: 'user' | 'contact' | null; - ticket_id: string; + ticket_id?: string; contact_id?: string; user_id?: string; + attachments?: string[]; //uuids of Attachments objects } //TODO: add remote_id diff --git a/packages/api/src/ticketing/contact/contact.controller.ts b/packages/api/src/ticketing/contact/contact.controller.ts index a1a8c3389..9db580672 100644 --- a/packages/api/src/ticketing/contact/contact.controller.ts +++ b/packages/api/src/ticketing/contact/contact.controller.ts @@ -1,24 +1,13 @@ -import { - Controller, - Post, - Body, - Query, - Get, - Patch, - Param, - Headers, -} from '@nestjs/common'; +import { Controller, Query, Get, Param, Headers } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { - ApiBody, ApiOperation, ApiParam, ApiQuery, - ApiTags, ApiHeader, + ApiTags, } from '@nestjs/swagger'; import { ContactService } from './services/contact.service'; -import { UnifiedContactInput } from './types/model.unified'; @ApiTags('ticketing/contact') @Controller('ticketing/contact') @@ -29,4 +18,58 @@ export class ContactController { ) { this.logger.setContext(ContactController.name); } + + @ApiOperation({ + operationId: 'getContacts', + summary: 'List a batch of Contacts', + }) + @ApiHeader({ name: 'integrationId', required: true }) + @ApiHeader({ name: 'linkedUserId', required: true }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(ContactResponse) + @Get() + getContacts( + @Headers('integrationId') integrationId: string, + @Headers('linkedUserId') linkedUserId: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.contactService.getContacts( + integrationId, + linkedUserId, + remote_data, + ); + } + + @ApiOperation({ + operationId: 'getContact', + summary: 'Retrieve a Contact', + description: 'Retrieve a contact from any connected Ticketing software', + }) + @ApiParam({ + name: 'id', + required: true, + type: String, + description: 'id of the contact you want to retrieve.', + }) + @ApiQuery({ + name: 'remoteData', + required: false, + type: Boolean, + description: + 'Set to true to include data from the original Ticketing software.', + }) + //@ApiCustomResponse(ContactResponse) + @Get(':id') + getContact( + @Param('id') id: string, + @Query('remoteData') remote_data?: boolean, + ) { + return this.contactService.getContact(id, remote_data); + } } diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index e2dc6c75b..8cd400dbd 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -4,15 +4,18 @@ import { UnifiedTicketInput, UnifiedTicketOutput, } from '@ticketing/ticket/types/model.unified'; +import { Utils } from '@ticketing/ticket/utils'; export class FrontTicketMapper implements ITicketMapper { - desunify( + private readonly utils = new Utils(); + + async desunify( source: UnifiedTicketInput, customFieldMappings?: { slug: string; remote_id: string; }[], - ): FrontTicketInput { + ): Promise { const result: FrontTicketInput = { type: 'discussion', // Assuming 'discussion' as a default type for Front conversations subject: source.name, @@ -23,7 +26,11 @@ export class FrontTicketMapper implements ITicketMapper { source.comment.creator_type === 'user' ? source.comment.user_id : source.comment.contact_id, - attachments: source.attachments, + attachments: source.comment.attachments + ? await this.utils.get_Front_AttachmentsFromUuid( + source.comment.attachments, + ) + : [], }, }; diff --git a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts index e0e8cd8a6..04a33eb46 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts @@ -65,16 +65,16 @@ export class HubspotTicketMapper implements ITicketMapper { return { id: ticket.id, - name: ticket.properties.subject, + name: ticket.properties.name, //TODO status: ticket.properties.hs_pipeline_stage, - description: ticket.properties.subject, + description: ticket.properties.description, //TODO due_date: new Date(ticket.properties.createdate), type: ticket.properties.hs_pipeline, parent_ticket: '', // Define how you determine the parent ticket tags: '', // Define how you map or store tags completed_at: new Date(ticket.properties.hs_lastmodifieddate), priority: ticket.properties.hs_ticket_priority, - assigned_to: [], // Define how you determine assigned users + assigned_to: [ticket.properties.hubspot_owner_id], // Define how you determine assigned users field_mappings: field_mappings, }; } diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index 6d5ed894a..5a9f8e1ab 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -77,22 +77,44 @@ export class TicketService { id_linked_user: linkedUserId, }, }); - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.ticket.created', //sync, push or pull - method: 'POST', - url: '/ticketing/ticket', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + //CHECKS + if (!linkedUser) throw new Error('Linked User Not Found'); + const acc = unifiedTicketData.account_id; + //check if contact_id and account_id refer to real uuids + if (acc) { + const search = await this.prisma.tcg_accounts.findUnique({ + where: { + id_tcg_account: acc, + }, + }); + if (!search) + throw new Error('You inserted an account_id which does not exist'); + } - //TODO + const contact = unifiedTicketData.contact_id; + //check if contact_id and account_id refer to real uuids + if (contact) { + const search = await this.prisma.tcg_contacts.findUnique({ + where: { + id_tcg_contact: contact, + }, + }); + if (!search) + throw new Error('You inserted a contact_id which does not exist'); + } + const assignees = unifiedTicketData.assigned_to; + //CHEK IF assigned_to contains valid Users uuids + if (assignees && assignees.length > 0) { + assignees.map(async (assignee) => { + const search = await this.prisma.tcg_users.findUnique({ + where: { + id_tcg_user: assignee, + }, + }); + if (!search) + throw new Error('You inserted an assignee which does not exist'); + }); + } // Retrieve custom field mappings // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -136,7 +158,7 @@ export class TicketService { where: { remote_id: originId, remote_platform: integrationId, - events: { + linked_users: { id_linked_user: linkedUserId, }, }, @@ -182,7 +204,7 @@ export class TicketService { assigned_to: target_ticket.assigned_to || [], created_at: new Date(), modified_at: new Date(), - id_event: job_id, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: integrationId, }; @@ -261,19 +283,24 @@ export class TicketService { ); const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: status_resp, + type: 'ticketing.ticket.push', //sync, push or pull + method: 'POST', + url: '/ticketing/ticket', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( result_ticket.data.tickets, 'ticketing.ticket.created', linkedUser.id_project, - job_id, + event.id_event, ); return { ...resp, data: result_ticket.data }; } catch (error) { @@ -366,20 +393,6 @@ export class TicketService { ): Promise> { try { //TODO: handle case where data is not there (not synced) or old synced - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.ticket.pull', - method: 'GET', - url: '/ticketing/ticket', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; const tickets = await this.prisma.tcg_tickets.findMany({ where: { remote_id: integrationId.toLowerCase(), @@ -422,7 +435,6 @@ export class TicketService { return { id: ticket.id_tcg_ticket, name: ticket.name || '', - //TODO: remote_id: ticket.remote_id || '', status: ticket.status || '', description: ticket.description || '', due_date: ticket.due_date || null, @@ -432,12 +444,6 @@ export class TicketService { completed_at: ticket.completed_at || null, priority: ticket.priority || '', assigned_to: ticket.assigned_to || [], - comments: ticket.tcg_comments.map((comment) => ({ - remote_id: comment.remote_id, - body: comment.body, - html_body: comment.html_body, - is_private: comment.is_private, - })), field_mappings: field_mappings, }; }), @@ -465,12 +471,18 @@ export class TicketService { remote_data: remote_array_data, }; } - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.ticket.pulled', + method: 'GET', + url: '/ticketing/ticket', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts index ca5b54154..f187e4ed6 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts @@ -4,17 +4,22 @@ import { UnifiedTicketInput, UnifiedTicketOutput, } from '@ticketing/ticket/types/model.unified'; +import { Utils } from '@ticketing/ticket/utils'; export class ZendeskTicketMapper implements ITicketMapper { - desunify( + private readonly utils = new Utils(); + + async desunify( source: UnifiedTicketInput, customFieldMappings?: { slug: string; remote_id: string; }[], - ): ZendeskTicketInput { + ): Promise { const result: ZendeskTicketInput = { - assignee_email: source.assigned_to?.[0], //TODO; get the mail of the uuid + assignee_email: await this.utils.getAssigneeMetadataFromUuid( + source.assigned_to?.[0], + ), // get the mail of the uuid description: source.description, due_at: source.due_date?.toISOString(), priority: source.priority as 'urgent' | 'high' | 'normal' | 'low', @@ -32,6 +37,11 @@ export class ZendeskTicketMapper implements ITicketMapper { body: source.comment.body, html_body: source.comment.html_body, public: !source.comment.is_private, + uploads: source.comment.attachments + ? await this.utils.get_Zendesk_AttachmentsTokensFromUuid( + source.comment.attachments, + ) + : [], //fetch token attachments for this uuid }, }; @@ -93,9 +103,9 @@ export class ZendeskTicketMapper implements ITicketMapper { type: ticket.type, parent_ticket: undefined, // If available, add logic to map parent ticket tags: JSON.stringify(ticket.tags), //TODO - completed_at: undefined, // If available, add logic to determine the completed date + completed_at: new Date(ticket.updated_at), priority: ticket.priority, - assigned_to: undefined, // If available, add logic to map assigned users + assigned_to: [String(ticket.assignee_id)], field_mappings: field_mappings, id: ticket.id.toString(), }; diff --git a/packages/api/src/ticketing/ticket/sync/sync.service.ts b/packages/api/src/ticketing/ticket/sync/sync.service.ts index 8620bd299..70e0be05c 100644 --- a/packages/api/src/ticketing/ticket/sync/sync.service.ts +++ b/packages/api/src/ticketing/ticket/sync/sync.service.ts @@ -100,20 +100,6 @@ export class SyncService implements OnModuleInit { }, }); if (!connection) return; - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.ticket.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -141,7 +127,7 @@ export class SyncService implements OnModuleInit { customFieldMappings, })) as UnifiedTicketOutput[]; - //TODO + //remote Ids in provider's tools const ticketIds = sourceObject.map((ticket) => 'id' in ticket ? String(ticket.id) : undefined, ); @@ -152,22 +138,28 @@ export class SyncService implements OnModuleInit { unifiedObject, ticketIds, integrationId, - job_id, sourceObject, ); - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.ticket.synced', + method: 'SYNC', + url: '/sync', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); + await this.webhook.handleWebhook( tickets_data, - 'ticketing.ticket.pulled', + 'ticketing.ticket.synced', id_project, - job_id, + event.id_event, ); } catch (error) { handleServiceError(error, this.logger); @@ -179,7 +171,6 @@ export class SyncService implements OnModuleInit { tickets: UnifiedTicketOutput[], originIds: string[], originSource: string, - jobId: string, remote_data: Record[], ): Promise { try { @@ -196,7 +187,7 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - events: { + linked_users: { id_linked_user: linkedUserId, }, }, @@ -244,7 +235,7 @@ export class SyncService implements OnModuleInit { assigned_to: ticket.assigned_to || [], created_at: new Date(), modified_at: new Date(), - id_event: jobId, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, }; diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index a1bd4480f..ba0a397e8 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -11,12 +11,11 @@ export class UnifiedTicketInput { tags?: string; // TODO: create a real Tag object here completed_at?: Date; priority?: string; - assigned_to?: string[]; - field_mappings?: Record[]; + assigned_to?: string[]; //uuid of Users objects ? comment?: UnifiedCommentInput; - attachments?: string[]; account_id?: string; contact_id?: string; + field_mappings?: Record[]; } export class UnifiedTicketOutput extends UnifiedTicketInput { @ApiProperty() diff --git a/packages/api/src/ticketing/ticket/utils/index.ts b/packages/api/src/ticketing/ticket/utils/index.ts index e69de29bb..adcd25ed3 100644 --- a/packages/api/src/ticketing/ticket/utils/index.ts +++ b/packages/api/src/ticketing/ticket/utils/index.ts @@ -0,0 +1,56 @@ +import { PrismaClient } from '@prisma/client'; + +export class Utils { + private readonly prisma: PrismaClient; + + constructor() { + this.prisma = new PrismaClient(); + } + + async getAssigneeMetadataFromUuid(uuid: string) { + try { + const res = await this.prisma.tcg_users.findUnique({ + where: { + id_tcg_user: uuid, + }, + }); + if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); + return res.email_address; + } catch (error) {} + } + + async get_Zendesk_AttachmentsTokensFromUuid(uuids: string[]) { + try { + let uploads = []; + uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + select: token, + }); + if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); + uploads = [...uploads, res.token]; + }); + return uploads; + } catch (error) {} + } + + async get_Front_AttachmentsFromUuid(uuids: string[]) { + try { + let uploads = []; + uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); + //TODO: construct the right binary attachment + const url = res.file_url; + uploads = [...uploads, url]; + }); + return uploads; + } catch (error) {} + } +} From 40caa42818afa10897a6c6dfb54264179a8ca4aa Mon Sep 17 00:00:00 2001 From: nael Date: Fri, 5 Jan 2024 17:18:19 +0100 Subject: [PATCH 11/25] :construction: Comment object updated --- packages/api/prisma/schema.prisma | 36 +-- .../ticketing/comment/comment.controller.ts | 12 - .../comment/services/comment.service.ts | 265 +++++++++++++++--- .../comment/services/front/mappers.ts | 17 +- .../comment/services/zendesk/mappers.ts | 23 +- .../ticketing/comment/sync/sync.service.ts | 41 +-- .../ticketing/comment/types/model.unified.ts | 15 +- .../ticket/services/ticket.service.ts | 2 +- 8 files changed, 299 insertions(+), 112 deletions(-) diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index e8a309ac8..fb378c985 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -379,23 +379,24 @@ model remote_data { /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model tcg_comments { - id_tcg_comment String @id(map: "pk_tcg_comments") @db.Uuid - body String? - html_body String? - is_private Boolean? - remote_id String? - remote_platform String? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) - creator_type String? - id_tcg_ticket String? @db.Uuid - id_tcg_contact String? @db.Uuid - id_tcg_user String? @db.Uuid - id_event String? @db.Uuid - tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_40_1") - tcg_contacts tcg_contacts? @relation(fields: [id_tcg_contact], references: [id_tcg_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_41") - tcg_users tcg_users? @relation(fields: [id_tcg_user], references: [id_tcg_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_42") - events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_46") + id_tcg_comment String @id(map: "pk_tcg_comments") @db.Uuid + body String? + html_body String? + is_private Boolean? + remote_id String? + remote_platform String? + created_at DateTime? @db.Timestamp(6) + modified_at DateTime? @db.Timestamp(6) + creator_type String? + id_tcg_ticket String? @db.Uuid + id_tcg_contact String? @db.Uuid + id_tcg_user String? @db.Uuid + id_event String? @db.Uuid + id_tcg_attachment String[] @db.Uuid + tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_40_1") + tcg_contacts tcg_contacts? @relation(fields: [id_tcg_contact], references: [id_tcg_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_41") + tcg_users tcg_users? @relation(fields: [id_tcg_user], references: [id_tcg_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_42") + events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_46") @@index([id_tcg_contact], map: "fk_tcg_comment_tcg_contact") @@index([id_tcg_ticket], map: "fk_tcg_comment_tcg_ticket") @@ -592,4 +593,5 @@ model tcg_attachments { uploader String @db.Uuid created_at DateTime @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) + id_linked_user String? @db.Uuid } diff --git a/packages/api/src/ticketing/comment/comment.controller.ts b/packages/api/src/ticketing/comment/comment.controller.ts index c670842a0..40ecc1433 100644 --- a/packages/api/src/ticketing/comment/comment.controller.ts +++ b/packages/api/src/ticketing/comment/comment.controller.ts @@ -154,16 +154,4 @@ export class CommentController { remote_data, ); } - - @ApiOperation({ - operationId: 'updateComment', - summary: 'Update a Comment', - }) - @Patch() - updateComment( - @Query('id') id: string, - @Body() updateCommentData: Partial, - ) { - return this.commentService.updateComment(id, updateCommentData); - } } diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index 89e655693..dd0ec7e8e 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -77,20 +77,59 @@ export class CommentService { id_linked_user: linkedUserId, }, }); - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.comment.created', //sync, push or pull - method: 'POST', - url: '/ticketing/comment', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + + //CHECKS + if (!linkedUser) throw new Error('Linked User Not Found'); + const tick = unifiedCommentData.ticket_id; + //check if contact_id and account_id refer to real uuids + if (tick) { + const search = await this.prisma.tcg_tickets.findUnique({ + where: { + id_tcg_ticket: tick, + }, + }); + if (!search) + throw new Error('You inserted a ticket_id which does not exist'); + } + + const contact = unifiedCommentData.contact_id; + //check if contact_id and account_id refer to real uuids + if (contact) { + const search = await this.prisma.tcg_contacts.findUnique({ + where: { + id_tcg_contact: contact, + }, + }); + if (!search) + throw new Error('You inserted a contact_id which does not exist'); + } + const user = unifiedCommentData.user_id; + //check if contact_id and account_id refer to real uuids + if (user) { + const search = await this.prisma.tcg_users.findUnique({ + where: { + id_tcg_user: user, + }, + }); + if (!search) + throw new Error('You inserted a user_id which does not exist'); + } + + const attachmts = unifiedCommentData.attachments; + //CHEK IF attachments contains valid Attachment uuids + if (attachmts && attachmts.length > 0) { + attachmts.map(async (attachmt) => { + const search = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: attachmt, + }, + }); + if (!search) + throw new Error( + 'You inserted an attachment_id which does not exist', + ); + }); + } //desunify the data according to the target obj wanted const desunifiedObject = await desunify({ @@ -135,7 +174,7 @@ export class CommentService { where: { remote_id: originId, remote_platform: integrationId, - events: { + linked_users: { id_linked_user: linkedUserId, }, }, @@ -155,7 +194,6 @@ export class CommentService { is_private: target_comment.is_private, creator_type: target_comment.creator_type, id_tcg_ticket: target_comment.ticket_id, - id_event: job_id, modified_at: new Date(), }, }); @@ -172,7 +210,7 @@ export class CommentService { modified_at: new Date(), creator_type: target_comment.creator_type, id_tcg_ticket: target_comment.ticket_id, - id_event: job_id, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: integrationId, //TODO; id_tcg_contact String? @db.Uuid @@ -204,26 +242,31 @@ export class CommentService { }); } - ///// const result_comment = await this.getComment( unique_ticketing_comment_id, remote_data, ); const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: status_resp, + type: 'ticketing.comment.push', //sync, push or pull + method: 'PUSH', + url: '/ticketing/comment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( result_comment.data.comments, 'ticketing.comment.created', linkedUser.id_project, - job_id, + event.id_event, ); return { ...resp, data: result_comment.data }; } catch (error) { @@ -235,29 +278,179 @@ export class CommentService { id_commenting_comment: string, remote_data?: boolean, ): Promise> { - return; + try { + const comment = await this.prisma.tcg_comments.findUnique({ + where: { + id_tcg_comment: id_commenting_comment, + }, + }); + + // WE SHOULDNT HAVE FIELD MAPPINGS TO COMMENT + + // Fetch field mappings for the comment + /*const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: comment.id_tcg_comment, + }, + }, + include: { + attribute: true, + }, + }); + + Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from(fieldMappingsMap, ([key, value]) => ({ + [key]: value, + }));*/ + + // Transform to UnifiedCommentOutput format + const unifiedComment: UnifiedCommentOutput = { + id: comment.id_tcg_comment, + body: comment.body, + html_body: comment.html_body, + is_private: comment.is_private, + creator_type: comment.creator_type, + ticket_id: comment.id_tcg_ticket, + contact_id: comment.id_tcg_contact, // uuid of Contact object + user_id: comment.id_tcg_user, // uuid of User object + attachments: comment.attachments, //uuids of Attachments objects + }; + + let res: CommentResponse = { + comments: [unifiedComment], + }; + + if (remote_data) { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: comment.id_tcg_comment, + }, + }); + const remote_data = JSON.parse(resp.data); + + res = { + ...res, + remote_data: [remote_data], + }; + } + + return { + data: res, + statusCode: 200, + }; + } catch (error) { + handleServiceError(error, this.logger); + } } async getComments( integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { - return; - } - //TODO - async updateComment( - id: string, - updateCommentData: Partial, ): Promise> { try { + const comments = await this.prisma.tcg_comments.findMany({ + where: { + remote_id: integrationId.toLowerCase(), + events: { + id_linked_user: linkedUserId, + }, + }, + }); + + const unifiedComments: UnifiedCommentOutput[] = await Promise.all( + comments.map(async (comment) => { + //WE SHOULDNT HAVE FIELD MAPPINGS FOR COMMENT + // Fetch field mappings for the ticket + /*const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: comment.id_tcg_ticket, + }, + }, + include: { + attribute: true, + }, + }); + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); + + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); + }); + + // Convert the map to an array of objects + const field_mappings = Array.from( + fieldMappingsMap, + ([key, value]) => ({ [key]: value }), + );*/ + + // Transform to UnifiedCommentOutput format + return { + id: comment.id_tcg_comment, + body: comment.body, + html_body: comment.html_body, + is_private: comment.is_private, + creator_type: comment.creator_type, + ticket_id: comment.id_tcg_ticket, + contact_id: comment.id_tcg_contact, // uuid of Contact object + user_id: comment.id_tcg_user, // uuid of User object + attachments: comment.attachments, //uuids of Attachments objects + }; + }), + ); + + let res: CommentResponse = { + comments: unifiedComments, + }; + + if (remote_data) { + const remote_array_data: Record[] = await Promise.all( + comments.map(async (comment) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: comment.id_tcg_comment, + }, + }); + const remote_data = JSON.parse(resp.data); + return remote_data; + }), + ); + + res = { + ...res, + remote_data: remote_array_data, + }; + } + + const event = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'ticketing.comment.pulled', + method: 'GET', + url: '/ticketing/comment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + + return { + data: res, + statusCode: 200, + }; } catch (error) { handleServiceError(error, this.logger); } - // TODO: fetch the comment from the database using 'id' - // TODO: update the comment with 'updateCommentData' - // TODO: save the updated comment back to the database - // TODO: return the updated comment - return; } } diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts index f3482d576..c9109bae0 100644 --- a/packages/api/src/ticketing/comment/services/front/mappers.ts +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -4,20 +4,25 @@ import { UnifiedCommentOutput, } from '@ticketing/comment/types/model.unified'; import { FrontCommentInput, FrontCommentOutput } from './types'; +import { Utils } from '@ticketing/ticket/utils'; export class FrontCommentMapper implements ICommentMapper { - desunify( + private readonly utils = new Utils(); + + async desunify( source: UnifiedCommentInput, customFieldMappings?: { slug: string; remote_id: string; }[], - ): FrontCommentInput { + ): Promise { const result: FrontCommentInput = { body: source.body, - author_id: source.user_id || source.contact_id, //TODO: + author_id: source.user_id || source.contact_id, //TODO: make sure either one is passed + attachments: source.attachments + ? await this.utils.get_Front_AttachmentsFromUuid(source.attachments) + : [], }; - return result; } @@ -47,9 +52,7 @@ export class FrontCommentMapper implements ICommentMapper { id: comment.id, body: comment.body, html_body: '', - created_at: new Date(comment.posted_at * 1000), // Convert UNIX timestamp to Date - modified_at: new Date(), // Placeholder, as modified_at is not available - creator_type: comment.author ? 'user' : null, + creator_type: comment.author ? 'contact' : null, ticket_id: '', // TODO: Need to be determined from related data contact_id: '', // TODO: Need to be determined from related data user_id: '', //TODO diff --git a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts index 56061a7c0..87190227b 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts @@ -4,21 +4,33 @@ import { UnifiedCommentInput, UnifiedCommentOutput, } from '@ticketing/comment/types/model.unified'; +import { Utils } from '@ticketing/ticket/utils'; export class ZendeskCommentMapper implements ICommentMapper { - desunify( + private readonly utils = new Utils(); + + async desunify( source: UnifiedCommentInput, customFieldMappings?: { slug: string; remote_id: string; }[], - ): ZendeskCommentInput { + ): Promise { const result: ZendeskCommentInput = { body: source.body, html_body: source.html_body, public: !source.is_private, - author_id: source.user_id ? parseInt(source.user_id) : undefined, + author_id: source.user_id + ? parseInt(source.user_id) + : source.contact_id + ? parseInt(source.contact_id) + : undefined, //TODO: make sure either one is passed type: 'Comment', + uploads: source.attachments + ? await this.utils.get_Zendesk_AttachmentsTokensFromUuid( + source.attachments, + ) + : [], //fetch token attachments for this uuid }; return result; @@ -47,14 +59,15 @@ export class ZendeskCommentMapper implements ICommentMapper { }[], ): UnifiedCommentOutput { return { - id: comment.id.toString(), body: comment.body || '', html_body: comment.html_body || '', is_private: !comment.public, created_at: new Date(comment.created_at), modified_at: new Date(comment.created_at), // Assuming the creation date for modification as well - creator_type: null, //TODO + creator_type: 'contact', ticket_id: '', //TODO + contact_id: '', // TODO: + user_id: '', //TODO }; } } diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index 0079b4cde..a1029ab88 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -68,7 +68,7 @@ export class SyncService implements OnModuleInit { const tickets = await this.prisma.tcg_tickets.findMany({ where: { remote_platform: provider, - events: { + linked_users: { id_linked_user: linkedUser.id_linked_user, }, }, @@ -113,20 +113,6 @@ export class SyncService implements OnModuleInit { }, }); if (!connection) return; - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.comment.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -165,22 +151,27 @@ export class SyncService implements OnModuleInit { commentsIds, integrationId, id_ticket, - job_id, sourceObject, ); - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + + const event = await this.prisma.events.create({ data: { - status: 'success', + id_event: uuidv4(), + status: 'initialized', + type: 'ticketing.comment.synced', + method: 'SYNC', + url: '/sync', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( comments_data, - 'ticketing.comment.pulled', + 'ticketing.comment.synced', id_project, - job_id, + event.id_event, ); } catch (error) { handleServiceError(error, this.logger); @@ -193,7 +184,6 @@ export class SyncService implements OnModuleInit { originIds: string[], originSource: string, id_ticket: string, - jobId: string, remote_data: Record[], ): Promise { try { @@ -230,7 +220,6 @@ export class SyncService implements OnModuleInit { is_private: comment.is_private, creator_type: comment.creator_type, id_tcg_ticket: id_ticket, - id_event: jobId, modified_at: new Date(), }, }); @@ -248,7 +237,7 @@ export class SyncService implements OnModuleInit { modified_at: new Date(), creator_type: comment.creator_type, id_tcg_ticket: id_ticket, - id_event: jobId, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, //TODO; id_tcg_contact String? @db.Uuid diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index 52c4d6625..69f35c270 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -1,20 +1,19 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { UnifiedAttachmentInput } from '@ticketing/attachment/types/model.unified'; export class UnifiedCommentInput { body: string; html_body?: string; is_private?: boolean; - created_at: Date; - modified_at: Date; - creator_type: 'user' | 'contact' | null; - ticket_id?: string; - contact_id?: string; - user_id?: string; + created_at?: Date; + modified_at?: Date; + creator_type: 'user' | 'contact' | null | string; + ticket_id?: string; // uuid of Ticket object + contact_id?: string; // uuid of Contact object + user_id?: string; // uuid of User object attachments?: string[]; //uuids of Attachments objects } //TODO: add remote_id export class UnifiedCommentOutput extends UnifiedCommentInput { - id: string; + id?: string; } diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index 5a9f8e1ab..817914215 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -288,7 +288,7 @@ export class TicketService { id_event: uuidv4(), status: status_resp, type: 'ticketing.ticket.push', //sync, push or pull - method: 'POST', + method: 'PUSH', url: '/ticketing/ticket', provider: integrationId, direction: '0', From 32850790d45ab0ca1afa8dd84e41abc8b7406643 Mon Sep 17 00:00:00 2001 From: nael Date: Fri, 5 Jan 2024 18:28:45 +0100 Subject: [PATCH 12/25] :construction: Attachment working --- packages/api/prisma/schema.prisma | 83 +++++++++---------- .../crm/contact/services/contact.service.ts | 64 +++++--------- .../api/src/crm/contact/sync/sync.service.ts | 39 +++------ .../account/services/account.service.ts | 33 +++----- .../ticketing/account/sync/sync.service.ts | 39 +++------ .../attachment/services/attachment.service.ts | 42 ++++------ .../comment/services/comment.service.ts | 6 +- .../ticketing/comment/sync/sync.service.ts | 10 +-- .../contact/services/contact.service.ts | 27 ++---- .../ticketing/contact/sync/sync.service.ts | 39 +++------ .../src/ticketing/tag/services/tag.service.ts | 27 ++---- .../src/ticketing/tag/sync/sync.service.ts | 39 +++------ .../ticketing/team/services/team.service.ts | 32 +++---- .../src/ticketing/team/sync/sync.service.ts | 41 ++++----- .../ticket/services/ticket.service.ts | 4 +- .../src/ticketing/ticket/sync/sync.service.ts | 5 +- .../ticketing/user/services/user.service.ts | 28 +++---- .../src/ticketing/user/sync/sync.service.ts | 39 +++------ 18 files changed, 216 insertions(+), 381 deletions(-) diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index fb378c985..ea52e0ab5 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -379,24 +379,25 @@ model remote_data { /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model tcg_comments { - id_tcg_comment String @id(map: "pk_tcg_comments") @db.Uuid + id_tcg_comment String @id(map: "pk_tcg_comments") @db.Uuid body String? html_body String? is_private Boolean? remote_id String? remote_platform String? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) + created_at DateTime? @db.Timestamp(6) + modified_at DateTime? @db.Timestamp(6) creator_type String? - id_tcg_ticket String? @db.Uuid - id_tcg_contact String? @db.Uuid - id_tcg_user String? @db.Uuid - id_event String? @db.Uuid - id_tcg_attachment String[] @db.Uuid - tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_40_1") - tcg_contacts tcg_contacts? @relation(fields: [id_tcg_contact], references: [id_tcg_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_41") - tcg_users tcg_users? @relation(fields: [id_tcg_user], references: [id_tcg_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_42") - events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_46") + id_tcg_attachment String[] + id_tcg_ticket String? @db.Uuid + id_tcg_contact String? @db.Uuid + id_tcg_user String? @db.Uuid + id_event String? @db.Uuid + tcg_attachments tcg_attachments[] + tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_40_1") + tcg_contacts tcg_contacts? @relation(fields: [id_tcg_contact], references: [id_tcg_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_41") + tcg_users tcg_users? @relation(fields: [id_tcg_user], references: [id_tcg_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_42") + events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_46") @@index([id_tcg_contact], map: "fk_tcg_comment_tcg_contact") @@index([id_tcg_ticket], map: "fk_tcg_comment_tcg_ticket") @@ -418,6 +419,7 @@ model tcg_contacts { id_tcg_account String? @db.Uuid tcg_comments tcg_comments[] events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_43") + tcg_accounts tcg_accounts? @relation(fields: [id_tcg_account], references: [id_tcg_account], onDelete: NoAction, onUpdate: NoAction, map: "fk_49") @@index([id_event], map: "fk_tcg_contact_event_id") @@index([id_tcg_account], map: "fk_tcg_contact_tcg_account_id") @@ -425,26 +427,27 @@ model tcg_contacts { /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model tcg_tickets { - id_tcg_ticket String @id(map: "pk_tcg_tickets") @db.Uuid + id_tcg_ticket String @id(map: "pk_tcg_tickets") @db.Uuid name String? status String? description String? - due_date DateTime? @db.Timestamp(6) + due_date DateTime? @db.Timestamp(6) ticket_type String? - parent_ticket String? @db.Uuid + parent_ticket String? @db.Uuid tags String? - completed_at DateTime? @db.Timestamp(6) + completed_at DateTime? @db.Timestamp(6) priority String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) assigned_to String[] remote_id String? remote_platform String? - id_event String? @db.Uuid + id_event String? @db.Uuid creator_type String? - id_tcg_user String? @db.Uuid + id_tcg_user String? @db.Uuid + tcg_attachments tcg_attachments[] tcg_comments tcg_comments[] - events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_44") + events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_44") @@index([id_event], map: "fk_tcg_tickets_eventid") @@index([id_tcg_user], map: "fk_tcg_ticket_tcg_user") @@ -549,26 +552,14 @@ model webhooks_reponses { /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model tcg_accounts { - id_tcg_account String @id(map: "pk_tcg_account") @db.Uuid + id_tcg_account String @id(map: "pk_tcg_account") @db.Uuid remote_id String? name String? domains String[] remote_platform String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) -} - -/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. -model tcg_tags { - id_tcg_tag String @id(map: "pk_tcg_tags") @db.Uuid - name String? - remote_id String? - remote_platform String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_tcg_ticket String? @db.Uuid - - @@index([id_tcg_ticket], map: "fk_tcg_tag_tcg_ticketid") + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) + tcg_contacts tcg_contacts[] } /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. @@ -584,14 +575,20 @@ model tcg_teams { /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model tcg_attachments { - id_tcg_attachment String @id(map: "pk_tcg_attachments") @db.Uuid + id_tcg_attachment String @id(map: "pk_tcg_attachments") @db.Uuid remote_id String? remote_platform String? file_name String? - id_tcg_ticket String? @db.Uuid file_url String? - uploader String @db.Uuid - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_linked_user String? @db.Uuid + uploader String @db.Uuid + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) + id_linked_user String? @db.Uuid + id_tcg_ticket String? @db.Uuid + id_tcg_comment String? @db.Uuid + tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_50") + tcg_comments tcg_comments? @relation(fields: [id_tcg_comment], references: [id_tcg_comment], onDelete: NoAction, onUpdate: NoAction, map: "fk_51") + + @@index([id_tcg_comment], map: "fk_tcg_attachment_tcg_commentid") + @@index([id_tcg_ticket], map: "fk_tcg_attachment_tcg_ticketid") } diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index 8d8460d72..784f22b49 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -80,20 +80,6 @@ export class ContactService { id_linked_user: linkedUserId, }, }); - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'crm.contact.created', //sync, push or pull - method: 'POST', - url: '/crm/contact', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; // Retrieve custom field mappings // get potential fieldMappings and extract the original properties name @@ -142,9 +128,7 @@ export class ContactService { where: { remote_id: originId, remote_platform: integrationId, - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, include: { crm_email_addresses: true, crm_phone_numbers: true }, }); @@ -197,7 +181,7 @@ export class ContactService { last_name: target_contact.last_name || '', created_at: new Date(), modified_at: new Date(), - id_event: job_id, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: integrationId, }; @@ -287,19 +271,24 @@ export class ContactService { ); const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: status_resp, + type: 'crm.contact.created', //sync, push or pull + method: 'POST', + url: '/crm/contact', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( result_contact.data.contacts, 'crm.contact.created', linkedUser.id_project, - job_id, + event.id_event, ); return { ...resp, data: result_contact.data }; } catch (error) { @@ -396,20 +385,7 @@ export class ContactService { ): Promise> { try { //TODO: handle case where data is not there (not synced) or old synced - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'crm.contact.pull', - method: 'GET', - url: '/crm/contact', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + const contacts = await this.prisma.crm_contacts.findMany({ where: { remote_id: integrationId.toLowerCase(), @@ -489,15 +465,19 @@ export class ContactService { remote_data: remote_array_data, }; } - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'crm.contact.pull', + method: 'GET', + url: '/crm/contact', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); - return { data: res, statusCode: 200, diff --git a/packages/api/src/crm/contact/sync/sync.service.ts b/packages/api/src/crm/contact/sync/sync.service.ts index 7deb67562..9f0774bec 100644 --- a/packages/api/src/crm/contact/sync/sync.service.ts +++ b/packages/api/src/crm/contact/sync/sync.service.ts @@ -90,7 +90,6 @@ export class SyncContactsService implements OnModuleInit { contacts: UnifiedContactOutput[], originIds: string[], originSource: string, - jobId: string, remote_data: Record[], ): Promise { try { @@ -107,9 +106,7 @@ export class SyncContactsService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, include: { crm_email_addresses: true, crm_phone_numbers: true }, }); @@ -164,7 +161,7 @@ export class SyncContactsService implements OnModuleInit { last_name: contact.last_name ? contact.last_name : '', created_at: new Date(), modified_at: new Date(), - id_event: jobId, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, }; @@ -269,21 +266,7 @@ export class SyncContactsService implements OnModuleInit { provider_slug: integrationId, }, }); - if (!connection) return; - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'crm.contact.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + if (!connection) throw new Error('connection not found'); // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -326,22 +309,26 @@ export class SyncContactsService implements OnModuleInit { unifiedObject, contactIds, integrationId, - job_id, sourceObject, ); - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'crm.contact.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( contacts_data, 'crm.contact.pulled', id_project, - job_id, + event.id_event, ); } catch (error) { handleServiceError(error, this.logger); diff --git a/packages/api/src/ticketing/account/services/account.service.ts b/packages/api/src/ticketing/account/services/account.service.ts index d4c529c94..28cc84ee7 100644 --- a/packages/api/src/ticketing/account/services/account.service.ts +++ b/packages/api/src/ticketing/account/services/account.service.ts @@ -90,26 +90,11 @@ export class AccountService { ): Promise> { try { //TODO: handle case where data is not there (not synced) or old synced - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.account.pull', - method: 'GET', - url: '/ticketing/account', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + const accounts = await this.prisma.tcg_accounts.findMany({ where: { remote_id: integrationId.toLowerCase(), - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -171,15 +156,19 @@ export class AccountService { remote_data: remote_array_data, }; } - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.account.pull', + method: 'GET', + url: '/ticketing/account', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); - return { data: res, statusCode: 200, diff --git a/packages/api/src/ticketing/account/sync/sync.service.ts b/packages/api/src/ticketing/account/sync/sync.service.ts index e543f0378..50712239c 100644 --- a/packages/api/src/ticketing/account/sync/sync.service.ts +++ b/packages/api/src/ticketing/account/sync/sync.service.ts @@ -99,21 +99,7 @@ export class SyncService implements OnModuleInit { provider_slug: integrationId, }, }); - if (!connection) return; - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.account.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + if (!connection) throw new Error('connection not found'); // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -152,22 +138,26 @@ export class SyncService implements OnModuleInit { unifiedObject, accountIds, integrationId, - job_id, sourceObject, ); - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.account.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( account_data, 'ticketing.account.pulled', id_project, - job_id, + event.id_event, ); } catch (error) { handleServiceError(error, this.logger); @@ -179,7 +169,6 @@ export class SyncService implements OnModuleInit { accounts: UnifiedAccountOutput[], originIds: string[], originSource: string, - jobId: string, remote_data: Record[], ): Promise { try { @@ -196,9 +185,7 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - events: { - id_linked_account: linkedUserId, - }, + id_linked_account: linkedUserId, }, }); @@ -227,7 +214,7 @@ export class SyncService implements OnModuleInit { domains: account.domains, created_at: new Date(), modified_at: new Date(), - id_event: jobId, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, }; diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index 3d2ad0ba0..0ea02abc9 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -79,20 +79,7 @@ export class AttachmentService { id_linked_user: linkedUserId, }, }); - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.attachment.created', //sync, push or pull - method: 'POST', - url: '/ticketing/attachment', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + if (!linkedUser) throw new Error('Linked User Not Found'); //TODO // Retrieve custom field mappings @@ -136,9 +123,7 @@ export class AttachmentService { where: { remote_id: originId, remote_platform: integrationId, - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -162,9 +147,10 @@ export class AttachmentService { const data = { id_tcg_attachment: uuidv4(), //TODO + created_at: new Date(), modified_at: new Date(), - id_event: job_id, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: integrationId, }; @@ -236,26 +222,31 @@ export class AttachmentService { }); } - ///// const result_attachment = await this.getAttachment( unique_ticketing_attachment_id, remote_data, ); const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: status_resp, + type: 'ticketing.attachment.push', //sync, push or pull + method: 'POST', + url: '/ticketing/attachment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); + await this.webhook.handleWebhook( result_attachment.data.attachments, 'ticketing.attachment.created', linkedUser.id_project, - job_id, + event.id_event, ); return { ...resp, data: result_attachment.data }; } catch (error) { @@ -437,7 +428,8 @@ export class AttachmentService { } } - async downloadAttachmentt( + //TODO + async downloadAttachment( id_ticketing_attachment: string, remote_data?: boolean, ): Promise> { diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index dd0ec7e8e..1cb639e4e 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -174,9 +174,7 @@ export class CommentService { where: { remote_id: originId, remote_platform: integrationId, - linked_users: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -254,7 +252,7 @@ export class CommentService { id_event: uuidv4(), status: status_resp, type: 'ticketing.comment.push', //sync, push or pull - method: 'PUSH', + method: 'POST', url: '/ticketing/comment', provider: integrationId, direction: '0', diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index a1029ab88..0c814c01f 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -68,9 +68,7 @@ export class SyncService implements OnModuleInit { const tickets = await this.prisma.tcg_tickets.findMany({ where: { remote_platform: provider, - linked_users: { - id_linked_user: linkedUser.id_linked_user, - }, + id_linked_user: linkedUser.id_linked_user, }, }); for (const ticket of tickets) { @@ -157,7 +155,7 @@ export class SyncService implements OnModuleInit { const event = await this.prisma.events.create({ data: { id_event: uuidv4(), - status: 'initialized', + status: 'success', type: 'ticketing.comment.synced', method: 'SYNC', url: '/sync', @@ -200,9 +198,7 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/contact/services/contact.service.ts b/packages/api/src/ticketing/contact/services/contact.service.ts index 374b861bf..a4e8eb718 100644 --- a/packages/api/src/ticketing/contact/services/contact.service.ts +++ b/packages/api/src/ticketing/contact/services/contact.service.ts @@ -92,20 +92,6 @@ export class ContactService { ): Promise> { try { //TODO: handle case where data is not there (not synced) or old synced - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.contact.pull', - method: 'GET', - url: '/ticketing/contact', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; const contacts = await this.prisma.tcg_contacts.findMany({ where: { remote_id: integrationId.toLowerCase(), @@ -175,12 +161,17 @@ export class ContactService { remote_data: remote_array_data, }; } - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.contact.pull', + method: 'GET', + url: '/ticketing/contact', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/contact/sync/sync.service.ts b/packages/api/src/ticketing/contact/sync/sync.service.ts index 3d78975f8..ca6c959ca 100644 --- a/packages/api/src/ticketing/contact/sync/sync.service.ts +++ b/packages/api/src/ticketing/contact/sync/sync.service.ts @@ -99,21 +99,7 @@ export class SyncService implements OnModuleInit { provider_slug: integrationId, }, }); - if (!connection) return; - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.contact.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + if (!connection) throw new Error('connection not found'); // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -152,22 +138,26 @@ export class SyncService implements OnModuleInit { unifiedObject, contactIds, integrationId, - job_id, sourceObject, ); - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.contact.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( contact_data, 'ticketing.contact.pulled', id_project, - job_id, + event.id_event, ); } catch (error) { handleServiceError(error, this.logger); @@ -179,7 +169,6 @@ export class SyncService implements OnModuleInit { contacts: UnifiedContactOutput[], originIds: string[], originSource: string, - jobId: string, remote_data: Record[], ): Promise { try { @@ -196,9 +185,7 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -231,7 +218,7 @@ export class SyncService implements OnModuleInit { details: contact.details, created_at: new Date(), modified_at: new Date(), - id_event: jobId, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, }; diff --git a/packages/api/src/ticketing/tag/services/tag.service.ts b/packages/api/src/ticketing/tag/services/tag.service.ts index ba4bc4438..9c308818b 100644 --- a/packages/api/src/ticketing/tag/services/tag.service.ts +++ b/packages/api/src/ticketing/tag/services/tag.service.ts @@ -89,20 +89,6 @@ export class TagService { ): Promise> { try { //TODO: handle case where data is not there (not synced) or old synced - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.tag.pull', - method: 'GET', - url: '/ticketing/tag', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; const tags = await this.prisma.tcg_tags.findMany({ where: { remote_id: integrationId.toLowerCase(), @@ -169,12 +155,17 @@ export class TagService { remote_data: remote_array_data, }; } - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.tag.pull', + method: 'GET', + url: '/ticketing/tag', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/tag/sync/sync.service.ts b/packages/api/src/ticketing/tag/sync/sync.service.ts index 3d40e23e2..84ebe1662 100644 --- a/packages/api/src/ticketing/tag/sync/sync.service.ts +++ b/packages/api/src/ticketing/tag/sync/sync.service.ts @@ -99,21 +99,7 @@ export class SyncService implements OnModuleInit { provider_slug: integrationId, }, }); - if (!connection) return; - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.tag.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + if (!connection) throw new Error('connection not found'); // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -154,22 +140,26 @@ export class SyncService implements OnModuleInit { unifiedObject, tagIds, integrationId, - job_id, sourceObject, ); - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.tag.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( tag_data, 'ticketing.tag.pulled', id_project, - job_id, + event.id_event, ); } catch (error) { handleServiceError(error, this.logger); @@ -181,7 +171,6 @@ export class SyncService implements OnModuleInit { tags: UnifiedTagOutput[], originIds: string[], originSource: string, - jobId: string, remote_data: Record[], ): Promise { try { @@ -198,9 +187,7 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -227,7 +214,7 @@ export class SyncService implements OnModuleInit { name: tag.name, created_at: new Date(), modified_at: new Date(), - id_event: jobId, + id_linked_users: linkedUserId, remote_id: originId, remote_platform: originSource, }; diff --git a/packages/api/src/ticketing/team/services/team.service.ts b/packages/api/src/ticketing/team/services/team.service.ts index af79317d0..0cb5d4a31 100644 --- a/packages/api/src/ticketing/team/services/team.service.ts +++ b/packages/api/src/ticketing/team/services/team.service.ts @@ -90,26 +90,11 @@ export class TeamService { ): Promise> { try { //TODO: handle case where data is not there (not synced) or old synced - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.team.pull', - method: 'GET', - url: '/ticketing/team', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + const teams = await this.prisma.tcg_teams.findMany({ where: { remote_id: integrationId.toLowerCase(), - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -171,12 +156,17 @@ export class TeamService { remote_data: remote_array_data, }; } - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.team.pull', + method: 'GET', + url: '/ticketing/team', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/team/sync/sync.service.ts b/packages/api/src/ticketing/team/sync/sync.service.ts index 414956acf..1c290355e 100644 --- a/packages/api/src/ticketing/team/sync/sync.service.ts +++ b/packages/api/src/ticketing/team/sync/sync.service.ts @@ -99,21 +99,7 @@ export class SyncService implements OnModuleInit { provider_slug: integrationId, }, }); - if (!connection) return; - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.team.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + if (!connection) throw new Error('connection not found'); // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -154,22 +140,28 @@ export class SyncService implements OnModuleInit { unifiedObject, teamIds, integrationId, - job_id, sourceObject, ); - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.team.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); + await this.webhook.handleWebhook( team_data, 'ticketing.team.pulled', id_project, - job_id, + event.id_event, ); } catch (error) { handleServiceError(error, this.logger); @@ -181,7 +173,6 @@ export class SyncService implements OnModuleInit { teams: UnifiedTeamOutput[], originIds: string[], originSource: string, - jobId: string, remote_data: Record[], ): Promise { try { @@ -198,9 +189,7 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -229,7 +218,7 @@ export class SyncService implements OnModuleInit { description: team.description, created_at: new Date(), modified_at: new Date(), - id_event: jobId, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, }; diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index 817914215..b0cafefd5 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -158,9 +158,7 @@ export class TicketService { where: { remote_id: originId, remote_platform: integrationId, - linked_users: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/ticket/sync/sync.service.ts b/packages/api/src/ticketing/ticket/sync/sync.service.ts index 70e0be05c..09d123d2b 100644 --- a/packages/api/src/ticketing/ticket/sync/sync.service.ts +++ b/packages/api/src/ticketing/ticket/sync/sync.service.ts @@ -187,11 +187,8 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - linked_users: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, - include: { tcg_comments: true }, }); let unique_ticketing_ticket_id: string; diff --git a/packages/api/src/ticketing/user/services/user.service.ts b/packages/api/src/ticketing/user/services/user.service.ts index d788e4306..9b102211e 100644 --- a/packages/api/src/ticketing/user/services/user.service.ts +++ b/packages/api/src/ticketing/user/services/user.service.ts @@ -91,20 +91,6 @@ export class UserService { ): Promise> { try { //TODO: handle case where data is not there (not synced) or old synced - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.user.pull', - method: 'GET', - url: '/ticketing/user', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; const users = await this.prisma.tcg_users.findMany({ where: { remote_id: integrationId.toLowerCase(), @@ -173,12 +159,18 @@ export class UserService { remote_data: remote_array_data, }; } - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.user.pull', + method: 'GET', + url: '/ticketing/user', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/user/sync/sync.service.ts b/packages/api/src/ticketing/user/sync/sync.service.ts index ce46d9e01..949657475 100644 --- a/packages/api/src/ticketing/user/sync/sync.service.ts +++ b/packages/api/src/ticketing/user/sync/sync.service.ts @@ -99,21 +99,7 @@ export class SyncService implements OnModuleInit { provider_slug: integrationId, }, }); - if (!connection) return; - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.user.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + if (!connection) throw new Error('connection not found'); // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -154,22 +140,26 @@ export class SyncService implements OnModuleInit { unifiedObject, userIds, integrationId, - job_id, sourceObject, ); - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.user.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( user_data, 'ticketing.user.pulled', id_project, - job_id, + event.id_event, ); } catch (error) { handleServiceError(error, this.logger); @@ -181,7 +171,6 @@ export class SyncService implements OnModuleInit { users: UnifiedUserOutput[], originIds: string[], originSource: string, - jobId: string, remote_data: Record[], ): Promise { try { @@ -198,9 +187,7 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -231,7 +218,7 @@ export class SyncService implements OnModuleInit { teams: user.teams || [], created_at: new Date(), modified_at: new Date(), - id_event: jobId, + id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, }; From a9b030b1e968c8bc75f0c7cb301e1e96e5ccf78d Mon Sep 17 00:00:00 2001 From: nael Date: Fri, 5 Jan 2024 18:31:39 +0100 Subject: [PATCH 13/25] :construction: Updated jobs syntax --- .../attachment/services/attachment.service.ts | 32 +++++---------- .../ticketing/attachment/sync/sync.service.ts | 40 ++++++------------- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index 0ea02abc9..9875ff5e0 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -330,26 +330,10 @@ export class AttachmentService { ): Promise> { try { //TODO: handle case where data is not there (not synced) or old synced - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.attachment.pull', - method: 'GET', - url: '/ticketing/attachment', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; const attachments = await this.prisma.tcg_attachments.findMany({ where: { remote_id: integrationId.toLowerCase(), - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -410,12 +394,18 @@ export class AttachmentService { remote_data: remote_array_data, }; } - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.attachment.pull', + method: 'GET', + url: '/ticketing/attachment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/attachment/sync/sync.service.ts b/packages/api/src/ticketing/attachment/sync/sync.service.ts index d6a9b1afc..e7705ec38 100644 --- a/packages/api/src/ticketing/attachment/sync/sync.service.ts +++ b/packages/api/src/ticketing/attachment/sync/sync.service.ts @@ -112,21 +112,7 @@ export class SyncService implements OnModuleInit { provider_slug: integrationId, }, }); - if (!connection) return; - const job_resp_create = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'initialized', - type: 'ticketing.attachment.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - const job_id = job_resp_create.id_event; + if (!connection) throw new Error('connection not found'); // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -165,22 +151,26 @@ export class SyncService implements OnModuleInit { attachmentsIds, integrationId, id_ticket, - job_id, sourceObject, ); - await this.prisma.events.update({ - where: { - id_event: job_id, - }, + const event = await this.prisma.events.create({ data: { + id_event: uuidv4(), status: 'success', + type: 'ticketing.attachment.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, }, }); await this.webhook.handleWebhook( attachments_data, 'ticketing.attachment.pulled', id_project, - job_id, + event.id_event, ); } catch (error) { handleServiceError(error, this.logger); @@ -193,7 +183,6 @@ export class SyncService implements OnModuleInit { originIds: string[], originSource: string, id_ticket: string, - jobId: string, remote_data: Record[], ): Promise { try { @@ -210,9 +199,7 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -227,7 +214,6 @@ export class SyncService implements OnModuleInit { data: { //TODO id_tcg_ticket: id_ticket, - id_event: jobId, modified_at: new Date(), }, }); @@ -242,7 +228,7 @@ export class SyncService implements OnModuleInit { created_at: new Date(), modified_at: new Date(), id_tcg_ticket: id_ticket, - id_event: jobId, + id_linkedUser: linkedUserId, remote_id: originId, remote_platform: originSource, //TODO; id_tcg_contact String? @db.Uuid From c23a457ec4ed421a9a34bc80d904e49bba3dda66 Mon Sep 17 00:00:00 2001 From: nael Date: Fri, 5 Jan 2024 19:39:10 +0100 Subject: [PATCH 14/25] :construction: Upadted attachment, comment and ticket relationship --- .../attachment/services/attachment.service.ts | 123 +------- .../attachment/services/front/index.ts | 70 ----- .../attachment/services/front/mappers.ts | 43 --- .../attachment/services/front/types.ts | 5 - .../attachment/services/github/index.ts | 69 ----- .../attachment/services/github/mappers.ts | 28 -- .../attachment/services/github/types.ts | 6 - .../attachment/services/zendesk/index.ts | 80 ------ .../attachment/services/zendesk/mappers.ts | 43 --- .../attachment/services/zendesk/types.ts | 7 - .../ticketing/attachment/sync/sync.service.ts | 267 ------------------ .../attachment/types/model.unified.ts | 1 - .../ticketing/comment/services/front/index.ts | 24 +- .../comment/services/front/mappers.ts | 4 +- .../comment/services/github/index.ts | 4 +- .../comment/services/hubspot/index.ts | 4 +- .../comment/services/zendesk/index.ts | 39 ++- .../comment/services/zendesk/mappers.ts | 6 +- .../ticketing/ticket/services/front/index.ts | 20 +- .../ticket/services/front/mappers.ts | 6 +- .../ticket/services/zendesk/index.ts | 37 ++- .../ticket/services/zendesk/mappers.ts | 6 +- .../api/src/ticketing/ticket/utils/index.ts | 36 --- .../api/src/ticketing/ticketing.module.ts | 5 +- 24 files changed, 129 insertions(+), 804 deletions(-) delete mode 100644 packages/api/src/ticketing/attachment/services/front/index.ts delete mode 100644 packages/api/src/ticketing/attachment/services/front/mappers.ts delete mode 100644 packages/api/src/ticketing/attachment/services/front/types.ts delete mode 100644 packages/api/src/ticketing/attachment/services/github/index.ts delete mode 100644 packages/api/src/ticketing/attachment/services/github/mappers.ts delete mode 100644 packages/api/src/ticketing/attachment/services/github/types.ts delete mode 100644 packages/api/src/ticketing/attachment/services/zendesk/index.ts delete mode 100644 packages/api/src/ticketing/attachment/services/zendesk/mappers.ts delete mode 100644 packages/api/src/ticketing/attachment/services/zendesk/types.ts delete mode 100644 packages/api/src/ticketing/attachment/sync/sync.service.ts diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index 9875ff5e0..f5c729ea1 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -9,13 +9,7 @@ import { UnifiedAttachmentInput, UnifiedAttachmentOutput, } from '../types/model.unified'; -import { AttachmentResponse, IAttachmentService } from '../types'; -import { desunify } from '@@core/utils/unification/desunify'; -import { TicketingObject } from '@ticketing/@utils/@types'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { unify } from '@@core/utils/unification/unify'; -import { ServiceRegistry } from './registry.service'; -import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; +import { AttachmentResponse } from '../types'; @Injectable() export class AttachmentService { @@ -23,8 +17,6 @@ export class AttachmentService { private prisma: PrismaService, private logger: LoggerService, private webhook: WebhookService, - private fieldMappingService: FieldMappingService, - private serviceRegistry: ServiceRegistry, ) { this.logger.setContext(AttachmentService.name); } @@ -81,47 +73,14 @@ export class AttachmentService { }); if (!linkedUser) throw new Error('Linked User Not Found'); - //TODO - // Retrieve custom field mappings - // get potential fieldMappings and extract the original properties name - const customFieldMappings = - await this.fieldMappingService.getCustomFieldMappings( - integrationId, - linkedUserId, - 'attachment', - ); - //desunify the data according to the target obj wanted - const desunifiedObject = await desunify({ - sourceObject: unifiedAttachmentData, - targetType: TicketingObject.attachment, - providerName: integrationId, - customFieldMappings: unifiedAttachmentData.field_mappings - ? customFieldMappings - : [], - }); - - const service: IAttachmentService = - this.serviceRegistry.getService(integrationId); - const resp: ApiResponse = - await service.addAttachment(desunifiedObject, linkedUserId); - - //unify the data according to the target obj wanted - const unifiedObject = (await unify({ - sourceObject: [resp.data], - targetType: TicketingObject.attachment, - providerName: integrationId, - customFieldMappings: customFieldMappings, - })) as UnifiedAttachmentOutput[]; + //EXCEPTION: for Attachments we directly store them inside our db (no raw call to the provider) + //the actual job to retrieve the attachment info would be done inside /comments // add the attachment inside our db - const source_attachment = resp.data; - const target_attachment = unifiedObject[0]; - const originId = - 'id' in source_attachment ? String(source_attachment.id) : undefined; //TODO const existingAttachment = await this.prisma.tcg_attachments.findFirst({ where: { - remote_id: originId, + file_name: unifiedAttachmentData.file_name, remote_platform: integrationId, id_linked_user: linkedUserId, }, @@ -136,22 +95,20 @@ export class AttachmentService { id_tcg_attachment: existingAttachment.id_tcg_attachment, }, data: { - //TODO + file_name: unifiedAttachmentData.file_name, modified_at: new Date(), }, }); unique_ticketing_attachment_id = res.id_tcg_attachment; } else { // Create a new attachment - this.logger.log('not existing attachment ' + target_attachment); + this.logger.log('not existing attachment '); const data = { id_tcg_attachment: uuidv4(), - //TODO - + file_name: unifiedAttachmentData.file_name, created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, - remote_id: originId, remote_platform: integrationId, }; @@ -161,77 +118,15 @@ export class AttachmentService { unique_ticketing_attachment_id = res.id_tcg_attachment; } - // check duplicate or existing values - if ( - target_attachment.field_mappings && - target_attachment.field_mappings.length > 0 - ) { - const entity = await this.prisma.entity.create({ - data: { - id_entity: uuidv4(), - ressource_owner_id: unique_ticketing_attachment_id, - }, - }); - - for (const mapping of target_attachment.field_mappings) { - const attribute = await this.prisma.attribute.findFirst({ - where: { - slug: Object.keys(mapping)[0], - source: integrationId, - id_consumer: linkedUserId, - }, - }); - - if (attribute) { - await this.prisma.value.create({ - data: { - id_value: uuidv4(), - data: Object.values(mapping)[0] || 'null', - attribute: { - connect: { - id_attribute: attribute.id_attribute, - }, - }, - entity: { - connect: { - id_entity: entity.id_entity, - }, - }, - }, - }); - } - } - } - if (remote_data) { - //insert remote_data in db - await this.prisma.remote_data.upsert({ - where: { - ressource_owner_id: unique_ticketing_attachment_id, - }, - create: { - id_remote_data: uuidv4(), - ressource_owner_id: unique_ticketing_attachment_id, - format: 'json', - data: JSON.stringify(source_attachment), - created_at: new Date(), - }, - update: { - data: JSON.stringify(source_attachment), - created_at: new Date(), - }, - }); - } - const result_attachment = await this.getAttachment( unique_ticketing_attachment_id, remote_data, ); - const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; const event = await this.prisma.events.create({ data: { id_event: uuidv4(), - status: status_resp, + status: 'success', type: 'ticketing.attachment.push', //sync, push or pull method: 'POST', url: '/ticketing/attachment', @@ -248,7 +143,7 @@ export class AttachmentService { linkedUser.id_project, event.id_event, ); - return { ...resp, data: result_attachment.data }; + return { statusCode: 201, data: result_attachment.data }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/attachment/services/front/index.ts b/packages/api/src/ticketing/attachment/services/front/index.ts deleted file mode 100644 index 0714ce137..000000000 --- a/packages/api/src/ticketing/attachment/services/front/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -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 { IAttachmentService } from '@ticketing/attachment/types'; -import { FrontAttachmentInput, FrontAttachmentOutput } from './types'; - -@Injectable() -export class FrontService implements IAttachmentService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private cryptoService: EncryptionService, - private registry: ServiceRegistry, - ) { - this.logger.setContext( - TicketingObject.attachment.toUpperCase() + ':' + FrontService.name, - ); - this.registry.registerService('front', this); - } - - async addAttachment( - attachmentData: FrontAttachmentInput, - linkedUserId: string, - ): Promise> { - return; - } - - async syncAttachments( - linkedUserId: string, - ): Promise> { - try { - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'front', - }, - }); - - const resp = await axios.get('https://api2.frontapp.com/teammates', { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }); - this.logger.log(`Synced front attachments !`); - - return { - data: resp.data._results, - message: 'Front attachments retrieved', - statusCode: 200, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Front', - TicketingObject.attachment, - ActionType.GET, - ); - } - } -} diff --git a/packages/api/src/ticketing/attachment/services/front/mappers.ts b/packages/api/src/ticketing/attachment/services/front/mappers.ts deleted file mode 100644 index 164cd3ef4..000000000 --- a/packages/api/src/ticketing/attachment/services/front/mappers.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IAttachmentMapper } from '@ticketing/attachment/types'; -import { FrontAttachmentInput, FrontAttachmentOutput } from './types'; -import { - UnifiedAttachmentInput, - UnifiedAttachmentOutput, -} from '@ticketing/attachment/types/model.unified'; - -export class FrontAttachmentMapper implements IAttachmentMapper { - desunify( - source: UnifiedAttachmentInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): FrontAttachmentInput { - return; - } - - unify( - source: FrontAttachmentOutput | FrontAttachmentOutput[], - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedAttachmentOutput | UnifiedAttachmentOutput[] { - // If the source is not an array, convert it to an array for mapping - const sourcesArray = Array.isArray(source) ? source : [source]; - - return sourcesArray.map((ticket) => - this.mapSingleTicketToUnified(ticket, customFieldMappings), - ); - } - - private mapSingleTicketToUnified( - ticket: FrontAttachmentOutput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedAttachmentOutput { - return; - } -} diff --git a/packages/api/src/ticketing/attachment/services/front/types.ts b/packages/api/src/ticketing/attachment/services/front/types.ts deleted file mode 100644 index 6bdd606aa..000000000 --- a/packages/api/src/ticketing/attachment/services/front/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type FrontAttachmentInput = { - id: string; -}; - -export type FrontAttachmentOutput = FrontAttachmentInput; diff --git a/packages/api/src/ticketing/attachment/services/github/index.ts b/packages/api/src/ticketing/attachment/services/github/index.ts deleted file mode 100644 index 63e842989..000000000 --- a/packages/api/src/ticketing/attachment/services/github/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -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 { IAttachmentService } from '@ticketing/attachment/types'; -import { GithubAttachmentInput, GithubAttachmentOutput } from './types'; - -//TODO -@Injectable() -export class GithubService implements IAttachmentService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private cryptoService: EncryptionService, - private registry: ServiceRegistry, - ) { - this.logger.setContext( - TicketingObject.attachment.toUpperCase() + ':' + GithubService.name, - ); - this.registry.registerService('github', this); - } - async addAttachment( - attachmentData: GithubAttachmentInput, - linkedUserId: string, - ): Promise> { - return; - } - async syncAttachments( - linkedUserId: string, - custom_properties?: string[], - ): Promise> { - try { - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'github', - }, - }); - const resp = await axios.get(`https://api.github.com/attachments`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }); - this.logger.log(`Synced github attachments !`); - - return { - data: resp.data, - message: 'Github attachments retrieved', - statusCode: 200, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Github', - TicketingObject.attachment, - ActionType.GET, - ); - } - } -} diff --git a/packages/api/src/ticketing/attachment/services/github/mappers.ts b/packages/api/src/ticketing/attachment/services/github/mappers.ts deleted file mode 100644 index e90772e67..000000000 --- a/packages/api/src/ticketing/attachment/services/github/mappers.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { IAttachmentMapper } from '@ticketing/attachment/types'; -import { GithubAttachmentInput, GithubAttachmentOutput } from './types'; -import { - UnifiedAttachmentInput, - UnifiedAttachmentOutput, -} from '@ticketing/attachment/types/model.unified'; - -export class GithubAttachmentMapper implements IAttachmentMapper { - desunify( - source: UnifiedAttachmentInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): GithubAttachmentInput { - return; - } - - unify( - source: GithubAttachmentOutput | GithubAttachmentOutput[], - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedAttachmentOutput | UnifiedAttachmentOutput[] { - return; - } -} diff --git a/packages/api/src/ticketing/attachment/services/github/types.ts b/packages/api/src/ticketing/attachment/services/github/types.ts deleted file mode 100644 index 690d2fb75..000000000 --- a/packages/api/src/ticketing/attachment/services/github/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type GithubAttachmentInput = { - name: string; -}; - -//TODO -export type GithubAttachmentOutput = GithubAttachmentInput; diff --git a/packages/api/src/ticketing/attachment/services/zendesk/index.ts b/packages/api/src/ticketing/attachment/services/zendesk/index.ts deleted file mode 100644 index af9af88b4..000000000 --- a/packages/api/src/ticketing/attachment/services/zendesk/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -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, - ZendeskAttachmentInput, - ZendeskAttachmentOutput, -} from '@ticketing/@utils/@types'; -import { ApiResponse } from '@@core/utils/types'; -import axios from 'axios'; -import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { EnvironmentService } from '@@core/environment/environment.service'; -import { ServiceRegistry } from '../registry.service'; -import { IAttachmentService } from '@ticketing/attachment/types'; -import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; - -@Injectable() -export class ZendeskService implements IAttachmentService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private cryptoService: EncryptionService, - private env: EnvironmentService, - private registry: ServiceRegistry, - ) { - this.logger.setContext( - TicketingObject.attachment.toUpperCase() + ':' + ZendeskService.name, - ); - this.registry.registerService('zendesk_t', this); - } - - addAttachment( - attachmentData: ZendeskAttachmentInput, - linkedUserId: string, - ): Promise> { - return; - } - async syncAttachments( - linkedUserId: string, - custom_properties?: string[], - ): Promise> { - try { - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', - }, - }); - - const resp = await axios.get( - `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/attachments`, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }, - ); - this.logger.log(`Synced zendesk attachments !`); - - return { - data: resp.data.attachments, - message: 'Zendesk attachments retrieved', - statusCode: 200, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Zendesk', - TicketingObject.attachment, - ActionType.GET, - ); - } - } -} diff --git a/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts b/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts deleted file mode 100644 index 8e9e402b8..000000000 --- a/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IAttachmentMapper } from '@ticketing/attachment/types'; -import { ZendeskAttachmentInput, ZendeskAttachmentOutput } from './types'; -import { - UnifiedAttachmentInput, - UnifiedAttachmentOutput, -} from '@ticketing/attachment/types/model.unified'; - -export class ZendeskAttachmentMapper implements IAttachmentMapper { - desunify( - source: UnifiedAttachmentInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): ZendeskAttachmentInput { - return; - } - - unify( - source: ZendeskAttachmentOutput | ZendeskAttachmentOutput[], - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedAttachmentOutput | UnifiedAttachmentOutput[] { - if (!Array.isArray(source)) { - return this.mapSingleAttachmentToUnified(source, customFieldMappings); - } - return source.map((ticket) => - this.mapSingleAttachmentToUnified(ticket, customFieldMappings), - ); - } - - private mapSingleAttachmentToUnified( - ticket: ZendeskAttachmentOutput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedAttachmentOutput { - return; - } -} diff --git a/packages/api/src/ticketing/attachment/services/zendesk/types.ts b/packages/api/src/ticketing/attachment/services/zendesk/types.ts deleted file mode 100644 index 97083238d..000000000 --- a/packages/api/src/ticketing/attachment/services/zendesk/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type ZendeskAttachmentInput = { - _: string; -}; - -export type ZendeskAttachmentOutput = ZendeskAttachmentInput & { - id: number; // Read-only. Automatically assigned when the ticket is created. -}; diff --git a/packages/api/src/ticketing/attachment/sync/sync.service.ts b/packages/api/src/ticketing/attachment/sync/sync.service.ts deleted file mode 100644 index e7705ec38..000000000 --- a/packages/api/src/ticketing/attachment/sync/sync.service.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { LoggerService } from '@@core/logger/logger.service'; -import { PrismaService } from '@@core/prisma/prisma.service'; -import { NotFoundError, handleServiceError } from '@@core/utils/errors'; -import { Cron } from '@nestjs/schedule'; -import { ApiResponse, TICKETING_PROVIDERS } from '@@core/utils/types'; -import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { unify } from '@@core/utils/unification/unify'; -import { TicketingObject } from '@ticketing/@utils/@types'; -import { WebhookService } from '@@core/webhook/webhook.service'; -import { UnifiedAttachmentOutput } from '../types/model.unified'; -import { IAttachmentService } from '../types'; -import { ServiceRegistry } from '../services/registry.service'; -import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; -import { tcg_attachments as TicketingAttachment } from '@prisma/client'; - -@Injectable() -export class SyncService implements OnModuleInit { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private webhook: WebhookService, - private fieldMappingService: FieldMappingService, - private serviceRegistry: ServiceRegistry, - ) { - this.logger.setContext(SyncService.name); - } - - async onModuleInit() { - try { - await this.syncAttachments(); - } catch (error) { - handleServiceError(error, this.logger); - } - } - - @Cron('*/20 * * * *') - //function used by sync worker which populate our tcg_attachments table - //its role is to fetch all attachments from providers 3rd parties and save the info inside our db - async syncAttachments() { - try { - this.logger.log(`Syncing attachments....`); - const defaultOrg = await this.prisma.organizations.findFirst({ - where: { - name: 'Acme Inc', - }, - }); - - const defaultProject = await this.prisma.projects.findFirst({ - where: { - id_organization: defaultOrg.id_organization, - name: 'Project 1', - }, - }); - const id_project = defaultProject.id_project; - const linkedUsers = await this.prisma.linked_users.findMany({ - where: { - id_project: id_project, - }, - }); - linkedUsers.map(async (linkedUser) => { - try { - const providers = TICKETING_PROVIDERS; - for (const provider of providers) { - try { - //call the sync attachments for every ticket of the linkedUser (a attachment is tied to a ticket) - const tickets = await this.prisma.tcg_tickets.findMany({ - where: { - remote_platform: provider, - events: { - id_linked_user: linkedUser.id_linked_user, - }, - }, - }); - for (const ticket of tickets) { - await this.syncAttachmentsForLinkedUser( - provider, - linkedUser.id_linked_user, - id_project, - ticket.id_tcg_ticket, - ); - } - } catch (error) { - handleServiceError(error, this.logger); - } - } - } catch (error) { - handleServiceError(error, this.logger); - } - }); - } catch (error) { - handleServiceError(error, this.logger); - } - } - - //todo: HANDLE DATA REMOVED FROM PROVIDER - async syncAttachmentsForLinkedUser( - integrationId: string, - linkedUserId: string, - id_project: string, - id_ticket: string, - ) { - try { - this.logger.log( - `Syncing ${integrationId} attachments for linkedUser ${linkedUserId}`, - ); - // check if linkedUser has a connection if not just stop sync - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: integrationId, - }, - }); - if (!connection) throw new Error('connection not found'); - - // get potential fieldMappings and extract the original properties name - const customFieldMappings = - await this.fieldMappingService.getCustomFieldMappings( - integrationId, - linkedUserId, - 'attachment', - ); - const remoteProperties: string[] = customFieldMappings.map( - (mapping) => mapping.remote_id, - ); - - const service: IAttachmentService = - this.serviceRegistry.getService(integrationId); - const resp: ApiResponse = - await service.syncAttachments(linkedUserId, remoteProperties); - - const sourceObject: OriginalAttachmentOutput[] = resp.data; - //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); - //unify the data according to the target obj wanted - const unifiedObject = (await unify({ - sourceObject, - targetType: TicketingObject.attachment, - providerName: integrationId, - customFieldMappings, - })) as UnifiedAttachmentOutput[]; - - //TODO - const attachmentsIds = sourceObject.map((attachment) => - 'id' in attachment ? String(attachment.id) : undefined, - ); - //insert the data in the DB with the fieldMappings (value table) - const attachments_data = await this.saveAttachmentsInDb( - linkedUserId, - unifiedObject, - attachmentsIds, - integrationId, - id_ticket, - sourceObject, - ); - const event = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'success', - type: 'ticketing.attachment.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - await this.webhook.handleWebhook( - attachments_data, - 'ticketing.attachment.pulled', - id_project, - event.id_event, - ); - } catch (error) { - handleServiceError(error, this.logger); - } - } - - async saveAttachmentsInDb( - linkedUserId: string, - attachments: UnifiedAttachmentOutput[], - originIds: string[], - originSource: string, - id_ticket: string, - remote_data: Record[], - ): Promise { - try { - let attachments_results: TicketingAttachment[] = []; - for (let i = 0; i < attachments.length; i++) { - const attachment = attachments[i]; - const originId = originIds[i]; - - if (!originId || originId == '') { - throw new NotFoundError(`Origin id not there, found ${originId}`); - } - - const existingAttachment = await this.prisma.tcg_attachments.findFirst({ - where: { - remote_id: originId, - remote_platform: originSource, - id_linked_user: linkedUserId, - }, - }); - - let unique_ticketing_attachment_id: string; - - if (existingAttachment) { - // Update the existing attachment - const res = await this.prisma.tcg_attachments.update({ - where: { - id_tcg_attachment: existingAttachment.id_tcg_attachment, - }, - data: { - //TODO - id_tcg_ticket: id_ticket, - modified_at: new Date(), - }, - }); - unique_ticketing_attachment_id = res.id_tcg_attachment; - attachments_results = [...attachments_results, res]; - } else { - // Create a new attachment - this.logger.log('attachment not exists'); - const data = { - id_tcg_attachment: uuidv4(), - //TODO - created_at: new Date(), - modified_at: new Date(), - id_tcg_ticket: id_ticket, - id_linkedUser: linkedUserId, - remote_id: originId, - remote_platform: originSource, - //TODO; id_tcg_contact String? @db.Uuid - //TODO; id_tcg_user String? @db.Uuid - }; - const res = await this.prisma.tcg_attachments.create({ - data: data, - }); - attachments_results = [...attachments_results, res]; - unique_ticketing_attachment_id = res.id_tcg_attachment; - } - - //insert remote_data in db - await this.prisma.remote_data.upsert({ - where: { - ressource_owner_id: unique_ticketing_attachment_id, - }, - create: { - id_remote_data: uuidv4(), - ressource_owner_id: unique_ticketing_attachment_id, - format: 'json', - data: JSON.stringify(remote_data[i]), - created_at: new Date(), - }, - update: { - data: JSON.stringify(remote_data[i]), - created_at: new Date(), - }, - }); - } - return attachments_results; - } catch (error) { - handleServiceError(error, this.logger); - } - } -} diff --git a/packages/api/src/ticketing/attachment/types/model.unified.ts b/packages/api/src/ticketing/attachment/types/model.unified.ts index 90785cde2..a931ecf27 100644 --- a/packages/api/src/ticketing/attachment/types/model.unified.ts +++ b/packages/api/src/ticketing/attachment/types/model.unified.ts @@ -1,6 +1,5 @@ export class UnifiedAttachmentInput { file_name: string; - ticket_id?: string; file_url: string; uploader?: string; field_mappings?: Record[]; diff --git a/packages/api/src/ticketing/comment/services/front/index.ts b/packages/api/src/ticketing/comment/services/front/index.ts index a0663be98..5487b027c 100644 --- a/packages/api/src/ticketing/comment/services/front/index.ts +++ b/packages/api/src/ticketing/comment/services/front/index.ts @@ -3,12 +3,11 @@ 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 { DesunifyReturnType } from '@@core/utils/types/desunify.input'; import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; import { ICommentService } from '@ticketing/comment/types'; import { TicketingObject } from '@ticketing/@utils/@types'; -import { FrontCommentOutput } from './types'; +import { FrontCommentInput, FrontCommentOutput } from './types'; import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; @@ -26,7 +25,7 @@ export class FrontService implements ICommentService { this.registry.registerService('front', this); } async addComment( - commentData: DesunifyReturnType, + commentData: FrontCommentInput, linkedUserId: string, remoteIdTicket: string, ): Promise> { @@ -38,7 +37,24 @@ export class FrontService implements ICommentService { provider_slug: 'front', }, }); - const dataBody = commentData; + const uuids = commentData.attachments; + let uploads = []; + uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); + //TODO: construct the right binary attachment + //get the AWS s3 right file + const url = res.file_url; + uploads = [...uploads, url]; + }); + const dataBody = { + ...commentData, + attachments: uploads, + }; const resp = await axios.post( `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, JSON.stringify(dataBody), diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts index c9109bae0..0e0b55210 100644 --- a/packages/api/src/ticketing/comment/services/front/mappers.ts +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -19,9 +19,7 @@ export class FrontCommentMapper implements ICommentMapper { const result: FrontCommentInput = { body: source.body, author_id: source.user_id || source.contact_id, //TODO: make sure either one is passed - attachments: source.attachments - ? await this.utils.get_Front_AttachmentsFromUuid(source.attachments) - : [], + attachments: source.attachments, }; return result; } diff --git a/packages/api/src/ticketing/comment/services/github/index.ts b/packages/api/src/ticketing/comment/services/github/index.ts index 0ce4de98b..8c1dae630 100644 --- a/packages/api/src/ticketing/comment/services/github/index.ts +++ b/packages/api/src/ticketing/comment/services/github/index.ts @@ -10,7 +10,7 @@ import { ICommentService } from '@ticketing/comment/types'; import { TicketingObject } from '@ticketing/@utils/@types'; import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; -import { GithubCommentOutput } from './types'; +import { GithubCommentInput, GithubCommentOutput } from './types'; @Injectable() export class GithubService implements ICommentService { @@ -26,7 +26,7 @@ export class GithubService implements ICommentService { this.registry.registerService('github', this); } async addComment( - commentData: DesunifyReturnType, + commentData: GithubCommentInput, linkedUserId: string, remoteIdTicket: string, ): Promise> { diff --git a/packages/api/src/ticketing/comment/services/hubspot/index.ts b/packages/api/src/ticketing/comment/services/hubspot/index.ts index ccab00f34..d993b0e44 100644 --- a/packages/api/src/ticketing/comment/services/hubspot/index.ts +++ b/packages/api/src/ticketing/comment/services/hubspot/index.ts @@ -10,7 +10,7 @@ import { ICommentService } from '@ticketing/comment/types'; import { TicketingObject } from '@ticketing/@utils/@types'; import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; -import { HubspotCommentOutput } from './types'; +import { HubspotCommentInput, HubspotCommentOutput } from './types'; @Injectable() export class HubspotService implements ICommentService { @@ -26,7 +26,7 @@ export class HubspotService implements ICommentService { this.registry.registerService('hubspot_t', this); } async addComment( - commentData: DesunifyReturnType, + commentData: HubspotCommentInput, linkedUserId: string, remoteIdTicket: string, ): Promise> { diff --git a/packages/api/src/ticketing/comment/services/zendesk/index.ts b/packages/api/src/ticketing/comment/services/zendesk/index.ts index 1a94351c2..8ce85b684 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/index.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/index.ts @@ -10,7 +10,7 @@ import { ICommentService } from '@ticketing/comment/types'; import { TicketingObject } from '@ticketing/@utils/@types'; import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; -import { ZendeskCommentOutput } from './types'; +import { ZendeskCommentInput, ZendeskCommentOutput } from './types'; import { EnvironmentService } from '@@core/environment/environment.service'; @Injectable() export class ZendeskService implements ICommentService { @@ -26,8 +26,9 @@ export class ZendeskService implements ICommentService { ); this.registry.registerService('zendesk_t', this); } + async addComment( - commentData: DesunifyReturnType, + commentData: ZendeskCommentInput, linkedUserId: string, remoteIdTicket: string, ): Promise> { @@ -38,9 +39,41 @@ export class ZendeskService implements ICommentService { provider_slug: 'zendesk_t', }, }); + + // We must fetch tokens from zendesk with the commentData.uploads array of Attachment uuids + const uuids = commentData.uploads; + let uploads = []; + uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); + + //TODO:; fetch the right file from AWS s3 + const s3File = ''; + const url = `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/uploads.json?filename=${ + res.file_name + }`; + + const resp = await axios.get(url, { + headers: { + 'Content-Type': 'image/png', //TODO: get the right content-type given a file name extension + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + uploads = [...uploads, resp.data.upload.token]; + }); + const finalData = { + ...commentData, + uploads: uploads, + }; const dataBody = { ticket: { - comment: commentData, + comment: finalData, }, }; //to add a comment on Zendesk you must update a ticket using the Ticket API diff --git a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts index 87190227b..c6cabb783 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts @@ -26,11 +26,7 @@ export class ZendeskCommentMapper implements ICommentMapper { ? parseInt(source.contact_id) : undefined, //TODO: make sure either one is passed type: 'Comment', - uploads: source.attachments - ? await this.utils.get_Zendesk_AttachmentsTokensFromUuid( - source.attachments, - ) - : [], //fetch token attachments for this uuid + uploads: source.attachments, //we let the array of uuids on purpose (it will be modified in the given service on the fly!) }; return result; diff --git a/packages/api/src/ticketing/ticket/services/front/index.ts b/packages/api/src/ticketing/ticket/services/front/index.ts index 59311a1d3..6d18d12c6 100644 --- a/packages/api/src/ticketing/ticket/services/front/index.ts +++ b/packages/api/src/ticketing/ticket/services/front/index.ts @@ -34,7 +34,25 @@ export class FrontService implements ITicketService { provider_slug: 'front', }, }); - const dataBody = ticketData; + + const uuids = ticketData.comment.attachments; + let uploads = []; + uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); + //TODO: construct the right binary attachment + //get the AWS s3 right file + const url = res.file_url; + uploads = [...uploads, url]; + }); + const dataBody = { + ...ticketData, + comment: { ...ticketData.comment, attachments: uploads }, + }; const resp = await axios.post( `https://api2.frontapp.com/conversations`, JSON.stringify(dataBody), diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index 8cd400dbd..c241fb025 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -26,11 +26,7 @@ export class FrontTicketMapper implements ITicketMapper { source.comment.creator_type === 'user' ? source.comment.user_id : source.comment.contact_id, - attachments: source.comment.attachments - ? await this.utils.get_Front_AttachmentsFromUuid( - source.comment.attachments, - ) - : [], + attachments: source.comment.attachments, }, }; diff --git a/packages/api/src/ticketing/ticket/services/zendesk/index.ts b/packages/api/src/ticketing/ticket/services/zendesk/index.ts index 947ed3bd4..bcbaaa4fb 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/index.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/index.ts @@ -39,8 +39,43 @@ export class ZendeskService implements ITicketService { provider_slug: 'zendesk_t', }, }); + + // We must fetch tokens from zendesk with the commentData.uploads array of Attachment uuids + const uuids = ticketData.comment.uploads; + let uploads = []; + uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); + + //TODO:; fetch the right file from AWS s3 + const s3File = ''; + const url = `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/uploads.json?filename=${ + res.file_name + }`; + + const resp = await axios.get(url, { + headers: { + 'Content-Type': 'image/png', //TODO: get the right content-type given a file name extension + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + uploads = [...uploads, resp.data.upload.token]; + }); + const finalData = { + ...ticketData, + comment: { + ...ticketData.comment, + uploads: uploads, + }, + }; const dataBody = { - ticket: ticketData, + ticket: finalData, }; const resp = await axios.post( `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tickets.json`, diff --git a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts index f187e4ed6..082916b38 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts @@ -37,11 +37,7 @@ export class ZendeskTicketMapper implements ITicketMapper { body: source.comment.body, html_body: source.comment.html_body, public: !source.comment.is_private, - uploads: source.comment.attachments - ? await this.utils.get_Zendesk_AttachmentsTokensFromUuid( - source.comment.attachments, - ) - : [], //fetch token attachments for this uuid + uploads: source.comment.attachments, //fetch token attachments for this uuid }, }; diff --git a/packages/api/src/ticketing/ticket/utils/index.ts b/packages/api/src/ticketing/ticket/utils/index.ts index adcd25ed3..75dfcc49b 100644 --- a/packages/api/src/ticketing/ticket/utils/index.ts +++ b/packages/api/src/ticketing/ticket/utils/index.ts @@ -2,7 +2,6 @@ import { PrismaClient } from '@prisma/client'; export class Utils { private readonly prisma: PrismaClient; - constructor() { this.prisma = new PrismaClient(); } @@ -18,39 +17,4 @@ export class Utils { return res.email_address; } catch (error) {} } - - async get_Zendesk_AttachmentsTokensFromUuid(uuids: string[]) { - try { - let uploads = []; - uuids.map(async (uuid) => { - const res = await this.prisma.tcg_attachments.findUnique({ - where: { - id_tcg_attachment: uuid, - }, - select: token, - }); - if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); - uploads = [...uploads, res.token]; - }); - return uploads; - } catch (error) {} - } - - async get_Front_AttachmentsFromUuid(uuids: string[]) { - try { - let uploads = []; - uuids.map(async (uuid) => { - const res = await this.prisma.tcg_attachments.findUnique({ - where: { - id_tcg_attachment: uuid, - }, - }); - if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); - //TODO: construct the right binary attachment - const url = res.file_url; - uploads = [...uploads, url]; - }); - return uploads; - } catch (error) {} - } } diff --git a/packages/api/src/ticketing/ticketing.module.ts b/packages/api/src/ticketing/ticketing.module.ts index c293ff4aa..4e1cd7302 100644 --- a/packages/api/src/ticketing/ticketing.module.ts +++ b/packages/api/src/ticketing/ticketing.module.ts @@ -21,9 +21,6 @@ import { TeamModule } from './team/team.module'; ], providers: [], controllers: [], - exports: [ UserModule, - AttachmentModule, - ContactModule, - ], + exports: [UserModule, AttachmentModule, ContactModule], }) export class TicketingModule {} From d43ba6b33689604d4ed4f99090c4adedc3948064 Mon Sep 17 00:00:00 2001 From: nael Date: Fri, 5 Jan 2024 19:43:33 +0100 Subject: [PATCH 15/25] :construction: Fixed some build issues --- .../types/original/original.ticketing.ts | 21 +++--------------- .../api/src/ticketing/@utils/@types/index.ts | 5 ++--- .../ticketing/attachment/attachment.module.ts | 9 -------- .../attachment/types/mappingsTypes.ts | 22 ------------------- 4 files changed, 5 insertions(+), 52 deletions(-) delete mode 100644 packages/api/src/ticketing/attachment/types/mappingsTypes.ts 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 032d5bcaa..6165c6c1c 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -5,8 +5,6 @@ import { ZendeskTicketOutput, ZendeskCommentOutput, ZendeskUserOutput, - ZendeskAttachmentOutput, - ZendeskAttachmentInput, ZendeskAccountInput, ZendeskAccountOutput, ZendeskContactInput, @@ -24,14 +22,7 @@ import { GithubAccountInput, GithubAccountOutput, } from '@ticketing/account/services/github/types'; -import { - FrontAttachmentInput, - FrontAttachmentOutput, -} from '@ticketing/attachment/services/front/types'; -import { - GithubAttachmentInput, - GithubAttachmentOutput, -} from '@ticketing/attachment/services/github/types'; + import { FrontCommentInput, FrontCommentOutput, @@ -129,10 +120,7 @@ export type OriginalTeamInput = | FrontTeamInput; /* attachment */ -export type OriginalAttachmentInput = - | ZendeskAttachmentInput - | FrontAttachmentInput - | GithubAttachmentInput; +export type OriginalAttachmentInput = null; export type TicketingObjectInput = | OriginalTicketInput @@ -187,10 +175,7 @@ export type OriginalTeamOutput = | FrontTeamOutput; /* attachment */ -export type OriginalAttachmentOutput = - | ZendeskAttachmentOutput - | FrontAttachmentOutput - | GithubAttachmentOutput; +export type OriginalAttachmentOutput = null; export type TicketingObjectOutput = | OriginalTicketOutput diff --git a/packages/api/src/ticketing/@utils/@types/index.ts b/packages/api/src/ticketing/@utils/@types/index.ts index 079ceee92..94e9d2fdf 100644 --- a/packages/api/src/ticketing/@utils/@types/index.ts +++ b/packages/api/src/ticketing/@utils/@types/index.ts @@ -6,7 +6,6 @@ import { UnifiedAccountOutput, } from '@ticketing/account/types/model.unified'; import { IAttachmentService } from '@ticketing/attachment/types'; -import { attachmentUnificationMapping } from '@ticketing/attachment/types/mappingsTypes'; import { UnifiedAttachmentInput, UnifiedAttachmentOutput, @@ -84,7 +83,6 @@ export const unificationMapping = { [TicketingObject.contact]: contactUnificationMapping, [TicketingObject.team]: teamUnificationMapping, [TicketingObject.tag]: tagUnificationMapping, - [TicketingObject.attachment]: attachmentUnificationMapping, }; export type ITicketingService = @@ -96,11 +94,12 @@ export type ITicketingService = | IAccountService | ITeamService | ITagService; + +//TODO; export everything export * from '../../ticket/services/zendesk/types'; export * from '../../comment/services/zendesk/types'; export * from '../../user/services/zendesk/types'; export * from '../../contact/services/zendesk/types'; -export * from '../../attachment/services/zendesk/types'; export * from '../../account/services/zendesk/types'; export * from '../../team/services/zendesk/types'; export * from '../../tag/services/zendesk/types'; diff --git a/packages/api/src/ticketing/attachment/attachment.module.ts b/packages/api/src/ticketing/attachment/attachment.module.ts index e28533cea..935a8bb6a 100644 --- a/packages/api/src/ticketing/attachment/attachment.module.ts +++ b/packages/api/src/ticketing/attachment/attachment.module.ts @@ -1,8 +1,6 @@ import { Module } from '@nestjs/common'; import { AttachmentController } from './attachment.controller'; -import { SyncService } from './sync/sync.service'; import { LoggerService } from '@@core/logger/logger.service'; -import { ZendeskService } from './services/zendesk'; import { AttachmentService } from './services/attachment.service'; import { ServiceRegistry } from './services/registry.service'; import { EncryptionService } from '@@core/encryption/encryption.service'; @@ -10,8 +8,6 @@ import { FieldMappingService } from '@@core/field-mapping/field-mapping.service' import { PrismaService } from '@@core/prisma/prisma.service'; import { WebhookService } from '@@core/webhook/webhook.service'; import { BullModule } from '@nestjs/bull'; -import { GithubService } from './services/github'; -import { FrontService } from './services/front'; @Module({ imports: [ @@ -24,16 +20,11 @@ import { FrontService } from './services/front'; AttachmentService, PrismaService, LoggerService, - SyncService, WebhookService, EncryptionService, FieldMappingService, ServiceRegistry, /* PROVIDERS SERVICES */ - ZendeskService, - FrontService, - GithubService, ], - exports: [SyncService], }) export class AttachmentModule {} diff --git a/packages/api/src/ticketing/attachment/types/mappingsTypes.ts b/packages/api/src/ticketing/attachment/types/mappingsTypes.ts deleted file mode 100644 index 267326725..000000000 --- a/packages/api/src/ticketing/attachment/types/mappingsTypes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { FrontAttachmentMapper } from '../services/front/mappers'; -import { GithubAttachmentMapper } from '../services/github/mappers'; -import { ZendeskAttachmentMapper } from '../services/zendesk/mappers'; - -const zendeskAttachmentMapper = new ZendeskAttachmentMapper(); -const frontAttachmentMapper = new FrontAttachmentMapper(); -const githubAttachmentMapper = new GithubAttachmentMapper(); - -export const attachmentUnificationMapping = { - zendesk: { - unify: zendeskAttachmentMapper.unify, - desunify: zendeskAttachmentMapper.desunify, - }, - front: { - unify: frontAttachmentMapper.unify, - desunify: frontAttachmentMapper.desunify, - }, - github: { - unify: githubAttachmentMapper.unify, - desunify: githubAttachmentMapper.desunify, - }, -}; From 16d7435e1a9b16981d9e968d8fc0054034d5b738 Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 6 Jan 2024 12:36:59 +0100 Subject: [PATCH 16/25] :construction: BUILD works --- packages/api/prisma/schema.prisma | 55 ++++++++++++++----- packages/api/scripts/webhook.testing.ts | 2 +- .../api/src/@core/webhook/webhook.service.ts | 4 +- .../crm/contact/services/contact.service.ts | 4 +- .../ticketing/account/sync/sync.service.ts | 2 +- .../attachment/services/attachment.service.ts | 9 ++- .../comment/services/comment.service.ts | 2 - .../src/ticketing/tag/services/tag.service.ts | 4 +- 8 files changed, 53 insertions(+), 29 deletions(-) diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index ea52e0ab5..e2bda898d 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -91,6 +91,7 @@ model crm_companies { modified_at DateTime @db.Timestamp(6) id_crm_user String? @db.Uuid id_event String @db.Uuid + id_linked_user String? @db.Uuid crm_addresses crm_addresses[] events events @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_13") crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_24") @@ -110,18 +111,16 @@ model crm_contacts { last_name String created_at DateTime @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) - remote_platform String remote_id String + remote_platform String id_crm_user String? @db.Uuid - id_event String @db.Uuid + id_linked_user String? @db.Uuid crm_addresses crm_addresses[] crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_23") - events events @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "job_id_crm_contact") crm_email_addresses crm_email_addresses[] crm_notes crm_notes[] crm_phone_numbers crm_phone_numbers[] - @@index([id_event], map: "crm_contact_id_job") @@index([id_crm_user], map: "fk_crm_contact_userid") } @@ -135,6 +134,7 @@ model crm_deals { modified_at DateTime @db.Timestamp(6) id_crm_user String? @db.Uuid id_crm_deals_stage String? @db.Uuid + id_linked_user String? @db.Uuid crm_deals_stages crm_deals_stages? @relation(fields: [id_crm_deals_stage], references: [id_crm_deals_stage], onDelete: NoAction, onUpdate: NoAction, map: "fk_21") crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_22") crm_notes crm_notes[] @@ -149,6 +149,7 @@ model crm_deals_stages { stage_name String? created_at DateTime @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) + id_linked_user String? @db.Uuid crm_deals crm_deals[] } @@ -202,6 +203,7 @@ model crm_engagements { remote_id String? id_crm_engagement_type String @db.Uuid id_crm_company String? @db.Uuid + id_linked_user String? @db.Uuid crm_engagement_contacts crm_engagement_contacts[] crm_engagement_types crm_engagement_types @relation(fields: [id_crm_engagement_type], references: [id_crm_engagement_type], onDelete: NoAction, onUpdate: NoAction, map: "fk_28") crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_29") @@ -218,6 +220,7 @@ model crm_notes { id_crm_company String? @db.Uuid id_crm_contact String? @db.Uuid id_crm_deal String? @db.Uuid + id_linked_user String? @db.Uuid crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_18") crm_contacts crm_contacts? @relation(fields: [id_crm_contact], references: [id_crm_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_19") crm_deals crm_deals? @relation(fields: [id_crm_deal], references: [id_crm_deal], onDelete: NoAction, onUpdate: NoAction, map: "fk_20") @@ -256,6 +259,7 @@ model crm_tasks { id_crm_user String? @db.Uuid id_crm_company String? @db.Uuid id_crm_deal String? @db.Uuid + id_linked_user String? @db.Uuid crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_25") crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_26") crm_deals crm_deals? @relation(fields: [id_crm_deal], references: [id_crm_deal], onDelete: NoAction, onUpdate: NoAction, map: "fk_27") @@ -266,15 +270,16 @@ model crm_tasks { } model crm_users { - id_crm_user String @id(map: "pk_crm_users") @db.Uuid - name String? - email String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - crm_companies crm_companies[] - crm_contacts crm_contacts[] - crm_deals crm_deals[] - crm_tasks crm_tasks[] + id_crm_user String @id(map: "pk_crm_users") @db.Uuid + name String? + email String? + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) + id_linked_user String? @db.Uuid + crm_companies crm_companies[] + crm_contacts crm_contacts[] + crm_deals crm_deals[] + crm_tasks crm_tasks[] } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments @@ -297,7 +302,6 @@ model events { provider String id_linked_user String @db.Uuid crm_companies crm_companies[] - crm_contacts crm_contacts[] linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_12") jobs_status_history jobs_status_history[] tcg_comments tcg_comments[] @@ -393,6 +397,7 @@ model tcg_comments { id_tcg_contact String? @db.Uuid id_tcg_user String? @db.Uuid id_event String? @db.Uuid + id_linked_user String? @db.Uuid tcg_attachments tcg_attachments[] tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_40_1") tcg_contacts tcg_contacts? @relation(fields: [id_tcg_contact], references: [id_tcg_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_41") @@ -417,6 +422,7 @@ model tcg_contacts { modified_at DateTime? @db.Timestamp(6) id_event String? @db.Uuid id_tcg_account String? @db.Uuid + id_linked_user String? @db.Uuid tcg_comments tcg_comments[] events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_43") tcg_accounts tcg_accounts? @relation(fields: [id_tcg_account], references: [id_tcg_account], onDelete: NoAction, onUpdate: NoAction, map: "fk_49") @@ -445,8 +451,10 @@ model tcg_tickets { id_event String? @db.Uuid creator_type String? id_tcg_user String? @db.Uuid + id_linked_user String @db.Uuid tcg_attachments tcg_attachments[] tcg_comments tcg_comments[] + tcg_tags tcg_tags[] events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_44") @@index([id_event], map: "fk_tcg_tickets_eventid") @@ -464,6 +472,7 @@ model tcg_users { teams String[] created_at DateTime? @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) + id_linked_user String? @db.Uuid tcg_comments tcg_comments[] events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_45") @@ -529,7 +538,7 @@ model webhook_endpoints { secret String active Boolean created_at DateTime @db.Timestamp(6) - scope String? + scope String[] id_project String @db.Uuid last_update DateTime? @db.Timestamp(6) webhook_delivery_attempts webhook_delivery_attempts[] @@ -559,6 +568,7 @@ model tcg_accounts { remote_platform String? created_at DateTime @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) + id_linked_user String? @db.Uuid tcg_contacts tcg_contacts[] } @@ -571,6 +581,7 @@ model tcg_teams { description String? created_at DateTime @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) + id_linked_user String? @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments @@ -592,3 +603,17 @@ model tcg_attachments { @@index([id_tcg_comment], map: "fk_tcg_attachment_tcg_commentid") @@index([id_tcg_ticket], map: "fk_tcg_attachment_tcg_ticketid") } + +model tcg_tags { + id_tcg_tag String @id(map: "pk_tcg_tags") @db.Uuid + name String? + remote_id String? + remote_platform String? + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) + id_tcg_ticket String? @db.Uuid + id_linked_user String? @db.Uuid + tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_48") + + @@index([id_tcg_ticket], map: "fk_tcg_tag_tcg_ticketid") +} diff --git a/packages/api/scripts/webhook.testing.ts b/packages/api/scripts/webhook.testing.ts index 3c6d88f7f..bc0975a33 100644 --- a/packages/api/scripts/webhook.testing.ts +++ b/packages/api/scripts/webhook.testing.ts @@ -20,7 +20,7 @@ async function main() { id_webhook_endpoint: 'a18682af-43f6-4ed2-8bde-b84298f51dde', }, data: { - scope: 'crm.contact.pulled', + scope: ['crm.contact.pulled'], }, }); } diff --git a/packages/api/src/@core/webhook/webhook.service.ts b/packages/api/src/@core/webhook/webhook.service.ts index c6c0781f1..09e2995b2 100644 --- a/packages/api/src/@core/webhook/webhook.service.ts +++ b/packages/api/src/@core/webhook/webhook.service.ts @@ -46,7 +46,7 @@ export class WebhookService { active: true, created_at: new Date(), id_project: data.id_project, - scope: JSON.stringify(data.scope), + scope: data.scope, }, }); } catch (error) { @@ -73,7 +73,7 @@ export class WebhookService { if (!webhooks) return; const webhook = webhooks.find((wh) => { - const scopes = JSON.parse(wh.scope); + const scopes = wh.scope; return scopes.includes(eventType); }); diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index 784f22b49..e2a293d87 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -389,9 +389,7 @@ export class ContactService { const contacts = await this.prisma.crm_contacts.findMany({ where: { remote_id: integrationId.toLowerCase(), - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, include: { crm_email_addresses: true, diff --git a/packages/api/src/ticketing/account/sync/sync.service.ts b/packages/api/src/ticketing/account/sync/sync.service.ts index 50712239c..ba509fe07 100644 --- a/packages/api/src/ticketing/account/sync/sync.service.ts +++ b/packages/api/src/ticketing/account/sync/sync.service.ts @@ -185,7 +185,7 @@ export class SyncService implements OnModuleInit { where: { remote_id: originId, remote_platform: originSource, - id_linked_account: linkedUserId, + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index f5c729ea1..510a82631 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -106,6 +106,7 @@ export class AttachmentService { const data = { id_tcg_attachment: uuidv4(), file_name: unifiedAttachmentData.file_name, + uploader: linkedUserId, //TODO created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, @@ -187,7 +188,9 @@ export class AttachmentService { // Transform to UnifiedAttachmentOutput format const unifiedAttachment: UnifiedAttachmentOutput = { id: attachment.id_tcg_attachment, - //TODO + file_name: attachment.file_name, + file_url: attachment.file_url, + uploader: attachment.uploader, //TODO field_mappings: field_mappings, }; @@ -261,7 +264,9 @@ export class AttachmentService { // Transform to UnifiedAttachmentOutput format return { id: attachment.id_tcg_attachment, - //TODO + file_name: attachment.file_name, + file_url: attachment.file_url, + uploader: attachment.uploader, //TODO field_mappings: field_mappings, }; }), diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index 1cb639e4e..6a7ba4f3f 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -319,7 +319,6 @@ export class CommentService { ticket_id: comment.id_tcg_ticket, contact_id: comment.id_tcg_contact, // uuid of Contact object user_id: comment.id_tcg_user, // uuid of User object - attachments: comment.attachments, //uuids of Attachments objects }; let res: CommentResponse = { @@ -401,7 +400,6 @@ export class CommentService { ticket_id: comment.id_tcg_ticket, contact_id: comment.id_tcg_contact, // uuid of Contact object user_id: comment.id_tcg_user, // uuid of User object - attachments: comment.attachments, //uuids of Attachments objects }; }), ); diff --git a/packages/api/src/ticketing/tag/services/tag.service.ts b/packages/api/src/ticketing/tag/services/tag.service.ts index 9c308818b..04ca3dc05 100644 --- a/packages/api/src/ticketing/tag/services/tag.service.ts +++ b/packages/api/src/ticketing/tag/services/tag.service.ts @@ -92,9 +92,7 @@ export class TagService { const tags = await this.prisma.tcg_tags.findMany({ where: { remote_id: integrationId.toLowerCase(), - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); From 4d1605e759e6974bd96b1cec24fdd1d71a02326e Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 6 Jan 2024 14:23:45 +0100 Subject: [PATCH 17/25] :construction: Comment <> Attachment <> Ticket Sync --- .../types/original/original.ticketing.ts | 8 +- .../src/crm/contact/types/model.unified.ts | 2 + .../ticketing/account/types/model.unified.ts | 3 +- .../attachment/services/front/mappers.ts | 47 ++++++++++++ .../attachment/services/front/types.ts | 13 ++++ .../attachment/services/github/mappers.ts | 44 +++++++++++ .../attachment/services/github/types.ts | 3 + .../attachment/services/zendesk/mappers.ts | 47 ++++++++++++ .../attachment/services/zendesk/types.ts | 20 +++++ .../attachment/types/mappingsTypes.ts | 22 ++++++ .../attachment/types/model.unified.ts | 3 +- .../ticketing/comment/services/front/index.ts | 2 +- .../comment/services/front/mappers.ts | 34 ++++++--- .../comment/services/github/mappers.ts | 4 +- .../comment/services/hubspot/mappers.ts | 4 +- .../comment/services/zendesk/index.ts | 1 - .../comment/services/zendesk/mappers.ts | 31 +++++--- .../ticketing/comment/sync/sync.service.ts | 76 ++++++++++++++++++- .../api/src/ticketing/comment/types/index.ts | 2 +- .../ticketing/comment/types/model.unified.ts | 15 +++- .../ticketing/contact/types/model.unified.ts | 3 +- .../src/ticketing/tag/types/model.unified.ts | 3 +- .../src/ticketing/team/types/model.unified.ts | 3 +- .../ticket/services/front/mappers.ts | 1 - .../ticket/services/hubspot/mappers.ts | 1 - .../ticket/services/zendesk/mappers.ts | 1 - .../ticketing/ticket/types/model.unified.ts | 3 +- .../src/ticketing/user/types/model.unified.ts | 3 +- 28 files changed, 356 insertions(+), 43 deletions(-) create mode 100644 packages/api/src/ticketing/attachment/services/front/mappers.ts create mode 100644 packages/api/src/ticketing/attachment/services/front/types.ts create mode 100644 packages/api/src/ticketing/attachment/services/github/mappers.ts create mode 100644 packages/api/src/ticketing/attachment/services/github/types.ts create mode 100644 packages/api/src/ticketing/attachment/services/zendesk/mappers.ts create mode 100644 packages/api/src/ticketing/attachment/services/zendesk/types.ts create mode 100644 packages/api/src/ticketing/attachment/types/mappingsTypes.ts 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 6165c6c1c..b9ce99b62 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -22,6 +22,9 @@ import { GithubAccountInput, GithubAccountOutput, } from '@ticketing/account/services/github/types'; +import { FrontAttachmentOutput } from '@ticketing/attachment/services/front/types'; +import { GithubAttachmentOutput } from '@ticketing/attachment/services/github/types'; +import { ZendeskAttachmentOutput } from '@ticketing/attachment/services/zendesk/types'; import { FrontCommentInput, @@ -175,7 +178,10 @@ export type OriginalTeamOutput = | FrontTeamOutput; /* attachment */ -export type OriginalAttachmentOutput = null; +export type OriginalAttachmentOutput = + | ZendeskAttachmentOutput + | FrontAttachmentOutput + | GithubAttachmentOutput; export type TicketingObjectOutput = | OriginalTicketOutput diff --git a/packages/api/src/crm/contact/types/model.unified.ts b/packages/api/src/crm/contact/types/model.unified.ts index e8004d954..246b4b14d 100644 --- a/packages/api/src/crm/contact/types/model.unified.ts +++ b/packages/api/src/crm/contact/types/model.unified.ts @@ -17,4 +17,6 @@ export class UnifiedContactInput { export class UnifiedContactOutput extends UnifiedContactInput { @ApiPropertyOptional() id?: string; + @ApiPropertyOptional() + remote_id?: string; } diff --git a/packages/api/src/ticketing/account/types/model.unified.ts b/packages/api/src/ticketing/account/types/model.unified.ts index 943753498..c0630f76b 100644 --- a/packages/api/src/ticketing/account/types/model.unified.ts +++ b/packages/api/src/ticketing/account/types/model.unified.ts @@ -5,5 +5,6 @@ export class UnifiedAccountInput { } export class UnifiedAccountOutput extends UnifiedAccountInput { - id: string; + id?: string; + remote_id?: string; } diff --git a/packages/api/src/ticketing/attachment/services/front/mappers.ts b/packages/api/src/ticketing/attachment/services/front/mappers.ts new file mode 100644 index 000000000..3c6ddda0a --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/front/mappers.ts @@ -0,0 +1,47 @@ +import { IAttachmentMapper } from '@ticketing/attachment/types'; +import { + UnifiedAttachmentInput, + UnifiedAttachmentOutput, +} from '@ticketing/attachment/types/model.unified'; +import { FrontAttachmentOutput } from './types'; + +export class FrontAttachmentMapper implements IAttachmentMapper { + async desunify( + source: UnifiedAttachmentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + unify( + source: FrontAttachmentOutput | FrontAttachmentOutput[], + 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: FrontAttachmentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput { + return { + remote_id: attachment.id, + file_name: attachment.filename, + file_url: attachment.url, + }; + } +} diff --git a/packages/api/src/ticketing/attachment/services/front/types.ts b/packages/api/src/ticketing/attachment/services/front/types.ts new file mode 100644 index 000000000..1fd21c1bc --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/front/types.ts @@ -0,0 +1,13 @@ +export type FrontAttachmentOutput = { + id: string; + filename: string; + url: string; + content_type: string; + size: number; + metadata: AttachmentMetadata; +}; + +type AttachmentMetadata = { + is_inline: boolean; + cid: string; +}; diff --git a/packages/api/src/ticketing/attachment/services/github/mappers.ts b/packages/api/src/ticketing/attachment/services/github/mappers.ts new file mode 100644 index 000000000..0fd35df71 --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/github/mappers.ts @@ -0,0 +1,44 @@ +import { IAttachmentMapper } from '@ticketing/attachment/types'; +import { + UnifiedAttachmentInput, + UnifiedAttachmentOutput, +} from '@ticketing/attachment/types/model.unified'; +import { GithubAttachmentOutput } from './types'; + +export class GithubAttachmentMapper implements IAttachmentMapper { + async desunify( + source: UnifiedAttachmentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + unify( + source: GithubAttachmentOutput | GithubAttachmentOutput[], + 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), + ); + } + + //TODO; + private mapSingleAttachmentToUnified( + attachment: GithubAttachmentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput { + return; + } +} diff --git a/packages/api/src/ticketing/attachment/services/github/types.ts b/packages/api/src/ticketing/attachment/services/github/types.ts new file mode 100644 index 000000000..39b76d9cf --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/github/types.ts @@ -0,0 +1,3 @@ +export type GithubAttachmentOutput = { + id: string; +}; diff --git a/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts b/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts new file mode 100644 index 000000000..99a391c4c --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/zendesk/mappers.ts @@ -0,0 +1,47 @@ +import { IAttachmentMapper } from '@ticketing/attachment/types'; +import { + UnifiedAttachmentInput, + UnifiedAttachmentOutput, +} from '@ticketing/attachment/types/model.unified'; +import { ZendeskAttachmentOutput } from './types'; + +export class ZendeskAttachmentMapper implements IAttachmentMapper { + async desunify( + source: UnifiedAttachmentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + unify( + source: ZendeskAttachmentOutput | ZendeskAttachmentOutput[], + 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: ZendeskAttachmentOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedAttachmentOutput { + return { + remote_id: String(attachment.id), + file_name: attachment.file_name, + file_url: attachment.url, + }; + } +} diff --git a/packages/api/src/ticketing/attachment/services/zendesk/types.ts b/packages/api/src/ticketing/attachment/services/zendesk/types.ts new file mode 100644 index 000000000..06933e01a --- /dev/null +++ b/packages/api/src/ticketing/attachment/services/zendesk/types.ts @@ -0,0 +1,20 @@ +export type ZendeskAttachmentOutput = { + content_type: string; // The content type of the image, e.g., "image/png". + content_url: string; // A full URL where the attachment image file can be downloaded. + deleted: boolean; // If true, the attachment has been deleted. + file_name: string; // The name of the image file. + height: string | null; // The height of the image file in pixels, or null if unknown. + id: number; // Automatically assigned when created. + inline: boolean; // If true, the attachment is excluded from the attachment list. + malware_access_override: boolean; // If true, you can download an attachment flagged as malware. + malware_scan_result: + | 'malware_found' + | 'malware_not_found' + | 'failed_to_scan' + | 'not_scanned'; // The result of the malware scan. + mapped_content_url: string; // The URL the attachment image file has been mapped to. + size: number; // The size of the image file in bytes. + thumbnails: ZendeskAttachmentOutput[]; // An array of attachment objects. + url: string; // A URL to access the attachment details. + width: string | null; // The width of the image file in pixels, or null if unknown. +}; diff --git a/packages/api/src/ticketing/attachment/types/mappingsTypes.ts b/packages/api/src/ticketing/attachment/types/mappingsTypes.ts new file mode 100644 index 000000000..1177b8d7b --- /dev/null +++ b/packages/api/src/ticketing/attachment/types/mappingsTypes.ts @@ -0,0 +1,22 @@ +import { FrontAttachmentMapper } from '../services/front/mappers'; +import { GithubAttachmentMapper } from '../services/github/mappers'; +import { ZendeskAttachmentMapper } from '../services/zendesk/mappers'; + +const zendeskAttachmentMapper = new ZendeskAttachmentMapper(); +const githubAttachmentMapper = new GithubAttachmentMapper(); +const frontAttachmentMapper = new FrontAttachmentMapper(); + +export const commentUnificationMapping = { + zendesk: { + unify: zendeskAttachmentMapper.unify, + desunify: zendeskAttachmentMapper.desunify, + }, + front: { + unify: frontAttachmentMapper.unify, + desunify: frontAttachmentMapper.desunify, + }, + github: { + unify: githubAttachmentMapper.unify, + desunify: githubAttachmentMapper.desunify, + }, +}; diff --git a/packages/api/src/ticketing/attachment/types/model.unified.ts b/packages/api/src/ticketing/attachment/types/model.unified.ts index a931ecf27..976430946 100644 --- a/packages/api/src/ticketing/attachment/types/model.unified.ts +++ b/packages/api/src/ticketing/attachment/types/model.unified.ts @@ -6,5 +6,6 @@ export class UnifiedAttachmentInput { } export class UnifiedAttachmentOutput extends UnifiedAttachmentInput { - id: string; + id?: string; + remote_id?: string; } diff --git a/packages/api/src/ticketing/comment/services/front/index.ts b/packages/api/src/ticketing/comment/services/front/index.ts index 5487b027c..d15f613a9 100644 --- a/packages/api/src/ticketing/comment/services/front/index.ts +++ b/packages/api/src/ticketing/comment/services/front/index.ts @@ -85,7 +85,7 @@ export class FrontService implements ICommentService { async syncComments( linkedUserId: string, id_ticket: string, - ): Promise> { + ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts index 0e0b55210..4605b58d8 100644 --- a/packages/api/src/ticketing/comment/services/front/mappers.ts +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -4,11 +4,12 @@ import { UnifiedCommentOutput, } from '@ticketing/comment/types/model.unified'; import { FrontCommentInput, FrontCommentOutput } from './types'; -import { Utils } from '@ticketing/ticket/utils'; +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'; export class FrontCommentMapper implements ICommentMapper { - private readonly utils = new Utils(); - async desunify( source: UnifiedCommentInput, customFieldMappings?: { @@ -24,36 +25,47 @@ export class FrontCommentMapper implements ICommentMapper { return result; } - unify( + async unify( source: FrontCommentOutput | FrontCommentOutput[], customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedCommentOutput | UnifiedCommentOutput[] { + ): Promise { if (!Array.isArray(source)) { - return this.mapSingleCommentToUnified(source, customFieldMappings); + return await this.mapSingleCommentToUnified(source, customFieldMappings); } - return source.map((comment) => - this.mapSingleCommentToUnified(comment, customFieldMappings), + return Promise.all( + source.map((comment) => + this.mapSingleCommentToUnified(comment, customFieldMappings), + ), ); } - private mapSingleCommentToUnified( + private async mapSingleCommentToUnified( comment: FrontCommentOutput, customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedCommentOutput { + ): Promise { + //map the front attachment to our unified version of attachment + //unifying the original attachment object coming from Front + const unifiedObject = (await unify({ + sourceObject: comment.attachments, + targetType: TicketingObject.attachment, + providerName: 'front', + customFieldMappings: [], + })) as UnifiedAttachmentOutput[]; + return { - id: comment.id, body: comment.body, html_body: '', creator_type: comment.author ? 'contact' : null, ticket_id: '', // TODO: Need to be determined from related data contact_id: '', // TODO: Need to be determined from related data user_id: '', //TODO + attachments: unifiedObject, }; } } diff --git a/packages/api/src/ticketing/comment/services/github/mappers.ts b/packages/api/src/ticketing/comment/services/github/mappers.ts index e2127e808..048fe09d1 100644 --- a/packages/api/src/ticketing/comment/services/github/mappers.ts +++ b/packages/api/src/ticketing/comment/services/github/mappers.ts @@ -17,13 +17,13 @@ export class GithubCommentMapper implements ICommentMapper { return; } - unify( + async unify( source: GithubCommentOutput | GithubCommentOutput[], customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedCommentOutput | UnifiedCommentOutput[] { + ): Promise { if (!Array.isArray(source)) { return this.mapSingleCommentToUnified(source, customFieldMappings); } diff --git a/packages/api/src/ticketing/comment/services/hubspot/mappers.ts b/packages/api/src/ticketing/comment/services/hubspot/mappers.ts index 92a262771..e6b306658 100644 --- a/packages/api/src/ticketing/comment/services/hubspot/mappers.ts +++ b/packages/api/src/ticketing/comment/services/hubspot/mappers.ts @@ -18,13 +18,13 @@ export class HubspotCommentMapper implements ICommentMapper { return; } - unify( + async unify( source: HubspotCommentOutput | HubspotCommentOutput[], customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedCommentOutput | UnifiedCommentOutput[] { + ): Promise { if (!Array.isArray(source)) { return this.mapSingleCommentToUnified(source, customFieldMappings); } diff --git a/packages/api/src/ticketing/comment/services/zendesk/index.ts b/packages/api/src/ticketing/comment/services/zendesk/index.ts index 8ce85b684..7c8c31112 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/index.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/index.ts @@ -3,7 +3,6 @@ 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 { DesunifyReturnType } from '@@core/utils/types/desunify.input'; import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; import { ICommentService } from '@ticketing/comment/types'; diff --git a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts index c6cabb783..ec39f1dda 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts @@ -4,11 +4,12 @@ import { UnifiedCommentInput, UnifiedCommentOutput, } from '@ticketing/comment/types/model.unified'; -import { Utils } from '@ticketing/ticket/utils'; +import { UnifiedAttachmentOutput } from '@ticketing/attachment/types/model.unified'; +import { unify } from '@@core/utils/unification/unify'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; export class ZendeskCommentMapper implements ICommentMapper { - private readonly utils = new Utils(); - async desunify( source: UnifiedCommentInput, customFieldMappings?: { @@ -32,38 +33,46 @@ export class ZendeskCommentMapper implements ICommentMapper { return result; } - unify( + async unify( source: ZendeskCommentOutput | ZendeskCommentOutput[], customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedCommentOutput | UnifiedCommentOutput[] { + ): Promise { if (!Array.isArray(source)) { return this.mapSingleCommentToUnified(source, customFieldMappings); } - return source.map((comment) => - this.mapSingleCommentToUnified(comment, customFieldMappings), + return Promise.all( + source.map((comment) => + this.mapSingleCommentToUnified(comment, customFieldMappings), + ), ); } - private mapSingleCommentToUnified( + private async mapSingleCommentToUnified( comment: ZendeskCommentOutput, customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedCommentOutput { + ): Promise { + const unifiedObject = (await unify({ + sourceObject: comment.attachments, + targetType: TicketingObject.attachment, + providerName: 'front', + customFieldMappings: [], + })) as UnifiedAttachmentOutput[]; + return { body: comment.body || '', html_body: comment.html_body || '', is_private: !comment.public, - created_at: new Date(comment.created_at), - modified_at: new Date(comment.created_at), // Assuming the creation date for modification as well creator_type: 'contact', ticket_id: '', //TODO contact_id: '', // TODO: user_id: '', //TODO + attachments: unifiedObject, }; } } diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index 0c814c01f..d501c6dd2 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -110,7 +110,7 @@ export class SyncService implements OnModuleInit { provider_slug: integrationId, }, }); - if (!connection) return; + if (!connection) throw new Error('connection not found'); // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -246,6 +246,80 @@ export class SyncService implements OnModuleInit { unique_ticketing_comment_id = res.id_tcg_comment; } + // now insert the attachment of the comment inside tcg_attachments + // we should already have at least initial data (as it must have been inserted by the end linked user before adding comment) + // though we might sync comments that have been also directly been added to the provider without passing through Panora + // in this case just create a new attachment row ! + + for (const attchmt of comment.attachments) { + let unique_ticketing_attachmt_id: string; + + const existingAttachmt = await this.prisma.tcg_attachments.findFirst({ + where: { + remote_platform: originSource, + id_linked_user: linkedUserId, + file_name: attchmt.file_name, + }, + }); + + if (existingAttachmt) { + // Update the existing attachmt + const res = await this.prisma.tcg_attachments.update({ + where: { + id_tcg_attachment: existingAttachmt.id_tcg_attachment, + }, + data: { + remote_id: attchmt.id, + file_url: attchmt.file_url, + id_tcg_comment: unique_ticketing_comment_id, + id_tcg_ticket: id_ticket, + modified_at: new Date(), + }, + }); + unique_ticketing_attachmt_id = res.id_tcg_attachment; + } else { + // Create a new comment + this.logger.log('attchmt not exists'); + const data = { + id_tcg_attachment: uuidv4(), + remote_id: attchmt.id, + file_name: attchmt.file_name, + file_url: attchmt.file_url, + id_tcg_comment: unique_ticketing_comment_id, + created_at: new Date(), + modified_at: new Date(), + uploader: linkedUserId, //TODO + id_tcg_ticket: id_ticket, + id_linked_user: linkedUserId, + remote_platform: originSource, + //TODO; id_tcg_contact String? @db.Uuid + //TODO; id_tcg_user String? @db.Uuid + }; + const res = await this.prisma.tcg_attachments.create({ + data: data, + }); + unique_ticketing_attachmt_id = res.id_tcg_attachment; + } + + //TODO: insert remote_data in db i dont know the type of remote_data to extract the right source attachment object + /*await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_attachmt_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_attachmt_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + });*/ + } + //insert remote_data in db await this.prisma.remote_data.upsert({ where: { diff --git a/packages/api/src/ticketing/comment/types/index.ts b/packages/api/src/ticketing/comment/types/index.ts index 2bdfe5264..69a95465e 100644 --- a/packages/api/src/ticketing/comment/types/index.ts +++ b/packages/api/src/ticketing/comment/types/index.ts @@ -32,7 +32,7 @@ export interface ICommentMapper { slug: string; remote_id: string; }[], - ): UnifiedCommentOutput | UnifiedCommentOutput[]; + ): Promise; } export type Comment = { diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index 69f35c270..9f1d06103 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -1,4 +1,5 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; +import { UnifiedAttachmentOutput } from '@ticketing/attachment/types/model.unified'; export class UnifiedCommentInput { body: string; @@ -13,7 +14,17 @@ export class UnifiedCommentInput { attachments?: string[]; //uuids of Attachments objects } -//TODO: add remote_id -export class UnifiedCommentOutput extends UnifiedCommentInput { +export class UnifiedCommentOutput { id?: string; + remote_id?: string; + body: string; + html_body?: string; + is_private?: boolean; + created_at?: Date; + modified_at?: Date; + creator_type: 'user' | 'contact' | null | string; + ticket_id?: string; // uuid of Ticket object + contact_id?: string; // uuid of Contact object + user_id?: string; // uuid of User object + attachments?: UnifiedAttachmentOutput[]; // Attachments objects } diff --git a/packages/api/src/ticketing/contact/types/model.unified.ts b/packages/api/src/ticketing/contact/types/model.unified.ts index a59aa7cdb..1db24f6c2 100644 --- a/packages/api/src/ticketing/contact/types/model.unified.ts +++ b/packages/api/src/ticketing/contact/types/model.unified.ts @@ -7,5 +7,6 @@ export class UnifiedContactInput { } export class UnifiedContactOutput extends UnifiedContactInput { - id: string; + id?: string; + remote_id?: string; } diff --git a/packages/api/src/ticketing/tag/types/model.unified.ts b/packages/api/src/ticketing/tag/types/model.unified.ts index 5af56978f..29bd24465 100644 --- a/packages/api/src/ticketing/tag/types/model.unified.ts +++ b/packages/api/src/ticketing/tag/types/model.unified.ts @@ -4,5 +4,6 @@ export class UnifiedTagInput { } export class UnifiedTagOutput extends UnifiedTagInput { - id: string; + id?: string; + remote_id?: string; } diff --git a/packages/api/src/ticketing/team/types/model.unified.ts b/packages/api/src/ticketing/team/types/model.unified.ts index 9745c704c..7ce74381a 100644 --- a/packages/api/src/ticketing/team/types/model.unified.ts +++ b/packages/api/src/ticketing/team/types/model.unified.ts @@ -5,5 +5,6 @@ export class UnifiedTeamInput { } export class UnifiedTeamOutput extends UnifiedTeamInput { - id: string; + id?: string; + remote_id?: string; } diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index c241fb025..80221d06a 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -74,7 +74,6 @@ export class FrontTicketMapper implements ITicketMapper { })); const unifiedTicket: UnifiedTicketOutput = { - id: ticket.id, name: ticket.subject, status: ticket.status, description: ticket.subject, // todo: ? diff --git a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts index 04a33eb46..8540833a4 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts @@ -64,7 +64,6 @@ export class HubspotTicketMapper implements ITicketMapper { })); return { - id: ticket.id, name: ticket.properties.name, //TODO status: ticket.properties.hs_pipeline_stage, description: ticket.properties.description, //TODO diff --git a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts index 082916b38..6a4f9f1c9 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts @@ -103,7 +103,6 @@ export class ZendeskTicketMapper implements ITicketMapper { priority: ticket.priority, assigned_to: [String(ticket.assignee_id)], field_mappings: field_mappings, - id: ticket.id.toString(), }; return unifiedTicket; diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index ba0a397e8..bd1ff689d 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -19,5 +19,6 @@ export class UnifiedTicketInput { } export class UnifiedTicketOutput extends UnifiedTicketInput { @ApiProperty() - id: string; + id?: string; + remote_id?: string; } diff --git a/packages/api/src/ticketing/user/types/model.unified.ts b/packages/api/src/ticketing/user/types/model.unified.ts index 801abbed0..d779e055d 100644 --- a/packages/api/src/ticketing/user/types/model.unified.ts +++ b/packages/api/src/ticketing/user/types/model.unified.ts @@ -6,5 +6,6 @@ export class UnifiedUserInput { } export class UnifiedUserOutput extends UnifiedUserInput { - id: string; + id?: string; + remote_id?: string; } From 02592a3a109db23c9c9bb87008da1e8bcdbd7e44 Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 6 Jan 2024 14:40:46 +0100 Subject: [PATCH 18/25] :construction: Little fixes --- .../attachment/services/attachment.service.ts | 5 +++-- .../api/src/ticketing/comment/comment.controller.ts | 1 - .../src/ticketing/comment/services/comment.service.ts | 11 ++++++++--- .../src/ticketing/ticket/services/ticket.service.ts | 4 +++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index 510a82631..48d5c17af 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -96,6 +96,7 @@ export class AttachmentService { }, data: { file_name: unifiedAttachmentData.file_name, + uploader: linkedUserId, modified_at: new Date(), }, }); @@ -106,7 +107,7 @@ export class AttachmentService { const data = { id_tcg_attachment: uuidv4(), file_name: unifiedAttachmentData.file_name, - uploader: linkedUserId, //TODO + uploader: linkedUserId, created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, @@ -190,7 +191,7 @@ export class AttachmentService { id: attachment.id_tcg_attachment, file_name: attachment.file_name, file_url: attachment.file_url, - uploader: attachment.uploader, //TODO + uploader: attachment.uploader, field_mappings: field_mappings, }; diff --git a/packages/api/src/ticketing/comment/comment.controller.ts b/packages/api/src/ticketing/comment/comment.controller.ts index 40ecc1433..8fb5396c7 100644 --- a/packages/api/src/ticketing/comment/comment.controller.ts +++ b/packages/api/src/ticketing/comment/comment.controller.ts @@ -4,7 +4,6 @@ import { Body, Query, Get, - Patch, Param, Headers, } from '@nestjs/common'; diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index 6a7ba4f3f..6e9f54415 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -150,6 +150,10 @@ export class CommentService { remote_id: true, }, }); + if (!ticket) + throw new Error( + 'ticket does not exist for the comment you try to create', + ); const resp: ApiResponse = await service.addComment( desunifiedObject, linkedUserId, @@ -272,6 +276,7 @@ export class CommentService { } } + //TODO: return attachments if specified in param async getComment( id_commenting_comment: string, remote_data?: boolean, @@ -348,6 +353,8 @@ export class CommentService { } } + //TODO: return attachments if specified in param + async getComments( integrationId: string, linkedUserId: string, @@ -357,9 +364,7 @@ export class CommentService { const comments = await this.prisma.tcg_comments.findMany({ where: { remote_id: integrationId.toLowerCase(), - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index b0cafefd5..7f8d90f01 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -306,6 +306,7 @@ export class TicketService { } } + //TODO: given params return attachments and comments async getTicket( id_ticketing_ticket: string, remote_data?: boolean, @@ -398,9 +399,10 @@ export class TicketService { id_linked_user: linkedUserId, }, }, + /* TODO: only if params include: { tcg_comments: true, - }, + },*/ }); const unifiedTickets: UnifiedTicketOutput[] = await Promise.all( From 8ec2a5002f9f0d6329078b9119aa4284d2baab3d Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 6 Jan 2024 15:18:34 +0100 Subject: [PATCH 19/25] :construction: Updated all responses types --- packages/api/prisma/schema.prisma | 21 ---------- packages/api/scripts/commonObject.sh | 10 ++--- .../api/src/crm/contact/contact.controller.ts | 8 ++-- .../crm/contact/services/contact.service.ts | 38 ++++++------------ .../src/crm/contact/types/model.unified.ts | 6 ++- packages/api/src/crm/deal/deal.controller.ts | 23 +++-------- .../api/src/crm/deal/services/deal.service.ts | 22 ++++------ .../account/services/account.service.ts | 14 ++----- .../attachment/services/attachment.service.ts | 36 ++++++----------- .../attachment/types/model.unified.ts | 11 +++++ .../comment/services/comment.service.ts | 36 ++++++----------- .../ticketing/comment/types/model.unified.ts | 6 +++ .../contact/services/contact.service.ts | 18 +++------ .../src/ticketing/tag/services/tag.service.ts | 14 ++----- .../ticketing/team/services/team.service.ts | 14 ++----- .../ticket/services/ticket.service.ts | 40 +++++++------------ .../ticketing/ticket/types/model.unified.ts | 7 +++- .../ticketing/user/services/user.service.ts | 18 +++------ 18 files changed, 124 insertions(+), 218 deletions(-) diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index e2bda898d..9964147c6 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -90,10 +90,8 @@ model crm_companies { created_at DateTime @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_crm_user String? @db.Uuid - id_event String @db.Uuid id_linked_user String? @db.Uuid crm_addresses crm_addresses[] - events events @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_13") crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_24") crm_email_addresses crm_email_addresses[] crm_engagements crm_engagements[] @@ -102,7 +100,6 @@ model crm_companies { crm_tasks crm_tasks[] @@index([id_crm_user], map: "fk_crm_company_crm_userid") - @@index([id_event], map: "fk_crm_company_jobid") } model crm_contacts { @@ -301,13 +298,8 @@ model events { url String provider String id_linked_user String @db.Uuid - crm_companies crm_companies[] linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_12") jobs_status_history jobs_status_history[] - tcg_comments tcg_comments[] - tcg_contacts tcg_contacts[] - tcg_tickets tcg_tickets[] - tcg_users tcg_users[] webhook_delivery_attempts webhook_delivery_attempts[] @@index([id_linked_user], map: "fk_linkeduserid_projectid") @@ -396,18 +388,15 @@ model tcg_comments { id_tcg_ticket String? @db.Uuid id_tcg_contact String? @db.Uuid id_tcg_user String? @db.Uuid - id_event String? @db.Uuid id_linked_user String? @db.Uuid tcg_attachments tcg_attachments[] tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_40_1") tcg_contacts tcg_contacts? @relation(fields: [id_tcg_contact], references: [id_tcg_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_41") tcg_users tcg_users? @relation(fields: [id_tcg_user], references: [id_tcg_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_42") - events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_46") @@index([id_tcg_contact], map: "fk_tcg_comment_tcg_contact") @@index([id_tcg_ticket], map: "fk_tcg_comment_tcg_ticket") @@index([id_tcg_user], map: "fk_tcg_comment_tcg_userid") - @@index([id_event], map: "fk_tcg_comments_eventid") } model tcg_contacts { @@ -420,14 +409,11 @@ model tcg_contacts { remote_platform String? created_at DateTime? @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) - id_event String? @db.Uuid id_tcg_account String? @db.Uuid id_linked_user String? @db.Uuid tcg_comments tcg_comments[] - events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_43") tcg_accounts tcg_accounts? @relation(fields: [id_tcg_account], references: [id_tcg_account], onDelete: NoAction, onUpdate: NoAction, map: "fk_49") - @@index([id_event], map: "fk_tcg_contact_event_id") @@index([id_tcg_account], map: "fk_tcg_contact_tcg_account_id") } @@ -448,16 +434,13 @@ model tcg_tickets { assigned_to String[] remote_id String? remote_platform String? - id_event String? @db.Uuid creator_type String? id_tcg_user String? @db.Uuid id_linked_user String @db.Uuid tcg_attachments tcg_attachments[] tcg_comments tcg_comments[] tcg_tags tcg_tags[] - events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_44") - @@index([id_event], map: "fk_tcg_tickets_eventid") @@index([id_tcg_user], map: "fk_tcg_ticket_tcg_user") } @@ -468,15 +451,11 @@ model tcg_users { email_address String? remote_id String? remote_platform String? - id_event String? @db.Uuid teams String[] created_at DateTime? @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) id_linked_user String? @db.Uuid tcg_comments tcg_comments[] - events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_45") - - @@index([id_event], map: "fk_tcg_users_event_id") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments diff --git a/packages/api/scripts/commonObject.sh b/packages/api/scripts/commonObject.sh index 0b9347dcd..29e19d00d 100755 --- a/packages/api/scripts/commonObject.sh +++ b/packages/api/scripts/commonObject.sh @@ -88,7 +88,7 @@ export class ${ObjectCap}Service { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise<${ObjectCap}Response> { return } @@ -97,14 +97,14 @@ export class ${ObjectCap}Service { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise<${ObjectCap}Response> { return; } async get${ObjectCap}( id_${VerticalLow}_${objectType}: string, remote_data?: boolean, - ): Promise> { + ): Promise<${ObjectCap}Response> { return; } @@ -112,14 +112,14 @@ export class ${ObjectCap}Service { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise<${ObjectCap}Response> { return; } async update${ObjectCap}( id: string, update${ObjectCap}Data: Partial, - ): Promise> { + ): Promise<${ObjectCap}Response> { return; } } diff --git a/packages/api/src/crm/contact/contact.controller.ts b/packages/api/src/crm/contact/contact.controller.ts index 30eb0ea69..32bfb5082 100644 --- a/packages/api/src/crm/contact/contact.controller.ts +++ b/packages/api/src/crm/contact/contact.controller.ts @@ -7,7 +7,7 @@ import { Patch, Param, UseGuards, - Headers + Headers, } from '@nestjs/common'; import { ContactService } from './services/contact.service'; import { LoggerService } from '@@core/logger/logger.service'; @@ -18,7 +18,7 @@ import { ApiParam, ApiQuery, ApiTags, - ApiHeader + ApiHeader, } from '@nestjs/swagger'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; @@ -35,9 +35,9 @@ export class ContactController { @ApiOperation({ operationId: 'getContacts', summary: 'List a batch of CRM Contacts', - }) + }) @ApiHeader({ name: 'integrationId', required: true }) - @ApiHeader({ name: 'linkedUserId', required: true}) + @ApiHeader({ name: 'linkedUserId', required: true }) @ApiQuery({ name: 'remoteData', required: false, diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index e2a293d87..585613fbd 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -35,7 +35,7 @@ export class ContactService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const responses = await Promise.all( unifiedContactData.map((unifiedData) => @@ -48,20 +48,14 @@ export class ContactService { ), ); - const allContacts = responses.flatMap( - (response) => response.data.contacts, - ); + const allContacts = responses.flatMap((response) => response.contacts); const allRemoteData = responses.flatMap( - (response) => response.data.remote_data || [], + (response) => response.remote_data || [], ); return { - data: { - contacts: allContacts, - remote_data: allRemoteData, - }, - message: 'All contacts inserted successfully', - statusCode: 201, + contacts: allContacts, + remote_data: allRemoteData, }; } catch (error) { handleServiceError(error, this.logger); @@ -73,7 +67,7 @@ export class ContactService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const linkedUser = await this.prisma.linked_users.findUnique({ where: { @@ -285,12 +279,12 @@ export class ContactService { }, }); await this.webhook.handleWebhook( - result_contact.data.contacts, + result_contact.contacts, 'crm.contact.created', linkedUser.id_project, event.id_event, ); - return { ...resp, data: result_contact.data }; + return result_contact; } catch (error) { handleServiceError(error, this.logger); } @@ -299,7 +293,7 @@ export class ContactService { async getContact( id_crm_contact: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const contact = await this.prisma.crm_contacts.findUnique({ where: { @@ -369,10 +363,7 @@ export class ContactService { }; } - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -382,7 +373,7 @@ export class ContactService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced @@ -476,10 +467,7 @@ export class ContactService { id_linked_user: linkedUserId, }, }); - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -488,7 +476,7 @@ export class ContactService { async updateContact( id: string, updateContactData: Partial, - ): Promise> { + ): Promise { try { } catch (error) { handleServiceError(error, this.logger); diff --git a/packages/api/src/crm/contact/types/model.unified.ts b/packages/api/src/crm/contact/types/model.unified.ts index 246b4b14d..2705e9ba9 100644 --- a/packages/api/src/crm/contact/types/model.unified.ts +++ b/packages/api/src/crm/contact/types/model.unified.ts @@ -15,8 +15,10 @@ export class UnifiedContactInput { } export class UnifiedContactOutput extends UnifiedContactInput { - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'The id of the contact' }) id?: string; - @ApiPropertyOptional() + @ApiPropertyOptional({ + description: 'The id of the contact in the context of the Crm software', + }) remote_id?: string; } diff --git a/packages/api/src/crm/deal/deal.controller.ts b/packages/api/src/crm/deal/deal.controller.ts index f22663c67..5cf2ff386 100644 --- a/packages/api/src/crm/deal/deal.controller.ts +++ b/packages/api/src/crm/deal/deal.controller.ts @@ -42,8 +42,7 @@ export class DealController { name: 'remoteData', required: false, type: Boolean, - description: - 'Set to true to include data from the original Crm software.', + description: 'Set to true to include data from the original Crm software.', }) //@ApiCustomResponse(DealResponse) @Get() @@ -52,11 +51,7 @@ export class DealController { @Headers('linkedUserId') linkedUserId: string, @Query('remoteData') remote_data?: boolean, ) { - return this.dealService.getDeals( - integrationId, - linkedUserId, - remote_data, - ); + return this.dealService.getDeals(integrationId, linkedUserId, remote_data); } @ApiOperation({ @@ -74,15 +69,11 @@ export class DealController { name: 'remoteData', required: false, type: Boolean, - description: - 'Set to true to include data from the original Crm software.', + description: 'Set to true to include data from the original Crm software.', }) //@ApiCustomResponse(DealResponse) @Get(':id') - getDeal( - @Param('id') id: string, - @Query('remoteData') remote_data?: boolean, - ) { + getDeal(@Param('id') id: string, @Query('remoteData') remote_data?: boolean) { return this.dealService.getDeal(id, remote_data); } @@ -107,8 +98,7 @@ export class DealController { name: 'remoteData', required: false, type: Boolean, - description: - 'Set to true to include data from the original Crm software.', + description: 'Set to true to include data from the original Crm software.', }) @ApiBody({ type: UnifiedDealInput }) //@ApiCustomResponse(DealResponse) @@ -137,8 +127,7 @@ export class DealController { name: 'remoteData', required: false, type: Boolean, - description: - 'Set to true to include data from the original Crm software.', + description: 'Set to true to include data from the original Crm software.', }) @ApiBody({ type: UnifiedDealInput, isArray: true }) //@ApiCustomResponse(DealResponse) diff --git a/packages/api/src/crm/deal/services/deal.service.ts b/packages/api/src/crm/deal/services/deal.service.ts index cb9134e7c..493080ede 100644 --- a/packages/api/src/crm/deal/services/deal.service.ts +++ b/packages/api/src/crm/deal/services/deal.service.ts @@ -2,17 +2,11 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; -import { UnifiedDealInput, UnifiedDealOutput } from '../types/model.unified'; +import { UnifiedDealInput } from '../types/model.unified'; import { DealResponse } from '../types'; -import { desunify } from '@@core/utils/unification/desunify'; -import { CrmObject } from '@crm/@utils/@types'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalDealOutput } from '@@core/utils/types/original/original.crm'; -import { unify } from '@@core/utils/unification/unify'; @Injectable() export class DealService { @@ -31,23 +25,23 @@ export class DealService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { - return + ): Promise { + return; } - async addDeal( + async addDeal( unifiedDealData: UnifiedDealInput, integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { return; } async getDeal( id_crm_deal: string, remote_data?: boolean, - ): Promise> { + ): Promise { return; } @@ -55,14 +49,14 @@ export class DealService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { return; } async updateDeal( id: string, updateDealData: Partial, - ): Promise> { + ): Promise { return; } } diff --git a/packages/api/src/ticketing/account/services/account.service.ts b/packages/api/src/ticketing/account/services/account.service.ts index 28cc84ee7..b84f378ab 100644 --- a/packages/api/src/ticketing/account/services/account.service.ts +++ b/packages/api/src/ticketing/account/services/account.service.ts @@ -16,7 +16,7 @@ export class AccountService { async getAccount( id_ticketing_account: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const account = await this.prisma.tcg_accounts.findUnique({ where: { @@ -74,10 +74,7 @@ export class AccountService { }; } - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -87,7 +84,7 @@ export class AccountService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced @@ -169,10 +166,7 @@ export class AccountService { id_linked_user: linkedUserId, }, }); - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index 48d5c17af..fdde2a686 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -26,7 +26,7 @@ export class AttachmentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const responses = await Promise.all( unifiedAttachmentData.map((unifiedData) => @@ -40,19 +40,15 @@ export class AttachmentService { ); const allAttachments = responses.flatMap( - (response) => response.data.attachments, + (response) => response.attachments, ); const allRemoteData = responses.flatMap( - (response) => response.data.remote_data || [], + (response) => response.remote_data || [], ); return { - data: { - attachments: allAttachments, - remote_data: allRemoteData, - }, - message: 'All attachments inserted successfully', - statusCode: 201, + attachments: allAttachments, + remote_data: allRemoteData, }; } catch (error) { handleServiceError(error, this.logger); @@ -64,7 +60,7 @@ export class AttachmentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const linkedUser = await this.prisma.linked_users.findUnique({ where: { @@ -140,12 +136,12 @@ export class AttachmentService { }); await this.webhook.handleWebhook( - result_attachment.data.attachments, + result_attachment.attachments, 'ticketing.attachment.created', linkedUser.id_project, event.id_event, ); - return { statusCode: 201, data: result_attachment.data }; + return result_attachment; } catch (error) { handleServiceError(error, this.logger); } @@ -154,7 +150,7 @@ export class AttachmentService { async getAttachment( id_ticketing_attachment: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const attachment = await this.prisma.tcg_attachments.findUnique({ where: { @@ -213,10 +209,7 @@ export class AttachmentService { }; } - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -226,7 +219,7 @@ export class AttachmentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const attachments = await this.prisma.tcg_attachments.findMany({ @@ -310,10 +303,7 @@ export class AttachmentService { }, }); - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -323,7 +313,7 @@ export class AttachmentService { async downloadAttachment( id_ticketing_attachment: string, remote_data?: boolean, - ): Promise> { + ): Promise { return; } } diff --git a/packages/api/src/ticketing/attachment/types/model.unified.ts b/packages/api/src/ticketing/attachment/types/model.unified.ts index 976430946..e000714cc 100644 --- a/packages/api/src/ticketing/attachment/types/model.unified.ts +++ b/packages/api/src/ticketing/attachment/types/model.unified.ts @@ -1,3 +1,5 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; + export class UnifiedAttachmentInput { file_name: string; file_url: string; @@ -6,6 +8,15 @@ export class UnifiedAttachmentInput { } export class UnifiedAttachmentOutput extends UnifiedAttachmentInput { + @ApiPropertyOptional({ + description: 'The id of the attachment', + type: String, + }) id?: string; + @ApiPropertyOptional({ + description: + 'The id of the attachment in the context of the Ticketing software', + type: String, + }) remote_id?: string; } diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index 6e9f54415..7ae362da5 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -32,7 +32,7 @@ export class CommentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const responses = await Promise.all( unifiedCommentData.map((unifiedData) => @@ -45,20 +45,14 @@ export class CommentService { ), ); - const allComments = responses.flatMap( - (response) => response.data.comments, - ); + const allComments = responses.flatMap((response) => response.comments); const allRemoteData = responses.flatMap( - (response) => response.data.remote_data || [], + (response) => response.remote_data || [], ); return { - data: { - comments: allComments, - remote_data: allRemoteData, - }, - message: 'All comments inserted successfully', - statusCode: 201, + comments: allComments, + remote_data: allRemoteData, }; } catch (error) { handleServiceError(error, this.logger); @@ -70,7 +64,7 @@ export class CommentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const linkedUser = await this.prisma.linked_users.findUnique({ where: { @@ -265,12 +259,12 @@ export class CommentService { }, }); await this.webhook.handleWebhook( - result_comment.data.comments, + result_comment.comments, 'ticketing.comment.created', linkedUser.id_project, event.id_event, ); - return { ...resp, data: result_comment.data }; + return result_comment; } catch (error) { handleServiceError(error, this.logger); } @@ -280,7 +274,7 @@ export class CommentService { async getComment( id_commenting_comment: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const comment = await this.prisma.tcg_comments.findUnique({ where: { @@ -344,10 +338,7 @@ export class CommentService { }; } - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -359,7 +350,7 @@ export class CommentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const comments = await this.prisma.tcg_comments.findMany({ where: { @@ -446,10 +437,7 @@ export class CommentService { }, }); - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index 9f1d06103..914010230 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -15,7 +15,13 @@ export class UnifiedCommentInput { } export class UnifiedCommentOutput { + @ApiPropertyOptional({ description: 'The id of the comment', type: String }) id?: string; + @ApiPropertyOptional({ + description: + 'The id of the comment in the context of the Ticketing software', + type: String, + }) remote_id?: string; body: string; html_body?: string; diff --git a/packages/api/src/ticketing/contact/services/contact.service.ts b/packages/api/src/ticketing/contact/services/contact.service.ts index a4e8eb718..37a9eafc7 100644 --- a/packages/api/src/ticketing/contact/services/contact.service.ts +++ b/packages/api/src/ticketing/contact/services/contact.service.ts @@ -16,7 +16,7 @@ export class ContactService { async getContact( id_ticketing_contact: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const contact = await this.prisma.tcg_contacts.findUnique({ where: { @@ -76,10 +76,7 @@ export class ContactService { }; } - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -89,15 +86,13 @@ export class ContactService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const contacts = await this.prisma.tcg_contacts.findMany({ where: { remote_id: integrationId.toLowerCase(), - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -175,10 +170,7 @@ export class ContactService { }, }); - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/tag/services/tag.service.ts b/packages/api/src/ticketing/tag/services/tag.service.ts index 04ca3dc05..cfdfbb032 100644 --- a/packages/api/src/ticketing/tag/services/tag.service.ts +++ b/packages/api/src/ticketing/tag/services/tag.service.ts @@ -16,7 +16,7 @@ export class TagService { async getTag( id_ticketing_tag: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const tag = await this.prisma.tcg_tags.findUnique({ where: { @@ -73,10 +73,7 @@ export class TagService { }; } - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -86,7 +83,7 @@ export class TagService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const tags = await this.prisma.tcg_tags.findMany({ @@ -167,10 +164,7 @@ export class TagService { }, }); - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/team/services/team.service.ts b/packages/api/src/ticketing/team/services/team.service.ts index 0cb5d4a31..669851f43 100644 --- a/packages/api/src/ticketing/team/services/team.service.ts +++ b/packages/api/src/ticketing/team/services/team.service.ts @@ -16,7 +16,7 @@ export class TeamService { async getTeam( id_ticketing_team: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const team = await this.prisma.tcg_teams.findUnique({ where: { @@ -74,10 +74,7 @@ export class TeamService { }; } - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -87,7 +84,7 @@ export class TeamService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced @@ -170,10 +167,7 @@ export class TeamService { }, }); - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index 7f8d90f01..b34881e84 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -34,7 +34,7 @@ export class TicketService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const responses = await Promise.all( unifiedTicketData.map((unifiedData) => @@ -47,18 +47,14 @@ export class TicketService { ), ); - const allTickets = responses.flatMap((response) => response.data.tickets); + const allTickets = responses.flatMap((response) => response.tickets); const allRemoteData = responses.flatMap( - (response) => response.data.remote_data || [], + (response) => response.remote_data || [], ); return { - data: { - tickets: allTickets, - remote_data: allRemoteData, - }, - message: 'All tickets inserted successfully', - statusCode: 201, + tickets: allTickets, + remote_data: allRemoteData, }; } catch (error) { handleServiceError(error, this.logger); @@ -70,7 +66,7 @@ export class TicketService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const linkedUser = await this.prisma.linked_users.findUnique({ where: { @@ -295,12 +291,12 @@ export class TicketService { }, }); await this.webhook.handleWebhook( - result_ticket.data.tickets, + result_ticket.tickets, 'ticketing.ticket.created', linkedUser.id_project, event.id_event, ); - return { ...resp, data: result_ticket.data }; + return result_ticket; } catch (error) { handleServiceError(error, this.logger); } @@ -310,7 +306,7 @@ export class TicketService { async getTicket( id_ticketing_ticket: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const ticket = await this.prisma.tcg_tickets.findUnique({ where: { @@ -376,10 +372,7 @@ export class TicketService { }; } - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -389,15 +382,13 @@ export class TicketService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const tickets = await this.prisma.tcg_tickets.findMany({ where: { remote_id: integrationId.toLowerCase(), - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, /* TODO: only if params include: { @@ -486,10 +477,7 @@ export class TicketService { }, }); - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -498,7 +486,7 @@ export class TicketService { async updateTicket( id: string, updateTicketData: Partial, - ): Promise> { + ): Promise { try { } catch (error) { handleServiceError(error, this.logger); diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index bd1ff689d..028fe4feb 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -18,7 +18,12 @@ export class UnifiedTicketInput { field_mappings?: Record[]; } export class UnifiedTicketOutput extends UnifiedTicketInput { - @ApiProperty() + @ApiPropertyOptional({ description: 'The id of the ticket', type: String }) id?: string; + @ApiPropertyOptional({ + description: + 'The id of the ticket in the context of the Ticketing software', + type: String, + }) remote_id?: string; } diff --git a/packages/api/src/ticketing/user/services/user.service.ts b/packages/api/src/ticketing/user/services/user.service.ts index 9b102211e..269d49ca7 100644 --- a/packages/api/src/ticketing/user/services/user.service.ts +++ b/packages/api/src/ticketing/user/services/user.service.ts @@ -16,7 +16,7 @@ export class UserService { async getUser( id_ticketing_user: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { const user = await this.prisma.tcg_users.findUnique({ where: { @@ -75,10 +75,7 @@ export class UserService { }; } - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } @@ -88,15 +85,13 @@ export class UserService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise> { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const users = await this.prisma.tcg_users.findMany({ where: { remote_id: integrationId.toLowerCase(), - events: { - id_linked_user: linkedUserId, - }, + id_linked_user: linkedUserId, }, }); @@ -174,10 +169,7 @@ export class UserService { }, }); - return { - data: res, - statusCode: 200, - }; + return res; } catch (error) { handleServiceError(error, this.logger); } From 371bced70eea84b5cfe598e7ec400e5a15be14af Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 6 Jan 2024 17:08:30 +0100 Subject: [PATCH 20/25] :construction: Added integrations front and zendesk --- .../services/zendesk/zendesk.service.ts | 13 ++- packages/api/src/@core/utils/types/index.ts | 2 +- .../account/services/zendesk/index.ts | 4 +- .../ticketing/comment/services/front/index.ts | 95 +++++++++++----- .../comment/services/zendesk/index.ts | 6 +- .../api/src/ticketing/comment/utils/index.ts | 4 + .../contact/services/zendesk/index.ts | 4 +- .../ticketing/tag/services/zendesk/index.ts | 4 +- .../ticketing/team/services/zendesk/index.ts | 4 +- .../ticketing/ticket/services/front/index.ts | 102 +++++++++++++----- .../ticket/services/zendesk/index.ts | 6 +- .../ticketing/user/services/zendesk/index.ts | 4 +- 12 files changed, 172 insertions(+), 76 deletions(-) diff --git a/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts b/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts index 2f7e03066..1b67d3c94 100644 --- a/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts @@ -24,7 +24,7 @@ export class ZendeskConnectionService implements ITicketingConnectionService { private registry: ServiceRegistry, ) { this.logger.setContext(ZendeskConnectionService.name); - this.registry.registerService('zendesk_t', this); + this.registry.registerService('zendesk_tcg', this); } async handleCallback(opts: CallbackParams) { @@ -33,7 +33,7 @@ export class ZendeskConnectionService implements ITicketingConnectionService { const isNotUnique = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', //TODO + provider_slug: 'zendesk_tcg', }, }); @@ -81,7 +81,7 @@ export class ZendeskConnectionService implements ITicketingConnectionService { db_res = await this.prisma.connections.create({ data: { id_connection: uuidv4(), - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', token_type: 'oauth', access_token: this.cryptoService.encrypt(data.access_token), refresh_token: '', @@ -99,7 +99,12 @@ export class ZendeskConnectionService implements ITicketingConnectionService { } return db_res; } catch (error) { - handleServiceError(error, this.logger, 'zendesk_t', Action.oauthCallback); + handleServiceError( + error, + this.logger, + 'zendesk_tcg', + Action.oauthCallback, + ); } } diff --git a/packages/api/src/@core/utils/types/index.ts b/packages/api/src/@core/utils/types/index.ts index 998123644..7cf470ff2 100644 --- a/packages/api/src/@core/utils/types/index.ts +++ b/packages/api/src/@core/utils/types/index.ts @@ -90,7 +90,7 @@ export const CRM_PROVIDERS = [ export const HRIS_PROVIDERS = ['']; export const ATS_PROVIDERS = ['']; export const ACCOUNTING_PROVIDERS = ['']; -export const TICKETING_PROVIDERS = ['zendesk_t']; +export const TICKETING_PROVIDERS = ['zendesk_tcg', 'front']; //TODO: add github export const MARKETING_AUTOMATION_PROVIDERS = ['']; export const FILE_STORAGE_PROVIDERS = ['']; diff --git a/packages/api/src/ticketing/account/services/zendesk/index.ts b/packages/api/src/ticketing/account/services/zendesk/index.ts index 9f5908ef0..584190c40 100644 --- a/packages/api/src/ticketing/account/services/zendesk/index.ts +++ b/packages/api/src/ticketing/account/services/zendesk/index.ts @@ -25,7 +25,7 @@ export class ZendeskService implements IAccountService { this.logger.setContext( TicketingObject.account.toUpperCase() + ':' + ZendeskService.name, ); - this.registry.registerService('zendesk_t', this); + this.registry.registerService('zendesk_tcg', this); } async syncAccounts( @@ -36,7 +36,7 @@ export class ZendeskService implements IAccountService { const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', }, }); diff --git a/packages/api/src/ticketing/comment/services/front/index.ts b/packages/api/src/ticketing/comment/services/front/index.ts index d15f613a9..89aeb0b1e 100644 --- a/packages/api/src/ticketing/comment/services/front/index.ts +++ b/packages/api/src/ticketing/comment/services/front/index.ts @@ -8,8 +8,8 @@ import { ActionType, handleServiceError } from '@@core/utils/errors'; import { ICommentService } from '@ticketing/comment/types'; import { TicketingObject } from '@ticketing/@utils/@types'; import { FrontCommentInput, FrontCommentOutput } from './types'; -import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../registry.service'; +import { fetchFileStreamFromURL } from '@ticketing/comment/utils'; @Injectable() export class FrontService implements ICommentService { @@ -37,36 +37,75 @@ export class FrontService implements ICommentService { provider_slug: 'front', }, }); - const uuids = commentData.attachments; + let uploads = []; - uuids.map(async (uuid) => { - const res = await this.prisma.tcg_attachments.findUnique({ - where: { - id_tcg_attachment: uuid, + const uuids = commentData.attachments; + if (uuids && uuids.length > 0) { + for (const uuid of uuids) { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) + 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 + const fileStream = await fetchFileStreamFromURL(res.file_url); + + uploads = [...uploads, fileStream]; + } + } + + let resp; + if (uploads.length > 0) { + const dataBody = { + ...commentData, + attachments: uploads, + }; + const formData = new FormData(); + + if (dataBody.author_id) { + formData.append('author_id', dataBody.author_id); + } + formData.append('body', dataBody.body); + + for (let i = 0; i < uploads.length; i++) { + const up = uploads[i]; + formData.append(`attachments[${i}]`, up); + } + + resp = await axios.post( + `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, }, - }); - if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); - //TODO: construct the right binary attachment - //get the AWS s3 right file - const url = res.file_url; - uploads = [...uploads, url]; - }); - const dataBody = { - ...commentData, - attachments: uploads, - }; - const resp = await axios.post( - `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, - JSON.stringify(dataBody), - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, + ); + } else { + const dataBody = { + ...commentData, + }; + resp = await axios.post( + `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, + JSON.stringify(dataBody), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, }, - }, - ); + ); + } + return { data: resp.data, message: 'Front comment created', diff --git a/packages/api/src/ticketing/comment/services/zendesk/index.ts b/packages/api/src/ticketing/comment/services/zendesk/index.ts index 7c8c31112..d32f1e327 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/index.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/index.ts @@ -23,7 +23,7 @@ export class ZendeskService implements ICommentService { this.logger.setContext( TicketingObject.comment.toUpperCase() + ':' + ZendeskService.name, ); - this.registry.registerService('zendesk_t', this); + this.registry.registerService('zendesk_tcg', this); } async addComment( @@ -35,7 +35,7 @@ export class ZendeskService implements ICommentService { const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', }, }); @@ -111,7 +111,7 @@ export class ZendeskService implements ICommentService { const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', }, }); //retrieve ticket remote id so we can retrieve the comments in the original software diff --git a/packages/api/src/ticketing/comment/utils/index.ts b/packages/api/src/ticketing/comment/utils/index.ts index e69de29bb..2e71ef4f3 100644 --- a/packages/api/src/ticketing/comment/utils/index.ts +++ b/packages/api/src/ticketing/comment/utils/index.ts @@ -0,0 +1,4 @@ +export async function fetchFileStreamFromURL(file_url: string) { + //TODO; + return; +} diff --git a/packages/api/src/ticketing/contact/services/zendesk/index.ts b/packages/api/src/ticketing/contact/services/zendesk/index.ts index 06e4c2581..9fd7d4abf 100644 --- a/packages/api/src/ticketing/contact/services/zendesk/index.ts +++ b/packages/api/src/ticketing/contact/services/zendesk/index.ts @@ -25,7 +25,7 @@ export class ZendeskService implements IContactService { this.logger.setContext( TicketingObject.contact.toUpperCase() + ':' + ZendeskService.name, ); - this.registry.registerService('zendesk_t', this); + this.registry.registerService('zendesk_tcg', this); } async syncContacts( @@ -36,7 +36,7 @@ export class ZendeskService implements IContactService { const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', }, }); diff --git a/packages/api/src/ticketing/tag/services/zendesk/index.ts b/packages/api/src/ticketing/tag/services/zendesk/index.ts index 5e2d83dc9..4f368e979 100644 --- a/packages/api/src/ticketing/tag/services/zendesk/index.ts +++ b/packages/api/src/ticketing/tag/services/zendesk/index.ts @@ -22,7 +22,7 @@ export class ZendeskService implements ITagService { this.logger.setContext( TicketingObject.tag.toUpperCase() + ':' + ZendeskService.name, ); - this.registry.registerService('zendesk_t', this); + this.registry.registerService('zendesk_tcg', this); } async syncTags( @@ -33,7 +33,7 @@ export class ZendeskService implements ITagService { const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', }, }); diff --git a/packages/api/src/ticketing/team/services/zendesk/index.ts b/packages/api/src/ticketing/team/services/zendesk/index.ts index de82ebf62..ac0362b48 100644 --- a/packages/api/src/ticketing/team/services/zendesk/index.ts +++ b/packages/api/src/ticketing/team/services/zendesk/index.ts @@ -22,7 +22,7 @@ export class ZendeskService implements ITeamService { this.logger.setContext( TicketingObject.team.toUpperCase() + ':' + ZendeskService.name, ); - this.registry.registerService('zendesk_t', this); + this.registry.registerService('zendesk_tcg', this); } async syncTeams( @@ -33,7 +33,7 @@ export class ZendeskService implements ITeamService { const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', }, }); diff --git a/packages/api/src/ticketing/ticket/services/front/index.ts b/packages/api/src/ticketing/ticket/services/front/index.ts index 6d18d12c6..005a4dccc 100644 --- a/packages/api/src/ticketing/ticket/services/front/index.ts +++ b/packages/api/src/ticketing/ticket/services/front/index.ts @@ -9,6 +9,7 @@ import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; import { ServiceRegistry } from '../registry.service'; import { FrontTicketInput, FrontTicketOutput } from './types'; +import { fetchFileStreamFromURL } from '@ticketing/comment/utils'; @Injectable() export class FrontService implements ITicketService { @@ -23,6 +24,7 @@ export class FrontService implements ITicketService { ); this.registry.registerService('front', this); } + async addTicket( ticketData: FrontTicketInput, linkedUserId: string, @@ -35,36 +37,82 @@ export class FrontService implements ITicketService { }, }); - const uuids = ticketData.comment.attachments; let uploads = []; - uuids.map(async (uuid) => { - const res = await this.prisma.tcg_attachments.findUnique({ - where: { - id_tcg_attachment: uuid, + const uuids = ticketData.comment.attachments; + if (uuids && uuids.length > 0) { + for (const uuid of uuids) { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) + 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 + const fileStream = await fetchFileStreamFromURL(res.file_url); + + uploads = [...uploads, fileStream]; + } + } + + let resp; + if (uploads.length > 0) { + const dataBody = { + ...ticketData, + comment: { ...ticketData.comment, attachments: uploads }, + }; + const formData = new FormData(); + + formData.append('type', dataBody.type); + + if (dataBody.inbox_id) { + formData.append('inbox_id', dataBody.inbox_id); + } + if (dataBody.teammate_ids && dataBody.teammate_ids.length > 0) { + for (let i = 0; i < dataBody.teammate_ids.length; i++) { + const item = dataBody.teammate_ids[i]; + formData.append(`teammate_ids[${i}]`, item); + } + } + if (dataBody.comment.author_id) { + formData.append('comment[author_id]', dataBody.comment.author_id); + } + formData.append('comment[body]', dataBody.comment.body); + + for (let i = 0; i < uploads.length; i++) { + const up = uploads[i]; + formData.append(`comment[attachments][${i}]`, up); + } + + resp = await axios.post( + `https://api2.frontapp.com/conversations`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, }, - }); - if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); - //TODO: construct the right binary attachment - //get the AWS s3 right file - const url = res.file_url; - uploads = [...uploads, url]; - }); - const dataBody = { - ...ticketData, - comment: { ...ticketData.comment, attachments: uploads }, - }; - const resp = await axios.post( - `https://api2.frontapp.com/conversations`, - JSON.stringify(dataBody), - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, + ); + } else { + resp = await axios.post( + `https://api2.frontapp.com/conversations`, + JSON.stringify(ticketData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, }, - }, - ); + ); + } + return { data: resp.data, message: 'Front ticket created', diff --git a/packages/api/src/ticketing/ticket/services/zendesk/index.ts b/packages/api/src/ticketing/ticket/services/zendesk/index.ts index bcbaaa4fb..918c6b8c7 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/index.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/index.ts @@ -26,7 +26,7 @@ export class ZendeskService implements ITicketService { this.logger.setContext( TicketingObject.ticket.toUpperCase() + ':' + ZendeskService.name, ); - this.registry.registerService('zendesk_t', this); + this.registry.registerService('zendesk_tcg', this); } async addTicket( ticketData: ZendeskTicketInput, @@ -36,7 +36,7 @@ export class ZendeskService implements ITicketService { const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', }, }); @@ -112,7 +112,7 @@ export class ZendeskService implements ITicketService { const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', }, }); diff --git a/packages/api/src/ticketing/user/services/zendesk/index.ts b/packages/api/src/ticketing/user/services/zendesk/index.ts index 532981e03..7d6dc2a37 100644 --- a/packages/api/src/ticketing/user/services/zendesk/index.ts +++ b/packages/api/src/ticketing/user/services/zendesk/index.ts @@ -22,7 +22,7 @@ export class ZendeskService implements IUserService { this.logger.setContext( TicketingObject.user.toUpperCase() + ':' + ZendeskService.name, ); - this.registry.registerService('zendesk_t', this); + this.registry.registerService('zendesk_tcg', this); } async syncUsers( @@ -33,7 +33,7 @@ export class ZendeskService implements IUserService { const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'zendesk_t', + provider_slug: 'zendesk_tcg', }, }); From e0b18294e1d540eba550b8a4e1c24d9bae1c39e3 Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 6 Jan 2024 17:52:14 +0100 Subject: [PATCH 21/25] :construction: Added zendesk and front mappings --- .../account/services/account.service.ts | 1 - .../ticketing/account/services/front/index.ts | 2 +- .../account/services/front/mappers.ts | 20 ++++++-- .../ticketing/account/services/front/types.ts | 21 +++++++- .../contact/services/front/mappers.ts | 27 ++++++++-- .../ticketing/contact/services/front/types.ts | 37 +++++++++++++- .../ticketing/team/services/front/index.ts | 2 +- .../ticketing/team/services/front/mappers.ts | 14 ++++-- .../ticketing/team/services/front/types.ts | 43 +++++++++++++++- .../ticketing/user/services/front/mappers.ts | 20 ++++++-- .../ticketing/user/services/front/types.ts | 25 +++++++++- .../user/services/zendesk/mappers.ts | 18 +++++-- .../ticketing/user/services/zendesk/types.ts | 49 +++++++++++++++++-- 13 files changed, 245 insertions(+), 34 deletions(-) diff --git a/packages/api/src/ticketing/account/services/account.service.ts b/packages/api/src/ticketing/account/services/account.service.ts index b84f378ab..d603d4443 100644 --- a/packages/api/src/ticketing/account/services/account.service.ts +++ b/packages/api/src/ticketing/account/services/account.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; import { handleServiceError } from '@@core/utils/errors'; import { UnifiedAccountOutput } from '../types/model.unified'; import { AccountResponse } from '../types'; diff --git a/packages/api/src/ticketing/account/services/front/index.ts b/packages/api/src/ticketing/account/services/front/index.ts index 15ab6f74a..d1ed87b59 100644 --- a/packages/api/src/ticketing/account/services/front/index.ts +++ b/packages/api/src/ticketing/account/services/front/index.ts @@ -35,7 +35,7 @@ export class FrontService implements IAccountService { }, }); - const resp = await axios.get('https://api2.frontapp.com/teammates', { + const resp = await axios.get('https://api2.frontapp.com/accounts', { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cryptoService.decrypt( diff --git a/packages/api/src/ticketing/account/services/front/mappers.ts b/packages/api/src/ticketing/account/services/front/mappers.ts index 60eacfc43..cf4089ec3 100644 --- a/packages/api/src/ticketing/account/services/front/mappers.ts +++ b/packages/api/src/ticketing/account/services/front/mappers.ts @@ -26,18 +26,28 @@ export class FrontAccountMapper implements IAccountMapper { // If the source is not an array, convert it to an array for mapping const sourcesArray = Array.isArray(source) ? source : [source]; - return sourcesArray.map((ticket) => - this.mapSingleTicketToUnified(ticket, customFieldMappings), + return sourcesArray.map((account) => + this.mapSingleAccountToUnified(account, customFieldMappings), ); } - private mapSingleTicketToUnified( - ticket: FrontAccountOutput, + private mapSingleAccountToUnified( + account: FrontAccountOutput, customFieldMappings?: { slug: string; remote_id: string; }[], ): UnifiedAccountOutput { - return; + const field_mappings = customFieldMappings?.map((mapping) => ({ + [mapping.slug]: account.custom_fields?.[mapping.remote_id], + })); + + const unifiedAccount: UnifiedAccountOutput = { + name: account.name, + domains: account.domains.flat(), + field_mappings: field_mappings, + }; + + return unifiedAccount; } } diff --git a/packages/api/src/ticketing/account/services/front/types.ts b/packages/api/src/ticketing/account/services/front/types.ts index ab8889055..24fd1e68b 100644 --- a/packages/api/src/ticketing/account/services/front/types.ts +++ b/packages/api/src/ticketing/account/services/front/types.ts @@ -2,4 +2,23 @@ export type FrontAccountInput = { id: string; }; -export type FrontAccountOutput = FrontAccountInput; +export type FrontAccountOutput = { + _links: { + self: string; + related: { + contacts: string; + }; + }; + id: string; + name: string; + logo_url: string; + description: string; + domains: string[][]; + external_id: number; + custom_fields: { + employees: number; + headquarters: string; + }; + created_at: number; + updated_at: number; +}; diff --git a/packages/api/src/ticketing/contact/services/front/mappers.ts b/packages/api/src/ticketing/contact/services/front/mappers.ts index 1e9bd9119..b3f604ae6 100644 --- a/packages/api/src/ticketing/contact/services/front/mappers.ts +++ b/packages/api/src/ticketing/contact/services/front/mappers.ts @@ -26,18 +26,35 @@ export class FrontContactMapper implements IContactMapper { // If the source is not an array, convert it to an array for mapping const sourcesArray = Array.isArray(source) ? source : [source]; - return sourcesArray.map((ticket) => - this.mapSingleTicketToUnified(ticket, customFieldMappings), + return sourcesArray.map((contact) => + this.mapSingleContactToUnified(contact, customFieldMappings), ); } - private mapSingleTicketToUnified( - ticket: FrontContactOutput, + private mapSingleContactToUnified( + contact: FrontContactOutput, customFieldMappings?: { slug: string; remote_id: string; }[], ): UnifiedContactOutput { - return; + const field_mappings = customFieldMappings?.map((mapping) => ({ + [mapping.slug]: contact.custom_fields?.[mapping.remote_id], + })); + const emailHandle = contact.handles.find( + (handle) => handle.source === 'email', + ); + const phoneHandle = contact.handles.find( + (handle) => handle.source === 'phone', + ); + + const unifiedContact: UnifiedContactOutput = { + name: contact.name, + email_address: emailHandle.handle || '', + phone_number: phoneHandle.handle || '', + field_mappings: field_mappings, + }; + + return unifiedContact; } } diff --git a/packages/api/src/ticketing/contact/services/front/types.ts b/packages/api/src/ticketing/contact/services/front/types.ts index f744e464a..d8c60213a 100644 --- a/packages/api/src/ticketing/contact/services/front/types.ts +++ b/packages/api/src/ticketing/contact/services/front/types.ts @@ -2,4 +2,39 @@ export type FrontContactInput = { id: string; }; -export type FrontContactOutput = FrontContactInput; +export type FrontContactOutput = { + _links: ContactLink; + id: string; + name: string; + description: string; + avatar_url: string; + is_spammer: boolean; + links: string[][]; + groups: Group[]; + handles: Handle[]; + custom_fields: { + [key: string]: string | boolean; + }; + is_private: boolean; +}; + +type ContactLink = { + self: string; + related: { + notes?: string; + conversations?: string; + owner?: string | null; + }; +}; + +type Group = { + _links: ContactLink; + id: string; + name: string; + is_private: boolean; +}; + +type Handle = { + handle: string; + source: string; +}; diff --git a/packages/api/src/ticketing/team/services/front/index.ts b/packages/api/src/ticketing/team/services/front/index.ts index a5913cc11..f28d17b50 100644 --- a/packages/api/src/ticketing/team/services/front/index.ts +++ b/packages/api/src/ticketing/team/services/front/index.ts @@ -35,7 +35,7 @@ export class FrontService implements ITeamService { }, }); - const resp = await axios.get('https://api2.frontapp.com/teammates', { + const resp = await axios.get('https://api2.frontapp.com/teams', { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cryptoService.decrypt( diff --git a/packages/api/src/ticketing/team/services/front/mappers.ts b/packages/api/src/ticketing/team/services/front/mappers.ts index e3879bf8f..e746bf085 100644 --- a/packages/api/src/ticketing/team/services/front/mappers.ts +++ b/packages/api/src/ticketing/team/services/front/mappers.ts @@ -26,18 +26,22 @@ export class FrontTeamMapper implements ITeamMapper { // If the source is not an array, convert it to an array for mapping const sourcesArray = Array.isArray(source) ? source : [source]; - return sourcesArray.map((ticket) => - this.mapSingleTicketToUnified(ticket, customFieldMappings), + return sourcesArray.map((team) => + this.mapSingleTeamToUnified(team, customFieldMappings), ); } - private mapSingleTicketToUnified( - ticket: FrontTeamOutput, + private mapSingleTeamToUnified( + team: FrontTeamOutput, customFieldMappings?: { slug: string; remote_id: string; }[], ): UnifiedTeamOutput { - return; + const unifiedTeam: UnifiedTeamOutput = { + name: team.name, + }; + + return unifiedTeam; } } diff --git a/packages/api/src/ticketing/team/services/front/types.ts b/packages/api/src/ticketing/team/services/front/types.ts index 04b193f85..91651377b 100644 --- a/packages/api/src/ticketing/team/services/front/types.ts +++ b/packages/api/src/ticketing/team/services/front/types.ts @@ -2,4 +2,45 @@ export type FrontTeamInput = { id: string; }; -export type FrontTeamOutput = FrontTeamInput; +export type FrontTeamOutput = { + _links: TeamLink; + id: string; + name: string; + inboxes: Inbox[]; + members: TeamMember[]; +}; + +type TeamLink = { + self: string; + related?: { + teammates?: string; + conversations?: string; + channels?: string; + owner?: string; + }; +}; + +type CustomFields = { + [key: string]: string | boolean | number | null; +}; + +type Inbox = { + _links: TeamLink; + id: string; + name: string; + is_private: boolean; + custom_fields: CustomFields; +}; + +type TeamMember = { + _links: TeamLink; + id: string; + email: string; + username: string; + first_name: string; + last_name: string; + is_admin: boolean; + is_available: boolean; + is_blocked: boolean; + custom_fields: CustomFields; +}; diff --git a/packages/api/src/ticketing/user/services/front/mappers.ts b/packages/api/src/ticketing/user/services/front/mappers.ts index 9e37057cf..cfb94b288 100644 --- a/packages/api/src/ticketing/user/services/front/mappers.ts +++ b/packages/api/src/ticketing/user/services/front/mappers.ts @@ -26,18 +26,28 @@ export class FrontUserMapper implements IUserMapper { // If the source is not an array, convert it to an array for mapping const sourcesArray = Array.isArray(source) ? source : [source]; - return sourcesArray.map((ticket) => - this.mapSingleTicketToUnified(ticket, customFieldMappings), + return sourcesArray.map((user) => + this.mapSingleUserToUnified(user, customFieldMappings), ); } - private mapSingleTicketToUnified( - ticket: FrontUserOutput, + private mapSingleUserToUnified( + user: FrontUserOutput, customFieldMappings?: { slug: string; remote_id: string; }[], ): UnifiedUserOutput { - return; + const field_mappings = customFieldMappings?.map((mapping) => ({ + [mapping.slug]: user.custom_fields?.[mapping.remote_id], + })); + + const unifiedUser: UnifiedUserOutput = { + name: `${user.last_name} ${user.last_name}`, + email_address: user.email, + field_mappings: field_mappings, + }; + + return unifiedUser; } } diff --git a/packages/api/src/ticketing/user/services/front/types.ts b/packages/api/src/ticketing/user/services/front/types.ts index 6323ca120..b421d1e68 100644 --- a/packages/api/src/ticketing/user/services/front/types.ts +++ b/packages/api/src/ticketing/user/services/front/types.ts @@ -2,4 +2,27 @@ export type FrontUserInput = { id: string; }; -export type FrontUserOutput = FrontUserInput; +export type FrontUserOutput = { + _links: TeammateLink; + id: string; + email: string; + username: string; + first_name: string; + last_name: string; + is_admin: boolean; + is_available: boolean; + is_blocked: boolean; + custom_fields: CustomFields; +}; + +type TeammateLink = { + self: string; + related: { + inboxes: string; + conversations: string; + }; +}; + +type CustomFields = { + [key: string]: string | boolean | number | null; +}; diff --git a/packages/api/src/ticketing/user/services/zendesk/mappers.ts b/packages/api/src/ticketing/user/services/zendesk/mappers.ts index 223be54ef..6c58979c3 100644 --- a/packages/api/src/ticketing/user/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/user/services/zendesk/mappers.ts @@ -26,18 +26,28 @@ export class ZendeskUserMapper implements IUserMapper { if (!Array.isArray(source)) { return this.mapSingleUserToUnified(source, customFieldMappings); } - return source.map((ticket) => - this.mapSingleUserToUnified(ticket, customFieldMappings), + return source.map((user) => + this.mapSingleUserToUnified(user, customFieldMappings), ); } private mapSingleUserToUnified( - ticket: ZendeskUserOutput, + user: ZendeskUserOutput, customFieldMappings?: { slug: string; remote_id: string; }[], ): UnifiedUserOutput { - return; + const field_mappings = customFieldMappings?.map((mapping) => ({ + [mapping.slug]: user.user_fields?.[mapping.remote_id], + })); + + const unifiedUser: UnifiedUserOutput = { + name: user.name, + email_address: user.email, + field_mappings: field_mappings, + }; + + return unifiedUser; } } diff --git a/packages/api/src/ticketing/user/services/zendesk/types.ts b/packages/api/src/ticketing/user/services/zendesk/types.ts index 15356640c..f16b0433f 100644 --- a/packages/api/src/ticketing/user/services/zendesk/types.ts +++ b/packages/api/src/ticketing/user/services/zendesk/types.ts @@ -2,6 +2,49 @@ export type ZendeskUserInput = { _: string; }; -export type ZendeskUserOutput = ZendeskUserInput & { - id: number; // Read-only. Automatically assigned when the ticket is created. -}; +export type ZendeskUserOutput = Partial<{ + active: boolean; + alias?: string; + chat_only: boolean; + created_at: string; + custom_role_id?: number; + default_group_id?: number; + details?: string; + email: string; + external_id?: string; + iana_time_zone: string; + id: number; + last_login_at: string; + locale?: string; + locale_id?: number; + moderator?: boolean; + name: string; + notes?: string; + only_private_comments?: boolean; + organization_id?: number; + phone?: string; + photo?: { [key: string]: any }; // Assuming an object type for the Attachment object + remote_photo_url?: string; + report_csv: boolean; + restricted_agent?: boolean; + role?: string; + role_type: number; + shared: boolean; + shared_agent: boolean; + shared_phone_number?: boolean; + signature?: string; + suspended?: boolean; + tags?: string[]; + ticket_restriction?: + | 'organization' + | 'groups' + | 'assigned' + | 'requested' + | null; + time_zone?: string; + two_factor_auth_enabled: boolean; + updated_at: string; + url: string; + user_fields?: { [key: string]: any }; // Assuming an object type for custom fields + verified?: boolean; +}>; From ad74d63c400ca8025f15ae93650ebd0011167e11 Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 6 Jan 2024 18:48:39 +0100 Subject: [PATCH 22/25] :construction: Added tags --- packages/api/prisma/schema.prisma | 2 +- .../src/ticketing/tag/services/front/index.ts | 22 +++++++++-- .../ticketing/tag/services/front/mappers.ts | 14 ++++--- .../src/ticketing/tag/services/front/types.ts | 22 ++++++++++- .../ticketing/tag/services/github/index.ts | 2 +- .../ticketing/tag/services/zendesk/index.ts | 15 ++++++- .../ticketing/tag/services/zendesk/mappers.ts | 12 ++++-- .../ticketing/tag/services/zendesk/types.ts | 4 +- .../src/ticketing/tag/sync/sync.service.ts | 39 ++++++++++++------- packages/api/src/ticketing/tag/types/index.ts | 2 +- .../ticketing/ticket/services/front/index.ts | 30 ++++++++++++-- .../ticket/services/front/mappers.ts | 3 +- .../ticketing/ticket/services/front/types.ts | 1 + .../ticket/services/hubspot/mappers.ts | 1 - .../ticket/services/ticket.service.ts | 8 ++-- .../ticket/services/zendesk/mappers.ts | 4 +- .../src/ticketing/ticket/sync/sync.service.ts | 4 +- .../ticketing/ticket/types/model.unified.ts | 2 +- 18 files changed, 138 insertions(+), 49 deletions(-) diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 9964147c6..b8b0933c9 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -426,7 +426,7 @@ model tcg_tickets { due_date DateTime? @db.Timestamp(6) ticket_type String? parent_ticket String? @db.Uuid - tags String? + tags String[] completed_at DateTime? @db.Timestamp(6) priority String? created_at DateTime @db.Timestamp(6) diff --git a/packages/api/src/ticketing/tag/services/front/index.ts b/packages/api/src/ticketing/tag/services/front/index.ts index 33bd415a2..32fac9bee 100644 --- a/packages/api/src/ticketing/tag/services/front/index.ts +++ b/packages/api/src/ticketing/tag/services/front/index.ts @@ -24,7 +24,10 @@ export class FrontService implements ITagService { this.registry.registerService('front', this); } - async syncTags(linkedUserId: string): Promise> { + async syncTags( + linkedUserId: string, + id_ticket: string, + ): Promise> { try { const connection = await this.prisma.connections.findFirst({ where: { @@ -33,7 +36,16 @@ export class FrontService implements ITagService { }, }); - const resp = await axios.get('https://api2.frontapp.com/teammates', { + 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.frontapp.com/conversations', { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cryptoService.decrypt( @@ -43,8 +55,12 @@ export class FrontService implements ITagService { }); this.logger.log(`Synced front tags !`); + const conversation = resp.data._results.find( + (c) => c.id === ticket.remote_id, + ); + return { - data: resp.data._results, + data: conversation.tags, message: 'Front tags retrieved', statusCode: 200, }; diff --git a/packages/api/src/ticketing/tag/services/front/mappers.ts b/packages/api/src/ticketing/tag/services/front/mappers.ts index 965723b22..1ab02aef1 100644 --- a/packages/api/src/ticketing/tag/services/front/mappers.ts +++ b/packages/api/src/ticketing/tag/services/front/mappers.ts @@ -26,18 +26,22 @@ export class FrontTagMapper implements ITagMapper { // If the source is not an array, convert it to an array for mapping const sourcesArray = Array.isArray(source) ? source : [source]; - return sourcesArray.map((ticket) => - this.mapSingleTicketToUnified(ticket, customFieldMappings), + return sourcesArray.map((tag) => + this.mapSingleTagToUnified(tag, customFieldMappings), ); } - private mapSingleTicketToUnified( - ticket: FrontTagOutput, + private mapSingleTagToUnified( + tag: FrontTagOutput, customFieldMappings?: { slug: string; remote_id: string; }[], ): UnifiedTagOutput { - return; + const unifiedTag: UnifiedTagOutput = { + name: tag.name, + }; + + return unifiedTag; } } diff --git a/packages/api/src/ticketing/tag/services/front/types.ts b/packages/api/src/ticketing/tag/services/front/types.ts index 38e26e603..39c04ce83 100644 --- a/packages/api/src/ticketing/tag/services/front/types.ts +++ b/packages/api/src/ticketing/tag/services/front/types.ts @@ -2,4 +2,24 @@ export type FrontTagInput = { id: string; }; -export type FrontTagOutput = FrontTagInput; +export type FrontTagOutput = { + _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/services/github/index.ts b/packages/api/src/ticketing/tag/services/github/index.ts index 030b03358..d7dee0208 100644 --- a/packages/api/src/ticketing/tag/services/github/index.ts +++ b/packages/api/src/ticketing/tag/services/github/index.ts @@ -27,7 +27,7 @@ export class GithubService implements ITagService { async syncTags( linkedUserId: string, - custom_properties?: string[], + id_ticket: string, ): Promise> { try { const connection = await this.prisma.connections.findFirst({ diff --git a/packages/api/src/ticketing/tag/services/zendesk/index.ts b/packages/api/src/ticketing/tag/services/zendesk/index.ts index 4f368e979..e7f9baf25 100644 --- a/packages/api/src/ticketing/tag/services/zendesk/index.ts +++ b/packages/api/src/ticketing/tag/services/zendesk/index.ts @@ -27,7 +27,7 @@ export class ZendeskService implements ITagService { async syncTags( linkedUserId: string, - custom_properties?: string[], + id_ticket: string, ): Promise> { try { const connection = await this.prisma.connections.findFirst({ @@ -37,8 +37,19 @@ export class ZendeskService implements ITagService { }, }); + const ticket = await this.prisma.tcg_tickets.findUnique({ + where: { + id_tcg_ticket: id_ticket, + }, + select: { + remote_id: true, + }, + }); + const resp = await axios.get( - `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tags`, + `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tickets/${ + ticket.remote_id + }/tags`, { headers: { 'Content-Type': 'application/json', diff --git a/packages/api/src/ticketing/tag/services/zendesk/mappers.ts b/packages/api/src/ticketing/tag/services/zendesk/mappers.ts index 40d81ea4d..420b1c8a6 100644 --- a/packages/api/src/ticketing/tag/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/tag/services/zendesk/mappers.ts @@ -26,18 +26,22 @@ export class ZendeskTagMapper implements ITagMapper { if (!Array.isArray(source)) { return this.mapSingleTagToUnified(source, customFieldMappings); } - return source.map((ticket) => - this.mapSingleTagToUnified(ticket, customFieldMappings), + return source.map((tag) => + this.mapSingleTagToUnified(tag, customFieldMappings), ); } private mapSingleTagToUnified( - ticket: ZendeskTagOutput, + tag: ZendeskTagOutput, customFieldMappings?: { slug: string; remote_id: string; }[], ): UnifiedTagOutput { - return; + const unifiedTag: UnifiedTagOutput = { + name: tag.tags[0], //TODO + }; + + return unifiedTag; } } diff --git a/packages/api/src/ticketing/tag/services/zendesk/types.ts b/packages/api/src/ticketing/tag/services/zendesk/types.ts index e8891b6d8..51532d488 100644 --- a/packages/api/src/ticketing/tag/services/zendesk/types.ts +++ b/packages/api/src/ticketing/tag/services/zendesk/types.ts @@ -2,6 +2,6 @@ export type ZendeskTagInput = { _: string; }; -export type ZendeskTagOutput = ZendeskTagInput & { - id: number; // Read-only. Automatically assigned when the ticket is created. +export type ZendeskTagOutput = { + tags: string[]; }; diff --git a/packages/api/src/ticketing/tag/sync/sync.service.ts b/packages/api/src/ticketing/tag/sync/sync.service.ts index 84ebe1662..ffeff614b 100644 --- a/packages/api/src/ticketing/tag/sync/sync.service.ts +++ b/packages/api/src/ticketing/tag/sync/sync.service.ts @@ -54,21 +54,31 @@ export class SyncService implements OnModuleInit { }, }); const id_project = defaultProject.id_project; - const linkedTags = await this.prisma.linked_users.findMany({ + const linkedUsers = await this.prisma.linked_users.findMany({ where: { id_project: id_project, }, }); - linkedTags.map(async (linkedTag) => { + linkedUsers.map(async (linkedUser) => { try { const providers = TICKETING_PROVIDERS; for (const provider of providers) { try { - await this.syncTagsForLinkedTag( - provider, - linkedTag.id_linked_user, - id_project, - ); + //call the sync comments for every ticket of the linkedUser (a comment is tied to a ticket) + const tickets = await this.prisma.tcg_tickets.findMany({ + where: { + remote_platform: provider, + id_linked_user: linkedUser.id_linked_user, + }, + }); + for (const ticket of tickets) { + await this.syncTagsForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ticket.id_tcg_ticket, + ); + } } catch (error) { handleServiceError(error, this.logger); } @@ -83,10 +93,11 @@ export class SyncService implements OnModuleInit { } //todo: HANDLE DATA REMOVED FROM PROVIDER - async syncTagsForLinkedTag( + async syncTagsForLinkedUser( integrationId: string, linkedUserId: string, id_project: string, + id_ticket: string, ) { try { this.logger.log( @@ -108,15 +119,12 @@ export class SyncService implements OnModuleInit { linkedUserId, 'tag', ); - const remoteProperties: string[] = customFieldMappings.map( - (mapping) => mapping.remote_id, - ); const service: ITagService = this.serviceRegistry.getService(integrationId); const resp: ApiResponse = await service.syncTags( linkedUserId, - remoteProperties, + id_ticket, ); const sourceObject: OriginalTagOutput[] = resp.data; @@ -140,6 +148,7 @@ export class SyncService implements OnModuleInit { unifiedObject, tagIds, integrationId, + id_ticket, sourceObject, ); const event = await this.prisma.events.create({ @@ -171,16 +180,17 @@ export class SyncService implements OnModuleInit { tags: UnifiedTagOutput[], originIds: string[], originSource: string, + id_ticket: string, remote_data: Record[], ): Promise { try { let tags_results: TicketingTag[] = []; for (let i = 0; i < tags.length; i++) { const tag = tags[i]; - const originId = originIds[i]; + let originId = originIds[i]; if (!originId || originId == '') { - throw new NotFoundError(`Origin id not there, found ${originId}`); + originId = 'zendesk_id_tag'; //zendesk does not return a uuid so we put that as default value } const existingTag = await this.prisma.tcg_tags.findFirst({ @@ -214,6 +224,7 @@ export class SyncService implements OnModuleInit { name: tag.name, created_at: new Date(), modified_at: new Date(), + id_tcg_ticket: id_ticket, id_linked_users: linkedUserId, remote_id: originId, remote_platform: originSource, diff --git a/packages/api/src/ticketing/tag/types/index.ts b/packages/api/src/ticketing/tag/types/index.ts index 52abd91f0..7cfe6f28a 100644 --- a/packages/api/src/ticketing/tag/types/index.ts +++ b/packages/api/src/ticketing/tag/types/index.ts @@ -7,7 +7,7 @@ import { ApiResponse } from '@@core/utils/types'; export interface ITagService { syncTags( linkedUserId: string, - custom_properties?: string[], + id_ticket: string, ): Promise>; } diff --git a/packages/api/src/ticketing/ticket/services/front/index.ts b/packages/api/src/ticketing/ticket/services/front/index.ts index 005a4dccc..6dfd856ee 100644 --- a/packages/api/src/ticketing/ticket/services/front/index.ts +++ b/packages/api/src/ticketing/ticket/services/front/index.ts @@ -37,8 +37,11 @@ export class FrontService implements ITicketService { }, }); + //We deconstruct as tags must be added separately + const { tags, ...restOfTicketData } = ticketData; + let uploads = []; - const uuids = ticketData.comment.attachments; + const uuids = restOfTicketData.comment.attachments; if (uuids && uuids.length > 0) { for (const uuid of uuids) { const res = await this.prisma.tcg_attachments.findUnique({ @@ -60,8 +63,8 @@ export class FrontService implements ITicketService { let resp; if (uploads.length > 0) { const dataBody = { - ...ticketData, - comment: { ...ticketData.comment, attachments: uploads }, + ...restOfTicketData, + comment: { ...restOfTicketData.comment, attachments: uploads }, }; const formData = new FormData(); @@ -101,7 +104,26 @@ export class FrontService implements ITicketService { } else { resp = await axios.post( `https://api2.frontapp.com/conversations`, - JSON.stringify(ticketData), + JSON.stringify(restOfTicketData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + } + + //now we can add tags to the conversation we just created + if (tags && tags.length > 0) { + const data = { + tag_ids: tags, + }; + const tag_resp = await axios.post( + `https://api2.frontapp.com/conversations/${resp.data.id}/tags`, + JSON.stringify(data), { headers: { 'Content-Type': 'application/json', diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index 80221d06a..41838feec 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -28,6 +28,7 @@ export class FrontTicketMapper implements ITicketMapper { : source.comment.contact_id, attachments: source.comment.attachments, }, + tags: source.tags, }; //TODO: custom fields => https://dev.frontapp.com/reference/patch_conversations-conversation-id @@ -78,7 +79,7 @@ export class FrontTicketMapper implements ITicketMapper { status: ticket.status, description: ticket.subject, // todo: ? due_date: new Date(ticket.created_at), // todo ? - tags: JSON.stringify(ticket.tags?.map((tag) => tag.name)), + tags: ticket.tags?.map((tag) => tag.name), assigned_to: ticket.assignee ? [ticket.assignee.email] : undefined, //TODO: it must be a uuid of a user object field_mappings: field_mappings, }; diff --git a/packages/api/src/ticketing/ticket/services/front/types.ts b/packages/api/src/ticketing/ticket/services/front/types.ts index ef1520a85..954138722 100644 --- a/packages/api/src/ticketing/ticket/services/front/types.ts +++ b/packages/api/src/ticketing/ticket/services/front/types.ts @@ -5,6 +5,7 @@ export type FrontTicketInput = { subject: string; comment: Comment; custom_fields?: CustomFields; + tags?: string[]; }; type Comment = { diff --git a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts index 8540833a4..c6a45d2b7 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts @@ -70,7 +70,6 @@ export class HubspotTicketMapper implements ITicketMapper { due_date: new Date(ticket.properties.createdate), type: ticket.properties.hs_pipeline, parent_ticket: '', // Define how you determine the parent ticket - tags: '', // Define how you map or store tags completed_at: new Date(ticket.properties.hs_lastmodifieddate), priority: ticket.properties.hs_ticket_priority, assigned_to: [ticket.properties.hubspot_owner_id], // Define how you determine assigned users diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index b34881e84..dd1007331 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -173,7 +173,7 @@ export class TicketService { due_date: target_ticket.due_date || '', ticket_type: target_ticket.type || '', parent_ticket: target_ticket.parent_ticket || '', - tags: target_ticket.tags || '', + tags: target_ticket.tags || [], completed_at: target_ticket.completed_at || '', priority: target_ticket.priority || '', assigned_to: target_ticket.assigned_to || [], @@ -192,7 +192,7 @@ export class TicketService { due_date: target_ticket.due_date || '', ticket_type: target_ticket.type || '', parent_ticket: target_ticket.parent_ticket || '', - tags: target_ticket.tags || '', + tags: target_ticket.tags || [], completed_at: target_ticket.completed_at || '', priority: target_ticket.priority || '', assigned_to: target_ticket.assigned_to || [], @@ -347,7 +347,7 @@ export class TicketService { due_date: ticket.due_date || null, type: ticket.ticket_type || '', parent_ticket: ticket.parent_ticket || '', - tags: ticket.tags || '', + tags: ticket.tags || [], completed_at: ticket.completed_at || null, priority: ticket.priority || '', assigned_to: ticket.assigned_to || [], @@ -431,7 +431,7 @@ export class TicketService { due_date: ticket.due_date || null, type: ticket.ticket_type || '', parent_ticket: ticket.parent_ticket || '', - tags: ticket.tags || '', + tags: ticket.tags || [], completed_at: ticket.completed_at || null, priority: ticket.priority || '', assigned_to: ticket.assigned_to || [], diff --git a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts index 6a4f9f1c9..0d019b397 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts @@ -31,7 +31,7 @@ export class ZendeskTicketMapper implements ITicketMapper { | 'solved' | 'closed', subject: source.name, - tags: [source.tags], + tags: source.tags, type: source.type as 'problem' | 'incident' | 'question' | 'task', comment: { body: source.comment.body, @@ -98,7 +98,7 @@ export class ZendeskTicketMapper implements ITicketMapper { due_date: ticket.due_at ? new Date(ticket.due_at) : undefined, type: ticket.type, parent_ticket: undefined, // If available, add logic to map parent ticket - tags: JSON.stringify(ticket.tags), //TODO + tags: ticket.tags, completed_at: new Date(ticket.updated_at), priority: ticket.priority, assigned_to: [String(ticket.assignee_id)], diff --git a/packages/api/src/ticketing/ticket/sync/sync.service.ts b/packages/api/src/ticketing/ticket/sync/sync.service.ts index 09d123d2b..aac9b2c72 100644 --- a/packages/api/src/ticketing/ticket/sync/sync.service.ts +++ b/packages/api/src/ticketing/ticket/sync/sync.service.ts @@ -206,7 +206,7 @@ export class SyncService implements OnModuleInit { due_date: ticket.due_date || '', ticket_type: ticket.type || '', parent_ticket: ticket.parent_ticket || '', - tags: ticket.tags || '', + tags: ticket.tags || [], completed_at: ticket.completed_at || '', priority: ticket.priority || '', assigned_to: ticket.assigned_to || [], @@ -226,7 +226,7 @@ export class SyncService implements OnModuleInit { due_date: ticket.due_date || '', ticket_type: ticket.type || '', parent_ticket: ticket.parent_ticket || '', - tags: ticket.tags || '', + tags: ticket.tags || [], completed_at: ticket.completed_at || '', priority: ticket.priority || '', assigned_to: ticket.assigned_to || [], diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index 028fe4feb..f3cfa2b85 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -8,7 +8,7 @@ export class UnifiedTicketInput { due_date?: Date; type?: string; parent_ticket?: string; - tags?: string; // TODO: create a real Tag object here + tags?: string[]; // TODO: create a real Tag object here completed_at?: Date; priority?: string; assigned_to?: string[]; //uuid of Users objects ? From 6d355a06d80fb99521e121833c094242f587d52c Mon Sep 17 00:00:00 2001 From: nael Date: Sun, 7 Jan 2024 16:51:59 +0100 Subject: [PATCH 23/25] :construction: All internal objects should be mapped --- .../comment/services/comment.service.ts | 35 ++++++--- .../ticketing/comment/services/front/index.ts | 20 ++++- .../comment/services/front/mappers.ts | 34 +++++++-- .../comment/services/zendesk/index.ts | 75 +++++++++++++------ .../comment/services/zendesk/mappers.ts | 33 ++++++-- .../ticketing/comment/sync/sync.service.ts | 36 +++++---- .../api/src/ticketing/comment/utils/index.ts | 50 ++++++++++++- .../ticketing/tag/services/zendesk/mappers.ts | 13 ++-- .../ticketing/ticket/services/front/index.ts | 20 +++-- .../ticket/services/front/mappers.ts | 3 +- .../ticketing/ticket/types/model.unified.ts | 2 +- .../api/src/ticketing/ticket/utils/index.ts | 4 +- 12 files changed, 239 insertions(+), 86 deletions(-) diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index 7ae362da5..9377e9026 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -177,27 +177,37 @@ export class CommentService { }); let unique_ticketing_comment_id: string; + const opts = + target_comment.creator_type === 'contact' + ? { + id_tcg_contact: unifiedCommentData.contact_id, + } + : { + id_tcg_user: unifiedCommentData.user_id, + }; if (existingComment) { // Update the existing comment + const data = { + body: target_comment.body, + html_body: target_comment.html_body, + is_private: target_comment.is_private, + creator_type: target_comment.creator_type, + id_tcg_ticket: unifiedCommentData.ticket_id, + modified_at: new Date(), + ...opts, + }; const res = await this.prisma.tcg_comments.update({ where: { id_tcg_comment: existingComment.id_tcg_comment, }, - data: { - body: target_comment.body, - html_body: target_comment.html_body, - is_private: target_comment.is_private, - creator_type: target_comment.creator_type, - id_tcg_ticket: target_comment.ticket_id, - modified_at: new Date(), - }, + data: data, }); unique_ticketing_comment_id = res.id_tcg_comment; } else { // Create a new comment this.logger.log('comment not exists'); - const data = { + let data = { id_tcg_comment: uuidv4(), body: target_comment.body, html_body: target_comment.html_body, @@ -205,13 +215,14 @@ export class CommentService { created_at: new Date(), modified_at: new Date(), creator_type: target_comment.creator_type, - id_tcg_ticket: target_comment.ticket_id, + id_tcg_ticket: unifiedCommentData.ticket_id, id_linked_user: linkedUserId, remote_id: originId, remote_platform: integrationId, - //TODO; id_tcg_contact String? @db.Uuid - //TODO; id_tcg_user String? @db.Uuid }; + + data = { ...data, ...opts }; + const res = await this.prisma.tcg_comments.create({ data: data, }); diff --git a/packages/api/src/ticketing/comment/services/front/index.ts b/packages/api/src/ticketing/comment/services/front/index.ts index 89aeb0b1e..1e0dfb294 100644 --- a/packages/api/src/ticketing/comment/services/front/index.ts +++ b/packages/api/src/ticketing/comment/services/front/index.ts @@ -9,7 +9,7 @@ import { ICommentService } from '@ticketing/comment/types'; import { TicketingObject } from '@ticketing/@utils/@types'; import { FrontCommentInput, FrontCommentOutput } from './types'; import { ServiceRegistry } from '../registry.service'; -import { fetchFileStreamFromURL } from '@ticketing/comment/utils'; +import { Utils } from '@ticketing/comment/utils'; @Injectable() export class FrontService implements ICommentService { @@ -24,6 +24,8 @@ export class FrontService implements ICommentService { ); this.registry.registerService('front', this); } + private readonly utils = new Utils(); + async addComment( commentData: FrontCommentInput, linkedUserId: string, @@ -38,6 +40,16 @@ export class FrontService implements ICommentService { }, }); + //retreive the right user for author + const user = await this.prisma.tcg_users.findUnique({ + where: { + id_tcg_user: commentData.author_id, + }, + select: { remote_id: true }, + }); + if (!user) + throw new Error('author_id is invalid, it must be a valid User'); + let uploads = []; const uuids = commentData.attachments; if (uuids && uuids.length > 0) { @@ -52,7 +64,9 @@ export class FrontService implements ICommentService { //TODO: construct the right binary attachment //get the AWS s3 right file //TODO: check how to send a stream of a url - const fileStream = await fetchFileStreamFromURL(res.file_url); + const fileStream = await this.utils.fetchFileStreamFromURL( + res.file_url, + ); uploads = [...uploads, fileStream]; } @@ -62,6 +76,7 @@ export class FrontService implements ICommentService { if (uploads.length > 0) { const dataBody = { ...commentData, + author_id: user.remote_id, attachments: uploads, }; const formData = new FormData(); @@ -91,6 +106,7 @@ export class FrontService implements ICommentService { } else { const dataBody = { ...commentData, + author_id: user.remote_id, }; resp = await axios.post( `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts index 4605b58d8..96b043209 100644 --- a/packages/api/src/ticketing/comment/services/front/mappers.ts +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -8,8 +8,11 @@ import { UnifiedAttachmentOutput } from '@ticketing/attachment/types/model.unifi 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 FrontCommentMapper implements ICommentMapper { + private readonly utils = new Utils(); + async desunify( source: UnifiedCommentInput, customFieldMappings?: { @@ -19,7 +22,7 @@ export class FrontCommentMapper implements ICommentMapper { ): Promise { const result: FrontCommentInput = { body: source.body, - author_id: source.user_id || source.contact_id, //TODO: make sure either one is passed + author_id: source.user_id || source.contact_id, // for Front it must be a User attachments: source.attachments, }; return result; @@ -58,14 +61,31 @@ export class FrontCommentMapper implements ICommentMapper { customFieldMappings: [], })) as UnifiedAttachmentOutput[]; - return { + const user_id = await this.utils.getUserUuidFromRemoteId( + String(comment.id), + 'zendesk_tcg', + ); + let creator_type: string; + let opts; + if (user_id) { + creator_type = 'user'; + opts = { user_id: user_id }; + } else { + const contact_id = await this.utils.getContactUuidFromRemoteId( + String(comment.id), + 'zendesk_tcg', + ); + creator_type = 'contact'; + opts = { user_id: contact_id }; + } + + const res = { body: comment.body, - html_body: '', - creator_type: comment.author ? 'contact' : null, - ticket_id: '', // TODO: Need to be determined from related data - contact_id: '', // TODO: Need to be determined from related data - user_id: '', //TODO + creator_type: creator_type, //it must be user attachments: unifiedObject, + ...opts, }; + + return res; } } diff --git a/packages/api/src/ticketing/comment/services/zendesk/index.ts b/packages/api/src/ticketing/comment/services/zendesk/index.ts index d32f1e327..8a096a3f1 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/index.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/index.ts @@ -39,35 +39,66 @@ export class ZendeskService implements ICommentService { }, }); - // We must fetch tokens from zendesk with the commentData.uploads array of Attachment uuids - const uuids = commentData.uploads; - let uploads = []; - uuids.map(async (uuid) => { - const res = await this.prisma.tcg_attachments.findUnique({ + //first we retrieve the right author_id (it must be either a User or a Cntact) + const author_id = commentData.author_id; //uuid of either a User or a Contact + let author_data; + const res_user = await this.prisma.tcg_users.findUnique({ + where: { + id_tcg_user: String(author_id), + }, + select: { remote_id: true }, + }); + author_data = res_user; //it might be undefined but if it is i insert the right data below + + if (!res_user) { + //try to see if there is a contact for this uuid + const res_contact = await this.prisma.tcg_contacts.findUnique({ where: { - id_tcg_attachment: uuid, + id_tcg_contact: String(author_id), }, + select: { remote_id: true }, }); - if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); + if (!res_contact) { + throw new Error( + 'author_id is invalid, it must be a valid User or Contact', + ); + } + author_data = res_contact; + } - //TODO:; fetch the right file from AWS s3 - const s3File = ''; - const url = `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/uploads.json?filename=${ - res.file_name - }`; + // We must fetch tokens from zendesk with the commentData.uploads array of Attachment uuids + const uuids = commentData.uploads; + let uploads = []; + const uploadTokens = await Promise.all( + uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) + throw new Error(`tcg_attachment not found for uuid ${uuid}`); - const resp = await axios.get(url, { - headers: { - 'Content-Type': 'image/png', //TODO: get the right content-type given a file name extension - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }); - uploads = [...uploads, resp.data.upload.token]; - }); + //TODO:; fetch the right file from AWS s3 + const s3File = ''; + const url = `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/uploads.json?filename=${ + res.file_name + }`; + + const resp = await axios.get(url, { + headers: { + 'Content-Type': 'image/png', //TODO: get the right content-type given a file name extension + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + uploads = [...uploads, resp.data.upload.token]; + }), + ); const finalData = { ...commentData, + author_id: author_data.remote_id, uploads: uploads, }; const dataBody = { diff --git a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts index ec39f1dda..990258a31 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts @@ -8,8 +8,10 @@ import { UnifiedAttachmentOutput } from '@ticketing/attachment/types/model.unifi import { unify } from '@@core/utils/unification/unify'; import { TicketingObject } from '@ticketing/@utils/@types'; import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; +import { Utils } from '@ticketing/comment/utils'; export class ZendeskCommentMapper implements ICommentMapper { + private readonly utils = new Utils(); async desunify( source: UnifiedCommentInput, customFieldMappings?: { @@ -23,9 +25,7 @@ export class ZendeskCommentMapper implements ICommentMapper { public: !source.is_private, author_id: source.user_id ? parseInt(source.user_id) - : source.contact_id - ? parseInt(source.contact_id) - : undefined, //TODO: make sure either one is passed + : parseInt(source.contact_id), // either one must be passed type: 'Comment', uploads: source.attachments, //we let the array of uuids on purpose (it will be modified in the given service on the fly!) }; @@ -63,16 +63,33 @@ export class ZendeskCommentMapper implements ICommentMapper { providerName: 'front', customFieldMappings: [], })) as UnifiedAttachmentOutput[]; + const user_id = await this.utils.getUserUuidFromRemoteId( + String(comment.id), + 'zendesk_tcg', + ); + let creator_type: string; + let opts; + if (user_id) { + creator_type = 'user'; + opts = { user_id: user_id }; + } else { + const contact_id = await this.utils.getContactUuidFromRemoteId( + String(comment.id), + 'zendesk_tcg', + ); + creator_type = 'contact'; + opts = { user_id: contact_id }; + } - return { + const res = { body: comment.body || '', html_body: comment.html_body || '', is_private: !comment.public, - creator_type: 'contact', - ticket_id: '', //TODO - contact_id: '', // TODO: - user_id: '', //TODO + creator_type: creator_type, attachments: unifiedObject, + ...opts, }; + + return res; } } diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index d501c6dd2..80738bddf 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -203,28 +203,37 @@ export class SyncService implements OnModuleInit { }); let unique_ticketing_comment_id: string; - + const opts = + comment.creator_type === 'contact' + ? { + id_tcg_contact: comment.contact_id, + } + : { + id_tcg_user: comment.user_id, + }; if (existingComment) { // Update the existing comment + const data = { + body: comment.body, + html_body: comment.html_body, + is_private: comment.is_private, + creator_type: comment.creator_type, + id_tcg_ticket: id_ticket, + modified_at: new Date(), + ...opts, + }; const res = await this.prisma.tcg_comments.update({ where: { id_tcg_comment: existingComment.id_tcg_comment, }, - data: { - body: comment.body, - html_body: comment.html_body, - is_private: comment.is_private, - creator_type: comment.creator_type, - id_tcg_ticket: id_ticket, - modified_at: new Date(), - }, + data: data, }); unique_ticketing_comment_id = res.id_tcg_comment; comments_results = [...comments_results, res]; } else { // Create a new comment this.logger.log('comment not exists'); - const data = { + let data = { id_tcg_comment: uuidv4(), body: comment.body, html_body: comment.html_body, @@ -236,9 +245,10 @@ export class SyncService implements OnModuleInit { id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, - //TODO; id_tcg_contact String? @db.Uuid - //TODO; id_tcg_user String? @db.Uuid }; + + data = { ...data, ...opts }; + const res = await this.prisma.tcg_comments.create({ data: data, }); @@ -292,8 +302,6 @@ export class SyncService implements OnModuleInit { id_tcg_ticket: id_ticket, id_linked_user: linkedUserId, remote_platform: originSource, - //TODO; id_tcg_contact String? @db.Uuid - //TODO; id_tcg_user String? @db.Uuid }; const res = await this.prisma.tcg_attachments.create({ data: data, diff --git a/packages/api/src/ticketing/comment/utils/index.ts b/packages/api/src/ticketing/comment/utils/index.ts index 2e71ef4f3..90172977e 100644 --- a/packages/api/src/ticketing/comment/utils/index.ts +++ b/packages/api/src/ticketing/comment/utils/index.ts @@ -1,4 +1,48 @@ -export async function fetchFileStreamFromURL(file_url: string) { - //TODO; - return; +import { PrismaClient } from '@prisma/client'; + +export class Utils { + private readonly prisma: PrismaClient; + constructor() { + this.prisma = new PrismaClient(); + } + + async fetchFileStreamFromURL(file_url: string) { + //TODO; + return; + } + + async getUserUuidFromRemoteId(remote_id: string, remote_platform: string) { + try { + const res = await this.prisma.tcg_users.findFirst({ + where: { + remote_id: remote_id, + remote_platform: remote_platform, + }, + }); + if (!res) + throw new Error( + `tcg_user not found for remote_id ${remote_id} and integration ${remote_platform}`, + ); + return res.id_tcg_user; + } catch (error) { + throw new Error(error); + } + } + async getContactUuidFromRemoteId(remote_id: string, remote_platform: string) { + try { + const res = await this.prisma.tcg_contacts.findFirst({ + where: { + remote_id: remote_id, + remote_platform: remote_platform, + }, + }); + if (!res) + throw new Error( + `tcg_contact not found for remote_id ${remote_id} and integration ${remote_platform}`, + ); + return res.id_tcg_contact; + } catch (error) { + throw new Error(error); + } + } } diff --git a/packages/api/src/ticketing/tag/services/zendesk/mappers.ts b/packages/api/src/ticketing/tag/services/zendesk/mappers.ts index 420b1c8a6..2e2639479 100644 --- a/packages/api/src/ticketing/tag/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/tag/services/zendesk/mappers.ts @@ -17,29 +17,28 @@ export class ZendeskTagMapper implements ITagMapper { } unify( - source: ZendeskTagOutput | ZendeskTagOutput[], + source: ZendeskTagOutput, customFieldMappings?: { slug: string; remote_id: string; }[], ): UnifiedTagOutput | UnifiedTagOutput[] { - if (!Array.isArray(source)) { - return this.mapSingleTagToUnified(source, customFieldMappings); - } - return source.map((tag) => + const tags = source.tags; + + return tags.map((tag) => this.mapSingleTagToUnified(tag, customFieldMappings), ); } private mapSingleTagToUnified( - tag: ZendeskTagOutput, + tag: string, customFieldMappings?: { slug: string; remote_id: string; }[], ): UnifiedTagOutput { const unifiedTag: UnifiedTagOutput = { - name: tag.tags[0], //TODO + name: tag, }; return unifiedTag; diff --git a/packages/api/src/ticketing/ticket/services/front/index.ts b/packages/api/src/ticketing/ticket/services/front/index.ts index 6dfd856ee..15851c4ac 100644 --- a/packages/api/src/ticketing/ticket/services/front/index.ts +++ b/packages/api/src/ticketing/ticket/services/front/index.ts @@ -9,7 +9,7 @@ import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; import { ServiceRegistry } from '../registry.service'; import { FrontTicketInput, FrontTicketOutput } from './types'; -import { fetchFileStreamFromURL } from '@ticketing/comment/utils'; +import { Utils } from '@ticketing/comment/utils'; @Injectable() export class FrontService implements ITicketService { @@ -24,6 +24,7 @@ export class FrontService implements ITicketService { ); this.registry.registerService('front', this); } + private readonly utils = new Utils(); async addTicket( ticketData: FrontTicketInput, @@ -38,7 +39,7 @@ export class FrontService implements ITicketService { }); //We deconstruct as tags must be added separately - const { tags, ...restOfTicketData } = ticketData; + const { tags, custom_fields, ...restOfTicketData } = ticketData; let uploads = []; const uuids = restOfTicketData.comment.attachments; @@ -54,7 +55,9 @@ export class FrontService implements ITicketService { //TODO: construct the right binary attachment //get the AWS s3 right file //TODO: check how to send a stream of a url - const fileStream = await fetchFileStreamFromURL(res.file_url); + const fileStream = await this.utils.fetchFileStreamFromURL( + res.file_url, + ); uploads = [...uploads, fileStream]; } @@ -116,13 +119,14 @@ export class FrontService implements ITicketService { ); } - //now we can add tags to the conversation we just created - if (tags && tags.length > 0) { + //now we can add tags and/or custom fields to the conversation we just created + if ((tags && tags.length > 0) || custom_fields) { const data = { tag_ids: tags, + custom_fields: custom_fields, }; - const tag_resp = await axios.post( - `https://api2.frontapp.com/conversations/${resp.data.id}/tags`, + const tag_resp = await axios.patch( + `https://api2.frontapp.com/conversations/${resp.data.id}`, JSON.stringify(data), { headers: { @@ -135,6 +139,8 @@ export class FrontService implements ITicketService { ); } + //now we can insert + return { data: resp.data, message: 'Front ticket created', diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index 41838feec..6fc25278c 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -31,7 +31,6 @@ export class FrontTicketMapper implements ITicketMapper { tags: source.tags, }; - //TODO: custom fields => https://dev.frontapp.com/reference/patch_conversations-conversation-id if (customFieldMappings && source.field_mappings) { for (const fieldMapping of source.field_mappings) { for (const key in fieldMapping) { @@ -39,7 +38,7 @@ export class FrontTicketMapper implements ITicketMapper { (mapping) => mapping.slug === key, ); if (mapping) { - result[mapping.remote_id] = fieldMapping[key]; + result['custom_fields'][mapping.remote_id] = fieldMapping[key]; } } } diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index f3cfa2b85..f153dae60 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -8,7 +8,7 @@ export class UnifiedTicketInput { due_date?: Date; type?: string; parent_ticket?: string; - tags?: string[]; // TODO: create a real Tag object here + tags?: string[]; // tags names completed_at?: Date; priority?: string; assigned_to?: string[]; //uuid of Users objects ? diff --git a/packages/api/src/ticketing/ticket/utils/index.ts b/packages/api/src/ticketing/ticket/utils/index.ts index 75dfcc49b..1b873df92 100644 --- a/packages/api/src/ticketing/ticket/utils/index.ts +++ b/packages/api/src/ticketing/ticket/utils/index.ts @@ -15,6 +15,8 @@ export class Utils { }); if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); return res.email_address; - } catch (error) {} + } catch (error) { + throw new Error(error); + } } } From cde29622853073da40e575904b0a76500410d08a Mon Sep 17 00:00:00 2001 From: nael Date: Sun, 7 Jan 2024 19:57:20 +0100 Subject: [PATCH 24/25] :construction: Added fixes to the whole app --- apps/frontend-snippet/.env.example | 3 +- .../public/assets/ticketing/front.png | Bin 0 -> 22835 bytes apps/frontend-snippet/src/helpers/utils.ts | 14 + apps/frontend-snippet/src/hooks/useOAuth.ts | 4 +- packages/api/.env.example | 2 + packages/api/scripts/seed.webapp.ts | 10 +- .../ticketing/account/sync/sync.service.ts | 2 +- .../ticketing/comment/sync/sync.service.ts | 2 +- .../ticketing/contact/sync/sync.service.ts | 2 +- .../src/ticketing/tag/sync/sync.service.ts | 2 +- .../src/ticketing/team/sync/sync.service.ts | 2 +- .../src/ticketing/ticket/sync/sync.service.ts | 2 +- .../src/ticketing/user/sync/sync.service.ts | 2 +- packages/api/swagger/swagger-spec.json | 641 +++++++++++++++++- 14 files changed, 640 insertions(+), 48 deletions(-) create mode 100644 apps/frontend-snippet/public/assets/ticketing/front.png diff --git a/apps/frontend-snippet/.env.example b/apps/frontend-snippet/.env.example index c2a0a9011..cf785b53a 100644 --- a/apps/frontend-snippet/.env.example +++ b/apps/frontend-snippet/.env.example @@ -1 +1,2 @@ -VITE_BACKEND_DOMAIN= \ No newline at end of file +VITE_BACKEND_DOMAIN= +VITE_ML_FRONTEND_URL= \ No newline at end of file diff --git a/apps/frontend-snippet/public/assets/ticketing/front.png b/apps/frontend-snippet/public/assets/ticketing/front.png new file mode 100644 index 0000000000000000000000000000000000000000..0c13ebd2668b1dc6d1768d3de5816ef9fd1d07f3 GIT binary patch literal 22835 zcmeIaXIN8N_da}(QB+Wz(Ge9T<4BpY01ATC;5Y*!N>q%fl;D6M(v&JS_$cr=iU^2E z3DL)b1R*K{QiCvxp-K%sMgox@5<(IPIsbM1mG{&8<^S!y-cc^f$vL~Mz1F?fy7$`W z=5c$QHLJI*Mi69;?cv`~BFJ~}>vzb?74U<+ZP#`9vEolVo8J)>`~R>iHx)t7G}!)r z@YL15sip9H9%BbJuHub<4y4lxiz~`2$|LezHH*)g^_@HX+Yi6(I=piG-4HkHP1oPK ze;A(CFR7Qy!lk2E&D=LFZk<%n(~p0r_~5JPEf!BWP|}P82HwtkUN~jZS|zR`y0FArc_~M=H?M+0>hB&+aXxag z^z}Nq`YqD)+iT#OO5)QdQt5cPW%iQ{k2kcPUT_Vmx%+$~!jgG0rF=Ztbn5*`A}w$k zc3tIGd1304RLufslO-Jdy~#4s;}LSkRPmwrQTWsC>zmXkL}J%Sd11ch@CodZ8hvk_ zj6xdCUn?Vjqplp6_^q9IYa)@ddtmj;&?P+Fw82UNz9Y+zW8bY5Hef%FBg?TL|9te1 zG%!8*M>qa42T+55z~LXL#NgonC*I-W=4D8{ZN*?q)1>n=FHZwA<8+~#?#Qe9R^BU} zR^Ck=J#PERm~&1Jv#H6~)6=UXFLCqBR|dI+k*R=~kj&<1?~oOl%aGV>r&uAQqoYBy zg5Vr;@^;eI&zz{)YYBolJ^VdA3(4hb$>+LoNj|C7osdIPSq#dcWHrj7%%(!&OJ~Zy z-fEen=nHQswKRGne*;oe;{dV5y76*X%m1M7l{6Z1Ef?dWCR?W)6Y#UWQPaw#mSKYH z$5fu)s9^5)yJg7Ng9=E^wXW&lK&c>DBTCX1HJ3_lR>I3}$~e?0bLwKPqO{MQuyPSW zvK}JHXREPb(=v05)J4WzS|xf2*BXEhDGBRw(o^uTPubP6*9yy!o}U2`7Yhp~np#VR zX>`j>@zSh~aDpLJ3ZGHJr|97&mpyr-Yt|t(Yay<988BYn^0cu-`ZagDkuD#N6u_O; zQ42w&k0TjpuPY)i>%h@Y=9L#Z=!Ab^DoY2&X&xEfC9Scs^!^!-#9I6ZZeVqVQ0j9IP*E9)jK|L!pnGvLzK#K} zF9vRSiroM$D68p>7|Z(?g=Bdr`Qsvf?q*Z7vK(fF$*S-{2Bi@vYqJrw#t3-Aeuns( zbNS>cG7`Iljld?vlmcQ_rkM`tEn3AIS9GQe5kBV7PW2e!hmsVY0hFp&Q6+wF5NJGtVl27$Q zF?o{B4U0jO3=GV?pwqPnV6C$DSy-f!782Rb=uAE8XDRJn>=^5Wak(MCAjsE#cY=8a zRgyt!NkThYd5>(W{jDIb{x@`+PlIm5B|cI%DkbpPOSN>KEY1#lvId^)Iqt$Ln*nK% z7j4jej5&2i)>+n~BBG)Rold}`R7um4l!^`|@k)n;2TLW2$R;FVE$6Fl}IrvQKPDJ!q8tltc;Hw5$chA7`K2QZJvZmW?n( z1mTcsjOJ@RQgit-qyvqhWz^_tWoa^FiM%_WJcdKu&cGNCL&!aYwmS+O(di_f#SD6v zuZrke@eBuOvHpGWy488Chbd#S9)9W8OvB_rwt@ok%MBQ@J}4GXNI<(T8A$J<>_fuSk9Nz@ZZ?_am z^+rgt3Q`jbM{ak67O)w|Hg*_D88P#Y0uCYS19tU@0;10aiDPRglhNg&fN^V9B36$; zu7{>y>9p{4Vn*?!?-CfE`p_SUVAG$J@)kPE!hf8p6kpGq=mVi`I10`Dtr5eWkTOx8 zqfjEfwL%f3zZ#@ge0dHP3vI{?w$~Mq*k!Po7BX0@0C^p`<$YrZ+y`k&jXo$DLs~(8 zpc*~dssd_Yiw$6G76#xIGwV_o9F7h5?(fh~FgDyF^brPpO>prg*0~2@yKp9k<5G5+ zS&Slx^CKAPKHOp_PY{WdI4A-ltj0!jk`)re!86Q~Ea#uR14Ud8eXo-?)R7v=3sd4} zg+4II|GHD4M3%jZTBz3Byb?jW!2(rof^9M+6O2@8Xg}6(JWOUK;E-O7^ikzZ(%^zB z5<7$q`->`mZZ=A~i?n|og3JNjY+440fsU1u@l&Do@aU&h>{0W%E*pU4B?D}9JpB>xK=oeK6|GS(}TE8+kA35@qHG*%Nm z_AWtKrf2Tze5nmr|{5mFsw)5F#x0kKry~O7dze=GK~rx z_gNVD{O`3(t))S1H4S}C5`Vx#J?x{{V>hWlOVOa&D$k|T&}u=K&6@wcprhy9G^`Em zz(WEye-StC7PvBZ7~a=gFc>Qep8RWi?w|sK#LB@4R&;_IWn>T$kSh*`A6p(Q4fOQn z{XacqWRx{Ex2nTBE`W2{bUa8X^e#190kBy8?fIw*ioFdqXGMlZxK1;Ik<1a`@BizZ z2@5(t*U(aF6CKtm$)=l+rJZ(pn(-uKXQF1}LPK)mctc&PCPkC7bL>e=R7z-C)6*8; zVt#nlrKKNX%7=b~z9;Dm4FT%5AI5hM=j}VK;}EhV!HlRqVXZx}^KnSXQ{-^_b2Uq& zqPx9Wc_*JvY^aYI>DawNK~!6>8ARNdYO1DAaegswej-HR@wg?J$U9m5(ziO2^dn%p zK4AJwy>lV^PB>;f&v)+d=AIe+m~6jeI3#Vhyy0TXhc9hQH2c?*#W=P|m_J_S@8Tj| zXg{kswTnbgZOUyKp|y-yQ|j~WHN1lbmy0Mm`Sv-!!TXBdoJThO1Wv(C+{HYc!hHXd zOzUWBv)|!x>pUYl&Z>GtZIR?kh-60IN326>cFQ8t6dg;*BT{wSb9wvD>JZJ!#v1HC z!dRKKR2y}QdC!B^>&>AWKEZ*HXb!u+Lk@g~)u}1G*sXjGJ(HW9O7XnYow${jf4h5E zQZ0Qgk13BsX^UkQu6D(5RTIW{CYUB>d$Ynlt@ovKnbtb0#(iZB6RKWupETle>KJWa z35iw2R(u%c@PUC$_%iQ}=Ti{eKy(AKy(axU1=ITR3GH zTfIcj5I;lNAwG$UPX;T1JY%{zU0zY!~(p+N{e5MR4|*8}@vXoF38_6DrZ#M|5> z$E$yk7^};R%J?LD)s2U&pciY8fv>nXqLls(e3{aJy-QV{H~RsICxe@emqa=%%GU zrdSuw7zX{PIFNZueuq<)+JuO~e;y||s)6TO#h!mttHre3}DEwt_OZLwYOJd_c{e0YmRv1`5*feHu;1E*$=V&B{3}5e` z+z3fa)?~4oosA8Wd3!c0;5Jd3(=&yCAxP*w;LsqTaMMvsl8{*b9y=2aCw84T-h7YG z2>hXz`w;r3mYy8(7;I|vtQR(wwM7Mc7|l4skgvm#kC(r9SfWoD-cOPkPo%NLI@f=c zmj>rEOecmiUwWOmTabVjCzYk}vu^!S)96(%ye(~$+4>mGs8g8Nn@da}@ZJbQyGOIP zPR$-S3_4ViQ5TZtNgL5;EOHp3FM$s@&2*gxHzk< zq@yyhoE*PbMvw5z*T}lI76$(Ma%p_2Jz;cOnk>tmZIpWWwDf|7K`?_HdT~}K7i`?c ze4JtW&|h0-oHF9X)v85f;?U{(+s_(`My-Q{QM+cgFtWpvj`1ps6p%9sFxdDLl-n#~ zi>~VgKE>Be7REcmA)|F<@r3CKW1&lWuHYVYR{QtU0d_XQW))Ji6F_a#J|bmnfqGd; zb;gbmcj-CQMB;VTjHTNn<9Jv zi^jrxWv2_jmlyFk3|{q2jATY5N!_$S7I!N_IGFw{@P)r-F;yAacmTqo_=A+&T4fii zix0H1`}(D6>*N(U_Abqt2+52=C%v@5!;FJA)H6?o4>x(Vgr1XT+*B@O^x97yMPk2z zlXv^n#pIMXObH9#JIKR~WOS91syeixgROc-LRWQ@(8Kg(dP|qI!>1=N7W$D=sTq}b z@mn%N(jlfS1J}3WEG2u=P|#VHyq`nPFI_NO$zdeFTa9+DO+bHie;LQ8j=U-e45~;& zIkm}^F%drx+bbf!K#q*8IORSYftrmvWt>?%S^-=LC$D z+wW*rTrba8fZ-p%f#xt~Hp}TR4DRpL%i7kOT%JtLgw#39?_G)N`eFCI$7}SDqswZL z&GghDVL|q^Vf&VLKI08~eUsd{dWcM}L)&=;r^2TPF2+c{BoFLopNrY^Vn))oBacgV zTM40rn-wUQe^$T<_e(PR)XKz{!|1!tCp~TAGYoSI${Q?#c1St5Yy@wl-)q$n2d*yV z85%SCT9 zIeHIKFB3geKlv*JwE%;$ivB+nTbfG)|I^{Tc5rcNT|<3VK{ z^(`)P7;P5uqP`{j(9w_Nc@tS!IzsSH;wI>6v`~b5w5-xQSUDfpuqcM>YK~(L!xg$xt{V+%{?uXC56=MmP+Z+NITJ?7{ z_Wn;B9kkS}Q74n)5k5)uqddKPN>Aox1Dcq{r*2b56Mt-FbZ{79Cz2LL$Tnc3+I%X!*yFD=-k3KEhmYFB`SMRGR^nG2~Gl%(*8P-;ldp!K2f56e2 zPv2&FqKg?aGtEge?%LaWm@@D)9=34uQ=q7TB-tBKmt9 z8@OJSkkl`SX+u7k$!C)!q=f?+)r9L8d6Cm@&X{jG$+O8*TfmfcQF9NiWWe(agHPNl8FAxI&R z#i#c6rCBC}-qR#W5}$G6A>ju4lyzmMsi*cy>~h&fOJQCquS+kwTKtRKSIjE$oy_EN zEQUDOTg9j5$Ds?c03s(Ot>9&=>OC>L7Cg08AQ>6td{{?au5W6Zg3A5qt$FL~D+vmR zupn{K&5&6pJ^i7mh(rE$gKj~^LP>;@rHmE1K|V1Rhq{LC>AA_LZUsZ1n>{@EQA{$= zHXBU{vy9~p1vw7MGSjnUs+*3$D)qP%DFdDUBNqpIX8F_@!XmDg&;FQpyt*GW;qs7| zWM+RM^W9q1E;8JwB4ef^_!pZA&Y~BWb0}4sJQL|uXcjV{{-gdqK+2ct-xgfGubHw2 z4FOBhOAsrpM9=Pb-GwI&jKZ(Ga)N4V(RL%o70{bX^W^Szl;xi;g5$_{O<^$v?}u_@ zA3X)|urSQwc8BiT!PKrL7jBVPBt-;h$;1&kBkRiO97es*SshsbSa2x?w5UFMuzuP> zLYgTStI6py6M11{YWRnNub-S(UtZkfC2%Bvp7N9{#bXX({xDI;7(nT5VfS~U#5!{E z{PU>6T`$(UL!_!DqpOb_Z^rQD(oeAZOMOQHpJLo|GiJ&R)7Deop*(K#AqVL$HaZyo zp~F9H3a{UZ&01}odo?$?gECrDi~e=@9DPj#v%8io9=?GBrSO*uYDf0Zue=(%o1UdH zX{TU)t~rXQPRf{(mju}7_s+R4SJ{v0-?cVP<5H*1$3o3z0U;%zuoK~N=&?BT>1^in z)Kb@+CWld*6$0LC8m)3JuC*me{>dzm%fDy5)9owfW7CNtfzcHiKI4~0{J5dN$kMNJ z$wj3Lb~n(j#o|HGf^6f`tnmJ)%&p@pLwUm1tK(%dGCD5il{mE=5aE9L7f2`6fdc8v zz$gD_XCuHX>&i&dDn5`-ks5NfHz!B3JU;}@O!oX0ZTYojFv?xtLRyknel`iq-7Zs% z!*E3RlD0{QzvpWk$xQUA$hWoVc>PM$2fsED4EE{^f3BGxVWdwXV;#S{$e4?heN`O| zp9VVF(Mfc9|8Nq~G zUd?y7`#aWqYw(MZZ^L<)BzGH{%S`tS_R#0)B0azyAd@M{nqH>Ow>`F7?SFlfPntO| zX&nOp^|BT{__mA0;E=19`U?0AkA1`;LyKgmz5+$5@C=bS9o|qTOJyvj3#aJO;UBDR zs%#bB136IR|2%a)#rjUzmb~#xLJoc+0SzIqafTccwU0+>Ue5y{7Gl&_7oH=(cZXns zzH&(YJE6!m^0gUaCM|=vw8>@_e;mS z7Z(-WY7$w@M1TkNcyx`uv~_`l-+Z6Xc;`c)|NPKKLelx9qptM1IRTx#Dj~fZ98^Ik zaNK=ZGX(3+e{Dlma}m39COhfr}m2nIqnA@q#$Y{Dluv7D_Oju^o`9BdQ`RRLiHf4O zEPIf>skMOXe#j;o`SLIJaa>^G;H-pJ$o$d9Oq_SMo_Ai4NVjk9CtLKSAO` zz8}fxj$;q@_>Y3@q($A%qhkKU$o#IR!LXtGGmC}BqXhv`e-xU951b&!>SB;~@Ublb zviB;~X*HT(?=!-KI9JRek5IY!u=r!UV`pVw{(c%&vY#=54=eCAD9x$b&X#^~9F43V zE;b2k%rJDBP{rspB=m^533a(;w-Qa%SSpHo`vqBgM{!@Q))!| z+S;U~#L>baFX6L*@UzuICsJC4f5orOo*j|j*O+23ap;_Fp^venY^0pRXT%&|5A1*j zeF=#}cLF=%sF$;x?^aZ`1Mg1UXfI^Q%N@d}4W=7P8S^9I(px#H>V;-$)^&Mu#1Rtj zSjur78?(ghuZHQ0ZFl0)m1x!~bdatG)8tU|D5|I($;=p^lHPoKgr}{m<@Aw9Orf0^ z`PAv*ZxR%pGus&y{=TZUQJK0pHxSXWW;4#93|Hfc8KIJ4igUlW=bH&(4hQd~f@W29 zkxFp#RESqY+I@}Mj~4%|B0gt4=CW?LUv9GK`uw6zYWt+!}NnM!*^SVEzc$hQQN1qAUceHp;A+1yj zjn;n2sN<56fu&_;-p!j^EzL6~>@`|Drr+)tn~yk8n}->B1yt(h-oe=wjrGOE7C_NX z&DsYq@*z1WUpBW^L&`oPp(pS^l3kv5UGfYJG*8gc4wQU6b?4|Y&r5%rJ3M~uy+3Q$ zOy#FMHKW&(^HQ$t^dt{s*HifF!JC*TlNLd+b z*%Q(t2cj~YO5evD-ZUBj!>2qX!CbZV4D>SB!H{W8Gl%PzU*a-(2K&G9CUh1-b6z*6@ z9wbRN$`_SU+kGtinBh8RDITA(Rro>SCE;5W?s<@poXLsu&j&R4Ro1VH?qcEJa?p=| zU!5+W^34gHzQ5J-%?26$hfMJlUtv2~NvbLNAKsFbdVQ6{E!U zskaP<%#rz<2B1N=KBy5;(zZ8Zyv)&O-rd_9&%v8*lBcF|P)TxK_&WpYeW!qO%S3B; zPB8krWM=dHqDo(Wy(XkygsRzIM7^p-bicq=ep4xBk&!tNooc);HX6&`_LvUj?JEmj zKsVN+X^VpaTjdpp7TXsF6{1H>T}r>0S#cONTDD2Z&w5eagJtubHw|fXxXdt9S-^&f zFasGYI$oHVV|*5W5jV#tv6RylDrXFUJCFYI1HkiNrY5fp7a4rUc}ajFZ6IcJp;8vt z$_!x@u9r_l_+4~$EwSS;hT{0_i4aYQq{Pz})T5)LRSC$XBKRBMcZXAkDb`y;igO{X zA55D7?$WNYbiKT&^2IszxkaFQ24XrA%f#WWO>I7F={=);`jkY)(AN+ui{?S8=`q`w zqqSscUL7Qdo zTI#@Iurih@0W;J?^*vw9AU59r@Pu_{TPOHDRg0Xc`A2CaMK=P5sLFRjwrnoQZ;nGv zBO#qq{@T9^O`HpFCZ}W(s13^05b<;Z+BJdegh z4L4BeamPI3lQvBoQzC8J4p~q^nWH+gif`_DjSRMsD%{` zq>Qk2!nsj`IGbkJfUo~H16D8zWhikN4eylEv#(|x=f}DYa)WqfMczgmSzEgKBrmX# z(^V}V!@S-pkYLah_Yl<)0*k%aYFk(7E4Ud%s>v%_4bMb0#o(k2foWl7^{~xaUIw9R z$MECFx75Q{?KEz7dd=l;@5G9iYOKMYCWiB$g9c$^gUCd;A_vR^4%IiAx zVo^rL%-j7sNjR6DnuDv*u~*wTc#GW6>_$*nCJ$#@9Xgb?x<&)@GTOKIkWkW;v!oSf zyuTE8G;pa{2e87x`&3s~9h%aWOAgSH=>-@>s>>bC>o)={^o+JY(1iH4)U2OOk9H}N zoAxh_uV-FQ3U^TGIke{xf-I@$=O68KQbjH5BC4u@Oxdj&iIdwn6%2&*H)m|c^~->= zVXFdh^Q8X5inC;Ob<3Ep*B<&1R%7k0=#Lfzjwfgk#!ZRD@F0aAMcBuVJ$As^(fcVZ zMCdX`*Y?W3I6lc`zf4>n`L2&Q1~l8YCC1o-!>!g|WvWLd?8`sY&Bxc(pDkHrc?OdA#Ze-j?$#X zDw0?z3hG1c6VOiPO`o;Acpri;x!N5P&)&rZ6B%n_hVUAq?6QTRg3?5fMSwsDJpatN zw8VJhx3Y{_%fwEmhsz#9PpKJ8hFR*iw|w>s+CbFk!cqI%e*PtB46MbO>I_hbhkW)@ zPvYup=hvWBM<_Nq&XVOdIgqwkQLSN5OJoG}DM3PC0bp#aU0)wK9Fn`SyEo5iX)0Jk zimBF^crjEuxE59S54tXjhh((JUnF^d3NXzA3&<=JH8&HAMSSY@^>X9#%7;`wNz3HQ zT&8o-)ty}jRe^#!TNO}?<|Lq+%(x8htlf|^G7ohS$b#WX$IS)lo};))KFQBOX6!K1 zB+A{_x8-clb3UUKOnSma93H4&M^0&K(Moc0F4I6NtRZlZ&ZOlVP8=H=pP>Oa^_zq~ zzaK{a;e&OMK+<-KcmrKpiJBlBM$0_-80U2)S*Do+GMRjTJ8V+k7MX;z1@w`k<=v4- zi{n8@B>7q^?*`sr+pSV(g7)2Tifj0YcdqF zi042e^Ct|nv4~k|izv)vY4XDHF6G>2;)KhJmrr5MOl4x=VrM{j9#6WOdcL+#p*P4J zvN~vMYOmBIq9seEM&t4-*us63{DkP0P)9D=AWvOVMu{n>A78n!tmbzKqwn?LYP3ot ztT_4zE7Vbso%9U_J^Y8TXE7bc8JSLzr`jZ(H7JpC+i=TfKi%nKKK={`y;QVBKV$6F z?@zCPQCijVWiw&`K+vPC3&jnVhAD=QR5=Ma_WSjZevs2ke0PCi9{iPqx9y{NluR94 z;iif){0o{gy=n2`U3nW>puaH#){W()#jfrtW0}}AkzC~$OlOcdxNntCv6f&E^wYkP z`Y_`sv@Oa93-9sSV_|_+5fj&{sUhj<7rg4waX+!}9Ba^+gZunb5H|O`k9=xz*tRnV z{)k5E(7$+_YQV!}e7AH1^aw#Fd zaYAcR>qk+K0h=!*p!qI;eQNcYDjxKRk>{xI2l{@d2k~jOsV7w#uDCFJtCQL4e-19%HtM?VYWdT7}=Dgw{?$xx3Z=X zDu%46o*UPT?gO=N^j1d+mcCm3UHjm7C6}Ag%#mMrKsYqmdy#E$6-X0SIwZoZkie_X zAblH5`nipR>Bxq4^2jN#V$#3|pcag%3`ou4`(M9t?bu6j|eb-V|w`>2g zg_Y=p`RFAM!#+@Uw;x&dZP#{g?9bxMJ8DuuKTMFdM}XQGiUlfj;)NP5VXsVwIrbQX z+i&LXGHxoXf+&o)d(q@0hf)569H`!gdjI`0mZNG{R_}*#-VCpJzVE|$y{}4mu|T4iB`8ze1+&XsfR@E`B|``1D`N2UVwO<~B+-LjzwR#R zxTwi;asOYKX+F(7Iu5(56A(AuDp7}xFh&<2|C}WY*e~;{0lmMHmwNP$wv9H(B=T=9 zOfbEj8HM8`$D@9#MNRu%izt>by``z|<&Fa@E;HZ>-njtvn|w01*vGOjQ;YURn_szj{s${${)QcPKV_#0 z`|I3;_l#^l<3-C5kkC9^@YP`}G-bK=mfQPRbZ-OX zz8mHYo}F%NI`(7$JYH2Dr#+B^y4^rqkQ=CG_=mGl{-Pncql{$nfA3;KDaB7fXs%i% z2DpjdAXg0q<9LimvkUD`ytn*NfgcxxG ziM&~B9pd=01oUa_P>__UQQB6LS_OO2rTC-m?(Nm(yYA@~7CAwRzzW+XV=4V23ozuc zJ@3fqrB!CSbwsD$36ACvC1dN^UGLoP`Z2WfkhE z5p-cnL~8AtUUs+8R>85r@GA#DR^@xQ4z_0riJ2p7eyTZ|ukU}e5P)0>}Z=lUh&qRSzBzkkewThXOeaanO9==v7)#?#T>f zog0?Mx}|!inh#veoz7gT-z)8qr>@Z+?gyfm=QD9JQ_F3Md3ykRGPVx(dnj-sFK6Qw(<(7}??-Ek> zB%QTcLHS@fk?;KdT$m&ioi19l!c@lcq3Ucdhhs=yvFV}Knbg{>I1E(deMBP8Wu1%$M9V{9u>yS@qy#Gz?zG9~=7j675 zem@}7Hdb!=Qt+1ra;!xzXziW{q>#-ur(xqM2m&~|i!y50t&Cb+m(l7WT%bopy*36t zb5^Q=y?WM+uy}q;R#!ixPdLAjGxyqogFky>at{R5vM+Z|N+Rie%3H25cs293p!osEcTJrr8 z#-&Jb?XHcHbkc?9oBfN?edIBSRowe0_U7YQ6OoyKucDsAf)BhWfs6Rq=;3XfkP=R^ zr;aLk&X>T_f4CMS6;Hfyy3jSK_zf?7S6@n`d1i*zuPus2xJQwe+egWJnC8TW#d?g-zVaQ7Hd(W7xCkd za$`o~^sBdhK7$wgCldW2ev;=rsb}3UDeB-bPCSk1b#NF2q+C&BiRFNyr39><|L1-Go~?yp>^!Rowq)B8akgh*`{4@^N6 zd2b=7D32O_%UC%!`WfhmUuJ#^R{Io4RE>s^!SMbxM;AEwrA^-2?1cd?_k6%cWD&Cz zv(K=vJ!}-Rs|2phc?PPCc+B^nWxEwZeJar`pd?EU0coWjaQJeFFhGMw(K(;+D}An=eR)E!g?j(e`(MSQ9MtH!N-EQpxa zU{h33kw!cU=G^a^y)}bBid?qBR9ea6lP_<_j-Wh;nZuG+*m!I#Lg_KD@~?mOtRqvl znNN`qgXf)Eb3{To$>87Vq{qY?FOOA(YF@Xzsn{`YeZ_6-08**Q9zPpxO98kL8q$i{Q@_o1Ry!whMUp>go z)c?(AyH~mP+sA9mSP36e!=l^+2lo#_@c>j}Ba?4nK$dSV>BcUtMIHUcr6LFxroZnI z?~{cExZkZMQ(%w8^g58hwyuGWf_UD61=Y(4hWKht+S2RLdEZxGQ<-DM26>SpBola6 z&V3xDbyLqEX*pK;Of=~z!CR({Q`q;$`*sX{hphbx>otC-^kk4FRGiuJ8vaua$avt2 z+gdjXsQ{9PUf8MTfKjGZj^}XG2qcLGq6EeANGLNZMGQ-E%`>tCSRL4x-WIB~N0!T4 z$m;PIFYL+*P^u%-Z8wS`ont1}r_7Kq1o}3p;%gM1f`JX5)FSx>3#MObrGNNKJ=%JW@Tqb?}>5uXvy zXOvCp>P@^Vh#_@EuoI%!qM}E$on%g0-gGsy2}P8$u{k=WMFo23GN67g;>t$Jt~N3l z80U<~bnzbu)-ZQZ2@=ss^r>5*__PFzw-^+n! zNlMb3kv-$S57SmM&*EynDZEDJF+u)ty5PO188OL85ty8QU~!?y$DL7&hO(yQeNe{O6}G7- z9}*}pf;M?7k-4UR>A0fGI;_j5JHwy%GEuWDd)4wyiL3$zHAZCt)GC2v2k=iTRk+P( zP$Bo=4O|fYeQ_MOaG$FcW+K9UJ22vglQBQ~#`yS}PWf(?v+gPO9+1&p0+#rUi2gB= zM1w=-oDLAV#iP@9N)STj)3{F5^h)6cX3xKI9AD z11@rJT2sjQ@V%=0`+N%PNoXg!Cmx+V4wYi<;{)NxJr@ZP0i~>J?WF`Xrlewsm7po( zaXH+Xs{NutuZ=C=A#R^BD8oe;noy}O*{VPFzK6X4s7W|4%9~T#E+hF#PuG$0PisOo zSU%E&t5NqI%%m768q0hau?~+@SdkWX8R;PqF~E?GOP)6A+OK^3_NsIko+=KmjLCwu zDR6)>aRm}5)`@`WD}zDb&keX5#Z2bNHy5Oova@70yHeZkQ_seJ)AQ&OeO%2*r5A{p zs8tgR`mpVQfFz1?9^VRlD;AGel!g>ftvLyFiQ@o8RyA{UZQ|nj<(uHt1-KerVn~et zmp|oDw@N8=QY@@IRFlTmfys;J!ursLq{eZ@4f068(nTL9AdZYNmh((e)eAIx@q0Nn z!d*ZZN*=h}6oMj$u=-M^gK6gNU%9zDdZR(j(x-o%{Y%(yB4AH^MLi3pBv`R`;oj1h zH^B2$RQgKl$kBDLI?>6<6t98ZGDVeDpl@3@5Ccg;mZ<0WZ`A|7dVBg_b;z*|FM5%- z6>n#pRlE))yotSk`;3qLq#?y$`9+!#KyVEww6BvZ#K(c1u6Uq+zmmI!#dvMiJDOT~BgBDi* zOCUHmvD7IeBe0^5AxTWcc)dIwrz69C`|r!%JL=#CeQ|6>{a~>E%T|!HV%4!cZ9VSI z94)KjO@(4p8SnEAu!Yq((1NTjb}^rcOjPh84zkE^H9-ezp5>b|_F7V?S#RIczI%TE z`mif_@c^XQDJd{BtX?Na7qU{;CoFwZP{DZmt(VjDO;PVJfiV$+-!$?!^1kb- z=QA#=#h#PkGWlo&mriyHJSkDy6pE4GN^D35U53*;>;+ZnYe!BE9uB#%A{HaeSy5Gk zXIIjR4d7ZyBzpamdLz|y61-`K?0tZ}ykR0W)kbz#;oCa|6#pzg65}ol)O8Gjs>?^t zY;Wx>^MA1&CG^O_`I&~NO3IDnagiHWqt`df2^y{sCT9{DS|&y#3Ca)GflhAvsfdWV z2j{|Z*=MMYdphWRMWe3sxRkijCeALENbvclo+miI;)uz@kC=D2^{>}{`FH^rdZfd?SecDg7(ARaJUW#I6 zkWZ?I7r0O%*yWT%y`8jhe%UX#fmn1yLTFrqQ;;+vf!efaS5oEBE8X|ivO_Z*C%+nU?;-`WwIKWS;gY%Yod)w z7%C(|+3J-4nhyWf=#fT#Gqe8S9LhwL-Ek(}xtvl2Oj6)hg3xoBa|ytQ~#aK|n| zP|&g7M|ep+)k3BTRKr6I^TSm6h2!gfl14JRAS;%wOqk1m7 zZ|YCcvlGpyYqD_1w%l1zd}#v4_{)hj^AlatVkoKq{MFah*diGlcLCHu`8-=y`GeeX z>(WfgiNka?6iWX%i>I`_?&t(WpBGf=IX&P8bmG}a&ld7^I4NmL6u1>p$Y$Z&8D^#w z!V60>fg9#fjJb~vT|uu=V4yaTv?dz_ssI?0W6%`_KxpIZOeUaaKi z1omwO=50_QmEAhT6J`x}gbzb$cev-za4w)qN8v@$x|0CZ71v+$*-k{Q+$3#!w5NdQ zni5r4_V=V5nZzQRmu^>LVv|Zo1-*xYu!)6FK@%4|&As{y-n?E2ja5Ik?5zaPVc?ss z1BYk1A+1NMg?})E0_@hpq2bBhSPLi9nlz=7kIt5ZnT_<@qz2>G+q^D=^<~l3X|JA& zZlF`@K3Y6`z3g%HS-|0(D@J>Z@JI!g#k}w-JYs3EkGgnHO|WZyx-ccYxQ zv=8gkQ$y+eJ+gr3N5*}$Q@sXL#@Dpkx{4>c-9Bm!$UEG(Gto#PXtJtdzf^kDE`0K= zjUbaDo4+mNM8o0e)|5q@ZO9eRy`ROI%5D@}#7-F<-jUb3l^|-*)uNM62hKR8q$a3A zQvBa%dK|Sn))xR>l@Zlp!90|tGlL!#Ym}3zVq#OPs}2x_Qo}Je4!wf2}CF zvJT>gDPL?4=Y#deJCk_fC|8}DSt%G}`mvsw8jWlqBlB>85b4RGoqOm8XlL0^YBPHGkS?e+u&z z7ZA>*R8y}!h{M{GoANmIH$0+*RgLwS5Yvv{c?y(J;Nlp!pLEw;z^(07m{~k3NY>M) zM(B$iHJlEZxrJ7XlhJ~Bv_&@o-D#Vj5%H&C*>vV9f<=*j5PECI`4=ny1DW6?g5an< zu%Sq3>tTbgHT5a#ceL1-{kQY+vfl;#^?9g7m}e=q3Pc@K^(-?Nl67F;>6CEFquS;! zY4%k;E7TM~^hw)c>vPQ6Jnkej?&NSzRtk?|*RquW(0=reX@hEw6{PV<?i9Bf`APk$N=e?Y)IB!Tb8{yT*2#KPwtn zxz?hsBXe3knYc5AsMTyuyV=J5>M`zMlQNN{yFsm#YM*l@C~)Co;P~ahFUQ+2{yw1O zwhn+JcFBRrOuote_&Y6#rPa!aJ|CWI-$Jt2sCeM~z+Sg`_rUqjjU~tq>AU-$%8uqZQeyF=>hcC2 zyj%%@pvKMTl(Nmd%~H_{#OTX_5s2v*Y_A9o89l_l$1Z~~9RLsk6#M6+f28q`Zv21N z9E_f}@mt}Q5WmY;C?Q4{-t14I(csj!3dWng{B`{>>^~x#tl*oQ!yR0O_OW!>d8wH# zHHHsaIR`K@NP!y=0H3xDU3EEE@a!5~0BQkQFtRktR3}jPDM1K~(P5uT-EFWvJ~t~2 z0jpiY1XumbXHDVU{WtDlZVFDOG{a#7I1;`B3+TSuL5bx!+{ z*I=|4R-)K`c@FpOO*F_E*4K}R<+3ow-dy{w`!^x_-_Avs=LEI7gm?J3dj15j7I>qg zfP8A?{(UuuyI5@iMOWB;-(_Qw$_XNKf+YP?$;l%9sgJlF2C>BM_qPm+mM@SNhPLh# z;t`cK*aFnT40P}vOa+|>*p-&Q(`TWV(9%W8F|!V>?_Cr2#%j3_eriX~tH7r$qVtuXvEBhC)$-D_+w{i787AbOH zVm!PA<~VjtLM5Xu`Lw@9O7c*p0CRWc$h8orF3D2~*PAMVXPr<~F7o0Xo$kkr+_c|KQ z!#CJ4HFY>6GyCK0W1GOKo9<=Orm=yL>Cu5`RsnWk&?Mx`nGf~WYc?UD(rJYgbRBp( zqSofUn#grdTYb*QCDOuQ*Ean|OnY;#;CA92tUZU+^(Ijl_qjjbafh#nWbIGq{+R-G z#_+GsrwqBc2{vSrO)>xYa}dKn{`?<*jv2;3;q60A4F8zJKjwhx!T+r}6lm@UadEWW zLL_ef9-7(oLx_uU%I#A`q9euLE9MFmhhweRJuaNk^wv@KLJZfeg1=P?JYOG2f;Xx? zP{lruC+M901<|lOsg-d?wiPaHS}U~mOzkY7(OSG7As2Nv@5QoI{jD@wrd^k0 Qr- Date: Mon, 8 Jan 2024 16:39:50 +0100 Subject: [PATCH 25/25] :sparkles: Zendesk ticketing working --- .../public/providers/crm/zendesk_tcg.png | Bin 0 -> 8267 bytes .../crm/contact/services/contact.service.ts | 49 ++--- .../src/crm/contact/types/model.unified.ts | 1 + .../api/src/ticketing/@utils/@types/index.ts | 2 +- .../account/services/account.service.ts | 29 ++- .../ticketing/account/sync/sync.service.ts | 2 +- .../ticketing/account/types/mappingsTypes.ts | 2 +- .../ticketing/account/types/model.unified.ts | 1 + .../attachment/services/attachment.service.ts | 49 ++--- .../attachment/types/mappingsTypes.ts | 8 +- .../attachment/types/model.unified.ts | 1 + .../ticketing/comment/comment.controller.ts | 4 +- .../comment/services/comment.service.ts | 93 +++++----- .../ticketing/comment/services/front/index.ts | 93 +++++----- .../comment/services/front/mappers.ts | 51 +++--- .../comment/services/zendesk/index.ts | 126 +++++++------ .../comment/services/zendesk/mappers.ts | 53 +++--- .../ticketing/comment/sync/sync.service.ts | 167 ++++++++++-------- .../ticketing/comment/types/mappingsTypes.ts | 8 +- .../ticketing/comment/types/model.unified.ts | 3 +- .../contact/services/contact.service.ts | 29 ++- .../ticketing/contact/sync/sync.service.ts | 2 +- .../ticketing/contact/types/mappingsTypes.ts | 2 +- .../ticketing/contact/types/model.unified.ts | 1 + .../src/ticketing/tag/services/tag.service.ts | 29 ++- .../src/ticketing/tag/sync/sync.service.ts | 2 +- .../src/ticketing/tag/types/mappingsTypes.ts | 2 +- .../src/ticketing/tag/types/model.unified.ts | 1 + .../ticketing/team/services/team.service.ts | 29 ++- .../src/ticketing/team/sync/sync.service.ts | 2 +- .../src/ticketing/team/types/mappingsTypes.ts | 2 +- .../src/ticketing/team/types/model.unified.ts | 1 + .../ticketing/ticket/services/front/index.ts | 10 +- .../ticket/services/front/mappers.ts | 54 +++++- .../ticket/services/github/mappers.ts | 4 +- .../ticket/services/hubspot/mappers.ts | 4 +- .../ticket/services/ticket.service.ts | 140 +++++++++------ .../ticket/services/zendesk/index.ts | 74 ++++---- .../ticket/services/zendesk/mappers.ts | 101 ++++++++--- .../src/ticketing/ticket/sync/sync.service.ts | 88 ++++++--- .../api/src/ticketing/ticket/types/index.ts | 2 +- .../ticketing/ticket/types/mappingsTypes.ts | 10 +- .../ticketing/ticket/types/model.unified.ts | 1 + .../api/src/ticketing/ticket/utils/index.ts | 36 ++++ .../ticketing/user/services/user.service.ts | 29 ++- .../src/ticketing/user/sync/sync.service.ts | 2 +- .../src/ticketing/user/types/mappingsTypes.ts | 2 +- .../src/ticketing/user/types/model.unified.ts | 1 + 48 files changed, 801 insertions(+), 601 deletions(-) create mode 100644 apps/webapp/public/providers/crm/zendesk_tcg.png diff --git a/apps/webapp/public/providers/crm/zendesk_tcg.png b/apps/webapp/public/providers/crm/zendesk_tcg.png new file mode 100644 index 0000000000000000000000000000000000000000..697e340fbfff5b5a09a590247613740bacf51320 GIT binary patch literal 8267 zcmeHMc{tQ>yMIj7MA?d@5DFzmNwzE{`&Ps#3=)d6uVYOMS(80$F(}KBHH@`rMPuLh zrm-)>*qz7k_n!B>=eo}M|D5Z3$6VLfe4p?0eDCL8KKJLopBFcDR1ed0(gOevtE*kt z0{{X4MF8a9#hX(pA6{r}u4-QepeT}I%YqL6K4qz%j$ec*=25% zElTNtdU%L~N`z8`W#2<_+3lS*x6PG&vxb?zXx=k+A5l@UB69Vw3)slr{ykqech{e) z`nxa#{d@NorL2w(6#ed58Gl^wq>n?7c+l1zRrXHdEdQi0tduO{|Hkfykv%4r!n_p$ z5(3R`%|4awqD}1W<=XcIg)@!i4pcY=vea6C#H5;Q4-}2jGcb4u=??f$B)}QRX~aH% zEhc3O8r|qxnLdWkRL8f8x~^9H`+wBL((NGN@Mw%a@yPiilh;Ci*@=xJ;n$bI|j6%&D-OOSw&R0djC!OW;5eJl8sOo-__D>i{`IZWoQIEye3X+I`v zZCWczXs~6(Wvc1kn8NDlkS@F->7g{4>W6v3ereCKObTtTwCC1S3N3C!H%wDQ{xktm zt_Th-tq;xP1dYB;n)%B)QYt9);Q}pY)l#3LQO{u4&S!9rxV3R5@>YH65TD-L79btU7RMLe;< z{m`M`#fKQ5f`n(yd1KD2dgoLh$7Y`R_|dfKd8&;6RV;E2(1?AnO#1E84>Ta<*mTr!NMvO7i#ACkg z>rwfqn7fzfhmKq($&uNF=n>}0Op*>?TU+BXqdmd*Ur4)N#Mv0#yL$C8h50BQjabXa zj_>_zTVtj|LPBfi?d)W>b0+H&gvLMS1r;L|$bc8W@ta22zj~9Iouc% z!n~k*Ys*4EOB3(N!SkrM3BT)ZaN|asun(eJw3HQLUea2lJ=VZk&d)DycL}f2e|x0A zSkiYp9KKu-VP1K&-Nx#kwK`sGaLtRM#q9S|u{&kQUq11pb1Zy996sUlE}ox+g8C@o z@t2qJ?z}^0GhAKS+4p2TO9QXC$PXCJyHxFJ)^-k%*`8OXMz{OlJw$q2l%`KYxgI|e z_q}u~FBnJ#z+Ga-1xz%m&dGhc&Y#`dw--P3-@pB6nd9L2(VA+&0-7B~SDb5u|1>mQ zBlB9T4^)OP3-sz*SXk5s-`}vcpsllb*tDac#?hhYuK}KEQbQQ*rF@EO<5t-@f>6$lss6qa5 z+a5(SIp^i24VCM)gg}>HJ-=Y!s??~O zx$%OC*&m-nMKJ!;N&5_Ikaw-!-J5^a3(F5e<$qh{k?ulWyo06F;G}W%L=)T7j?3^l zOsjO1YCDXuyAH>QD+Cbf`U@4oc+4smIUFBZX)R(=>nP^2;n!>DE z_01`&kAzRl*32~HazWkiLGP)RKgXcr!v_ZLR`=@R)_8Xrz6$EB9txefCi894q45J$ z=2+bM+;&3i)FatLyB>a0lefx87LKWgN$-5`9V1LOMRv^eW#K4Po8%Ho7-Pnhc5!iB zkRk_7VMUA!sVYdNWBS+p6K&(ACm zxZ6uhOPGTPQL9}IEG&SHF-41f?6_&MT`S%6u4y%@@Mv$j0@ut@4q~ClZfC!H&$x!o zoV~qW_NOa&lf5k5I^hV-e|1;%7p(lUlxrc3o2{iqVzA+&z@ z9U5OS@Bt#j&($5*-9V?S+P2;2)*ad$=L_y^4pkl|3Y*g-(siaf&4$%YmUYHntG<`- zAUz{tX)%3IFk#~j^H&z_L>jib@=Y6So$Hi# zBy!i#OUuQ*56it|$=w&3(q!D$Cf+rk&4#YMG0C*4x#J=bX}H@=JUDLeU(5<0kqu+i z(QJLmjVNXYTLp!Mj`MFCcQ>bej(bqS?l-^5xs|1b1tP9TyPIe3HZq z3Il%;yeSUDI@o7UHADU*;XP-eaYPXyh&rQ?^oh~S6b|U5)V5hY{F|>Ngo3)FPR}p9 zjR|TtHZ~A6zy1ZMK4)j&Q^79Jfz)DFzRW;oa|E;?!gk|}#UpOJ(}V`87slnEQMW}2h=|9aj+f9| z+WstB2tYA{K%Irwc6Xcm&(xO~i{!&;{>Lvkyn(FoGy*iYLEqi!%t)fp zkHekjIb1J+1LqLnz=G$XjEfQihvp#Z^>s!(u7~7>s5Fv;1X}2?tqv=h_dO&fp$!u@5{csHKcuHCVv%_Ppziu% zyZz_snWErY5L_Z2^O6%2VUA&;1x;Rc-5!H66&G=XxS)hHDAauH%Y}^WRyC%_ZpJ153AlP~ zlRCRNBfjTpl5)Cm%xe+uHIW^A?^$0R9fIiar=p z4=*QCn2R&bzR!rs{L#QxQkad7&;(R3$q!9uH+f+_)tD}5UcGvCe(^}OZYC7biUlBD z+?;N+@tET7((M^pc6m_OeKrn5Bg_w&FMH4AWOH6Q2SwDBw12&a`*i0cfFkirD*n)) zg||==AHr_kNX0uC48FABZr|lcth-(2WVR-0x1DR<=}wZ)F1Vn&4J5cnyN1TFoFLE4 zoQK{H&6|Qy62+)Tb0gT0fOgH(%U&&*x=M-Rz8lxj(17N*^t&^2&_m=*3hO%@1;jw9$aUDoqi6Ngqjh6rGlsL*VFGg!H2 zfu5JylQ`Iu}#i+4kHAT&JG>RYXzhM2t95{prQ7UP%t&g3`&^%i@saf zpl}*Yk5y>FnfmsOAJ3nv*9;92mBPfuutbjo(ePccg0F&L$8%k|td47s+aLvR;i?OjtNew*kQvx@(1Q@w{aBiA# zV4adAXK6!#fw0C!qj+JuFU#QmZVeRM<S6=r_Km32hG2fDm&B ztI}NelJY|f4)HL9qe%yRe7m(@2P!KFthE;AIts`42{_> zFEKO&M``K?vE+Axf{ffk&^Md?rTaysuAmgmut-OJTHt|eY3z*CK2+ZOkf`+Im7hp) z)&3pPPt2nhxwU3V_!DTq5hg$|jlsXcp1;P28_ z5z?Q5YyVqd(7?)TWZn}Sb32WBetK2}n)S(D0e=s%+i>$}B(VQxT`UqAl4F_=6Uz7} z)a|md_IxN}l`?SDkA6;AzgsMr2_b(RRl_N1Q$wLo=YUzdbO?1t_C4BT4@4X(Mrn&T z;gqp@S?@K5$nJb`)V(k$BrI0?E{L5Lmo;{WnJ9O{BMqu4J}3x&(Y+T_xw+>G?7Ke{ z7g$-@+_QpJl+`^eQ!PzR$TMnM%`YJW6mlm>FaQ5NUykjdnc#BHSth+&5RcK-)I{29qj1& zuM}jqU0D06#w5Uh(lGo)ne9N1!T5gVouh_cJZVev^1AYOsJq)~X%T5XQ6&zl#Gw># z|FxCAQr1m(a{(xr5$o};bd>y@r3>K;mI#hT4OJG~q*ma{81Mm*5q*QYM+(#KE zv~Z%s`&;kyphA~n>OIIm)CXDQlI~@PEn5$apWjy>H;zCsB8gHMlzFJAuxE;7=l81j z_iFYO`&vK6m6todGG(3`Q>+a31qCU0F3)#-@S5t(6-O7O5^P0KhspKLilea3>qu9r zd%M64S5Vh%{UatLB`4m_ExFSwQ!07-lc0$49snH?Ez}=swiz3FEOu*5{<8)%>(NaS$#>gd;ykkaOC5D&mm`X+nu{N*?aDVxp4Q=(e=59d z#7)!I@^|9Zpl(89pflDr+h5afTwU%pFNbjn+~*;Z+`mzzLL43*K5LxyBL_1WjrMJu zUN0jWo2$|)R;^oDCjFI~uYdOQqWYoa)2gSQTutfge|VYfFjuw~TSeXe;?z%S$ulSo zp|JPR(cBQpF`G|qT@+3oH1IAQBhCNBaB%AlHLGI3bJ-20?+i_bzTMAsaWtI%C<>9@ zt*I+_T*5P*LO(rCtW+ivpY3{IifeRob2>;KwrL(njNS5l*QSRI&@0U~Fui=C`qo)L zF!SYSrrB;PD#}>>ndB>D$9aHWN(Pbs zePIq&+ckOywjjDnwR3lWBtZ7Kfx=7VnmLfFZ?k>d)L^>$iv=h{j&RXx4SIy(<-&1I z9u?J!kChrsXL;Rr7Z=bBzZaa}j`M*#kS^?q2?Rzu_v?0F78yRwnLj)DE9l&nLh|gp zNna_CmrBHy#`kfN@uZEXo;)ZNEdM)UO@liUiUf0rZCuC|Tr}fDGmIPJaCWFYLb=8A3!g)DHP=b$gTC@nPNgO4SCxB)U7c z=!lf!kDJWEZiymPEYF+Du-U5%rC@)001_4!YS=zrz<#f|H%?W#qrlJr8yqXb$UFnPP7I9ZoGugxw0sX0{fbp$Zd#*_N@uaaS- z>|3>*oFLP97-zZTFhTx;5+)w=*!UEPBbMmoQ?E5^;0r4fc=5|0Tl+8MSbEE|+>=6> zbM283oruZvv+kWa*5TciPwQyzz$8Ave)C@tv8dvO17n9*L)zO1 z6t!^Cd}9rKo%UkJLzYnf2Iz9IMsNzZ?ebhr)5f8t5?-P=)pYB$S%7db$E9?;q&T8* z9?DhGjC?SH^ee!BA@PzW3kd2i)&A~;Ux|jTJU8xwt{N{$w-{eIa)tt_vi%YW_Hd?2 z-!fL_C0m|3(%T1mRfb8;oa*>JqQN+`<2>1={bEJP^bTxc#8MaMh?S5_9=Kd#CrCcH z#z0$l^W(?Lf)rL`vY!H(_nfQ4W(h-fXBES2s?XMtcg3j`C)f*m&BNYNq9a0PyF+=@ zocL%iBq?`VxV0n-^H>4)+#Wm}#t>2+gbCL|?-=A>%Db(EebzWmaoo=bB1oUDb+6Y- zt6`t57)KJbBDJ@>Z;;tI z>DJH%H)^UW^qLQPb6{t4ulrtrozxJ%%a@Xpf(yET3%}7wJd&3+*EoRN&kIzRS1AKx zk6uL{Yi|3FT%g9MnoD4;5h*OUis@;D{3N)?FcPr!+g%dX znEGG?wi`PUo;e@(rJ@s`01jL8=J=thE@kX9cZq-AO~~ee z*PO&Mq>%wm5W`+_@JxQ|CE>ihu^PIC0uHSoh({JqAk5iyBQKs{9s9ny2J4;ZVnJquSW8B1p? zcmWs1E?y83lMuNmc1!G%jJTAH#06n7F&Qy2gX4x<|82q}hkFmKz5e$Jr?1Z`!U;fK MS?7AeHS@s#0C&hei~s-t literal 0 HcmV?d00001 diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index 585613fbd..f2051d45d 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -35,7 +35,7 @@ export class ContactService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const responses = await Promise.all( unifiedContactData.map((unifiedData) => @@ -48,15 +48,7 @@ export class ContactService { ), ); - const allContacts = responses.flatMap((response) => response.contacts); - const allRemoteData = responses.flatMap( - (response) => response.remote_data || [], - ); - - return { - contacts: allContacts, - remote_data: allRemoteData, - }; + return responses; } catch (error) { handleServiceError(error, this.logger); } @@ -67,7 +59,7 @@ export class ContactService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const linkedUser = await this.prisma.linked_users.findUnique({ where: { @@ -258,7 +250,6 @@ export class ContactService { }); } - ///// const result_contact = await this.getContact( unique_crm_contact_id, remote_data, @@ -279,7 +270,7 @@ export class ContactService { }, }); await this.webhook.handleWebhook( - result_contact.contacts, + result_contact, 'crm.contact.created', linkedUser.id_project, event.id_event, @@ -293,7 +284,7 @@ export class ContactService { async getContact( id_crm_contact: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const contact = await this.prisma.crm_contacts.findUnique({ where: { @@ -345,10 +336,7 @@ export class ContactService { field_mappings: field_mappings, }; - let res: ContactResponse = { - contacts: [unifiedContact], - }; - + let res: UnifiedContactOutput = unifiedContact; if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ where: { @@ -359,7 +347,7 @@ export class ContactService { res = { ...res, - remote_data: [remote_data], + remote_data: remote_data, }; } @@ -373,13 +361,13 @@ export class ContactService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const contacts = await this.prisma.crm_contacts.findMany({ where: { - remote_id: integrationId.toLowerCase(), + remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, include: { @@ -432,27 +420,22 @@ export class ContactService { }), ); - let res: ContactResponse = { - contacts: unifiedContacts, - }; + let res: UnifiedContactOutput[] = unifiedContacts; if (remote_data) { - const remote_array_data: Record[] = await Promise.all( - contacts.map(async (contact) => { + const remote_array_data: UnifiedContactOutput[] = await Promise.all( + res.map(async (contact) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: contact.id_crm_contact, + ressource_owner_id: contact.id, }, }); const remote_data = JSON.parse(resp.data); - return remote_data; + return { ...contact, remote_data }; }), ); - res = { - ...res, - remote_data: remote_array_data, - }; + res = remote_array_data; } const event = await this.prisma.events.create({ data: { @@ -476,7 +459,7 @@ export class ContactService { async updateContact( id: string, updateContactData: Partial, - ): Promise { + ): Promise { try { } catch (error) { handleServiceError(error, this.logger); diff --git a/packages/api/src/crm/contact/types/model.unified.ts b/packages/api/src/crm/contact/types/model.unified.ts index 2705e9ba9..c883d74f1 100644 --- a/packages/api/src/crm/contact/types/model.unified.ts +++ b/packages/api/src/crm/contact/types/model.unified.ts @@ -21,4 +21,5 @@ export class UnifiedContactOutput extends UnifiedContactInput { description: 'The id of the contact in the context of the Crm software', }) remote_id?: string; + remote_data?: Record; } diff --git a/packages/api/src/ticketing/@utils/@types/index.ts b/packages/api/src/ticketing/@utils/@types/index.ts index 94e9d2fdf..b303a219b 100644 --- a/packages/api/src/ticketing/@utils/@types/index.ts +++ b/packages/api/src/ticketing/@utils/@types/index.ts @@ -50,7 +50,7 @@ export enum TicketingObject { ticket = 'ticket', comment = 'comment', user = 'user', - attachment = 'attachement', + attachment = 'attachment', contact = 'contact', account = 'account', tag = 'tag', diff --git a/packages/api/src/ticketing/account/services/account.service.ts b/packages/api/src/ticketing/account/services/account.service.ts index d603d4443..e53ce14ed 100644 --- a/packages/api/src/ticketing/account/services/account.service.ts +++ b/packages/api/src/ticketing/account/services/account.service.ts @@ -15,7 +15,7 @@ export class AccountService { async getAccount( id_ticketing_account: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const account = await this.prisma.tcg_accounts.findUnique({ where: { @@ -55,9 +55,7 @@ export class AccountService { field_mappings: field_mappings, }; - let res: AccountResponse = { - accounts: [unifiedAccount], - }; + let res: UnifiedAccountOutput = unifiedAccount; if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ @@ -69,7 +67,7 @@ export class AccountService { res = { ...res, - remote_data: [remote_data], + remote_data: remote_data, }; } @@ -83,13 +81,13 @@ export class AccountService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const accounts = await this.prisma.tcg_accounts.findMany({ where: { - remote_id: integrationId.toLowerCase(), + remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); @@ -130,27 +128,22 @@ export class AccountService { }), ); - let res: AccountResponse = { - accounts: unifiedAccounts, - }; + let res: UnifiedAccountOutput[] = unifiedAccounts; if (remote_data) { - const remote_array_data: Record[] = await Promise.all( - accounts.map(async (account) => { + const remote_array_data: UnifiedAccountOutput[] = await Promise.all( + res.map(async (account) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: account.id_tcg_account, + ressource_owner_id: account.id, }, }); const remote_data = JSON.parse(resp.data); - return remote_data; + return { ...account, remote_data }; }), ); - res = { - ...res, - remote_data: remote_array_data, - }; + res = remote_array_data; } const event = await this.prisma.events.create({ data: { diff --git a/packages/api/src/ticketing/account/sync/sync.service.ts b/packages/api/src/ticketing/account/sync/sync.service.ts index ec2ba6643..5ef2f914d 100644 --- a/packages/api/src/ticketing/account/sync/sync.service.ts +++ b/packages/api/src/ticketing/account/sync/sync.service.ts @@ -35,7 +35,7 @@ export class SyncService implements OnModuleInit { } } - @Cron('*/20 * * * *') + //@Cron('*/20 * * * *') //function used by sync worker which populate our tcg_accounts table //its role is to fetch all accounts from providers 3rd parties and save the info inside our db async syncAccounts() { diff --git a/packages/api/src/ticketing/account/types/mappingsTypes.ts b/packages/api/src/ticketing/account/types/mappingsTypes.ts index 7d68374e2..75f712a42 100644 --- a/packages/api/src/ticketing/account/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/account/types/mappingsTypes.ts @@ -7,7 +7,7 @@ const frontAccountMapper = new FrontAccountMapper(); const githubAccountMapper = new GithubAccountMapper(); export const accountUnificationMapping = { - zendesk: { + zendesk_tcg: { unify: zendeskAccountMapper.unify, desunify: zendeskAccountMapper.desunify, }, diff --git a/packages/api/src/ticketing/account/types/model.unified.ts b/packages/api/src/ticketing/account/types/model.unified.ts index c0630f76b..d0ca96e5a 100644 --- a/packages/api/src/ticketing/account/types/model.unified.ts +++ b/packages/api/src/ticketing/account/types/model.unified.ts @@ -7,4 +7,5 @@ export class UnifiedAccountInput { export class UnifiedAccountOutput extends UnifiedAccountInput { id?: string; remote_id?: string; + remote_data?: Record; } diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index fdde2a686..fbfcdd2b9 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -26,7 +26,7 @@ export class AttachmentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const responses = await Promise.all( unifiedAttachmentData.map((unifiedData) => @@ -39,17 +39,7 @@ export class AttachmentService { ), ); - const allAttachments = responses.flatMap( - (response) => response.attachments, - ); - const allRemoteData = responses.flatMap( - (response) => response.remote_data || [], - ); - - return { - attachments: allAttachments, - remote_data: allRemoteData, - }; + return responses; } catch (error) { handleServiceError(error, this.logger); } @@ -60,7 +50,7 @@ export class AttachmentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const linkedUser = await this.prisma.linked_users.findUnique({ where: { @@ -136,7 +126,7 @@ export class AttachmentService { }); await this.webhook.handleWebhook( - result_attachment.attachments, + result_attachment, 'ticketing.attachment.created', linkedUser.id_project, event.id_event, @@ -150,7 +140,7 @@ export class AttachmentService { async getAttachment( id_ticketing_attachment: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const attachment = await this.prisma.tcg_attachments.findUnique({ where: { @@ -191,9 +181,7 @@ export class AttachmentService { field_mappings: field_mappings, }; - let res: AttachmentResponse = { - attachments: [unifiedAttachment], - }; + let res: UnifiedAttachmentOutput = unifiedAttachment; if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ @@ -205,7 +193,7 @@ export class AttachmentService { res = { ...res, - remote_data: [remote_data], + remote_data: remote_data, }; } @@ -219,12 +207,12 @@ export class AttachmentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const attachments = await this.prisma.tcg_attachments.findMany({ where: { - remote_id: integrationId.toLowerCase(), + remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); @@ -266,27 +254,22 @@ export class AttachmentService { }), ); - let res: AttachmentResponse = { - attachments: unifiedAttachments, - }; + let res: UnifiedAttachmentOutput[] = unifiedAttachments; if (remote_data) { - const remote_array_data: Record[] = await Promise.all( - attachments.map(async (attachment) => { + const remote_array_data: UnifiedAttachmentOutput[] = await Promise.all( + res.map(async (attachment) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: attachment.id_tcg_attachment, + ressource_owner_id: attachment.id, }, }); const remote_data = JSON.parse(resp.data); - return remote_data; + return { ...attachment, remote_data }; }), ); - res = { - ...res, - remote_data: remote_array_data, - }; + res = remote_array_data; } const event = await this.prisma.events.create({ @@ -313,7 +296,7 @@ export class AttachmentService { async downloadAttachment( id_ticketing_attachment: string, remote_data?: boolean, - ): Promise { + ): Promise { return; } } diff --git a/packages/api/src/ticketing/attachment/types/mappingsTypes.ts b/packages/api/src/ticketing/attachment/types/mappingsTypes.ts index 1177b8d7b..68ffceec2 100644 --- a/packages/api/src/ticketing/attachment/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/attachment/types/mappingsTypes.ts @@ -7,16 +7,16 @@ const githubAttachmentMapper = new GithubAttachmentMapper(); const frontAttachmentMapper = new FrontAttachmentMapper(); export const commentUnificationMapping = { - zendesk: { - unify: zendeskAttachmentMapper.unify, + zendesk_tcg: { + unify: zendeskAttachmentMapper.unify.bind(zendeskAttachmentMapper), desunify: zendeskAttachmentMapper.desunify, }, front: { - unify: frontAttachmentMapper.unify, + unify: frontAttachmentMapper.unify.bind(frontAttachmentMapper), desunify: frontAttachmentMapper.desunify, }, github: { - unify: githubAttachmentMapper.unify, + unify: githubAttachmentMapper.unify.bind(githubAttachmentMapper), desunify: githubAttachmentMapper.desunify, }, }; diff --git a/packages/api/src/ticketing/attachment/types/model.unified.ts b/packages/api/src/ticketing/attachment/types/model.unified.ts index e000714cc..dd3e2e447 100644 --- a/packages/api/src/ticketing/attachment/types/model.unified.ts +++ b/packages/api/src/ticketing/attachment/types/model.unified.ts @@ -19,4 +19,5 @@ export class UnifiedAttachmentOutput extends UnifiedAttachmentInput { type: String, }) remote_id?: string; + remote_data?: Record; } diff --git a/packages/api/src/ticketing/comment/comment.controller.ts b/packages/api/src/ticketing/comment/comment.controller.ts index 8fb5396c7..cfd466a94 100644 --- a/packages/api/src/ticketing/comment/comment.controller.ts +++ b/packages/api/src/ticketing/comment/comment.controller.ts @@ -45,8 +45,8 @@ export class CommentController { //@ApiCustomResponse(CommentResponse) @Get() getComments( - @Query('integrationId') integrationId: string, - @Query('linkedUserId') linkedUserId: string, + @Headers('integrationId') integrationId: string, + @Headers('linkedUserId') linkedUserId: string, @Query('remoteData') remote_data?: boolean, ) { return this.commentService.getComments( diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index 9377e9026..99b9c1290 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -9,7 +9,7 @@ import { UnifiedCommentInput, UnifiedCommentOutput, } from '../types/model.unified'; -import { CommentResponse, ICommentService } from '../types'; +import { ICommentService } from '../types'; import { desunify } from '@@core/utils/unification/desunify'; import { TicketingObject } from '@ticketing/@utils/@types'; import { unify } from '@@core/utils/unification/unify'; @@ -32,7 +32,7 @@ export class CommentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const responses = await Promise.all( unifiedCommentData.map((unifiedData) => @@ -45,15 +45,7 @@ export class CommentService { ), ); - const allComments = responses.flatMap((response) => response.comments); - const allRemoteData = responses.flatMap( - (response) => response.remote_data || [], - ); - - return { - comments: allComments, - remote_data: allRemoteData, - }; + return responses; } catch (error) { handleServiceError(error, this.logger); } @@ -64,7 +56,7 @@ export class CommentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const linkedUser = await this.prisma.linked_users.findUnique({ where: { @@ -182,21 +174,32 @@ export class CommentService { ? { id_tcg_contact: unifiedCommentData.contact_id, } - : { + : target_comment.creator_type === 'user' + ? { id_tcg_user: unifiedCommentData.user_id, - }; + } + : {}; //case where nothing is passed for creator or a not authorized value; if (existingComment) { // Update the existing comment - const data = { - body: target_comment.body, - html_body: target_comment.html_body, - is_private: target_comment.is_private, - creator_type: target_comment.creator_type, + let data: any = { id_tcg_ticket: unifiedCommentData.ticket_id, modified_at: new Date(), - ...opts, }; + if (target_comment.body) { + data = { ...data, body: target_comment.body }; + } + if (target_comment.html_body) { + data = { ...data, html_body: target_comment.html_body }; + } + if (target_comment.is_private) { + data = { ...data, is_private: target_comment.is_private }; + } + if (target_comment.creator_type) { + data = { ...data, creator_type: target_comment.creator_type }; + } + data = { ...data, ...opts }; + const res = await this.prisma.tcg_comments.update({ where: { id_tcg_comment: existingComment.id_tcg_comment, @@ -207,20 +210,28 @@ export class CommentService { } else { // Create a new comment this.logger.log('comment not exists'); - let data = { + let data: any = { id_tcg_comment: uuidv4(), - body: target_comment.body, - html_body: target_comment.html_body, - is_private: target_comment.is_private, created_at: new Date(), modified_at: new Date(), - creator_type: target_comment.creator_type, id_tcg_ticket: unifiedCommentData.ticket_id, id_linked_user: linkedUserId, remote_id: originId, remote_platform: integrationId, }; + if (target_comment.body) { + data = { ...data, body: target_comment.body }; + } + if (target_comment.html_body) { + data = { ...data, html_body: target_comment.html_body }; + } + if (target_comment.is_private) { + data = { ...data, is_private: target_comment.is_private }; + } + if (target_comment.creator_type) { + data = { ...data, creator_type: target_comment.creator_type }; + } data = { ...data, ...opts }; const res = await this.prisma.tcg_comments.create({ @@ -270,7 +281,7 @@ export class CommentService { }, }); await this.webhook.handleWebhook( - result_comment.comments, + result_comment, 'ticketing.comment.created', linkedUser.id_project, event.id_event, @@ -285,7 +296,7 @@ export class CommentService { async getComment( id_commenting_comment: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const comment = await this.prisma.tcg_comments.findUnique({ where: { @@ -331,8 +342,8 @@ export class CommentService { user_id: comment.id_tcg_user, // uuid of User object }; - let res: CommentResponse = { - comments: [unifiedComment], + let res: UnifiedCommentOutput = { + ...unifiedComment, }; if (remote_data) { @@ -345,7 +356,7 @@ export class CommentService { res = { ...res, - remote_data: [remote_data], + remote_data: remote_data, }; } @@ -361,11 +372,11 @@ export class CommentService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const comments = await this.prisma.tcg_comments.findMany({ where: { - remote_id: integrationId.toLowerCase(), + remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); @@ -411,27 +422,21 @@ export class CommentService { }), ); - let res: CommentResponse = { - comments: unifiedComments, - }; + let res: UnifiedCommentOutput[] = unifiedComments; if (remote_data) { - const remote_array_data: Record[] = await Promise.all( - comments.map(async (comment) => { + const remote_array_data: UnifiedCommentOutput[] = await Promise.all( + res.map(async (comment) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: comment.id_tcg_comment, + ressource_owner_id: comment.id, }, }); const remote_data = JSON.parse(resp.data); - return remote_data; + return { ...comment, remote_data }; }), ); - - res = { - ...res, - remote_data: remote_array_data, - }; + res = remote_array_data; } const event = await this.prisma.events.create({ diff --git a/packages/api/src/ticketing/comment/services/front/index.ts b/packages/api/src/ticketing/comment/services/front/index.ts index 1e0dfb294..b33c09fd3 100644 --- a/packages/api/src/ticketing/comment/services/front/index.ts +++ b/packages/api/src/ticketing/comment/services/front/index.ts @@ -32,7 +32,7 @@ export class FrontService implements ICommentService { remoteIdTicket: string, ): Promise> { try { - //TODO: check required scope => crm.objects.contacts.write + // Check required scope => crm.objects.contacts.write const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, @@ -40,57 +40,62 @@ export class FrontService implements ICommentService { }, }); - //retreive the right user for author - const user = await this.prisma.tcg_users.findUnique({ - where: { - id_tcg_user: commentData.author_id, - }, - select: { remote_id: true }, - }); - if (!user) - throw new Error('author_id is invalid, it must be a valid User'); + let dataBody = commentData; + + //first we retrieve the right author_id (it must be either a User or a Cntact) + const author_id = commentData.author_id; //uuid of either a User or a Contact + let author_data; + + if (author_id) { + // Retrieve the right user for author + const user = await this.prisma.tcg_users.findUnique({ + where: { + id_tcg_user: commentData.author_id, + }, + select: { remote_id: true }, + }); + if (!user) { + throw new Error('author_id is invalid, it must be a valid User'); + } + author_data = user; //it might be undefined but if it is i insert the right data below + dataBody = { ...dataBody, author_id: user.remote_id }; + } + // Process attachments let uploads = []; const uuids = commentData.attachments; if (uuids && uuids.length > 0) { - for (const uuid of uuids) { - const res = await this.prisma.tcg_attachments.findUnique({ - where: { - id_tcg_attachment: uuid, - }, - }); - if (!res) - 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 - const fileStream = await this.utils.fetchFileStreamFromURL( - res.file_url, - ); - - uploads = [...uploads, fileStream]; - } + 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); + }), + ); } + // Prepare request data let resp; if (uploads.length > 0) { - const dataBody = { - ...commentData, - author_id: user.remote_id, - attachments: uploads, - }; const formData = new FormData(); - - if (dataBody.author_id) { - formData.append('author_id', dataBody.author_id); - } - formData.append('body', dataBody.body); - - for (let i = 0; i < uploads.length; i++) { - const up = uploads[i]; - formData.append(`attachments[${i}]`, up); + if (author_data) { + formData.append('author_id', author_data.remote_id); } + formData.append('body', commentData.body); + uploads.forEach((fileStream, index) => { + formData.append(`attachments[${index}]`, fileStream); + }); + // Send request with attachments resp = await axios.post( `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, formData, @@ -104,10 +109,7 @@ export class FrontService implements ICommentService { }, ); } else { - const dataBody = { - ...commentData, - author_id: user.remote_id, - }; + // Send request without attachments resp = await axios.post( `https://api2.frontapp.com/conversations/${remoteIdTicket}/comments`, JSON.stringify(dataBody), @@ -122,6 +124,7 @@ export class FrontService implements ICommentService { ); } + // Return response return { data: resp.data, message: 'Front comment created', diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts index 96b043209..c3ae0c4fd 100644 --- a/packages/api/src/ticketing/comment/services/front/mappers.ts +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -22,6 +22,9 @@ export class FrontCommentMapper implements ICommentMapper { ): Promise { const result: FrontCommentInput = { body: source.body, + // for author and attachments + // we let the Panora uuids on purpose (it will be modified in the given service on the fly where we'll retrieve the actual remote id for the given uuid!) + // either one must be passed author_id: source.user_id || source.contact_id, // for Front it must be a User attachments: source.attachments, }; @@ -54,35 +57,39 @@ export class FrontCommentMapper implements ICommentMapper { ): Promise { //map the front attachment to our unified version of attachment //unifying the original attachment object coming from Front - const unifiedObject = (await unify({ - sourceObject: comment.attachments, - targetType: TicketingObject.attachment, - providerName: 'front', - customFieldMappings: [], - })) as UnifiedAttachmentOutput[]; - - const user_id = await this.utils.getUserUuidFromRemoteId( - String(comment.id), - 'zendesk_tcg', - ); - let creator_type: string; let opts; - if (user_id) { - creator_type = 'user'; - opts = { user_id: user_id }; - } else { - const contact_id = await this.utils.getContactUuidFromRemoteId( - String(comment.id), + + if (comment.attachments && comment.attachments.length > 0) { + const unifiedObject = (await unify({ + sourceObject: comment.attachments, + targetType: TicketingObject.attachment, + providerName: 'front', + customFieldMappings: [], + })) as UnifiedAttachmentOutput[]; + + opts = { ...opts, attachments: unifiedObject }; + } + + if (comment.author.id) { + const user_id = await this.utils.getUserUuidFromRemoteId( + String(comment.author.id), 'zendesk_tcg', ); - creator_type = 'contact'; - opts = { user_id: contact_id }; + + if (user_id) { + // we must always fall here for Front + opts = { user_id: user_id, creator_type: 'user' }; + } else { + const contact_id = await this.utils.getContactUuidFromRemoteId( + String(comment.author.id), + 'zendesk_tcg', + ); + opts = { creator_type: 'contact', contact_id: contact_id }; + } } const res = { body: comment.body, - creator_type: creator_type, //it must be user - attachments: unifiedObject, ...opts, }; diff --git a/packages/api/src/ticketing/comment/services/zendesk/index.ts b/packages/api/src/ticketing/comment/services/zendesk/index.ts index 8a096a3f1..82aed3d51 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/index.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/index.ts @@ -39,73 +39,94 @@ export class ZendeskService implements ICommentService { }, }); + let dataBody = { + ticket: { + comment: commentData, + }, + }; + //first we retrieve the right author_id (it must be either a User or a Cntact) const author_id = commentData.author_id; //uuid of either a User or a Contact let author_data; - const res_user = await this.prisma.tcg_users.findUnique({ - where: { - id_tcg_user: String(author_id), - }, - select: { remote_id: true }, - }); - author_data = res_user; //it might be undefined but if it is i insert the right data below - if (!res_user) { - //try to see if there is a contact for this uuid - const res_contact = await this.prisma.tcg_contacts.findUnique({ + if (author_id) { + const res_user = await this.prisma.tcg_users.findUnique({ where: { - id_tcg_contact: String(author_id), + id_tcg_user: String(author_id), }, select: { remote_id: true }, }); - if (!res_contact) { - throw new Error( - 'author_id is invalid, it must be a valid User or Contact', - ); + author_data = res_user; //it might be undefined but if it is i insert the right data below + + if (!res_user) { + //try to see if there is a contact for this uuid + const res_contact = await this.prisma.tcg_contacts.findUnique({ + where: { + id_tcg_contact: String(author_id), + }, + select: { remote_id: true }, + }); + if (!res_contact) { + throw new Error( + 'author_id is invalid, it must be a valid User or Contact', + ); + } + author_data = res_contact; } - author_data = res_contact; + + const finalData = { + ticket: { + comment: { + ...commentData, + author_id: author_data.remote_id, + }, + }, + }; + dataBody = finalData; } // We must fetch tokens from zendesk with the commentData.uploads array of Attachment uuids const uuids = commentData.uploads; let uploads = []; - const uploadTokens = await Promise.all( - uuids.map(async (uuid) => { - const res = await this.prisma.tcg_attachments.findUnique({ - where: { - id_tcg_attachment: uuid, - }, - }); - if (!res) - throw new Error(`tcg_attachment not found for uuid ${uuid}`); + if (uuids && uuids.length > 0) { + await Promise.all( + uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) + throw new Error(`tcg_attachment not found for uuid ${uuid}`); - //TODO:; fetch the right file from AWS s3 - const s3File = ''; - const url = `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/uploads.json?filename=${ - res.file_name - }`; + //TODO:; fetch the right file from AWS s3 + const s3File = ''; + const url = `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/uploads.json?filename=${ + res.file_name + }`; - const resp = await axios.get(url, { - headers: { - 'Content-Type': 'image/png', //TODO: get the right content-type given a file name extension - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, + const resp = await axios.get(url, { + headers: { + 'Content-Type': 'image/png', //TODO: get the right content-type given a file name extension + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + uploads = [...uploads, resp.data.upload.token]; + }), + ); + const finalData = { + ticket: { + comment: { + ...commentData, + uploads: uploads, }, - }); - uploads = [...uploads, resp.data.upload.token]; - }), - ); - const finalData = { - ...commentData, - author_id: author_data.remote_id, - uploads: uploads, - }; - const dataBody = { - ticket: { - comment: finalData, - }, - }; + }, + }; + dataBody = finalData; + } + //to add a comment on Zendesk you must update a ticket using the Ticket API const resp = await axios.put( `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tickets/${remoteIdTicket}.json`, @@ -119,8 +140,11 @@ export class ZendeskService implements ICommentService { }, }, ); + const pre_res = resp.data.audit.events.find((obj) => + obj.hasOwnProperty('body'), + ); return { - data: resp.data, + data: pre_res, message: 'Zendesk comment created', statusCode: 201, }; diff --git a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts index 990258a31..cf9dfc4dc 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts @@ -25,7 +25,9 @@ export class ZendeskCommentMapper implements ICommentMapper { public: !source.is_private, author_id: source.user_id ? parseInt(source.user_id) - : parseInt(source.contact_id), // either one must be passed + : parseInt(source.contact_id), + // we let the Panora uuids on purpose (it will be modified in the given service on the fly where we'll retrieve the actual remote id for the given uuid!) + // either one must be passed type: 'Comment', uploads: source.attachments, //we let the array of uuids on purpose (it will be modified in the given service on the fly!) }; @@ -57,36 +59,41 @@ export class ZendeskCommentMapper implements ICommentMapper { remote_id: string; }[], ): Promise { - const unifiedObject = (await unify({ - sourceObject: comment.attachments, - targetType: TicketingObject.attachment, - providerName: 'front', - customFieldMappings: [], - })) as UnifiedAttachmentOutput[]; - const user_id = await this.utils.getUserUuidFromRemoteId( - String(comment.id), - 'zendesk_tcg', - ); - let creator_type: string; let opts; - if (user_id) { - creator_type = 'user'; - opts = { user_id: user_id }; - } else { - const contact_id = await this.utils.getContactUuidFromRemoteId( - String(comment.id), + + if (comment.attachments && comment.attachments.length > 0) { + const unifiedObject = (await unify({ + sourceObject: comment.attachments, + targetType: TicketingObject.attachment, + providerName: 'zendesk_tcg', + customFieldMappings: [], + })) as UnifiedAttachmentOutput[]; + + opts = { ...opts, attachments: unifiedObject }; + } + + /*TODO: uncomment when test for sync of users/contacts is done as right now we dont have any real users nor contacts inside our db + if (comment.author_id) { + const user_id = await this.utils.getUserUuidFromRemoteId( + String(comment.author_id), 'zendesk_tcg', ); - creator_type = 'contact'; - opts = { user_id: contact_id }; - } + + if (user_id) { + opts = { user_id: user_id, creator_type: 'user' }; + } else { + const contact_id = await this.utils.getContactUuidFromRemoteId( + String(comment.author_id), + 'zendesk_tcg', + ); + opts = { creator_type: 'contact', contact_id: contact_id }; + } + }*/ const res = { body: comment.body || '', html_body: comment.html_body || '', is_private: !comment.public, - creator_type: creator_type, - attachments: unifiedObject, ...opts, }; diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index 10e1166e4..e405c13e0 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -208,20 +208,31 @@ export class SyncService implements OnModuleInit { ? { id_tcg_contact: comment.contact_id, } - : { + : comment.creator_type === 'user' + ? { id_tcg_user: comment.user_id, - }; + } + : {}; //case where nothing is passed for creator or a not authorized value; + if (existingComment) { // Update the existing comment - const data = { - body: comment.body, - html_body: comment.html_body, - is_private: comment.is_private, - creator_type: comment.creator_type, - id_tcg_ticket: id_ticket, + let data: any = { + id_tcg_ticket: comment.ticket_id, modified_at: new Date(), - ...opts, }; + if (comment.body) { + data = { ...data, body: comment.body }; + } + if (comment.html_body) { + data = { ...data, html_body: comment.html_body }; + } + if (comment.is_private) { + data = { ...data, is_private: comment.is_private }; + } + if (comment.creator_type) { + data = { ...data, creator_type: comment.creator_type }; + } + data = { ...data, ...opts }; const res = await this.prisma.tcg_comments.update({ where: { id_tcg_comment: existingComment.id_tcg_comment, @@ -233,20 +244,28 @@ export class SyncService implements OnModuleInit { } else { // Create a new comment this.logger.log('comment not exists'); - let data = { + let data: any = { id_tcg_comment: uuidv4(), - body: comment.body, - html_body: comment.html_body, - is_private: comment.is_private, created_at: new Date(), modified_at: new Date(), - creator_type: comment.creator_type, - id_tcg_ticket: id_ticket, + id_tcg_ticket: comment.ticket_id, id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, }; + if (comment.body) { + data = { ...data, body: comment.body }; + } + if (comment.html_body) { + data = { ...data, html_body: comment.html_body }; + } + if (comment.is_private) { + data = { ...data, is_private: comment.is_private }; + } + if (comment.creator_type) { + data = { ...data, creator_type: comment.creator_type }; + } data = { ...data, ...opts }; const res = await this.prisma.tcg_comments.create({ @@ -260,72 +279,74 @@ export class SyncService implements OnModuleInit { // we should already have at least initial data (as it must have been inserted by the end linked user before adding comment) // though we might sync comments that have been also directly been added to the provider without passing through Panora // in this case just create a new attachment row ! + if (comment.attachments && comment.attachments.length > 0) { + for (const attchmt of comment.attachments) { + let unique_ticketing_attachmt_id: string; - for (const attchmt of comment.attachments) { - let unique_ticketing_attachmt_id: string; - - const existingAttachmt = await this.prisma.tcg_attachments.findFirst({ - where: { - remote_platform: originSource, - id_linked_user: linkedUserId, - file_name: attchmt.file_name, - }, - }); + const existingAttachmt = + await this.prisma.tcg_attachments.findFirst({ + where: { + remote_platform: originSource, + id_linked_user: linkedUserId, + file_name: attchmt.file_name, + }, + }); - if (existingAttachmt) { - // Update the existing attachmt - const res = await this.prisma.tcg_attachments.update({ - where: { - id_tcg_attachment: existingAttachmt.id_tcg_attachment, - }, - data: { + if (existingAttachmt) { + // Update the existing attachmt + const res = await this.prisma.tcg_attachments.update({ + where: { + id_tcg_attachment: existingAttachmt.id_tcg_attachment, + }, + data: { + remote_id: attchmt.id, + file_url: attchmt.file_url, + id_tcg_comment: unique_ticketing_comment_id, + id_tcg_ticket: id_ticket, + modified_at: new Date(), + }, + }); + unique_ticketing_attachmt_id = res.id_tcg_attachment; + } else { + // Create a new attachment + this.logger.log('attchmt not exists'); + const data = { + id_tcg_attachment: uuidv4(), remote_id: attchmt.id, + file_name: attchmt.file_name, file_url: attchmt.file_url, id_tcg_comment: unique_ticketing_comment_id, - id_tcg_ticket: id_ticket, + created_at: new Date(), modified_at: new Date(), + uploader: linkedUserId, //TODO + id_tcg_ticket: id_ticket, + id_linked_user: linkedUserId, + remote_platform: originSource, + }; + const res = await this.prisma.tcg_attachments.create({ + data: data, + }); + unique_ticketing_attachmt_id = res.id_tcg_attachment; + } + + //TODO: insert remote_data in db i dont know the type of remote_data to extract the right source attachment object + /*await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_ticketing_attachmt_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_ticketing_attachmt_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), }, - }); - unique_ticketing_attachmt_id = res.id_tcg_attachment; - } else { - // Create a new comment - this.logger.log('attchmt not exists'); - const data = { - id_tcg_attachment: uuidv4(), - remote_id: attchmt.id, - file_name: attchmt.file_name, - file_url: attchmt.file_url, - id_tcg_comment: unique_ticketing_comment_id, - created_at: new Date(), - modified_at: new Date(), - uploader: linkedUserId, //TODO - id_tcg_ticket: id_ticket, - id_linked_user: linkedUserId, - remote_platform: originSource, - }; - const res = await this.prisma.tcg_attachments.create({ - data: data, - }); - unique_ticketing_attachmt_id = res.id_tcg_attachment; + });*/ } - - //TODO: insert remote_data in db i dont know the type of remote_data to extract the right source attachment object - /*await this.prisma.remote_data.upsert({ - where: { - ressource_owner_id: unique_ticketing_attachmt_id, - }, - create: { - id_remote_data: uuidv4(), - ressource_owner_id: unique_ticketing_attachmt_id, - format: 'json', - data: JSON.stringify(remote_data[i]), - created_at: new Date(), - }, - update: { - data: JSON.stringify(remote_data[i]), - created_at: new Date(), - }, - });*/ } //insert remote_data in db diff --git a/packages/api/src/ticketing/comment/types/mappingsTypes.ts b/packages/api/src/ticketing/comment/types/mappingsTypes.ts index 609276e10..a01408950 100644 --- a/packages/api/src/ticketing/comment/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/comment/types/mappingsTypes.ts @@ -7,16 +7,16 @@ const githubCommentMapper = new GithubCommentMapper(); const frontCommentMapper = new FrontCommentMapper(); export const commentUnificationMapping = { - zendesk: { - unify: zendeskCommentMapper.unify, + zendesk_tcg: { + unify: zendeskCommentMapper.unify.bind(zendeskCommentMapper), desunify: zendeskCommentMapper.desunify, }, front: { - unify: frontCommentMapper.unify, + unify: frontCommentMapper.unify.bind(frontCommentMapper), desunify: frontCommentMapper.desunify, }, github: { - unify: githubCommentMapper.unify, + unify: githubCommentMapper.unify.bind(githubCommentMapper), desunify: githubCommentMapper.desunify, }, }; diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index 914010230..088e70ca1 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -5,8 +5,6 @@ export class UnifiedCommentInput { body: string; html_body?: string; is_private?: boolean; - created_at?: Date; - modified_at?: Date; creator_type: 'user' | 'contact' | null | string; ticket_id?: string; // uuid of Ticket object contact_id?: string; // uuid of Contact object @@ -23,6 +21,7 @@ export class UnifiedCommentOutput { type: String, }) remote_id?: string; + remote_data?: Record; body: string; html_body?: string; is_private?: boolean; diff --git a/packages/api/src/ticketing/contact/services/contact.service.ts b/packages/api/src/ticketing/contact/services/contact.service.ts index 37a9eafc7..2077d6b94 100644 --- a/packages/api/src/ticketing/contact/services/contact.service.ts +++ b/packages/api/src/ticketing/contact/services/contact.service.ts @@ -16,7 +16,7 @@ export class ContactService { async getContact( id_ticketing_contact: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const contact = await this.prisma.tcg_contacts.findUnique({ where: { @@ -58,9 +58,7 @@ export class ContactService { field_mappings: field_mappings, }; - let res: ContactResponse = { - contacts: [unifiedContact], - }; + let res: UnifiedContactOutput = unifiedContact; if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ @@ -72,7 +70,7 @@ export class ContactService { res = { ...res, - remote_data: [remote_data], + remote_data: remote_data, }; } @@ -86,12 +84,12 @@ export class ContactService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const contacts = await this.prisma.tcg_contacts.findMany({ where: { - remote_id: integrationId.toLowerCase(), + remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); @@ -134,27 +132,22 @@ export class ContactService { }), ); - let res: ContactResponse = { - contacts: unifiedContacts, - }; + let res: UnifiedContactOutput[] = unifiedContacts; if (remote_data) { - const remote_array_data: Record[] = await Promise.all( - contacts.map(async (contact) => { + const remote_array_data: UnifiedContactOutput[] = await Promise.all( + res.map(async (contact) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: contact.id_tcg_contact, + ressource_owner_id: contact.id, }, }); const remote_data = JSON.parse(resp.data); - return remote_data; + return { ...contact, remote_data }; }), ); - res = { - ...res, - remote_data: remote_array_data, - }; + res = remote_array_data; } const event = await this.prisma.events.create({ data: { diff --git a/packages/api/src/ticketing/contact/sync/sync.service.ts b/packages/api/src/ticketing/contact/sync/sync.service.ts index 0fdaba976..4985a6aed 100644 --- a/packages/api/src/ticketing/contact/sync/sync.service.ts +++ b/packages/api/src/ticketing/contact/sync/sync.service.ts @@ -35,7 +35,7 @@ export class SyncService implements OnModuleInit { } } - @Cron('*/20 * * * *') + //@Cron('*/20 * * * *') //function used by sync worker which populate our tcg_contacts table //its role is to fetch all contacts from providers 3rd parties and save the info inside our db async syncContacts() { diff --git a/packages/api/src/ticketing/contact/types/mappingsTypes.ts b/packages/api/src/ticketing/contact/types/mappingsTypes.ts index 7bd112f89..96f7d9a85 100644 --- a/packages/api/src/ticketing/contact/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/contact/types/mappingsTypes.ts @@ -7,7 +7,7 @@ const frontContactMapper = new FrontContactMapper(); const githubContactMapper = new GithubContactMapper(); export const accountUnificationMapping = { - zendesk: { + zendesk_tcg: { unify: zendeskContactMapper.unify, desunify: zendeskContactMapper.desunify, }, diff --git a/packages/api/src/ticketing/contact/types/model.unified.ts b/packages/api/src/ticketing/contact/types/model.unified.ts index 1db24f6c2..20aa78319 100644 --- a/packages/api/src/ticketing/contact/types/model.unified.ts +++ b/packages/api/src/ticketing/contact/types/model.unified.ts @@ -9,4 +9,5 @@ export class UnifiedContactInput { export class UnifiedContactOutput extends UnifiedContactInput { id?: string; remote_id?: string; + remote_data?: Record; } diff --git a/packages/api/src/ticketing/tag/services/tag.service.ts b/packages/api/src/ticketing/tag/services/tag.service.ts index cfdfbb032..e39262ee8 100644 --- a/packages/api/src/ticketing/tag/services/tag.service.ts +++ b/packages/api/src/ticketing/tag/services/tag.service.ts @@ -16,7 +16,7 @@ export class TagService { async getTag( id_ticketing_tag: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const tag = await this.prisma.tcg_tags.findUnique({ where: { @@ -55,9 +55,7 @@ export class TagService { field_mappings: field_mappings, }; - let res: TagResponse = { - tags: [unifiedTag], - }; + let res: UnifiedTagOutput = unifiedTag; if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ @@ -69,7 +67,7 @@ export class TagService { res = { ...res, - remote_data: [remote_data], + remote_data: remote_data, }; } @@ -83,12 +81,12 @@ export class TagService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const tags = await this.prisma.tcg_tags.findMany({ where: { - remote_id: integrationId.toLowerCase(), + remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); @@ -128,27 +126,22 @@ export class TagService { }), ); - let res: TagResponse = { - tags: unifiedTags, - }; + let res: UnifiedTagOutput[] = unifiedTags; if (remote_data) { - const remote_array_data: Record[] = await Promise.all( - tags.map(async (tag) => { + const remote_array_data: UnifiedTagOutput[] = await Promise.all( + res.map(async (tag) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: tag.id_tcg_tag, + ressource_owner_id: tag.id, }, }); const remote_data = JSON.parse(resp.data); - return remote_data; + return { ...tag, remote_data }; }), ); - res = { - ...res, - remote_data: remote_array_data, - }; + res = remote_array_data; } const event = await this.prisma.events.create({ data: { diff --git a/packages/api/src/ticketing/tag/sync/sync.service.ts b/packages/api/src/ticketing/tag/sync/sync.service.ts index 47797b90c..0fc88060d 100644 --- a/packages/api/src/ticketing/tag/sync/sync.service.ts +++ b/packages/api/src/ticketing/tag/sync/sync.service.ts @@ -35,7 +35,7 @@ export class SyncService implements OnModuleInit { } } - @Cron('*/20 * * * *') + //@Cron('*/20 * * * *') //function used by sync worker which populate our tcg_tags table //its role is to fetch all tags from providers 3rd parties and save the info inside our db async syncTags() { diff --git a/packages/api/src/ticketing/tag/types/mappingsTypes.ts b/packages/api/src/ticketing/tag/types/mappingsTypes.ts index 010647ab5..b098ba23a 100644 --- a/packages/api/src/ticketing/tag/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/tag/types/mappingsTypes.ts @@ -7,7 +7,7 @@ const frontTagMapper = new FrontTagMapper(); const githubTagMapper = new GithubTagMapper(); export const tagUnificationMapping = { - zendesk: { + zendesk_tcg: { unify: zendeskTagMapper.unify, desunify: zendeskTagMapper.desunify, }, diff --git a/packages/api/src/ticketing/tag/types/model.unified.ts b/packages/api/src/ticketing/tag/types/model.unified.ts index 29bd24465..ed5005e5d 100644 --- a/packages/api/src/ticketing/tag/types/model.unified.ts +++ b/packages/api/src/ticketing/tag/types/model.unified.ts @@ -6,4 +6,5 @@ export class UnifiedTagInput { export class UnifiedTagOutput extends UnifiedTagInput { id?: string; remote_id?: string; + remote_data?: Record; } diff --git a/packages/api/src/ticketing/team/services/team.service.ts b/packages/api/src/ticketing/team/services/team.service.ts index 669851f43..d55d3ab5e 100644 --- a/packages/api/src/ticketing/team/services/team.service.ts +++ b/packages/api/src/ticketing/team/services/team.service.ts @@ -16,7 +16,7 @@ export class TeamService { async getTeam( id_ticketing_team: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const team = await this.prisma.tcg_teams.findUnique({ where: { @@ -56,9 +56,7 @@ export class TeamService { field_mappings: field_mappings, }; - let res: TeamResponse = { - teams: [unifiedTeam], - }; + let res: UnifiedTeamOutput = unifiedTeam; if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ @@ -70,7 +68,7 @@ export class TeamService { res = { ...res, - remote_data: [remote_data], + remote_data: remote_data, }; } @@ -84,13 +82,13 @@ export class TeamService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const teams = await this.prisma.tcg_teams.findMany({ where: { - remote_id: integrationId.toLowerCase(), + remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); @@ -131,27 +129,22 @@ export class TeamService { }), ); - let res: TeamResponse = { - teams: unifiedTeams, - }; + let res: UnifiedTeamOutput[] = unifiedTeams; if (remote_data) { - const remote_array_data: Record[] = await Promise.all( - teams.map(async (team) => { + const remote_array_data: UnifiedTeamOutput[] = await Promise.all( + res.map(async (team) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: team.id_tcg_team, + ressource_owner_id: team.id, }, }); const remote_data = JSON.parse(resp.data); - return remote_data; + return { ...team, remote_data }; }), ); - res = { - ...res, - remote_data: remote_array_data, - }; + res = remote_array_data; } const event = await this.prisma.events.create({ data: { diff --git a/packages/api/src/ticketing/team/sync/sync.service.ts b/packages/api/src/ticketing/team/sync/sync.service.ts index d078dba82..8810127eb 100644 --- a/packages/api/src/ticketing/team/sync/sync.service.ts +++ b/packages/api/src/ticketing/team/sync/sync.service.ts @@ -35,7 +35,7 @@ export class SyncService implements OnModuleInit { } } - @Cron('*/20 * * * *') + //@Cron('*/20 * * * *') //function used by sync worker which populate our tcg_teams table //its role is to fetch all teams from providers 3rd parties and save the info inside our db async syncTeams() { diff --git a/packages/api/src/ticketing/team/types/mappingsTypes.ts b/packages/api/src/ticketing/team/types/mappingsTypes.ts index 06240c4f8..b43fbada8 100644 --- a/packages/api/src/ticketing/team/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/team/types/mappingsTypes.ts @@ -7,7 +7,7 @@ const frontTeamMapper = new FrontTeamMapper(); const githubTeamMapper = new GithubTeamMapper(); export const teamUnificationMapping = { - zendesk: { + zendesk_tcg: { unify: zendeskTeamMapper.unify, desunify: zendeskTeamMapper.desunify, }, diff --git a/packages/api/src/ticketing/team/types/model.unified.ts b/packages/api/src/ticketing/team/types/model.unified.ts index 7ce74381a..f823be0d7 100644 --- a/packages/api/src/ticketing/team/types/model.unified.ts +++ b/packages/api/src/ticketing/team/types/model.unified.ts @@ -7,4 +7,5 @@ export class UnifiedTeamInput { export class UnifiedTeamOutput extends UnifiedTeamInput { id?: string; remote_id?: string; + remote_data?: Record; } diff --git a/packages/api/src/ticketing/ticket/services/front/index.ts b/packages/api/src/ticketing/ticket/services/front/index.ts index 15851c4ac..b2d2415d9 100644 --- a/packages/api/src/ticketing/ticket/services/front/index.ts +++ b/packages/api/src/ticketing/ticket/services/front/index.ts @@ -120,14 +120,16 @@ export class FrontService implements ITicketService { } //now we can add tags and/or custom fields to the conversation we just created - if ((tags && tags.length > 0) || custom_fields) { - const data = { + if (tags && tags.length > 0) { + let final: any = { tag_ids: tags, - custom_fields: custom_fields, }; + if (custom_fields) { + final = { ...final, custom_fields: custom_fields }; + } const tag_resp = await axios.patch( `https://api2.frontapp.com/conversations/${resp.data.id}`, - JSON.stringify(data), + JSON.stringify(final), { headers: { 'Content-Type': 'application/json', diff --git a/packages/api/src/ticketing/ticket/services/front/mappers.ts b/packages/api/src/ticketing/ticket/services/front/mappers.ts index 6fc25278c..0ef897aba 100644 --- a/packages/api/src/ticketing/ticket/services/front/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/front/mappers.ts @@ -16,7 +16,7 @@ export class FrontTicketMapper implements ITicketMapper { remote_id: string; }[], ): Promise { - const result: FrontTicketInput = { + let result: FrontTicketInput = { type: 'discussion', // Assuming 'discussion' as a default type for Front conversations subject: source.name, teammate_ids: source.assigned_to, @@ -28,9 +28,28 @@ export class FrontTicketMapper implements ITicketMapper { : source.comment.contact_id, attachments: source.comment.attachments, }, - tags: source.tags, }; + if (source.assigned_to && source.assigned_to.length > 0) { + const res: string[] = []; + for (const assignee of source.assigned_to) { + res.push( + await this.utils.getAsigneeRemoteIdFromUserUuid(assignee, 'front'), + ); + } + result = { + ...result, + teammate_ids: res, + }; + } + + if (source.tags) { + result = { + ...result, + tags: source.tags, + }; + } + if (customFieldMappings && source.field_mappings) { for (const fieldMapping of source.field_mappings) { for (const key in fieldMapping) { @@ -47,40 +66,57 @@ export class FrontTicketMapper implements ITicketMapper { return result; } - unify( + async unify( source: FrontTicketOutput | FrontTicketOutput[], customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedTicketOutput | UnifiedTicketOutput[] { + ): Promise { // If the source is not an array, convert it to an array for mapping const sourcesArray = Array.isArray(source) ? source : [source]; - return sourcesArray.map((ticket) => - this.mapSingleTicketToUnified(ticket, customFieldMappings), + return Promise.all( + sourcesArray.map((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ), ); } - private mapSingleTicketToUnified( + private async mapSingleTicketToUnified( ticket: FrontTicketOutput, customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedTicketOutput { + ): Promise { const field_mappings = customFieldMappings?.map((mapping) => ({ [mapping.slug]: ticket.custom_fields?.[mapping.remote_id], })); + let opts: any; + + if (ticket.assignee) { + //fetch the right assignee uuid from remote id + const user_id = await this.utils.getUserUuidFromRemoteId( + String(ticket.assignee.id), + 'front', + ); + if (user_id) { + opts = { assigned_to: [user_id] }; + } else { + throw new Error('user id not found for this ticket'); + } + } + const unifiedTicket: UnifiedTicketOutput = { name: ticket.subject, status: ticket.status, description: ticket.subject, // todo: ? due_date: new Date(ticket.created_at), // todo ? tags: ticket.tags?.map((tag) => tag.name), - assigned_to: ticket.assignee ? [ticket.assignee.email] : undefined, //TODO: it must be a uuid of a user object field_mappings: field_mappings, + ...opts, }; return unifiedTicket; diff --git a/packages/api/src/ticketing/ticket/services/github/mappers.ts b/packages/api/src/ticketing/ticket/services/github/mappers.ts index c4149c025..bf9da57fb 100644 --- a/packages/api/src/ticketing/ticket/services/github/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/github/mappers.ts @@ -16,13 +16,13 @@ export class GithubTicketMapper implements ITicketMapper { return; } - unify( + async unify( source: GithubTicketOutput | GithubTicketOutput[], customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedTicketOutput | UnifiedTicketOutput[] { + ): Promise { return; } } diff --git a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts index c6a45d2b7..6fb5e3ea9 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts @@ -37,13 +37,13 @@ export class HubspotTicketMapper implements ITicketMapper { return result; } - unify( + async unify( source: HubspotTicketOutput | HubspotTicketOutput[], customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedTicketOutput | UnifiedTicketOutput[] { + ): Promise { // If the source is not an array, convert it to an array for mapping const sourcesArray = Array.isArray(source) ? source : [source]; diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index dd1007331..52edcef97 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -34,7 +34,7 @@ export class TicketService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const responses = await Promise.all( unifiedTicketData.map((unifiedData) => @@ -46,16 +46,7 @@ export class TicketService { ), ), ); - - const allTickets = responses.flatMap((response) => response.tickets); - const allRemoteData = responses.flatMap( - (response) => response.remote_data || [], - ); - - return { - tickets: allTickets, - remote_data: allRemoteData, - }; + return responses; } catch (error) { handleServiceError(error, this.logger); } @@ -66,7 +57,7 @@ export class TicketService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const linkedUser = await this.prisma.linked_users.findUnique({ where: { @@ -162,46 +153,83 @@ export class TicketService { if (existingTicket) { // Update the existing ticket + let data: any = { + id_tcg_ticket: uuidv4(), + modified_at: new Date(), + }; + if (target_ticket.name) { + data = { ...data, name: target_ticket.name }; + } + if (target_ticket.status) { + data = { ...data, status: target_ticket.status }; + } + if (target_ticket.description) { + data = { ...data, description: target_ticket.description }; + } + if (target_ticket.type) { + data = { ...data, ticket_type: target_ticket.type }; + } + if (target_ticket.tags) { + data = { ...data, tags: target_ticket.tags }; + } + if (target_ticket.priority) { + data = { ...data, priority: target_ticket.priority }; + } + if (target_ticket.completed_at) { + data = { ...data, completed_at: target_ticket.completed_at }; + } + if (target_ticket.assigned_to) { + data = { ...data, assigned_to: target_ticket.assigned_to }; + } + /* + parent_ticket: target_ticket.parent_ticket || 'd', + */ const res = await this.prisma.tcg_tickets.update({ where: { id_tcg_ticket: existingTicket.id_tcg_ticket, }, - data: { - name: target_ticket.name || '', - status: target_ticket.status || '', - description: target_ticket.description || '', - due_date: target_ticket.due_date || '', - ticket_type: target_ticket.type || '', - parent_ticket: target_ticket.parent_ticket || '', - tags: target_ticket.tags || [], - completed_at: target_ticket.completed_at || '', - priority: target_ticket.priority || '', - assigned_to: target_ticket.assigned_to || [], - modified_at: new Date(), - }, + data: data, }); unique_ticketing_ticket_id = res.id_tcg_ticket; } else { // Create a new ticket this.logger.log('not existing ticket ' + target_ticket.name); - const data = { + + let data: any = { id_tcg_ticket: uuidv4(), - name: target_ticket.name || '', - status: target_ticket.status || '', - description: target_ticket.description || '', - due_date: target_ticket.due_date || '', - ticket_type: target_ticket.type || '', - parent_ticket: target_ticket.parent_ticket || '', - tags: target_ticket.tags || [], - completed_at: target_ticket.completed_at || '', - priority: target_ticket.priority || '', - assigned_to: target_ticket.assigned_to || [], created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, remote_platform: integrationId, }; + if (target_ticket.name) { + data = { ...data, name: target_ticket.name }; + } + if (target_ticket.status) { + data = { ...data, status: target_ticket.status }; + } + if (target_ticket.description) { + data = { ...data, description: target_ticket.description }; + } + if (target_ticket.type) { + data = { ...data, ticket_type: target_ticket.type }; + } + if (target_ticket.tags) { + data = { ...data, tags: target_ticket.tags }; + } + if (target_ticket.priority) { + data = { ...data, priority: target_ticket.priority }; + } + if (target_ticket.completed_at) { + data = { ...data, completed_at: target_ticket.completed_at }; + } + if (target_ticket.assigned_to) { + data = { ...data, assigned_to: target_ticket.assigned_to }; + } + /* + parent_ticket: target_ticket.parent_ticket || 'd', + */ const res = await this.prisma.tcg_tickets.create({ data: data, @@ -270,7 +298,6 @@ export class TicketService { }); } - ///// const result_ticket = await this.getTicket( unique_ticketing_ticket_id, remote_data, @@ -291,7 +318,7 @@ export class TicketService { }, }); await this.webhook.handleWebhook( - result_ticket.tickets, + result_ticket, 'ticketing.ticket.created', linkedUser.id_project, event.id_event, @@ -306,7 +333,7 @@ export class TicketService { async getTicket( id_ticketing_ticket: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const ticket = await this.prisma.tcg_tickets.findUnique({ where: { @@ -354,8 +381,8 @@ export class TicketService { field_mappings: field_mappings, }; - let res: TicketResponse = { - tickets: [unifiedTicket], + let res: UnifiedTicketOutput = { + ...unifiedTicket, }; if (remote_data) { @@ -368,7 +395,7 @@ export class TicketService { res = { ...res, - remote_data: [remote_data], + remote_data: remote_data, }; } @@ -382,12 +409,12 @@ export class TicketService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const tickets = await this.prisma.tcg_tickets.findMany({ where: { - remote_id: integrationId.toLowerCase(), + remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, /* TODO: only if params @@ -440,27 +467,24 @@ export class TicketService { }), ); - let res: TicketResponse = { - tickets: unifiedTickets, - }; - + let res: UnifiedTicketOutput[] = unifiedTickets; if (remote_data) { - const remote_array_data: Record[] = await Promise.all( - tickets.map(async (ticket) => { + const remote_array_data: UnifiedTicketOutput[] = await Promise.all( + res.map(async (ticket) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: ticket.id_tcg_ticket, + ressource_owner_id: ticket.id, }, }); - const remote_data = JSON.parse(resp.data); - return remote_data; + //TODO: + let remote_data: any; + if (resp && resp.data) { + remote_data = JSON.parse(resp.data); + } + return { ...ticket, remote_data }; }), ); - - res = { - ...res, - remote_data: remote_array_data, - }; + res = remote_array_data; } const event = await this.prisma.events.create({ diff --git a/packages/api/src/ticketing/ticket/services/zendesk/index.ts b/packages/api/src/ticketing/ticket/services/zendesk/index.ts index 918c6b8c7..6585e97e1 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/index.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/index.ts @@ -39,44 +39,52 @@ export class ZendeskService implements ITicketService { provider_slug: 'zendesk_tcg', }, }); - + let dataBody = { + ticket: ticketData, + }; // We must fetch tokens from zendesk with the commentData.uploads array of Attachment uuids const uuids = ticketData.comment.uploads; let uploads = []; - uuids.map(async (uuid) => { - const res = await this.prisma.tcg_attachments.findUnique({ - where: { - id_tcg_attachment: uuid, - }, - }); - if (!res) throw new Error(`tcg_attachment not found for uuid ${uuid}`); + if (uuids && uuids.length > 0) { + await Promise.all( + uuids.map(async (uuid) => { + const res = await this.prisma.tcg_attachments.findUnique({ + where: { + id_tcg_attachment: uuid, + }, + }); + if (!res) + throw new Error(`tcg_attachment not found for uuid ${uuid}`); - //TODO:; fetch the right file from AWS s3 - const s3File = ''; - const url = `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/uploads.json?filename=${ - res.file_name - }`; + //TODO:; fetch the right file from AWS s3 + const s3File = ''; + const url = `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/uploads.json?filename=${ + res.file_name + }`; - const resp = await axios.get(url, { - headers: { - 'Content-Type': 'image/png', //TODO: get the right content-type given a file name extension - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, + const resp = await axios.get(url, { + headers: { + 'Content-Type': 'image/png', //TODO: get the right content-type given a file name extension + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + uploads = [...uploads, resp.data.upload.token]; + }), + ); + const finalData = { + ...ticketData, + comment: { + ...ticketData.comment, + uploads: uploads, }, - }); - uploads = [...uploads, resp.data.upload.token]; - }); - const finalData = { - ...ticketData, - comment: { - ...ticketData.comment, - uploads: uploads, - }, - }; - const dataBody = { - ticket: finalData, - }; + }; + dataBody = { + ticket: finalData, + }; + } + const resp = await axios.post( `https://${this.env.getZendeskTicketingSubdomain()}.zendesk.com/api/v2/tickets.json`, JSON.stringify(dataBody), @@ -90,7 +98,7 @@ export class ZendeskService implements ITicketService { }, ); return { - data: resp.data, + data: resp.data.ticket, message: 'Zendesk ticket created', statusCode: 201, }; diff --git a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts index 0d019b397..0ad5b254a 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts @@ -16,30 +16,62 @@ export class ZendeskTicketMapper implements ITicketMapper { remote_id: string; }[], ): Promise { - const result: ZendeskTicketInput = { - assignee_email: await this.utils.getAssigneeMetadataFromUuid( - source.assigned_to?.[0], - ), // get the mail of the uuid + let result: ZendeskTicketInput = { description: source.description, - due_at: source.due_date?.toISOString(), - priority: source.priority as 'urgent' | 'high' | 'normal' | 'low', - status: source.status as - | 'new' - | 'open' - | 'pending' - | 'hold' - | 'solved' - | 'closed', + priority: 'high', + status: 'new', subject: source.name, - tags: source.tags, - type: source.type as 'problem' | 'incident' | 'question' | 'task', comment: { body: source.comment.body, - html_body: source.comment.html_body, - public: !source.comment.is_private, - uploads: source.comment.attachments, //fetch token attachments for this uuid + html_body: source.comment.html_body || null, + public: !source.comment.is_private || true, + uploads: source.comment.attachments, //fetch token attachments for this uuid, would be done on the fly in dest service }, }; + if (source.assigned_to && source.assigned_to.length > 0) { + result = { + ...result, + assignee_email: await this.utils.getAssigneeMetadataFromUuid( + source.assigned_to?.[0], + ), // get the mail of the uuid + }; + } + if (source.due_date) { + result = { + ...result, + due_at: source.due_date?.toISOString(), + }; + } + if (source.priority) { + result = { + ...result, + priority: source.priority as 'urgent' | 'high' | 'normal' | 'low', + }; + } + if (source.status) { + result = { + ...result, + status: source.status as + | 'new' + | 'open' + | 'pending' + | 'hold' + | 'solved' + | 'closed', + }; + } + if (source.tags) { + result = { + ...result, + tags: source.tags, + }; + } + if (source.type) { + result = { + ...result, + type: source.type as 'problem' | 'incident' | 'question' | 'task', + }; + } if (customFieldMappings && source.field_mappings) { let res: CustomField[] = []; @@ -59,28 +91,30 @@ export class ZendeskTicketMapper implements ITicketMapper { return result; } - unify( + async unify( source: ZendeskTicketOutput | ZendeskTicketOutput[], customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedTicketOutput | UnifiedTicketOutput[] { + ): Promise { if (!Array.isArray(source)) { return this.mapSingleTicketToUnified(source, customFieldMappings); } - return source.map((ticket) => - this.mapSingleTicketToUnified(ticket, customFieldMappings), + return Promise.all( + source.map((ticket) => + this.mapSingleTicketToUnified(ticket, customFieldMappings), + ), ); } - private mapSingleTicketToUnified( + private async mapSingleTicketToUnified( ticket: ZendeskTicketOutput, customFieldMappings?: { slug: string; remote_id: string; }[], - ): UnifiedTicketOutput { + ): Promise> { const field_mappings = customFieldMappings.reduce((acc, mapping) => { const customField = ticket.custom_fields.find( (field) => field.id === mapping.remote_id, @@ -90,6 +124,23 @@ export class ZendeskTicketMapper implements ITicketMapper { } return acc; }, [] as Record[]); + let opts: any; + + /* TODO: uncomment when test for sync of users/contacts is done as right now we dont have any real users nor contacts inside our db + + if (ticket.assignee_id) { + //fetch the right assignee uuid from remote id + const user_id = await this.utils.getUserUuidFromRemoteId( + String(ticket.assignee_id), + 'zendesk_tcg', + ); + if (user_id) { + opts = { assigned_to: [user_id] }; + } else { + //TODO: in future we must throw an error ? + //throw new Error('user id not found for this ticket'); + } + }*/ const unifiedTicket: UnifiedTicketOutput = { name: ticket.subject, @@ -101,8 +152,8 @@ export class ZendeskTicketMapper implements ITicketMapper { tags: ticket.tags, completed_at: new Date(ticket.updated_at), priority: ticket.priority, - assigned_to: [String(ticket.assignee_id)], field_mappings: field_mappings, + ...opts, }; return unifiedTicket; diff --git a/packages/api/src/ticketing/ticket/sync/sync.service.ts b/packages/api/src/ticketing/ticket/sync/sync.service.ts index a36363123..d2df76233 100644 --- a/packages/api/src/ticketing/ticket/sync/sync.service.ts +++ b/packages/api/src/ticketing/ticket/sync/sync.service.ts @@ -195,52 +195,90 @@ export class SyncService implements OnModuleInit { if (existingTicket) { // Update the existing ticket + let data: any = { + id_tcg_ticket: uuidv4(), + modified_at: new Date(), + }; + if (ticket.name) { + data = { ...data, name: ticket.name }; + } + if (ticket.status) { + data = { ...data, status: ticket.status }; + } + if (ticket.description) { + data = { ...data, description: ticket.description }; + } + if (ticket.type) { + data = { ...data, ticket_type: ticket.type }; + } + if (ticket.tags) { + data = { ...data, tags: ticket.tags }; + } + if (ticket.priority) { + data = { ...data, priority: ticket.priority }; + } + if (ticket.completed_at) { + data = { ...data, completed_at: ticket.completed_at }; + } + if (ticket.assigned_to) { + data = { ...data, assigned_to: ticket.assigned_to }; + } + /* + parent_ticket: ticket.parent_ticket || 'd', + */ const res = await this.prisma.tcg_tickets.update({ where: { id_tcg_ticket: existingTicket.id_tcg_ticket, }, - data: { - name: ticket.name || '', - status: ticket.status || '', - description: ticket.description || '', - due_date: ticket.due_date || '', - ticket_type: ticket.type || '', - parent_ticket: ticket.parent_ticket || '', - tags: ticket.tags || [], - completed_at: ticket.completed_at || '', - priority: ticket.priority || '', - assigned_to: ticket.assigned_to || [], - modified_at: new Date(), - }, + data: data, }); unique_ticketing_ticket_id = res.id_tcg_ticket; tickets_results = [...tickets_results, res]; } else { // Create a new ticket this.logger.log('not existing ticket ' + ticket.name); - const data = { + + let data: any = { id_tcg_ticket: uuidv4(), - name: ticket.name || '', - status: ticket.status || '', - description: ticket.description || '', - due_date: ticket.due_date || '', - ticket_type: ticket.type || '', - parent_ticket: ticket.parent_ticket || '', - tags: ticket.tags || [], - completed_at: ticket.completed_at || '', - priority: ticket.priority || '', - assigned_to: ticket.assigned_to || [], created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, remote_platform: originSource, }; + if (ticket.name) { + data = { ...data, name: ticket.name }; + } + if (ticket.status) { + data = { ...data, status: ticket.status }; + } + if (ticket.description) { + data = { ...data, description: ticket.description }; + } + if (ticket.type) { + data = { ...data, ticket_type: ticket.type }; + } + if (ticket.tags) { + data = { ...data, tags: ticket.tags }; + } + if (ticket.priority) { + data = { ...data, priority: ticket.priority }; + } + if (ticket.completed_at) { + data = { ...data, completed_at: ticket.completed_at }; + } + if (ticket.assigned_to) { + data = { ...data, assigned_to: ticket.assigned_to }; + } + /* + parent_ticket: ticket.parent_ticket || 'd', + */ + const res = await this.prisma.tcg_tickets.create({ data: data, }); - tickets_results = [...tickets_results, res]; unique_ticketing_ticket_id = res.id_tcg_ticket; + tickets_results = [...tickets_results, res]; } // check duplicate or existing values diff --git a/packages/api/src/ticketing/ticket/types/index.ts b/packages/api/src/ticketing/ticket/types/index.ts index 98d495abc..3fbb662ed 100644 --- a/packages/api/src/ticketing/ticket/types/index.ts +++ b/packages/api/src/ticketing/ticket/types/index.ts @@ -30,7 +30,7 @@ export interface ITicketMapper { slug: string; remote_id: string; }[], - ): UnifiedTicketOutput | UnifiedTicketOutput[]; + ): Promise; } export type Comment = { diff --git a/packages/api/src/ticketing/ticket/types/mappingsTypes.ts b/packages/api/src/ticketing/ticket/types/mappingsTypes.ts index 08e8b6c24..91d90131b 100644 --- a/packages/api/src/ticketing/ticket/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/ticket/types/mappingsTypes.ts @@ -9,20 +9,20 @@ const githubTicketMapper = new GithubTicketMapper(); const hubspotTicketMapper = new HubspotTicketMapper(); export const ticketUnificationMapping = { - zendesk: { - unify: zendeskTicketMapper.unify, + zendesk_tcg: { + unify: zendeskTicketMapper.unify.bind(zendeskTicketMapper), desunify: zendeskTicketMapper.desunify, }, front: { - unify: frontTicketMapper.unify, + unify: frontTicketMapper.unify.bind(frontTicketMapper), desunify: frontTicketMapper.desunify, }, github: { - unify: githubTicketMapper.unify, + unify: githubTicketMapper.unify.bind(githubTicketMapper), desunify: githubTicketMapper.desunify, }, hubspot: { - unify: hubspotTicketMapper.unify, + unify: hubspotTicketMapper.unify.bind(hubspotTicketMapper), desunify: hubspotTicketMapper.desunify, }, }; diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index f153dae60..133f8e564 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -26,4 +26,5 @@ export class UnifiedTicketOutput extends UnifiedTicketInput { type: String, }) remote_id?: string; + remote_data?: Record; } diff --git a/packages/api/src/ticketing/ticket/utils/index.ts b/packages/api/src/ticketing/ticket/utils/index.ts index 1b873df92..73b748e99 100644 --- a/packages/api/src/ticketing/ticket/utils/index.ts +++ b/packages/api/src/ticketing/ticket/utils/index.ts @@ -6,6 +6,42 @@ export class Utils { this.prisma = new PrismaClient(); } + async getUserUuidFromRemoteId(remote_id: string, remote_platform: string) { + try { + const res = await this.prisma.tcg_users.findFirst({ + where: { + remote_id: remote_id, + remote_platform: remote_platform, + }, + }); + if (!res) + throw new Error( + `tcg_user not found for remote_id ${remote_id} and integration ${remote_platform}`, + ); + return res.id_tcg_user; + } catch (error) { + throw new Error(error); + } + } + + async getAsigneeRemoteIdFromUserUuid(uuid: string, remote_platform: string) { + try { + const res = await this.prisma.tcg_users.findFirst({ + where: { + id_tcg_user: uuid, + remote_platform: remote_platform, + }, + }); + if (!res) + throw new Error( + `tcg_user not found for uuid ${uuid} and integration ${remote_platform}`, + ); + return res.remote_id; + } catch (error) { + throw new Error(error); + } + } + async getAssigneeMetadataFromUuid(uuid: string) { try { const res = await this.prisma.tcg_users.findUnique({ diff --git a/packages/api/src/ticketing/user/services/user.service.ts b/packages/api/src/ticketing/user/services/user.service.ts index 269d49ca7..91bc5c1ab 100644 --- a/packages/api/src/ticketing/user/services/user.service.ts +++ b/packages/api/src/ticketing/user/services/user.service.ts @@ -16,7 +16,7 @@ export class UserService { async getUser( id_ticketing_user: string, remote_data?: boolean, - ): Promise { + ): Promise { try { const user = await this.prisma.tcg_users.findUnique({ where: { @@ -57,9 +57,7 @@ export class UserService { field_mappings: field_mappings, }; - let res: UserResponse = { - users: [unifiedUser], - }; + let res: UnifiedUserOutput = unifiedUser; if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ @@ -71,7 +69,7 @@ export class UserService { res = { ...res, - remote_data: [remote_data], + remote_data: remote_data, }; } @@ -85,12 +83,12 @@ export class UserService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { //TODO: handle case where data is not there (not synced) or old synced const users = await this.prisma.tcg_users.findMany({ where: { - remote_id: integrationId.toLowerCase(), + remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); @@ -132,27 +130,22 @@ export class UserService { }), ); - let res: UserResponse = { - users: unifiedUsers, - }; + let res: UnifiedUserOutput[] = unifiedUsers; if (remote_data) { - const remote_array_data: Record[] = await Promise.all( - users.map(async (user) => { + const remote_array_data: UnifiedUserOutput[] = await Promise.all( + res.map(async (user) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: user.id_tcg_user, + ressource_owner_id: user.id, }, }); const remote_data = JSON.parse(resp.data); - return remote_data; + return { ...user, remote_data }; }), ); - res = { - ...res, - remote_data: remote_array_data, - }; + res = remote_array_data; } const event = await this.prisma.events.create({ diff --git a/packages/api/src/ticketing/user/sync/sync.service.ts b/packages/api/src/ticketing/user/sync/sync.service.ts index c8bf2428a..cfdce00a6 100644 --- a/packages/api/src/ticketing/user/sync/sync.service.ts +++ b/packages/api/src/ticketing/user/sync/sync.service.ts @@ -35,7 +35,7 @@ export class SyncService implements OnModuleInit { } } - @Cron('*/20 * * * *') + //@Cron('*/20 * * * *') //function used by sync worker which populate our tcg_users table //its role is to fetch all users from providers 3rd parties and save the info inside our db async syncUsers() { diff --git a/packages/api/src/ticketing/user/types/mappingsTypes.ts b/packages/api/src/ticketing/user/types/mappingsTypes.ts index 9a7bf0f37..c3d3a6c6b 100644 --- a/packages/api/src/ticketing/user/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/user/types/mappingsTypes.ts @@ -7,7 +7,7 @@ const frontUserMapper = new FrontUserMapper(); const githubUserMapper = new GithubUserMapper(); export const userUnificationMapping = { - zendesk: { + zendesk_tcg: { unify: zendeskUserMapper.unify, desunify: zendeskUserMapper.desunify, }, diff --git a/packages/api/src/ticketing/user/types/model.unified.ts b/packages/api/src/ticketing/user/types/model.unified.ts index d779e055d..97d61e2b5 100644 --- a/packages/api/src/ticketing/user/types/model.unified.ts +++ b/packages/api/src/ticketing/user/types/model.unified.ts @@ -8,4 +8,5 @@ export class UnifiedUserInput { export class UnifiedUserOutput extends UnifiedUserInput { id?: string; remote_id?: string; + remote_data?: Record; }