PaymentProviderEntities.mjs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. // @ts-check
  2. /**
  3. * @import { PaymentProvider } from '../../../../types/subscription/dashboard/subscription'
  4. * @import { CurrencyCode, StripeCurrencyCode } from '../../../../types/subscription/currency'
  5. * @import { AddOn } from '../../../../types/subscription/plan'
  6. */
  7. /**
  8. * @typedef {object} ImmediateChargeLineItem
  9. * @property {string | null | undefined} planCode
  10. * @property {string} description
  11. * @property {number} subtotal
  12. * @property {number} discount
  13. * @property {number} tax
  14. * @property {boolean} [isAiAssist]
  15. */
  16. import OError from '@overleaf/o-error'
  17. import { DuplicateAddOnError, AddOnNotPresentError } from './Errors.mjs'
  18. import PlansLocator from './PlansLocator.mjs'
  19. import SubscriptionHelper from './SubscriptionHelper.mjs'
  20. import { AI_ADD_ON_CODE, isStandaloneAiAddOnPlanCode } from './AiHelper.mjs'
  21. export const MEMBERS_LIMIT_ADD_ON_CODE = 'additional-license'
  22. export class PaymentProviderSubscription {
  23. /**
  24. * @param {object} props
  25. * @param {string} props.id
  26. * @param {string} props.userId
  27. * @param {string} props.planCode
  28. * @param {string} props.planName
  29. * @param {number} props.planPrice
  30. * @param {PaymentProviderSubscriptionAddOn[]} [props.addOns]
  31. * @param {number} props.subtotal
  32. * @param {number} [props.taxRate]
  33. * @param {number} [props.taxAmount]
  34. * // Recurly uses uppercase currency codes, but Stripe uses lowercase
  35. * @param {CurrencyCode | StripeCurrencyCode} props.currency
  36. * @param {number} props.total
  37. * @param {Date} props.periodStart
  38. * @param {Date} props.periodEnd
  39. * @param {string} props.collectionMethod
  40. * @param {number} [props.netTerms]
  41. * @param {string} [props.poNumber]
  42. * @param {string} [props.termsAndConditions]
  43. * @param {PaymentProviderSubscriptionChange} [props.pendingChange]
  44. * @param {PaymentProvider['service']} [props.service]
  45. * @param {string} [props.state]
  46. * @param {Date|null} [props.trialPeriodStart]
  47. * @param {Date|null} [props.trialPeriodEnd]
  48. * @param {Date|null} [props.pausePeriodStart]
  49. * @param {Date|null} [props.pausePeriodEnd]
  50. * @param {number|null} [props.remainingPauseCycles]
  51. * @param {boolean} [props.isMigratedFromRecurly]
  52. */
  53. constructor(props) {
  54. this.id = props.id
  55. this.userId = props.userId
  56. this.planCode = props.planCode
  57. this.planName = props.planName
  58. this.planPrice = props.planPrice
  59. this.addOns = props.addOns ?? []
  60. this.subtotal = props.subtotal
  61. this.taxRate = props.taxRate ?? 0
  62. this.taxAmount = props.taxAmount ?? 0
  63. this.currency = props.currency.toUpperCase() // ensure that currency codes are always uppercase
  64. this.total = props.total
  65. this.periodStart = props.periodStart
  66. this.periodEnd = props.periodEnd
  67. this.collectionMethod = props.collectionMethod
  68. this.netTerms = props.netTerms ?? 0
  69. this.poNumber = props.poNumber ?? ''
  70. this.termsAndConditions = props.termsAndConditions ?? ''
  71. this.pendingChange = props.pendingChange ?? null
  72. this.service = props.service ?? 'recurly'
  73. this.state = props.state ?? 'active'
  74. this.trialPeriodStart = props.trialPeriodStart ?? null
  75. this.trialPeriodEnd = props.trialPeriodEnd ?? null
  76. this.pausePeriodStart = props.pausePeriodStart ?? null
  77. this.pausePeriodEnd = props.pausePeriodEnd ?? null
  78. this.remainingPauseCycles = props.remainingPauseCycles ?? null
  79. this.isMigratedFromRecurly = props.isMigratedFromRecurly ?? false
  80. }
  81. /**
  82. * Returns whether this subscription currently has the given add-on
  83. *
  84. * @param {string} code
  85. * @return {boolean}
  86. */
  87. hasAddOn(code) {
  88. return this.addOns.some(addOn => addOn.code === code)
  89. }
  90. /**
  91. * Returns whether this subscription is a standalone AI add-on subscription
  92. *
  93. * @return {boolean}
  94. */
  95. isStandaloneAiAddOn() {
  96. return isStandaloneAiAddOnPlanCode(this.planCode)
  97. }
  98. /**
  99. * Returns whether this subscription is a group subscription
  100. *
  101. * @return {boolean}
  102. */
  103. isGroupSubscription() {
  104. return PlansLocator.isGroupPlanCode(this.planCode)
  105. }
  106. /**
  107. * Returns whether this subcription will have the given add-on next billing
  108. * period.
  109. *
  110. * There are two cases: either the subscription already has the add-on and
  111. * won't change next period, or the subscription will change next period and
  112. * the change includes the add-on.
  113. *
  114. * @param {string} code
  115. * @return {boolean}
  116. */
  117. hasAddOnNextPeriod(code) {
  118. if (this.pendingChange != null) {
  119. return this.pendingChange.nextAddOns.some(addOn => addOn.code === code)
  120. } else {
  121. return this.hasAddOn(code)
  122. }
  123. }
  124. /**
  125. * Returns the add-ons that should be used as the base for a change request.
  126. *
  127. * For immediate changes (timeframe = 'now'), we use the current add-ons because
  128. * the user is still paying for them until term_end. Any pending term_end changes
  129. * will need to be re-applied after the immediate change by the payment provider client.
  130. *
  131. * For term_end changes, if there's already a pending change, we use its nextAddOns
  132. * as the base to merge scheduled changes together.
  133. *
  134. * @param {"now" | "term_end"} timeframe - when the change will be applied
  135. * @return {PaymentProviderSubscriptionAddOn[]}
  136. */
  137. #getBaseAddOnsForChangeRequest(timeframe) {
  138. if (timeframe === 'term_end' && this.pendingChange != null) {
  139. return this.pendingChange.nextAddOns
  140. }
  141. return this.addOns
  142. }
  143. /**
  144. * Change this subscription's plan
  145. * @param {string} planCode - the new plan code
  146. * @param {number} [quantity] - the quantity of the plan
  147. * @param {boolean} [shouldChangeAtTermEnd] - whether the change should be applied at the end of the term
  148. * @return {PaymentProviderSubscriptionChangeRequest}
  149. */
  150. getRequestForPlanChange(planCode, quantity, shouldChangeAtTermEnd) {
  151. const timeframe = shouldChangeAtTermEnd ? 'term_end' : 'now'
  152. const baseAddOns = this.#getBaseAddOnsForChangeRequest(timeframe)
  153. // Start with all existing add-ons, but filter out additional-license
  154. // since we'll handle it specially for group plans below
  155. const addOnUpdates = baseAddOns
  156. .filter(addOn => addOn.code !== MEMBERS_LIMIT_ADD_ON_CODE)
  157. .map(addOn => addOn.toAddOnUpdate())
  158. if (quantity !== 1) {
  159. // Only group plans in Stripe can have larger than 1 quantity
  160. // This is because in Stripe, the group plans are configured with per-seat pricing
  161. // and the quantity is the number of seats
  162. // Setting the members limit add-on quantity accordingly
  163. // so it is compatible with Recurly's group plan model (1 base plan + add-on for each member)
  164. addOnUpdates.push(
  165. new PaymentProviderSubscriptionAddOnUpdate({
  166. code: MEMBERS_LIMIT_ADD_ON_CODE,
  167. quantity,
  168. })
  169. )
  170. }
  171. // When upgrading from standalone AI plan, add the AI add-on since
  172. // the standalone plan doesn't have it as an add-on
  173. if (this.isStandaloneAiAddOn() && !isStandaloneAiAddOnPlanCode(planCode)) {
  174. addOnUpdates.push(
  175. new PaymentProviderSubscriptionAddOnUpdate({
  176. code: AI_ADD_ON_CODE,
  177. quantity: 1,
  178. })
  179. )
  180. }
  181. const changeRequest = new PaymentProviderSubscriptionChangeRequest({
  182. subscription: this,
  183. timeframe,
  184. planCode,
  185. addOnUpdates,
  186. })
  187. return changeRequest
  188. }
  189. /**
  190. * Purchase an add-on on this subscription
  191. *
  192. * @param {string} code
  193. * @param {number} [quantity]
  194. * @param {number} [unitPrice]
  195. * @return {PaymentProviderSubscriptionChangeRequest} - the change request to send to
  196. * Recurly
  197. *
  198. * @throws {DuplicateAddOnError} if the add-on is already present on the subscription
  199. */
  200. getRequestForAddOnPurchase(code, quantity = 1, unitPrice) {
  201. if (this.hasAddOn(code)) {
  202. throw new DuplicateAddOnError('Subscription already has add-on', {
  203. subscriptionId: this.id,
  204. addOnCode: code,
  205. })
  206. }
  207. const baseAddOns = this.#getBaseAddOnsForChangeRequest('now')
  208. const addOnUpdates = baseAddOns.map(addOn => addOn.toAddOnUpdate())
  209. addOnUpdates.push(
  210. new PaymentProviderSubscriptionAddOnUpdate({ code, quantity, unitPrice })
  211. )
  212. return new PaymentProviderSubscriptionChangeRequest({
  213. subscription: this,
  214. timeframe: 'now',
  215. addOnUpdates,
  216. })
  217. }
  218. /**
  219. * Update an add-on on this subscription
  220. *
  221. * @param {string} code
  222. * @param {number} quantity
  223. * @return {PaymentProviderSubscriptionChangeRequest} - the change request to send to
  224. * Recurly
  225. *
  226. * @throws {AddOnNotPresentError} if the subscription doesn't have the add-on
  227. */
  228. getRequestForAddOnUpdate(code, quantity) {
  229. if (!this.hasAddOn(code)) {
  230. throw new AddOnNotPresentError(
  231. 'Subscription does not have add-on to update',
  232. {
  233. subscriptionId: this.id,
  234. addOnCode: code,
  235. }
  236. )
  237. }
  238. const baseAddOns = this.#getBaseAddOnsForChangeRequest('now')
  239. const addOnUpdates = baseAddOns.map(addOn => {
  240. const update = addOn.toAddOnUpdate()
  241. if (update.code === code) {
  242. update.quantity = quantity
  243. }
  244. return update
  245. })
  246. return new PaymentProviderSubscriptionChangeRequest({
  247. subscription: this,
  248. timeframe: 'now',
  249. addOnUpdates,
  250. })
  251. }
  252. /**
  253. * Remove an add-on from this subscription
  254. *
  255. * @param {string} code
  256. * @return {PaymentProviderSubscriptionChangeRequest}
  257. *
  258. * @throws {AddOnNotPresentError} if the subscription doesn't have the add-on
  259. */
  260. getRequestForAddOnRemoval(code) {
  261. if (!this.hasAddOn(code)) {
  262. throw new AddOnNotPresentError(
  263. 'Subscription does not have add-on to remove',
  264. {
  265. subscriptionId: this.id,
  266. addOnCode: code,
  267. }
  268. )
  269. }
  270. const isInTrial = SubscriptionHelper.isInTrial(this.trialPeriodEnd)
  271. const timeframe = isInTrial ? 'now' : 'term_end'
  272. const baseAddOns = this.#getBaseAddOnsForChangeRequest(timeframe)
  273. const addOnUpdates = baseAddOns
  274. .filter(addOn => addOn.code !== code)
  275. .map(addOn => addOn.toAddOnUpdate())
  276. // preserve pending plan change when scheduling add-on removal at term_end
  277. const planCode =
  278. timeframe === 'term_end' ? this.pendingChange?.nextPlanCode : undefined
  279. return new PaymentProviderSubscriptionChangeRequest({
  280. subscription: this,
  281. timeframe,
  282. planCode,
  283. addOnUpdates,
  284. })
  285. }
  286. /**
  287. * Reactivate an add-on on this subscription
  288. *
  289. * @param {string} code - add-on code
  290. * @return {PaymentProviderSubscriptionChangeRequest}
  291. *
  292. * @throws {AddOnNotPresentError} if the add-on is not pending cancellation
  293. */
  294. getRequestForAddOnReactivation(code) {
  295. const reactivatedAddOn = this.addOns.find(addOn => addOn.code === code)
  296. const pendingChange = this.pendingChange
  297. if (reactivatedAddOn == null || pendingChange == null) {
  298. throw new AddOnNotPresentError('Add-on is not pending cancellation', {
  299. subscriptionId: this.id,
  300. addOnCode: code,
  301. })
  302. }
  303. const addOnUpdates = pendingChange.nextAddOns
  304. .filter(addOn => addOn.code !== code)
  305. .map(addOn => addOn.toAddOnUpdate())
  306. addOnUpdates.push(reactivatedAddOn.toAddOnUpdate())
  307. // preserve pending plan change when reactivating add-on
  308. const planCode = pendingChange.nextPlanCode
  309. return new PaymentProviderSubscriptionChangeRequest({
  310. subscription: this,
  311. timeframe: 'term_end',
  312. planCode,
  313. addOnUpdates,
  314. })
  315. }
  316. /**
  317. * Cancel only the pending plan change while preserving any pending add-on changes
  318. *
  319. * This is used when a user wants to "keep their current plan" but has also
  320. * scheduled add-on changes (additions or removals) that should still happen.
  321. *
  322. * @return {PaymentProviderSubscriptionChangeRequest | null}
  323. */
  324. getRequestForPlanChangeCancellation() {
  325. const pendingChange = this.pendingChange
  326. if (pendingChange == null) {
  327. return null
  328. }
  329. const currentAddOnCodes = this.addOns.map(a => a.code)
  330. const pendingAddOnCodes = pendingChange.nextAddOns.map(a => a.code)
  331. const hasAddOnChanges =
  332. currentAddOnCodes.length !== pendingAddOnCodes.length ||
  333. !currentAddOnCodes.every(code => pendingAddOnCodes.includes(code))
  334. if (!hasAddOnChanges) {
  335. return null
  336. }
  337. const addOnUpdates = pendingChange.nextAddOns
  338. .filter(addOn => {
  339. if (
  340. addOn.code === MEMBERS_LIMIT_ADD_ON_CODE &&
  341. !this.isGroupSubscription() &&
  342. PlansLocator.isGroupPlanCode(pendingChange.nextPlanCode)
  343. ) {
  344. // if there was a pending group plan upgrade, removing the plan change
  345. // means we also need to remove the members limit add-on
  346. return false
  347. }
  348. return true
  349. })
  350. .map(addOn => addOn.toAddOnUpdate())
  351. return new PaymentProviderSubscriptionChangeRequest({
  352. subscription: this,
  353. timeframe: 'term_end',
  354. planCode: this.planCode,
  355. addOnUpdates,
  356. })
  357. }
  358. /**
  359. * Form a request to revert the plan to it's last saved backup state
  360. *
  361. * @param {string} previousPlanCode
  362. * @param {Array<AddOn> | null} previousAddOns
  363. * @return {PaymentProviderSubscriptionChangeRequest}
  364. *
  365. * @throws {OError} if the restore point plan doesnt exist
  366. */
  367. getRequestForPlanRevert(previousPlanCode, previousAddOns) {
  368. const lastSuccessfulPlan =
  369. PlansLocator.findLocalPlanInSettings(previousPlanCode)
  370. if (lastSuccessfulPlan == null) {
  371. throw new OError('Unable to find plan in settings', { previousPlanCode })
  372. }
  373. const changeRequest = new PaymentProviderSubscriptionChangeRequest({
  374. subscription: this,
  375. timeframe: 'now',
  376. planCode: previousPlanCode,
  377. })
  378. // defaulting to empty array is important, as that will wipe away any add-ons that were added in the failed payment
  379. // but were not part of the last successful subscription
  380. const addOns = []
  381. for (const previousAddon of previousAddOns || []) {
  382. const addOnUpdate = new PaymentProviderSubscriptionAddOnUpdate({
  383. code: previousAddon.addOnCode,
  384. quantity: previousAddon.quantity,
  385. unitPrice: previousAddon.unitAmountInCents / 100,
  386. })
  387. addOns.push(addOnUpdate)
  388. }
  389. changeRequest.addOnUpdates = addOns
  390. return changeRequest
  391. }
  392. /**
  393. * Upgrade group plan with the plan code provided
  394. *
  395. * @param {string} newPlanCode
  396. * @return {PaymentProviderSubscriptionChangeRequest}
  397. */
  398. getRequestForGroupPlanUpgrade(newPlanCode) {
  399. const baseAddOns = this.#getBaseAddOnsForChangeRequest('now')
  400. const addOns = baseAddOns.map(
  401. addOn =>
  402. new PaymentProviderSubscriptionAddOnUpdate({
  403. code: addOn.code,
  404. quantity: addOn.quantity,
  405. })
  406. )
  407. return new PaymentProviderSubscriptionChangeRequest({
  408. subscription: this,
  409. timeframe: 'now',
  410. addOnUpdates: addOns,
  411. planCode: newPlanCode,
  412. })
  413. }
  414. /**
  415. * Update the "PO number" and "Terms and conditions" in a subscription
  416. *
  417. * @param {string} poNumber
  418. * @param {string} termsAndConditions
  419. * @return {PaymentProviderSubscriptionUpdateRequest} - the update request to send to
  420. * Recurly
  421. */
  422. getRequestForPoNumberAndTermsAndConditionsUpdate(
  423. poNumber,
  424. termsAndConditions
  425. ) {
  426. return new PaymentProviderSubscriptionUpdateRequest({
  427. subscription: this,
  428. poNumber,
  429. termsAndConditions,
  430. })
  431. }
  432. /**
  433. * Update the "Terms and conditions" in a subscription
  434. *
  435. * @param {string} termsAndConditions
  436. * @return {PaymentProviderSubscriptionUpdateRequest} - the update request to send to
  437. * Recurly
  438. */
  439. getRequestForTermsAndConditionsUpdate(termsAndConditions) {
  440. return new PaymentProviderSubscriptionUpdateRequest({
  441. subscription: this,
  442. termsAndConditions,
  443. })
  444. }
  445. /**
  446. * Returns whether this subscription is manually collected
  447. *
  448. * @return {boolean}
  449. */
  450. get isCollectionMethodManual() {
  451. return this.collectionMethod === 'manual'
  452. }
  453. /**
  454. * Determine if a plan change should be applied at the end of the term
  455. *
  456. * @param {string} newPlanCode
  457. * @returns {boolean}
  458. */
  459. shouldPlanChangeAtTermEnd(newPlanCode) {
  460. const currentPlan = PlansLocator.findLocalPlanInSettings(this.planCode)
  461. if (currentPlan == null) {
  462. throw new OError('Unable to find plan in settings', {
  463. planCode: this.planCode,
  464. })
  465. }
  466. const newPlan = PlansLocator.findLocalPlanInSettings(newPlanCode)
  467. if (newPlan == null) {
  468. throw new OError('Unable to find plan in settings', { newPlanCode })
  469. }
  470. const isInTrial = SubscriptionHelper.isInTrial(this.trialPeriodEnd)
  471. return SubscriptionHelper.shouldPlanChangeAtTermEnd(
  472. currentPlan,
  473. newPlan,
  474. isInTrial
  475. )
  476. }
  477. }
  478. /**
  479. * An add-on attached to a subscription
  480. */
  481. export class PaymentProviderSubscriptionAddOn {
  482. /**
  483. * @param {object} props
  484. * @param {string} props.code
  485. * @param {string} props.name
  486. * @param {number} props.quantity
  487. * @param {number} props.unitPrice
  488. */
  489. constructor(props) {
  490. this.code = props.code
  491. this.name = props.name
  492. this.quantity = props.quantity
  493. this.unitPrice = props.unitPrice
  494. this.preTaxTotal = this.quantity * this.unitPrice
  495. }
  496. /**
  497. * Return an add-on update that doesn't modify the add-on
  498. */
  499. toAddOnUpdate() {
  500. return new PaymentProviderSubscriptionAddOnUpdate({
  501. code: this.code,
  502. quantity: this.quantity,
  503. unitPrice: this.unitPrice,
  504. })
  505. }
  506. }
  507. export class PaymentProviderSubscriptionUpdateRequest {
  508. /**
  509. * @param {object} props
  510. * @param {PaymentProviderSubscription} props.subscription
  511. * @param {string} [props.poNumber]
  512. * @param {string} [props.termsAndConditions]
  513. */
  514. constructor(props) {
  515. this.subscription = props.subscription
  516. this.poNumber = props.poNumber ?? ''
  517. this.termsAndConditions = props.termsAndConditions ?? ''
  518. }
  519. }
  520. export class PaymentProviderSubscriptionChangeRequest {
  521. /**
  522. * @param {object} props
  523. * @param {PaymentProviderSubscription} props.subscription
  524. * @param {"now" | "term_end"} props.timeframe
  525. * @param {string} [props.planCode]
  526. * @param {PaymentProviderSubscriptionAddOnUpdate[]} [props.addOnUpdates]
  527. */
  528. constructor(props) {
  529. if (props.planCode == null && props.addOnUpdates == null) {
  530. throw new OError('Invalid PaymentProviderSubscriptionChangeRequest', {
  531. props,
  532. })
  533. }
  534. this.subscription = props.subscription
  535. this.timeframe = props.timeframe
  536. this.planCode = props.planCode ?? null
  537. this.addOnUpdates = props.addOnUpdates ?? null
  538. }
  539. }
  540. export class PaymentProviderSubscriptionAddOnUpdate {
  541. /**
  542. * @param {object} props
  543. * @param {string} props.code
  544. * @param {number} [props.quantity]
  545. * @param {number} [props.unitPrice]
  546. */
  547. constructor(props) {
  548. this.code = props.code
  549. this.quantity = props.quantity
  550. this.unitPrice = props.unitPrice ?? null
  551. }
  552. }
  553. export class PaymentProviderSubscriptionChange {
  554. /**
  555. * @param {object} props
  556. * @param {PaymentProviderSubscription} props.subscription
  557. * @param {string} props.nextPlanCode
  558. * @param {string} props.nextPlanName
  559. * @param {number} props.nextPlanPrice
  560. * @param {PaymentProviderSubscriptionAddOn[]} props.nextAddOns
  561. * @param {PaymentProviderImmediateCharge} [props.immediateCharge]
  562. */
  563. constructor(props) {
  564. this.subscription = props.subscription
  565. this.nextPlanCode = props.nextPlanCode
  566. this.nextPlanName = props.nextPlanName
  567. this.nextPlanPrice = props.nextPlanPrice
  568. this.nextAddOns = props.nextAddOns
  569. this.immediateCharge =
  570. props.immediateCharge ??
  571. new PaymentProviderImmediateCharge({
  572. subtotal: 0,
  573. tax: 0,
  574. total: 0,
  575. discount: 0,
  576. })
  577. this.subtotal = this.nextPlanPrice
  578. for (const addOn of this.nextAddOns) {
  579. this.subtotal += addOn.preTaxTotal
  580. }
  581. this.tax = Math.round(this.subtotal * 100 * this.subscription.taxRate) / 100
  582. this.total = this.subtotal + this.tax
  583. }
  584. getAddOn(addOnCode) {
  585. return this.nextAddOns.find(addOn => addOn.code === addOnCode)
  586. }
  587. }
  588. export class PaypalPaymentMethod {
  589. toString() {
  590. return 'Paypal'
  591. }
  592. }
  593. export class CreditCardPaymentMethod {
  594. /**
  595. * @param {object} props
  596. * @param {string} props.cardType
  597. * @param {string} props.lastFour
  598. */
  599. constructor(props) {
  600. this.cardType = props.cardType
  601. this.lastFour = props.lastFour
  602. }
  603. toString() {
  604. return `${this.cardType} **** ${this.lastFour}`
  605. }
  606. }
  607. export class PaymentProviderImmediateCharge {
  608. /**
  609. * @param {object} props
  610. * @param {number} props.subtotal
  611. * @param {number} props.tax
  612. * @param {number} props.total
  613. * @param {number} props.discount
  614. * @param {ImmediateChargeLineItem[]} [props.lineItems]
  615. */
  616. constructor(props) {
  617. this.subtotal = props.subtotal
  618. this.tax = props.tax
  619. this.total = props.total
  620. this.discount = props.discount
  621. this.lineItems = props.lineItems ?? []
  622. }
  623. }
  624. /**
  625. * An add-on configuration, independent of any subscription
  626. */
  627. export class PaymentProviderAddOn {
  628. /**
  629. * @param {object} props
  630. * @param {string} props.code
  631. * @param {string} props.name
  632. */
  633. constructor(props) {
  634. this.code = props.code
  635. this.name = props.name
  636. }
  637. }
  638. /**
  639. * A plan configuration
  640. */
  641. export class PaymentProviderPlan {
  642. /**
  643. * @param {object} props
  644. * @param {string} props.code
  645. * @param {string} props.name
  646. */
  647. constructor(props) {
  648. this.code = props.code
  649. this.name = props.name
  650. }
  651. }
  652. /**
  653. * A coupon in the payment provider
  654. */
  655. export class PaymentProviderCoupon {
  656. /**
  657. * @param {object} props
  658. * @param {string} props.code
  659. * @param {string} props.name
  660. * @param {string} [props.description]
  661. * @param {boolean} [props.isSingleUse]
  662. * @param {number | null} [props.discountMonths]
  663. */
  664. constructor(props) {
  665. this.code = props.code
  666. this.name = props.name
  667. this.description = props.description
  668. this.isSingleUse = props.isSingleUse
  669. this.discountMonths = props.discountMonths
  670. }
  671. }
  672. /**
  673. * An account in the payment provider
  674. */
  675. export class PaymentProviderAccount {
  676. /**
  677. * @param {object} props
  678. * @param {string} props.code
  679. * @param {string} props.email
  680. * @param {boolean} [props.hasPastDueInvoice]
  681. * @param {object} [props.metadata]
  682. * @param {string} [props.metadata.userId]
  683. * @param {string} [props.metadata.segment]
  684. */
  685. constructor(props) {
  686. this.code = props.code
  687. this.email = props.email
  688. this.hasPastDueInvoice = props.hasPastDueInvoice ?? false
  689. this.metadata = props.metadata ?? {}
  690. }
  691. }
  692. export default {
  693. MEMBERS_LIMIT_ADD_ON_CODE,
  694. PaymentProviderSubscription,
  695. PaymentProviderSubscriptionAddOn,
  696. PaymentProviderSubscriptionChange,
  697. PaymentProviderSubscriptionChangeRequest,
  698. PaymentProviderSubscriptionUpdateRequest,
  699. PaymentProviderSubscriptionAddOnUpdate,
  700. PaypalPaymentMethod,
  701. CreditCardPaymentMethod,
  702. PaymentProviderAddOn,
  703. PaymentProviderPlan,
  704. PaymentProviderCoupon,
  705. PaymentProviderAccount,
  706. PaymentProviderImmediateCharge,
  707. }