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 logger = require('logger-sharelatex')
const ProjectGetter = require('../Project/ProjectGetter')
@ -9,8 +8,9 @@ const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
const CollaboratorsInvitesHandler = require('../Collaborators/CollaboratorsInviteHandler')
const V1SubscriptionManager = require('./V1SubscriptionManager')
const { V1ConnectionError } = require('../Errors/Errors')
const { promisifyAll } = require('../../util/promises')
module.exports = LimitationsManager = {
const LimitationsManager = {
allowedNumberOfCollaboratorsInProject(projectId, callback) {
ProjectGetter.getProject(
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 SubscriptionHandler = require('./SubscriptionHandler')
const PlansLocator = require('./PlansLocator')
@ -24,7 +7,6 @@ const RecurlyWrapper = require('./RecurlyWrapper')
const Settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
const GeoIpLookup = require('../../infrastructure/GeoIpLookup')
const UserGetter = require('../User/UserGetter')
const FeaturesUpdater = require('./FeaturesUpdater')
const planFeatures = require('./planFeatures')
const GroupPlansData = require('./GroupPlansData')
@ -34,537 +16,485 @@ const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const SubscriptionErrors = require('./Errors')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const { expressify } = require('../../util/promises')
const OError = require('@overleaf/o-error')
const _ = require('lodash')
const SUBSCRIPTION_PAGE_SPLIT_TEST = 'subscription-page'
module.exports = SubscriptionController = {
plansPage(req, res, next) {
const plans = SubscriptionViewModelBuilder.buildPlansList()
let currentUser = null
async function plansPage(req, res) {
const plans = SubscriptionViewModelBuilder.buildPlansList()
return GeoIpLookup.getCurrencyCode(
(req.query != null ? req.query.ip : undefined) || req.ip,
function (err, recomendedCurrency) {
if (err != null) {
return next(err)
}
const render = () =>
res.render('subscriptions/plans', {
title: 'plans_and_pricing',
plans,
gaExperiments: Settings.gaExperiments.plansPage,
gaOptimize: true,
recomendedCurrency,
planFeatures,
groupPlans: GroupPlansData,
})
const user_id = AuthenticationController.getLoggedInUserId(req)
if (user_id != null) {
return UserGetter.getUser(
user_id,
{ signUpDate: 1 },
function (err, user) {
if (err != null) {
return next(err)
}
currentUser = user
return render()
}
)
} else {
return render()
}
}
const recommendedCurrency = await GeoIpLookup.promises.getCurrencyCode(
(req.query ? req.query.ip : undefined) || req.ip
)
res.render('subscriptions/plans', {
title: 'plans_and_pricing',
plans,
gaExperiments: Settings.gaExperiments.plansPage,
gaOptimize: true,
recomendedCurrency: recommendedCurrency,
planFeatures,
groupPlans: GroupPlansData,
})
}
// get to show the recurly.js page
async function paymentPage(req, res) {
const user = AuthenticationController.getSessionUser(req)
const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
if (!plan) {
return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
}
const hasSubscription = await LimitationsManager.promises.userHasV1OrV2Subscription(
user
)
if (hasSubscription) {
res.redirect('/user/subscription?hasSubscription=true')
} else {
// LimitationsManager.userHasV2Subscription only checks Mongo. Double check with
// Recurly as well at this point (we don't do this most places for speed).
const valid = await SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
user._id
)
},
// get to show the recurly.js page
paymentPage(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
if (!plan) {
return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
if (!valid) {
res.redirect('/user/subscription?hasSubscription=true')
} else {
let currency = req.query.currency
? req.query.currency.toUpperCase()
: undefined
const {
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) {
return next(err)
}
if (hasSubscription) {
return res.redirect('/user/subscription?hasSubscription=true')
} else {
// LimitationsManager.userHasV2Subscription only checks Mongo. Double check with
// 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,
})
}
)
}
}
)
}
}
}
function userSubscriptionPage(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
user,
function (error, results) {
if (error) {
return next(error)
}
)
},
userSubscriptionPage(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
user,
function (error, results) {
if (error != null) {
return next(error)
}
const {
personalSubscription,
memberGroupSubscriptions,
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 {
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
confirmedMemberAffiliations,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
} = results
LimitationsManager.userHasV1OrV2Subscription(
user,
function (err, hasSubscription) {
if (error) {
return next(error)
}
)
}
)
},
const fromPlansPage = req.query.hasSubscription
const plans = SubscriptionViewModelBuilder.buildPlansList(
personalSubscription ? personalSubscription.plan : undefined
)
createSubscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
const recurlyTokenIds = {
billing: req.body.recurly_token_id,
threeDSecureActionResult:
req.body.recurly_three_d_secure_action_result_token_id,
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,
}
res.render('subscriptions/dashboard', data)
}
)
}
const { subscriptionDetails } = req.body
)
}
return LimitationsManager.userHasV1OrV2Subscription(
user,
function (err, hasSubscription) {
if (err != null) {
return next(err)
}
if (hasSubscription) {
logger.warn({ user_id: user._id }, 'user already has subscription')
return res.sendStatus(409) // conflict
}
return SubscriptionHandler.createSubscription(
user,
subscriptionDetails,
recurlyTokenIds,
function (err) {
if (!err) {
return res.sendStatus(201)
}
function createSubscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
const recurlyTokenIds = {
billing: req.body.recurly_token_id,
threeDSecureActionResult:
req.body.recurly_three_d_secure_action_result_token_id,
}
const { subscriptionDetails } = req.body
if (
err instanceof SubscriptionErrors.RecurlyTransactionError ||
err instanceof Errors.InvalidError
) {
logger.error({ err }, 'recurly transaction error, potential 422')
return HttpErrorHandler.unprocessableEntity(
req,
res,
err.message,
OError.getFullInfo(err).public
)
}
LimitationsManager.userHasV1OrV2Subscription(
user,
function (err, hasSubscription) {
if (err) {
return next(err)
}
if (hasSubscription) {
logger.warn({ user_id: user._id }, 'user already has subscription')
return res.sendStatus(409) // conflict
}
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(
{ err, user_id: user._id },
'something went wrong creating subscription'
)
next(err)
}
)
}
)
},
}
)
}
)
}
successful_subscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
user,
function (error, { personalSubscription }) {
if (error != null) {
return next(error)
}
if (personalSubscription == null) {
return res.redirect('/user/subscription/plans')
}
return res.render('subscriptions/successful_subscription', {
function successfulSubscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
user,
function (error, { personalSubscription }) {
if (error) {
return next(error)
}
if (personalSubscription == null) {
res.redirect('/user/subscription/plans')
} else {
res.render('subscriptions/successful_subscription', {
title: 'thank_you',
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) {
const user = AuthenticationController.getSessionUser(req)
logger.log({ user_id: user._id }, 'canceling subscription')
return SubscriptionHandler.cancelSubscription(user, function (err) {
if (err != null) {
OError.tag(err, 'something went wrong canceling subscription', {
function 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) {
OError.tag(
err,
'something went wrong canceling pending subscription change',
{
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)
}
logger.log({ planCode, user_id: user._id }, 'updating subscription')
return SubscriptionHandler.updateSubscription(
user,
planCode,
null,
res.redirect('/user/subscription')
})
}
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) {
if (err != null) {
OError.tag(err, 'something went wrong updating subscription', {
user_id: user._id,
})
if (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)
}
)
},
} 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) {
const user = AuthenticationController.getSessionUser(req)
logger.log({ user_id: user._id }, 'reactivating subscription')
SubscriptionHandler.reactivateSubscription(user, function (err) {
if (err != null) {
OError.tag(err, 'something went wrong reactivating subscription', {
function renderUpgradeToAnnualPlanPage(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasV2Subscription(
user,
function (err, hasSubscription, 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,
})
return next(err)
}
res.redirect('/user/subscription')
})
},
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)
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) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
async function extendTrial(req, res) {
const user = AuthenticationController.getSessionUser(req)
const {
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(
'/user/subscription/thank-you',
AuthenticationController.requireLogin(),
SubscriptionController.successful_subscription
SubscriptionController.successfulSubscription
)
webRouter.get(

View file

@ -10,6 +10,7 @@ const sanitizeHtml = require('sanitize-html')
const _ = require('underscore')
const async = require('async')
const SubscriptionHelper = require('./SubscriptionHelper')
const { promisify } = require('../../util/promises')
function buildHostedLink(recurlySubscription, type) {
const recurlySubdomain = Settings.apis.recurly.subdomain
@ -29,311 +30,312 @@ function buildHostedLink(recurlySubscription, type) {
}
}
module.exports = {
buildUsersSubscriptionViewModel(user, callback) {
async.auto(
{
personalSubscription(cb) {
SubscriptionLocator.getUsersSubscription(user, cb)
},
recurlySubscription: [
'personalSubscription',
(cb, { personalSubscription }) => {
if (
personalSubscription == null ||
personalSubscription.recurlySubscription_id == null ||
personalSubscription.recurlySubscription_id === ''
) {
return cb(null, null)
}
RecurlyWrapper.getSubscription(
personalSubscription.recurlySubscription_id,
{ includeAccount: true },
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)
}
function buildUsersSubscriptionViewModel(user, callback) {
async.auto(
{
personalSubscription(cb) {
SubscriptionLocator.getUsersSubscription(user, cb)
},
recurlySubscription: [
'personalSubscription',
(cb, { personalSubscription }) => {
if (
personalSubscription == null ||
personalSubscription.recurlySubscription_id == null ||
personalSubscription.recurlySubscription_id === ''
) {
return cb(null, null)
}
RecurlyWrapper.getSubscription(
personalSubscription.recurlySubscription_id,
{ includeAccount: true },
cb
)
},
},
(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
}
})
],
recurlyCoupons: [
'recurlySubscription',
(cb, { recurlySubscription }) => {
if (!recurlySubscription) {
return cb(null, null)
}
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,
const accountId = recurlySubscription.account.account_code
RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
},
],
plan: [
'personalSubscription',
(cb, { personalSubscription }) => {
if (personalSubscription == null) {
return cb()
}
if (recurlySubscription.pending_subscription) {
const pendingPlan = PlansLocator.findLocalPlanInSettings(
recurlySubscription.pending_subscription.plan.plan_code
const plan = PlansLocator.findLocalPlanInSettings(
personalSubscription.planCode
)
if (plan == null) {
return cb(
new Error(
`No plan found for planCode '${personalSubscription.planCode}'`
)
)
if (pendingPlan == null) {
return callback(
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) => {
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
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
}
}
)
}
// 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
)
// 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
)
}
for (const memberGroupSubscription of memberGroupSubscriptions) {
if (memberGroupSubscription.teamNotice) {
memberGroupSubscription.teamNotice = sanitizeHtml(
memberGroupSubscription.teamNotice
)
}
}
callback(null, {
personalSubscription,
managedGroupSubscriptions,
memberGroupSubscriptions,
confirmedMemberAffiliations,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
})
}
)
},
buildPlansList(currentPlan) {
const { plans } = Settings
for (const memberGroupSubscription of memberGroupSubscriptions) {
if (memberGroupSubscription.teamNotice) {
memberGroupSubscription.teamNotice = sanitizeHtml(
memberGroupSubscription.teamNotice
)
}
}
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'
)
callback(null, {
personalSubscription,
managedGroupSubscriptions,
memberGroupSubscriptions,
confirmedMemberAffiliations,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
})
}
)
}
result.studentAccounts = _.filter(
plans,
plan => plan.planCode.indexOf('student') !== -1
function buildPlansList(currentPlan) {
const { plans } = Settings
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(
plans,
plan => plan.groupPlan && !plan.annual
)
result.studentAccounts = _.filter(
plans,
plan => plan.planCode.indexOf('student') !== -1
)
result.groupAnnualPlans = _.filter(
plans,
plan => plan.groupPlan && plan.annual
)
result.groupMonthlyPlans = _.filter(
plans,
plan => plan.groupPlan && !plan.annual
)
result.individualMonthlyPlans = _.filter(
plans,
plan =>
!plan.groupPlan &&
!plan.annual &&
plan.planCode !== 'personal' && // Prevent the personal plan from appearing on the change-plans page
plan.planCode.indexOf('student') === -1
)
result.groupAnnualPlans = _.filter(
plans,
plan => plan.groupPlan && plan.annual
)
result.individualAnnualPlans = _.filter(
plans,
plan =>
!plan.groupPlan &&
plan.annual &&
plan.planCode.indexOf('student') === -1
)
result.individualMonthlyPlans = _.filter(
plans,
plan =>
!plan.groupPlan &&
!plan.annual &&
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 settings = require('settings-sharelatex')
const _ = require('underscore')
const logger = require('logger-sharelatex')
const URL = require('url')
const { promisify } = require('../util/promises')
const currencyMappings = {
GB: 'GBP',
@ -61,49 +48,50 @@ const EuroCountries = [
_.each(EuroCountries, country => (currencyMappings[country] = 'EUR'))
module.exports = GeoIpLookup = {
getDetails(ip, callback) {
if (ip == null) {
const e = new Error('no ip passed')
return callback(e)
function getDetails(ip, callback) {
if (ip == null) {
const e = new Error('no ip passed')
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]
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)
})
},
callback(err, ipDetails)
})
}
getCurrencyCode(ip, callback) {
return GeoIpLookup.getDetails(ip, function (err, ipDetails) {
if (err != null || ipDetails == null) {
logger.err(
{ err, ip },
'problem getting currencyCode for ip, defaulting to USD'
)
return callback(null, 'USD')
}
const countryCode = __guard__(
ipDetails != null ? ipDetails.country_code : undefined,
x => x.toUpperCase()
function getCurrencyCode(ip, callback) {
getDetails(ip, function (err, ipDetails) {
if (err || !ipDetails) {
logger.err(
{ err, ip },
'problem getting currencyCode for ip, defaulting to USD'
)
const currencyCode = currencyMappings[countryCode] || 'USD'
logger.log({ ip, currencyCode, ipDetails }, 'got currencyCode for ip')
return callback(err, currencyCode, countryCode)
})
},
return callback(null, 'USD')
}
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) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
module.exports = {
getDetails,
getCurrencyCode,
promises: {
getDetails: promisify(getDetails),
getCurrencyCode: promisify(getCurrencyCode),
},
}

View file

@ -61,6 +61,15 @@ describe('SubscriptionController', function () {
syncSubscription: sinon.stub().yields(),
attemptPaypalInvoiceCollection: sinon.stub().yields(),
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() }
@ -69,11 +78,19 @@ describe('SubscriptionController', function () {
hasPaidSubscription: sinon.stub(),
userHasV1OrV2Subscription: sinon.stub(),
userHasV2Subscription: sinon.stub(),
promises: {
hasPaidSubscription: sinon.stub().resolves(),
userHasV1OrV2Subscription: sinon.stub().resolves(),
userHasV2Subscription: sinon.stub().resolves(),
},
}
this.SubscriptionViewModelBuilder = {
buildUsersSubscriptionViewModel: sinon.stub().callsArgWith(1, null, {}),
buildPlansList: sinon.stub(),
promises: {
buildUsersSubscriptionViewModel: sinon.stub().resolves({}),
},
}
this.settings = {
coupon_codes: {
@ -90,9 +107,17 @@ describe('SubscriptionController', function () {
siteUrl: 'http://de.sharelatex.dev:3000',
gaExperiments: {},
}
this.GeoIpLookup = { getCurrencyCode: sinon.stub() }
this.GeoIpLookup = {
getCurrencyCode: sinon.stub(),
promises: {
getCurrencyCode: sinon.stub(),
},
}
this.UserGetter = {
getUser: sinon.stub().callsArgWith(2, null, this.user),
promises: {
getUser: sinon.stub().resolves(this.user),
},
}
this.SubscriptionController = SandboxedModule.require(modulePath, {
requires: {
@ -135,75 +160,26 @@ describe('SubscriptionController', function () {
describe('plansPage', function () {
beforeEach(function () {
this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534'
return this.GeoIpLookup.getCurrencyCode.callsArgWith(
1,
null,
return this.GeoIpLookup.promises.getCurrencyCode.resolves(
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 () {
beforeEach(function () {
this.req.headers = {}
this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly = sinon
.stub()
.yields(null, true)
return this.GeoIpLookup.getCurrencyCode.callsArgWith(
1,
null,
this.stubbedCurrencyCode
)
.resolves(true)
return this.GeoIpLookup.promises.getCurrencyCode.resolves({
recomendedCurrency: this.stubbedCurrencyCode,
})
})
describe('with a user without a subscription', function () {
beforeEach(function () {
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
1,
null,
this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
false
)
return this.PlansLocator.findLocalPlanInSettings.returns({})
@ -213,9 +189,9 @@ describe('SubscriptionController', function () {
it('should render the new subscription page', function (done) {
this.res.render = (page, opts) => {
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 () {
it('should redirect to the subscription dashboard', function (done) {
this.PlansLocator.findLocalPlanInSettings.returns({})
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
1,
null,
this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
true
)
this.res.redirect = url => {
@ -238,9 +212,7 @@ describe('SubscriptionController', function () {
describe('with an invalid plan code', function () {
it('should return 422 error - Unprocessable Entity', function (done) {
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
1,
null,
this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
false
)
this.PlansLocator.findLocalPlanInSettings.returns(null)
@ -252,19 +224,13 @@ describe('SubscriptionController', function () {
done()
}
)
return this.SubscriptionController.paymentPage(
this.req,
this.res,
this.next
)
return this.SubscriptionController.paymentPage(this.req, this.res)
})
})
describe('which currency to use', function () {
beforeEach(function () {
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
1,
null,
this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
false
)
return this.PlansLocator.findLocalPlanInSettings.returns({})
@ -293,23 +259,21 @@ describe('SubscriptionController', function () {
this.req.query.currency = null
this.res.render = (page, opts) => {
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 () {
it('should redirect to the subscription dashboard', function (done) {
this.PlansLocator.findLocalPlanInSettings.returns({})
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
1,
null,
this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
false
)
this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly.resolves(
false
)
this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
.stub()
.yields(null, false)
this.res.redirect = url => {
url.should.equal('/user/subscription?hasSubscription=true')
return done()
@ -319,7 +283,7 @@ describe('SubscriptionController', function () {
})
})
describe('successful_subscription', function () {
describe('successfulSubscription', function () {
beforeEach(function (done) {
this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(
1,
@ -327,7 +291,7 @@ describe('SubscriptionController', function () {
{}
)
this.res.callback = done
return this.SubscriptionController.successful_subscription(
return this.SubscriptionController.successfulSubscription(
this.req,
this.res
)
@ -359,12 +323,9 @@ describe('SubscriptionController', function () {
this.res.render = (view, data) => {
this.data = data
expect(view).to.equal('subscriptions/dashboard')
return done()
done()
}
return this.SubscriptionController.userSubscriptionPage(
this.req,
this.res
)
this.SubscriptionController.userSubscriptionPage(this.req, this.res)
})
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()
}
)
})
})
})