Newer
Older
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cron, CronExpression } from '@nestjs/schedule';
import { md5 } from '../shared/utils';
import { IMailchimpSubscription } from './interface/mailchimp-subscription';
import { INewsletterSubscription } from './interface/newsletter-subscription.interface';
import { NewsletterSubscription } from './newsletter-subscription.schema';
import mailchimp = require('@mailchimp/mailchimp_marketing');
import { UsersService } from '../users/services/users.service';
@Injectable()
export class NewsletterService {
private readonly logger = new Logger(NewsletterService.name);
private LIST_ID = process.env.MC_LIST_ID;
@InjectModel(NewsletterSubscription.name) private newsletterSubscriptionModel: Model<INewsletterSubscription>,
private readonly userService: UsersService
) {
// Configure mailchimp client
mailchimp.setConfig({
apiKey: process.env.MC_API_KEY,
server: process.env.MC_SERVER,
});
}
@Cron(CronExpression.EVERY_DAY_AT_3AM)
public async updateNewsletterSubscription(): Promise<void> {
this.logger.debug('updateNewsletterSubscription');
const { total_items } = await mailchimp.lists.getListMembersInfo(this.LIST_ID);
const { members } = await mailchimp.lists.getListMembersInfo(this.LIST_ID, {
fields: ['members.email_address,members.id,members.status'],
count: total_items,
});
const memberToRemove = members.filter((user: IMailchimpSubscription) => user.status !== 'subscribed');
memberToRemove.forEach(async (member: IMailchimpSubscription) => {
const userSubscription = await this.findOne(member.email_address);
if (userSubscription) {
this.logger.log(`Remove subscription ${member.id}`);
userSubscription.deleteOne();
}
});
}
public async setMedNumTag(email: string, status: boolean) {
this.logger.debug(`newsletter MedNum tag: ${email} - ${status}`);
return mailchimp.lists
.updateListMemberTags(this.LIST_ID, md5(email), {
tags: [{ name: 'MedNum', status: status ? 'active' : 'inactive' }],
})
.then(() => this.logger.log(`newsletter MedNum set: ${email} - ${status}`))
.catch((e) => {
// email may not be found in mailchimp if the user hadn't subscribed to the newsletter
if (e.status === 404) {
this.logger.debug(`newsletter MedNum tag: user not found: ${email} - ${status}`);
} else {
this.logger.error(`newsletter MedNum tag error: ${e.status} - ${e}`);
}
});
}
public async newsletterSubscribe(email: string): Promise<NewsletterSubscription> {
this.logger.log(`newsletterSubscribe: ${email}`);
email = email.toLocaleLowerCase();
// Add or update list member (to be able to subscribe again a member who had already subscribed then unsubscribed)
// (the second parameter is the MD5 hash of the lowercase email, we actually don't need to maintain a mapping in newsletterSubscription : cf. https://mailchimp.com/developer/marketing/docs/methods-parameters/#path-parameters )
const member = await mailchimp.lists.setListMember(this.LIST_ID, md5(email), {
status_if_new: 'subscribed', // cf. https://mailchimp.com/developer/marketing/api/list-members/add-or-update-list-member/
// We may not be aware that the user has unsubscribed from the newsletter, so it is ok if it already exists in newsletterSubscription
let newsletterSubscription = await this.findOne(email);
if (!newsletterSubscription) {
newsletterSubscription = await this.newsletterSubscriptionModel.create({
email: email,
mailchimpId: member.id,
});
// Find user for this email to update his MedNum tag (user may not exist if it is a newsletter subscription from footer)
const user = await this.userService.findOne(email);
this.setMedNumTag(email, user?.job?.hasPersonalOffer);
return newsletterSubscription;
} catch (e) {
if (e.status === 400) {
if (e.response?.text?.includes('fake')) {
this.logger.log(`newsletterSubscribe: Fake or invalid email: ${email}`);
throw new HttpException('Fake or invalid email', HttpStatus.I_AM_A_TEAPOT);
} else {
// Example: email "has signed up to a lot of lists very recently; we're not allowing more signups for now"
this.logger.error(`newsletterSubscribe ${email}: ${JSON.stringify(e)}`);
throw new HttpException(JSON.parse(e.response?.text)?.detail, HttpStatus.BAD_REQUEST);
}
} else {
this.logger.error(`newsletterSubscribe ${email}: ${JSON.stringify(e)}`);
throw new HttpException('Subscribe error', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public async newsletterUnsubscribe(email: string): Promise<NewsletterSubscription> {
this.logger.log(`newsletterUnsubscribe: ${email}`);
email = email.toLocaleLowerCase();
const emailMd5Hashed = md5(email);
let newsletterSubscription = await this.findOne(email);
if (newsletterSubscription) {
newsletterSubscription = newsletterSubscription.deleteOne();
try {
const response = await mailchimp.lists.getListMember(this.LIST_ID, emailMd5Hashed);
if (response.status === 'unsubscribed') {
throw new HttpException('Email not found', HttpStatus.NOT_FOUND);
}
await mailchimp.lists.setListMember(this.LIST_ID, emailMd5Hashed, {
status: 'unsubscribed',
});
} catch (e) {
if (e.status === 404) {
throw new HttpException('Email not found', HttpStatus.NOT_FOUND);
} else {
this.logger.error(`newsletterUnsubscribe ${email}: ${JSON.stringify(e)}`);
throw new HttpException('Unsubscribe error', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
return newsletterSubscription;
public async findNewsletterSubscription(email: string): Promise<object> {
try {
// The actual subscription info is in mailchimp, we actually don't need to maintain a mapping in newsletterSubscription : cf. https://mailchimp.com/developer/marketing/docs/methods-parameters/#path-parameters
const mailchimpUser = await mailchimp.lists.getListMember(this.LIST_ID, md5(email));
return mailchimpUser.status === 'unsubscribed' ? null : { email };
} catch (e) {
if (e.status === 404) {
return null;
} else {
this.logger.error(`findNewsletterSubscription: ${e.status} - ${e}`);
throw new HttpException('find Newsletter Subscription', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
public async findOne(mail: string): Promise<INewsletterSubscription | undefined> {
this.logger.debug('findOne');
return this.newsletterSubscriptionModel.findOne({ email: mail }).exec();
}
public async findAll(): Promise<NewsletterSubscription[]> {
this.logger.debug('findAll');
return this.newsletterSubscriptionModel.find().exec();