2019-05-29 05:21:06 -04:00
|
|
|
const async = require('async')
|
|
|
|
const RecurlyWrapper = require('./RecurlyWrapper')
|
2021-04-27 10:17:39 -04:00
|
|
|
const RecurlyClient = require('./RecurlyClient')
|
2019-05-29 05:21:06 -04:00
|
|
|
const { User } = require('../../models/User')
|
2019-08-28 08:59:41 -04:00
|
|
|
const { promisifyAll } = require('../../util/promises')
|
2019-05-29 05:21:06 -04:00
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
const SubscriptionUpdater = require('./SubscriptionUpdater')
|
|
|
|
const LimitationsManager = require('./LimitationsManager')
|
|
|
|
const EmailHandler = require('../Email/EmailHandler')
|
|
|
|
const Analytics = require('../Analytics/AnalyticsManager')
|
2021-04-27 10:17:39 -04:00
|
|
|
const PlansLocator = require('./PlansLocator')
|
|
|
|
const SubscriptionHelper = require('./SubscriptionHelper')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
const SubscriptionHandler = {
|
2020-07-16 02:48:20 -04:00
|
|
|
validateNoSubscriptionInRecurly(userId, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (callback == null) {
|
2021-04-14 09:17:21 -04:00
|
|
|
callback = function () {}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
RecurlyWrapper.listAccountActiveSubscriptions(
|
|
|
|
userId,
|
|
|
|
function (error, subscriptions) {
|
|
|
|
if (subscriptions == null) {
|
|
|
|
subscriptions = []
|
|
|
|
}
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
if (subscriptions.length > 0) {
|
|
|
|
SubscriptionUpdater.syncSubscription(
|
|
|
|
subscriptions[0],
|
|
|
|
userId,
|
|
|
|
function (error) {
|
2020-07-16 02:48:20 -04:00
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
callback(null, false)
|
2020-07-16 02:48:20 -04:00
|
|
|
}
|
|
|
|
)
|
2021-04-14 09:17:21 -04:00
|
|
|
} else {
|
|
|
|
callback(null, true)
|
2020-07-16 02:48:20 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
}
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
createSubscription(user, subscriptionDetails, recurlyTokenIds, callback) {
|
|
|
|
SubscriptionHandler.validateNoSubscriptionInRecurly(
|
|
|
|
user._id,
|
|
|
|
function (error, valid) {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
if (!valid) {
|
|
|
|
return callback(new Error('user already has subscription in recurly'))
|
|
|
|
}
|
|
|
|
RecurlyWrapper.createSubscription(
|
|
|
|
user,
|
|
|
|
subscriptionDetails,
|
|
|
|
recurlyTokenIds,
|
|
|
|
function (error, recurlySubscription) {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
return SubscriptionUpdater.syncSubscription(
|
|
|
|
recurlySubscription,
|
|
|
|
user._id,
|
|
|
|
function (error) {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
return callback()
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
2020-07-16 02:48:20 -04:00
|
|
|
)
|
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
updateSubscription(user, planCode, couponCode, callback) {
|
|
|
|
LimitationsManager.userHasV2Subscription(
|
|
|
|
user,
|
|
|
|
function (err, hasSubscription, subscription) {
|
|
|
|
if (err) {
|
|
|
|
logger.warn(
|
|
|
|
{ err, user_id: user._id, hasSubscription },
|
|
|
|
'there was an error checking user v2 subscription'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (!hasSubscription) {
|
|
|
|
return callback()
|
|
|
|
} else {
|
|
|
|
return async.series(
|
|
|
|
[
|
|
|
|
function (cb) {
|
|
|
|
if (couponCode == null) {
|
|
|
|
return cb()
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
RecurlyWrapper.getSubscription(
|
|
|
|
subscription.recurlySubscription_id,
|
|
|
|
{ includeAccount: true },
|
|
|
|
function (err, usersSubscription) {
|
|
|
|
if (err != null) {
|
2021-04-27 10:17:39 -04:00
|
|
|
return cb(err)
|
2021-04-14 09:17:21 -04:00
|
|
|
}
|
|
|
|
RecurlyWrapper.redeemCoupon(
|
|
|
|
usersSubscription.account.account_code,
|
|
|
|
couponCode,
|
|
|
|
cb
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
)
|
|
|
|
},
|
2021-04-27 10:17:39 -04:00
|
|
|
function (cb) {
|
|
|
|
let changeAtTermEnd
|
|
|
|
const currentPlan = PlansLocator.findLocalPlanInSettings(
|
|
|
|
subscription.planCode
|
|
|
|
)
|
|
|
|
const newPlan = PlansLocator.findLocalPlanInSettings(planCode)
|
|
|
|
if (currentPlan && newPlan) {
|
|
|
|
changeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd(
|
|
|
|
currentPlan,
|
|
|
|
newPlan
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
logger.error(
|
|
|
|
{ currentPlan: subscription.planCode, newPlan: planCode },
|
|
|
|
'unable to locate both plans in settings'
|
|
|
|
)
|
|
|
|
return cb(
|
|
|
|
new Error('unable to locate both plans in settings')
|
|
|
|
)
|
|
|
|
}
|
|
|
|
const timeframe = changeAtTermEnd ? 'term_end' : 'now'
|
|
|
|
RecurlyClient.changeSubscriptionByUuid(
|
2021-04-14 09:17:21 -04:00
|
|
|
subscription.recurlySubscription_id,
|
2021-04-27 10:17:39 -04:00
|
|
|
{ planCode: planCode, timeframe: timeframe },
|
|
|
|
function (error, subscriptionChange) {
|
2021-04-14 09:17:21 -04:00
|
|
|
if (error != null) {
|
2021-04-27 10:17:39 -04:00
|
|
|
return cb(error)
|
2021-04-14 09:17:21 -04:00
|
|
|
}
|
2021-04-27 10:17:39 -04:00
|
|
|
// v2 recurly API wants a UUID, but UUID isn't included in the subscription change response
|
|
|
|
// we got the UUID from the DB using userHasV2Subscription() - it is the only property
|
|
|
|
// we need to be able to build a 'recurlySubscription' object for syncSubscription()
|
|
|
|
SubscriptionHandler.syncSubscription(
|
|
|
|
{ uuid: subscription.recurlySubscription_id },
|
2021-04-14 09:17:21 -04:00
|
|
|
user._id,
|
|
|
|
cb
|
|
|
|
)
|
|
|
|
}
|
2021-04-27 10:17:39 -04:00
|
|
|
)
|
|
|
|
},
|
2021-04-14 09:17:21 -04:00
|
|
|
],
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
2021-04-27 10:17:39 -04:00
|
|
|
cancelPendingSubscriptionChange(user, callback) {
|
|
|
|
LimitationsManager.userHasV2Subscription(
|
|
|
|
user,
|
|
|
|
function (err, hasSubscription, subscription) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
if (hasSubscription) {
|
|
|
|
RecurlyClient.removeSubscriptionChangeByUuid(
|
|
|
|
subscription.recurlySubscription_id,
|
|
|
|
function (error) {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
callback()
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
callback()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2019-05-29 05:21:06 -04:00
|
|
|
cancelSubscription(user, callback) {
|
2021-04-14 09:17:21 -04:00
|
|
|
LimitationsManager.userHasV2Subscription(
|
|
|
|
user,
|
|
|
|
function (err, hasSubscription, subscription) {
|
|
|
|
if (err) {
|
|
|
|
logger.warn(
|
|
|
|
{ err, user_id: user._id, hasSubscription },
|
|
|
|
'there was an error checking user v2 subscription'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (hasSubscription) {
|
|
|
|
RecurlyWrapper.cancelSubscription(
|
|
|
|
subscription.recurlySubscription_id,
|
|
|
|
function (error) {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
const emailOpts = {
|
|
|
|
to: user.email,
|
2021-04-27 03:52:58 -04:00
|
|
|
first_name: user.first_name,
|
2021-04-14 09:17:21 -04:00
|
|
|
}
|
|
|
|
const ONE_HOUR_IN_MS = 1000 * 60 * 60
|
|
|
|
setTimeout(
|
|
|
|
() =>
|
|
|
|
EmailHandler.sendEmail(
|
|
|
|
'canceledSubscription',
|
|
|
|
emailOpts,
|
|
|
|
err => {
|
|
|
|
if (err != null) {
|
|
|
|
logger.warn(
|
|
|
|
{ err },
|
|
|
|
'failed to send confirmation email for subscription cancellation'
|
|
|
|
)
|
|
|
|
}
|
2019-10-15 09:12:11 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
),
|
|
|
|
ONE_HOUR_IN_MS
|
|
|
|
)
|
|
|
|
Analytics.recordEvent(user._id, 'subscription-canceled')
|
|
|
|
callback()
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
callback()
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
reactivateSubscription(user, callback) {
|
2021-04-14 09:17:21 -04:00
|
|
|
LimitationsManager.userHasV2Subscription(
|
|
|
|
user,
|
|
|
|
function (err, hasSubscription, subscription) {
|
|
|
|
if (err) {
|
|
|
|
logger.warn(
|
|
|
|
{ err, user_id: user._id, hasSubscription },
|
|
|
|
'there was an error checking user v2 subscription'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (hasSubscription) {
|
|
|
|
RecurlyWrapper.reactivateSubscription(
|
|
|
|
subscription.recurlySubscription_id,
|
|
|
|
function (error) {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
2019-10-15 09:12:11 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
EmailHandler.sendEmail(
|
|
|
|
'reactivatedSubscription',
|
|
|
|
{ to: user.email },
|
|
|
|
err => {
|
|
|
|
if (err != null) {
|
|
|
|
logger.warn(
|
|
|
|
{ err },
|
|
|
|
'failed to send reactivation confirmation email'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
Analytics.recordEvent(user._id, 'subscription-reactivated')
|
|
|
|
callback()
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
callback()
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
2019-11-12 03:56:08 -05:00
|
|
|
syncSubscription(recurlySubscription, requesterData, callback) {
|
2020-07-16 02:48:20 -04:00
|
|
|
RecurlyWrapper.getSubscription(
|
2019-05-29 05:21:06 -04:00
|
|
|
recurlySubscription.uuid,
|
|
|
|
{ includeAccount: true },
|
2021-04-14 09:17:21 -04:00
|
|
|
function (error, recurlySubscription) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2020-07-16 02:48:20 -04:00
|
|
|
User.findById(
|
|
|
|
recurlySubscription.account.account_code,
|
|
|
|
{ _id: 1 },
|
2021-04-14 09:17:21 -04:00
|
|
|
function (error, user) {
|
2020-07-16 02:48:20 -04:00
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
if (user == null) {
|
|
|
|
return callback(new Error('no user found'))
|
|
|
|
}
|
|
|
|
SubscriptionUpdater.syncSubscription(
|
|
|
|
recurlySubscription,
|
|
|
|
user != null ? user._id : undefined,
|
|
|
|
requesterData,
|
|
|
|
callback
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-07-16 02:48:20 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2019-11-12 03:56:08 -05:00
|
|
|
// attempt to collect past due invoice for customer. Only do that when a) the
|
|
|
|
// customer is using Paypal and b) there is only one past due invoice.
|
|
|
|
// This is used because Recurly doesn't always attempt collection of paast due
|
|
|
|
// invoices after Paypal billing info were updated.
|
|
|
|
attemptPaypalInvoiceCollection(recurlyAccountCode, callback) {
|
|
|
|
RecurlyWrapper.getBillingInfo(recurlyAccountCode, (error, billingInfo) => {
|
|
|
|
if (error) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
if (!billingInfo.paypal_billing_agreement_id) {
|
|
|
|
// this is not a Paypal user
|
|
|
|
return callback()
|
|
|
|
}
|
|
|
|
RecurlyWrapper.getAccountPastDueInvoices(
|
|
|
|
recurlyAccountCode,
|
|
|
|
(error, pastDueInvoices) => {
|
|
|
|
if (error) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
if (pastDueInvoices.length !== 1) {
|
|
|
|
// no past due invoices, or more than one. Ignore.
|
|
|
|
return callback()
|
|
|
|
}
|
|
|
|
RecurlyWrapper.attemptInvoiceCollection(
|
|
|
|
pastDueInvoices[0].invoice_number,
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-05-29 05:21:06 -04:00
|
|
|
extendTrial(subscription, daysToExend, callback) {
|
|
|
|
return RecurlyWrapper.extendTrial(
|
|
|
|
subscription.recurlySubscription_id,
|
|
|
|
daysToExend,
|
|
|
|
callback
|
|
|
|
)
|
2021-04-27 03:52:58 -04:00
|
|
|
},
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-28 08:59:41 -04:00
|
|
|
|
|
|
|
SubscriptionHandler.promises = promisifyAll(SubscriptionHandler)
|
|
|
|
module.exports = SubscriptionHandler
|