RecurlyEventHandler.test.mjs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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. vi.doMock(
  62. '../../../../app/src/Features/Subscription/SubscriptionLocator',
  63. () => ({
  64. default: (ctx.SubscriptionLocator = {
  65. promises: {
  66. getUsersSubscription: sinon.stub().resolves(null),
  67. },
  68. }),
  69. })
  70. )
  71. ctx.RecurlyEventHandler = (await import(modulePath)).default
  72. })
  73. it('should not send events for subscriptions managed by stripe', async function (ctx) {
  74. ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({
  75. _id: 'sub123',
  76. paymentProvider: {
  77. service: 'stripe-uk',
  78. },
  79. })
  80. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  81. 'new_subscription_notification',
  82. ctx.eventData
  83. )
  84. sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
  85. sinon.assert.notCalled(
  86. ctx.AnalyticsManager.setUserPropertyForUserInBackground
  87. )
  88. sinon.assert.notCalled(
  89. ctx.SubscriptionEmailHandler.sendTrialOnboardingEmail
  90. )
  91. })
  92. it('should send events for subscriptions without stripe payment provider', async function (ctx) {
  93. ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({
  94. _id: 'sub123',
  95. paymentProvider: {
  96. service: 'recurly',
  97. },
  98. })
  99. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  100. 'new_subscription_notification',
  101. ctx.eventData
  102. )
  103. sinon.assert.called(ctx.AnalyticsManager.recordEventForUserInBackground)
  104. sinon.assert.called(ctx.AnalyticsManager.setUserPropertyForUserInBackground)
  105. })
  106. it('with new_subscription_notification - free trial', async function (ctx) {
  107. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  108. 'new_subscription_notification',
  109. ctx.eventData
  110. )
  111. sinon.assert.calledWith(
  112. ctx.AnalyticsManager.recordEventForUserInBackground,
  113. ctx.userId,
  114. 'subscription-started',
  115. {
  116. plan_code: ctx.planCode,
  117. quantity: 1,
  118. is_trial: true,
  119. has_ai_add_on: false,
  120. subscriptionId: ctx.eventData.subscription.uuid,
  121. payment_provider: 'recurly',
  122. 'customerio-integration': false,
  123. }
  124. )
  125. sinon.assert.calledWith(
  126. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  127. ctx.userId,
  128. 'subscription-plan-code',
  129. ctx.planCode
  130. )
  131. sinon.assert.calledWith(
  132. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  133. ctx.userId,
  134. 'subscription-state',
  135. 'active'
  136. )
  137. sinon.assert.calledWith(
  138. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  139. ctx.userId,
  140. 'subscription-is-trial',
  141. true
  142. )
  143. })
  144. it('with new_subscription_notification - free trial with customerio integration enabled', async function (ctx) {
  145. ctx.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
  146. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  147. 'new_subscription_notification',
  148. ctx.eventData
  149. )
  150. sinon.assert.calledWith(
  151. ctx.AnalyticsManager.recordEventForUserInBackground,
  152. ctx.userId,
  153. 'subscription-started',
  154. {
  155. plan_code: ctx.planCode,
  156. quantity: 1,
  157. is_trial: true,
  158. has_ai_add_on: false,
  159. subscriptionId: ctx.eventData.subscription.uuid,
  160. payment_provider: 'recurly',
  161. 'customerio-integration': true,
  162. }
  163. )
  164. sinon.assert.calledWith(
  165. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  166. ctx.userId,
  167. 'subscription-plan-code',
  168. ctx.planCode
  169. )
  170. sinon.assert.calledWith(
  171. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  172. ctx.userId,
  173. 'subscription-state',
  174. 'active'
  175. )
  176. sinon.assert.calledWith(
  177. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  178. ctx.userId,
  179. 'subscription-is-trial',
  180. true
  181. )
  182. })
  183. it('sends free trial onboarding email if user starting a trial', async function (ctx) {
  184. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  185. 'new_subscription_notification',
  186. ctx.eventData
  187. )
  188. sinon.assert.called(ctx.SubscriptionEmailHandler.sendTrialOnboardingEmail)
  189. })
  190. it('with new_subscription_notification - no free trial', async function (ctx) {
  191. ctx.eventData.subscription.current_period_started_at = new Date(
  192. '2021-02-10 12:34:56'
  193. )
  194. ctx.eventData.subscription.current_period_ends_at = new Date(
  195. '2021-02-17 12:34:56'
  196. )
  197. ctx.eventData.subscription.quantity = 3
  198. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  199. 'new_subscription_notification',
  200. ctx.eventData
  201. )
  202. sinon.assert.calledWith(
  203. ctx.AnalyticsManager.recordEventForUserInBackground,
  204. ctx.userId,
  205. 'subscription-started',
  206. {
  207. plan_code: ctx.planCode,
  208. quantity: 3,
  209. is_trial: false,
  210. has_ai_add_on: false,
  211. subscriptionId: ctx.eventData.subscription.uuid,
  212. payment_provider: 'recurly',
  213. 'customerio-integration': false,
  214. }
  215. )
  216. sinon.assert.calledWith(
  217. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  218. ctx.userId,
  219. 'subscription-state',
  220. 'active'
  221. )
  222. sinon.assert.calledWith(
  223. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  224. ctx.userId,
  225. 'subscription-is-trial',
  226. false
  227. )
  228. })
  229. it('with updated_subscription_notification', async function (ctx) {
  230. ctx.planCode = 'new-plan-code'
  231. ctx.eventData.subscription.plan.plan_code = ctx.planCode
  232. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  233. 'updated_subscription_notification',
  234. ctx.eventData
  235. )
  236. sinon.assert.calledWith(
  237. ctx.AnalyticsManager.recordEventForUserInBackground,
  238. ctx.userId,
  239. 'subscription-updated',
  240. {
  241. plan_code: ctx.planCode,
  242. quantity: 1,
  243. is_trial: true,
  244. has_ai_add_on: false,
  245. subscriptionId: ctx.eventData.subscription.uuid,
  246. payment_provider: 'recurly',
  247. 'customerio-integration': false,
  248. }
  249. )
  250. sinon.assert.calledWith(
  251. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  252. ctx.userId,
  253. 'subscription-plan-code',
  254. ctx.planCode
  255. )
  256. sinon.assert.calledWith(
  257. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  258. ctx.userId,
  259. 'subscription-state',
  260. 'active'
  261. )
  262. sinon.assert.calledWith(
  263. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  264. ctx.userId,
  265. 'subscription-is-trial',
  266. true
  267. )
  268. })
  269. it('with updated_subscription_notification with customerio integration enabled', async function (ctx) {
  270. ctx.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
  271. ctx.planCode = 'new-plan-code'
  272. ctx.eventData.subscription.plan.plan_code = ctx.planCode
  273. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  274. 'updated_subscription_notification',
  275. ctx.eventData
  276. )
  277. sinon.assert.calledWith(
  278. ctx.AnalyticsManager.recordEventForUserInBackground,
  279. ctx.userId,
  280. 'subscription-updated',
  281. {
  282. plan_code: ctx.planCode,
  283. quantity: 1,
  284. is_trial: true,
  285. has_ai_add_on: false,
  286. subscriptionId: ctx.eventData.subscription.uuid,
  287. payment_provider: 'recurly',
  288. 'customerio-integration': true,
  289. }
  290. )
  291. sinon.assert.calledWith(
  292. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  293. ctx.userId,
  294. 'subscription-plan-code',
  295. ctx.planCode
  296. )
  297. sinon.assert.calledWith(
  298. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  299. ctx.userId,
  300. 'subscription-state',
  301. 'active'
  302. )
  303. sinon.assert.calledWith(
  304. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  305. ctx.userId,
  306. 'subscription-is-trial',
  307. true
  308. )
  309. })
  310. it('with canceled_subscription_notification', async function (ctx) {
  311. ctx.eventData.subscription.state = 'cancelled'
  312. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  313. 'canceled_subscription_notification',
  314. ctx.eventData
  315. )
  316. sinon.assert.calledWith(
  317. ctx.AnalyticsManager.recordEventForUserInBackground,
  318. ctx.userId,
  319. 'subscription-cancelled',
  320. {
  321. plan_code: ctx.planCode,
  322. quantity: 1,
  323. is_trial: true,
  324. has_ai_add_on: false,
  325. subscriptionId: ctx.eventData.subscription.uuid,
  326. payment_provider: 'recurly',
  327. 'customerio-integration': false,
  328. }
  329. )
  330. sinon.assert.calledWith(
  331. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  332. ctx.userId,
  333. 'subscription-state',
  334. 'cancelled'
  335. )
  336. sinon.assert.calledWith(
  337. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  338. ctx.userId,
  339. 'subscription-is-trial',
  340. true
  341. )
  342. })
  343. it('with expired_subscription_notification', async function (ctx) {
  344. ctx.eventData.subscription.state = 'expired'
  345. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  346. 'expired_subscription_notification',
  347. ctx.eventData
  348. )
  349. sinon.assert.calledWith(
  350. ctx.AnalyticsManager.recordEventForUserInBackground,
  351. ctx.userId,
  352. 'subscription-expired',
  353. {
  354. plan_code: ctx.planCode,
  355. quantity: 1,
  356. is_trial: true,
  357. has_ai_add_on: false,
  358. subscriptionId: ctx.eventData.subscription.uuid,
  359. payment_provider: 'recurly',
  360. 'customerio-integration': false,
  361. }
  362. )
  363. sinon.assert.calledWith(
  364. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  365. ctx.userId,
  366. 'subscription-plan-code',
  367. ctx.planCode
  368. )
  369. sinon.assert.calledWith(
  370. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  371. ctx.userId,
  372. 'subscription-state',
  373. 'expired'
  374. )
  375. sinon.assert.calledWith(
  376. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  377. ctx.userId,
  378. 'subscription-is-trial',
  379. true
  380. )
  381. })
  382. it('with renewed_subscription_notification', async function (ctx) {
  383. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  384. 'renewed_subscription_notification',
  385. ctx.eventData
  386. )
  387. sinon.assert.calledWith(
  388. ctx.AnalyticsManager.recordEventForUserInBackground,
  389. ctx.userId,
  390. 'subscription-renewed',
  391. {
  392. plan_code: ctx.planCode,
  393. quantity: 1,
  394. is_trial: true,
  395. has_ai_add_on: false,
  396. subscriptionId: ctx.eventData.subscription.uuid,
  397. payment_provider: 'recurly',
  398. 'customerio-integration': false,
  399. }
  400. )
  401. })
  402. it('with reactivated_account_notification', async function (ctx) {
  403. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  404. 'reactivated_account_notification',
  405. ctx.eventData
  406. )
  407. sinon.assert.calledWith(
  408. ctx.AnalyticsManager.recordEventForUserInBackground,
  409. ctx.userId,
  410. 'subscription-reactivated',
  411. {
  412. plan_code: ctx.planCode,
  413. quantity: 1,
  414. has_ai_add_on: false,
  415. subscriptionId: ctx.eventData.subscription.uuid,
  416. payment_provider: 'recurly',
  417. 'customerio-integration': false,
  418. }
  419. )
  420. })
  421. it('with paid_charge_invoice_notification', async function (ctx) {
  422. const invoice = {
  423. invoice_number: 1234,
  424. currency: 'USD',
  425. state: 'paid',
  426. total_in_cents: 720,
  427. tax_in_cents: 12,
  428. address: {
  429. country: 'Liurnia',
  430. },
  431. collection_method: 'automatic',
  432. subscription_ids: ['abcd1234', 'defa3214'],
  433. }
  434. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  435. 'paid_charge_invoice_notification',
  436. {
  437. account: {
  438. account_code: ctx.userId,
  439. },
  440. invoice,
  441. }
  442. )
  443. sinon.assert.calledWith(
  444. ctx.AnalyticsManager.recordEventForUserInBackground,
  445. ctx.userId,
  446. 'subscription-invoice-collected',
  447. {
  448. invoiceNumber: invoice.invoice_number,
  449. currency: invoice.currency,
  450. totalInCents: invoice.total_in_cents,
  451. taxInCents: invoice.tax_in_cents,
  452. country: invoice.address.country,
  453. collectionMethod: invoice.collection_method,
  454. subscriptionId1: invoice.subscription_ids[0],
  455. subscriptionId2: invoice.subscription_ids[1],
  456. payment_provider: 'recurly',
  457. }
  458. )
  459. })
  460. it('with paid_charge_invoice_notification and total_in_cents 0', async function (ctx) {
  461. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  462. 'paid_charge_invoice_notification',
  463. {
  464. account: {
  465. account_code: ctx.userId,
  466. },
  467. invoice: {
  468. state: 'paid',
  469. total_in_cents: 0,
  470. },
  471. }
  472. )
  473. sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
  474. })
  475. it('with closed_invoice_notification', async function (ctx) {
  476. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  477. 'closed_invoice_notification',
  478. {
  479. account: {
  480. account_code: ctx.userId,
  481. },
  482. invoice: {
  483. state: 'collected',
  484. total_in_cents: 720,
  485. },
  486. }
  487. )
  488. sinon.assert.calledWith(
  489. ctx.AnalyticsManager.recordEventForUserInBackground,
  490. ctx.userId,
  491. 'subscription-invoice-collected'
  492. )
  493. })
  494. it('with closed_invoice_notification and total_in_cents 0', function (ctx) {
  495. ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  496. 'closed_invoice_notification',
  497. {
  498. account: {
  499. account_code: ctx.userId,
  500. },
  501. invoice: {
  502. state: 'collected',
  503. total_in_cents: 0,
  504. },
  505. }
  506. )
  507. sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
  508. })
  509. it('nothing is called with invalid account code', function (ctx) {
  510. ctx.eventData.account.account_code = 'foo_bar'
  511. ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  512. 'new_subscription_notification',
  513. ctx.eventData
  514. )
  515. sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
  516. sinon.assert.notCalled(
  517. ctx.AnalyticsManager.setUserPropertyForUserInBackground
  518. )
  519. sinon.assert.notCalled(
  520. ctx.AnalyticsManager.setUserPropertyForUserInBackground
  521. )
  522. sinon.assert.notCalled(
  523. ctx.AnalyticsManager.setUserPropertyForUserInBackground
  524. )
  525. })
  526. })