From 00ffac5b976834478ab90f4fb1bc8e38f72ca1b7 Mon Sep 17 00:00:00 2001 From: cristianizzo <05May1986!> Date: Wed, 27 Oct 2021 21:13:53 +0200 Subject: [PATCH 1/3] interest email --- src/modules/notification.js | 36 +++++++++++++++++----- src/services/investment/index.js | 2 +- test/unit-dep/db/serviceInvestment.spec.js | 2 +- test/unit/modules/notification.spec.js | 31 +++++++++++++++++-- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/modules/notification.js b/src/modules/notification.js index a76f14660..57788977a 100644 --- a/src/modules/notification.js +++ b/src/modules/notification.js @@ -563,20 +563,42 @@ const Notification = { }, // Investment - async interestIncoming(userId, interest, coinCode) { + async interestIncoming(user, interest, coinCode) { + + const url = `${config.DEEPLINK_APP}/interests/${coinCode}/${interest.id}`; + const userId = user.id; + const logInfo = { userId, notification: 'interest-incoming', interest, coinCode, - url: `${config.DEEPLINK_APP}/interests/${interest.id}`, + url, }; - return PushNotification.send( - `💰 Ding Ding! You received ${interest.amount} ${coinCode} interests in your Wallet 🎉`, - userId, - logInfo - ); + await Promise.all([ + Mail.send( + { + toEmail: user.email, + toName: `${user.firstName} ${user.lastName}`, + dynamicContent: { + name: user.firstName, + coinCode, + amount: interest.amount, + url, + }, + templateId: 'd-b8c2dd914c8a457d8a4de194c8cfa108', + }, + logInfo + ), + PushNotification.send( + `💰 Ding Ding! You received ${interest.amount} ${coinCode} interests in your Wallet 🎉`, + userId, + logInfo + ) + ]); + + return true; }, // Card diff --git a/src/services/investment/index.js b/src/services/investment/index.js index 58dc9e103..7547ed826 100644 --- a/src/services/investment/index.js +++ b/src/services/investment/index.js @@ -226,7 +226,7 @@ const Investment = { const dbUser = await investment.Wallet.getUser(); Utils.setImmediateAsync(() => - Notification.interestIncoming(dbUser.id, interest.filterKeys(), interestRate.CoinCode) + Notification.interestIncoming(dbUser.filterKeys(), interest.filterKeys(), interestRate.CoinCode) ); }, onError: async (investment, error) => { diff --git a/test/unit-dep/db/serviceInvestment.spec.js b/test/unit-dep/db/serviceInvestment.spec.js index d074b5410..61332d4fe 100644 --- a/test/unit-dep/db/serviceInvestment.spec.js +++ b/test/unit-dep/db/serviceInvestment.spec.js @@ -1294,7 +1294,7 @@ describe('Investment service', () => { expect(interests11.length).to.be.eq(0); expect(stubNotification.calledOnce).to.be.true; - expect(stubNotification.args[0][0]).to.be.eq(this.user1.id); + expect(stubNotification.args[0][0].id).to.be.eq(this.user1.id); expect(stubNotification.args[0][1]).to.be.deep.eq(interests[0].filterKeys()); expect(stubNotification.args[0][2]).to.be.eq(this.wallet1.CoinCode); }); diff --git a/test/unit/modules/notification.spec.js b/test/unit/modules/notification.spec.js index 5fefcfed6..7a317aa73 100644 --- a/test/unit/modules/notification.spec.js +++ b/test/unit/modules/notification.spec.js @@ -1242,9 +1242,36 @@ describe('Module: Notification', () => { amount: '2.1', amountEUR: '200', }); - const res = await Notification.interestIncoming(this.user.id, interest.filterKeys(), coinCode); + const url = `${config.DEEPLINK_APP}/interests/${coinCode}/${interest.id}`; + + const res = await Notification.interestIncoming(this.user.filterKeys(), interest.filterKeys(), coinCode); expect(res).to.be.true; + + expect(this.stuMailSend.calledOnce).to.be.true; + expect( + this.stuMailSend.calledWith( + { + toEmail: this.user.email, + toName: `${this.user.firstName} ${this.user.lastName}`, + dynamicContent: { + name: this.user.firstName, + coinCode, + amount: interest.amount, + url, + }, + templateId: 'd-b8c2dd914c8a457d8a4de194c8cfa108', + }, + { + userId: this.user.id, + notification: 'interest-incoming', + interest: interest.filterKeys(), + coinCode, + url, + } + ) + ).to.be.true; + expect(this.stubPushbNotificationSend.calledOnce).to.be.true; expect( this.stubPushbNotificationSend.calledWith( @@ -1255,7 +1282,7 @@ describe('Module: Notification', () => { notification: 'interest-incoming', interest: interest.filterKeys(), coinCode, - url: `${config.DEEPLINK_APP}/interests/${interest.id}`, + url, } ) ).to.be.true; From 40169d7416d53e81d4e8d9960b998334235236d3 Mon Sep 17 00:00:00 2001 From: cristianizzo <05May1986!> Date: Wed, 27 Oct 2021 21:15:16 +0200 Subject: [PATCH 2/3] write --- src/modules/notification.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/notification.js b/src/modules/notification.js index 57788977a..444d2e3c6 100644 --- a/src/modules/notification.js +++ b/src/modules/notification.js @@ -564,7 +564,6 @@ const Notification = { // Investment async interestIncoming(user, interest, coinCode) { - const url = `${config.DEEPLINK_APP}/interests/${coinCode}/${interest.id}`; const userId = user.id; @@ -595,7 +594,7 @@ const Notification = { `💰 Ding Ding! You received ${interest.amount} ${coinCode} interests in your Wallet 🎉`, userId, logInfo - ) + ), ]); return true; From 6274354afc79d8cc8336a472a96e3f3a38a33123 Mon Sep 17 00:00:00 2001 From: cristianizzo Date: Fri, 29 Oct 2021 02:49:29 +0200 Subject: [PATCH 3/3] notify multiple interests --- src/modules/notification.js | 20 ++-- src/services/investment/index.js | 39 +++++++- test/unit-dep/db/serviceInvestment.spec.js | 107 ++++++++++++++++++++- test/unit/modules/notification.spec.js | 61 ++++++++---- 4 files changed, 189 insertions(+), 38 deletions(-) diff --git a/src/modules/notification.js b/src/modules/notification.js index 444d2e3c6..4b7593dbb 100644 --- a/src/modules/notification.js +++ b/src/modules/notification.js @@ -563,15 +563,14 @@ const Notification = { }, // Investment - async interestIncoming(user, interest, coinCode) { - const url = `${config.DEEPLINK_APP}/interests/${coinCode}/${interest.id}`; + async interestIncoming(user, interests) { + const url = `${config.DEEPLINK_APP}/earn`; const userId = user.id; const logInfo = { userId, notification: 'interest-incoming', - interest, - coinCode, + interests, url, }; @@ -582,18 +581,19 @@ const Notification = { toName: `${user.firstName} ${user.lastName}`, dynamicContent: { name: user.firstName, - coinCode, - amount: interest.amount, + interests, url, }, templateId: 'd-b8c2dd914c8a457d8a4de194c8cfa108', }, logInfo ), - PushNotification.send( - `💰 Ding Ding! You received ${interest.amount} ${coinCode} interests in your Wallet 🎉`, - userId, - logInfo + interests.map((w) => + PushNotification.send( + `💰 Ding Ding! You received ${w.amount} ${w.coinCode} interests in your Wallet 🎉`, + user.id, + logInfo + ) ), ]); diff --git a/src/services/investment/index.js b/src/services/investment/index.js index 7547ed826..a5d0d9022 100644 --- a/src/services/investment/index.js +++ b/src/services/investment/index.js @@ -20,6 +20,7 @@ const Investment = { NEED_CONNECTIONS: ['postgre', 'redis', 'coins', 'special-wallet'], repeaters: {}, + aggregateNotifications: [], convertCelCoinCode(coinCode) { if (coinCode === 'DAI') { @@ -225,9 +226,12 @@ const Investment = { const dbUser = await investment.Wallet.getUser(); - Utils.setImmediateAsync(() => - Notification.interestIncoming(dbUser.filterKeys(), interest.filterKeys(), interestRate.CoinCode) - ); + Investment.aggregateNotifications.push({ + user: dbUser.filterKeys(), + interest: Object.assign(interest.filterKeys(), { + coinCode: interestRate.CoinCode, + }), + }); }, onError: async (investment, error) => { logger.error( @@ -405,13 +409,40 @@ const Investment = { if (remainInterestRates.length > 0) { await Utils.asyncForEach(remainInterestRates, Investment.createInterests); - + Investment.notifyInterests(); return true; } return false; }, + notifyInterests() { + if (Investment.aggregateNotifications.length > 0) { + const grouped = Investment.aggregateNotifications.reduce((acc, item) => { + if (!acc[item.user.id]) { + acc[item.user.id] = { + user: item.user, + interests: [], + }; + } + + const existingInterest = acc[item.user.id].interests.find((w) => w.id === item.interest.id); + + if (!existingInterest) { + acc[item.user.id].interests.push(item.interest); + } + + return acc; + }, {}); + + Object.keys(grouped).map((k) => { + Utils.setImmediateAsync(() => Notification.interestIncoming(grouped[k].user, grouped[k].interests)); + }); + + Investment.aggregateNotifications = []; + } + }, + async checkInterest() { logger.info('Check interests start', llo({})); diff --git a/test/unit-dep/db/serviceInvestment.spec.js b/test/unit-dep/db/serviceInvestment.spec.js index 61332d4fe..c49976870 100644 --- a/test/unit-dep/db/serviceInvestment.spec.js +++ b/test/unit-dep/db/serviceInvestment.spec.js @@ -92,6 +92,7 @@ describe('Investment service', () => { afterEach(() => { sandbox && sandbox.restore(); + InvestmentService.aggregateNotifications = []; }); it('Should start and stop', async () => { @@ -562,11 +563,13 @@ describe('Investment service', () => { endedAt: date, startedAt: date, }); + const stubNotifyInterests = sandbox.stub(InvestmentService, 'notifyInterests').resolves(); const res = await InvestmentService.checkRemainInterestPeriods(); expect(res).to.be.be.false; expect(this.stubCreateInterests.callCount).to.be.eq(0); + expect(stubNotifyInterests.notCalled).to.be.true; }); it('Should create interest when one remained', async () => { @@ -581,11 +584,13 @@ describe('Investment service', () => { startedAt: date, endedAt: date, }); + const stubNotifyInterests = sandbox.stub(InvestmentService, 'notifyInterests').resolves(); const res = await InvestmentService.checkRemainInterestPeriods(); expect(res).to.be.be.true; expect(this.stubCreateInterests.callCount).to.be.eq(1); + expect(stubNotifyInterests.calledOnce).to.be.true; }); it('Should create interest when two remained', async () => { @@ -623,10 +628,13 @@ describe('Investment service', () => { endedAt: date2, }); + const stubNotifyInterests = sandbox.stub(InvestmentService, 'notifyInterests').resolves(); + const res = await InvestmentService.checkRemainInterestPeriods(); expect(res).to.be.be.true; expect(this.stubCreateInterests.callCount).to.be.eq(2); + expect(stubNotifyInterests.calledOnce).to.be.true; }); }); @@ -1263,7 +1271,6 @@ describe('Investment service', () => { it('Should create interest on one invest one wallet', async () => { const balance = this.wallet1.balance; - const stubNotification = sandbox.stub(Notification, 'interestIncoming').resolves(); await Models.Investment.create({ amount: '1', @@ -1293,10 +1300,13 @@ describe('Investment service', () => { const interests11 = await this.wallet11.getInterests(); expect(interests11.length).to.be.eq(0); - expect(stubNotification.calledOnce).to.be.true; - expect(stubNotification.args[0][0].id).to.be.eq(this.user1.id); - expect(stubNotification.args[0][1]).to.be.deep.eq(interests[0].filterKeys()); - expect(stubNotification.args[0][2]).to.be.eq(this.wallet1.CoinCode); + expect(InvestmentService.aggregateNotifications.length).to.eq(1); + expect(InvestmentService.aggregateNotifications[0].user.id).to.eq(this.user1.id); + expect(InvestmentService.aggregateNotifications[0].interest).to.be.deep.eq( + Object.assign(interests[0].filterKeys(), { + coinCode: this.wallet1.CoinCode, + }) + ); }); it('Should rollback on error', async () => { @@ -1326,6 +1336,93 @@ describe('Investment service', () => { await this.interestRate.reload(); expect(this.interestRate.isDistributed).to.be.false; + expect(InvestmentService.aggregateNotifications.length).to.eq(0); + }); + }); + + describe('notifyInterests', () => { + it('should send notifications', () => { + const fakeNotifications = [ + { + user: { id: 'xxx' }, + interest: { id: 1, amount: 1, coinCode: 'BTC' }, + }, + { + user: { id: 'xxx' }, + interest: { id: 2, amount: 2, coinCode: 'ETH' }, + }, + { + user: { id: 'xxy' }, + interest: { id: 3, amount: 1, coinCode: 'ETH' }, + }, + ]; + + InvestmentService.aggregateNotifications = fakeNotifications; + + const stubNotification = sandbox.stub(Notification, 'interestIncoming').resolves(); + + InvestmentService.notifyInterests(); + + expect(stubNotification.calledTwice).to.be.true; + expect(stubNotification.args[0][0].id).to.eq(fakeNotifications[0].user.id); + expect(stubNotification.args[0][1].length).to.eq(2); + expect(stubNotification.args[0][1][0].coinCode).to.eq(fakeNotifications[0].interest.coinCode); + expect(stubNotification.args[0][1][1].coinCode).to.eq(fakeNotifications[1].interest.coinCode); + + expect(stubNotification.args[1][0].id).to.eq(fakeNotifications[2].user.id); + expect(stubNotification.args[1][1].length).to.eq(1); + expect(stubNotification.args[1][1][0].coinCode).to.eq(fakeNotifications[2].interest.coinCode); + + expect(InvestmentService.aggregateNotifications.length).to.eq(0); + }); + + it('should send notifications and skip duplicate', () => { + const fakeNotifications = [ + { + user: { id: 'xxx' }, + interest: { id: 1, amount: 1, coinCode: 'BTC' }, + }, + { + user: { id: 'xxx' }, + interest: { id: 1, amount: 1, coinCode: 'BTC' }, + }, + { + user: { id: 'xxx' }, + interest: { id: 2, amount: 2, coinCode: 'ETH' }, + }, + { + user: { id: 'xxy' }, + interest: { id: 3, amount: 1, coinCode: 'ETH' }, + }, + ]; + InvestmentService.aggregateNotifications = fakeNotifications; + + const stubNotification = sandbox.stub(Notification, 'interestIncoming').resolves(); + + InvestmentService.notifyInterests(); + + expect(stubNotification.calledTwice).to.be.true; + expect(stubNotification.args[0][0].id).to.eq(fakeNotifications[0].user.id); + expect(stubNotification.args[0][1].length).to.eq(2); + expect(stubNotification.args[0][1][0].coinCode).to.eq(fakeNotifications[0].interest.coinCode); + expect(stubNotification.args[0][1][1].coinCode).to.eq(fakeNotifications[2].interest.coinCode); + + expect(stubNotification.args[1][0].id).to.eq(fakeNotifications[3].user.id); + expect(stubNotification.args[1][1].length).to.eq(1); + expect(stubNotification.args[1][1][0].coinCode).to.eq(fakeNotifications[3].interest.coinCode); + + expect(InvestmentService.aggregateNotifications.length).to.eq(0); + }); + + it('should not send notifications', () => { + InvestmentService.aggregateNotifications = []; + + const stubNotification = sandbox.stub(Notification, 'interestIncoming').resolves(); + + InvestmentService.notifyInterests(); + + expect(stubNotification.notCalled).to.be.true; + expect(InvestmentService.aggregateNotifications.length).to.eq(0); }); }); }); diff --git a/test/unit/modules/notification.spec.js b/test/unit/modules/notification.spec.js index 7a317aa73..686e58301 100644 --- a/test/unit/modules/notification.spec.js +++ b/test/unit/modules/notification.spec.js @@ -1237,14 +1237,22 @@ describe('Module: Notification', () => { }); it('Should notify interest incoming', async () => { - const coinCode = 'BTC'; const interest = await Models.Interest.create({ amount: '2.1', amountEUR: '200', }); - const url = `${config.DEEPLINK_APP}/interests/${coinCode}/${interest.id}`; - const res = await Notification.interestIncoming(this.user.filterKeys(), interest.filterKeys(), coinCode); + const interest1 = Object.assign(interest.filterKeys(), { + coinCode: 'BTC', + }); + + const interest2 = Object.assign(interest.filterKeys(), { + coinCode: 'ETH', + }); + + const url = `${config.DEEPLINK_APP}/earn`; + + const res = await Notification.interestIncoming(this.user.filterKeys(), [interest1, interest2]); expect(res).to.be.true; @@ -1256,8 +1264,7 @@ describe('Module: Notification', () => { toName: `${this.user.firstName} ${this.user.lastName}`, dynamicContent: { name: this.user.firstName, - coinCode, - amount: interest.amount, + interests: [interest1, interest2], url, }, templateId: 'd-b8c2dd914c8a457d8a4de194c8cfa108', @@ -1265,26 +1272,42 @@ describe('Module: Notification', () => { { userId: this.user.id, notification: 'interest-incoming', - interest: interest.filterKeys(), - coinCode, + interests: [interest1, interest2], url, } ) ).to.be.true; - expect(this.stubPushbNotificationSend.calledOnce).to.be.true; + expect(this.stubPushbNotificationSend.calledTwice).to.be.true; + expect( - this.stubPushbNotificationSend.calledWith( - `💰 Ding Ding! You received ${interest.amount} ${coinCode} interests in your Wallet 🎉`, - this.user.id, - { - userId: this.user.id, - notification: 'interest-incoming', - interest: interest.filterKeys(), - coinCode, - url, - } - ) + this.stubPushbNotificationSend + .getCall(0) + .calledWith( + `💰 Ding Ding! You received ${interest1.amount} ${interest1.coinCode} interests in your Wallet 🎉`, + this.user.id, + { + userId: this.user.id, + notification: 'interest-incoming', + interests: [interest1, interest2], + url, + } + ) + ).to.be.true; + + expect( + this.stubPushbNotificationSend + .getCall(1) + .calledWith( + `💰 Ding Ding! You received ${interest2.amount} ${interest2.coinCode} interests in your Wallet 🎉`, + this.user.id, + { + userId: this.user.id, + notification: 'interest-incoming', + interests: [interest1, interest2], + url, + } + ) ).to.be.true; });