RecurlyEventHandler.test.mjs 14 KB

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