RecurlyEventHandler.test.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  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. }
  123. )
  124. sinon.assert.calledWith(
  125. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  126. ctx.userId,
  127. 'subscription-plan-code',
  128. ctx.planCode
  129. )
  130. sinon.assert.calledWith(
  131. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  132. ctx.userId,
  133. 'subscription-state',
  134. 'active'
  135. )
  136. sinon.assert.calledWith(
  137. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  138. ctx.userId,
  139. 'subscription-is-trial',
  140. true
  141. )
  142. })
  143. it('sends free trial onboarding email if user starting a trial', async function (ctx) {
  144. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  145. 'new_subscription_notification',
  146. ctx.eventData
  147. )
  148. sinon.assert.called(ctx.SubscriptionEmailHandler.sendTrialOnboardingEmail)
  149. })
  150. it('with new_subscription_notification - no free trial', async function (ctx) {
  151. ctx.eventData.subscription.current_period_started_at = new Date(
  152. '2021-02-10 12:34:56'
  153. )
  154. ctx.eventData.subscription.current_period_ends_at = new Date(
  155. '2021-02-17 12:34:56'
  156. )
  157. ctx.eventData.subscription.quantity = 3
  158. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  159. 'new_subscription_notification',
  160. ctx.eventData
  161. )
  162. sinon.assert.calledWith(
  163. ctx.AnalyticsManager.recordEventForUserInBackground,
  164. ctx.userId,
  165. 'subscription-started',
  166. {
  167. plan_code: ctx.planCode,
  168. quantity: 3,
  169. is_trial: false,
  170. has_ai_add_on: false,
  171. subscriptionId: ctx.eventData.subscription.uuid,
  172. payment_provider: 'recurly',
  173. }
  174. )
  175. sinon.assert.calledWith(
  176. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  177. ctx.userId,
  178. 'subscription-state',
  179. 'active'
  180. )
  181. sinon.assert.calledWith(
  182. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  183. ctx.userId,
  184. 'subscription-is-trial',
  185. false
  186. )
  187. })
  188. it('with updated_subscription_notification', async function (ctx) {
  189. ctx.planCode = 'new-plan-code'
  190. ctx.eventData.subscription.plan.plan_code = ctx.planCode
  191. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  192. 'updated_subscription_notification',
  193. ctx.eventData
  194. )
  195. sinon.assert.calledWith(
  196. ctx.AnalyticsManager.recordEventForUserInBackground,
  197. ctx.userId,
  198. 'subscription-updated',
  199. {
  200. plan_code: ctx.planCode,
  201. quantity: 1,
  202. is_trial: true,
  203. has_ai_add_on: false,
  204. subscriptionId: ctx.eventData.subscription.uuid,
  205. payment_provider: 'recurly',
  206. }
  207. )
  208. sinon.assert.calledWith(
  209. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  210. ctx.userId,
  211. 'subscription-plan-code',
  212. ctx.planCode
  213. )
  214. sinon.assert.calledWith(
  215. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  216. ctx.userId,
  217. 'subscription-state',
  218. 'active'
  219. )
  220. sinon.assert.calledWith(
  221. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  222. ctx.userId,
  223. 'subscription-is-trial',
  224. true
  225. )
  226. })
  227. it('with canceled_subscription_notification', async function (ctx) {
  228. ctx.eventData.subscription.state = 'cancelled'
  229. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  230. 'canceled_subscription_notification',
  231. ctx.eventData
  232. )
  233. sinon.assert.calledWith(
  234. ctx.AnalyticsManager.recordEventForUserInBackground,
  235. ctx.userId,
  236. 'subscription-cancelled',
  237. {
  238. plan_code: ctx.planCode,
  239. quantity: 1,
  240. is_trial: true,
  241. has_ai_add_on: false,
  242. subscriptionId: ctx.eventData.subscription.uuid,
  243. payment_provider: 'recurly',
  244. }
  245. )
  246. sinon.assert.calledWith(
  247. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  248. ctx.userId,
  249. 'subscription-state',
  250. 'cancelled'
  251. )
  252. sinon.assert.calledWith(
  253. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  254. ctx.userId,
  255. 'subscription-is-trial',
  256. true
  257. )
  258. })
  259. it('with expired_subscription_notification', async function (ctx) {
  260. ctx.eventData.subscription.state = 'expired'
  261. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  262. 'expired_subscription_notification',
  263. ctx.eventData
  264. )
  265. sinon.assert.calledWith(
  266. ctx.AnalyticsManager.recordEventForUserInBackground,
  267. ctx.userId,
  268. 'subscription-expired',
  269. {
  270. plan_code: ctx.planCode,
  271. quantity: 1,
  272. is_trial: true,
  273. has_ai_add_on: false,
  274. subscriptionId: ctx.eventData.subscription.uuid,
  275. payment_provider: 'recurly',
  276. }
  277. )
  278. sinon.assert.calledWith(
  279. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  280. ctx.userId,
  281. 'subscription-plan-code',
  282. ctx.planCode
  283. )
  284. sinon.assert.calledWith(
  285. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  286. ctx.userId,
  287. 'subscription-state',
  288. 'expired'
  289. )
  290. sinon.assert.calledWith(
  291. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  292. ctx.userId,
  293. 'subscription-is-trial',
  294. true
  295. )
  296. })
  297. it('with renewed_subscription_notification', async function (ctx) {
  298. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  299. 'renewed_subscription_notification',
  300. ctx.eventData
  301. )
  302. sinon.assert.calledWith(
  303. ctx.AnalyticsManager.recordEventForUserInBackground,
  304. ctx.userId,
  305. 'subscription-renewed',
  306. {
  307. plan_code: ctx.planCode,
  308. quantity: 1,
  309. is_trial: true,
  310. has_ai_add_on: false,
  311. subscriptionId: ctx.eventData.subscription.uuid,
  312. payment_provider: 'recurly',
  313. }
  314. )
  315. })
  316. it('with reactivated_account_notification', async function (ctx) {
  317. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  318. 'reactivated_account_notification',
  319. ctx.eventData
  320. )
  321. sinon.assert.calledWith(
  322. ctx.AnalyticsManager.recordEventForUserInBackground,
  323. ctx.userId,
  324. 'subscription-reactivated',
  325. {
  326. plan_code: ctx.planCode,
  327. quantity: 1,
  328. has_ai_add_on: false,
  329. subscriptionId: ctx.eventData.subscription.uuid,
  330. payment_provider: 'recurly',
  331. }
  332. )
  333. })
  334. it('with paid_charge_invoice_notification', async function (ctx) {
  335. const invoice = {
  336. invoice_number: 1234,
  337. currency: 'USD',
  338. state: 'paid',
  339. total_in_cents: 720,
  340. tax_in_cents: 12,
  341. address: {
  342. country: 'Liurnia',
  343. },
  344. collection_method: 'automatic',
  345. subscription_ids: ['abcd1234', 'defa3214'],
  346. }
  347. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  348. 'paid_charge_invoice_notification',
  349. {
  350. account: {
  351. account_code: ctx.userId,
  352. },
  353. invoice,
  354. }
  355. )
  356. sinon.assert.calledWith(
  357. ctx.AnalyticsManager.recordEventForUserInBackground,
  358. ctx.userId,
  359. 'subscription-invoice-collected',
  360. {
  361. invoiceNumber: invoice.invoice_number,
  362. currency: invoice.currency,
  363. totalInCents: invoice.total_in_cents,
  364. taxInCents: invoice.tax_in_cents,
  365. country: invoice.address.country,
  366. collectionMethod: invoice.collection_method,
  367. subscriptionId1: invoice.subscription_ids[0],
  368. subscriptionId2: invoice.subscription_ids[1],
  369. payment_provider: 'recurly',
  370. }
  371. )
  372. })
  373. it('with paid_charge_invoice_notification and total_in_cents 0', async function (ctx) {
  374. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  375. 'paid_charge_invoice_notification',
  376. {
  377. account: {
  378. account_code: ctx.userId,
  379. },
  380. invoice: {
  381. state: 'paid',
  382. total_in_cents: 0,
  383. },
  384. }
  385. )
  386. sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
  387. })
  388. it('with closed_invoice_notification', async function (ctx) {
  389. await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  390. 'closed_invoice_notification',
  391. {
  392. account: {
  393. account_code: ctx.userId,
  394. },
  395. invoice: {
  396. state: 'collected',
  397. total_in_cents: 720,
  398. },
  399. }
  400. )
  401. sinon.assert.calledWith(
  402. ctx.AnalyticsManager.recordEventForUserInBackground,
  403. ctx.userId,
  404. 'subscription-invoice-collected'
  405. )
  406. })
  407. it('with closed_invoice_notification and total_in_cents 0', function (ctx) {
  408. ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  409. 'closed_invoice_notification',
  410. {
  411. account: {
  412. account_code: ctx.userId,
  413. },
  414. invoice: {
  415. state: 'collected',
  416. total_in_cents: 0,
  417. },
  418. }
  419. )
  420. sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
  421. })
  422. it('nothing is called with invalid account code', function (ctx) {
  423. ctx.eventData.account.account_code = 'foo_bar'
  424. ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
  425. 'new_subscription_notification',
  426. ctx.eventData
  427. )
  428. sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
  429. sinon.assert.notCalled(
  430. ctx.AnalyticsManager.setUserPropertyForUserInBackground
  431. )
  432. sinon.assert.notCalled(
  433. ctx.AnalyticsManager.setUserPropertyForUserInBackground
  434. )
  435. sinon.assert.notCalled(
  436. ctx.AnalyticsManager.setUserPropertyForUserInBackground
  437. )
  438. })
  439. })