From 2fd8d8d6f68715ba64b9560067176d17f64a4364 Mon Sep 17 00:00:00 2001 From: Emile Bex Date: Fri, 27 Oct 2023 14:55:47 +0200 Subject: [PATCH] [EN-6471] chore(newsletter): connect newsletter route to mailjet --- src/contacts/contacts.controller.ts | 4 +- .../mailjet/mailjet.service.ts | 229 +++++++++--------- .../mailjet/mailjet.types.ts | 67 +++-- .../mailjet/mailjet.utils.ts | 89 +++++++ tests/mocks.types.ts | 8 +- 5 files changed, 253 insertions(+), 144 deletions(-) create mode 100644 src/external-services/mailjet/mailjet.utils.ts diff --git a/src/contacts/contacts.controller.ts b/src/contacts/contacts.controller.ts index 595416b9..7dc73edd 100644 --- a/src/contacts/contacts.controller.ts +++ b/src/contacts/contacts.controller.ts @@ -80,8 +80,8 @@ export class ContactsController { @Post('newsletter') async addContactForNewsletter( @Body('email') email: string, - @Body('zone') zone?: AdminZone | AdminZone[], - @Body('status') status?: ContactStatus | ContactStatus[] + @Body('zone') zone?: AdminZone, + @Body('status') status?: ContactStatus ) { if (!email) { throw new BadRequestException(); diff --git a/src/external-services/mailjet/mailjet.service.ts b/src/external-services/mailjet/mailjet.service.ts index 1f0cb876..1e8c2c9a 100644 --- a/src/external-services/mailjet/mailjet.service.ts +++ b/src/external-services/mailjet/mailjet.service.ts @@ -1,157 +1,150 @@ import { Injectable } from '@nestjs/common'; -import _ from 'lodash'; import { connect, Email } from 'node-mailjet'; import { CustomContactParams, CustomMailParams, - MailjetError, - MailjetTemplates, + MailjetContactList, + MailjetContactTag, + MailjetContactTagNames, + MailjetCustomContact, + MailjetCustomResponse, + MailjetListActions, + MailjetListIds, } from './mailjet.types'; +import { createMail } from './mailjet.utils'; import SendParams = Email.SendParams; import SendParamsRecipient = Email.SendParamsRecipient; -const useCampaigns = process.env.MAILJET_CAMPAIGNS_ACTIVATED === 'true'; - @Injectable() export class MailjetService { - private send: Email.PostResource; - private contact: Email.PostResource; + private mailjetTransactional: Email.Client; + private mailjetNewsletter: Email.Client; constructor() { - const mailjetTransactional = connect( + this.mailjetTransactional = connect( `${process.env.MAILJET_PUB}`, `${process.env.MAILJET_SEC}` ); - this.send = mailjetTransactional.post('send', { - version: 'v3.1', - }); - - const mailjetNewsletter = connect( + this.mailjetNewsletter = connect( `${process.env.MAILJET_NEWSLETTER_PUB}`, `${process.env.MAILJET_NEWSLETTER_SEC}` ); - - this.contact = mailjetNewsletter.post('contact', { version: 'v3' }); - } - - createMail({ - toEmail, - replyTo, - subject, - text, - html, - variables, - templateId, - }: CustomMailParams) { - const recipients: SendParams['Messages'][number] = { - To: [], - Cc: [], - From: { Email: '' }, - }; - if (typeof toEmail === 'string') { - recipients.To = [{ Email: toEmail }]; - } else if (Array.isArray(toEmail)) { - recipients.To = toEmail.map((email) => { - return { Email: email }; - }); - } else if (typeof toEmail === 'object') { - const { to, cc, bcc } = toEmail; - if (cc) { - recipients.Cc = Array.isArray(cc) - ? cc.map((email) => { - return { Email: email }; - }) - : [{ Email: cc }]; - } - if (to) { - recipients.To = Array.isArray(to) - ? to.map((email) => { - return { Email: email }; - }) - : [{ Email: to }]; - } - if (bcc) { - recipients.Bcc = Array.isArray(bcc) - ? bcc.map((email) => { - return { Email: email }; - }) - : [{ Email: bcc }]; - } - } - - const content = templateId - ? { - Variables: { - siteLink: process.env.FRONT_URL, - ...variables, - }, - TemplateID: templateId, - CustomCampaign: useCampaigns - ? _.findKey(MailjetTemplates, (id) => { - return id === templateId; - }) - : undefined, - TemplateLanguage: true, - TemplateErrorReporting: { - Email: `${process.env.MAILJET_SUPPORT_EMAIL}`, - Name: `${process.env.MAILJET_FROM_NAME}`, - }, - } - : { - 'Text-part': text, - 'HTML-part': html, - }; - return { - From: { - Email: `${process.env.MAILJET_FROM_EMAIL}`, - Name: `${process.env.MAILJET_FROM_NAME}`, - }, - Subject: subject, - Headers: replyTo - ? { - 'Reply-To': replyTo, - } - : undefined, - ...recipients, - ...content, - }; } async sendMail(params: CustomMailParams | CustomMailParams[]) { const mailjetParams: SendParams = { Messages: [] }; if (Array.isArray(params)) { mailjetParams.Messages = params.map((p) => { - return this.createMail(p); + return createMail(p); }); } else { - mailjetParams.Messages = [this.createMail(params)]; + mailjetParams.Messages = [createMail(params)]; } - return this.send.request(mailjetParams); + return this.mailjetTransactional + .post('send', { + version: 'v3.1', + }) + .request(mailjetParams); } - createContact({ email /* status, zone */ }: CustomContactParams) { - const contacts: SendParamsRecipient = { + async findContact( + email: CustomContactParams['email'] + ): Promise { + const contact: SendParamsRecipient = { + Email: email.toLowerCase(), + }; + + const { + body: { Data: data }, + } = (await this.mailjetNewsletter + .get('contact', { version: 'v3' }) + .request(contact)) as MailjetCustomResponse; + + return data[0]; + } + + async createContact( + email: CustomContactParams['email'] + ): Promise { + const contact: SendParamsRecipient = { Email: email, }; - return contacts; + const { + body: { Data: data }, + } = (await this.mailjetNewsletter + .post('contact', { version: 'v3' }) + .request(contact)) as MailjetCustomResponse; + + return data[0]; + } + + async updateContactTags( + id: string, + { zone, status }: Pick + ) { + let dataToUpdate: { Data: MailjetContactTag[] } = { + Data: [ + { + Name: MailjetContactTagNames.NEWSLETTER, + Value: true, + }, + ], + }; + + if (zone) { + dataToUpdate = { + Data: [ + ...dataToUpdate.Data, + { + Name: MailjetContactTagNames.ZONE, + Value: zone, + }, + ], + }; + } + if (status) { + dataToUpdate = { + Data: [ + ...dataToUpdate.Data, + { + Name: MailjetContactTagNames.STATUS, + Value: status, + }, + ], + }; + } + + return this.mailjetNewsletter + .put('contactdata', { version: 'v3' }) + .id(id) + .request(dataToUpdate); + } + + async subscribeToNewsletterList(id: string) { + const dataToUpdate: { ContactsLists: MailjetContactList[] } = { + ContactsLists: [ + { + Action: MailjetListActions.FORCE, + ListID: MailjetListIds.NEWSLETTER, + }, + ], + }; + + return this.mailjetNewsletter + .post('contact', { version: 'v3' }) + .id(id) + .action('managecontactslists') + .request(dataToUpdate); } - async sendContact(params: CustomContactParams) { - const mailjetParams: SendParamsRecipient = this.createContact(params); - - try { - await this.contact.request(mailjetParams); - } catch (err) { - console.error(err); - // Check if not a duplicate value error, otherwise consider it as successful request - if ( - (err as MailjetError).statusCode !== 400 || - !(err as MailjetError).ErrorMessage.includes('MJ18') - ) { - throw err; - } + async sendContact({ email, zone, status }: CustomContactParams) { + let contact = await this.findContact(email); + if (!contact) { + contact = await this.createContact(email); } + await this.subscribeToNewsletterList(contact.ID); + await this.updateContactTags(contact.ID, { zone, status }); } } diff --git a/src/external-services/mailjet/mailjet.types.ts b/src/external-services/mailjet/mailjet.types.ts index 6cecb8a1..6b810bc8 100644 --- a/src/external-services/mailjet/mailjet.types.ts +++ b/src/external-services/mailjet/mailjet.types.ts @@ -1,4 +1,4 @@ -import { AdminZone, AdminZones } from 'src/utils/types'; +import { AdminZone } from 'src/utils/types'; export interface CustomMailParams { toEmail: @@ -19,8 +19,41 @@ export interface CustomMailParams { export interface CustomContactParams { email: string; - zone: AdminZone | AdminZone[]; - status: ContactStatus | ContactStatus[]; + zone: AdminZone; + status: ContactStatus; +} + +export const MailjetContactTagNames = { + NEWSLETTER: 'newsletter_linkedout', + ZONE: 'antenne_linkedout', + STATUS: 'profil_linkedout', +} as const; + +export type MailjetContactTagName = + typeof MailjetContactTagNames[keyof typeof MailjetContactTagNames]; + +export interface MailjetContactTag { + Name: MailjetContactTagName; + Value: string | boolean; +} + +export const MailjetListIds = { + NEWSLETTER: 2822632, +} as const; + +type MailjetListId = typeof MailjetListIds[keyof typeof MailjetListIds]; + +export const MailjetListActions = { + NO_FORCE: 'addnoforce', + FORCE: 'addforce', +} as const; + +export type MailjetListAction = + typeof MailjetListActions[keyof typeof MailjetListActions]; + +export interface MailjetContactList { + Action: MailjetListAction; + ListID: MailjetListId; } export const MailjetTemplates = { @@ -64,30 +97,22 @@ export type MailjetTemplate = typeof MailjetTemplates[MailjetTemplateKey]; export const ContactStatuses = { INDIVIDUAL: 'PARTICULIER', COMPANY: 'ENTREPRISE', - STRUCTURE: 'STRUCTURE_INSERTION', - CANDIDATE: 'CANDIDAT_POTENTIEL', + STRUCTURE: 'ASSOCIATION', + CANDIDATE: 'CANDIDAT', } as const; export type ContactStatus = typeof ContactStatuses[keyof typeof ContactStatuses]; -export const MailjetContactRegions: { [K in AdminZone]: string } = { - [AdminZones.LYON]: AdminZones.LYON.toLowerCase(), - [AdminZones.PARIS]: AdminZones.PARIS.toLowerCase(), - [AdminZones.LILLE]: AdminZones.LILLE.toLowerCase(), - [AdminZones.LORIENT]: AdminZones.LORIENT.toLowerCase(), - [AdminZones.RENNES]: AdminZones.RENNES.toLowerCase(), - [AdminZones.HZ]: AdminZones.HZ.toLowerCase(), -} as const; - -export const MailjetContactStatuses: { [K in ContactStatus]: string } = { - PARTICULIER: 'un-particulier', - ENTREPRISE: 'une-entreprise', - STRUCTURE_INSERTION: 'une-structure-d-insertion', - CANDIDAT_POTENTIEL: 'un-candidat-potentiel', -} as const; - export interface MailjetError { statusCode: number; ErrorMessage: string; } + +export interface MailjetCustomContact { + ID: string; +} + +export interface MailjetCustomResponse { + body: { Data: ReadonlyArray }; +} diff --git a/src/external-services/mailjet/mailjet.utils.ts b/src/external-services/mailjet/mailjet.utils.ts new file mode 100644 index 00000000..c6ca6794 --- /dev/null +++ b/src/external-services/mailjet/mailjet.utils.ts @@ -0,0 +1,89 @@ +import _ from 'lodash'; +import { Email } from 'node-mailjet'; +import { CustomMailParams, MailjetTemplates } from './mailjet.types'; +import SendParams = Email.SendParams; + +const useCampaigns = process.env.MAILJET_CAMPAIGNS_ACTIVATED === 'true'; + +export function createMail({ + toEmail, + replyTo, + subject, + text, + html, + variables, + templateId, +}: CustomMailParams) { + const recipients: SendParams['Messages'][number] = { + To: [], + Cc: [], + From: { Email: '' }, + }; + if (typeof toEmail === 'string') { + recipients.To = [{ Email: toEmail }]; + } else if (Array.isArray(toEmail)) { + recipients.To = toEmail.map((email) => { + return { Email: email }; + }); + } else if (typeof toEmail === 'object') { + const { to, cc, bcc } = toEmail; + if (cc) { + recipients.Cc = Array.isArray(cc) + ? cc.map((email) => { + return { Email: email }; + }) + : [{ Email: cc }]; + } + if (to) { + recipients.To = Array.isArray(to) + ? to.map((email) => { + return { Email: email }; + }) + : [{ Email: to }]; + } + if (bcc) { + recipients.Bcc = Array.isArray(bcc) + ? bcc.map((email) => { + return { Email: email }; + }) + : [{ Email: bcc }]; + } + } + + const content = templateId + ? { + Variables: { + siteLink: process.env.FRONT_URL, + ...variables, + }, + TemplateID: templateId, + CustomCampaign: useCampaigns + ? _.findKey(MailjetTemplates, (id) => { + return id === templateId; + }) + : undefined, + TemplateLanguage: true, + TemplateErrorReporting: { + Email: `${process.env.MAILJET_SUPPORT_EMAIL}`, + Name: `${process.env.MAILJET_FROM_NAME}`, + }, + } + : { + 'Text-part': text, + 'HTML-part': html, + }; + return { + From: { + Email: `${process.env.MAILJET_FROM_EMAIL}`, + Name: `${process.env.MAILJET_FROM_NAME}`, + }, + Subject: subject, + Headers: replyTo + ? { + 'Reply-To': replyTo, + } + : undefined, + ...recipients, + ...content, + }; +} diff --git a/tests/mocks.types.ts b/tests/mocks.types.ts index d55577dc..8bd337a3 100644 --- a/tests/mocks.types.ts +++ b/tests/mocks.types.ts @@ -101,8 +101,10 @@ export const SalesforceMocks: ProviderMock = { } as const; export const MailjetMock: ProviderMock = { - sendContact: jest.fn(), - createContact: jest.fn(), sendMail: jest.fn(), - createMail: jest.fn(), + findContact: jest.fn(), + createContact: jest.fn(), + updateContactTags: jest.fn(), + subscribeToNewsletterList: jest.fn(), + sendContact: jest.fn(), } as const;