Merge pull request #1531 from sharelatex/spd-notify-users-on-affiliation-upgrade

Notify users when affiliations are upgraded

GitOrigin-RevId: 0f9e92b6a49f2ddef559e9e23fc73436910fb9f6
This commit is contained in:
Simon Detheridge 2019-02-25 10:22:47 +00:00 committed by James Allen
parent b8bb118fe3
commit 838fe00058
5 changed files with 142 additions and 20 deletions

View file

@ -1,17 +1,29 @@
logger = require 'logger-sharelatex' logger = require 'logger-sharelatex'
async = require 'async' async = require 'async'
db = require("../../infrastructure/mongojs").db db = require("../../infrastructure/mongojs").db
_ = require("underscore")
ObjectId = require("../../infrastructure/mongojs").ObjectId ObjectId = require("../../infrastructure/mongojs").ObjectId
{ getInstitutionAffiliations } = require('./InstitutionsAPI') { getInstitutionAffiliations } = require('./InstitutionsAPI')
FeaturesUpdater = require('../Subscription/FeaturesUpdater') FeaturesUpdater = require('../Subscription/FeaturesUpdater')
UserGetter = require('../User/UserGetter') UserGetter = require('../User/UserGetter')
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
SubscriptionLocator = require("../Subscription/SubscriptionLocator")
Institution = require("../../models/Institution").Institution
ASYNC_LIMIT = 10 ASYNC_LIMIT = 10
module.exports = InstitutionsManager = module.exports = InstitutionsManager =
upgradeInstitutionUsers: (institutionId, callback = (error) ->) -> upgradeInstitutionUsers: (institutionId, callback = (error) ->) ->
getInstitutionAffiliations institutionId, (error, affiliations) -> async.waterfall [
return callback(error) if error (cb) ->
async.eachLimit affiliations, ASYNC_LIMIT, refreshFeatures, callback 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) ->) -> checkInstitutionUsers: (institutionId, callback = (error) ->) ->
getInstitutionAffiliations institutionId, (error, affiliations) -> getInstitutionAffiliations institutionId, (error, affiliations) ->
UserGetter.getUsersByAnyConfirmedEmail( UserGetter.getUsersByAnyConfirmedEmail(
@ -21,9 +33,48 @@ module.exports = InstitutionsManager =
callback(error, checkFeatures(users)) 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) -> refreshFeatures = (affiliation, callback) ->
userId = ObjectId(affiliation.user_id) 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) -> checkFeatures = (users) ->
usersSummary = { usersSummary = {
@ -39,4 +90,4 @@ checkFeatures = (users) ->
usersSummary.totalConfirmedNonProUsers += 1 usersSummary.totalConfirmedNonProUsers += 1
usersSummary.confirmedNonProUsers.push user._id usersSummary.confirmedNonProUsers.push user._id
) )
return usersSummary return usersSummary

View file

@ -4,8 +4,26 @@ request = require "request"
settings = require "settings-sharelatex" settings = require "settings-sharelatex"
module.exports = 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) -> projectInvite: (invite, project, sendingUser, user) ->
key: "project-invite-#{invite._id}" key: "project-invite-#{invite._id}"
create: (callback=()->) -> create: (callback=()->) ->

View file

@ -12,7 +12,7 @@ InstitutionsFeatures = require '../Institutions/InstitutionsFeatures'
oneMonthInSeconds = 60 * 60 * 24 * 30 oneMonthInSeconds = 60 * 60 * 24 * 30
module.exports = FeaturesUpdater = module.exports = FeaturesUpdater =
refreshFeatures: (user_id, notifyV1 = true, callback = () ->)-> refreshFeatures: (user_id, notifyV1 = true, callback = (error, features, featuresChanged) ->)->
if typeof notifyV1 == 'function' if typeof notifyV1 == 'function'
callback = notifyV1 callback = notifyV1
notifyV1 = true notifyV1 = true

View file

@ -2,11 +2,11 @@ logger = require("logger-sharelatex")
User = require('../../models/User').User User = require('../../models/User').User
module.exports = module.exports =
updateFeatures: (user_id, features, callback = (err, features)->)-> updateFeatures: (user_id, features, callback = (err, features, featuresChanged)->)->
conditions = _id:user_id conditions = _id:user_id
update = {} update = {}
logger.log user_id:user_id, features:features, "updating users features" logger.log user_id:user_id, features:features, "updating users features"
update["features.#{key}"] = value for key, value of features update["features.#{key}"] = value for key, value of features
User.update conditions, update, (err)-> User.update conditions, update, (err, result)->
callback err, features callback err, features, result?.nModified == 1

View file

@ -9,30 +9,83 @@ describe "InstitutionsManager", ->
beforeEach -> beforeEach ->
@institutionId = 123 @institutionId = 123
@logger = log: -> @logger = log: ->
@user = {}
@getInstitutionAffiliations = sinon.stub() @getInstitutionAffiliations = sinon.stub()
@refreshFeatures = sinon.stub().yields() @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: @InstitutionsManager = SandboxedModule.require modulePath, requires:
'logger-sharelatex': @logger 'logger-sharelatex': @logger
'./InstitutionsAPI': './InstitutionsAPI':
getInstitutionAffiliations: @getInstitutionAffiliations getInstitutionAffiliations: @getInstitutionAffiliations
'../Subscription/FeaturesUpdater': '../Subscription/FeaturesUpdater':
refreshFeatures: @refreshFeatures refreshFeatures: @refreshFeatures
'../User/UserGetter': '../User/UserGetter': @UserGetter
getUsersByAnyConfirmedEmail: @getUsersByAnyConfirmedEmail '../Notifications/NotificationsBuilder': @NotificationsBuilder
'../Subscription/SubscriptionLocator': @SubscriptionLocator
'../../models/Institution': @InstitutionModel
'../../infrastructure/mongojs': @Mongo
describe 'upgradeInstitutionUsers', -> describe 'upgradeInstitutionUsers', ->
it 'refresh all users Features', (done) -> beforeEach ->
affiliations = [ @user1Id = '123abc123abc123abc123abc'
{ user_id: '123abc123abc123abc123abc' } @user2Id = '456def456def456def456def'
{ user_id: '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) => @InstitutionsManager.upgradeInstitutionUsers @institutionId, (error) =>
should.not.exist(error) should.not.exist(error)
sinon.assert.calledTwice(@refreshFeatures) sinon.assert.calledTwice(@refreshFeatures)
done() 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', -> describe 'checkInstitutionUsers', ->
it 'check all users Features', (done) -> it 'check all users Features', (done) ->
affiliations = [ affiliations = [
@ -54,11 +107,11 @@ describe "InstitutionsManager", ->
} }
] ]
@getInstitutionAffiliations.yields(null, affiliations) @getInstitutionAffiliations.yields(null, affiliations)
@getUsersByAnyConfirmedEmail.yields(null, stubbedUsers) @UserGetter.getUsersByAnyConfirmedEmail.yields(null, stubbedUsers)
@InstitutionsManager.checkInstitutionUsers @institutionId, (error, usersSummary) => @InstitutionsManager.checkInstitutionUsers @institutionId, (error, usersSummary) =>
should.not.exist(error) should.not.exist(error)
usersSummary.totalConfirmedUsers.should.equal 3 usersSummary.totalConfirmedUsers.should.equal 3
usersSummary.totalConfirmedProUsers.should.equal 1 usersSummary.totalConfirmedProUsers.should.equal 1
usersSummary.totalConfirmedNonProUsers.should.equal 2 usersSummary.totalConfirmedNonProUsers.should.equal 2
expect(usersSummary.confirmedNonProUsers).to.deep.equal ['456def456def456def456def', '789def789def789def789def'] expect(usersSummary.confirmedNonProUsers).to.deep.equal ['456def456def456def456def', '789def789def789def789def']
done() done()