PaymentProviderEntitiesTest.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. // @ts-check
  2. const SandboxedModule = require('sandboxed-module')
  3. const { expect } = require('chai')
  4. const Errors = require('../../../../app/src/Features/Subscription/Errors')
  5. const {
  6. PaymentProviderSubscriptionChangeRequest,
  7. PaymentProviderSubscriptionUpdateRequest,
  8. PaymentProviderSubscriptionChange,
  9. PaymentProviderSubscription,
  10. PaymentProviderSubscriptionAddOnUpdate,
  11. } = require('../../../../app/src/Features/Subscription/PaymentProviderEntities')
  12. const {
  13. AI_ADD_ON_CODE,
  14. } = require('../../../../app/src/Features/Subscription/AiHelper')
  15. const SubscriptionHelper = require('../../../../app/src/Features/Subscription/SubscriptionHelper')
  16. const MODULE_PATH =
  17. '../../../../app/src/Features/Subscription/PaymentProviderEntities'
  18. describe('PaymentProviderEntities', function () {
  19. describe('PaymentProviderSubscription', function () {
  20. beforeEach(function () {
  21. this.Settings = {
  22. plans: [
  23. { planCode: 'assistant-annual', price_in_cents: 5900 },
  24. { planCode: 'cheap-plan', price_in_cents: 500 },
  25. { planCode: 'regular-plan', price_in_cents: 1000 },
  26. { planCode: 'premium-plan', price_in_cents: 2000 },
  27. {
  28. planCode: 'group_collaborator_10_enterprise',
  29. price_in_cents: 10000,
  30. },
  31. ],
  32. features: [],
  33. }
  34. this.PaymentProviderEntities = SandboxedModule.require(MODULE_PATH, {
  35. requires: {
  36. '@overleaf/settings': this.Settings,
  37. './Errors': Errors,
  38. './SubscriptionHelper': SubscriptionHelper,
  39. },
  40. })
  41. })
  42. describe('with add-ons', function () {
  43. beforeEach(function () {
  44. const {
  45. PaymentProviderSubscription,
  46. PaymentProviderSubscriptionAddOn,
  47. } = this.PaymentProviderEntities
  48. this.addOn = new PaymentProviderSubscriptionAddOn({
  49. code: 'add-on-code',
  50. name: 'My Add-On',
  51. quantity: 1,
  52. unitPrice: 2,
  53. })
  54. this.subscription = new PaymentProviderSubscription({
  55. id: 'subscription-id',
  56. userId: 'user-id',
  57. planCode: 'regular-plan',
  58. planName: 'My Plan',
  59. planPrice: 10,
  60. addOns: [this.addOn],
  61. subtotal: 10.99,
  62. taxRate: 0.2,
  63. taxAmount: 2.4,
  64. total: 14.4,
  65. currency: 'USD',
  66. })
  67. })
  68. describe('hasAddOn()', function () {
  69. it('returns true if the subscription has the given add-on', function () {
  70. expect(this.subscription.hasAddOn(this.addOn.code)).to.be.true
  71. })
  72. it("returns false if the subscription doesn't have the given add-on", function () {
  73. expect(this.subscription.hasAddOn('another-add-on')).to.be.false
  74. })
  75. })
  76. describe('getRequestForPlanChange()', function () {
  77. it('returns a change request for upgrades', function () {
  78. const { PaymentProviderSubscriptionChangeRequest } =
  79. this.PaymentProviderEntities
  80. const changeRequest = this.subscription.getRequestForPlanChange(
  81. 'premium-plan',
  82. 1,
  83. this.subscription.shouldPlanChangeAtTermEnd('premium-plan')
  84. )
  85. expect(changeRequest).to.deep.equal(
  86. new PaymentProviderSubscriptionChangeRequest({
  87. subscription: this.subscription,
  88. timeframe: 'now',
  89. planCode: 'premium-plan',
  90. })
  91. )
  92. })
  93. it('returns a change request for downgrades', function () {
  94. const { PaymentProviderSubscriptionChangeRequest } =
  95. this.PaymentProviderEntities
  96. const changeRequest = this.subscription.getRequestForPlanChange(
  97. 'cheap-plan',
  98. 1,
  99. this.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
  100. )
  101. expect(changeRequest).to.deep.equal(
  102. new PaymentProviderSubscriptionChangeRequest({
  103. subscription: this.subscription,
  104. timeframe: 'term_end',
  105. planCode: 'cheap-plan',
  106. })
  107. )
  108. })
  109. it('returns a change request for downgrades while on trial', function () {
  110. const fiveDaysFromNow = new Date()
  111. fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5)
  112. this.subscription.trialPeriodEnd = fiveDaysFromNow
  113. const { PaymentProviderSubscriptionChangeRequest } =
  114. this.PaymentProviderEntities
  115. const changeRequest = this.subscription.getRequestForPlanChange(
  116. 'cheap-plan',
  117. 1,
  118. this.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
  119. )
  120. expect(changeRequest).to.deep.equal(
  121. new PaymentProviderSubscriptionChangeRequest({
  122. subscription: this.subscription,
  123. timeframe: 'now',
  124. planCode: 'cheap-plan',
  125. })
  126. )
  127. })
  128. it('preserves the AI add-on on upgrades', function () {
  129. const { PaymentProviderSubscriptionChangeRequest } =
  130. this.PaymentProviderEntities
  131. this.addOn.code = AI_ADD_ON_CODE
  132. const changeRequest = this.subscription.getRequestForPlanChange(
  133. 'premium-plan',
  134. 1,
  135. this.subscription.shouldPlanChangeAtTermEnd('premium-plan')
  136. )
  137. expect(changeRequest).to.deep.equal(
  138. new PaymentProviderSubscriptionChangeRequest({
  139. subscription: this.subscription,
  140. timeframe: 'now',
  141. planCode: 'premium-plan',
  142. addOnUpdates: [
  143. new PaymentProviderSubscriptionAddOnUpdate({
  144. code: AI_ADD_ON_CODE,
  145. quantity: 1,
  146. }),
  147. ],
  148. })
  149. )
  150. })
  151. it('preserves the AI add-on on downgrades', function () {
  152. const { PaymentProviderSubscriptionChangeRequest } =
  153. this.PaymentProviderEntities
  154. this.addOn.code = AI_ADD_ON_CODE
  155. const changeRequest = this.subscription.getRequestForPlanChange(
  156. 'cheap-plan',
  157. 1,
  158. this.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
  159. )
  160. expect(changeRequest).to.deep.equal(
  161. new PaymentProviderSubscriptionChangeRequest({
  162. subscription: this.subscription,
  163. timeframe: 'term_end',
  164. planCode: 'cheap-plan',
  165. addOnUpdates: [
  166. new PaymentProviderSubscriptionAddOnUpdate({
  167. code: AI_ADD_ON_CODE,
  168. quantity: 1,
  169. }),
  170. ],
  171. })
  172. )
  173. })
  174. it('preserves the AI add-on on upgrades from the standalone AI plan', function () {
  175. const { PaymentProviderSubscriptionChangeRequest } =
  176. this.PaymentProviderEntities
  177. this.subscription.planCode = 'assistant-annual'
  178. this.subscription.addOns = []
  179. const changeRequest = this.subscription.getRequestForPlanChange(
  180. 'cheap-plan',
  181. 1,
  182. this.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
  183. )
  184. expect(changeRequest).to.deep.equal(
  185. new PaymentProviderSubscriptionChangeRequest({
  186. subscription: this.subscription,
  187. timeframe: 'now',
  188. planCode: 'cheap-plan',
  189. addOnUpdates: [
  190. new PaymentProviderSubscriptionAddOnUpdate({
  191. code: AI_ADD_ON_CODE,
  192. quantity: 1,
  193. }),
  194. ],
  195. })
  196. )
  197. })
  198. it('upgrade from individual to group plan for Stripe subscription', function () {
  199. this.subscription.service = 'stripe-uk'
  200. const { PaymentProviderSubscriptionChangeRequest } =
  201. this.PaymentProviderEntities
  202. const changeRequest = this.subscription.getRequestForPlanChange(
  203. 'group_collaborator',
  204. 10,
  205. this.subscription.shouldPlanChangeAtTermEnd(
  206. 'group_collaborator_10_enterprise'
  207. )
  208. )
  209. expect(changeRequest).to.deep.equal(
  210. new PaymentProviderSubscriptionChangeRequest({
  211. subscription: this.subscription,
  212. timeframe: 'now',
  213. planCode: 'group_collaborator',
  214. addOnUpdates: [
  215. new PaymentProviderSubscriptionAddOnUpdate({
  216. code: 'additional-license',
  217. quantity: 10,
  218. }),
  219. ],
  220. })
  221. )
  222. })
  223. it('upgrade from individual to group plan and preserves the AI add-on for Stripe subscription', function () {
  224. this.subscription.service = 'stripe-uk'
  225. const { PaymentProviderSubscriptionChangeRequest } =
  226. this.PaymentProviderEntities
  227. this.addOn.code = AI_ADD_ON_CODE
  228. const changeRequest = this.subscription.getRequestForPlanChange(
  229. 'group_collaborator',
  230. 10,
  231. this.subscription.shouldPlanChangeAtTermEnd(
  232. 'group_collaborator_10_enterprise'
  233. )
  234. )
  235. expect(changeRequest).to.deep.equal(
  236. new PaymentProviderSubscriptionChangeRequest({
  237. subscription: this.subscription,
  238. timeframe: 'now',
  239. planCode: 'group_collaborator',
  240. addOnUpdates: [
  241. new PaymentProviderSubscriptionAddOnUpdate({
  242. code: 'additional-license',
  243. quantity: 10,
  244. }),
  245. new PaymentProviderSubscriptionAddOnUpdate({
  246. code: AI_ADD_ON_CODE,
  247. quantity: 1,
  248. }),
  249. ],
  250. })
  251. )
  252. })
  253. })
  254. describe('getRequestForAddOnPurchase()', function () {
  255. it('returns a change request', function () {
  256. const {
  257. PaymentProviderSubscriptionChangeRequest,
  258. PaymentProviderSubscriptionAddOnUpdate,
  259. } = this.PaymentProviderEntities
  260. const changeRequest =
  261. this.subscription.getRequestForAddOnPurchase('another-add-on')
  262. expect(changeRequest).to.deep.equal(
  263. new PaymentProviderSubscriptionChangeRequest({
  264. subscription: this.subscription,
  265. timeframe: 'now',
  266. addOnUpdates: [
  267. new PaymentProviderSubscriptionAddOnUpdate({
  268. code: this.addOn.code,
  269. quantity: this.addOn.quantity,
  270. unitPrice: this.addOn.unitPrice,
  271. }),
  272. new PaymentProviderSubscriptionAddOnUpdate({
  273. code: 'another-add-on',
  274. quantity: 1,
  275. }),
  276. ],
  277. })
  278. )
  279. })
  280. it('returns a change request with quantity and unit price specified', function () {
  281. const {
  282. PaymentProviderSubscriptionChangeRequest,
  283. PaymentProviderSubscriptionAddOnUpdate,
  284. } = this.PaymentProviderEntities
  285. const quantity = 5
  286. const unitPrice = 10
  287. const changeRequest = this.subscription.getRequestForAddOnPurchase(
  288. 'another-add-on',
  289. quantity,
  290. unitPrice
  291. )
  292. expect(changeRequest).to.deep.equal(
  293. new PaymentProviderSubscriptionChangeRequest({
  294. subscription: this.subscription,
  295. timeframe: 'now',
  296. addOnUpdates: [
  297. new PaymentProviderSubscriptionAddOnUpdate({
  298. code: this.addOn.code,
  299. quantity: this.addOn.quantity,
  300. unitPrice: this.addOn.unitPrice,
  301. }),
  302. new PaymentProviderSubscriptionAddOnUpdate({
  303. code: 'another-add-on',
  304. quantity,
  305. unitPrice,
  306. }),
  307. ],
  308. })
  309. )
  310. })
  311. it('throws a DuplicateAddOnError if the subscription already has the add-on', function () {
  312. expect(() =>
  313. this.subscription.getRequestForAddOnPurchase(this.addOn.code)
  314. ).to.throw(Errors.DuplicateAddOnError)
  315. })
  316. })
  317. describe('getRequestForAddOnUpdate()', function () {
  318. it('returns a change request', function () {
  319. const {
  320. PaymentProviderSubscriptionChangeRequest,
  321. PaymentProviderSubscriptionAddOnUpdate,
  322. } = this.PaymentProviderEntities
  323. const newQuantity = 2
  324. const changeRequest = this.subscription.getRequestForAddOnUpdate(
  325. 'add-on-code',
  326. newQuantity
  327. )
  328. expect(changeRequest).to.deep.equal(
  329. new PaymentProviderSubscriptionChangeRequest({
  330. subscription: this.subscription,
  331. timeframe: 'now',
  332. addOnUpdates: [
  333. new PaymentProviderSubscriptionAddOnUpdate({
  334. code: this.addOn.code,
  335. quantity: newQuantity,
  336. unitPrice: this.addOn.unitPrice,
  337. }),
  338. ],
  339. })
  340. )
  341. })
  342. it("throws a AddOnNotPresentError if the subscription doesn't have the add-on", function () {
  343. expect(() =>
  344. this.subscription.getRequestForAddOnUpdate('another-add-on', 2)
  345. ).to.throw(Errors.AddOnNotPresentError)
  346. })
  347. })
  348. describe('getRequestForAddOnRemoval()', function () {
  349. it('returns a change request', function () {
  350. const changeRequest = this.subscription.getRequestForAddOnRemoval(
  351. this.addOn.code
  352. )
  353. expect(changeRequest).to.deep.equal(
  354. new PaymentProviderSubscriptionChangeRequest({
  355. subscription: this.subscription,
  356. timeframe: 'term_end',
  357. addOnUpdates: [],
  358. })
  359. )
  360. })
  361. it('returns a change request when in trial', function () {
  362. const fiveDaysFromNow = new Date()
  363. fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5)
  364. this.subscription.trialPeriodEnd = fiveDaysFromNow
  365. const changeRequest = this.subscription.getRequestForAddOnRemoval(
  366. this.addOn.code
  367. )
  368. expect(changeRequest).to.deep.equal(
  369. new PaymentProviderSubscriptionChangeRequest({
  370. subscription: this.subscription,
  371. timeframe: 'now',
  372. addOnUpdates: [],
  373. })
  374. )
  375. })
  376. it("throws an AddOnNotPresentError if the subscription doesn't have the add-on", function () {
  377. expect(() =>
  378. this.subscription.getRequestForAddOnRemoval('another-add-on')
  379. ).to.throw(Errors.AddOnNotPresentError)
  380. })
  381. })
  382. describe('getRequestForGroupPlanUpgrade()', function () {
  383. it('returns a correct change request', function () {
  384. const changeRequest =
  385. this.subscription.getRequestForGroupPlanUpgrade('test_plan_code')
  386. const addOns = [
  387. new PaymentProviderSubscriptionAddOnUpdate({
  388. code: 'add-on-code',
  389. quantity: 1,
  390. }),
  391. ]
  392. expect(changeRequest).to.deep.equal(
  393. new PaymentProviderSubscriptionChangeRequest({
  394. subscription: this.subscription,
  395. timeframe: 'now',
  396. addOnUpdates: addOns,
  397. planCode: 'test_plan_code',
  398. })
  399. )
  400. })
  401. })
  402. describe('getRequestForPoNumberAndTermsAndConditionsUpdate()', function () {
  403. it('returns a correct update request', function () {
  404. const updateRequest =
  405. this.subscription.getRequestForPoNumberAndTermsAndConditionsUpdate(
  406. 'O12345',
  407. 'T&C copy'
  408. )
  409. expect(updateRequest).to.deep.equal(
  410. new PaymentProviderSubscriptionUpdateRequest({
  411. subscription: this.subscription,
  412. poNumber: 'O12345',
  413. termsAndConditions: 'T&C copy',
  414. })
  415. )
  416. })
  417. })
  418. describe('getRequestForTermsAndConditionsUpdate()', function () {
  419. it('returns a correct update request', function () {
  420. const updateRequest =
  421. this.subscription.getRequestForTermsAndConditionsUpdate('T&C copy')
  422. expect(updateRequest).to.deep.equal(
  423. new PaymentProviderSubscriptionUpdateRequest({
  424. subscription: this.subscription,
  425. termsAndConditions: 'T&C copy',
  426. })
  427. )
  428. })
  429. })
  430. describe('without add-ons', function () {
  431. beforeEach(function () {
  432. const { PaymentProviderSubscription } = this.PaymentProviderEntities
  433. this.subscription = new PaymentProviderSubscription({
  434. id: 'subscription-id',
  435. userId: 'user-id',
  436. planCode: 'regular-plan',
  437. planName: 'My Plan',
  438. planPrice: 10,
  439. subtotal: 10.99,
  440. taxRate: 0.2,
  441. taxAmount: 2.4,
  442. total: 14.4,
  443. currency: 'USD',
  444. })
  445. })
  446. describe('hasAddOn()', function () {
  447. it('returns false for any add-on', function () {
  448. expect(this.subscription.hasAddOn('some-add-on')).to.be.false
  449. })
  450. })
  451. describe('getRequestForAddOnPurchase()', function () {
  452. it('returns a change request', function () {
  453. const {
  454. PaymentProviderSubscriptionChangeRequest,
  455. PaymentProviderSubscriptionAddOnUpdate,
  456. } = this.PaymentProviderEntities
  457. const changeRequest =
  458. this.subscription.getRequestForAddOnPurchase('some-add-on')
  459. expect(changeRequest).to.deep.equal(
  460. new PaymentProviderSubscriptionChangeRequest({
  461. subscription: this.subscription,
  462. timeframe: 'now',
  463. addOnUpdates: [
  464. new PaymentProviderSubscriptionAddOnUpdate({
  465. code: 'some-add-on',
  466. quantity: 1,
  467. }),
  468. ],
  469. })
  470. )
  471. })
  472. })
  473. describe('getRequestForAddOnRemoval()', function () {
  474. it('throws an AddOnNotPresentError', function () {
  475. expect(() =>
  476. this.subscription.getRequestForAddOnRemoval('some-add-on')
  477. ).to.throw(Errors.AddOnNotPresentError)
  478. })
  479. })
  480. })
  481. })
  482. })
  483. describe('PaymentProviderSubscriptionChange', function () {
  484. describe('constructor', function () {
  485. it('rounds the amounts when calculating the taxes', function () {
  486. const subscription = new PaymentProviderSubscription({
  487. id: 'subscription-id',
  488. userId: 'user-id',
  489. planCode: 'premium-plan',
  490. planName: 'Premium plan',
  491. planPrice: 10,
  492. subtotal: 10,
  493. taxRate: 0.15,
  494. taxAmount: 1.5,
  495. currency: 'USD',
  496. total: 11.5,
  497. periodStart: new Date(),
  498. periodEnd: new Date(),
  499. collectionMethod: 'automatic',
  500. netTerms: 0,
  501. poNumber: '012345',
  502. termsAndConditions: 'T&C copy',
  503. })
  504. const change = new PaymentProviderSubscriptionChange({
  505. subscription,
  506. nextPlanCode: 'promotional-plan',
  507. nextPlanName: 'Promotial plan',
  508. nextPlanPrice: 8.99,
  509. nextAddOns: [],
  510. })
  511. expect(change.tax).to.equal(1.35)
  512. expect(change.total).to.equal(10.34)
  513. })
  514. })
  515. })
  516. })