Merge pull request #4071 from overleaf/ab-subscription-decaf-cleanup

Subscription controller decaf cleanup

GitOrigin-RevId: 79b8adfabe30e4557a95b1aad71a5162e6f42cce
This commit is contained in:
Alexandre Bourdin 2021-05-26 14:26:59 +02:00 committed by Copybot
parent b93761f275
commit 18d62dcee9
7 changed files with 826 additions and 1130 deletions

View file

@ -1,4 +1,3 @@
let LimitationsManager
const OError = require('@overleaf/o-error') const OError = require('@overleaf/o-error')
const logger = require('logger-sharelatex') const logger = require('logger-sharelatex')
const ProjectGetter = require('../Project/ProjectGetter') const ProjectGetter = require('../Project/ProjectGetter')
@ -9,8 +8,9 @@ const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
const CollaboratorsInvitesHandler = require('../Collaborators/CollaboratorsInviteHandler') const CollaboratorsInvitesHandler = require('../Collaborators/CollaboratorsInviteHandler')
const V1SubscriptionManager = require('./V1SubscriptionManager') const V1SubscriptionManager = require('./V1SubscriptionManager')
const { V1ConnectionError } = require('../Errors/Errors') const { V1ConnectionError } = require('../Errors/Errors')
const { promisifyAll } = require('../../util/promises')
module.exports = LimitationsManager = { const LimitationsManager = {
allowedNumberOfCollaboratorsInProject(projectId, callback) { allowedNumberOfCollaboratorsInProject(projectId, callback) {
ProjectGetter.getProject( ProjectGetter.getProject(
projectId, projectId,
@ -226,3 +226,12 @@ module.exports = LimitationsManager = {
) )
}, },
} }
LimitationsManager.promises = promisifyAll(LimitationsManager, {
multiResult: {
userHasV2Subscription: ['hasSubscription', 'subscription'],
userIsMemberOfGroupSubscription: ['isMember', 'subscriptions'],
hasGroupMembersLimitReached: ['limitReached', 'subscription'],
},
})
module.exports = LimitationsManager

View file

@ -1,20 +1,3 @@
/* eslint-disable
camelcase,
node/handle-callback-err,
max-len,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let SubscriptionController
const AuthenticationController = require('../Authentication/AuthenticationController') const AuthenticationController = require('../Authentication/AuthenticationController')
const SubscriptionHandler = require('./SubscriptionHandler') const SubscriptionHandler = require('./SubscriptionHandler')
const PlansLocator = require('./PlansLocator') const PlansLocator = require('./PlansLocator')
@ -24,7 +7,6 @@ const RecurlyWrapper = require('./RecurlyWrapper')
const Settings = require('settings-sharelatex') const Settings = require('settings-sharelatex')
const logger = require('logger-sharelatex') const logger = require('logger-sharelatex')
const GeoIpLookup = require('../../infrastructure/GeoIpLookup') const GeoIpLookup = require('../../infrastructure/GeoIpLookup')
const UserGetter = require('../User/UserGetter')
const FeaturesUpdater = require('./FeaturesUpdater') const FeaturesUpdater = require('./FeaturesUpdater')
const planFeatures = require('./planFeatures') const planFeatures = require('./planFeatures')
const GroupPlansData = require('./GroupPlansData') const GroupPlansData = require('./GroupPlansData')
@ -34,537 +16,485 @@ const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const SubscriptionErrors = require('./Errors') const SubscriptionErrors = require('./Errors')
const SplitTestHandler = require('../SplitTests/SplitTestHandler') const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const AnalyticsManager = require('../Analytics/AnalyticsManager') const AnalyticsManager = require('../Analytics/AnalyticsManager')
const { expressify } = require('../../util/promises')
const OError = require('@overleaf/o-error') const OError = require('@overleaf/o-error')
const _ = require('lodash') const _ = require('lodash')
const SUBSCRIPTION_PAGE_SPLIT_TEST = 'subscription-page' const SUBSCRIPTION_PAGE_SPLIT_TEST = 'subscription-page'
module.exports = SubscriptionController = { async function plansPage(req, res) {
plansPage(req, res, next) { const plans = SubscriptionViewModelBuilder.buildPlansList()
const plans = SubscriptionViewModelBuilder.buildPlansList()
let currentUser = null
return GeoIpLookup.getCurrencyCode( const recommendedCurrency = await GeoIpLookup.promises.getCurrencyCode(
(req.query != null ? req.query.ip : undefined) || req.ip, (req.query ? req.query.ip : undefined) || req.ip
function (err, recomendedCurrency) { )
if (err != null) {
return next(err) res.render('subscriptions/plans', {
} title: 'plans_and_pricing',
const render = () => plans,
res.render('subscriptions/plans', { gaExperiments: Settings.gaExperiments.plansPage,
title: 'plans_and_pricing', gaOptimize: true,
plans, recomendedCurrency: recommendedCurrency,
gaExperiments: Settings.gaExperiments.plansPage, planFeatures,
gaOptimize: true, groupPlans: GroupPlansData,
recomendedCurrency, })
planFeatures, }
groupPlans: GroupPlansData,
}) // get to show the recurly.js page
const user_id = AuthenticationController.getLoggedInUserId(req) async function paymentPage(req, res) {
if (user_id != null) { const user = AuthenticationController.getSessionUser(req)
return UserGetter.getUser( const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
user_id, if (!plan) {
{ signUpDate: 1 }, return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
function (err, user) { }
if (err != null) { const hasSubscription = await LimitationsManager.promises.userHasV1OrV2Subscription(
return next(err) user
} )
currentUser = user if (hasSubscription) {
return render() res.redirect('/user/subscription?hasSubscription=true')
} } else {
) // LimitationsManager.userHasV2Subscription only checks Mongo. Double check with
} else { // Recurly as well at this point (we don't do this most places for speed).
return render() const valid = await SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
} user._id
}
) )
}, if (!valid) {
res.redirect('/user/subscription?hasSubscription=true')
// get to show the recurly.js page } else {
paymentPage(req, res, next) { let currency = req.query.currency
const user = AuthenticationController.getSessionUser(req) ? req.query.currency.toUpperCase()
const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) : undefined
if (!plan) { const {
return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found') recomendedCurrency: recommendedCurrency,
countryCode,
} = await GeoIpLookup.promises.getCurrencyCode(
(req.query ? req.query.ip : undefined) || req.ip
)
if (recommendedCurrency && currency == null) {
currency = recommendedCurrency
}
res.render('subscriptions/new', {
title: 'subscribe',
currency,
countryCode,
plan,
showStudentPlan: req.query.ssp === 'true',
recurlyConfig: JSON.stringify({
currency,
subdomain: Settings.apis.recurly.subdomain,
}),
showCouponField: !!req.query.scf,
showVatField: !!req.query.svf,
gaOptimize: true,
})
} }
return LimitationsManager.userHasV1OrV2Subscription( }
user, }
function (err, hasSubscription) {
if (err != null) { function userSubscriptionPage(req, res, next) {
return next(err) const user = AuthenticationController.getSessionUser(req)
} SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
if (hasSubscription) { user,
return res.redirect('/user/subscription?hasSubscription=true') function (error, results) {
} else { if (error) {
// LimitationsManager.userHasV2Subscription only checks Mongo. Double check with return next(error)
// Recurly as well at this point (we don't do this most places for speed).
return SubscriptionHandler.validateNoSubscriptionInRecurly(
user._id,
function (error, valid) {
if (error != null) {
return next(error)
}
if (!valid) {
res.redirect('/user/subscription?hasSubscription=true')
} else {
let currency =
req.query.currency != null
? req.query.currency.toUpperCase()
: undefined
return GeoIpLookup.getCurrencyCode(
(req.query != null ? req.query.ip : undefined) || req.ip,
function (err, recomendedCurrency, countryCode) {
if (err != null) {
return next(err)
}
if (recomendedCurrency != null && currency == null) {
currency = recomendedCurrency
}
return res.render('subscriptions/new', {
title: 'subscribe',
currency,
countryCode,
plan,
showStudentPlan: req.query.ssp === 'true',
recurlyConfig: JSON.stringify({
currency,
subdomain: Settings.apis.recurly.subdomain,
}),
showCouponField: !!req.query.scf,
showVatField: !!req.query.svf,
gaOptimize: true,
})
}
)
}
}
)
}
} }
) const {
}, personalSubscription,
memberGroupSubscriptions,
userSubscriptionPage(req, res, next) { managedGroupSubscriptions,
const user = AuthenticationController.getSessionUser(req) confirmedMemberAffiliations,
return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( managedInstitutions,
user, managedPublishers,
function (error, results) { v1SubscriptionStatus,
if (error != null) { } = results
return next(error) LimitationsManager.userHasV1OrV2Subscription(
} user,
const { function (err, hasSubscription) {
personalSubscription, if (error) {
memberGroupSubscriptions, return next(error)
managedGroupSubscriptions,
confirmedMemberAffiliations,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
} = results
return LimitationsManager.userHasV1OrV2Subscription(
user,
function (err, hasSubscription) {
if (error != null) {
return next(error)
}
const fromPlansPage = req.query.hasSubscription
const plans = SubscriptionViewModelBuilder.buildPlansList(
personalSubscription ? personalSubscription.plan : undefined
)
let subscriptionCopy = 'default'
if (
personalSubscription ||
hasSubscription ||
(memberGroupSubscriptions &&
memberGroupSubscriptions.length > 0) ||
(confirmedMemberAffiliations &&
confirmedMemberAffiliations.length > 0 &&
_.find(confirmedMemberAffiliations, affiliation => {
return affiliation.licence && affiliation.licence !== 'free'
}))
) {
AnalyticsManager.recordEvent(user._id, 'subscription-page-view')
} else {
const testSegmentation = SplitTestHandler.getTestSegmentation(
user._id,
SUBSCRIPTION_PAGE_SPLIT_TEST
)
if (testSegmentation.enabled) {
subscriptionCopy = testSegmentation.variant
AnalyticsManager.recordEvent(
user._id,
'subscription-page-view',
{
splitTestId: SUBSCRIPTION_PAGE_SPLIT_TEST,
splitTestVariantId: testSegmentation.variant,
}
)
} else {
AnalyticsManager.recordEvent(user._id, 'subscription-page-view')
}
}
const data = {
title: 'your_subscription',
plans,
user,
hasSubscription,
subscriptionCopy,
fromPlansPage,
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
confirmedMemberAffiliations,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
}
return res.render('subscriptions/dashboard', data)
} }
) const fromPlansPage = req.query.hasSubscription
} const plans = SubscriptionViewModelBuilder.buildPlansList(
) personalSubscription ? personalSubscription.plan : undefined
}, )
createSubscription(req, res, next) { let subscriptionCopy = 'default'
const user = AuthenticationController.getSessionUser(req) if (
const recurlyTokenIds = { personalSubscription ||
billing: req.body.recurly_token_id, hasSubscription ||
threeDSecureActionResult: (memberGroupSubscriptions && memberGroupSubscriptions.length > 0) ||
req.body.recurly_three_d_secure_action_result_token_id, (confirmedMemberAffiliations &&
confirmedMemberAffiliations.length > 0 &&
_.find(confirmedMemberAffiliations, affiliation => {
return affiliation.licence && affiliation.licence !== 'free'
}))
) {
AnalyticsManager.recordEvent(user._id, 'subscription-page-view')
} else {
const testSegmentation = SplitTestHandler.getTestSegmentation(
user._id,
SUBSCRIPTION_PAGE_SPLIT_TEST
)
if (testSegmentation.enabled) {
subscriptionCopy = testSegmentation.variant
AnalyticsManager.recordEvent(user._id, 'subscription-page-view', {
splitTestId: SUBSCRIPTION_PAGE_SPLIT_TEST,
splitTestVariantId: testSegmentation.variant,
})
} else {
AnalyticsManager.recordEvent(user._id, 'subscription-page-view')
}
}
const data = {
title: 'your_subscription',
plans,
user,
hasSubscription,
subscriptionCopy,
fromPlansPage,
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
confirmedMemberAffiliations,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
}
res.render('subscriptions/dashboard', data)
}
)
} }
const { subscriptionDetails } = req.body )
}
return LimitationsManager.userHasV1OrV2Subscription( function createSubscription(req, res, next) {
user, const user = AuthenticationController.getSessionUser(req)
function (err, hasSubscription) { const recurlyTokenIds = {
if (err != null) { billing: req.body.recurly_token_id,
return next(err) threeDSecureActionResult:
} req.body.recurly_three_d_secure_action_result_token_id,
if (hasSubscription) { }
logger.warn({ user_id: user._id }, 'user already has subscription') const { subscriptionDetails } = req.body
return res.sendStatus(409) // conflict
}
return SubscriptionHandler.createSubscription(
user,
subscriptionDetails,
recurlyTokenIds,
function (err) {
if (!err) {
return res.sendStatus(201)
}
if ( LimitationsManager.userHasV1OrV2Subscription(
err instanceof SubscriptionErrors.RecurlyTransactionError || user,
err instanceof Errors.InvalidError function (err, hasSubscription) {
) { if (err) {
logger.error({ err }, 'recurly transaction error, potential 422') return next(err)
return HttpErrorHandler.unprocessableEntity( }
req, if (hasSubscription) {
res, logger.warn({ user_id: user._id }, 'user already has subscription')
err.message, return res.sendStatus(409) // conflict
OError.getFullInfo(err).public }
) return SubscriptionHandler.createSubscription(
} user,
subscriptionDetails,
recurlyTokenIds,
function (err) {
if (!err) {
return res.sendStatus(201)
}
if (
err instanceof SubscriptionErrors.RecurlyTransactionError ||
err instanceof Errors.InvalidError
) {
logger.error({ err }, 'recurly transaction error, potential 422')
HttpErrorHandler.unprocessableEntity(
req,
res,
err.message,
OError.getFullInfo(err).public
)
} else {
logger.warn( logger.warn(
{ err, user_id: user._id }, { err, user_id: user._id },
'something went wrong creating subscription' 'something went wrong creating subscription'
) )
next(err) next(err)
} }
) }
} )
) }
}, )
}
successful_subscription(req, res, next) { function successfulSubscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req) const user = AuthenticationController.getSessionUser(req)
return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
user, user,
function (error, { personalSubscription }) { function (error, { personalSubscription }) {
if (error != null) { if (error) {
return next(error) return next(error)
} }
if (personalSubscription == null) { if (personalSubscription == null) {
return res.redirect('/user/subscription/plans') res.redirect('/user/subscription/plans')
} } else {
return res.render('subscriptions/successful_subscription', { res.render('subscriptions/successful_subscription', {
title: 'thank_you', title: 'thank_you',
personalSubscription, personalSubscription,
}) })
} }
}
)
}
function cancelSubscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
logger.log({ user_id: user._id }, 'canceling subscription')
SubscriptionHandler.cancelSubscription(user, function (err) {
if (err) {
OError.tag(err, 'something went wrong canceling subscription', {
user_id: user._id,
})
return next(err)
}
// Note: this redirect isn't used in the main flow as the redirection is
// handled by Angular
res.redirect('/user/subscription/canceled')
})
}
function canceledSubscription(req, res, next) {
return res.render('subscriptions/canceled_subscription', {
title: 'subscription_canceled',
})
}
function cancelV1Subscription(req, res, next) {
const userId = AuthenticationController.getLoggedInUserId(req)
logger.log({ userId }, 'canceling v1 subscription')
V1SubscriptionManager.cancelV1Subscription(userId, function (err) {
if (err) {
OError.tag(err, 'something went wrong canceling v1 subscription', {
userId,
})
return next(err)
}
res.redirect('/user/subscription')
})
}
function updateSubscription(req, res, next) {
const origin = req && req.query ? req.query.origin : null
const user = AuthenticationController.getSessionUser(req)
const planCode = req.body.plan_code
if (planCode == null) {
const err = new Error('plan_code is not defined')
logger.warn(
{ user_id: user._id, err, planCode, origin, body: req.body },
'[Subscription] error in updateSubscription form'
) )
}, return next(err)
}
logger.log({ planCode, user_id: user._id }, 'updating subscription')
SubscriptionHandler.updateSubscription(user, planCode, null, function (err) {
if (err) {
OError.tag(err, 'something went wrong updating subscription', {
user_id: user._id,
})
return next(err)
}
res.redirect('/user/subscription')
})
}
cancelSubscription(req, res, next) { function cancelPendingSubscriptionChange(req, res, next) {
const user = AuthenticationController.getSessionUser(req) const user = AuthenticationController.getSessionUser(req)
logger.log({ user_id: user._id }, 'canceling subscription') logger.log({ user_id: user._id }, 'canceling pending subscription change')
return SubscriptionHandler.cancelSubscription(user, function (err) { SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) {
if (err != null) { if (err) {
OError.tag(err, 'something went wrong canceling subscription', { OError.tag(
err,
'something went wrong canceling pending subscription change',
{
user_id: user._id, user_id: user._id,
}) }
return next(err)
}
// Note: this redirect isn't used in the main flow as the redirection is
// handled by Angular
return res.redirect('/user/subscription/canceled')
})
},
canceledSubscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
return res.render('subscriptions/canceled_subscription', {
title: 'subscription_canceled',
})
},
cancelV1Subscription(req, res, next) {
const user_id = AuthenticationController.getLoggedInUserId(req)
logger.log({ user_id }, 'canceling v1 subscription')
return V1SubscriptionManager.cancelV1Subscription(user_id, function (err) {
if (err != null) {
OError.tag(err, 'something went wrong canceling v1 subscription', {
user_id,
})
return next(err)
}
return res.redirect('/user/subscription')
})
},
updateSubscription(req, res, next) {
const _origin =
__guard__(req != null ? req.query : undefined, x => x.origin) || null
const user = AuthenticationController.getSessionUser(req)
const planCode = req.body.plan_code
if (planCode == null) {
const err = new Error('plan_code is not defined')
logger.warn(
{ user_id: user._id, err, planCode, origin: _origin, body: req.body },
'[Subscription] error in updateSubscription form'
) )
return next(err) return next(err)
} }
logger.log({ planCode, user_id: user._id }, 'updating subscription') res.redirect('/user/subscription')
return SubscriptionHandler.updateSubscription( })
user, }
planCode,
null, function updateAccountEmailAddress(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
RecurlyWrapper.updateAccountEmailAddress(
user._id,
user.email,
function (error) {
if (error) {
return next(error)
}
res.sendStatus(200)
}
)
}
function reactivateSubscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
logger.log({ user_id: user._id }, 'reactivating subscription')
SubscriptionHandler.reactivateSubscription(user, function (err) {
if (err) {
OError.tag(err, 'something went wrong reactivating subscription', {
user_id: user._id,
})
return next(err)
}
res.redirect('/user/subscription')
})
}
function recurlyCallback(req, res, next) {
logger.log({ data: req.body }, 'received recurly callback')
const event = Object.keys(req.body)[0]
const eventData = req.body[event]
if (
[
'new_subscription_notification',
'updated_subscription_notification',
'expired_subscription_notification',
].includes(event)
) {
const recurlySubscription = eventData.subscription
SubscriptionHandler.syncSubscription(
recurlySubscription,
{ ip: req.ip },
function (err) { function (err) {
if (err != null) { if (err) {
OError.tag(err, 'something went wrong updating subscription', {
user_id: user._id,
})
return next(err) return next(err)
} }
return res.redirect('/user/subscription')
}
)
},
cancelPendingSubscriptionChange(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
logger.log({ user_id: user._id }, 'canceling pending subscription change')
SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) {
if (err != null) {
OError.tag(
err,
'something went wrong canceling pending subscription change',
{
user_id: user._id,
}
)
return next(err)
}
res.redirect('/user/subscription')
})
},
updateAccountEmailAddress(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
RecurlyWrapper.updateAccountEmailAddress(
user._id,
user.email,
function (error) {
if (error) {
return next(error)
}
res.sendStatus(200) res.sendStatus(200)
} }
) )
}, } else if (event === 'billing_info_updated_notification') {
const recurlyAccountCode = eventData.account.account_code
SubscriptionHandler.attemptPaypalInvoiceCollection(
recurlyAccountCode,
function (err) {
if (err) {
return next(err)
}
res.sendStatus(200)
}
)
} else {
res.sendStatus(200)
}
}
reactivateSubscription(req, res, next) { function renderUpgradeToAnnualPlanPage(req, res, next) {
const user = AuthenticationController.getSessionUser(req) const user = AuthenticationController.getSessionUser(req)
logger.log({ user_id: user._id }, 'reactivating subscription') LimitationsManager.userHasV2Subscription(
SubscriptionHandler.reactivateSubscription(user, function (err) { user,
if (err != null) { function (err, hasSubscription, subscription) {
OError.tag(err, 'something went wrong reactivating subscription', { let planName
if (err) {
return next(err)
}
const planCode = subscription
? subscription.planCode.toLowerCase()
: undefined
if ((planCode ? planCode.indexOf('annual') : undefined) !== -1) {
planName = 'annual'
} else if ((planCode ? planCode.indexOf('student') : undefined) !== -1) {
planName = 'student'
} else if (
(planCode ? planCode.indexOf('collaborator') : undefined) !== -1
) {
planName = 'collaborator'
}
if (hasSubscription) {
res.render('subscriptions/upgradeToAnnual', {
title: 'Upgrade to annual',
planName,
})
} else {
res.redirect('/user/subscription/plans')
}
}
)
}
function processUpgradeToAnnualPlan(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
const { planName } = req.body
const couponCode = Settings.coupon_codes.upgradeToAnnualPromo[planName]
const annualPlanName = `${planName}-annual`
logger.log(
{ user_id: user._id, planName: annualPlanName },
'user is upgrading to annual billing with discount'
)
return SubscriptionHandler.updateSubscription(
user,
annualPlanName,
couponCode,
function (err) {
if (err) {
OError.tag(err, 'error updating subscription', {
user_id: user._id, user_id: user._id,
}) })
return next(err) return next(err)
} }
res.redirect('/user/subscription') res.sendStatus(200)
})
},
recurlyCallback(req, res, next) {
logger.log({ data: req.body }, 'received recurly callback')
const event = Object.keys(req.body)[0]
const eventData = req.body[event]
if (
[
'new_subscription_notification',
'updated_subscription_notification',
'expired_subscription_notification',
].includes(event)
) {
const recurlySubscription = eventData.subscription
return SubscriptionHandler.syncSubscription(
recurlySubscription,
{ ip: req.ip },
function (err) {
if (err != null) {
return next(err)
}
return res.sendStatus(200)
}
)
} else if (event === 'billing_info_updated_notification') {
const recurlyAccountCode = eventData.account.account_code
return SubscriptionHandler.attemptPaypalInvoiceCollection(
recurlyAccountCode,
function (err) {
if (err) {
return next(err)
}
return res.sendStatus(200)
}
)
} else {
return res.sendStatus(200)
} }
}, )
renderUpgradeToAnnualPlanPage(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
return LimitationsManager.userHasV2Subscription(
user,
function (err, hasSubscription, subscription) {
let planName
if (err != null) {
return next(err)
}
const planCode =
subscription != null ? subscription.planCode.toLowerCase() : undefined
if (
(planCode != null ? planCode.indexOf('annual') : undefined) !== -1
) {
planName = 'annual'
} else if (
(planCode != null ? planCode.indexOf('student') : undefined) !== -1
) {
planName = 'student'
} else if (
(planCode != null ? planCode.indexOf('collaborator') : undefined) !==
-1
) {
planName = 'collaborator'
}
if (!hasSubscription) {
return res.redirect('/user/subscription/plans')
}
return res.render('subscriptions/upgradeToAnnual', {
title: 'Upgrade to annual',
planName,
})
}
)
},
processUpgradeToAnnualPlan(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
const { planName } = req.body
const coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
const annualPlanName = `${planName}-annual`
logger.log(
{ user_id: user._id, planName: annualPlanName },
'user is upgrading to annual billing with discount'
)
return SubscriptionHandler.updateSubscription(
user,
annualPlanName,
coupon_code,
function (err) {
if (err != null) {
OError.tag(err, 'error updating subscription', {
user_id: user._id,
})
return next(err)
}
return res.sendStatus(200)
}
)
},
extendTrial(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
return LimitationsManager.userHasV2Subscription(
user,
function (err, hasSubscription, subscription) {
if (err != null) {
return next(err)
}
return SubscriptionHandler.extendTrial(
subscription,
14,
function (err) {
if (err != null) {
return res.sendStatus(500)
} else {
return res.sendStatus(200)
}
}
)
}
)
},
recurlyNotificationParser(req, res, next) {
let xml = ''
req.on('data', chunk => (xml += chunk))
return req.on('end', () =>
RecurlyWrapper._parseXml(xml, function (error, body) {
if (error != null) {
return next(error)
}
req.body = body
return next()
})
)
},
refreshUserFeatures(req, res, next) {
const { user_id } = req.params
return FeaturesUpdater.refreshFeatures(
user_id,
'subscription-controller',
function (error) {
if (error != null) {
return next(error)
}
return res.sendStatus(200)
}
)
},
} }
function __guard__(value, transform) { async function extendTrial(req, res) {
return typeof value !== 'undefined' && value !== null const user = AuthenticationController.getSessionUser(req)
? transform(value) const {
: undefined subscription,
} = await LimitationsManager.promises.userHasV2Subscription(user)
try {
await SubscriptionHandler.promises.extendTrial(subscription, 14)
} catch (error) {
return res.sendStatus(500)
}
res.sendStatus(200)
}
function recurlyNotificationParser(req, res, next) {
let xml = ''
req.on('data', chunk => (xml += chunk))
req.on('end', () =>
RecurlyWrapper._parseXml(xml, function (error, body) {
if (error) {
return next(error)
}
req.body = body
next()
})
)
}
async function refreshUserFeatures(req, res) {
const { user_id: userId } = req.params
await FeaturesUpdater.promises.refreshFeatures(userId)
res.sendStatus(200)
}
module.exports = {
plansPage: expressify(plansPage),
paymentPage: expressify(paymentPage),
userSubscriptionPage,
createSubscription,
successfulSubscription,
cancelSubscription,
canceledSubscription,
cancelV1Subscription,
updateSubscription,
cancelPendingSubscriptionChange,
updateAccountEmailAddress,
reactivateSubscription,
recurlyCallback,
renderUpgradeToAnnualPlanPage,
processUpgradeToAnnualPlan,
extendTrial: expressify(extendTrial),
recurlyNotificationParser,
refreshUserFeatures: expressify(refreshUserFeatures),
} }

View file

@ -38,7 +38,7 @@ module.exports = {
webRouter.get( webRouter.get(
'/user/subscription/thank-you', '/user/subscription/thank-you',
AuthenticationController.requireLogin(), AuthenticationController.requireLogin(),
SubscriptionController.successful_subscription SubscriptionController.successfulSubscription
) )
webRouter.get( webRouter.get(

View file

@ -10,6 +10,7 @@ const sanitizeHtml = require('sanitize-html')
const _ = require('underscore') const _ = require('underscore')
const async = require('async') const async = require('async')
const SubscriptionHelper = require('./SubscriptionHelper') const SubscriptionHelper = require('./SubscriptionHelper')
const { promisify } = require('../../util/promises')
function buildHostedLink(recurlySubscription, type) { function buildHostedLink(recurlySubscription, type) {
const recurlySubdomain = Settings.apis.recurly.subdomain const recurlySubdomain = Settings.apis.recurly.subdomain
@ -29,311 +30,312 @@ function buildHostedLink(recurlySubscription, type) {
} }
} }
module.exports = { function buildUsersSubscriptionViewModel(user, callback) {
buildUsersSubscriptionViewModel(user, callback) { async.auto(
async.auto( {
{ personalSubscription(cb) {
personalSubscription(cb) { SubscriptionLocator.getUsersSubscription(user, cb)
SubscriptionLocator.getUsersSubscription(user, cb) },
}, recurlySubscription: [
recurlySubscription: [ 'personalSubscription',
'personalSubscription', (cb, { personalSubscription }) => {
(cb, { personalSubscription }) => { if (
if ( personalSubscription == null ||
personalSubscription == null || personalSubscription.recurlySubscription_id == null ||
personalSubscription.recurlySubscription_id == null || personalSubscription.recurlySubscription_id === ''
personalSubscription.recurlySubscription_id === '' ) {
) { return cb(null, null)
return cb(null, null) }
} RecurlyWrapper.getSubscription(
RecurlyWrapper.getSubscription( personalSubscription.recurlySubscription_id,
personalSubscription.recurlySubscription_id, { includeAccount: true },
{ includeAccount: true }, cb
cb
)
},
],
recurlyCoupons: [
'recurlySubscription',
(cb, { recurlySubscription }) => {
if (!recurlySubscription) {
return cb(null, null)
}
const accountId = recurlySubscription.account.account_code
RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
},
],
plan: [
'personalSubscription',
(cb, { personalSubscription }) => {
if (personalSubscription == null) {
return cb()
}
const plan = PlansLocator.findLocalPlanInSettings(
personalSubscription.planCode
)
if (plan == null) {
return cb(
new Error(
`No plan found for planCode '${personalSubscription.planCode}'`
)
)
}
cb(null, plan)
},
],
memberGroupSubscriptions(cb) {
SubscriptionLocator.getMemberSubscriptions(user, cb)
},
managedGroupSubscriptions(cb) {
SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
},
confirmedMemberAffiliations(cb) {
InstitutionsGetter.getConfirmedAffiliations(user._id, cb)
},
managedInstitutions(cb) {
InstitutionsGetter.getManagedInstitutions(user._id, cb)
},
managedPublishers(cb) {
PublishersGetter.getManagedPublishers(user._id, cb)
},
v1SubscriptionStatus(cb) {
V1SubscriptionManager.getSubscriptionStatusFromV1(
user._id,
(error, status, v1Id) => {
if (error) {
return cb(error)
}
cb(null, status)
}
) )
}, },
}, ],
(err, results) => { recurlyCoupons: [
if (err) { 'recurlySubscription',
return callback(err) (cb, { recurlySubscription }) => {
} if (!recurlySubscription) {
let { return cb(null, null)
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
confirmedMemberAffiliations,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
recurlySubscription,
recurlyCoupons,
plan,
} = results
if (memberGroupSubscriptions == null) {
memberGroupSubscriptions = []
}
if (managedGroupSubscriptions == null) {
managedGroupSubscriptions = []
}
if (confirmedMemberAffiliations == null) {
confirmedMemberAffiliations = []
}
if (managedInstitutions == null) {
managedInstitutions = []
}
if (v1SubscriptionStatus == null) {
v1SubscriptionStatus = {}
}
if (recurlyCoupons == null) {
recurlyCoupons = []
}
if (
personalSubscription &&
typeof personalSubscription.toObject === 'function'
) {
// Downgrade from Mongoose object, so we can add a recurly and plan attribute
personalSubscription = personalSubscription.toObject()
}
if (plan != null) {
personalSubscription.plan = plan
}
if (personalSubscription && recurlySubscription) {
const tax = recurlySubscription.tax_in_cents || 0
// Some plans allow adding more seats than the base plan provides.
// This is recorded as a subscription add on.
// Note: tax_in_cents already includes the tax for any addon.
let addOnPrice = 0
let additionalLicenses = 0
if (
plan.membersLimitAddOn &&
Array.isArray(recurlySubscription.subscription_add_ons)
) {
recurlySubscription.subscription_add_ons.forEach(addOn => {
if (addOn.add_on_code === plan.membersLimitAddOn) {
addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
additionalLicenses += addOn.quantity
}
})
} }
const totalLicenses = (plan.membersLimit || 0) + additionalLicenses const accountId = recurlySubscription.account.account_code
personalSubscription.recurly = { RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
tax, },
taxRate: recurlySubscription.tax_rate ],
? parseFloat(recurlySubscription.tax_rate._) plan: [
: 0, 'personalSubscription',
billingDetailsLink: buildHostedLink( (cb, { personalSubscription }) => {
recurlySubscription, if (personalSubscription == null) {
'billingDetails' return cb()
),
accountManagementLink: buildHostedLink(recurlySubscription),
additionalLicenses,
totalLicenses,
nextPaymentDueAt: SubscriptionFormatters.formatDate(
recurlySubscription.current_period_ends_at
),
currency: recurlySubscription.currency,
state: recurlySubscription.state,
trialEndsAtFormatted: SubscriptionFormatters.formatDate(
recurlySubscription.trial_ends_at
),
trial_ends_at: recurlySubscription.trial_ends_at,
activeCoupons: recurlyCoupons,
account: recurlySubscription.account,
} }
if (recurlySubscription.pending_subscription) { const plan = PlansLocator.findLocalPlanInSettings(
const pendingPlan = PlansLocator.findLocalPlanInSettings( personalSubscription.planCode
recurlySubscription.pending_subscription.plan.plan_code )
if (plan == null) {
return cb(
new Error(
`No plan found for planCode '${personalSubscription.planCode}'`
)
) )
if (pendingPlan == null) { }
return callback( cb(null, plan)
new Error( },
`No plan found for planCode '${personalSubscription.planCode}'` ],
) memberGroupSubscriptions(cb) {
SubscriptionLocator.getMemberSubscriptions(user, cb)
},
managedGroupSubscriptions(cb) {
SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
},
confirmedMemberAffiliations(cb) {
InstitutionsGetter.getConfirmedAffiliations(user._id, cb)
},
managedInstitutions(cb) {
InstitutionsGetter.getManagedInstitutions(user._id, cb)
},
managedPublishers(cb) {
PublishersGetter.getManagedPublishers(user._id, cb)
},
v1SubscriptionStatus(cb) {
V1SubscriptionManager.getSubscriptionStatusFromV1(
user._id,
(error, status, v1Id) => {
if (error) {
return cb(error)
}
cb(null, status)
}
)
},
},
(err, results) => {
if (err) {
return callback(err)
}
let {
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
confirmedMemberAffiliations,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
recurlySubscription,
recurlyCoupons,
plan,
} = results
if (memberGroupSubscriptions == null) {
memberGroupSubscriptions = []
}
if (managedGroupSubscriptions == null) {
managedGroupSubscriptions = []
}
if (confirmedMemberAffiliations == null) {
confirmedMemberAffiliations = []
}
if (managedInstitutions == null) {
managedInstitutions = []
}
if (v1SubscriptionStatus == null) {
v1SubscriptionStatus = {}
}
if (recurlyCoupons == null) {
recurlyCoupons = []
}
if (
personalSubscription &&
typeof personalSubscription.toObject === 'function'
) {
// Downgrade from Mongoose object, so we can add a recurly and plan attribute
personalSubscription = personalSubscription.toObject()
}
if (plan != null) {
personalSubscription.plan = plan
}
if (personalSubscription && recurlySubscription) {
const tax = recurlySubscription.tax_in_cents || 0
// Some plans allow adding more seats than the base plan provides.
// This is recorded as a subscription add on.
// Note: tax_in_cents already includes the tax for any addon.
let addOnPrice = 0
let additionalLicenses = 0
if (
plan.membersLimitAddOn &&
Array.isArray(recurlySubscription.subscription_add_ons)
) {
recurlySubscription.subscription_add_ons.forEach(addOn => {
if (addOn.add_on_code === plan.membersLimitAddOn) {
addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
additionalLicenses += addOn.quantity
}
})
}
const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
personalSubscription.recurly = {
tax,
taxRate: recurlySubscription.tax_rate
? parseFloat(recurlySubscription.tax_rate._)
: 0,
billingDetailsLink: buildHostedLink(
recurlySubscription,
'billingDetails'
),
accountManagementLink: buildHostedLink(recurlySubscription),
additionalLicenses,
totalLicenses,
nextPaymentDueAt: SubscriptionFormatters.formatDate(
recurlySubscription.current_period_ends_at
),
currency: recurlySubscription.currency,
state: recurlySubscription.state,
trialEndsAtFormatted: SubscriptionFormatters.formatDate(
recurlySubscription.trial_ends_at
),
trial_ends_at: recurlySubscription.trial_ends_at,
activeCoupons: recurlyCoupons,
account: recurlySubscription.account,
}
if (recurlySubscription.pending_subscription) {
const pendingPlan = PlansLocator.findLocalPlanInSettings(
recurlySubscription.pending_subscription.plan.plan_code
)
if (pendingPlan == null) {
return callback(
new Error(
`No plan found for planCode '${personalSubscription.planCode}'`
)
)
}
let pendingAdditionalLicenses = 0
let pendingAddOnTax = 0
let pendingAddOnPrice = 0
if (recurlySubscription.pending_subscription.subscription_add_ons) {
if (
pendingPlan.membersLimitAddOn &&
Array.isArray(
recurlySubscription.pending_subscription.subscription_add_ons
)
) {
recurlySubscription.pending_subscription.subscription_add_ons.forEach(
addOn => {
if (addOn.add_on_code === pendingPlan.membersLimitAddOn) {
pendingAddOnPrice +=
addOn.quantity * addOn.unit_amount_in_cents
pendingAdditionalLicenses += addOn.quantity
}
}
) )
} }
let pendingAdditionalLicenses = 0 // Need to calculate tax ourselves as we don't get tax amounts for pending subs
let pendingAddOnTax = 0 pendingAddOnTax =
let pendingAddOnPrice = 0 personalSubscription.recurly.taxRate * pendingAddOnPrice
if (recurlySubscription.pending_subscription.subscription_add_ons) {
if (
pendingPlan.membersLimitAddOn &&
Array.isArray(
recurlySubscription.pending_subscription.subscription_add_ons
)
) {
recurlySubscription.pending_subscription.subscription_add_ons.forEach(
addOn => {
if (addOn.add_on_code === pendingPlan.membersLimitAddOn) {
pendingAddOnPrice +=
addOn.quantity * addOn.unit_amount_in_cents
pendingAdditionalLicenses += addOn.quantity
}
}
)
}
// Need to calculate tax ourselves as we don't get tax amounts for pending subs
pendingAddOnTax =
personalSubscription.recurly.taxRate * pendingAddOnPrice
}
const pendingSubscriptionTax =
personalSubscription.recurly.taxRate *
recurlySubscription.pending_subscription.unit_amount_in_cents
personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
recurlySubscription.pending_subscription.unit_amount_in_cents +
pendingAddOnPrice +
pendingAddOnTax +
pendingSubscriptionTax,
recurlySubscription.currency
)
const pendingTotalLicenses =
(pendingPlan.membersLimit || 0) + pendingAdditionalLicenses
personalSubscription.recurly.pendingAdditionalLicenses = pendingAdditionalLicenses
personalSubscription.recurly.pendingTotalLicenses = pendingTotalLicenses
personalSubscription.pendingPlan = pendingPlan
} else {
personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
recurlySubscription.currency
)
} }
const pendingSubscriptionTax =
personalSubscription.recurly.taxRate *
recurlySubscription.pending_subscription.unit_amount_in_cents
personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
recurlySubscription.pending_subscription.unit_amount_in_cents +
pendingAddOnPrice +
pendingAddOnTax +
pendingSubscriptionTax,
recurlySubscription.currency
)
const pendingTotalLicenses =
(pendingPlan.membersLimit || 0) + pendingAdditionalLicenses
personalSubscription.recurly.pendingAdditionalLicenses = pendingAdditionalLicenses
personalSubscription.recurly.pendingTotalLicenses = pendingTotalLicenses
personalSubscription.pendingPlan = pendingPlan
} else {
personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
recurlySubscription.currency
)
} }
for (const memberGroupSubscription of memberGroupSubscriptions) {
if (memberGroupSubscription.teamNotice) {
memberGroupSubscription.teamNotice = sanitizeHtml(
memberGroupSubscription.teamNotice
)
}
}
callback(null, {
personalSubscription,
managedGroupSubscriptions,
memberGroupSubscriptions,
confirmedMemberAffiliations,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
})
} }
)
},
buildPlansList(currentPlan) { for (const memberGroupSubscription of memberGroupSubscriptions) {
const { plans } = Settings if (memberGroupSubscription.teamNotice) {
memberGroupSubscription.teamNotice = sanitizeHtml(
memberGroupSubscription.teamNotice
)
}
}
const allPlans = {} callback(null, {
plans.forEach(plan => { personalSubscription,
allPlans[plan.planCode] = plan managedGroupSubscriptions,
}) memberGroupSubscriptions,
confirmedMemberAffiliations,
const result = { allPlans } managedInstitutions,
managedPublishers,
if (currentPlan) { v1SubscriptionStatus,
result.planCodesChangingAtTermEnd = _.pluck( })
_.filter(plans, plan => {
if (!plan.hideFromUsers) {
return SubscriptionHelper.shouldPlanChangeAtTermEnd(
currentPlan,
plan
)
}
}),
'planCode'
)
} }
)
}
result.studentAccounts = _.filter( function buildPlansList(currentPlan) {
plans, const { plans } = Settings
plan => plan.planCode.indexOf('student') !== -1
const allPlans = {}
plans.forEach(plan => {
allPlans[plan.planCode] = plan
})
const result = { allPlans }
if (currentPlan) {
result.planCodesChangingAtTermEnd = _.pluck(
_.filter(plans, plan => {
if (!plan.hideFromUsers) {
return SubscriptionHelper.shouldPlanChangeAtTermEnd(currentPlan, plan)
}
}),
'planCode'
) )
}
result.groupMonthlyPlans = _.filter( result.studentAccounts = _.filter(
plans, plans,
plan => plan.groupPlan && !plan.annual plan => plan.planCode.indexOf('student') !== -1
) )
result.groupAnnualPlans = _.filter( result.groupMonthlyPlans = _.filter(
plans, plans,
plan => plan.groupPlan && plan.annual plan => plan.groupPlan && !plan.annual
) )
result.individualMonthlyPlans = _.filter( result.groupAnnualPlans = _.filter(
plans, plans,
plan => plan => plan.groupPlan && plan.annual
!plan.groupPlan && )
!plan.annual &&
plan.planCode !== 'personal' && // Prevent the personal plan from appearing on the change-plans page
plan.planCode.indexOf('student') === -1
)
result.individualAnnualPlans = _.filter( result.individualMonthlyPlans = _.filter(
plans, plans,
plan => plan =>
!plan.groupPlan && !plan.groupPlan &&
plan.annual && !plan.annual &&
plan.planCode.indexOf('student') === -1 plan.planCode !== 'personal' && // Prevent the personal plan from appearing on the change-plans page
) plan.planCode.indexOf('student') === -1
)
return result result.individualAnnualPlans = _.filter(
plans,
plan =>
!plan.groupPlan && plan.annual && plan.planCode.indexOf('student') === -1
)
return result
}
module.exports = {
buildUsersSubscriptionViewModel,
buildPlansList,
promises: {
buildUsersSubscriptionViewModel: promisify(buildUsersSubscriptionViewModel),
}, },
} }

View file

@ -1,22 +1,9 @@
/* eslint-disable
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let GeoIpLookup
const request = require('request') const request = require('request')
const settings = require('settings-sharelatex') const settings = require('settings-sharelatex')
const _ = require('underscore') const _ = require('underscore')
const logger = require('logger-sharelatex') const logger = require('logger-sharelatex')
const URL = require('url') const URL = require('url')
const { promisify } = require('../util/promises')
const currencyMappings = { const currencyMappings = {
GB: 'GBP', GB: 'GBP',
@ -61,49 +48,50 @@ const EuroCountries = [
_.each(EuroCountries, country => (currencyMappings[country] = 'EUR')) _.each(EuroCountries, country => (currencyMappings[country] = 'EUR'))
module.exports = GeoIpLookup = { function getDetails(ip, callback) {
getDetails(ip, callback) { if (ip == null) {
if (ip == null) { const e = new Error('no ip passed')
const e = new Error('no ip passed') return callback(e)
return callback(e) }
ip = ip.trim().split(' ')[0]
const opts = {
url: URL.resolve(settings.apis.geoIpLookup.url, ip),
timeout: 1000,
json: true,
}
logger.log({ ip, opts }, 'getting geo ip details')
request.get(opts, function (err, res, ipDetails) {
if (err) {
logger.warn({ err, ip }, 'error getting ip details')
} }
ip = ip.trim().split(' ')[0] callback(err, ipDetails)
const opts = { })
url: URL.resolve(settings.apis.geoIpLookup.url, ip), }
timeout: 1000,
json: true,
}
logger.log({ ip, opts }, 'getting geo ip details')
return request.get(opts, function (err, res, ipDetails) {
if (err != null) {
logger.warn({ err, ip }, 'error getting ip details')
}
return callback(err, ipDetails)
})
},
getCurrencyCode(ip, callback) { function getCurrencyCode(ip, callback) {
return GeoIpLookup.getDetails(ip, function (err, ipDetails) { getDetails(ip, function (err, ipDetails) {
if (err != null || ipDetails == null) { if (err || !ipDetails) {
logger.err( logger.err(
{ err, ip }, { err, ip },
'problem getting currencyCode for ip, defaulting to USD' 'problem getting currencyCode for ip, defaulting to USD'
)
return callback(null, 'USD')
}
const countryCode = __guard__(
ipDetails != null ? ipDetails.country_code : undefined,
x => x.toUpperCase()
) )
const currencyCode = currencyMappings[countryCode] || 'USD' return callback(null, 'USD')
logger.log({ ip, currencyCode, ipDetails }, 'got currencyCode for ip') }
return callback(err, currencyCode, countryCode) const countryCode =
}) ipDetails && ipDetails.countryCode
}, ? ipDetails.countryCode.toUpperCase()
: undefined
const currencyCode = currencyMappings[countryCode] || 'USD'
logger.log({ ip, currencyCode, ipDetails }, 'got currencyCode for ip')
return callback(err, currencyCode, countryCode)
})
} }
function __guard__(value, transform) { module.exports = {
return typeof value !== 'undefined' && value !== null getDetails,
? transform(value) getCurrencyCode,
: undefined promises: {
getDetails: promisify(getDetails),
getCurrencyCode: promisify(getCurrencyCode),
},
} }

View file

@ -61,6 +61,15 @@ describe('SubscriptionController', function () {
syncSubscription: sinon.stub().yields(), syncSubscription: sinon.stub().yields(),
attemptPaypalInvoiceCollection: sinon.stub().yields(), attemptPaypalInvoiceCollection: sinon.stub().yields(),
startFreeTrial: sinon.stub(), startFreeTrial: sinon.stub(),
promises: {
createSubscription: sinon.stub().resolves(),
updateSubscription: sinon.stub().resolves(),
reactivateSubscription: sinon.stub().resolves(),
cancelSubscription: sinon.stub().resolves(),
syncSubscription: sinon.stub().resolves(),
attemptPaypalInvoiceCollection: sinon.stub().resolves(),
startFreeTrial: sinon.stub().resolves(),
},
} }
this.PlansLocator = { findLocalPlanInSettings: sinon.stub() } this.PlansLocator = { findLocalPlanInSettings: sinon.stub() }
@ -69,11 +78,19 @@ describe('SubscriptionController', function () {
hasPaidSubscription: sinon.stub(), hasPaidSubscription: sinon.stub(),
userHasV1OrV2Subscription: sinon.stub(), userHasV1OrV2Subscription: sinon.stub(),
userHasV2Subscription: sinon.stub(), userHasV2Subscription: sinon.stub(),
promises: {
hasPaidSubscription: sinon.stub().resolves(),
userHasV1OrV2Subscription: sinon.stub().resolves(),
userHasV2Subscription: sinon.stub().resolves(),
},
} }
this.SubscriptionViewModelBuilder = { this.SubscriptionViewModelBuilder = {
buildUsersSubscriptionViewModel: sinon.stub().callsArgWith(1, null, {}), buildUsersSubscriptionViewModel: sinon.stub().callsArgWith(1, null, {}),
buildPlansList: sinon.stub(), buildPlansList: sinon.stub(),
promises: {
buildUsersSubscriptionViewModel: sinon.stub().resolves({}),
},
} }
this.settings = { this.settings = {
coupon_codes: { coupon_codes: {
@ -90,9 +107,17 @@ describe('SubscriptionController', function () {
siteUrl: 'http://de.sharelatex.dev:3000', siteUrl: 'http://de.sharelatex.dev:3000',
gaExperiments: {}, gaExperiments: {},
} }
this.GeoIpLookup = { getCurrencyCode: sinon.stub() } this.GeoIpLookup = {
getCurrencyCode: sinon.stub(),
promises: {
getCurrencyCode: sinon.stub(),
},
}
this.UserGetter = { this.UserGetter = {
getUser: sinon.stub().callsArgWith(2, null, this.user), getUser: sinon.stub().callsArgWith(2, null, this.user),
promises: {
getUser: sinon.stub().resolves(this.user),
},
} }
this.SubscriptionController = SandboxedModule.require(modulePath, { this.SubscriptionController = SandboxedModule.require(modulePath, {
requires: { requires: {
@ -135,75 +160,26 @@ describe('SubscriptionController', function () {
describe('plansPage', function () { describe('plansPage', function () {
beforeEach(function () { beforeEach(function () {
this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534' this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534'
return this.GeoIpLookup.getCurrencyCode.callsArgWith( return this.GeoIpLookup.promises.getCurrencyCode.resolves(
1,
null,
this.stubbedCurrencyCode this.stubbedCurrencyCode
) )
}) })
describe('when user is logged in', function (done) {
beforeEach(function (done) {
this.res.callback = done
return this.SubscriptionController.plansPage(this.req, this.res)
})
it('should fetch the current user', function (done) {
this.UserGetter.getUser.callCount.should.equal(1)
return done()
})
describe('not dependant on logged in state', function (done) {
// these could have been put in 'when user is not logged in' too
it('should set the recommended currency from the geoiplookup', function (done) {
this.res.renderedVariables.recomendedCurrency.should.equal(
this.stubbedCurrencyCode
)
this.GeoIpLookup.getCurrencyCode
.calledWith(this.req.ip)
.should.equal(true)
return done()
})
it('should include data for features table', function (done) {
this.res.renderedVariables.planFeatures.length.should.not.equal(0)
return done()
})
})
})
describe('when user is not logged in', function (done) {
beforeEach(function (done) {
this.res.callback = done
this.AuthenticationController.getLoggedInUserId = sinon
.stub()
.returns(null)
return this.SubscriptionController.plansPage(this.req, this.res)
})
it('should not fetch the current user', function (done) {
this.UserGetter.getUser.callCount.should.equal(0)
return done()
})
})
}) })
describe('paymentPage', function () { describe('paymentPage', function () {
beforeEach(function () { beforeEach(function () {
this.req.headers = {} this.req.headers = {}
this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly = sinon
.stub() .stub()
.yields(null, true) .resolves(true)
return this.GeoIpLookup.getCurrencyCode.callsArgWith( return this.GeoIpLookup.promises.getCurrencyCode.resolves({
1, recomendedCurrency: this.stubbedCurrencyCode,
null, })
this.stubbedCurrencyCode
)
}) })
describe('with a user without a subscription', function () { describe('with a user without a subscription', function () {
beforeEach(function () { beforeEach(function () {
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
1,
null,
false false
) )
return this.PlansLocator.findLocalPlanInSettings.returns({}) return this.PlansLocator.findLocalPlanInSettings.returns({})
@ -213,9 +189,9 @@ describe('SubscriptionController', function () {
it('should render the new subscription page', function (done) { it('should render the new subscription page', function (done) {
this.res.render = (page, opts) => { this.res.render = (page, opts) => {
page.should.equal('subscriptions/new') page.should.equal('subscriptions/new')
return done() done()
} }
return this.SubscriptionController.paymentPage(this.req, this.res) this.SubscriptionController.paymentPage(this.req, this.res)
}) })
}) })
}) })
@ -223,9 +199,7 @@ describe('SubscriptionController', function () {
describe('with a user with subscription', function () { describe('with a user with subscription', function () {
it('should redirect to the subscription dashboard', function (done) { it('should redirect to the subscription dashboard', function (done) {
this.PlansLocator.findLocalPlanInSettings.returns({}) this.PlansLocator.findLocalPlanInSettings.returns({})
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
1,
null,
true true
) )
this.res.redirect = url => { this.res.redirect = url => {
@ -238,9 +212,7 @@ describe('SubscriptionController', function () {
describe('with an invalid plan code', function () { describe('with an invalid plan code', function () {
it('should return 422 error - Unprocessable Entity', function (done) { it('should return 422 error - Unprocessable Entity', function (done) {
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
1,
null,
false false
) )
this.PlansLocator.findLocalPlanInSettings.returns(null) this.PlansLocator.findLocalPlanInSettings.returns(null)
@ -252,19 +224,13 @@ describe('SubscriptionController', function () {
done() done()
} }
) )
return this.SubscriptionController.paymentPage( return this.SubscriptionController.paymentPage(this.req, this.res)
this.req,
this.res,
this.next
)
}) })
}) })
describe('which currency to use', function () { describe('which currency to use', function () {
beforeEach(function () { beforeEach(function () {
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
1,
null,
false false
) )
return this.PlansLocator.findLocalPlanInSettings.returns({}) return this.PlansLocator.findLocalPlanInSettings.returns({})
@ -293,23 +259,21 @@ describe('SubscriptionController', function () {
this.req.query.currency = null this.req.query.currency = null
this.res.render = (page, opts) => { this.res.render = (page, opts) => {
opts.currency.should.equal(this.stubbedCurrencyCode) opts.currency.should.equal(this.stubbedCurrencyCode)
return done() done()
} }
return this.SubscriptionController.paymentPage(this.req, this.res) this.SubscriptionController.paymentPage(this.req, this.res)
}) })
}) })
describe('with a recurly subscription already', function () { describe('with a recurly subscription already', function () {
it('should redirect to the subscription dashboard', function (done) { it('should redirect to the subscription dashboard', function (done) {
this.PlansLocator.findLocalPlanInSettings.returns({}) this.PlansLocator.findLocalPlanInSettings.returns({})
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
1, false
null, )
this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly.resolves(
false false
) )
this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
.stub()
.yields(null, false)
this.res.redirect = url => { this.res.redirect = url => {
url.should.equal('/user/subscription?hasSubscription=true') url.should.equal('/user/subscription?hasSubscription=true')
return done() return done()
@ -319,7 +283,7 @@ describe('SubscriptionController', function () {
}) })
}) })
describe('successful_subscription', function () { describe('successfulSubscription', function () {
beforeEach(function (done) { beforeEach(function (done) {
this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith( this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(
1, 1,
@ -327,7 +291,7 @@ describe('SubscriptionController', function () {
{} {}
) )
this.res.callback = done this.res.callback = done
return this.SubscriptionController.successful_subscription( return this.SubscriptionController.successfulSubscription(
this.req, this.req,
this.res this.res
) )
@ -359,12 +323,9 @@ describe('SubscriptionController', function () {
this.res.render = (view, data) => { this.res.render = (view, data) => {
this.data = data this.data = data
expect(view).to.equal('subscriptions/dashboard') expect(view).to.equal('subscriptions/dashboard')
return done() done()
} }
return this.SubscriptionController.userSubscriptionPage( this.SubscriptionController.userSubscriptionPage(this.req, this.res)
this.req,
this.res
)
}) })
it('should load the personal, groups and v1 subscriptions', function () { it('should load the personal, groups and v1 subscriptions', function () {

View file

@ -1,194 +0,0 @@
/* eslint-disable
node/handle-callback-err,
max-len,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const assert = require('assert')
const path = require('path')
const sinon = require('sinon')
const modulePath = path.join(
__dirname,
'../../../../app/src/infrastructure/GeoIpLookup'
)
const { expect } = require('chai')
describe('GeoIpLookup', function () {
beforeEach(function () {
this.settings = {
apis: {
geoIpLookup: {
url: 'http://lookup.com',
},
},
}
this.request = { get: sinon.stub() }
this.GeoIpLookup = SandboxedModule.require(modulePath, {
requires: {
request: this.request,
'settings-sharelatex': this.settings,
},
})
this.ipAddress = '123.456.789.123'
return (this.stubbedResponse = {
ip: this.ipAddress,
country_code: 'GB',
country_name: 'United Kingdom',
region_code: 'H9',
region_name: 'London, City of',
city: 'London',
zipcode: 'SE16',
latitude: 51.0,
longitude: -0.0493,
metro_code: '',
area_code: '',
})
})
describe('getDetails', function () {
beforeEach(function () {
return this.request.get.callsArgWith(1, null, null, this.stubbedResponse)
})
it('should request the details using the ip', function (done) {
return this.GeoIpLookup.getDetails(this.ipAddress, err => {
this.request.get
.calledWith({
url: this.settings.apis.geoIpLookup.url + '/' + this.ipAddress,
timeout: 1000,
json: true,
})
.should.equal(true)
return done()
})
})
it('should return the ip details', function (done) {
return this.GeoIpLookup.getDetails(
this.ipAddress,
(err, returnedDetails) => {
assert.deepEqual(returnedDetails, this.stubbedResponse)
return done()
}
)
})
it('should take the first ip in the string', function (done) {
return this.GeoIpLookup.getDetails(
` ${this.ipAddress} 456.312.452.102 432.433.888.234`,
err => {
this.request.get
.calledWith({
url: this.settings.apis.geoIpLookup.url + '/' + this.ipAddress,
timeout: 1000,
json: true,
})
.should.equal(true)
return done()
}
)
})
})
describe('getCurrencyCode', function () {
it('should return GBP for GB country', function (done) {
this.GeoIpLookup.getDetails = sinon
.stub()
.callsArgWith(1, null, this.stubbedResponse)
return this.GeoIpLookup.getCurrencyCode(
this.ipAddress,
(err, currencyCode) => {
currencyCode.should.equal('GBP')
return done()
}
)
})
it('should return GBP for gb country', function (done) {
this.stubbedResponse.country_code = 'gb'
this.GeoIpLookup.getDetails = sinon
.stub()
.callsArgWith(1, null, this.stubbedResponse)
return this.GeoIpLookup.getCurrencyCode(
this.ipAddress,
(err, currencyCode) => {
currencyCode.should.equal('GBP')
return done()
}
)
})
it('should return USD for US', function (done) {
this.stubbedResponse.country_code = 'US'
this.GeoIpLookup.getDetails = sinon
.stub()
.callsArgWith(1, null, this.stubbedResponse)
return this.GeoIpLookup.getCurrencyCode(
this.ipAddress,
(err, currencyCode) => {
currencyCode.should.equal('USD')
return done()
}
)
})
it('should return EUR for DE', function (done) {
this.stubbedResponse.country_code = 'DE'
this.GeoIpLookup.getDetails = sinon
.stub()
.callsArgWith(1, null, this.stubbedResponse)
return this.GeoIpLookup.getCurrencyCode(
this.ipAddress,
(err, currencyCode) => {
currencyCode.should.equal('EUR')
return done()
}
)
})
it('should default to USD if there is an error', function (done) {
this.GeoIpLookup.getDetails = sinon.stub().callsArgWith(1, 'problem')
return this.GeoIpLookup.getCurrencyCode(
this.ipAddress,
(err, currencyCode) => {
currencyCode.should.equal('USD')
return done()
}
)
})
it('should default to USD if there are no details', function (done) {
this.GeoIpLookup.getDetails = sinon.stub().callsArgWith(1)
return this.GeoIpLookup.getCurrencyCode(
this.ipAddress,
(err, currencyCode) => {
currencyCode.should.equal('USD')
return done()
}
)
})
it('should default to USD if there is no match for their country', function (done) {
this.stubbedResponse.country_code = 'Non existant'
this.GeoIpLookup.getDetails = sinon
.stub()
.callsArgWith(1, null, this.stubbedResponse)
return this.GeoIpLookup.getCurrencyCode(
this.ipAddress,
(err, currencyCode) => {
currencyCode.should.equal('USD')
return done()
}
)
})
})
})