PaymentProviderEntitiesTest.js 16 KB

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