mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Properly merge subscriptions from different places
This commit is contained in:
parent
5b3fbe47db
commit
b1f378208d
12 changed files with 389 additions and 42 deletions
|
@ -9,7 +9,7 @@ COFFEE := node_modules/.bin/coffee $(COFFEE_OPTIONS)
|
|||
GRUNT := node_modules/.bin/grunt
|
||||
APP_COFFEE_FILES := $(shell find app/coffee -name '*.coffee')
|
||||
FRONT_END_COFFEE_FILES := $(shell find public/coffee -name '*.coffee')
|
||||
TEST_COFFEE_FILES := $(shell find test -name '*.coffee')
|
||||
TEST_COFFEE_FILES := $(shell find test/*/coffee -name '*.coffee')
|
||||
MODULE_MAIN_COFFEE_FILES := $(shell find modules -type f -wholename '*main/index.coffee')
|
||||
MODULE_IDE_COFFEE_FILES := $(shell find modules -type f -wholename '*ide/index.coffee')
|
||||
COFFEE_FILES := app.coffee $(APP_COFFEE_FILES) $(FRONT_END_COFFEE_FILES) $(TEST_COFFEE_FILES)
|
||||
|
|
|
@ -31,7 +31,7 @@ module.exports = ReferalAllocator =
|
|||
|
||||
|
||||
|
||||
assignBonus: (user_id, callback = (error) ->) ->
|
||||
getBonusFeatures: (user_id, callback = (error) ->) ->
|
||||
query = _id: user_id
|
||||
User.findOne query, (error, user) ->
|
||||
return callback(error) if error
|
||||
|
@ -39,9 +39,7 @@ module.exports = ReferalAllocator =
|
|||
logger.log user_id: user_id, refered_user_count: user.refered_user_count, "assigning bonus"
|
||||
if user.refered_user_count? and user.refered_user_count > 0
|
||||
newFeatures = ReferalAllocator._calculateFeatures(user)
|
||||
if _.isEqual newFeatures, user.features
|
||||
return callback()
|
||||
User.update query, { $set: features: newFeatures }, callback
|
||||
callback null, newFeatures
|
||||
else
|
||||
callback()
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ logger = require('logger-sharelatex')
|
|||
GeoIpLookup = require("../../infrastructure/GeoIpLookup")
|
||||
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
|
||||
UserGetter = require "../User/UserGetter"
|
||||
SubscriptionUpdater = require './SubscriptionUpdater'
|
||||
|
||||
module.exports = SubscriptionController =
|
||||
|
||||
|
@ -237,3 +238,9 @@ module.exports = SubscriptionController =
|
|||
return next(error) if error?
|
||||
req.body = body
|
||||
next()
|
||||
|
||||
refreshUserSubscription: (req, res, next) ->
|
||||
{user_id} = req.params
|
||||
SubscriptionUpdater.refreshSubscription user_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus 200
|
|
@ -28,8 +28,8 @@ module.exports =
|
|||
getSubscriptionByMemberIdAndId: (user_id, subscription_id, callback)->
|
||||
Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback
|
||||
|
||||
getGroupSubscriptionMemberOf: (user_id, callback)->
|
||||
Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
|
||||
getGroupSubscriptionsMemberOf: (user_id, callback)->
|
||||
Subscription.find {member_ids: user_id}, {_id:1, planCode:1}, callback
|
||||
|
||||
getGroupsWithEmailInvite: (email, callback) ->
|
||||
Subscription.find { invited_emails: email }, callback
|
|
@ -46,4 +46,6 @@ module.exports =
|
|||
webRouter.get "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage
|
||||
webRouter.post "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan
|
||||
|
||||
# Currently used in acceptance tests only, as a way to trigger the syncing logic
|
||||
publicApiRouter.post "/user/:user_id/subscription/sync", AuthenticationController.httpAuth, SubscriptionController.refreshUserSubscription
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ Settings = require("settings-sharelatex")
|
|||
logger = require("logger-sharelatex")
|
||||
ObjectId = require('mongoose').Types.ObjectId
|
||||
ReferalAllocator = require("../Referal/ReferalAllocator")
|
||||
V1SubscriptionManager = require("./V1SubscriptionManager")
|
||||
|
||||
oneMonthInSeconds = 60 * 60 * 24 * 30
|
||||
|
||||
|
@ -105,38 +106,66 @@ module.exports = SubscriptionUpdater =
|
|||
|
||||
_setUsersMinimumFeatures: (user_id, callback)->
|
||||
jobs =
|
||||
subscription: (cb)->
|
||||
SubscriptionLocator.getUsersSubscription user_id, cb
|
||||
groupSubscription: (cb)->
|
||||
SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb
|
||||
v1PlanCode: (cb) ->
|
||||
Modules = require '../../infrastructure/Modules'
|
||||
Modules.hooks.fire 'getV1PlanCode', user_id, (err, results) ->
|
||||
cb(err, results?[0] || null)
|
||||
individualFeatures: (cb)->
|
||||
SubscriptionLocator.getUsersSubscription user_id, (err, sub)->
|
||||
cb err, SubscriptionUpdater._subscriptionToFeatures(sub)
|
||||
groupFeatures: (cb) ->
|
||||
SubscriptionLocator.getGroupSubscriptionsMemberOf user_id, (err, subs) ->
|
||||
cb err, (subs or []).map SubscriptionUpdater._subscriptionToFeatures
|
||||
v1Features: (cb) ->
|
||||
V1SubscriptionManager.getPlanCodeFromV1 user_id, (err, planCode) ->
|
||||
cb err, SubscriptionUpdater._planCodeToFeatures(planCode)
|
||||
bonusFeatures: (cb) ->
|
||||
ReferalAllocator.getBonusFeatures user_id, cb
|
||||
async.series jobs, (err, results)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user_id,
|
||||
"error getting subscription or group for _setUsersMinimumFeatures"
|
||||
return callback(err)
|
||||
{subscription, groupSubscription, v1PlanCode} = results
|
||||
# Group Subscription
|
||||
if groupSubscription? and groupSubscription.planCode?
|
||||
logger.log user_id:user_id, "using group which user is memor of for features"
|
||||
UserFeaturesUpdater.updateFeatures user_id, groupSubscription.planCode, callback
|
||||
# Personal Subscription
|
||||
else if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode
|
||||
logger.log user_id:user_id, "using users subscription plan code for features"
|
||||
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
|
||||
# V1 Subscription
|
||||
else if v1PlanCode?
|
||||
logger.log user_id: user_id, "using the V1 plan for features"
|
||||
UserFeaturesUpdater.updateFeatures user_id, v1PlanCode, callback
|
||||
# Default
|
||||
else
|
||||
logger.log user_id:user_id, "using default features for user with no subscription or group"
|
||||
UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user_id, "Error setting minimum user feature"
|
||||
return callback(err)
|
||||
ReferalAllocator.assignBonus user_id, callback
|
||||
|
||||
{individualFeatures, groupFeatures, v1Features, bonusFeatures} = results
|
||||
logger.log {user_id, individualFeatures, groupFeatures, v1Features, bonusFeatures}, 'merging user features'
|
||||
featureSets = groupFeatures.concat [individualFeatures, v1Features, bonusFeatures]
|
||||
features = _.reduce(featureSets, SubscriptionUpdater._mergeFeatures, Settings.defaultFeatures)
|
||||
|
||||
logger.log {user_id, features}, 'updating user features'
|
||||
UserFeaturesUpdater.updateFeatures user_id, features, callback
|
||||
|
||||
_mergeFeatures: (featuresA, featuresB) ->
|
||||
features = Object.assign({}, featuresA)
|
||||
for key, value of featuresB
|
||||
# Special merging logic for non-boolean features
|
||||
if key == 'compileGroup'
|
||||
if features['compileGroup'] == 'priority' or featuresB['compileGroup'] == 'priority'
|
||||
features['compileGroup'] = 'priority'
|
||||
else
|
||||
features['compileGroup'] = 'standard'
|
||||
else if key == 'collaborators'
|
||||
if features['collaborators'] == -1 or featuresB['collaborators'] == -1
|
||||
features['collaborators'] = -1
|
||||
else
|
||||
features['collaborators'] = Math.max(
|
||||
features['collaborators'] or 0,
|
||||
featuresB['collaborators'] or 0
|
||||
)
|
||||
else if key == 'compileTimeout'
|
||||
features['compileTimeout'] = Math.max(
|
||||
features['compileTimeout'] or 0,
|
||||
featuresB['compileTimeout'] or 0
|
||||
)
|
||||
else
|
||||
# Boolean keys, true is better
|
||||
features[key] = features[key] or featuresB[key]
|
||||
return features
|
||||
|
||||
_subscriptionToFeatures: (subscription) ->
|
||||
SubscriptionUpdater._planCodeToFeatures(subscription?.planCode)
|
||||
|
||||
_planCodeToFeatures: (planCode) ->
|
||||
if !planCode?
|
||||
return {}
|
||||
plan = PlansLocator.findLocalPlanInSettings planCode
|
||||
if !plan?
|
||||
return {}
|
||||
else
|
||||
return plan.features
|
|
@ -4,12 +4,11 @@ PlansLocator = require("./PlansLocator")
|
|||
|
||||
module.exports =
|
||||
|
||||
updateFeatures: (user_id, plan_code, callback = (err, features)->)->
|
||||
updateFeatures: (user_id, features, callback = (err, features)->)->
|
||||
conditions = _id:user_id
|
||||
update = {}
|
||||
plan = PlansLocator.findLocalPlanInSettings(plan_code)
|
||||
logger.log user_id:user_id, features:plan.features, plan_code:plan_code, "updating users features"
|
||||
update["features.#{key}"] = value for key, value of plan.features
|
||||
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, plan.features
|
||||
callback err, features
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
UserGetter = require "../User/UserGetter"
|
||||
request = require "request"
|
||||
settings = require "settings-sharelatex"
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
module.exports = V1SubscriptionManager =
|
||||
# Returned planCode = 'v1_pro' | 'v1_pro_plus' | 'v1_student' | 'v1_free' | null
|
||||
# For this to work, we need plans in settings with plan-codes:
|
||||
# - 'v1_pro'
|
||||
# - 'v1_pro_plus'
|
||||
# - 'v1_student'
|
||||
# - 'v1_free'
|
||||
getPlanCodeFromV1: (userId, callback=(err, planCode)->) ->
|
||||
logger.log {userId}, "[V1SubscriptionManager] fetching v1 plan for user"
|
||||
UserGetter.getUser userId, {'overleaf.id': 1}, (err, user) ->
|
||||
return callback(err) if err?
|
||||
v1Id = user?.overleaf?.id
|
||||
if !v1Id?
|
||||
logger.log {userId}, "[V1SubscriptionManager] no v1 id found for user"
|
||||
return callback(null, null)
|
||||
V1SubscriptionManager._v1PlanRequest v1Id, (err, body) ->
|
||||
return callback(err) if err?
|
||||
planName = body.plan_name
|
||||
logger.log {userId, planName, body}, "[V1SubscriptionManager] fetched v1 plan for user"
|
||||
if planName in ['pro', 'pro_plus', 'student', 'free']
|
||||
planName = "v1_#{planName}"
|
||||
else
|
||||
# Throw away 'anonymous', etc as being equivalent to null
|
||||
planName = null
|
||||
return callback(null, planName)
|
||||
|
||||
_v1PlanRequest: (v1Id, callback=(err, body)->) ->
|
||||
request {
|
||||
method: 'GET',
|
||||
url: settings.apis.v1.url +
|
||||
"/api/v1/sharelatex/users/#{v1Id}/plan_code"
|
||||
auth:
|
||||
user: settings.apis.v1.user
|
||||
pass: settings.apis.v1.pass
|
||||
sendImmediately: true
|
||||
json: true,
|
||||
timeout: 5 * 1000
|
||||
}, (error, response, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= response.statusCode < 300
|
||||
return callback null, body
|
||||
else
|
||||
return callback new Error("non-success code from v1: #{response.statusCode}")
|
|
@ -17,6 +17,7 @@ services:
|
|||
PROJECT_HISTORY_ENABLED: 'true'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url'
|
||||
LINKED_URL_PROXY: 'http://localhost:6543'
|
||||
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
|
||||
depends_on:
|
||||
- redis
|
||||
- mongo
|
||||
|
|
151
services/web/test/acceptance/coffee/SubscriptionTests.coffee
Normal file
151
services/web/test/acceptance/coffee/SubscriptionTests.coffee
Normal file
|
@ -0,0 +1,151 @@
|
|||
expect = require("chai").expect
|
||||
async = require("async")
|
||||
UserClient = require "./helpers/User"
|
||||
request = require "./helpers/request"
|
||||
settings = require "settings-sharelatex"
|
||||
{ObjectId} = require("../../../app/js/infrastructure/mongojs")
|
||||
Subscription = require("../../../app/js/models/Subscription").Subscription
|
||||
User = require("../../../app/js/models/User").User
|
||||
|
||||
MockV1Api = require "./helpers/MockV1Api"
|
||||
|
||||
syncUserAndGetFeatures = (user, callback = (error, features) ->) ->
|
||||
request {
|
||||
method: 'POST',
|
||||
url: "/user/#{user._id}/subscription/sync",
|
||||
auth:
|
||||
user: 'sharelatex'
|
||||
pass: 'password'
|
||||
sendImmediately: true
|
||||
}, (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
User.findById user._id, (error, user) ->
|
||||
return callback(error) if error?
|
||||
features = user.toObject().features
|
||||
delete features.$init # mongoose internals
|
||||
return callback null, features
|
||||
|
||||
describe "Subscriptions", ->
|
||||
beforeEach (done) ->
|
||||
@user = new UserClient()
|
||||
@user.ensureUserExists (error) ->
|
||||
throw error if error?
|
||||
done()
|
||||
|
||||
describe "when user has no subscriptions", ->
|
||||
it "should set their features to the basic set", (done) ->
|
||||
syncUserAndGetFeatures @user, (error, features) =>
|
||||
throw error if error?
|
||||
expect(features).to.deep.equal(settings.defaultFeatures)
|
||||
done()
|
||||
|
||||
describe "when the user has an individual subscription", ->
|
||||
beforeEach ->
|
||||
Subscription.create {
|
||||
admin_id: @user._id
|
||||
planCode: 'collaborator'
|
||||
customAccount: true
|
||||
} # returns a promise
|
||||
|
||||
it "should set their features to the upgraded set", (done) ->
|
||||
syncUserAndGetFeatures @user, (error, features) =>
|
||||
throw error if error?
|
||||
plan = settings.plans.find (plan) -> plan.planCode == 'collaborator'
|
||||
expect(features).to.deep.equal(plan.features)
|
||||
done()
|
||||
|
||||
describe "when the user is in a group subscription", ->
|
||||
beforeEach ->
|
||||
Subscription.create {
|
||||
admin_id: ObjectId()
|
||||
member_ids: [@user._id]
|
||||
groupAccount: true
|
||||
planCode: 'collaborator'
|
||||
customAccount: true
|
||||
} # returns a promise
|
||||
|
||||
it "should set their features to the upgraded set", (done) ->
|
||||
syncUserAndGetFeatures @user, (error, features) =>
|
||||
throw error if error?
|
||||
plan = settings.plans.find (plan) -> plan.planCode == 'collaborator'
|
||||
expect(features).to.deep.equal(plan.features)
|
||||
done()
|
||||
|
||||
describe "when the user has bonus features", ->
|
||||
beforeEach ->
|
||||
User.update {
|
||||
_id: @user._id
|
||||
}, {
|
||||
refered_user_count: 10
|
||||
} # returns a promise
|
||||
|
||||
it "should set their features to the bonus set", (done) ->
|
||||
syncUserAndGetFeatures @user, (error, features) =>
|
||||
throw error if error?
|
||||
expect(features).to.deep.equal(Object.assign(
|
||||
{}, settings.defaultFeatures, settings.bonus_features[9]
|
||||
))
|
||||
done()
|
||||
|
||||
describe "when the user has a v1 plan", ->
|
||||
beforeEach ->
|
||||
MockV1Api.setUser 42, plan_name: 'free'
|
||||
User.update {
|
||||
_id: @user._id
|
||||
}, {
|
||||
overleaf:
|
||||
id: 42
|
||||
} # returns a promise
|
||||
|
||||
it "should set their features to the v1 plan", (done) ->
|
||||
syncUserAndGetFeatures @user, (error, features) =>
|
||||
throw error if error?
|
||||
plan = settings.plans.find (plan) -> plan.planCode == 'v1_free'
|
||||
expect(features).to.deep.equal(plan.features)
|
||||
done()
|
||||
|
||||
describe "when the user has a v1 plan and bonus features", ->
|
||||
beforeEach ->
|
||||
MockV1Api.setUser 42, plan_name: 'free'
|
||||
User.update {
|
||||
_id: @user._id
|
||||
}, {
|
||||
overleaf:
|
||||
id: 42
|
||||
refered_user_count: 10
|
||||
} # returns a promise
|
||||
|
||||
it "should set their features to the best of the v1 plan and bonus features", (done) ->
|
||||
syncUserAndGetFeatures @user, (error, features) =>
|
||||
throw error if error?
|
||||
v1plan = settings.plans.find (plan) -> plan.planCode == 'v1_free'
|
||||
expectedFeatures = Object.assign(
|
||||
{}, v1plan.features, settings.bonus_features[9]
|
||||
)
|
||||
expect(features).to.deep.equal(expectedFeatures)
|
||||
done()
|
||||
|
||||
describe "when the user has a group and personal subscription", ->
|
||||
beforeEach (done) ->
|
||||
Subscription.create {
|
||||
admin_id: @user._id
|
||||
planCode: 'professional'
|
||||
customAccount: true
|
||||
}, (error) =>
|
||||
throw error if error?
|
||||
Subscription.create {
|
||||
admin_id: ObjectId()
|
||||
member_ids: [@user._id]
|
||||
groupAccount: true
|
||||
planCode: 'collaborator'
|
||||
customAccount: true
|
||||
}, done
|
||||
return
|
||||
|
||||
it "should set their features to the best set", (done) ->
|
||||
syncUserAndGetFeatures @user, (error, features) =>
|
||||
throw error if error?
|
||||
plan = settings.plans.find (plan) -> plan.planCode == 'professional'
|
||||
expect(features).to.deep.equal(plan.features)
|
||||
done()
|
|
@ -5,6 +5,10 @@ bodyParser = require('body-parser')
|
|||
app.use(bodyParser.json())
|
||||
|
||||
module.exports = MockV1Api =
|
||||
users: { }
|
||||
|
||||
setUser: (id, user) ->
|
||||
@users[id] = user
|
||||
|
||||
exportId: null
|
||||
|
||||
|
@ -20,6 +24,13 @@ module.exports = MockV1Api =
|
|||
@exportParams = null
|
||||
|
||||
run: () ->
|
||||
app.get "/api/v1/sharelatex/users/:ol_user_id/plan_code", (req, res, next) =>
|
||||
user = @users[req.params.ol_user_id]
|
||||
if user
|
||||
res.json user
|
||||
else
|
||||
res.sendStatus 404
|
||||
|
||||
app.post "/api/v1/sharelatex/exports", (req, res, next) =>
|
||||
#{project, version, pathname}
|
||||
@exportParams = Object.assign({}, req.body)
|
||||
|
@ -28,7 +39,7 @@ module.exports = MockV1Api =
|
|||
app.listen 5000, (error) ->
|
||||
throw error if error?
|
||||
.on "error", (error) ->
|
||||
console.error "error starting MockOverleafAPI:", error.message
|
||||
console.error "error starting MockV1Api:", error.message
|
||||
process.exit(1)
|
||||
|
||||
MockV1Api.run()
|
||||
|
|
101
services/web/test/acceptance/config/settings.test.coffee
Normal file
101
services/web/test/acceptance/config/settings.test.coffee
Normal file
|
@ -0,0 +1,101 @@
|
|||
module.exports =
|
||||
apis:
|
||||
v1:
|
||||
url: "http://localhost:5000"
|
||||
user: 'overleaf'
|
||||
pass: 'password'
|
||||
|
||||
enableSubscriptions: true
|
||||
|
||||
features: features =
|
||||
v1_free:
|
||||
collaborators: 1
|
||||
dropbox: false
|
||||
versioning: false
|
||||
github: true
|
||||
templates: false
|
||||
references: false
|
||||
referencesSearch: false
|
||||
mendeley: true
|
||||
compileTimeout: 60
|
||||
compileGroup: "standard"
|
||||
trackChanges: false
|
||||
personal:
|
||||
collaborators: 1
|
||||
dropbox: false
|
||||
versioning: false
|
||||
github: false
|
||||
templates: false
|
||||
references: false
|
||||
referencesSearch: false
|
||||
mendeley: false
|
||||
compileTimeout: 60
|
||||
compileGroup: "standard"
|
||||
trackChanges: false
|
||||
collaborator:
|
||||
collaborators: 10
|
||||
dropbox: true
|
||||
versioning: true
|
||||
github: true
|
||||
templates: true
|
||||
references: true
|
||||
referencesSearch: true
|
||||
mendeley: true
|
||||
compileTimeout: 180
|
||||
compileGroup: "priority"
|
||||
trackChanges: true
|
||||
professional:
|
||||
collaborators: -1
|
||||
dropbox: true
|
||||
versioning: true
|
||||
github: true
|
||||
templates: true
|
||||
references: true
|
||||
referencesSearch: true
|
||||
mendeley: true
|
||||
compileTimeout: 180
|
||||
compileGroup: "priority"
|
||||
trackChanges: true
|
||||
|
||||
defaultFeatures: features.personal
|
||||
defaultPlanCode: 'personal'
|
||||
|
||||
plans: plans = [{
|
||||
planCode: "v1_free"
|
||||
name: "V1 Free"
|
||||
price: 0
|
||||
features: features.v1_free
|
||||
},{
|
||||
planCode: "personal"
|
||||
name: "Personal"
|
||||
price: 0
|
||||
features: features.personal
|
||||
},{
|
||||
planCode: "collaborator"
|
||||
name: "Collaborator"
|
||||
price: 1500
|
||||
features: features.collaborator
|
||||
},{
|
||||
planCode: "professional"
|
||||
name: "Professional"
|
||||
price: 3000
|
||||
features: features.professional
|
||||
}]
|
||||
|
||||
bonus_features:
|
||||
1:
|
||||
collaborators: 2
|
||||
dropbox: false
|
||||
versioning: false
|
||||
3:
|
||||
collaborators: 4
|
||||
dropbox: false
|
||||
versioning: false
|
||||
6:
|
||||
collaborators: 4
|
||||
dropbox: true
|
||||
versioning: true
|
||||
9:
|
||||
collaborators: -1
|
||||
dropbox: true
|
||||
versioning: true
|
Loading…
Reference in a new issue