mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
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:
parent
b8bb118fe3
commit
838fe00058
5 changed files with 142 additions and 20 deletions
|
@ -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
|
||||||
|
|
|
@ -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=()->) ->
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue