SubscriptionHelper.test.mjs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. import { vi, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. const modulePath =
  4. '../../../../app/src/Features/Subscription/SubscriptionHelper'
  5. const plans = {
  6. expensive: {
  7. planCode: 'expensive',
  8. price_in_cents: 1500,
  9. },
  10. cheaper: {
  11. planCode: 'cheaper',
  12. price_in_cents: 500,
  13. },
  14. alsoCheap: {
  15. plancode: 'also-cheap',
  16. price_in_cents: 500,
  17. },
  18. expensiveGroup: {
  19. plancode: 'group_expensive',
  20. price_in_cents: 49500,
  21. groupPlan: true,
  22. },
  23. cheapGroup: {
  24. plancode: 'group_cheap',
  25. price_in_cents: 1000,
  26. groupPlan: true,
  27. },
  28. bad: {},
  29. }
  30. describe('SubscriptionHelper', function () {
  31. beforeEach(async function (ctx) {
  32. ctx.clock = sinon.useFakeTimers(new Date('2023-06-15T10:00:00Z'))
  33. ctx.Subscription = {
  34. findOne: sinon.stub().returns({
  35. exec: sinon.stub().resolves(),
  36. }),
  37. updateOne: sinon.stub().returns({
  38. exec: sinon.stub().resolves(),
  39. }),
  40. find: sinon.stub().returns({
  41. populate: sinon.stub().returns({
  42. populate: sinon.stub().returns({
  43. exec: sinon.stub().resolves([]),
  44. }),
  45. }),
  46. exec: sinon.stub().resolves(),
  47. }),
  48. }
  49. ctx.INITIAL_LICENSE_SIZE = 2
  50. ctx.settings = {
  51. groupPlanModalOptions: {},
  52. }
  53. ctx.GroupPlansData = {
  54. enterprise: {
  55. collaborator: {
  56. CHF: {
  57. [ctx.INITIAL_LICENSE_SIZE]: {
  58. price_in_cents: 1000,
  59. },
  60. },
  61. DKK: {
  62. [ctx.INITIAL_LICENSE_SIZE]: {
  63. price_in_cents: 2000,
  64. },
  65. },
  66. SEK: {
  67. [ctx.INITIAL_LICENSE_SIZE]: {
  68. price_in_cents: 3000,
  69. },
  70. },
  71. NOK: {
  72. [ctx.INITIAL_LICENSE_SIZE]: {
  73. price_in_cents: 4000,
  74. },
  75. },
  76. USD: {
  77. [ctx.INITIAL_LICENSE_SIZE]: {
  78. price_in_cents: 5000,
  79. },
  80. },
  81. },
  82. professional: {
  83. CHF: {
  84. [ctx.INITIAL_LICENSE_SIZE]: {
  85. price_in_cents: 10000,
  86. },
  87. },
  88. DKK: {
  89. [ctx.INITIAL_LICENSE_SIZE]: {
  90. price_in_cents: 20000,
  91. },
  92. },
  93. SEK: {
  94. [ctx.INITIAL_LICENSE_SIZE]: {
  95. price_in_cents: 30000,
  96. },
  97. },
  98. NOK: {
  99. [ctx.INITIAL_LICENSE_SIZE]: {
  100. price_in_cents: 40000,
  101. },
  102. },
  103. USD: {
  104. [ctx.INITIAL_LICENSE_SIZE]: {
  105. price_in_cents: 50000,
  106. },
  107. },
  108. },
  109. },
  110. }
  111. vi.doMock('../../../../app/src/models/Subscription', () => ({
  112. Subscription: ctx.Subscription,
  113. }))
  114. vi.doMock('@overleaf/settings', () => ({
  115. default: ctx.settings,
  116. }))
  117. vi.doMock(
  118. '../../../../app/src/Features/Subscription/GroupPlansData',
  119. () => ({
  120. default: ctx.GroupPlansData,
  121. })
  122. )
  123. ctx.SubscriptionHelper = (await import(modulePath)).default
  124. })
  125. describe('shouldPlanChangeAtTermEnd', function () {
  126. it('should return false if isInTrial is true', function (ctx) {
  127. const isInTrial = true
  128. const changeAtTermEnd = ctx.SubscriptionHelper.shouldPlanChangeAtTermEnd(
  129. plans.expensive,
  130. plans.cheaper,
  131. isInTrial
  132. )
  133. expect(changeAtTermEnd).to.be.false
  134. })
  135. it('should return true if the new plan is less expensive', function (ctx) {
  136. const isInTrial = false
  137. const changeAtTermEnd = ctx.SubscriptionHelper.shouldPlanChangeAtTermEnd(
  138. plans.expensive,
  139. plans.cheaper,
  140. isInTrial
  141. )
  142. expect(changeAtTermEnd).to.be.true
  143. })
  144. it('should return false if the new plan is more exepensive', function (ctx) {
  145. const isInTrial = false
  146. const changeAtTermEnd = ctx.SubscriptionHelper.shouldPlanChangeAtTermEnd(
  147. plans.cheaper,
  148. plans.expensive,
  149. isInTrial
  150. )
  151. expect(changeAtTermEnd).to.be.false
  152. })
  153. it('should return false if the new plan is the same price', function (ctx) {
  154. const isInTrial = false
  155. const changeAtTermEnd = ctx.SubscriptionHelper.shouldPlanChangeAtTermEnd(
  156. plans.cheaper,
  157. plans.alsoCheap,
  158. isInTrial
  159. )
  160. expect(changeAtTermEnd).to.be.false
  161. })
  162. it('should return false if the change is from an individual plan to a more expensive group plan', function (ctx) {
  163. const isInTrial = false
  164. const changeAtTermEnd = ctx.SubscriptionHelper.shouldPlanChangeAtTermEnd(
  165. plans.expensive,
  166. plans.expensiveGroup,
  167. isInTrial
  168. )
  169. expect(changeAtTermEnd).to.be.false
  170. })
  171. it('should return true if the change is from an individual plan to a cheaper group plan', function (ctx) {
  172. const isInTrial = false
  173. const changeAtTermEnd = ctx.SubscriptionHelper.shouldPlanChangeAtTermEnd(
  174. plans.expensive,
  175. plans.cheapGroup,
  176. isInTrial
  177. )
  178. expect(changeAtTermEnd).to.be.true
  179. })
  180. })
  181. describe('generateInitialLocalizedGroupPrice', function () {
  182. describe('CHF currency', function () {
  183. it('should return the correct localized price for every plan', function (ctx) {
  184. const localizedPrice =
  185. ctx.SubscriptionHelper.generateInitialLocalizedGroupPrice('CHF', 'fr')
  186. expect(localizedPrice).to.deep.equal({
  187. price: {
  188. collaborator: '10 CHF',
  189. professional: '100 CHF',
  190. },
  191. pricePerUser: {
  192. collaborator: '5 CHF',
  193. professional: '50 CHF',
  194. },
  195. })
  196. })
  197. })
  198. describe('DKK currency', function () {
  199. it('should return the correct localized price for every plan', function (ctx) {
  200. const localizedPrice =
  201. ctx.SubscriptionHelper.generateInitialLocalizedGroupPrice('DKK', 'da')
  202. expect(localizedPrice).to.deep.equal({
  203. price: {
  204. collaborator: '20 kr.',
  205. professional: '200 kr.',
  206. },
  207. pricePerUser: {
  208. collaborator: '10 kr.',
  209. professional: '100 kr.',
  210. },
  211. })
  212. })
  213. })
  214. describe('SEK currency', function () {
  215. it('should return the correct localized price for every plan', function (ctx) {
  216. const localizedPrice =
  217. ctx.SubscriptionHelper.generateInitialLocalizedGroupPrice('SEK', 'sv')
  218. expect(localizedPrice).to.deep.equal({
  219. price: {
  220. collaborator: '30 kr',
  221. professional: '300 kr',
  222. },
  223. pricePerUser: {
  224. collaborator: '15 kr',
  225. professional: '150 kr',
  226. },
  227. })
  228. })
  229. })
  230. describe('NOK currency', function () {
  231. it('should return the correct localized price for every plan', function (ctx) {
  232. const localizedPrice =
  233. ctx.SubscriptionHelper.generateInitialLocalizedGroupPrice(
  234. 'NOK',
  235. // there seem to be possible inconsistencies with the CI
  236. // maybe it depends on what languages are installed on the server?
  237. 'en'
  238. )
  239. expect(localizedPrice).to.deep.equal({
  240. price: {
  241. collaborator: 'kr 40',
  242. professional: 'kr 400',
  243. },
  244. pricePerUser: {
  245. collaborator: 'kr 20',
  246. professional: 'kr 200',
  247. },
  248. })
  249. })
  250. })
  251. describe('other supported currencies', function () {
  252. it('should return the correct localized price for every plan', function (ctx) {
  253. const localizedPrice =
  254. ctx.SubscriptionHelper.generateInitialLocalizedGroupPrice('USD', 'en')
  255. expect(localizedPrice).to.deep.equal({
  256. price: {
  257. collaborator: '$50',
  258. professional: '$500',
  259. },
  260. pricePerUser: {
  261. collaborator: '$25',
  262. professional: '$250',
  263. },
  264. })
  265. })
  266. })
  267. })
  268. describe('isPaidSubscription', function () {
  269. it('should return true for a subscription with a recurly subscription id', function (ctx) {
  270. const result = ctx.SubscriptionHelper.isPaidSubscription({
  271. recurlySubscription_id: 'some-id',
  272. })
  273. expect(result).to.be.true
  274. })
  275. it('should return true for a subscription with a stripe subscription id', function (ctx) {
  276. const result = ctx.SubscriptionHelper.isPaidSubscription({
  277. paymentProvider: { subscriptionId: 'some-id' },
  278. })
  279. expect(result).to.be.true
  280. })
  281. it('should return false for a free subscription', function (ctx) {
  282. const result = ctx.SubscriptionHelper.isPaidSubscription({})
  283. expect(result).to.be.false
  284. })
  285. it('should return false for a missing subscription', function (ctx) {
  286. const result = ctx.SubscriptionHelper.isPaidSubscription()
  287. expect(result).to.be.false
  288. })
  289. })
  290. describe('isIndividualActivePaidSubscription', function () {
  291. it('should return true for an active recurly subscription', function (ctx) {
  292. const result = ctx.SubscriptionHelper.isIndividualActivePaidSubscription({
  293. groupPlan: false,
  294. recurlyStatus: { state: 'active' },
  295. recurlySubscription_id: 'some-id',
  296. })
  297. expect(result).to.be.true
  298. })
  299. it('should return true for an active stripe subscription', function (ctx) {
  300. const result = ctx.SubscriptionHelper.isIndividualActivePaidSubscription({
  301. groupPlan: false,
  302. paymentProvider: { subscriptionId: 'sub_123', state: 'active' },
  303. })
  304. expect(result).to.be.true
  305. })
  306. it('should return false for a canceled recurly subscription', function (ctx) {
  307. const result = ctx.SubscriptionHelper.isIndividualActivePaidSubscription({
  308. groupPlan: false,
  309. recurlyStatus: { state: 'canceled' },
  310. recurlySubscription_id: 'some-id',
  311. })
  312. expect(result).to.be.false
  313. })
  314. it('should return false for a canceled stripe subscription', function (ctx) {
  315. const result = ctx.SubscriptionHelper.isIndividualActivePaidSubscription({
  316. groupPlan: false,
  317. paymentProvider: { state: 'canceled', subscriptionId: 'sub_123' },
  318. })
  319. expect(result).to.be.false
  320. })
  321. it('should return false for a group plan subscription', function (ctx) {
  322. const result = ctx.SubscriptionHelper.isIndividualActivePaidSubscription({
  323. groupPlan: true,
  324. recurlyStatus: { state: 'active' },
  325. recurlySubscription_id: 'some-id',
  326. })
  327. expect(result).to.be.false
  328. })
  329. it('should return false for a free subscription', function (ctx) {
  330. const result = ctx.SubscriptionHelper.isIndividualActivePaidSubscription(
  331. {}
  332. )
  333. expect(result).to.be.false
  334. })
  335. it('should return false for a subscription with an empty string for recurlySubscription_id', function (ctx) {
  336. const result = ctx.SubscriptionHelper.isIndividualActivePaidSubscription({
  337. groupPlan: false,
  338. recurlySubscription_id: '',
  339. recurlyStatus: { state: 'active' },
  340. })
  341. expect(result).to.be.false
  342. })
  343. it('should return false for a subscription with an empty string for paymentProvider.subscriptionId', function (ctx) {
  344. const result = ctx.SubscriptionHelper.isIndividualActivePaidSubscription({
  345. groupPlan: false,
  346. paymentProvider: { state: 'active', subscriptionId: '' },
  347. })
  348. expect(result).to.be.false
  349. })
  350. it('should return false for a missing subscription', function (ctx) {
  351. const result = ctx.SubscriptionHelper.isPaidSubscription()
  352. expect(result).to.be.false
  353. })
  354. })
  355. describe('getPaymentProviderSubscriptionId', function () {
  356. it('should return the recurly subscription id if it exists', function (ctx) {
  357. const result = ctx.SubscriptionHelper.getPaymentProviderSubscriptionId({
  358. recurlySubscription_id: 'some-id',
  359. })
  360. expect(result).to.equal('some-id')
  361. })
  362. it('should return the payment provider subscription id if it exists', function (ctx) {
  363. const result = ctx.SubscriptionHelper.getPaymentProviderSubscriptionId({
  364. paymentProvider: { subscriptionId: 'sub_123' },
  365. })
  366. expect(result).to.equal('sub_123')
  367. })
  368. it('should return null if no subscription id exists', function (ctx) {
  369. const result = ctx.SubscriptionHelper.getPaymentProviderSubscriptionId({})
  370. expect(result).to.be.null
  371. })
  372. })
  373. describe('getPaidSubscriptionState', function () {
  374. it('should return the recurly state if it exists', function (ctx) {
  375. const result = ctx.SubscriptionHelper.getPaidSubscriptionState({
  376. recurlyStatus: { state: 'active' },
  377. })
  378. expect(result).to.equal('active')
  379. })
  380. it('should return the payment provider state if it exists', function (ctx) {
  381. const result = ctx.SubscriptionHelper.getPaidSubscriptionState({
  382. paymentProvider: { state: 'active' },
  383. })
  384. expect(result).to.equal('active')
  385. })
  386. it('should return null if no state exists', function (ctx) {
  387. const result = ctx.SubscriptionHelper.getPaidSubscriptionState({})
  388. expect(result).to.be.null
  389. })
  390. })
  391. describe('getSubscriptionTrialStartedAt', function () {
  392. it('should return the recurly trial start date if it exists', function (ctx) {
  393. const result = ctx.SubscriptionHelper.getSubscriptionTrialStartedAt({
  394. recurlySubscription_id: 'some-id',
  395. recurlyStatus: { trialStartedAt: new Date('2023-01-01') },
  396. })
  397. expect(result).to.deep.equal(new Date('2023-01-01'))
  398. })
  399. it('should return the payment provider trial start date if it exists', function (ctx) {
  400. const result = ctx.SubscriptionHelper.getSubscriptionTrialStartedAt({
  401. recurlyStatus: {},
  402. paymentProvider: { trialStartedAt: new Date('2023-01-01') },
  403. })
  404. expect(result).to.deep.equal(new Date('2023-01-01'))
  405. })
  406. it('should return undefined if no trial start date exists', function (ctx) {
  407. const result = ctx.SubscriptionHelper.getSubscriptionTrialStartedAt({})
  408. expect(result).to.be.undefined
  409. })
  410. })
  411. describe('getSubscriptionTrialEndsAt', function () {
  412. it('should return the recurly trial end date if it exists', function (ctx) {
  413. const result = ctx.SubscriptionHelper.getSubscriptionTrialEndsAt({
  414. recurlySubscription_id: 'some-id',
  415. recurlyStatus: { trialEndsAt: new Date('2023-01-01') },
  416. })
  417. expect(result).to.deep.equal(new Date('2023-01-01'))
  418. })
  419. it('should return the payment provider trial end date if it exists', function (ctx) {
  420. const result = ctx.SubscriptionHelper.getSubscriptionTrialEndsAt({
  421. recurlyStatus: {},
  422. paymentProvider: { trialEndsAt: new Date('2023-01-01') },
  423. })
  424. expect(result).to.deep.equal(new Date('2023-01-01'))
  425. })
  426. it('should return undefined if no trial end date exists', function (ctx) {
  427. const result = ctx.SubscriptionHelper.getSubscriptionTrialEndsAt({})
  428. expect(result).to.be.undefined
  429. })
  430. })
  431. describe('isInTrial', function () {
  432. it('should return false if trialEndsAt is null', function (ctx) {
  433. const result = ctx.SubscriptionHelper.isInTrial(null)
  434. expect(result).to.be.false
  435. })
  436. it('should return false if trialEndsAt is before now', function (ctx) {
  437. const tenDaysAgo = new Date()
  438. tenDaysAgo.setDate(tenDaysAgo.getDate() - 10)
  439. const result = ctx.SubscriptionHelper.isInTrial(tenDaysAgo)
  440. expect(result).to.be.false
  441. })
  442. it('should return true if trialEndsAt is after now', function (ctx) {
  443. const tenDaysFromNow = new Date()
  444. tenDaysFromNow.setDate(tenDaysFromNow.getDate() + 10)
  445. const result = ctx.SubscriptionHelper.isInTrial(tenDaysFromNow)
  446. expect(result).to.be.true
  447. })
  448. })
  449. describe('recomputeSubscriptionState', function () {
  450. beforeEach(function (ctx) {
  451. ctx.clock.now = new Date('2023-06-15T10:00:00Z')
  452. ctx.baseSubscription = {
  453. _id: 'subscription_id',
  454. paymentProvider: {
  455. service: 'stripe-test',
  456. subscriptionId: 'stripe_sub_123',
  457. state: 'active',
  458. pausePeriodStart: '2023-06-15T09:00:00Z',
  459. pausePeriodEnd: '2023-06-15T11:00:00Z',
  460. },
  461. }
  462. })
  463. describe('when subscription has no paymentProvider subscriptionId', function () {
  464. it('should return subscription unchanged', async function (ctx) {
  465. const subscription = { _id: 'subscription_id' }
  466. const result =
  467. await ctx.SubscriptionHelper.recomputeSubscriptionState(subscription)
  468. expect(result).to.equal(subscription)
  469. })
  470. })
  471. describe('when subscription has no pausePeriodStart', function () {
  472. it('should return subscription unchanged', async function (ctx) {
  473. const subscription = {
  474. _id: 'subscription_id',
  475. paymentProvider: {
  476. subscriptionId: 'stripe_sub_123',
  477. state: 'active',
  478. },
  479. }
  480. const result =
  481. await ctx.SubscriptionHelper.recomputeSubscriptionState(subscription)
  482. expect(result).to.equal(subscription)
  483. })
  484. })
  485. describe('when subscription should be paused', function () {
  486. describe('and current state is active', function () {
  487. it('should change state to paused', async function (ctx) {
  488. const subscription = { ...ctx.baseSubscription }
  489. const result =
  490. await ctx.SubscriptionHelper.recomputeSubscriptionState(
  491. subscription
  492. )
  493. expect(result.paymentProvider.state).to.equal('paused')
  494. expect(ctx.Subscription.updateOne).to.have.been.calledWith(
  495. { _id: 'subscription_id' },
  496. { 'paymentProvider.state': 'paused' }
  497. )
  498. })
  499. })
  500. describe('and current state is already paused', function () {
  501. it('should not change state', async function (ctx) {
  502. const subscription = {
  503. ...ctx.baseSubscription,
  504. paymentProvider: {
  505. ...ctx.baseSubscription.paymentProvider,
  506. state: 'paused',
  507. },
  508. }
  509. const result =
  510. await ctx.SubscriptionHelper.recomputeSubscriptionState(
  511. subscription
  512. )
  513. expect(result.paymentProvider.state).to.equal('paused')
  514. expect(ctx.Subscription.updateOne.called).to.be.false
  515. })
  516. })
  517. })
  518. describe('when subscription should not be paused', function () {
  519. describe('before pause period starts', function () {
  520. beforeEach(function (ctx) {
  521. ctx.clock.now = new Date('2023-06-15T08:00:00Z')
  522. })
  523. it('should keep active state unchanged', async function (ctx) {
  524. const subscription = { ...ctx.baseSubscription }
  525. const result =
  526. await ctx.SubscriptionHelper.recomputeSubscriptionState(
  527. subscription
  528. )
  529. expect(result.paymentProvider.state).to.equal('active')
  530. expect(ctx.Subscription.updateOne.called).to.be.false
  531. })
  532. })
  533. describe('after pause period ends', function () {
  534. beforeEach(function (ctx) {
  535. ctx.clock.now = new Date('2023-06-15T12:00:00Z')
  536. })
  537. describe('and current state is paused', function () {
  538. it('should change state to active', async function (ctx) {
  539. const subscription = {
  540. ...ctx.baseSubscription,
  541. paymentProvider: {
  542. ...ctx.baseSubscription.paymentProvider,
  543. state: 'paused',
  544. },
  545. }
  546. const result =
  547. await ctx.SubscriptionHelper.recomputeSubscriptionState(
  548. subscription
  549. )
  550. expect(result.paymentProvider.state).to.equal('active')
  551. expect(ctx.Subscription.updateOne).to.have.been.calledWith(
  552. { _id: 'subscription_id' },
  553. { 'paymentProvider.state': 'active' }
  554. )
  555. })
  556. })
  557. describe('and current state is already active', function () {
  558. it('should keep state unchanged', async function (ctx) {
  559. const subscription = { ...ctx.baseSubscription }
  560. const result =
  561. await ctx.SubscriptionHelper.recomputeSubscriptionState(
  562. subscription
  563. )
  564. expect(result.paymentProvider.state).to.equal('active')
  565. expect(ctx.Subscription.updateOne.called).to.be.false
  566. })
  567. })
  568. })
  569. })
  570. describe('when subscription has no pausePeriodEnd (indefinite pause)', function () {
  571. beforeEach(function (ctx) {
  572. ctx.baseSubscription.paymentProvider.pausePeriodEnd = undefined
  573. })
  574. it('should not transition to paused state when pausePeriodEnd is missing', async function (ctx) {
  575. const subscription = { ...ctx.baseSubscription }
  576. const result =
  577. await ctx.SubscriptionHelper.recomputeSubscriptionState(subscription)
  578. expect(result.paymentProvider.state).to.equal('active')
  579. expect(ctx.Subscription.updateOne.called).to.be.false
  580. })
  581. it('should keep paused state when already paused and no end date', async function (ctx) {
  582. const subscription = {
  583. ...ctx.baseSubscription,
  584. paymentProvider: {
  585. ...ctx.baseSubscription.paymentProvider,
  586. state: 'paused',
  587. pausePeriodEnd: undefined,
  588. },
  589. }
  590. const result =
  591. await ctx.SubscriptionHelper.recomputeSubscriptionState(subscription)
  592. expect(result.paymentProvider.state).to.equal('paused')
  593. expect(ctx.Subscription.updateOne.called).to.be.false
  594. })
  595. })
  596. })
  597. describe('getRecurlyCustomerAdminUrl', function () {
  598. beforeEach(function (ctx) {
  599. ctx.settings.siteUrl = 'https://www.overleaf.com'
  600. })
  601. it('should return production Recurly account URL', function (ctx) {
  602. const result =
  603. ctx.SubscriptionHelper.getRecurlyCustomerAdminUrl('user_789')
  604. expect(result).to.equal(
  605. 'https://sharelatex.recurly.com/accounts/user_789'
  606. )
  607. })
  608. it('should return sandbox Recurly account URL for dev environment', function (ctx) {
  609. ctx.settings.siteUrl = 'https://dev-overleaf.com'
  610. const result =
  611. ctx.SubscriptionHelper.getRecurlyCustomerAdminUrl('user_789')
  612. expect(result).to.equal(
  613. 'https://sharelatex-sandbox.recurly.com/accounts/user_789'
  614. )
  615. })
  616. it('should return sandbox Recurly account URL for staging environment', function (ctx) {
  617. ctx.settings.siteUrl = 'https://stag-overleaf.com'
  618. const result =
  619. ctx.SubscriptionHelper.getRecurlyCustomerAdminUrl('user_789')
  620. expect(result).to.equal(
  621. 'https://sharelatex-sandbox.recurly.com/accounts/user_789'
  622. )
  623. })
  624. it('should return null if customerId is null', function (ctx) {
  625. const result = ctx.SubscriptionHelper.getRecurlyCustomerAdminUrl(null)
  626. expect(result).to.be.null
  627. })
  628. it('should return null if customerId is undefined', function (ctx) {
  629. const result =
  630. ctx.SubscriptionHelper.getRecurlyCustomerAdminUrl(undefined)
  631. expect(result).to.be.null
  632. })
  633. it('should handle empty string customerId', function (ctx) {
  634. const result = ctx.SubscriptionHelper.getRecurlyCustomerAdminUrl('')
  635. expect(result).to.equal('https://sharelatex.recurly.com/accounts/')
  636. })
  637. })
  638. describe('getStripeCustomerAdminUrl', function () {
  639. beforeEach(function (ctx) {
  640. ctx.settings.siteUrl = 'https://www.overleaf.com'
  641. ctx.settings.apis = {
  642. stripeUS: { accountId: 'acct_us_123' },
  643. stripeUK: { accountId: 'acct_uk_456' },
  644. }
  645. })
  646. describe('stripe-us', function () {
  647. it('should return production Stripe US customer URL', function (ctx) {
  648. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  649. 'cus_us_789',
  650. 'stripe-us'
  651. )
  652. expect(result).to.equal(
  653. 'https://dashboard.stripe.com/acct_us_123/customers/cus_us_789'
  654. )
  655. })
  656. it('should return test Stripe US customer URL for dev environment', function (ctx) {
  657. ctx.settings.siteUrl = 'https://dev-overleaf.com'
  658. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  659. 'cus_us_789',
  660. 'stripe-us'
  661. )
  662. expect(result).to.equal(
  663. 'https://dashboard.stripe.com/acct_us_123/test/customers/cus_us_789'
  664. )
  665. })
  666. it('should return test Stripe US customer URL for staging environment', function (ctx) {
  667. ctx.settings.siteUrl = 'https://stag-overleaf.com'
  668. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  669. 'cus_us_789',
  670. 'stripe-us'
  671. )
  672. expect(result).to.equal(
  673. 'https://dashboard.stripe.com/acct_us_123/test/customers/cus_us_789'
  674. )
  675. })
  676. })
  677. describe('stripe-uk', function () {
  678. it('should return production Stripe UK customer URL', function (ctx) {
  679. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  680. 'cus_uk_123',
  681. 'stripe-uk'
  682. )
  683. expect(result).to.equal(
  684. 'https://dashboard.stripe.com/acct_uk_456/customers/cus_uk_123'
  685. )
  686. })
  687. it('should return test Stripe UK customer URL for dev environment', function (ctx) {
  688. ctx.settings.siteUrl = 'https://dev-overleaf.com'
  689. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  690. 'cus_uk_123',
  691. 'stripe-uk'
  692. )
  693. expect(result).to.equal(
  694. 'https://dashboard.stripe.com/acct_uk_456/test/customers/cus_uk_123'
  695. )
  696. })
  697. })
  698. it('should return null if accountId is missing', function (ctx) {
  699. ctx.settings.apis.stripeUS = {}
  700. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  701. 'cus_us_789',
  702. 'stripe-us'
  703. )
  704. expect(result).to.be.null
  705. })
  706. it('should return null if customerId is null', function (ctx) {
  707. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  708. null,
  709. 'stripe-us'
  710. )
  711. expect(result).to.be.null
  712. })
  713. it('should return null if service is null', function (ctx) {
  714. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  715. 'cus_us_789',
  716. null
  717. )
  718. expect(result).to.be.null
  719. })
  720. it('should return null if customerId is undefined', function (ctx) {
  721. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  722. undefined,
  723. 'stripe-us'
  724. )
  725. expect(result).to.be.null
  726. })
  727. it('should return null if service is undefined', function (ctx) {
  728. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  729. 'cus_us_789',
  730. undefined
  731. )
  732. expect(result).to.be.null
  733. })
  734. it('should return null if both customerId and service are null', function (ctx) {
  735. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  736. null,
  737. null
  738. )
  739. expect(result).to.be.null
  740. })
  741. it('should return null if accountId is missing for UK', function (ctx) {
  742. ctx.settings.apis.stripeUK = {}
  743. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  744. 'cus_uk_789',
  745. 'stripe-uk'
  746. )
  747. expect(result).to.be.null
  748. })
  749. it('should return null if apis object is missing', function (ctx) {
  750. ctx.settings.apis = {}
  751. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  752. 'cus_us_789',
  753. 'stripe-us'
  754. )
  755. expect(result).to.be.null
  756. })
  757. it('should handle empty string customerId', function (ctx) {
  758. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  759. '',
  760. 'stripe-us'
  761. )
  762. expect(result).to.equal(
  763. 'https://dashboard.stripe.com/acct_us_123/customers/'
  764. )
  765. })
  766. it('should return null if service is not stripe-us or stripe-uk', function (ctx) {
  767. const result = ctx.SubscriptionHelper.getStripeCustomerAdminUrl(
  768. 'cus_us_789',
  769. 'some-other-service'
  770. )
  771. expect(result).to.be.null
  772. })
  773. })
  774. })