SubscriptionHandler.js 10 KB

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