Skip to content

Commit

Permalink
[EN-6471] chore(newsletter): connect newsletter route to mailjet
Browse files Browse the repository at this point in the history
  • Loading branch information
emile-bex committed Oct 27, 2023
1 parent e235708 commit 2fd8d8d
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 144 deletions.
4 changes: 2 additions & 2 deletions src/contacts/contacts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
229 changes: 111 additions & 118 deletions src/external-services/mailjet/mailjet.service.ts
Original file line number Diff line number Diff line change
@@ -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<MailjetCustomContact> {
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<MailjetCustomContact> {
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<CustomContactParams, 'zone' | 'status'>
) {
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 });
}
}
67 changes: 46 additions & 21 deletions src/external-services/mailjet/mailjet.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AdminZone, AdminZones } from 'src/utils/types';
import { AdminZone } from 'src/utils/types';

export interface CustomMailParams {
toEmail:
Expand All @@ -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 = {
Expand Down Expand Up @@ -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<MailjetCustomContact> };
}
Loading

0 comments on commit 2fd8d8d

Please sign in to comment.