mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #4071 from overleaf/ab-subscription-decaf-cleanup
Subscription controller decaf cleanup GitOrigin-RevId: 79b8adfabe30e4557a95b1aad71a5162e6f42cce
This commit is contained in:
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),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue