import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Cron, CronExpression } from '@nestjs/schedule'; import { Model } from 'mongoose'; 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; constructor( @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(); try { // 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), { email_address: email, status_if_new: 'subscribed', // cf. https://mailchimp.com/developer/marketing/api/list-members/add-or-update-list-member/ status: 'subscribed', }); // 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(); } }