const async = require('async') const { promisify } = require('util') const RecurlyWrapper = require('./RecurlyWrapper') const RecurlyClient = require('./RecurlyClient') const { User } = require('../../models/User') const logger = require('@overleaf/logger') const SubscriptionUpdater = require('./SubscriptionUpdater') const LimitationsManager = require('./LimitationsManager') const EmailHandler = require('../Email/EmailHandler') const PlansLocator = require('./PlansLocator') const SubscriptionHelper = require('./SubscriptionHelper') function validateNoSubscriptionInRecurly(userId, callback) { RecurlyWrapper.listAccountActiveSubscriptions( userId, function (error, subscriptions) { if (!subscriptions) { subscriptions = [] } if (error) { return callback(error) } if (subscriptions.length > 0) { SubscriptionUpdater.syncSubscription( subscriptions[0], userId, function (error) { if (error) { return callback(error) } callback(null, false) } ) } else { callback(null, true) } } ) } function createSubscription( user, subscriptionDetails, recurlyTokenIds, callback ) { validateNoSubscriptionInRecurly(user._id, function (error, valid) { if (error) { 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) { return callback(error) } return SubscriptionUpdater.syncSubscription( recurlySubscription, user._id, function (error) { if (error) { return callback(error) } return callback() } ) } ) }) } function 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) { return cb() } RecurlyWrapper.getSubscription( subscription.recurlySubscription_id, { includeAccount: true }, function (err, usersSubscription) { if (err) { return cb(err) } RecurlyWrapper.redeemCoupon( usersSubscription.account.account_code, couponCode, cb ) } ) }, 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( subscription.recurlySubscription_id, { planCode, timeframe }, function (error, subscriptionChange) { if (error) { return cb(error) } // 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() syncSubscription( { uuid: subscription.recurlySubscription_id }, user._id, cb ) } ) }, ], callback ) } } ) } function 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) { return callback(error) } callback() } ) } else { callback() } } ) } function cancelSubscription(user, 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) { RecurlyClient.cancelSubscriptionByUuid( subscription.recurlySubscription_id, function (error) { if (error) { return callback(error) } const emailOpts = { to: user.email, first_name: user.first_name, } const ONE_HOUR_IN_MS = 1000 * 60 * 60 EmailHandler.sendDeferredEmail( 'canceledSubscription', emailOpts, ONE_HOUR_IN_MS ) callback() } ) } else { callback() } } ) } function reactivateSubscription(user, 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) { RecurlyClient.reactivateSubscriptionByUuid( subscription.recurlySubscription_id, function (error) { if (error) { return callback(error) } EmailHandler.sendEmail( 'reactivatedSubscription', { to: user.email }, err => { if (err) { logger.warn( { err }, 'failed to send reactivation confirmation email' ) } } ) callback() } ) } else { callback() } } ) } function syncSubscription(recurlySubscription, requesterData, callback) { RecurlyWrapper.getSubscription( recurlySubscription.uuid, { includeAccount: true }, function (error, recurlySubscription) { if (error) { return callback(error) } User.findById( recurlySubscription.account.account_code, { _id: 1 }, function (error, user) { if (error) { return callback(error) } if (!user) { return callback(new Error('no user found')) } SubscriptionUpdater.syncSubscription( recurlySubscription, user._id, requesterData, callback ) } ) } ) } // 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. function 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 ) } ) }) } function extendTrial(subscription, daysToExend, callback) { return RecurlyWrapper.extendTrial( subscription.recurlySubscription_id, daysToExend, callback ) } module.exports = { validateNoSubscriptionInRecurly, createSubscription, updateSubscription, cancelPendingSubscriptionChange, cancelSubscription, reactivateSubscription, syncSubscription, attemptPaypalInvoiceCollection, extendTrial, promises: { validateNoSubscriptionInRecurly: promisify(validateNoSubscriptionInRecurly), createSubscription: promisify(createSubscription), updateSubscription: promisify(updateSubscription), cancelPendingSubscriptionChange: promisify(cancelPendingSubscriptionChange), cancelSubscription: promisify(cancelSubscription), reactivateSubscription: promisify(reactivateSubscription), syncSubscription: promisify(syncSubscription), attemptPaypalInvoiceCollection: promisify(attemptPaypalInvoiceCollection), extendTrial: promisify(extendTrial), }, }