diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee new file mode 100644 index 0000000000..5c91058146 --- /dev/null +++ b/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee @@ -0,0 +1,23 @@ +UserGetter = require '../User/UserGetter' +PlansLocator = require '../Subscription/PlansLocator' +Settings = require 'settings-sharelatex' +logger = require 'logger-sharelatex' + +module.exports = InstitutionsFeatures = + getInstitutionsFeatures: (userId, callback = (error, features) ->) -> + InstitutionsFeatures.hasLicence userId, (error, hasLicence) -> + return callback error if error? + return callback(null, {}) unless hasLicence + plan = PlansLocator.findLocalPlanInSettings Settings.institutionPlanCode + callback(null, plan?.features or {}) + + + hasLicence: (userId, callback = (error, hasLicence) ->) -> + UserGetter.getUserFullEmails userId, (error, emailsData) -> + return callback error if error? + + affiliation = emailsData.find (emailData) -> + licence = emailData.affiliation?.institution?.licence + emailData.confirmedAt? and licence? and licence != 'free' + + callback(null, !!affiliation) diff --git a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee b/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee index 5627072c93..ef5a6c4bb2 100644 --- a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee @@ -7,6 +7,7 @@ Settings = require("settings-sharelatex") logger = require("logger-sharelatex") ReferalFeatures = require("../Referal/ReferalFeatures") V1SubscriptionManager = require("./V1SubscriptionManager") +InstitutionsFeatures = require '../Institutions/InstitutionsFeatures' oneMonthInSeconds = 60 * 60 * 24 * 30 @@ -21,9 +22,11 @@ module.exports = FeaturesUpdater = if error? logger.err {err: error, user_id}, "error notifying v1 about updated features" + jobs = individualFeatures: (cb) -> FeaturesUpdater._getIndividualFeatures user_id, cb groupFeatureSets: (cb) -> FeaturesUpdater._getGroupFeatureSets user_id, cb + institutionFeatures:(cb) -> InstitutionsFeatures.getInstitutionsFeatures user_id, cb v1Features: (cb) -> FeaturesUpdater._getV1Features user_id, cb bonusFeatures: (cb) -> ReferalFeatures.getBonusFeatures user_id, cb async.series jobs, (err, results)-> @@ -32,9 +35,9 @@ module.exports = FeaturesUpdater = "error getting subscription or group for refreshFeatures" return callback(err) - {individualFeatures, groupFeatureSets, v1Features, bonusFeatures} = results - logger.log {user_id, individualFeatures, groupFeatureSets, v1Features, bonusFeatures}, 'merging user features' - featureSets = groupFeatureSets.concat [individualFeatures, v1Features, bonusFeatures] + {individualFeatures, groupFeatureSets, institutionFeatures, v1Features, bonusFeatures} = results + logger.log {user_id, individualFeatures, groupFeatureSets, institutionFeatures, v1Features, bonusFeatures}, 'merging user features' + featureSets = groupFeatureSets.concat [individualFeatures, institutionFeatures, v1Features, bonusFeatures] features = _.reduce(featureSets, FeaturesUpdater._mergeFeatures, Settings.defaultFeatures) logger.log {user_id, features}, 'updating user features' diff --git a/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee b/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee index 7c6c106fe2..29c0cce6f1 100644 --- a/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee +++ b/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee @@ -83,6 +83,27 @@ describe "FeatureUpdater.refreshFeatures", -> )) done() + describe "when the user has affiliations", -> + beforeEach -> + @institutionPlan = settings.plans.find (plan) -> + plan.planCode == settings.institutionPlanCode + @email = @user.emails[0].email + affiliationData = + email: @email + institution: { licence: 'pro_plus' } + MockV1Api.setAffiliations [affiliationData] + + it "should not set their features if email is not confirmed", (done) -> + syncUserAndGetFeatures @user, (error, features) => + expect(features).to.deep.equal(settings.defaultFeatures) + done() + + it "should set their features if email is confirmed", (done) -> + @user.confirmEmail @email, (error) => + syncUserAndGetFeatures @user, (error, features) => + expect(features).to.deep.equal(@institutionPlan.features) + done() + describe "when the user is due bonus features and has extra features that no longer apply", -> beforeEach -> User.update { diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee index f537614f17..7599230b09 100644 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee @@ -26,6 +26,10 @@ module.exports = MockV1Api = syncUserFeatures: sinon.stub() + affiliations: [] + + setAffiliations: (affiliations) -> @affiliations = affiliations + run: () -> app.get "/api/v1/sharelatex/users/:v1_user_id/plan_code", (req, res, next) => user = @users[req.params.v1_user_id] @@ -43,7 +47,7 @@ module.exports = MockV1Api = res.json exportId: @exportId app.get "/api/v2/users/:userId/affiliations", (req, res, next) => - res.json [] + res.json @affiliations app.post "/api/v2/users/:userId/affiliations", (req, res, next) => res.sendStatus 201 diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index 54b660c6b5..b8740f709c 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -100,6 +100,11 @@ class User @emails.push(email: email, createdAt: new Date()) UserUpdater.addEmailAddress @id, email, callback + confirmEmail: (email, callback = (error) ->) -> + for emailData, idx in @emails + @emails[idx].confirmedAt = new Date() if emailData.email == email + UserUpdater.confirmEmail @id, email, callback + ensure_admin: (callback = (error) ->) -> db.users.update {_id: ObjectId(@id)}, { $set: { isAdmin: true }}, callback diff --git a/services/web/test/acceptance/config/settings.test.coffee b/services/web/test/acceptance/config/settings.test.coffee index b64090da4b..4dd820f4a7 100644 --- a/services/web/test/acceptance/config/settings.test.coffee +++ b/services/web/test/acceptance/config/settings.test.coffee @@ -56,6 +56,7 @@ module.exports = defaultFeatures: features.personal defaultPlanCode: 'personal' + institutionPlanCode: 'professional' plans: plans = [{ planCode: "v1_free" diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee new file mode 100644 index 0000000000..2304f2e5b7 --- /dev/null +++ b/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee @@ -0,0 +1,96 @@ +SandboxedModule = require('sandboxed-module') +assert = require('assert') +require('chai').should() +expect = require('chai').expect +sinon = require('sinon') +modulePath = require('path').join __dirname, '../../../../app/js/Features/Institutions/InstitutionsFeatures.js' + +describe 'InstitutionsFeatures', -> + + beforeEach -> + @UserGetter = getUserFullEmails: sinon.stub() + @PlansLocator = findLocalPlanInSettings: sinon.stub() + @institutionPlanCode = 'institution_plan_code' + @InstitutionsFeatures = SandboxedModule.require modulePath, requires: + '../User/UserGetter': @UserGetter + '../Subscription/PlansLocator': @PlansLocator + 'settings-sharelatex': institutionPlanCode: @institutionPlanCode + 'logger-sharelatex': + log:-> + err:-> + + @userId = '12345abcde' + + describe "hasLicence", -> + it 'should handle error', (done)-> + @UserGetter.getUserFullEmails.yields(new Error('Nope')) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.exist + done() + + it 'should return false if user has no affiliations', (done) -> + @UserGetter.getUserFullEmails.yields(null, []) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.not.exist + expect(hasLicence).to.be.false + done() + + it 'should return false if user has no confirmed affiliations', (done) -> + affiliations = [ + { confirmedAt: null, affiliation: institution: { licence: 'pro_plus' } } + ] + @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.not.exist + expect(hasLicence).to.be.false + done() + + it 'should return false if user has no paid affiliations', (done) -> + affiliations = [ + { confirmedAt: new Date(), affiliation: institution: { licence: 'free' } } + ] + @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.not.exist + expect(hasLicence).to.be.false + done() + + it 'should return true if user has confirmed paid affiliation', (done)-> + affiliations = [ + { confirmedAt: new Date(), affiliation: institution: { licence: 'pro_plus' } } + { confirmedAt: new Date(), affiliation: institution: { licence: 'free' } } + { confirmedAt: null, affiliation: institution: { licence: 'pro' } } + { confirmedAt: null, affiliation: institution: { licence: null } } + { confirmedAt: new Date(), affiliation: institution: {} } + ] + @UserGetter.getUserFullEmails.yields(null, affiliations) + @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> + expect(error).to.not.exist + expect(hasLicence).to.be.true + done() + + describe "getInstitutionsFeatures", -> + beforeEach -> + @InstitutionsFeatures.hasLicence = sinon.stub() + @testFeatures = features: { institution: 'all' } + @PlansLocator.findLocalPlanInSettings.withArgs(@institutionPlanCode).returns(@testFeatures) + + it 'should handle error', (done)-> + @InstitutionsFeatures.hasLicence.yields(new Error('Nope')) + @InstitutionsFeatures.getInstitutionsFeatures @userId, (error, features) -> + expect(error).to.exist + done() + + it 'should return no feaures if user has no plan code', (done) -> + @InstitutionsFeatures.hasLicence.yields(null, false) + @InstitutionsFeatures.getInstitutionsFeatures @userId, (error, features) -> + expect(error).to.not.exist + expect(features).to.deep.equal {} + done() + + it 'should return feaures if user has affiliations plan code', (done) -> + @InstitutionsFeatures.hasLicence.yields(null, true) + @InstitutionsFeatures.getInstitutionsFeatures @userId, (error, features) => + expect(error).to.not.exist + expect(features).to.deep.equal @testFeatures.features + done() diff --git a/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee index 489c5676ff..0b4378c5e2 100644 --- a/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee +++ b/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee @@ -19,6 +19,7 @@ describe "FeaturesUpdater", -> 'settings-sharelatex': @Settings = {} "../Referal/ReferalFeatures" : @ReferalFeatures = {} "./V1SubscriptionManager": @V1SubscriptionManager = {} + '../Institutions/InstitutionsFeatures': @InstitutionsFeatures = {} describe "refreshFeatures", -> beforeEach -> @@ -26,6 +27,7 @@ describe "FeaturesUpdater", -> @UserFeaturesUpdater.updateFeatures = sinon.stub().yields() @FeaturesUpdater._getIndividualFeatures = sinon.stub().yields(null, { 'individual': 'features' }) @FeaturesUpdater._getGroupFeatureSets = sinon.stub().yields(null, [{ 'group': 'features' }, { 'group': 'features2' }]) + @InstitutionsFeatures.getInstitutionsFeatures = sinon.stub().yields(null, { 'institutions': 'features' }) @FeaturesUpdater._getV1Features = sinon.stub().yields(null, { 'v1': 'features' }) @ReferalFeatures.getBonusFeatures = sinon.stub().yields(null, { 'bonus': 'features' }) @FeaturesUpdater._mergeFeatures = sinon.stub().returns({'merged': 'features'}) @@ -45,6 +47,11 @@ describe "FeaturesUpdater", -> .calledWith(@user_id) .should.equal true + it "should get the institution features", -> + @InstitutionsFeatures.getInstitutionsFeatures + .calledWith(@user_id) + .should.equal true + it "should get the v1 features", -> @FeaturesUpdater._getV1Features .calledWith(@user_id) @@ -65,6 +72,9 @@ describe "FeaturesUpdater", -> @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features' }).should.equal true @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features2' }).should.equal true + it "should merge the institutions features", -> + @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'institutions': 'features' }).should.equal true + it "should merge the v1 features", -> @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'v1': 'features' }).should.equal true