RecurlyEntities.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. // @ts-check
  2. const OError = require('@overleaf/o-error')
  3. const { DuplicateAddOnError, AddOnNotPresentError } = require('./Errors')
  4. const PlansLocator = require('./PlansLocator')
  5. const SubscriptionHelper = require('./SubscriptionHelper')
  6. const AI_ADD_ON_CODE = 'assistant'
  7. class RecurlySubscription {
  8. /**
  9. * @param {object} props
  10. * @param {string} props.id
  11. * @param {string} props.userId
  12. * @param {string} props.planCode
  13. * @param {string} props.planName
  14. * @param {number} props.planPrice
  15. * @param {RecurlySubscriptionAddOn[]} [props.addOns]
  16. * @param {number} props.subtotal
  17. * @param {number} [props.taxRate]
  18. * @param {number} [props.taxAmount]
  19. * @param {string} props.currency
  20. * @param {number} props.total
  21. * @param {Date} props.periodStart
  22. * @param {Date} props.periodEnd
  23. */
  24. constructor(props) {
  25. this.id = props.id
  26. this.userId = props.userId
  27. this.planCode = props.planCode
  28. this.planName = props.planName
  29. this.planPrice = props.planPrice
  30. this.addOns = props.addOns ?? []
  31. this.subtotal = props.subtotal
  32. this.taxRate = props.taxRate ?? 0
  33. this.taxAmount = props.taxAmount ?? 0
  34. this.currency = props.currency
  35. this.total = props.total
  36. this.periodStart = props.periodStart
  37. this.periodEnd = props.periodEnd
  38. }
  39. hasAddOn(code) {
  40. return this.addOns.some(addOn => addOn.code === code)
  41. }
  42. /**
  43. * Change this subscription's plan
  44. *
  45. * @return {RecurlySubscriptionChangeRequest}
  46. */
  47. getRequestForPlanChange(planCode) {
  48. const currentPlan = PlansLocator.findLocalPlanInSettings(this.planCode)
  49. if (currentPlan == null) {
  50. throw new OError('Unable to find plan in settings', {
  51. planCode: this.planCode,
  52. })
  53. }
  54. const newPlan = PlansLocator.findLocalPlanInSettings(planCode)
  55. if (newPlan == null) {
  56. throw new OError('Unable to find plan in settings', { planCode })
  57. }
  58. const changeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd(
  59. currentPlan,
  60. newPlan
  61. )
  62. const timeframe = changeAtTermEnd ? 'term_end' : 'now'
  63. return new RecurlySubscriptionChangeRequest({
  64. subscription: this,
  65. timeframe,
  66. planCode,
  67. })
  68. }
  69. /**
  70. * Purchase an add-on on this subscription
  71. *
  72. * @param {string} code
  73. * @param {number} [quantity]
  74. * @return {RecurlySubscriptionChangeRequest} - the change request to send to
  75. * Recurly
  76. *
  77. * @throws {DuplicateAddOnError} if the add-on is already present on the subscription
  78. */
  79. getRequestForAddOnPurchase(code, quantity = 1) {
  80. if (this.hasAddOn(code)) {
  81. throw new DuplicateAddOnError('Subscription already has add-on', {
  82. subscriptionId: this.id,
  83. addOnCode: code,
  84. })
  85. }
  86. const addOnUpdates = this.addOns.map(addOn => addOn.toAddOnUpdate())
  87. addOnUpdates.push(new RecurlySubscriptionAddOnUpdate({ code, quantity }))
  88. return new RecurlySubscriptionChangeRequest({
  89. subscription: this,
  90. timeframe: 'now',
  91. addOnUpdates,
  92. })
  93. }
  94. /**
  95. * Remove an add-on from this subscription
  96. *
  97. * @param {string} code
  98. * @return {RecurlySubscriptionChangeRequest}
  99. *
  100. * @throws {AddOnNotPresentError} if the subscription doesn't have the add-on
  101. */
  102. getRequestForAddOnRemoval(code) {
  103. if (!this.hasAddOn(code)) {
  104. throw new AddOnNotPresentError(
  105. 'Subscripiton does not have add-on to remove',
  106. {
  107. subscriptionId: this.id,
  108. addOnCode: code,
  109. }
  110. )
  111. }
  112. const addOnUpdates = this.addOns
  113. .filter(addOn => addOn.code !== code)
  114. .map(addOn => addOn.toAddOnUpdate())
  115. return new RecurlySubscriptionChangeRequest({
  116. subscription: this,
  117. timeframe: 'term_end',
  118. addOnUpdates,
  119. })
  120. }
  121. }
  122. /**
  123. * An add-on attached to a subscription
  124. */
  125. class RecurlySubscriptionAddOn {
  126. /**
  127. * @param {object} props
  128. * @param {string} props.code
  129. * @param {string} props.name
  130. * @param {number} props.quantity
  131. * @param {number} props.unitPrice
  132. */
  133. constructor(props) {
  134. this.code = props.code
  135. this.name = props.name
  136. this.quantity = props.quantity
  137. this.unitPrice = props.unitPrice
  138. this.preTaxTotal = this.quantity * this.unitPrice
  139. }
  140. /**
  141. * Return an add-on update that doesn't modify the add-on
  142. */
  143. toAddOnUpdate() {
  144. return new RecurlySubscriptionAddOnUpdate({
  145. code: this.code,
  146. quantity: this.quantity,
  147. unitPrice: this.unitPrice,
  148. })
  149. }
  150. }
  151. class RecurlySubscriptionChangeRequest {
  152. /**
  153. * @param {object} props
  154. * @param {RecurlySubscription} props.subscription
  155. * @param {"now" | "term_end"} props.timeframe
  156. * @param {string} [props.planCode]
  157. * @param {RecurlySubscriptionAddOnUpdate[]} [props.addOnUpdates]
  158. */
  159. constructor(props) {
  160. if (props.planCode == null && props.addOnUpdates == null) {
  161. throw new OError('Invalid RecurlySubscriptionChangeRequest', { props })
  162. }
  163. this.subscription = props.subscription
  164. this.timeframe = props.timeframe
  165. this.planCode = props.planCode ?? null
  166. this.addOnUpdates = props.addOnUpdates ?? null
  167. }
  168. }
  169. class RecurlySubscriptionAddOnUpdate {
  170. /**
  171. * @param {object} props
  172. * @param {string} props.code
  173. * @param {number} [props.quantity]
  174. * @param {number} [props.unitPrice]
  175. */
  176. constructor(props) {
  177. this.code = props.code
  178. this.quantity = props.quantity ?? null
  179. this.unitPrice = props.unitPrice ?? null
  180. }
  181. }
  182. class RecurlySubscriptionChange {
  183. /**
  184. * @param {object} props
  185. * @param {RecurlySubscription} props.subscription
  186. * @param {string} props.nextPlanCode
  187. * @param {string} props.nextPlanName
  188. * @param {number} props.nextPlanPrice
  189. * @param {RecurlySubscriptionAddOn[]} props.nextAddOns
  190. * @param {number} [props.immediateCharge]
  191. */
  192. constructor(props) {
  193. this.subscription = props.subscription
  194. this.nextPlanCode = props.nextPlanCode
  195. this.nextPlanName = props.nextPlanName
  196. this.nextPlanPrice = props.nextPlanPrice
  197. this.nextAddOns = props.nextAddOns
  198. this.immediateCharge = props.immediateCharge ?? 0
  199. this.subtotal = this.nextPlanPrice
  200. for (const addOn of this.nextAddOns) {
  201. this.subtotal += addOn.preTaxTotal
  202. }
  203. this.tax = Math.round(this.subtotal * 100 * this.subscription.taxRate) / 100
  204. this.total = this.subtotal + this.tax
  205. }
  206. getAddOn(addOnCode) {
  207. return this.nextAddOns.find(addOn => addOn.code === addOnCode)
  208. }
  209. }
  210. class PaypalPaymentMethod {
  211. toString() {
  212. return 'Paypal'
  213. }
  214. }
  215. class CreditCardPaymentMethod {
  216. /**
  217. * @param {object} props
  218. * @param {string} props.cardType
  219. * @param {string} props.lastFour
  220. */
  221. constructor(props) {
  222. this.cardType = props.cardType
  223. this.lastFour = props.lastFour
  224. }
  225. toString() {
  226. return `${this.cardType} **** ${this.lastFour}`
  227. }
  228. }
  229. /**
  230. * An add-on configuration, independent of any subscription
  231. */
  232. class RecurlyAddOn {
  233. /**
  234. * @param {object} props
  235. * @param {string} props.code
  236. * @param {string} props.name
  237. */
  238. constructor(props) {
  239. this.code = props.code
  240. this.name = props.name
  241. }
  242. }
  243. module.exports = {
  244. AI_ADD_ON_CODE,
  245. RecurlySubscription,
  246. RecurlySubscriptionAddOn,
  247. RecurlySubscriptionChange,
  248. RecurlySubscriptionChangeRequest,
  249. RecurlySubscriptionAddOnUpdate,
  250. PaypalPaymentMethod,
  251. CreditCardPaymentMethod,
  252. RecurlyAddOn,
  253. }