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-27 02:06:03 +00:00
committed by Copybot
parent b93761f275
commit 18d62dcee9
7 changed files with 826 additions and 1130 deletions
@@ -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
@@ -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),
}
@@ -38,7 +38,7 @@ module.exports = {
webRouter.get(
'/user/subscription/thank-you',
AuthenticationController.requireLogin(),
SubscriptionController.successful_subscription
SubscriptionController.successfulSubscription
)
webRouter.get(
@@ -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),
},
}
@@ -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),
},
}