RecurlyEventHandlerTests.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. const SandboxedModule = require('sandboxed-module')
  2. const { ObjectId } = require('mongodb-legacy')
  3. const sinon = require('sinon')
  4. const modulePath =
  5. '../../../../app/src/Features/Subscription/RecurlyEventHandler'
  6. describe('RecurlyEventHandler', function () {
  7. beforeEach(function () {
  8. this.userId = '123abc234bcd456cde567def'
  9. this.planCode = 'collaborator-annual'
  10. this.eventData = {
  11. account: {
  12. account_code: this.userId,
  13. },
  14. subscription: {
  15. uuid: '8435ad98c1ce45da99b07f6a6a2e780f',
  16. plan: {
  17. plan_code: 'collaborator-annual',
  18. },
  19. quantity: 1,
  20. state: 'active',
  21. trial_started_at: new Date('2021-01-01 12:34:56'),
  22. trial_ends_at: new Date('2021-01-08 12:34:56'),
  23. current_period_started_at: new Date('2021-01-01 12:34:56'),
  24. current_period_ends_at: new Date('2021-01-08 12:34:56'),
  25. },
  26. }
  27. this.RecurlyEventHandler = SandboxedModule.require(modulePath, {
  28. requires: {
  29. 'mongodb-legacy': { ObjectId },
  30. './SubscriptionEmailHandler': (this.SubscriptionEmailHandler = {
  31. sendTrialOnboardingEmail: sinon.stub(),
  32. }),
  33. '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
  34. recordEventForUserInBackground: sinon.stub(),
  35. setUserPropertyForUserInBackground: sinon.stub(),
  36. }),
  37. '../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
  38. promises: {
  39. getAssignmentForUser: sinon.stub().resolves({
  40. variant: 'default',
  41. }),
  42. hasUserBeenAssignedToVariant: sinon.stub().resolves(false),
  43. },
  44. }),
  45. },
  46. })
  47. })
  48. it('with new_subscription_notification - free trial', async function () {
  49. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  50. 'new_subscription_notification',
  51. this.eventData
  52. )
  53. sinon.assert.calledWith(
  54. this.AnalyticsManager.recordEventForUserInBackground,
  55. this.userId,
  56. 'subscription-started',
  57. {
  58. plan_code: this.planCode,
  59. quantity: 1,
  60. is_trial: true,
  61. has_ai_add_on: false,
  62. subscriptionId: this.eventData.subscription.uuid,
  63. payment_provider: 'recurly',
  64. 'customerio-integration': false,
  65. }
  66. )
  67. sinon.assert.calledWith(
  68. this.AnalyticsManager.setUserPropertyForUserInBackground,
  69. this.userId,
  70. 'subscription-plan-code',
  71. this.planCode
  72. )
  73. sinon.assert.calledWith(
  74. this.AnalyticsManager.setUserPropertyForUserInBackground,
  75. this.userId,
  76. 'subscription-state',
  77. 'active'
  78. )
  79. sinon.assert.calledWith(
  80. this.AnalyticsManager.setUserPropertyForUserInBackground,
  81. this.userId,
  82. 'subscription-is-trial',
  83. true
  84. )
  85. })
  86. it('with new_subscription_notification - free trial with customerio integration enabled', async function () {
  87. this.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
  88. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  89. 'new_subscription_notification',
  90. this.eventData
  91. )
  92. sinon.assert.calledWith(
  93. this.AnalyticsManager.recordEventForUserInBackground,
  94. this.userId,
  95. 'subscription-started',
  96. {
  97. plan_code: this.planCode,
  98. quantity: 1,
  99. is_trial: true,
  100. has_ai_add_on: false,
  101. subscriptionId: this.eventData.subscription.uuid,
  102. payment_provider: 'recurly',
  103. 'customerio-integration': true,
  104. }
  105. )
  106. sinon.assert.calledWith(
  107. this.AnalyticsManager.setUserPropertyForUserInBackground,
  108. this.userId,
  109. 'subscription-plan-code',
  110. this.planCode
  111. )
  112. sinon.assert.calledWith(
  113. this.AnalyticsManager.setUserPropertyForUserInBackground,
  114. this.userId,
  115. 'subscription-state',
  116. 'active'
  117. )
  118. sinon.assert.calledWith(
  119. this.AnalyticsManager.setUserPropertyForUserInBackground,
  120. this.userId,
  121. 'subscription-is-trial',
  122. true
  123. )
  124. })
  125. it('sends free trial onboarding email if user starting a trial', async function () {
  126. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  127. 'new_subscription_notification',
  128. this.eventData
  129. )
  130. sinon.assert.called(this.SubscriptionEmailHandler.sendTrialOnboardingEmail)
  131. })
  132. it('with new_subscription_notification - no free trial', async function () {
  133. this.eventData.subscription.current_period_started_at = new Date(
  134. '2021-02-10 12:34:56'
  135. )
  136. this.eventData.subscription.current_period_ends_at = new Date(
  137. '2021-02-17 12:34:56'
  138. )
  139. this.eventData.subscription.quantity = 3
  140. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  141. 'new_subscription_notification',
  142. this.eventData
  143. )
  144. sinon.assert.calledWith(
  145. this.AnalyticsManager.recordEventForUserInBackground,
  146. this.userId,
  147. 'subscription-started',
  148. {
  149. plan_code: this.planCode,
  150. quantity: 3,
  151. is_trial: false,
  152. has_ai_add_on: false,
  153. subscriptionId: this.eventData.subscription.uuid,
  154. payment_provider: 'recurly',
  155. 'customerio-integration': false,
  156. }
  157. )
  158. sinon.assert.calledWith(
  159. this.AnalyticsManager.setUserPropertyForUserInBackground,
  160. this.userId,
  161. 'subscription-state',
  162. 'active'
  163. )
  164. sinon.assert.calledWith(
  165. this.AnalyticsManager.setUserPropertyForUserInBackground,
  166. this.userId,
  167. 'subscription-is-trial',
  168. false
  169. )
  170. })
  171. it('with updated_subscription_notification', async function () {
  172. this.planCode = 'new-plan-code'
  173. this.eventData.subscription.plan.plan_code = this.planCode
  174. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  175. 'updated_subscription_notification',
  176. this.eventData
  177. )
  178. sinon.assert.calledWith(
  179. this.AnalyticsManager.recordEventForUserInBackground,
  180. this.userId,
  181. 'subscription-updated',
  182. {
  183. plan_code: this.planCode,
  184. quantity: 1,
  185. is_trial: true,
  186. has_ai_add_on: false,
  187. subscriptionId: this.eventData.subscription.uuid,
  188. payment_provider: 'recurly',
  189. 'customerio-integration': false,
  190. }
  191. )
  192. sinon.assert.calledWith(
  193. this.AnalyticsManager.setUserPropertyForUserInBackground,
  194. this.userId,
  195. 'subscription-plan-code',
  196. this.planCode
  197. )
  198. sinon.assert.calledWith(
  199. this.AnalyticsManager.setUserPropertyForUserInBackground,
  200. this.userId,
  201. 'subscription-state',
  202. 'active'
  203. )
  204. sinon.assert.calledWith(
  205. this.AnalyticsManager.setUserPropertyForUserInBackground,
  206. this.userId,
  207. 'subscription-is-trial',
  208. true
  209. )
  210. })
  211. it('with updated_subscription_notification with customerio integration enabled', async function () {
  212. this.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
  213. this.planCode = 'new-plan-code'
  214. this.eventData.subscription.plan.plan_code = this.planCode
  215. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  216. 'updated_subscription_notification',
  217. this.eventData
  218. )
  219. sinon.assert.calledWith(
  220. this.AnalyticsManager.recordEventForUserInBackground,
  221. this.userId,
  222. 'subscription-updated',
  223. {
  224. plan_code: this.planCode,
  225. quantity: 1,
  226. is_trial: true,
  227. has_ai_add_on: false,
  228. subscriptionId: this.eventData.subscription.uuid,
  229. payment_provider: 'recurly',
  230. 'customerio-integration': true,
  231. }
  232. )
  233. sinon.assert.calledWith(
  234. this.AnalyticsManager.setUserPropertyForUserInBackground,
  235. this.userId,
  236. 'subscription-plan-code',
  237. this.planCode
  238. )
  239. sinon.assert.calledWith(
  240. this.AnalyticsManager.setUserPropertyForUserInBackground,
  241. this.userId,
  242. 'subscription-state',
  243. 'active'
  244. )
  245. sinon.assert.calledWith(
  246. this.AnalyticsManager.setUserPropertyForUserInBackground,
  247. this.userId,
  248. 'subscription-is-trial',
  249. true
  250. )
  251. })
  252. it('with canceled_subscription_notification', async function () {
  253. this.eventData.subscription.state = 'cancelled'
  254. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  255. 'canceled_subscription_notification',
  256. this.eventData
  257. )
  258. sinon.assert.calledWith(
  259. this.AnalyticsManager.recordEventForUserInBackground,
  260. this.userId,
  261. 'subscription-cancelled',
  262. {
  263. plan_code: this.planCode,
  264. quantity: 1,
  265. is_trial: true,
  266. has_ai_add_on: false,
  267. subscriptionId: this.eventData.subscription.uuid,
  268. payment_provider: 'recurly',
  269. 'customerio-integration': false,
  270. }
  271. )
  272. sinon.assert.calledWith(
  273. this.AnalyticsManager.setUserPropertyForUserInBackground,
  274. this.userId,
  275. 'subscription-state',
  276. 'cancelled'
  277. )
  278. sinon.assert.calledWith(
  279. this.AnalyticsManager.setUserPropertyForUserInBackground,
  280. this.userId,
  281. 'subscription-is-trial',
  282. true
  283. )
  284. })
  285. it('with expired_subscription_notification', async function () {
  286. this.eventData.subscription.state = 'expired'
  287. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  288. 'expired_subscription_notification',
  289. this.eventData
  290. )
  291. sinon.assert.calledWith(
  292. this.AnalyticsManager.recordEventForUserInBackground,
  293. this.userId,
  294. 'subscription-expired',
  295. {
  296. plan_code: this.planCode,
  297. quantity: 1,
  298. is_trial: true,
  299. has_ai_add_on: false,
  300. subscriptionId: this.eventData.subscription.uuid,
  301. payment_provider: 'recurly',
  302. 'customerio-integration': false,
  303. }
  304. )
  305. sinon.assert.calledWith(
  306. this.AnalyticsManager.setUserPropertyForUserInBackground,
  307. this.userId,
  308. 'subscription-plan-code',
  309. this.planCode
  310. )
  311. sinon.assert.calledWith(
  312. this.AnalyticsManager.setUserPropertyForUserInBackground,
  313. this.userId,
  314. 'subscription-state',
  315. 'expired'
  316. )
  317. sinon.assert.calledWith(
  318. this.AnalyticsManager.setUserPropertyForUserInBackground,
  319. this.userId,
  320. 'subscription-is-trial',
  321. true
  322. )
  323. })
  324. it('with renewed_subscription_notification', async function () {
  325. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  326. 'renewed_subscription_notification',
  327. this.eventData
  328. )
  329. sinon.assert.calledWith(
  330. this.AnalyticsManager.recordEventForUserInBackground,
  331. this.userId,
  332. 'subscription-renewed',
  333. {
  334. plan_code: this.planCode,
  335. quantity: 1,
  336. is_trial: true,
  337. has_ai_add_on: false,
  338. subscriptionId: this.eventData.subscription.uuid,
  339. payment_provider: 'recurly',
  340. 'customerio-integration': false,
  341. }
  342. )
  343. })
  344. it('with reactivated_account_notification', async function () {
  345. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  346. 'reactivated_account_notification',
  347. this.eventData
  348. )
  349. sinon.assert.calledWith(
  350. this.AnalyticsManager.recordEventForUserInBackground,
  351. this.userId,
  352. 'subscription-reactivated',
  353. {
  354. plan_code: this.planCode,
  355. quantity: 1,
  356. has_ai_add_on: false,
  357. subscriptionId: this.eventData.subscription.uuid,
  358. payment_provider: 'recurly',
  359. 'customerio-integration': false,
  360. }
  361. )
  362. })
  363. it('with paid_charge_invoice_notification', async function () {
  364. const invoice = {
  365. invoice_number: 1234,
  366. currency: 'USD',
  367. state: 'paid',
  368. total_in_cents: 720,
  369. tax_in_cents: 12,
  370. address: {
  371. country: 'Liurnia',
  372. },
  373. collection_method: 'automatic',
  374. subscription_ids: ['abcd1234', 'defa3214'],
  375. }
  376. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  377. 'paid_charge_invoice_notification',
  378. {
  379. account: {
  380. account_code: this.userId,
  381. },
  382. invoice,
  383. }
  384. )
  385. sinon.assert.calledWith(
  386. this.AnalyticsManager.recordEventForUserInBackground,
  387. this.userId,
  388. 'subscription-invoice-collected',
  389. {
  390. invoiceNumber: invoice.invoice_number,
  391. currency: invoice.currency,
  392. totalInCents: invoice.total_in_cents,
  393. taxInCents: invoice.tax_in_cents,
  394. country: invoice.address.country,
  395. collectionMethod: invoice.collection_method,
  396. subscriptionId1: invoice.subscription_ids[0],
  397. subscriptionId2: invoice.subscription_ids[1],
  398. payment_provider: 'recurly',
  399. }
  400. )
  401. })
  402. it('with paid_charge_invoice_notification and total_in_cents 0', async function () {
  403. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  404. 'paid_charge_invoice_notification',
  405. {
  406. account: {
  407. account_code: this.userId,
  408. },
  409. invoice: {
  410. state: 'paid',
  411. total_in_cents: 0,
  412. },
  413. }
  414. )
  415. sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground)
  416. })
  417. it('with closed_invoice_notification', async function () {
  418. await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  419. 'closed_invoice_notification',
  420. {
  421. account: {
  422. account_code: this.userId,
  423. },
  424. invoice: {
  425. state: 'collected',
  426. total_in_cents: 720,
  427. },
  428. }
  429. )
  430. sinon.assert.calledWith(
  431. this.AnalyticsManager.recordEventForUserInBackground,
  432. this.userId,
  433. 'subscription-invoice-collected'
  434. )
  435. })
  436. it('with closed_invoice_notification and total_in_cents 0', function () {
  437. this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  438. 'closed_invoice_notification',
  439. {
  440. account: {
  441. account_code: this.userId,
  442. },
  443. invoice: {
  444. state: 'collected',
  445. total_in_cents: 0,
  446. },
  447. }
  448. )
  449. sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground)
  450. })
  451. it('nothing is called with invalid account code', function () {
  452. this.eventData.account.account_code = 'foo_bar'
  453. this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  454. 'new_subscription_notification',
  455. this.eventData
  456. )
  457. sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground)
  458. sinon.assert.notCalled(
  459. this.AnalyticsManager.setUserPropertyForUserInBackground
  460. )
  461. sinon.assert.notCalled(
  462. this.AnalyticsManager.setUserPropertyForUserInBackground
  463. )
  464. sinon.assert.notCalled(
  465. this.AnalyticsManager.setUserPropertyForUserInBackground
  466. )
  467. })
  468. })