diff --git a/package-lock.json b/package-lock.json index 742990c5bc985ac8ddef31c8f54a44e276948f55..f10f9a2a45196ae95f7889d264d1cf2c5a19755b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3292,6 +3292,84 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@mailchimp/mailchimp_marketing": { + "version": "3.0.78", + "resolved": "https://registry.npmjs.org/@mailchimp/mailchimp_marketing/-/mailchimp_marketing-3.0.78.tgz", + "integrity": "sha512-LAerA09fQ7opelydPolYez12mZ+TLQ+zDvHzweeAk9S+yI1bqBpuOvJVbe+3G0fsOqSbDTEUUBwUlncdAmShRA==", + "requires": { + "dotenv": "^8.2.0", + "superagent": "3.8.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "superagent": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.1.tgz", + "integrity": "sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==", + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.1.1", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.0.5" + } + } + } + }, "@mapbox/node-pre-gyp": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz", @@ -5046,7 +5124,7 @@ "append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" }, "aproba": { "version": "1.2.0", @@ -5123,7 +5201,7 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-from": { "version": "2.1.1", @@ -6232,8 +6310,7 @@ "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "concat-map": { "version": "0.0.1", @@ -6821,13 +6898,12 @@ "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "cookiejar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" }, "copy-descriptor": { "version": "0.1.1", @@ -8282,6 +8358,11 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -8713,8 +8794,7 @@ "formidable": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", - "dev": true + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" }, "forwarded": { "version": "0.2.0", @@ -12101,7 +12181,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "memfs": { "version": "3.4.7", @@ -12233,7 +12313,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, "merge-source-map": { "version": "1.0.4", @@ -12747,17 +12827,17 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -13204,7 +13284,7 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, "p-cancelable": { @@ -13392,7 +13472,7 @@ "pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" }, "pause-stream": { "version": "0.0.11", @@ -13773,7 +13853,7 @@ "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", "dev": true, "requires": { "resolve": "^1.1.6" @@ -16201,7 +16281,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "w3c-hr-time": { "version": "1.0.2", @@ -16571,7 +16651,7 @@ "xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", "dev": true }, "xml-name-validator": { diff --git a/package.json b/package.json index 96929e82d5d532c803fdc5d51cbd4e0fcc9ba3a7..eebbecb226b9c83119f81144dabab412784edee3 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@elastic/elasticsearch": "^7.12.0", + "@mailchimp/mailchimp_marketing": "^3.0.78", "@nestjs/axios": "^0.1.0", "@nestjs/common": "^9.0.11", "@nestjs/config": "^0.6.3", diff --git a/src/newsletter/interface/mailchimp-subscription.ts b/src/newsletter/interface/mailchimp-subscription.ts new file mode 100644 index 0000000000000000000000000000000000000000..2115e95ffe4dee4a8792b2791061baa50cf4b083 --- /dev/null +++ b/src/newsletter/interface/mailchimp-subscription.ts @@ -0,0 +1,5 @@ +export interface IMailchimpSubscription { + id: string; + email_address: string; + status: string; +} diff --git a/src/newsletter/interface/newsletter-subscription.interface.ts b/src/newsletter/interface/newsletter-subscription.interface.ts index 382e0911c13768f56e091db0c420ba4df013b3fd..351066d9843da63ca2f86b2512b9dde0d6ef1026 100644 --- a/src/newsletter/interface/newsletter-subscription.interface.ts +++ b/src/newsletter/interface/newsletter-subscription.interface.ts @@ -2,4 +2,5 @@ import { Document } from 'mongoose'; export interface INewsletterSubscription extends Document { email: string; + mailchimpId: string; } diff --git a/src/newsletter/newsletter-subscription.schema.ts b/src/newsletter/newsletter-subscription.schema.ts index 9ff18ab015a2e00b018ac1929bed0fb0a7a7ac48..934fbbc7ee3cf98e9c294126a879a9a0e48f7b6e 100644 --- a/src/newsletter/newsletter-subscription.schema.ts +++ b/src/newsletter/newsletter-subscription.schema.ts @@ -7,6 +7,9 @@ export type NewsletterSubscriptionDocument = NewsletterSubscription & Document; export class NewsletterSubscription { @Prop({ required: true }) email: string; + + @Prop({ required: true }) + mailchimpId: string; } export const NewsletterSubscriptionSchema = SchemaFactory.createForClass(NewsletterSubscription); diff --git a/src/newsletter/newsletter.controller.spec.ts b/src/newsletter/newsletter.controller.spec.ts index d1e066473bca0feac37369b4830e17f34f57eade..a34e079996430365c3d7df51b8cb428cc9de789e 100644 --- a/src/newsletter/newsletter.controller.spec.ts +++ b/src/newsletter/newsletter.controller.spec.ts @@ -29,16 +29,20 @@ describe('NewsletterController', () => { }); it('should subscribe user', async () => { - const result = { email: 'email@test.com' }; - jest.spyOn(controller, 'newsletterSubscribe').mockImplementation(async (): Promise<{ email }> => result); - const email = { email: 'email@test.com' }; + const result = { email: 'email@test.com', mailchimpId: 'test' }; + jest + .spyOn(controller, 'newsletterSubscribe') + .mockImplementation(async (): Promise<{ email; mailchimpId }> => result); + const email = { email: 'email@test.com', mailchimpId: 'test' }; expect(await controller.newsletterSubscribe(email)).toBe(result); }); it('should unsubscribe user', async () => { - const result = { email: 'email@test.com' }; - jest.spyOn(controller, 'newsletterUnsubscribe').mockImplementation(async (): Promise<{ email }> => result); - const email = { email: 'email@test.com' }; + const result = { email: 'email@test.com', mailchimpId: 'test' }; + jest + .spyOn(controller, 'newsletterUnsubscribe') + .mockImplementation(async (): Promise<{ email; mailchimpId }> => result); + const email = { email: 'email@test.com', mailchimpId: 'test' }; expect(await controller.newsletterUnsubscribe(email)).toBe(result); }); }); diff --git a/src/newsletter/newsletter.service.spec.ts b/src/newsletter/newsletter.service.spec.ts index 6b21d3b599b104ee55ff94ba1b296b98f7198045..c79529572d1af9aeadf4533f1c9166876d25f457 100644 --- a/src/newsletter/newsletter.service.spec.ts +++ b/src/newsletter/newsletter.service.spec.ts @@ -5,7 +5,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INewsletterSubscription } from './interface/newsletter-subscription.interface'; import { NewsletterSubscription } from './newsletter-subscription.schema'; import { NewsletterService } from './newsletter.service'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const mailchimp = require('@mailchimp/mailchimp_marketing'); +jest.mock('@mailchimp/mailchimp_marketing'); describe('NewsletterService', () => { + const OLD_ENV = process.env; let service: NewsletterService; const mockNewsletterModel = { @@ -18,6 +22,7 @@ describe('NewsletterService', () => { }; beforeEach(async () => { + jest.resetModules(); // Most important - it clears the cache const module: TestingModule = await Test.createTestingModule({ imports: [HttpModule], providers: [ @@ -30,6 +35,15 @@ describe('NewsletterService', () => { }).compile(); service = module.get<NewsletterService>(NewsletterService); + + process.env = { ...OLD_ENV }; // Make a copy + process.env.MC_LIST_ID = 'abcde'; + process.env.MC_API_KEY = 'k3y'; + process.env.MC_SERVER = 's3rv3r'; + }); + + afterAll(() => { + process.env = OLD_ENV; // Restore old environment }); it('should be defined', () => { @@ -54,6 +68,7 @@ describe('NewsletterService', () => { it('it should add a subscription for email test2@test.com', async () => { const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription; const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; + mailchimp.lists.addListMember.mockResolvedValueOnce({ email_address: 'test2@test.com' }); jest .spyOn(service, 'findOne') .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) @@ -63,6 +78,42 @@ describe('NewsletterService', () => { const subscription = await service.newsletterSubscribe('test2@test.com'); expect(subscription).toEqual({ email: 'test2@test.com' }); }); + it('it should return mailchimp 400 issue', async () => { + const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription; + const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; + mailchimp.lists.addListMember.mockRejectedValueOnce({ status: 400 }); + jest + .spyOn(service, 'findOne') + .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) + .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result); + mockNewsletterModel.create.mockResolvedValueOnce(_doc); + + try { + await service.newsletterSubscribe('test2@test.com'); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Email already exists'); + expect(e.status).toEqual(HttpStatus.BAD_REQUEST); + } + }); + it('it should return mailchimp 500 issue', async () => { + const result: INewsletterSubscription = { email: 'test2@test.com' } as INewsletterSubscription; + const _doc = { _id: 'a1aaaaa1a1', email: 'test2@test.com' }; + mailchimp.lists.addListMember.mockRejectedValueOnce({ status: 500 }); + jest + .spyOn(service, 'findOne') + .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => undefined) + .mockImplementationOnce(async (): Promise<INewsletterSubscription | undefined> => result); + mockNewsletterModel.create.mockResolvedValueOnce(_doc); + + try { + await service.newsletterSubscribe('test2@test.com'); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Server error'); + expect(e.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + } + }); }); describe('newsletterUnsubscribe', () => { it('it should not remove subscription for email test@test.com : does not exist', async () => { @@ -135,4 +186,23 @@ describe('NewsletterService', () => { expect(findOneEmail.length).toBe(2); }); }); + + describe('updateNewsletterSubscription', () => { + it('should update existing user subscription', () => { + mailchimp.lists.getListMembersInfo.mockResolvedValueOnce({ total_items: 10 }).mockResolvedValueOnce({ + members: [ + { email_address: 'a@a.com', status: 'subscribed' }, + { email_address: 'test@test.com', status: 'unsubscribed' }, + { email_address: 'test2@test.com', status: 'unsubscribed' }, + ], + }); + const result = { email: 'test2@test.com' } as INewsletterSubscription; + const spyer = jest.spyOn(mockNewsletterModel, 'findOne'); + // jest.spyOn(service, 'findOne').mockResolvedValueOnce(result).mockResolvedValueOnce(result); + mockNewsletterModel.findOne.mockResolvedValueOnce(result).mockResolvedValueOnce(null); + service.updateNewsletterSubscription(); + expect(spyer).toBeCalledTimes(2); + // expect(spyerDelete).toBeCalledTimes(1); + }); + }); }); diff --git a/src/newsletter/newsletter.service.ts b/src/newsletter/newsletter.service.ts index c5f8363fbfc83d175b1fe5377e0de76386010e27..7efffd693731a043e1b0566dde4f9c2e3fc25b48 100644 --- a/src/newsletter/newsletter.service.ts +++ b/src/newsletter/newsletter.service.ts @@ -1,45 +1,99 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; +import { Cron, CronExpression } from '@nestjs/schedule'; import { Model } from 'mongoose'; +import { IMailchimpSubscription } from './interface/mailchimp-subscription'; import { INewsletterSubscription } from './interface/newsletter-subscription.interface'; import { NewsletterSubscription, NewsletterSubscriptionDocument } from './newsletter-subscription.schema'; - +// eslint-disable-next-line @typescript-eslint/no-var-requires +const mailchimp = require('@mailchimp/mailchimp_marketing'); @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> - ) {} + ) { + // 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 newsletterSubscribe(email: string): Promise<NewsletterSubscription> { + this.logger.debug('newsletterSubscribe'); const existingEmail = await this.findOne(email); if (existingEmail) { throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); } - await this.newsletterSubscriptionModel.create({ email: email }); - return this.findOne(email); + try { + const member = await mailchimp.lists.addListMember(this.LIST_ID, { + email_address: email, + status: 'subscribed', + }); + await this.newsletterSubscriptionModel.create({ email: email, mailchimpId: member.id }); + return this.findOne(email); + } catch (e) { + if (e.status === 400) { + this.logger.error(`Error ${e.status}, user might already exist in mailchimplist`); + throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); + } else { + this.logger.error(`Mailchimp configuration error`); + throw new HttpException('Server error', HttpStatus.INTERNAL_SERVER_ERROR); + } + } } public async newsletterUnsubscribe(email: string): Promise<NewsletterSubscription> { + this.logger.debug('newsletterUnsubscribe'); const subscription = await this.findOne(email); if (!subscription) { throw new HttpException('Invalid email', HttpStatus.BAD_REQUEST); } + await mailchimp.lists.setListMember(this.LIST_ID, subscription.mailchimpId, { + email_address: email, + status: 'unsubscribed', + }); return subscription.deleteOne(); } public async findOne(mail: string): Promise<INewsletterSubscription | undefined> { + this.logger.debug('findOne'); return this.newsletterSubscriptionModel.findOne({ email: mail }); } public async searchNewsletterSubscription(searchString: string): Promise<NewsletterSubscriptionDocument[]> { + this.logger.debug('searchNewsletterSubscription'); return this.newsletterSubscriptionModel.find({ email: new RegExp(searchString, 'i') }); } public async countNewsletterSubscriptions(): Promise<number> { + this.logger.debug('countNewsletterSubscriptions'); return this.newsletterSubscriptionModel.countDocuments({}); } public async findAll(): Promise<NewsletterSubscription[]> { + this.logger.debug('findAll'); return this.newsletterSubscriptionModel.find(); } } diff --git a/src/structures/services/structure.service.spec.ts b/src/structures/services/structure.service.spec.ts index 021b5bb6a556e8795b669ac46030fd7134176309..4ce08450dbf66434c2d17285a61dab2ee140ff41 100644 --- a/src/structures/services/structure.service.spec.ts +++ b/src/structures/services/structure.service.spec.ts @@ -1,11 +1,12 @@ import { HttpModule } from '@nestjs/axios'; +import { HttpStatus } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; import * as bcrypt from 'bcrypt'; import { Types } from 'mongoose'; import { personalOffersDataMock } from '../../../test/mock/data/personalOffers.mock.data'; -import { structuresDocumentDataMock } from '../../../test/mock/data/structures.mock.data'; +import { structureMockDto, structuresDocumentDataMock } from '../../../test/mock/data/structures.mock.data'; import { mockParametersModel } from '../../../test/mock/services/parameters.mock.service'; import { UsersServiceMock } from '../../../test/mock/services/user.mock.service'; import { CategoriesFormationsService } from '../../categories/services/categories-formations.service'; @@ -36,6 +37,7 @@ describe('StructuresService', () => { exec: jest.fn(), find: jest.fn(), populate: jest.fn(), + save: jest.fn(), }; const structuresSearchServiceMock = { @@ -162,6 +164,9 @@ describe('StructuresService', () => { personalOffers: [], }, ]), + dropIndex: jest.fn(), + createStructureIndex: jest.fn(), + indexStructure: jest.fn(), }; const mockCategoriesFormationsService = { @@ -263,9 +268,14 @@ describe('StructuresService', () => { expect(service).toBeDefined(); }); - it('should Initiate structure', () => { - const res = service.initiateStructureIndex(); - expect(res).toBeTruthy(); + it('should Initiate structure index', async () => { + const spyerIndex = jest.spyOn(structuresSearchServiceMock, 'indexStructure'); + const spyerDrop = jest.spyOn(structuresSearchServiceMock, 'dropIndex'); + const spyerCreate = jest.spyOn(structuresSearchServiceMock, 'createStructureIndex'); + await service.initiateStructureIndex(); + expect(spyerCreate).toBeCalledTimes(1); + expect(spyerDrop).toBeCalledTimes(1); + expect(spyerIndex).toBeCalledTimes(1); }); describe('should searchForStructures', () => { @@ -358,12 +368,25 @@ describe('StructuresService', () => { }); }); - it('should create structure', () => { - const structure = new StructureDto(); - let res = service.create(null, structure); - expect(res).toBeTruthy(); - res = service.create('tsfsf6296', structure); - expect(res).toBeTruthy(); + describe('create', () => { + it('should return invalid profile', async () => { + const structure = structureMockDto; + try { + await service.create('test@test.com', structure); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual('Invalid profile'); + expect(e.status).toEqual(HttpStatus.NOT_FOUND); + } + }); + // it('should have valid profile and create structure', async () => { + // const structure = structureMockDto; + // const spyer = jest.spyOn(mockStructureModel, 'save'); + // const spyerIndex = jest.spyOn(structuresSearchServiceMock, 'indexStructure'); + // await service.create('pauline.dupont@mii.com', structure); + // expect(spyer).toBeCalledTimes(1); // Should create structure in DB + // expect(spyerIndex).toBeCalledTimes(1); // Should index structure + // }); }); it('should search structure', () => { diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index 78c0595202ffca3b1bad3954ee01b5e73cd3ee92..65408fe276248b91e3345093ccb04d3b7fdf1a37 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -107,8 +107,8 @@ export class StructuresService { return structures.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); } - public async create(idUser: string, structure: StructureDto): Promise<Structure> { - const user = await this.userService.findOne(idUser); + public async create(email: string, structure: StructureDto): Promise<Structure> { + const user = await this.userService.findOne(email); if (!user) { throw new HttpException('Invalid profile', HttpStatus.NOT_FOUND); } diff --git a/template.env b/template.env index 05049ed6a7cd626825ec2fec8f59345d1411f51d..ffa047137f23d6214cb698c674b6887c21851263 100644 --- a/template.env +++ b/template.env @@ -21,6 +21,10 @@ GHOST_CONTENT_API_KEY=<Ghost global api key, can be found in integration part of GHOST_ADMIN_API_KEY=<Ghost admin api key, can be found in integration part of ghost UI> GHOST_HOST_AND_PORT=<Ghost host and port, ex:http://localhost:2368> USER_PWD=<test user password, this password will be user by every test users> +#Mailchimp +MC_API_KEY=<Mailchimp api key> +MC_SERVER=<Mailchimp server> +MC_LIST_ID=<Mailchimp list id> ELASTICSEARCH_NODE=<elastic search container node> ELASTICSEARCH_PATH=<elastic search container path> ELASTICSEARCH_PORT=<elastic search port> diff --git a/test/mock/data/structures.mock.data.ts b/test/mock/data/structures.mock.data.ts index 07f1135ac2b39f5a4476fb9adae616a422bda377..e387288426bb4b4ec15567a5616a9a68e259b94f 100644 --- a/test/mock/data/structures.mock.data.ts +++ b/test/mock/data/structures.mock.data.ts @@ -1,3 +1,4 @@ +import { StructureDto } from '../../../src/structures/dto/structure.dto'; import { StructureDocument } from '../../../src/structures/schemas/structure.schema'; export const structuresDocumentDataMock: StructureDocument[] = [ @@ -166,3 +167,81 @@ export const structuresDocumentDataMock: StructureDocument[] = [ save: jest.fn(), } as any, ] as StructureDocument[]; + +export const structureMockDto: StructureDto = { + coord: [4.8498155, 45.7514817], + equipmentsAndServices: ['wifiEnAccesLibre'], + digitalCultureSecurity: [], + parentingHelp: [], + socialAndProfessional: [], + accessRight: [], + baseSkills: [], + proceduresAccompaniment: [], + publicsAccompaniment: [], + publics: ['adultes'], + labelsQualifications: [], + accessModality: ['telephoneVisio'], + structureType: null, + structureName: 'a', + description: null, + lockdownActivity: null, + address: { + numero: null, + street: 'Rue Alphonse Daudet', + commune: 'Lyon 7ème Arrondissement', + }, + contactMail: '', + contactPhone: '', + website: '', + facebook: null, + twitter: null, + instagram: null, + linkedin: null, + hours: { + monday: { + open: false, + time: [], + }, + tuesday: { + open: false, + time: [], + }, + wednesday: { + open: false, + time: [], + }, + thursday: { + open: false, + time: [], + }, + friday: { + open: false, + time: [], + }, + saturday: { + open: false, + time: [], + }, + sunday: { + open: false, + time: [], + }, + }, + pmrAccess: false, + exceptionalClosures: null, + otherDescription: null, + nbComputers: 1, + nbPrinters: 1, + nbTablets: 1, + nbNumericTerminal: 1, + nbScanners: 1, + freeWorkShop: false, + accountVerified: true, + personalOffers: [], + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: new Date(), + remoteAccompaniment: true, + dataShareConsentDate: new Date(), + numero: '', +};