SubscriptionHandler.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. const async = require('async')
  2. const { promisify } = require('util')
  3. const RecurlyWrapper = require('./RecurlyWrapper')
  4. const RecurlyClient = require('./RecurlyClient')
  5. const { User } = require('../../models/User')
  6. const logger = require('@overleaf/logger')
  7. const SubscriptionUpdater = require('./SubscriptionUpdater')
  8. const LimitationsManager = require('./LimitationsManager')
  9. const EmailHandler = require('../Email/EmailHandler')
  10. const PlansLocator = require('./PlansLocator')
  11. const SubscriptionHelper = require('./SubscriptionHelper')
  12. const { callbackify } = require('../../util/promises')
  13. function validateNoSubscriptionInRecurly(userId, callback) {
  14. RecurlyWrapper.listAccountActiveSubscriptions(
  15. userId,
  16. function (error, subscriptions) {
  17. if (!subscriptions) {
  18. subscriptions = []
  19. }
  20. if (error) {
  21. return callback(error)
  22. }
  23. if (subscriptions.length > 0) {
  24. SubscriptionUpdater.syncSubscription(
  25. subscriptions[0],
  26. userId,
  27. function (error) {
  28. if (error) {
  29. return callback(error)
  30. }
  31. callback(null, false)
  32. }
  33. )
  34. } else {
  35. callback(null, true)
  36. }
  37. }
  38. )
  39. }
  40. function createSubscription(
  41. user,
  42. subscriptionDetails,
  43. recurlyTokenIds,
  44. callback
  45. ) {
  46. validateNoSubscriptionInRecurly(user._id, function (error, valid) {
  47. if (error) {
  48. return callback(error)
  49. }
  50. if (!valid) {
  51. return callback(new Error('user already has subscription in recurly'))
  52. }
  53. RecurlyWrapper.createSubscription(
  54. user,
  55. subscriptionDetails,
  56. recurlyTokenIds,
  57. function (error, recurlySubscription) {
  58. if (error) {
  59. return callback(error)
  60. }
  61. SubscriptionUpdater.syncSubscription(
  62. recurlySubscription,
  63. user._id,
  64. function (error) {
  65. if (error) {
  66. return callback(error)
  67. }
  68. callback()
  69. }
  70. )
  71. }
  72. )
  73. })
  74. }
  75. function updateSubscription(user, planCode, couponCode, callback) {
  76. LimitationsManager.userHasV2Subscription(
  77. user,
  78. function (err, hasSubscription, subscription) {
  79. if (err) {
  80. logger.warn(
  81. { err, userId: user._id, hasSubscription },
  82. 'there was an error checking user v2 subscription'
  83. )
  84. }
  85. if (!hasSubscription) {
  86. callback()
  87. } else {
  88. async.series(
  89. [
  90. function (cb) {
  91. if (!couponCode) {
  92. return cb()
  93. }
  94. RecurlyWrapper.getSubscription(
  95. subscription.recurlySubscription_id,
  96. { includeAccount: true },
  97. function (err, usersSubscription) {
  98. if (err) {
  99. return cb(err)
  100. }
  101. RecurlyWrapper.redeemCoupon(
  102. usersSubscription.account.account_code,
  103. couponCode,
  104. cb
  105. )
  106. }
  107. )
  108. },
  109. function (cb) {
  110. let changeAtTermEnd
  111. const currentPlan = PlansLocator.findLocalPlanInSettings(
  112. subscription.planCode
  113. )
  114. const newPlan = PlansLocator.findLocalPlanInSettings(planCode)
  115. if (currentPlan && newPlan) {
  116. changeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd(
  117. currentPlan,
  118. newPlan
  119. )
  120. } else {
  121. logger.error(
  122. { currentPlan: subscription.planCode, newPlan: planCode },
  123. 'unable to locate both plans in settings'
  124. )
  125. return cb(new Error('unable to locate both plans in settings'))
  126. }
  127. const timeframe = changeAtTermEnd ? 'term_end' : 'now'
  128. RecurlyClient.changeSubscriptionByUuid(
  129. subscription.recurlySubscription_id,
  130. { planCode, timeframe },
  131. function (error, subscriptionChange) {
  132. if (error) {
  133. return cb(error)
  134. }
  135. // v2 recurly API wants a UUID, but UUID isn't included in the subscription change response
  136. // we got the UUID from the DB using userHasV2Subscription() - it is the only property
  137. // we need to be able to build a 'recurlySubscription' object for syncSubscription()
  138. syncSubscription(
  139. { uuid: subscription.recurlySubscription_id },
  140. user._id,
  141. cb
  142. )
  143. }
  144. )
  145. },
  146. ],
  147. callback
  148. )
  149. }
  150. }
  151. )
  152. }
  153. function cancelPendingSubscriptionChange(user, callback) {
  154. LimitationsManager.userHasV2Subscription(
  155. user,
  156. function (err, hasSubscription, subscription) {
  157. if (err) {
  158. return callback(err)
  159. }
  160. if (hasSubscription) {
  161. RecurlyClient.removeSubscriptionChangeByUuid(
  162. subscription.recurlySubscription_id,
  163. function (error) {
  164. if (error) {
  165. return callback(error)
  166. }
  167. callback()
  168. }
  169. )
  170. } else {
  171. callback()
  172. }
  173. }
  174. )
  175. }
  176. async function cancelSubscription(user) {
  177. try {
  178. const { hasSubscription, subscription } =
  179. await LimitationsManager.promises.userHasV2Subscription(user)
  180. if (hasSubscription) {
  181. await RecurlyClient.promises.cancelSubscriptionByUuid(
  182. subscription.recurlySubscription_id
  183. )
  184. await _updateSubscriptionFromRecurly(subscription)
  185. const emailOpts = {
  186. to: user.email,
  187. first_name: user.first_name,
  188. }
  189. const ONE_HOUR_IN_MS = 1000 * 60 * 60
  190. EmailHandler.sendDeferredEmail(
  191. 'canceledSubscription',
  192. emailOpts,
  193. ONE_HOUR_IN_MS
  194. )
  195. }
  196. } catch (err) {
  197. logger.warn(
  198. { err, userId: user._id },
  199. 'there was an error checking user v2 subscription'
  200. )
  201. }
  202. }
  203. async function reactivateSubscription(user) {
  204. try {
  205. const { hasSubscription, subscription } =
  206. await LimitationsManager.promises.userHasV2Subscription(user)
  207. if (hasSubscription) {
  208. await RecurlyClient.promises.reactivateSubscriptionByUuid(
  209. subscription.recurlySubscription_id
  210. )
  211. await _updateSubscriptionFromRecurly(subscription)
  212. EmailHandler.sendEmail(
  213. 'reactivatedSubscription',
  214. { to: user.email },
  215. err => {
  216. if (err) {
  217. logger.warn(
  218. { err },
  219. 'failed to send reactivation confirmation email'
  220. )
  221. }
  222. }
  223. )
  224. }
  225. } catch (err) {
  226. logger.warn(
  227. { err, userId: user._id },
  228. 'there was an error checking user v2 subscription'
  229. )
  230. }
  231. }
  232. function syncSubscription(recurlySubscription, requesterData, callback) {
  233. RecurlyWrapper.getSubscription(
  234. recurlySubscription.uuid,
  235. { includeAccount: true },
  236. function (error, recurlySubscription) {
  237. if (error) {
  238. return callback(error)
  239. }
  240. User.findById(
  241. recurlySubscription.account.account_code,
  242. { _id: 1 },
  243. function (error, user) {
  244. if (error) {
  245. return callback(error)
  246. }
  247. if (!user) {
  248. return callback(new Error('no user found'))
  249. }
  250. SubscriptionUpdater.syncSubscription(
  251. recurlySubscription,
  252. user._id,
  253. requesterData,
  254. callback
  255. )
  256. }
  257. )
  258. }
  259. )
  260. }
  261. // attempt to collect past due invoice for customer. Only do that when a) the
  262. // customer is using Paypal and b) there is only one past due invoice.
  263. // This is used because Recurly doesn't always attempt collection of paast due
  264. // invoices after Paypal billing info were updated.
  265. function attemptPaypalInvoiceCollection(recurlyAccountCode, callback) {
  266. RecurlyWrapper.getBillingInfo(recurlyAccountCode, (error, billingInfo) => {
  267. if (error) {
  268. return callback(error)
  269. }
  270. if (!billingInfo.paypal_billing_agreement_id) {
  271. // this is not a Paypal user
  272. return callback()
  273. }
  274. RecurlyWrapper.getAccountPastDueInvoices(
  275. recurlyAccountCode,
  276. (error, pastDueInvoices) => {
  277. if (error) {
  278. return callback(error)
  279. }
  280. if (pastDueInvoices.length !== 1) {
  281. // no past due invoices, or more than one. Ignore.
  282. return callback()
  283. }
  284. RecurlyWrapper.attemptInvoiceCollection(
  285. pastDueInvoices[0].invoice_number,
  286. callback
  287. )
  288. }
  289. )
  290. })
  291. }
  292. function extendTrial(subscription, daysToExend, callback) {
  293. RecurlyWrapper.extendTrial(
  294. subscription.recurlySubscription_id,
  295. daysToExend,
  296. callback
  297. )
  298. }
  299. async function _updateSubscriptionFromRecurly(subscription) {
  300. const recurlySubscription = await RecurlyWrapper.promises.getSubscription(
  301. subscription.recurlySubscription_id,
  302. {}
  303. )
  304. await SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  305. recurlySubscription,
  306. subscription
  307. )
  308. }
  309. module.exports = {
  310. validateNoSubscriptionInRecurly,
  311. createSubscription,
  312. updateSubscription,
  313. cancelPendingSubscriptionChange,
  314. cancelSubscription: callbackify(cancelSubscription),
  315. reactivateSubscription: callbackify(reactivateSubscription),
  316. syncSubscription,
  317. attemptPaypalInvoiceCollection,
  318. extendTrial,
  319. promises: {
  320. validateNoSubscriptionInRecurly: promisify(validateNoSubscriptionInRecurly),
  321. createSubscription: promisify(createSubscription),
  322. updateSubscription: promisify(updateSubscription),
  323. cancelPendingSubscriptionChange: promisify(cancelPendingSubscriptionChange),
  324. cancelSubscription,
  325. reactivateSubscription,
  326. syncSubscription: promisify(syncSubscription),
  327. attemptPaypalInvoiceCollection: promisify(attemptPaypalInvoiceCollection),
  328. extendTrial: promisify(extendTrial),
  329. },
  330. }