Skip to content
Snippets Groups Projects
newsletter.service.ts 7.02 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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();
    
          // 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);
          }
        }
      }
    
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
      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();