diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 3feefc99c2d1448c80bde58e026a0be4bff84858..becf1e4f0771e0c5260a305372abb3f90a1d5146 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -18,6 +18,8 @@ import { UsersService } from '../users/services/users.service'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { LoginDto } from './login-dto'; +import { NewsletterService } from '../newsletter/newsletter.service'; +import { NewsletterServiceMock } from '../../test/mock/services/newsletter.mock.service'; describe('AuthController', () => { let authController: AuthController; @@ -80,6 +82,10 @@ describe('AuthController', () => { provide: getModelToken('Job'), useValue: Job, }, + { + provide: NewsletterService, + useClass: NewsletterServiceMock, + }, ], }).compile(); diff --git a/src/newsletter/newsletter.controller.spec.ts b/src/newsletter/newsletter.controller.spec.ts index 4f2e38748f2872166de6922732be5e5f74fa3182..e68c39a40054363c68129a721d93eb9857bd3801 100644 --- a/src/newsletter/newsletter.controller.spec.ts +++ b/src/newsletter/newsletter.controller.spec.ts @@ -5,9 +5,14 @@ import { ConfigurationModule } from '../configuration/configuration.module'; import { NewsletterSubscription } from './newsletter-subscription.schema'; import { NewsletterController } from './newsletter.controller'; import { NewsletterService } from './newsletter.service'; +import { UsersService } from '../users/services/users.service'; describe('NewsletterController', () => { let newsletterController: NewsletterController; + const mockUsersService = { + find: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ConfigurationModule, HttpModule], @@ -17,6 +22,10 @@ describe('NewsletterController', () => { provide: getModelToken('NewsletterSubscription'), useValue: NewsletterSubscription, }, + { + provide: UsersService, + useValue: mockUsersService, + }, ], controllers: [NewsletterController], }).compile(); diff --git a/src/newsletter/newsletter.module.ts b/src/newsletter/newsletter.module.ts index bda06051c896e0f92b4e0198f79983228df7f7da..d9db580620f7146b1832bc5642f9b2ac07e2a4fd 100644 --- a/src/newsletter/newsletter.module.ts +++ b/src/newsletter/newsletter.module.ts @@ -1,13 +1,15 @@ import { HttpModule } from '@nestjs/axios'; -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { NewsletterSubscription, NewsletterSubscriptionSchema } from './newsletter-subscription.schema'; import { NewsletterController } from './newsletter.controller'; import { NewsletterService } from './newsletter.service'; +import { UsersModule } from '../users/users.module'; @Module({ imports: [ MongooseModule.forFeature([{ name: NewsletterSubscription.name, schema: NewsletterSubscriptionSchema }]), HttpModule, + forwardRef(() => UsersModule), ], providers: [NewsletterService], exports: [NewsletterService], diff --git a/src/newsletter/newsletter.service.spec.ts b/src/newsletter/newsletter.service.spec.ts index db882fda85bbc202c4a688bdd41311153bb9809c..0e4b447a2d5a4838b8a96a9d560d5852581141b7 100644 --- a/src/newsletter/newsletter.service.spec.ts +++ b/src/newsletter/newsletter.service.spec.ts @@ -5,6 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INewsletterSubscription } from './interface/newsletter-subscription.interface'; import { NewsletterSubscription } from './newsletter-subscription.schema'; import { NewsletterService } from './newsletter.service'; +import { UsersService } from '../users/services/users.service'; // eslint-disable-next-line @typescript-eslint/no-var-requires const mailchimp = require('@mailchimp/mailchimp_marketing'); jest.mock('@mailchimp/mailchimp_marketing'); @@ -21,6 +22,10 @@ describe('NewsletterService', () => { find: jest.fn(), }; + const mockUsersService = { + findOne: jest.fn(), + }; + beforeEach(async () => { jest.resetModules(); // Most important - it clears the cache const module: TestingModule = await Test.createTestingModule({ @@ -31,6 +36,10 @@ describe('NewsletterService', () => { provide: getModelToken(NewsletterSubscription.name), useValue: mockNewsletterModel, }, + { + provide: UsersService, + useValue: mockUsersService, + }, ], }).compile(); @@ -55,6 +64,7 @@ describe('NewsletterService', () => { it('it should add subscription for email test2@test.com even if it exists', async () => { const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' } as INewsletterSubscription; mailchimp.lists.setListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' }); + mailchimp.lists.updateListMemberTags.mockResolvedValueOnce({}); jest .spyOn(newsletterService, 'findOne') .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => _doc); @@ -67,6 +77,7 @@ describe('NewsletterService', () => { const result = { email: 'test2@test.com' } as INewsletterSubscription; const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; mailchimp.lists.setListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' }); + mailchimp.lists.updateListMemberTags.mockResolvedValueOnce({}); jest .spyOn(newsletterService, 'findOne') .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) @@ -80,6 +91,7 @@ describe('NewsletterService', () => { const result = { email: 'test2@test.com' } as INewsletterSubscription; const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; mailchimp.lists.setListMember.mockRejectedValueOnce({ status: 400 }); + mailchimp.lists.updateListMemberTags.mockResolvedValueOnce({}); jest .spyOn(newsletterService, 'findOne') .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) @@ -98,6 +110,7 @@ describe('NewsletterService', () => { const result = { email: 'test2@test.com' } as INewsletterSubscription; const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; mailchimp.lists.setListMember.mockRejectedValueOnce({ status: 500 }); + mailchimp.lists.updateListMemberTags.mockResolvedValueOnce({}); jest .spyOn(newsletterService, 'findOne') .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) diff --git a/src/newsletter/newsletter.service.ts b/src/newsletter/newsletter.service.ts index 1af9ef47bd2bd5b9dbe987d54ae7cbb86e82a6c5..1203e197e9e8a91aa777706f6afb5a47a9bad468 100644 --- a/src/newsletter/newsletter.service.ts +++ b/src/newsletter/newsletter.service.ts @@ -7,13 +7,15 @@ 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> + @InjectModel(NewsletterSubscription.name) private newsletterSubscriptionModel: Model<INewsletterSubscription>, + private readonly userService: UsersService ) { // Configure mailchimp client mailchimp.setConfig({ @@ -41,8 +43,25 @@ export class NewsletterService { }); } + 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.debug(`newsletterSubscribe: ${email}`); + 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) @@ -61,9 +80,15 @@ export class NewsletterService { 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 && 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 { this.logger.error(`newsletterSubscribe ${email}: ${JSON.stringify(e)}`); @@ -73,7 +98,7 @@ export class NewsletterService { } public async newsletterUnsubscribe(email: string): Promise<NewsletterSubscription> { - this.logger.debug(`newsletterUnsubscribe: ${email}`); + this.logger.log(`newsletterUnsubscribe: ${email}`); email = email.toLocaleLowerCase(); const emailMd5Hashed = md5(email); diff --git a/src/personal-offers/personal-offers.module.ts b/src/personal-offers/personal-offers.module.ts index a86967cf7a37716c14416fb8ab963c8d1fd763eb..4243331bef6aedda83d0fd44de9bedc261cfd0f9 100644 --- a/src/personal-offers/personal-offers.module.ts +++ b/src/personal-offers/personal-offers.module.ts @@ -10,7 +10,7 @@ import { PersonalOffer, PersonalOfferSchema } from './schemas/personal-offer.sch imports: [ MongooseModule.forFeature([{ name: PersonalOffer.name, schema: PersonalOfferSchema }]), forwardRef(() => StructuresModule), - UsersModule, + forwardRef(() => UsersModule), ], controllers: [PersonalOffersController], exports: [PersonalOffersService], diff --git a/src/users/services/users.service.spec.ts b/src/users/services/users.service.spec.ts index ad83f66a6b2c0c271a7c7fdf2ce6524e9448eb1a..5cdd709bba03272f896b1a4116951cf7409f8d17 100644 --- a/src/users/services/users.service.spec.ts +++ b/src/users/services/users.service.spec.ts @@ -26,6 +26,8 @@ import { User } from '../schemas/user.schema'; import { JobsService } from './jobs.service'; import { UserRegistrySearchService } from './userRegistry-search.service'; import { UsersService } from './users.service'; +import { NewsletterService } from '../../newsletter/newsletter.service'; +import { NewsletterServiceMock } from '../../../test/mock/services/newsletter.mock.service'; function hashPassword() { return bcrypt.hashSync(process.env.USER_PWD, process.env.SALT); @@ -155,6 +157,10 @@ describe('UsersService', () => { useValue: mockJobsService, }, { provide: MailerService, useClass: MailerMockService }, + { + provide: NewsletterService, + useClass: NewsletterServiceMock, + }, ], }).compile(); diff --git a/src/users/services/users.service.ts b/src/users/services/users.service.ts index 903addd3040628c76273a8fb450da1a0769f7974..8cdae41c079dfe5baf8afd66e453ebb84b2f7d0d 100644 --- a/src/users/services/users.service.ts +++ b/src/users/services/users.service.ts @@ -27,6 +27,7 @@ import { JobsService } from './jobs.service'; import { UserRegistrySearchService } from './userRegistry-search.service'; import { StructuresService } from '../../structures/services/structures.service'; import { AxiosResponse } from 'axios'; +import { NewsletterService } from '../../newsletter/newsletter.service'; @Injectable() export class UsersService { @@ -38,7 +39,9 @@ export class UsersService { private jobsService: JobsService, private jwtService: JwtService, @Inject(forwardRef(() => StructuresService)) - private readonly structuresService: StructuresService + private readonly structuresService: StructuresService, + @Inject(forwardRef(() => NewsletterService)) + private newsletterService: NewsletterService ) {} /** @@ -732,6 +735,7 @@ export class UsersService { } public async deleteOne(email: string): Promise<User> { + this.logger.log(`delete user : ${email}`); const user = await this.findOne(email); if (!user) { throw new HttpException('Invalid user email', HttpStatus.BAD_REQUEST); @@ -742,7 +746,8 @@ export class UsersService { // Delete user this.userRegistrySearchService.deleteIndex(user); - const deletedUser = user.deleteOne(); + this.newsletterService.newsletterUnsubscribe(user.email); + const deletedUser = await user.deleteOne(); // Update CNFS label of each structure of the deleted user deletedUserStructuresLink.forEach(async (idStructure) => { @@ -846,6 +851,9 @@ export class UsersService { } if (updated) { + // Update newsletter MedNum tag status + this.newsletterService.setMedNumTag(updated.email, updated.job?.hasPersonalOffer); + // Update CNFS label of user structures await this.updateStructuresDenormalizedFields(updated); diff --git a/src/users/users.module.ts b/src/users/users.module.ts index e39efda6717e6876c321795398ef848cc97e9576..6d8a18c061f50ea13e5cdb1d79aec04b3bb154f7 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -23,6 +23,7 @@ import { UserRegistryService } from './services/userRegistry.service'; import { UserRegistrySearchService } from './services/userRegistry-search.service'; import { JwtModule } from '@nestjs/jwt'; import { config } from 'dotenv'; +import { NewsletterModule } from '../newsletter/newsletter.module'; config(); @Module({ @@ -42,6 +43,7 @@ config(); secret: process.env.JWT_SECRET, signOptions: { expiresIn: '30d' }, // 1 month validity }), + forwardRef(() => NewsletterModule), ], providers: [ UsersService, diff --git a/test/mock/services/newsletter.mock.service.ts b/test/mock/services/newsletter.mock.service.ts index 1a8c1db8cd0e9ff2919f779c4169245c3f075c1e..95acf9954f5be9a07c346f9272fefa921c8d4f54 100644 --- a/test/mock/services/newsletter.mock.service.ts +++ b/test/mock/services/newsletter.mock.service.ts @@ -24,6 +24,10 @@ export class NewsletterServiceMock { ]; } + setMedNumTag() { + return; + } + searchNewsletterSubscription(search: string) { if (search === 'a@a.com') { return [