mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
Merge pull request #21274 from overleaf/em-recurly-client
Separate Recurly business logic GitOrigin-RevId: 9c3b5ce61bdc7a6a5d3f507a31dc8919c882e476
This commit is contained in:
parent
3d5bceffee
commit
1acb0d1bcd
7 changed files with 752 additions and 297 deletions
|
@ -7,14 +7,10 @@ const Queues = require('../../infrastructure/Queues')
|
|||
|
||||
const EMAIL_SETTINGS = Settings.email || {}
|
||||
|
||||
module.exports = {
|
||||
sendEmail: callbackify(sendEmail),
|
||||
sendDeferredEmail,
|
||||
promises: {
|
||||
sendEmail,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} emailType
|
||||
* @param {opts} any
|
||||
*/
|
||||
async function sendEmail(emailType, opts) {
|
||||
const email = EmailBuilder.buildEmail(emailType, opts)
|
||||
if (email.type === 'lifecycle' && !EMAIL_SETTINGS.lifecycle) {
|
||||
|
@ -35,3 +31,11 @@ function sendDeferredEmail(emailType, opts, delay) {
|
|||
logger.warn({ err, emailType, opts }, 'failed to queue deferred email')
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendEmail: callbackify(sendEmail),
|
||||
sendDeferredEmail,
|
||||
promises: {
|
||||
sendEmail,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
// @ts-check
|
||||
|
||||
const recurly = require('recurly')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const logger = require('@overleaf/logger')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const { callbackify } = require('util')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const {
|
||||
RecurlySubscription,
|
||||
RecurlySubscriptionAddOn,
|
||||
} = require('./RecurlyEntities')
|
||||
|
||||
/**
|
||||
* @import { RecurlySubscriptionChangeRequest } from './RecurlyEntities'
|
||||
*/
|
||||
|
||||
const recurlySettings = Settings.apis.recurly
|
||||
const recurlyApiKey = recurlySettings ? recurlySettings.apiKey : undefined
|
||||
|
@ -40,25 +51,51 @@ async function createAccountForUserId(userId) {
|
|||
return account
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a subscription from Recurly
|
||||
*
|
||||
* @param {string} subscriptionId
|
||||
* @return {Promise<RecurlySubscription>}
|
||||
*/
|
||||
async function getSubscription(subscriptionId) {
|
||||
return await client.getSubscription(subscriptionId)
|
||||
const subscription = await client.getSubscription(`uuid-${subscriptionId}`)
|
||||
return makeSubscription(subscription)
|
||||
}
|
||||
|
||||
async function getSubscriptionByUuid(subscriptionUuid) {
|
||||
return await client.getSubscription('uuid-' + subscriptionUuid)
|
||||
}
|
||||
|
||||
async function changeSubscription(subscriptionId, body) {
|
||||
const change = await client.createSubscriptionChange(subscriptionId, body)
|
||||
/**
|
||||
* Request a susbcription change from Recurly
|
||||
*
|
||||
* @param {RecurlySubscriptionChangeRequest} changeRequest
|
||||
*/
|
||||
async function applySubscriptionChangeRequest(changeRequest) {
|
||||
/** @type {recurly.SubscriptionChangeCreate} */
|
||||
const body = {
|
||||
timeframe: changeRequest.timeframe,
|
||||
}
|
||||
if (changeRequest.planCode != null) {
|
||||
body.planCode = changeRequest.planCode
|
||||
}
|
||||
if (changeRequest.addOnUpdates != null) {
|
||||
body.addOns = changeRequest.addOnUpdates.map(addOnUpdate => {
|
||||
/** @type {recurly.SubscriptionAddOnUpdate} */
|
||||
const update = { code: addOnUpdate.code }
|
||||
if (addOnUpdate.quantity != null) {
|
||||
update.quantity = addOnUpdate.quantity
|
||||
}
|
||||
if (addOnUpdate.unitPrice != null) {
|
||||
update.unitAmount = addOnUpdate.unitPrice
|
||||
}
|
||||
return update
|
||||
})
|
||||
}
|
||||
const change = await client.createSubscriptionChange(
|
||||
`uuid-${changeRequest.subscriptionId}`,
|
||||
body
|
||||
)
|
||||
logger.debug(
|
||||
{ subscriptionId, changeId: change.id },
|
||||
{ subscriptionId: changeRequest.subscriptionId, changeId: change.id },
|
||||
'created subscription change'
|
||||
)
|
||||
return change
|
||||
}
|
||||
|
||||
async function changeSubscriptionByUuid(subscriptionUuid, ...args) {
|
||||
return await changeSubscription('uuid-' + subscriptionUuid, ...args)
|
||||
}
|
||||
|
||||
async function removeSubscriptionChange(subscriptionId) {
|
||||
|
@ -99,15 +136,73 @@ function subscriptionIsCanceledOrExpired(subscription) {
|
|||
return state === 'canceled' || state === 'expired'
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a RecurlySubscription from Recurly API data
|
||||
*
|
||||
* @param {recurly.Subscription} subscription
|
||||
* @return {RecurlySubscription}
|
||||
*/
|
||||
function makeSubscription(subscription) {
|
||||
if (
|
||||
subscription.uuid == null ||
|
||||
subscription.plan == null ||
|
||||
subscription.plan.code == null ||
|
||||
subscription.plan.name == null ||
|
||||
subscription.account == null ||
|
||||
subscription.account.code == null ||
|
||||
subscription.unitAmount == null ||
|
||||
subscription.subtotal == null ||
|
||||
subscription.total == null ||
|
||||
subscription.currency == null
|
||||
) {
|
||||
throw new OError('Invalid Recurly subscription', { subscription })
|
||||
}
|
||||
return new RecurlySubscription({
|
||||
id: subscription.uuid,
|
||||
userId: subscription.account.code,
|
||||
planCode: subscription.plan.code,
|
||||
planName: subscription.plan.name,
|
||||
planPrice: subscription.unitAmount,
|
||||
addOns: (subscription.addOns ?? []).map(makeSubscriptionAddOn),
|
||||
subtotal: subscription.subtotal,
|
||||
taxRate: subscription.taxInfo?.rate ?? 0,
|
||||
taxAmount: subscription.tax ?? 0,
|
||||
total: subscription.total,
|
||||
currency: subscription.currency,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a RecurlySubscriptionAddOn from Recurly API data
|
||||
*
|
||||
* @param {recurly.SubscriptionAddOn} addOn
|
||||
* @return {RecurlySubscriptionAddOn}
|
||||
*/
|
||||
function makeSubscriptionAddOn(addOn) {
|
||||
if (
|
||||
addOn.addOn == null ||
|
||||
addOn.addOn.code == null ||
|
||||
addOn.addOn.name == null ||
|
||||
addOn.unitAmount == null
|
||||
) {
|
||||
throw new OError('Invalid Recurly add-on', { addOn })
|
||||
}
|
||||
|
||||
return new RecurlySubscriptionAddOn({
|
||||
code: addOn.addOn.code,
|
||||
name: addOn.addOn.name,
|
||||
quantity: addOn.quantity ?? 1,
|
||||
unitPrice: addOn.unitAmount,
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
errors: recurly.errors,
|
||||
|
||||
getAccountForUserId: callbackify(getAccountForUserId),
|
||||
createAccountForUserId: callbackify(createAccountForUserId),
|
||||
getSubscription: callbackify(getSubscription),
|
||||
getSubscriptionByUuid: callbackify(getSubscriptionByUuid),
|
||||
changeSubscription: callbackify(changeSubscription),
|
||||
changeSubscriptionByUuid: callbackify(changeSubscriptionByUuid),
|
||||
applySubscriptionChangeRequest: callbackify(applySubscriptionChangeRequest),
|
||||
removeSubscriptionChange: callbackify(removeSubscriptionChange),
|
||||
removeSubscriptionChangeByUuid: callbackify(removeSubscriptionChangeByUuid),
|
||||
reactivateSubscriptionByUuid: callbackify(reactivateSubscriptionByUuid),
|
||||
|
@ -116,11 +211,9 @@ module.exports = {
|
|||
|
||||
promises: {
|
||||
getSubscription,
|
||||
getSubscriptionByUuid,
|
||||
getAccountForUserId,
|
||||
createAccountForUserId,
|
||||
changeSubscription,
|
||||
changeSubscriptionByUuid,
|
||||
applySubscriptionChangeRequest,
|
||||
removeSubscriptionChange,
|
||||
removeSubscriptionChangeByUuid,
|
||||
reactivateSubscriptionByUuid,
|
||||
|
|
194
services/web/app/src/Features/Subscription/RecurlyEntities.js
Normal file
194
services/web/app/src/Features/Subscription/RecurlyEntities.js
Normal file
|
@ -0,0 +1,194 @@
|
|||
// @ts-check
|
||||
|
||||
const OError = require('@overleaf/o-error')
|
||||
const { DuplicateAddOnError, AddOnNotPresentError } = require('./Errors')
|
||||
const PlansLocator = require('./PlansLocator')
|
||||
const SubscriptionHelper = require('./SubscriptionHelper')
|
||||
|
||||
class RecurlySubscription {
|
||||
/**
|
||||
* @param {object} props
|
||||
* @param {string} props.id
|
||||
* @param {string} props.userId
|
||||
* @param {string} props.planCode
|
||||
* @param {string} props.planName
|
||||
* @param {number} props.planPrice
|
||||
* @param {RecurlySubscriptionAddOn[]} [props.addOns]
|
||||
* @param {number} props.subtotal
|
||||
* @param {number} [props.taxRate]
|
||||
* @param {number} [props.taxAmount]
|
||||
* @param {string} props.currency
|
||||
* @param {number} props.total
|
||||
*/
|
||||
constructor(props) {
|
||||
this.id = props.id
|
||||
this.userId = props.userId
|
||||
this.planCode = props.planCode
|
||||
this.planName = props.planName
|
||||
this.planPrice = props.planPrice
|
||||
this.addOns = props.addOns ?? []
|
||||
this.subtotal = props.subtotal
|
||||
this.taxRate = props.taxRate ?? 0
|
||||
this.taxAmount = props.taxAmount ?? 0
|
||||
this.currency = props.currency
|
||||
this.total = props.total
|
||||
}
|
||||
|
||||
hasAddOn(code) {
|
||||
return this.addOns.some(addOn => addOn.code === code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change this subscription's plan
|
||||
*
|
||||
* @return {RecurlySubscriptionChangeRequest}
|
||||
*/
|
||||
getRequestForPlanChange(planCode) {
|
||||
const currentPlan = PlansLocator.findLocalPlanInSettings(this.planCode)
|
||||
if (currentPlan == null) {
|
||||
throw new OError('Unable to find plan in settings', {
|
||||
planCode: this.planCode,
|
||||
})
|
||||
}
|
||||
const newPlan = PlansLocator.findLocalPlanInSettings(planCode)
|
||||
if (newPlan == null) {
|
||||
throw new OError('Unable to find plan in settings', { planCode })
|
||||
}
|
||||
const changeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd(
|
||||
currentPlan,
|
||||
newPlan
|
||||
)
|
||||
const timeframe = changeAtTermEnd ? 'term_end' : 'now'
|
||||
return new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.id,
|
||||
timeframe,
|
||||
planCode,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Purchase an add-on on this subscription
|
||||
*
|
||||
* @param {string} code
|
||||
* @param {number} [quantity]
|
||||
* @return {RecurlySubscriptionChangeRequest} - the change request to send to
|
||||
* Recurly
|
||||
*
|
||||
* @throws {DuplicateAddOnError} if the add-on is already present on the subscription
|
||||
*/
|
||||
getRequestForAddOnPurchase(code, quantity = 1) {
|
||||
if (this.hasAddOn(code)) {
|
||||
throw new DuplicateAddOnError('Subscription already has add-on', {
|
||||
subscriptionId: this.id,
|
||||
addOnCode: code,
|
||||
})
|
||||
}
|
||||
|
||||
const addOnUpdates = this.addOns.map(addOn => addOn.toAddOnUpdate())
|
||||
addOnUpdates.push(new RecurlySubscriptionAddOnUpdate({ code, quantity }))
|
||||
return new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.id,
|
||||
timeframe: 'now',
|
||||
addOnUpdates,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an add-on from this subscription
|
||||
*
|
||||
* @param {string} code
|
||||
* @return {RecurlySubscriptionChangeRequest}
|
||||
*
|
||||
* @throws {AddOnNotPresentError} if the subscription doesn't have the add-on
|
||||
*/
|
||||
getRequestForAddOnRemoval(code) {
|
||||
if (!this.hasAddOn(code)) {
|
||||
throw new AddOnNotPresentError(
|
||||
'Subscripiton does not have add-on to remove',
|
||||
{
|
||||
subscriptionId: this.id,
|
||||
addOnCode: code,
|
||||
}
|
||||
)
|
||||
}
|
||||
const addOnUpdates = this.addOns
|
||||
.filter(addOn => addOn.code !== code)
|
||||
.map(addOn => addOn.toAddOnUpdate())
|
||||
return new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.id,
|
||||
timeframe: 'term_end',
|
||||
addOnUpdates,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class RecurlySubscriptionAddOn {
|
||||
/**
|
||||
* @param {object} props
|
||||
* @param {string} props.code
|
||||
* @param {string} props.name
|
||||
* @param {number} props.quantity
|
||||
* @param {number} props.unitPrice
|
||||
*/
|
||||
constructor(props) {
|
||||
this.code = props.code
|
||||
this.name = props.name
|
||||
this.quantity = props.quantity
|
||||
this.unitPrice = props.unitPrice
|
||||
}
|
||||
|
||||
get preTaxTotal() {
|
||||
return this.quantity * this.unitPrice
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an add-on update that doesn't modify the add-on
|
||||
*/
|
||||
toAddOnUpdate() {
|
||||
return new RecurlySubscriptionAddOnUpdate({
|
||||
code: this.code,
|
||||
quantity: this.quantity,
|
||||
unitPrice: this.unitPrice,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class RecurlySubscriptionChangeRequest {
|
||||
/**
|
||||
* @param {object} props
|
||||
* @param {string} props.subscriptionId
|
||||
* @param {"now" | "term_end"} props.timeframe
|
||||
* @param {string} [props.planCode]
|
||||
* @param {RecurlySubscriptionAddOnUpdate[]} [props.addOnUpdates]
|
||||
*/
|
||||
constructor(props) {
|
||||
if (props.planCode == null && props.addOnUpdates == null) {
|
||||
throw new OError('Invalid RecurlySubscriptionChangeRequest', { props })
|
||||
}
|
||||
this.subscriptionId = props.subscriptionId
|
||||
this.timeframe = props.timeframe
|
||||
this.planCode = props.planCode ?? null
|
||||
this.addOnUpdates = props.addOnUpdates ?? null
|
||||
}
|
||||
}
|
||||
|
||||
class RecurlySubscriptionAddOnUpdate {
|
||||
/**
|
||||
* @param {object} props
|
||||
* @param {string} props.code
|
||||
* @param {number} [props.quantity]
|
||||
* @param {number} [props.unitPrice]
|
||||
*/
|
||||
constructor(props) {
|
||||
this.code = props.code
|
||||
this.quantity = props.quantity ?? null
|
||||
this.unitPrice = props.unitPrice ?? null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RecurlySubscription,
|
||||
RecurlySubscriptionAddOn,
|
||||
RecurlySubscriptionChangeRequest,
|
||||
RecurlySubscriptionAddOnUpdate,
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
// @ts-check
|
||||
|
||||
const RecurlyWrapper = require('./RecurlyWrapper')
|
||||
const RecurlyClient = require('./RecurlyClient')
|
||||
const { User } = require('../../models/User')
|
||||
|
@ -5,15 +7,13 @@ const logger = require('@overleaf/logger')
|
|||
const SubscriptionUpdater = require('./SubscriptionUpdater')
|
||||
const LimitationsManager = require('./LimitationsManager')
|
||||
const EmailHandler = require('../Email/EmailHandler')
|
||||
const PlansLocator = require('./PlansLocator')
|
||||
const SubscriptionHelper = require('./SubscriptionHelper')
|
||||
const { callbackify } = require('@overleaf/promise-utils')
|
||||
const UserUpdater = require('../User/UserUpdater')
|
||||
const {
|
||||
DuplicateAddOnError,
|
||||
AddOnNotPresentError,
|
||||
NoRecurlySubscriptionError,
|
||||
} = require('./Errors')
|
||||
const { NoRecurlySubscriptionError } = require('./Errors')
|
||||
|
||||
/**
|
||||
* @import { RecurlySubscription } from './RecurlyEntities'
|
||||
*/
|
||||
|
||||
async function validateNoSubscriptionInRecurly(userId) {
|
||||
let subscriptions =
|
||||
|
@ -76,13 +76,18 @@ async function updateSubscription(user, planCode, couponCode) {
|
|||
)
|
||||
}
|
||||
|
||||
if (!hasSubscription) {
|
||||
if (
|
||||
!hasSubscription ||
|
||||
subscription == null ||
|
||||
subscription.recurlySubscription_id == null
|
||||
) {
|
||||
return
|
||||
}
|
||||
const recurlySubscriptionId = subscription.recurlySubscription_id
|
||||
|
||||
if (couponCode) {
|
||||
const usersSubscription = await RecurlyWrapper.promises.getSubscription(
|
||||
subscription.recurlySubscription_id,
|
||||
recurlySubscriptionId,
|
||||
{ includeAccount: true }
|
||||
)
|
||||
|
||||
|
@ -91,47 +96,12 @@ async function updateSubscription(user, planCode, couponCode) {
|
|||
couponCode
|
||||
)
|
||||
}
|
||||
let changeAtTermEnd
|
||||
|
||||
const currentPlan = PlansLocator.findLocalPlanInSettings(
|
||||
subscription.planCode
|
||||
const recurlySubscription = await RecurlyClient.promises.getSubscription(
|
||||
recurlySubscriptionId
|
||||
)
|
||||
const newPlan = PlansLocator.findLocalPlanInSettings(planCode)
|
||||
if (currentPlan && newPlan) {
|
||||
changeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd(
|
||||
currentPlan,
|
||||
newPlan
|
||||
)
|
||||
} else {
|
||||
logger.error(
|
||||
{ currentPlan: subscription.planCode, newPlan: planCode },
|
||||
'unable to locate both plans in settings'
|
||||
)
|
||||
throw new Error('unable to locate both plans in settings')
|
||||
}
|
||||
|
||||
const timeframe = changeAtTermEnd ? 'term_end' : 'now'
|
||||
const subscriptionChangeOptions = { planCode, timeframe }
|
||||
await _updateAndSyncSubscription(
|
||||
user,
|
||||
subscription.recurlySubscription_id,
|
||||
subscriptionChangeOptions
|
||||
)
|
||||
}
|
||||
|
||||
async function _updateAndSyncSubscription(
|
||||
user,
|
||||
recurlySubscriptionId,
|
||||
subscriptionChangeOptions
|
||||
) {
|
||||
await RecurlyClient.promises.changeSubscriptionByUuid(
|
||||
recurlySubscriptionId,
|
||||
subscriptionChangeOptions
|
||||
)
|
||||
|
||||
// v2 recurly API wants a UUID, but UUID isn't included in the subscription change response
|
||||
// we got the UUID from the DB using userHasV2Subscription() - it is the only property
|
||||
// we need to be able to build a 'recurlySubscription' object for syncSubscription()
|
||||
const changeRequest = recurlySubscription.getRequestForPlanChange(planCode)
|
||||
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
|
||||
await syncSubscription({ uuid: recurlySubscriptionId }, user._id)
|
||||
}
|
||||
|
||||
|
@ -139,7 +109,7 @@ async function cancelPendingSubscriptionChange(user) {
|
|||
const { hasSubscription, subscription } =
|
||||
await LimitationsManager.promises.userHasV2Subscription(user)
|
||||
|
||||
if (hasSubscription) {
|
||||
if (hasSubscription && subscription != null) {
|
||||
await RecurlyClient.promises.removeSubscriptionChangeByUuid(
|
||||
subscription.recurlySubscription_id
|
||||
)
|
||||
|
@ -150,7 +120,7 @@ async function cancelSubscription(user) {
|
|||
try {
|
||||
const { hasSubscription, subscription } =
|
||||
await LimitationsManager.promises.userHasV2Subscription(user)
|
||||
if (hasSubscription) {
|
||||
if (hasSubscription && subscription != null) {
|
||||
await RecurlyClient.promises.cancelSubscriptionByUuid(
|
||||
subscription.recurlySubscription_id
|
||||
)
|
||||
|
@ -178,7 +148,7 @@ async function reactivateSubscription(user) {
|
|||
try {
|
||||
const { hasSubscription, subscription } =
|
||||
await LimitationsManager.promises.userHasV2Subscription(user)
|
||||
if (hasSubscription) {
|
||||
if (hasSubscription && subscription != null) {
|
||||
await RecurlyClient.promises.reactivateSubscriptionByUuid(
|
||||
subscription.recurlySubscription_id
|
||||
)
|
||||
|
@ -269,6 +239,9 @@ async function _updateSubscriptionFromRecurly(subscription) {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<RecurlySubscription>}
|
||||
*/
|
||||
async function _getSubscription(user) {
|
||||
const { hasSubscription = false, subscription } =
|
||||
await LimitationsManager.promises.userHasV2Subscription(user)
|
||||
|
@ -281,50 +254,26 @@ async function _getSubscription(user) {
|
|||
}
|
||||
|
||||
const currentSub = await RecurlyClient.promises.getSubscription(
|
||||
`uuid-${subscription.recurlySubscription_id}`
|
||||
subscription.recurlySubscription_id
|
||||
)
|
||||
return currentSub
|
||||
}
|
||||
|
||||
async function purchaseAddon(user, addOnCode, quantity) {
|
||||
const subscription = await _getSubscription(user)
|
||||
const currentAddons = subscription?.addOns || []
|
||||
|
||||
const hasAddon = currentAddons.some(addOn => addOn.addOn.code === addOnCode)
|
||||
if (hasAddon) {
|
||||
throw new DuplicateAddOnError('User already has add-on', {
|
||||
userId: user._id,
|
||||
addOnCode,
|
||||
})
|
||||
}
|
||||
const addOns = [...currentAddons, { code: addOnCode, quantity }]
|
||||
const subscriptionChangeOptions = { addOns, timeframe: 'now' }
|
||||
|
||||
await _updateAndSyncSubscription(
|
||||
user,
|
||||
subscription.uuid,
|
||||
subscriptionChangeOptions
|
||||
const changeRequest = subscription.getRequestForAddOnPurchase(
|
||||
addOnCode,
|
||||
quantity
|
||||
)
|
||||
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
|
||||
await syncSubscription({ uuid: subscription.id }, user._id)
|
||||
}
|
||||
|
||||
async function removeAddon(user, addOnCode) {
|
||||
const subscription = await _getSubscription(user)
|
||||
const currentAddons = subscription?.addOns || []
|
||||
|
||||
const hasAddon = currentAddons.some(addOn => addOn.addOn.code === addOnCode)
|
||||
if (!hasAddon) {
|
||||
throw new AddOnNotPresentError('User does not have add-on to remove', {
|
||||
userId: user._id,
|
||||
addOnCode,
|
||||
})
|
||||
}
|
||||
const addOns = currentAddons.filter(addOn => addOn.addOn.code !== addOnCode)
|
||||
const subscriptionChangeOptions = { addOns, timeframe: 'term_end' }
|
||||
await _updateAndSyncSubscription(
|
||||
user,
|
||||
subscription.uuid,
|
||||
subscriptionChangeOptions
|
||||
)
|
||||
const changeRequest = subscription.getRequestForAddOnRemoval(addOnCode)
|
||||
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
|
||||
await syncSubscription({ uuid: subscription.id }, user._id)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const recurly = require('recurly')
|
||||
const modulePath = '../../../../app/src/Features/Subscription/RecurlyClient'
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const {
|
||||
RecurlySubscriptionChangeRequest,
|
||||
RecurlySubscriptionAddOnUpdate,
|
||||
} = require('../../../../app/src/Features/Subscription/RecurlyEntities')
|
||||
|
||||
const MODULE_PATH = '../../../../app/src/Features/Subscription/RecurlyClient'
|
||||
|
||||
describe('RecurlyClient', function () {
|
||||
beforeEach(function () {
|
||||
|
@ -13,20 +18,62 @@ describe('RecurlyClient', function () {
|
|||
privateKey: 'private_nonsense',
|
||||
},
|
||||
},
|
||||
plans: [],
|
||||
features: [],
|
||||
}
|
||||
|
||||
this.user = { _id: '123456', email: 'joe@example.com', first_name: 'Joe' }
|
||||
this.subscription = {
|
||||
id: 'subscription-123',
|
||||
uuid: 'subscription-uuid-123',
|
||||
}
|
||||
this.subscriptionChange = { id: 'subscription-change-123' }
|
||||
|
||||
this.recurlyAccount = new recurly.Account()
|
||||
Object.assign(this.recurlyAccount, { code: this.user._id })
|
||||
|
||||
this.recurlySubscription = new recurly.Subscription()
|
||||
Object.assign(this.recurlySubscription, this.subscription)
|
||||
this.subscriptionAddOn = {
|
||||
code: 'addon-code',
|
||||
name: 'My Add-On',
|
||||
quantity: 1,
|
||||
unitPrice: 2,
|
||||
}
|
||||
|
||||
this.subscription = {
|
||||
id: 'subscription-id',
|
||||
userId: 'user-id',
|
||||
currency: 'EUR',
|
||||
planCode: 'plan-code',
|
||||
planName: 'plan-name',
|
||||
planPrice: 13,
|
||||
addOns: [this.subscriptionAddOn],
|
||||
subtotal: 15,
|
||||
taxRate: 0.1,
|
||||
taxAmount: 1.5,
|
||||
total: 16.5,
|
||||
}
|
||||
|
||||
this.recurlySubscription = {
|
||||
uuid: this.subscription.id,
|
||||
account: {
|
||||
code: this.subscription.userId,
|
||||
},
|
||||
plan: {
|
||||
code: this.subscription.planCode,
|
||||
name: this.subscription.planName,
|
||||
},
|
||||
addOns: [
|
||||
{
|
||||
addOn: {
|
||||
code: this.subscriptionAddOn.code,
|
||||
name: this.subscriptionAddOn.name,
|
||||
},
|
||||
quantity: this.subscriptionAddOn.quantity,
|
||||
unitAmount: this.subscriptionAddOn.unitPrice,
|
||||
},
|
||||
],
|
||||
unitAmount: this.subscription.planPrice,
|
||||
subtotal: this.subscription.subtotal,
|
||||
taxInfo: { rate: this.subscription.taxRate },
|
||||
tax: this.subscription.taxAmount,
|
||||
total: this.subscription.total,
|
||||
currency: this.subscription.currency,
|
||||
}
|
||||
|
||||
this.recurlySubscriptionChange = new recurly.SubscriptionChange()
|
||||
Object.assign(this.recurlySubscriptionChange, this.subscriptionChange)
|
||||
|
@ -52,7 +99,7 @@ describe('RecurlyClient', function () {
|
|||
},
|
||||
}
|
||||
|
||||
return (this.RecurlyClient = SandboxedModule.require(modulePath, {
|
||||
return (this.RecurlyClient = SandboxedModule.require(MODULE_PATH, {
|
||||
globals: {
|
||||
console,
|
||||
},
|
||||
|
@ -130,12 +177,12 @@ describe('RecurlyClient', function () {
|
|||
it('should return the subscription found by recurly', async function () {
|
||||
this.client.getSubscription = sinon
|
||||
.stub()
|
||||
.withArgs('uuid-subscription-id')
|
||||
.resolves(this.recurlySubscription)
|
||||
await expect(
|
||||
this.RecurlyClient.promises.getSubscription(this.subscription.id)
|
||||
const subscription = await this.RecurlyClient.promises.getSubscription(
|
||||
this.subscription.id
|
||||
)
|
||||
.to.eventually.be.an.instanceOf(recurly.Subscription)
|
||||
.that.has.property('id', this.subscription.id)
|
||||
expect(subscription).to.deep.equal(this.subscription)
|
||||
})
|
||||
|
||||
it('should throw any API errors', async function () {
|
||||
|
@ -146,69 +193,58 @@ describe('RecurlyClient', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('changeSubscription', function () {
|
||||
describe('applySubscriptionChangeRequest', function () {
|
||||
beforeEach(function () {
|
||||
this.client.createSubscriptionChange = sinon
|
||||
.stub()
|
||||
.resolves(this.recurlySubscriptionChange)
|
||||
})
|
||||
|
||||
it('should attempt to create a subscription change', async function () {
|
||||
this.RecurlyClient.promises.changeSubscription(this.subscription.id, {})
|
||||
it('handles plan changes', async function () {
|
||||
await this.RecurlyClient.promises.applySubscriptionChangeRequest(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.subscription.id,
|
||||
timeframe: 'now',
|
||||
planCode: 'new-plan',
|
||||
})
|
||||
)
|
||||
expect(this.client.createSubscriptionChange).to.be.calledWith(
|
||||
this.subscription.id
|
||||
'uuid-subscription-id',
|
||||
{ timeframe: 'now', planCode: 'new-plan' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the subscription change event', async function () {
|
||||
await expect(
|
||||
this.RecurlyClient.promises.changeSubscription(
|
||||
this.subscriptionChange.id,
|
||||
{}
|
||||
)
|
||||
it('handles add-on changes', async function () {
|
||||
await this.RecurlyClient.promises.applySubscriptionChangeRequest(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.subscription.id,
|
||||
timeframe: 'now',
|
||||
addOnUpdates: [
|
||||
new RecurlySubscriptionAddOnUpdate({
|
||||
code: 'new-add-on',
|
||||
quantity: 2,
|
||||
unitPrice: 8.99,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
expect(this.client.createSubscriptionChange).to.be.calledWith(
|
||||
'uuid-subscription-id',
|
||||
{
|
||||
timeframe: 'now',
|
||||
addOns: [{ code: 'new-add-on', quantity: 2, unitAmount: 8.99 }],
|
||||
}
|
||||
)
|
||||
.to.eventually.be.an.instanceOf(recurly.SubscriptionChange)
|
||||
.that.has.property('id', this.subscriptionChange.id)
|
||||
})
|
||||
|
||||
it('should throw any API errors', async function () {
|
||||
this.client.createSubscriptionChange = sinon.stub().throws()
|
||||
await expect(
|
||||
this.RecurlyClient.promises.changeSubscription(this.subscription.id, {})
|
||||
).to.eventually.be.rejectedWith(Error)
|
||||
})
|
||||
|
||||
describe('changeSubscriptionByUuid', function () {
|
||||
it('should attempt to create a subscription change', async function () {
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid(
|
||||
this.subscription.uuid,
|
||||
this.RecurlyClient.promises.applySubscriptionChangeRequest(
|
||||
this.subscription.id,
|
||||
{}
|
||||
)
|
||||
expect(this.client.createSubscriptionChange).to.be.calledWith(
|
||||
'uuid-' + this.subscription.uuid
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the subscription change event', async function () {
|
||||
await expect(
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid(
|
||||
this.subscriptionChange.id,
|
||||
{}
|
||||
)
|
||||
)
|
||||
.to.eventually.be.an.instanceOf(recurly.SubscriptionChange)
|
||||
.that.has.property('id', this.subscriptionChange.id)
|
||||
})
|
||||
|
||||
it('should throw any API errors', async function () {
|
||||
this.client.createSubscriptionChange = sinon.stub().throws()
|
||||
await expect(
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid(
|
||||
this.subscription.id,
|
||||
{}
|
||||
)
|
||||
).to.eventually.be.rejectedWith(Error)
|
||||
})
|
||||
).to.eventually.be.rejectedWith(Error)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -265,11 +301,11 @@ describe('RecurlyClient', function () {
|
|||
this.client.reactivateSubscription = sinon
|
||||
.stub()
|
||||
.resolves(this.recurlySubscription)
|
||||
await expect(
|
||||
this.RecurlyClient.promises.reactivateSubscriptionByUuid(
|
||||
const subscription =
|
||||
await this.RecurlyClient.promises.reactivateSubscriptionByUuid(
|
||||
this.subscription.uuid
|
||||
)
|
||||
).to.eventually.be.an.instanceOf(recurly.Subscription)
|
||||
expect(subscription).to.deep.equal(this.recurlySubscription)
|
||||
expect(this.client.reactivateSubscription).to.be.calledWith(
|
||||
'uuid-' + this.subscription.uuid
|
||||
)
|
||||
|
@ -281,11 +317,11 @@ describe('RecurlyClient', function () {
|
|||
this.client.cancelSubscription = sinon
|
||||
.stub()
|
||||
.resolves(this.recurlySubscription)
|
||||
await expect(
|
||||
this.RecurlyClient.promises.cancelSubscriptionByUuid(
|
||||
const subscription =
|
||||
await this.RecurlyClient.promises.cancelSubscriptionByUuid(
|
||||
this.subscription.uuid
|
||||
)
|
||||
).to.eventually.be.an.instanceOf(recurly.Subscription)
|
||||
expect(subscription).to.deep.equal(this.recurlySubscription)
|
||||
expect(this.client.cancelSubscription).to.be.calledWith(
|
||||
'uuid-' + this.subscription.uuid
|
||||
)
|
||||
|
|
206
services/web/test/unit/src/Subscription/RecurlyEntitiesTest.js
Normal file
206
services/web/test/unit/src/Subscription/RecurlyEntitiesTest.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
// @ts-check
|
||||
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { expect } = require('chai')
|
||||
const Errors = require('../../../../app/src/Features/Subscription/Errors')
|
||||
const {
|
||||
RecurlySubscriptionChangeRequest,
|
||||
} = require('../../../../app/src/Features/Subscription/RecurlyEntities')
|
||||
|
||||
const MODULE_PATH = '../../../../app/src/Features/Subscription/RecurlyEntities'
|
||||
|
||||
describe('RecurlyEntities', function () {
|
||||
describe('RecurlySubscription', function () {
|
||||
beforeEach(function () {
|
||||
this.Settings = {
|
||||
plans: [
|
||||
{ planCode: 'cheap-plan', price_in_cents: 500 },
|
||||
{ planCode: 'regular-plan', price_in_cents: 1000 },
|
||||
{ planCode: 'premium-plan', price_in_cents: 2000 },
|
||||
],
|
||||
features: [],
|
||||
}
|
||||
|
||||
this.RecurlyEntities = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'@overleaf/settings': this.Settings,
|
||||
'./Errors': Errors,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('with add-ons', function () {
|
||||
beforeEach(function () {
|
||||
const { RecurlySubscription, RecurlySubscriptionAddOn } =
|
||||
this.RecurlyEntities
|
||||
this.addOn = new RecurlySubscriptionAddOn({
|
||||
code: 'add-on-code',
|
||||
name: 'My Add-On',
|
||||
quantity: 1,
|
||||
unitPrice: 2,
|
||||
})
|
||||
this.subscription = new RecurlySubscription({
|
||||
id: 'subscription-id',
|
||||
userId: 'user-id',
|
||||
planCode: 'regular-plan',
|
||||
planName: 'My Plan',
|
||||
planPrice: 10,
|
||||
addOns: [this.addOn],
|
||||
subtotal: 10.99,
|
||||
taxRate: 0.2,
|
||||
taxAmount: 2.4,
|
||||
total: 14.4,
|
||||
currency: 'USD',
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasAddOn()', function () {
|
||||
it('returns true if the subscription has the given add-on', function () {
|
||||
expect(this.subscription.hasAddOn(this.addOn.code)).to.be.true
|
||||
})
|
||||
|
||||
it("returns false if the subscription doesn't have the given add-on", function () {
|
||||
expect(this.subscription.hasAddOn('another-add-on')).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRequestForPlanChange()', function () {
|
||||
it('returns a change request for upgrades', function () {
|
||||
const { RecurlySubscriptionChangeRequest } = this.RecurlyEntities
|
||||
const changeRequest =
|
||||
this.subscription.getRequestForPlanChange('premium-plan')
|
||||
expect(changeRequest).to.deep.equal(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.subscription.id,
|
||||
timeframe: 'now',
|
||||
planCode: 'premium-plan',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('returns a change request for downgrades', function () {
|
||||
const { RecurlySubscriptionChangeRequest } = this.RecurlyEntities
|
||||
const changeRequest =
|
||||
this.subscription.getRequestForPlanChange('cheap-plan')
|
||||
expect(changeRequest).to.deep.equal(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.subscription.id,
|
||||
timeframe: 'term_end',
|
||||
planCode: 'cheap-plan',
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRequestForAddOnPurchase()', function () {
|
||||
it('returns a change request', function () {
|
||||
const {
|
||||
RecurlySubscriptionChangeRequest,
|
||||
RecurlySubscriptionAddOnUpdate,
|
||||
} = this.RecurlyEntities
|
||||
const changeRequest =
|
||||
this.subscription.getRequestForAddOnPurchase('another-add-on')
|
||||
expect(changeRequest).to.deep.equal(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.subscription.id,
|
||||
timeframe: 'now',
|
||||
addOnUpdates: [
|
||||
new RecurlySubscriptionAddOnUpdate({
|
||||
code: this.addOn.code,
|
||||
quantity: this.addOn.quantity,
|
||||
unitPrice: this.addOn.unitPrice,
|
||||
}),
|
||||
new RecurlySubscriptionAddOnUpdate({
|
||||
code: 'another-add-on',
|
||||
quantity: 1,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('throws a DuplicateAddOnError if the subscription already has the add-on', function () {
|
||||
expect(() =>
|
||||
this.subscription.getRequestForAddOnPurchase(this.addOn.code)
|
||||
).to.throw(Errors.DuplicateAddOnError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRequestForAddOnRemoval()', function () {
|
||||
it('returns a change request', function () {
|
||||
const changeRequest = this.subscription.getRequestForAddOnRemoval(
|
||||
this.addOn.code
|
||||
)
|
||||
expect(changeRequest).to.deep.equal(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.subscription.id,
|
||||
timeframe: 'term_end',
|
||||
addOnUpdates: [],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("throws an AddOnNotPresentError if the subscription doesn't have the add-on", function () {
|
||||
expect(() =>
|
||||
this.subscription.getRequestForAddOnRemoval('another-add-on')
|
||||
).to.throw(Errors.AddOnNotPresentError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without add-ons', function () {
|
||||
beforeEach(function () {
|
||||
const { RecurlySubscription } = this.RecurlyEntities
|
||||
this.subscription = new RecurlySubscription({
|
||||
id: 'subscription-id',
|
||||
userId: 'user-id',
|
||||
planCode: 'regular-plan',
|
||||
planName: 'My Plan',
|
||||
planPrice: 10,
|
||||
subtotal: 10.99,
|
||||
taxRate: 0.2,
|
||||
taxAmount: 2.4,
|
||||
total: 14.4,
|
||||
currency: 'USD',
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasAddOn()', function () {
|
||||
it('returns false for any add-on', function () {
|
||||
expect(this.subscription.hasAddOn('some-add-on')).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRequestForAddOnPurchase()', function () {
|
||||
it('returns a change request', function () {
|
||||
const {
|
||||
RecurlySubscriptionChangeRequest,
|
||||
RecurlySubscriptionAddOnUpdate,
|
||||
} = this.RecurlyEntities
|
||||
const changeRequest =
|
||||
this.subscription.getRequestForAddOnPurchase('some-add-on')
|
||||
expect(changeRequest).to.deep.equal(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.subscription.id,
|
||||
timeframe: 'now',
|
||||
addOnUpdates: [
|
||||
new RecurlySubscriptionAddOnUpdate({
|
||||
code: 'some-add-on',
|
||||
quantity: 1,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRequestForAddOnRemoval()', function () {
|
||||
it('throws an AddOnNotPresentError', function () {
|
||||
expect(() =>
|
||||
this.subscription.getRequestForAddOnRemoval('some-add-on')
|
||||
).to.throw(Errors.AddOnNotPresentError)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -2,6 +2,10 @@ const SandboxedModule = require('sandboxed-module')
|
|||
const sinon = require('sinon')
|
||||
const chai = require('chai')
|
||||
const { expect } = chai
|
||||
const {
|
||||
RecurlySubscription,
|
||||
RecurlySubscriptionChangeRequest,
|
||||
} = require('../../../../app/src/Features/Subscription/RecurlyEntities')
|
||||
|
||||
const MODULE_PATH =
|
||||
'../../../../app/src/Features/Subscription/SubscriptionHandler'
|
||||
|
@ -23,20 +27,16 @@ const mockRecurlySubscriptions = {
|
|||
}
|
||||
|
||||
const mockRecurlyClientSubscriptions = {
|
||||
'subscription-123-active': {
|
||||
id: 'subscription-123-recurly-id',
|
||||
uuid: 'subscription-123-active',
|
||||
plan: {
|
||||
name: 'Gold',
|
||||
code: 'gold',
|
||||
},
|
||||
currentPeriodEndsAt: new Date(),
|
||||
state: 'active',
|
||||
unitAmount: 10,
|
||||
account: {
|
||||
code: 'user-123',
|
||||
},
|
||||
},
|
||||
'subscription-123-active': new RecurlySubscription({
|
||||
id: 'subscription-123-active',
|
||||
userId: 'user-id',
|
||||
planCode: 'collaborator',
|
||||
planName: 'Collaborator',
|
||||
planPrice: 10,
|
||||
subtotal: 10,
|
||||
currency: 'USD',
|
||||
total: 10,
|
||||
}),
|
||||
}
|
||||
|
||||
const mockSubscriptionChanges = {
|
||||
|
@ -53,11 +53,16 @@ describe('SubscriptionHandler', function () {
|
|||
{
|
||||
planCode: 'collaborator',
|
||||
name: 'Collaborator',
|
||||
price_in_cents: 1000,
|
||||
features: {
|
||||
collaborators: -1,
|
||||
versioning: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
planCode: 'professional',
|
||||
price_in_cents: 1500,
|
||||
},
|
||||
],
|
||||
defaultPlanCode: {
|
||||
collaborators: 0,
|
||||
|
@ -94,7 +99,7 @@ describe('SubscriptionHandler', function () {
|
|||
.stub()
|
||||
.resolves(this.activeRecurlyClientSubscription),
|
||||
cancelSubscriptionByUuid: sinon.stub().resolves(),
|
||||
changeSubscriptionByUuid: sinon
|
||||
applySubscriptionChangeRequest: sinon
|
||||
.stub()
|
||||
.resolves(this.activeRecurlySubscriptionChange),
|
||||
getSubscription: sinon
|
||||
|
@ -122,14 +127,6 @@ describe('SubscriptionHandler', function () {
|
|||
sendDeferredEmail: sinon.stub(),
|
||||
}
|
||||
|
||||
this.PlansLocator = {
|
||||
findLocalPlanInSettings: sinon.stub().returns({ planCode: 'plan' }),
|
||||
}
|
||||
|
||||
this.SubscriptionHelper = {
|
||||
shouldPlanChangeAtTermEnd: sinon.stub(),
|
||||
}
|
||||
|
||||
this.UserUpdater = {
|
||||
promises: {
|
||||
updateUser: sinon.stub().resolves(),
|
||||
|
@ -148,8 +145,6 @@ describe('SubscriptionHandler', function () {
|
|||
'./LimitationsManager': this.LimitationsManager,
|
||||
'../Email/EmailHandler': this.EmailHandler,
|
||||
'../Analytics/AnalyticsManager': this.AnalyticsManager,
|
||||
'./PlansLocator': this.PlansLocator,
|
||||
'./SubscriptionHelper': this.SubscriptionHelper,
|
||||
'../User/UserUpdater': this.UserUpdater,
|
||||
},
|
||||
})
|
||||
|
@ -245,105 +240,80 @@ describe('SubscriptionHandler', function () {
|
|||
})
|
||||
})
|
||||
|
||||
function shouldUpdateSubscription() {
|
||||
it('should update the subscription', function () {
|
||||
expect(
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid
|
||||
).to.have.been.calledWith(this.subscription.recurlySubscription_id)
|
||||
const updateOptions =
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid.args[0][1]
|
||||
updateOptions.planCode.should.equal(this.plan_code)
|
||||
})
|
||||
}
|
||||
|
||||
function shouldSyncSubscription() {
|
||||
it('should sync the new subscription to the user', function () {
|
||||
expect(this.SubscriptionUpdater.promises.syncSubscription).to.have.been
|
||||
.called
|
||||
|
||||
this.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal(
|
||||
this.activeRecurlySubscription
|
||||
)
|
||||
this.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal(
|
||||
this.user._id
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function testUserWithASubscription(shouldPlanChangeAtTermEnd, timeframe) {
|
||||
describe(
|
||||
'when change should happen with timeframe ' + timeframe,
|
||||
function () {
|
||||
beforeEach(async function () {
|
||||
this.user.id = this.activeRecurlySubscription.account.account_code
|
||||
this.User.findById = (userId, projection) => ({
|
||||
exec: () => {
|
||||
userId.should.equal(this.user.id)
|
||||
return Promise.resolve(this.user)
|
||||
},
|
||||
})
|
||||
this.plan_code = 'collaborator'
|
||||
this.SubscriptionHelper.shouldPlanChangeAtTermEnd.returns(
|
||||
shouldPlanChangeAtTermEnd
|
||||
)
|
||||
this.LimitationsManager.promises.userHasV2Subscription.resolves({
|
||||
hasSubscription: true,
|
||||
subscription: this.subscription,
|
||||
})
|
||||
await this.SubscriptionHandler.promises.updateSubscription(
|
||||
this.user,
|
||||
this.plan_code,
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
shouldUpdateSubscription()
|
||||
shouldSyncSubscription()
|
||||
|
||||
it('should update with timeframe ' + timeframe, function () {
|
||||
const updateOptions =
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid.args[0][1]
|
||||
updateOptions.timeframe.should.equal(timeframe)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
describe('updateSubscription', function () {
|
||||
describe('with a user with a subscription', function () {
|
||||
testUserWithASubscription(false, 'now')
|
||||
testUserWithASubscription(true, 'term_end')
|
||||
beforeEach(async function () {
|
||||
this.user.id = this.activeRecurlySubscription.account.account_code
|
||||
this.User.findById = (userId, projection) => ({
|
||||
exec: () => {
|
||||
userId.should.equal(this.user.id)
|
||||
return Promise.resolve(this.user)
|
||||
},
|
||||
})
|
||||
this.plan_code = 'professional'
|
||||
this.LimitationsManager.promises.userHasV2Subscription.resolves({
|
||||
hasSubscription: true,
|
||||
subscription: this.subscription,
|
||||
})
|
||||
await this.SubscriptionHandler.promises.updateSubscription(
|
||||
this.user,
|
||||
this.plan_code,
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
describe('when plan(s) could not be located in settings', function () {
|
||||
beforeEach(async function () {
|
||||
this.user.id = this.activeRecurlySubscription.account.account_code
|
||||
this.User.findById = (userId, projection) => ({
|
||||
exec: () => {
|
||||
userId.should.equal(this.user.id)
|
||||
return Promise.resolve(this.user)
|
||||
},
|
||||
it('should update the subscription', function () {
|
||||
expect(
|
||||
this.RecurlyClient.promises.applySubscriptionChangeRequest
|
||||
).to.have.been.calledWith(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.subscription.recurlySubscription_id,
|
||||
timeframe: 'now',
|
||||
planCode: this.plan_code,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
this.plan_code = 'collaborator'
|
||||
this.PlansLocator.findLocalPlanInSettings.returns(null)
|
||||
this.LimitationsManager.promises.userHasV2Subscription.resolves({
|
||||
hasSubscription: true,
|
||||
subscription: this.subscription,
|
||||
})
|
||||
it('should sync the new subscription to the user', function () {
|
||||
expect(this.SubscriptionUpdater.promises.syncSubscription).to.have.been
|
||||
.called
|
||||
|
||||
this.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal(
|
||||
this.activeRecurlySubscription
|
||||
)
|
||||
this.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal(
|
||||
this.user._id
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when plan(s) could not be located in settings', function () {
|
||||
beforeEach(async function () {
|
||||
this.user.id = this.activeRecurlySubscription.account.account_code
|
||||
this.User.findById = (userId, projection) => ({
|
||||
exec: () => {
|
||||
userId.should.equal(this.user.id)
|
||||
return Promise.resolve(this.user)
|
||||
},
|
||||
})
|
||||
|
||||
it('should be rejected and should not update the subscription', function () {
|
||||
expect(
|
||||
this.SubscriptionHandler.promises.updateSubscription(
|
||||
this.user,
|
||||
this.plan_code,
|
||||
null
|
||||
)
|
||||
).to.be.rejected
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid.called.should.equal(
|
||||
false
|
||||
this.LimitationsManager.promises.userHasV2Subscription.resolves({
|
||||
hasSubscription: true,
|
||||
subscription: this.subscription,
|
||||
})
|
||||
})
|
||||
|
||||
it('should be rejected and should not update the subscription', function () {
|
||||
expect(
|
||||
this.SubscriptionHandler.promises.updateSubscription(
|
||||
this.user,
|
||||
'unknown-plan',
|
||||
null
|
||||
)
|
||||
})
|
||||
).to.be.rejected
|
||||
this.RecurlyClient.promises.applySubscriptionChangeRequest.called.should.equal(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -358,7 +328,7 @@ describe('SubscriptionHandler', function () {
|
|||
})
|
||||
|
||||
it('should redirect to the subscription dashboard', function () {
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid.called.should.equal(
|
||||
this.RecurlyClient.promises.applySubscriptionChangeRequest.called.should.equal(
|
||||
false
|
||||
)
|
||||
this.SubscriptionUpdater.promises.syncSubscription.called.should.equal(
|
||||
|
@ -407,11 +377,14 @@ describe('SubscriptionHandler', function () {
|
|||
|
||||
it('should update the subscription', function () {
|
||||
expect(
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid
|
||||
).to.be.calledWith(this.subscription.recurlySubscription_id)
|
||||
const updateOptions =
|
||||
this.RecurlyClient.promises.changeSubscriptionByUuid.args[0][1]
|
||||
updateOptions.planCode.should.equal(this.plan_code)
|
||||
this.RecurlyClient.promises.applySubscriptionChangeRequest
|
||||
).to.be.calledWith(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscriptionId: this.subscription.recurlySubscription_id,
|
||||
timeframe: 'now',
|
||||
planCode: this.plan_code,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue