From 838fe000582ac79874e3e0d45b2ea2a9160829cb Mon Sep 17 00:00:00 2001 From: Simon Detheridge Date: Mon, 25 Feb 2019 10:22:47 +0000 Subject: [PATCH] Merge pull request #1531 from sharelatex/spd-notify-users-on-affiliation-upgrade Notify users when affiliations are upgraded GitOrigin-RevId: 0f9e92b6a49f2ddef559e9e23fc73436910fb9f6 --- .../Institutions/InstitutionsManager.coffee | 61 ++++++++++++++-- .../Notifications/NotificationsBuilder.coffee | 20 ++++- .../Subscription/FeaturesUpdater.coffee | 2 +- .../Subscription/UserFeaturesUpdater.coffee | 6 +- .../InstitutionsManagerTests.coffee | 73 ++++++++++++++++--- 5 files changed, 142 insertions(+), 20 deletions(-) diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee index 5de39aebf1..be07c31211 100644 --- a/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee +++ b/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee @@ -1,17 +1,29 @@ logger = require 'logger-sharelatex' async = require 'async' db = require("../../infrastructure/mongojs").db +_ = require("underscore") ObjectId = require("../../infrastructure/mongojs").ObjectId { getInstitutionAffiliations } = require('./InstitutionsAPI') FeaturesUpdater = require('../Subscription/FeaturesUpdater') UserGetter = require('../User/UserGetter') +NotificationsBuilder = require("../Notifications/NotificationsBuilder") +SubscriptionLocator = require("../Subscription/SubscriptionLocator") +Institution = require("../../models/Institution").Institution ASYNC_LIMIT = 10 module.exports = InstitutionsManager = upgradeInstitutionUsers: (institutionId, callback = (error) ->) -> - getInstitutionAffiliations institutionId, (error, affiliations) -> - return callback(error) if error - async.eachLimit affiliations, ASYNC_LIMIT, refreshFeatures, callback + async.waterfall [ + (cb) -> + fetchInstitutionAndAffiliations institutionId, cb + (institution, affiliations, cb) -> + affiliations = _.map affiliations, (affiliation) -> + affiliation.institutionName = institution.name + affiliation.institutionId = institutionId + return affiliation + async.eachLimit affiliations, ASYNC_LIMIT, refreshFeatures, (err) -> cb(err) + ], callback + checkInstitutionUsers: (institutionId, callback = (error) ->) -> getInstitutionAffiliations institutionId, (error, affiliations) -> UserGetter.getUsersByAnyConfirmedEmail( @@ -21,9 +33,48 @@ module.exports = InstitutionsManager = callback(error, checkFeatures(users)) ) +fetchInstitutionAndAffiliations = (institutionId, callback) -> + async.waterfall [ + (cb) -> + Institution.findOne {v1Id: institutionId}, (err, institution) -> cb(err, institution) + (institution, cb) -> + institution.fetchV1Data (err, institution) -> cb(err, institution) + (institution, cb) -> + getInstitutionAffiliations institutionId, (err, affiliations) -> cb(err, institution, affiliations) + ], callback + refreshFeatures = (affiliation, callback) -> userId = ObjectId(affiliation.user_id) - FeaturesUpdater.refreshFeatures(userId, true, callback) + async.waterfall [ + (cb) -> + FeaturesUpdater.refreshFeatures userId, true, (err, features, featuresChanged) -> cb(err, featuresChanged) + (featuresChanged, cb) -> + getUserInfo userId, (error, user, subscription) -> cb(error, user, subscription, featuresChanged) + (user, subscription, featuresChanged, cb) -> + notifyUser user, affiliation, subscription, featuresChanged, cb + ], callback + +getUserInfo = (userId, callback) -> + async.waterfall [ + (cb) -> + UserGetter.getUser userId, cb + (user, cb) -> + SubscriptionLocator.getUsersSubscription user, (err, subscription) -> cb(err, user, subscription) + ], callback + +notifyUser = (user, affiliation, subscription, featuresChanged, callback) -> + async.parallel [ + (cb) -> + if featuresChanged + NotificationsBuilder.featuresUpgradedByAffiliation(affiliation, user).create cb + else + cb() + (cb) -> + if subscription? and !subscription.planCode.match(/(free|trial)/)? and !subscription.groupPlan + NotificationsBuilder.redundantPersonalSubscription(affiliation, user).create cb + else + cb() + ], callback checkFeatures = (users) -> usersSummary = { @@ -39,4 +90,4 @@ checkFeatures = (users) -> usersSummary.totalConfirmedNonProUsers += 1 usersSummary.confirmedNonProUsers.push user._id ) - return usersSummary \ No newline at end of file + return usersSummary diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 14751a54e3..abac3f02a7 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -4,8 +4,26 @@ request = require "request" settings = require "settings-sharelatex" module.exports = +# Note: notification keys should be url-safe + + featuresUpgradedByAffiliation: (affiliation, user) -> + key: "features-updated-by=#{affiliation.institutionId}" + create: (callback=()->) -> + messageOpts = + institutionName: affiliation.institutionName + NotificationsHandler.createNotification user._id, @key, "notification_features_upgraded_by_affiliation", messageOpts, null, false, callback + read: (callback=()->) -> + NotificationsHandler.markAsRead @key, callback + + redundantPersonalSubscription: (affiliation, user) -> + key: "redundant-personal-subscription-#{affiliation.institutionId}" + create: (callback=()->) -> + messageOpts = + institutionName: affiliation.institutionName + NotificationsHandler.createNotification user._id, @key, "notification_personal_subscription_not_required_due_to_affiliation", messageOpts, null, false, callback + read: (callback=()->) -> + NotificationsHandler.markAsRead @key, callback - # Note: notification keys should be url-safe projectInvite: (invite, project, sendingUser, user) -> key: "project-invite-#{invite._id}" create: (callback=()->) -> diff --git a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee b/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee index b091ed9f30..32662b76c6 100644 --- a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee @@ -12,7 +12,7 @@ InstitutionsFeatures = require '../Institutions/InstitutionsFeatures' oneMonthInSeconds = 60 * 60 * 24 * 30 module.exports = FeaturesUpdater = - refreshFeatures: (user_id, notifyV1 = true, callback = () ->)-> + refreshFeatures: (user_id, notifyV1 = true, callback = (error, features, featuresChanged) ->)-> if typeof notifyV1 == 'function' callback = notifyV1 notifyV1 = true diff --git a/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee b/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee index 28a49b2126..3d20f40271 100644 --- a/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee @@ -2,11 +2,11 @@ logger = require("logger-sharelatex") User = require('../../models/User').User module.exports = - updateFeatures: (user_id, features, callback = (err, features)->)-> + updateFeatures: (user_id, features, callback = (err, features, featuresChanged)->)-> conditions = _id:user_id update = {} logger.log user_id:user_id, features:features, "updating users features" update["features.#{key}"] = value for key, value of features - User.update conditions, update, (err)-> - callback err, features + User.update conditions, update, (err, result)-> + callback err, features, result?.nModified == 1 diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee index 0c47e8a78b..14fab8d2aa 100644 --- a/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee +++ b/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee @@ -9,30 +9,83 @@ describe "InstitutionsManager", -> beforeEach -> @institutionId = 123 @logger = log: -> + @user = {} @getInstitutionAffiliations = sinon.stub() @refreshFeatures = sinon.stub().yields() - @getUsersByAnyConfirmedEmail = sinon.stub().yields() + @UserGetter = + getUsersByAnyConfirmedEmail: sinon.stub().yields() + getUser: sinon.stub().callsArgWith(1, null, @user) + @creator = + create: sinon.stub().callsArg(0) + @NotificationsBuilder = + featuresUpgradedByAffiliation: sinon.stub().returns(@creator) + redundantPersonalSubscription: sinon.stub().returns(@creator) + @SubscriptionLocator = + getUsersSubscription: sinon.stub().callsArg(1) + @institutionWithV1Data = + name: 'Wombat University' + @institution = + fetchV1Data: sinon.stub().callsArgWith(0, null, @institutionWithV1Data) + @InstitutionModel = + Institution: + findOne: sinon.stub().callsArgWith(1, null, @institution) + @Mongo = + ObjectId: sinon.stub().returnsArg(0) + @InstitutionsManager = SandboxedModule.require modulePath, requires: 'logger-sharelatex': @logger './InstitutionsAPI': getInstitutionAffiliations: @getInstitutionAffiliations '../Subscription/FeaturesUpdater': refreshFeatures: @refreshFeatures - '../User/UserGetter': - getUsersByAnyConfirmedEmail: @getUsersByAnyConfirmedEmail + '../User/UserGetter': @UserGetter + '../Notifications/NotificationsBuilder': @NotificationsBuilder + '../Subscription/SubscriptionLocator': @SubscriptionLocator + '../../models/Institution': @InstitutionModel + '../../infrastructure/mongojs': @Mongo describe 'upgradeInstitutionUsers', -> - it 'refresh all users Features', (done) -> - affiliations = [ - { user_id: '123abc123abc123abc123abc' } - { user_id: '456def456def456def456def' } + beforeEach -> + @user1Id = '123abc123abc123abc123abc' + @user2Id = '456def456def456def456def' + @affiliations = [ + { user_id: @user1Id } + { user_id: @user2Id } ] - @getInstitutionAffiliations.yields(null, affiliations) + @user1 = + _id: @user1Id + @user2 = + _id: @user2Id + @subscription = + planCode: 'pro' + groupPlan: false + @UserGetter.getUser.withArgs(@user1Id).callsArgWith(1, null, @user1) + @UserGetter.getUser.withArgs(@user2Id).callsArgWith(1, null, @user2) + @SubscriptionLocator.getUsersSubscription.withArgs(@user2).callsArgWith(1, null, @subscription) + @refreshFeatures.withArgs(@user1Id).callsArgWith(2, null, {}, true) + @getInstitutionAffiliations.yields(null, @affiliations) + + it 'refresh all users Features', (done) -> @InstitutionsManager.upgradeInstitutionUsers @institutionId, (error) => should.not.exist(error) sinon.assert.calledTwice(@refreshFeatures) done() + it "notifies users if their features have been upgraded", (done) -> + @InstitutionsManager.upgradeInstitutionUsers @institutionId, (error) => + should.not.exist(error) + sinon.assert.calledOnce(@NotificationsBuilder.featuresUpgradedByAffiliation) + sinon.assert.calledWith(@NotificationsBuilder.featuresUpgradedByAffiliation, @affiliations[0], @user1) + done() + + it "notifies users if they have a subscription that should be cancelled", (done) -> + @InstitutionsManager.upgradeInstitutionUsers @institutionId, (error) => + should.not.exist(error) + sinon.assert.calledOnce(@NotificationsBuilder.redundantPersonalSubscription) + sinon.assert.calledWith(@NotificationsBuilder.redundantPersonalSubscription, @affiliations[1], @user2) + done() + + describe 'checkInstitutionUsers', -> it 'check all users Features', (done) -> affiliations = [ @@ -54,11 +107,11 @@ describe "InstitutionsManager", -> } ] @getInstitutionAffiliations.yields(null, affiliations) - @getUsersByAnyConfirmedEmail.yields(null, stubbedUsers) + @UserGetter.getUsersByAnyConfirmedEmail.yields(null, stubbedUsers) @InstitutionsManager.checkInstitutionUsers @institutionId, (error, usersSummary) => should.not.exist(error) usersSummary.totalConfirmedUsers.should.equal 3 usersSummary.totalConfirmedProUsers.should.equal 1 usersSummary.totalConfirmedNonProUsers.should.equal 2 expect(usersSummary.confirmedNonProUsers).to.deep.equal ['456def456def456def456def', '789def789def789def789def'] - done() \ No newline at end of file + done()