RecurlyEventHandler.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
  2. import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
  3. import SubscriptionEmailHandler from './SubscriptionEmailHandler.mjs'
  4. import { AI_ADD_ON_CODE } from './AiHelper.mjs'
  5. import mongodb from 'mongodb-legacy'
  6. const { ObjectId } = mongodb
  7. const INVOICE_SUBSCRIPTION_LIMIT = 10
  8. async function sendRecurlyAnalyticsEvent(event, eventData) {
  9. const userId = _getUserId(eventData)
  10. if (!ObjectId.isValid(userId)) {
  11. return
  12. }
  13. const customerIoEnabled =
  14. await SplitTestHandler.promises.hasUserBeenAssignedToVariant(
  15. {},
  16. userId,
  17. 'customer-io-trial-conversion',
  18. 'enabled',
  19. true
  20. )
  21. eventData['customerio-integration'] = customerIoEnabled || false
  22. switch (event) {
  23. case 'new_subscription_notification':
  24. await _sendSubscriptionStartedEvent(userId, eventData)
  25. break
  26. case 'updated_subscription_notification':
  27. await _sendSubscriptionUpdatedEvent(userId, eventData)
  28. break
  29. case 'canceled_subscription_notification':
  30. await _sendSubscriptionCancelledEvent(userId, eventData)
  31. break
  32. case 'expired_subscription_notification':
  33. await _sendSubscriptionExpiredEvent(userId, eventData)
  34. break
  35. case 'renewed_subscription_notification':
  36. await _sendSubscriptionRenewedEvent(userId, eventData)
  37. break
  38. case 'reactivated_account_notification':
  39. await _sendSubscriptionReactivatedEvent(userId, eventData)
  40. break
  41. case 'subscription_paused_notification':
  42. await _sendSubscriptionPausedEvent(userId, eventData)
  43. break
  44. case 'subscription_resumed_notification':
  45. // 'resumed' here means resumed from pause
  46. await _sendSubscriptionResumedEvent(userId, eventData)
  47. break
  48. case 'paid_charge_invoice_notification':
  49. if (
  50. eventData.invoice.state === 'paid' &&
  51. eventData.invoice.total_in_cents > 0
  52. ) {
  53. await _sendInvoicePaidEvent(userId, eventData)
  54. }
  55. break
  56. case 'closed_invoice_notification':
  57. if (
  58. eventData.invoice.state === 'collected' &&
  59. eventData.invoice.total_in_cents > 0
  60. ) {
  61. await _sendInvoicePaidEvent(userId, eventData)
  62. }
  63. break
  64. }
  65. }
  66. async function _sendSubscriptionResumedEvent(userId, eventData) {
  67. const { planCode, state, subscriptionId } = _getSubscriptionData(eventData)
  68. AnalyticsManager.recordEventForUserInBackground(
  69. userId,
  70. 'subscription-resumed',
  71. {
  72. plan_code: planCode,
  73. subscriptionId,
  74. payment_provider: 'recurly',
  75. 'customerio-integration': eventData['customerio-integration'],
  76. }
  77. )
  78. AnalyticsManager.setUserPropertyForUserInBackground(
  79. userId,
  80. 'subscription-state',
  81. state
  82. )
  83. }
  84. async function _sendSubscriptionPausedEvent(userId, eventData) {
  85. const { planCode, state, subscriptionId } = _getSubscriptionData(eventData)
  86. const pauseLength = eventData.subscription.remaining_pause_cycles
  87. AnalyticsManager.recordEventForUserInBackground(
  88. userId,
  89. 'subscription-paused',
  90. {
  91. pause_length: pauseLength,
  92. plan_code: planCode,
  93. subscriptionId,
  94. payment_provider: 'recurly',
  95. 'customerio-integration': eventData['customerio-integration'],
  96. }
  97. )
  98. AnalyticsManager.setUserPropertyForUserInBackground(
  99. userId,
  100. 'subscription-state',
  101. state
  102. )
  103. }
  104. async function _sendSubscriptionStartedEvent(userId, eventData) {
  105. const { planCode, quantity, state, isTrial, hasAiAddOn, subscriptionId } =
  106. _getSubscriptionData(eventData)
  107. AnalyticsManager.recordEventForUserInBackground(
  108. userId,
  109. 'subscription-started',
  110. {
  111. plan_code: planCode,
  112. quantity,
  113. is_trial: isTrial,
  114. has_ai_add_on: hasAiAddOn,
  115. subscriptionId,
  116. payment_provider: 'recurly',
  117. 'customerio-integration': eventData['customerio-integration'],
  118. }
  119. )
  120. AnalyticsManager.setUserPropertyForUserInBackground(
  121. userId,
  122. 'subscription-plan-code',
  123. planCode
  124. )
  125. AnalyticsManager.setUserPropertyForUserInBackground(
  126. userId,
  127. 'subscription-state',
  128. state
  129. )
  130. AnalyticsManager.setUserPropertyForUserInBackground(
  131. userId,
  132. 'subscription-is-trial',
  133. isTrial
  134. )
  135. if (isTrial) {
  136. await SubscriptionEmailHandler.sendTrialOnboardingEmail(userId, planCode)
  137. const cioAssignment = await SplitTestHandler.promises.getAssignmentForUser(
  138. userId,
  139. 'customer-io-trial-conversion'
  140. )
  141. if (cioAssignment.variant === 'enabled') {
  142. AnalyticsManager.setUserPropertyForUserInBackground(
  143. userId,
  144. 'customer-io-integration',
  145. true
  146. )
  147. }
  148. }
  149. }
  150. async function _sendSubscriptionUpdatedEvent(userId, eventData) {
  151. const { planCode, quantity, state, isTrial, hasAiAddOn, subscriptionId } =
  152. _getSubscriptionData(eventData)
  153. AnalyticsManager.recordEventForUserInBackground(
  154. userId,
  155. 'subscription-updated',
  156. {
  157. plan_code: planCode,
  158. quantity,
  159. is_trial: isTrial,
  160. has_ai_add_on: hasAiAddOn,
  161. subscriptionId,
  162. payment_provider: 'recurly',
  163. 'customerio-integration': eventData['customerio-integration'],
  164. }
  165. )
  166. AnalyticsManager.setUserPropertyForUserInBackground(
  167. userId,
  168. 'subscription-plan-code',
  169. planCode
  170. )
  171. AnalyticsManager.setUserPropertyForUserInBackground(
  172. userId,
  173. 'subscription-state',
  174. state
  175. )
  176. AnalyticsManager.setUserPropertyForUserInBackground(
  177. userId,
  178. 'subscription-is-trial',
  179. isTrial
  180. )
  181. }
  182. async function _sendSubscriptionCancelledEvent(userId, eventData) {
  183. const { planCode, quantity, state, isTrial, hasAiAddOn, subscriptionId } =
  184. _getSubscriptionData(eventData)
  185. AnalyticsManager.recordEventForUserInBackground(
  186. userId,
  187. 'subscription-cancelled',
  188. {
  189. plan_code: planCode,
  190. quantity,
  191. is_trial: isTrial,
  192. has_ai_add_on: hasAiAddOn,
  193. subscriptionId,
  194. payment_provider: 'recurly',
  195. 'customerio-integration': eventData['customerio-integration'],
  196. }
  197. )
  198. AnalyticsManager.setUserPropertyForUserInBackground(
  199. userId,
  200. 'subscription-state',
  201. state
  202. )
  203. AnalyticsManager.setUserPropertyForUserInBackground(
  204. userId,
  205. 'subscription-is-trial',
  206. isTrial
  207. )
  208. }
  209. async function _sendSubscriptionExpiredEvent(userId, eventData) {
  210. const { planCode, quantity, state, isTrial, hasAiAddOn, subscriptionId } =
  211. _getSubscriptionData(eventData)
  212. AnalyticsManager.recordEventForUserInBackground(
  213. userId,
  214. 'subscription-expired',
  215. {
  216. plan_code: planCode,
  217. quantity,
  218. is_trial: isTrial,
  219. has_ai_add_on: hasAiAddOn,
  220. subscriptionId,
  221. payment_provider: 'recurly',
  222. 'customerio-integration': eventData['customerio-integration'],
  223. }
  224. )
  225. AnalyticsManager.setUserPropertyForUserInBackground(
  226. userId,
  227. 'subscription-plan-code',
  228. planCode
  229. )
  230. AnalyticsManager.setUserPropertyForUserInBackground(
  231. userId,
  232. 'subscription-state',
  233. state
  234. )
  235. AnalyticsManager.setUserPropertyForUserInBackground(
  236. userId,
  237. 'subscription-is-trial',
  238. isTrial
  239. )
  240. }
  241. async function _sendSubscriptionRenewedEvent(userId, eventData) {
  242. const { planCode, quantity, state, isTrial, hasAiAddOn, subscriptionId } =
  243. _getSubscriptionData(eventData)
  244. AnalyticsManager.recordEventForUserInBackground(
  245. userId,
  246. 'subscription-renewed',
  247. {
  248. plan_code: planCode,
  249. quantity,
  250. is_trial: isTrial,
  251. has_ai_add_on: hasAiAddOn,
  252. subscriptionId,
  253. payment_provider: 'recurly',
  254. 'customerio-integration': eventData['customerio-integration'],
  255. }
  256. )
  257. AnalyticsManager.setUserPropertyForUserInBackground(
  258. userId,
  259. 'subscription-plan-code',
  260. planCode
  261. )
  262. AnalyticsManager.setUserPropertyForUserInBackground(
  263. userId,
  264. 'subscription-state',
  265. state
  266. )
  267. AnalyticsManager.setUserPropertyForUserInBackground(
  268. userId,
  269. 'subscription-is-trial',
  270. isTrial
  271. )
  272. }
  273. async function _sendSubscriptionReactivatedEvent(userId, eventData) {
  274. const { planCode, quantity, state, isTrial, hasAiAddOn, subscriptionId } =
  275. _getSubscriptionData(eventData)
  276. AnalyticsManager.recordEventForUserInBackground(
  277. userId,
  278. 'subscription-reactivated',
  279. {
  280. plan_code: planCode,
  281. quantity,
  282. has_ai_add_on: hasAiAddOn,
  283. subscriptionId,
  284. payment_provider: 'recurly',
  285. 'customerio-integration': eventData['customerio-integration'],
  286. }
  287. )
  288. AnalyticsManager.setUserPropertyForUserInBackground(
  289. userId,
  290. 'subscription-plan-code',
  291. planCode
  292. )
  293. AnalyticsManager.setUserPropertyForUserInBackground(
  294. userId,
  295. 'subscription-state',
  296. state
  297. )
  298. AnalyticsManager.setUserPropertyForUserInBackground(
  299. userId,
  300. 'subscription-is-trial',
  301. isTrial
  302. )
  303. }
  304. async function _sendInvoicePaidEvent(userId, eventData) {
  305. const invoice = eventData.invoice
  306. if (!invoice) {
  307. return
  308. }
  309. const invoiceNumber = invoice.invoice_number
  310. const currency = invoice.currency
  311. const totalInCents = invoice.total_in_cents
  312. const taxInCents = invoice.tax_in_cents
  313. const country = invoice.address?.country
  314. const collectionMethod = invoice.collection_method
  315. const subscriptionIds = {}
  316. // This logic is currently only resulting in `subscriptionId1` being set, because real data shows that
  317. // there is only one subscription_id per invoice for recurly.
  318. invoice.subscription_ids?.forEach((e, idx) => {
  319. if (idx < INVOICE_SUBSCRIPTION_LIMIT) {
  320. subscriptionIds[`subscriptionId${idx + 1}`] = e
  321. }
  322. })
  323. AnalyticsManager.recordEventForUserInBackground(
  324. userId,
  325. 'subscription-invoice-collected',
  326. {
  327. invoiceNumber,
  328. currency,
  329. totalInCents,
  330. taxInCents,
  331. country,
  332. collectionMethod,
  333. payment_provider: 'recurly',
  334. ...subscriptionIds,
  335. }
  336. )
  337. AnalyticsManager.setUserPropertyForUserInBackground(
  338. userId,
  339. 'subscription-is-trial',
  340. false
  341. )
  342. }
  343. function _getUserId(eventData) {
  344. let userId
  345. if (eventData && eventData.account && eventData.account.account_code) {
  346. userId = eventData.account.account_code
  347. } else {
  348. throw new Error(
  349. 'account.account_code missing in event data to identity user ID'
  350. )
  351. }
  352. return userId
  353. }
  354. function _getSubscriptionData(eventData) {
  355. const isTrial =
  356. eventData.subscription.trial_started_at &&
  357. eventData.subscription.current_period_started_at &&
  358. eventData.subscription.trial_started_at.getTime() ===
  359. eventData.subscription.current_period_started_at.getTime()
  360. const hasAiAddOn =
  361. eventData.subscription.subscription_add_ons?.some(
  362. addOn => addOn.add_on_code === AI_ADD_ON_CODE
  363. ) ?? false
  364. return {
  365. planCode: eventData.subscription.plan.plan_code,
  366. quantity: eventData.subscription.quantity,
  367. state: eventData.subscription.state,
  368. subscriptionId: eventData.subscription.uuid,
  369. isTrial,
  370. hasAiAddOn,
  371. }
  372. }
  373. export default {
  374. sendRecurlyAnalyticsEvent,
  375. }