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 000000000..697e340fb Binary files /dev/null and b/apps/webapp/public/providers/crm/zendesk_tcg.png differ 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; }