mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-06 07:39:02 +02:00
Merge pull request #22518 from overleaf/ii-flexible-group-licensing-add-seats-legacy
[web] Unlock self-served license purchasing for legacy plans GitOrigin-RevId: bf3083d00a77417f0e78d2145f6192c57b163273
This commit is contained in:
@@ -42,7 +42,7 @@ for (const [usage, planData] of Object.entries(groups)) {
|
||||
|
||||
// Generate plans in settings
|
||||
for (const size of sizes) {
|
||||
Settings.plans.push({
|
||||
let plan = {
|
||||
planCode: `group_${planCode}_${size}_${usage}`,
|
||||
name: `${
|
||||
Settings.appName
|
||||
@@ -55,8 +55,19 @@ for (const [usage, planData] of Object.entries(groups)) {
|
||||
features: Settings.features[planCode],
|
||||
groupPlan: true,
|
||||
membersLimit: parseInt(size),
|
||||
membersLimitAddOn: 'additional-license',
|
||||
})
|
||||
// Unlock flexible licensing for all plans
|
||||
canUseFlexibleLicensing: true,
|
||||
}
|
||||
|
||||
// Add the `membersLimitAddOn` only to group plans of 5 or greater size
|
||||
if (size >= 5) {
|
||||
plan = {
|
||||
...plan,
|
||||
membersLimitAddOn: 'additional-license',
|
||||
}
|
||||
}
|
||||
|
||||
Settings.plans.push(plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,8 @@ function subscriptionFromApi(apiSubscription) {
|
||||
apiSubscription.total == null ||
|
||||
apiSubscription.currency == null ||
|
||||
apiSubscription.currentPeriodStartedAt == null ||
|
||||
apiSubscription.currentPeriodEndsAt == null
|
||||
apiSubscription.currentPeriodEndsAt == null ||
|
||||
apiSubscription.createdAt == null
|
||||
) {
|
||||
throw new OError('Invalid Recurly subscription', {
|
||||
subscription: apiSubscription,
|
||||
@@ -268,6 +269,7 @@ function subscriptionFromApi(apiSubscription) {
|
||||
currency: apiSubscription.currency,
|
||||
periodStart: apiSubscription.currentPeriodStartedAt,
|
||||
periodEnd: apiSubscription.currentPeriodEndsAt,
|
||||
createdAt: apiSubscription.createdAt,
|
||||
})
|
||||
|
||||
if (apiSubscription.pendingChange != null) {
|
||||
|
||||
@@ -6,6 +6,7 @@ const PlansLocator = require('./PlansLocator')
|
||||
const SubscriptionHelper = require('./SubscriptionHelper')
|
||||
|
||||
const AI_ADD_ON_CODE = 'assistant'
|
||||
const MEMBERS_LIMIT_ADD_ON_CODE = 'additional-license'
|
||||
const STANDALONE_AI_ADD_ON_CODES = ['assistant', 'assistant-annual']
|
||||
|
||||
class RecurlySubscription {
|
||||
@@ -24,6 +25,7 @@ class RecurlySubscription {
|
||||
* @param {number} props.total
|
||||
* @param {Date} props.periodStart
|
||||
* @param {Date} props.periodEnd
|
||||
* @param {Date} props.createdAt
|
||||
* @param {RecurlySubscriptionChange} [props.pendingChange]
|
||||
*/
|
||||
constructor(props) {
|
||||
@@ -40,6 +42,7 @@ class RecurlySubscription {
|
||||
this.total = props.total
|
||||
this.periodStart = props.periodStart
|
||||
this.periodEnd = props.periodEnd
|
||||
this.createdAt = props.createdAt
|
||||
this.pendingChange = props.pendingChange ?? null
|
||||
}
|
||||
|
||||
@@ -129,12 +132,13 @@ class RecurlySubscription {
|
||||
*
|
||||
* @param {string} code
|
||||
* @param {number} [quantity]
|
||||
* @param {number} [unitPrice]
|
||||
* @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) {
|
||||
getRequestForAddOnPurchase(code, quantity = 1, unitPrice) {
|
||||
if (this.hasAddOn(code)) {
|
||||
throw new DuplicateAddOnError('Subscription already has add-on', {
|
||||
subscriptionId: this.id,
|
||||
@@ -143,7 +147,9 @@ class RecurlySubscription {
|
||||
}
|
||||
|
||||
const addOnUpdates = this.addOns.map(addOn => addOn.toAddOnUpdate())
|
||||
addOnUpdates.push(new RecurlySubscriptionAddOnUpdate({ code, quantity }))
|
||||
addOnUpdates.push(
|
||||
new RecurlySubscriptionAddOnUpdate({ code, quantity, unitPrice })
|
||||
)
|
||||
return new RecurlySubscriptionChangeRequest({
|
||||
subscription: this,
|
||||
timeframe: 'now',
|
||||
@@ -431,6 +437,7 @@ function isStandaloneAiAddOnPlanCode(planCode) {
|
||||
|
||||
module.exports = {
|
||||
AI_ADD_ON_CODE,
|
||||
MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
RecurlySubscription,
|
||||
RecurlySubscriptionAddOn,
|
||||
RecurlySubscriptionChange,
|
||||
|
||||
@@ -128,6 +128,7 @@ async function addSeatsToGroupSubscription(req, res) {
|
||||
req
|
||||
)
|
||||
await SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled(plan)
|
||||
await SubscriptionGroupHandler.promises.ensureAddSeatsEnabled(plan)
|
||||
|
||||
res.render('subscriptions/add-seats', {
|
||||
subscriptionId: subscription._id,
|
||||
|
||||
@@ -7,6 +7,8 @@ const SessionManager = require('../Authentication/SessionManager')
|
||||
const RecurlyClient = require('./RecurlyClient')
|
||||
const PlansLocator = require('./PlansLocator')
|
||||
const SubscriptionHandler = require('./SubscriptionHandler')
|
||||
const GroupPlansData = require('./GroupPlansData')
|
||||
const { MEMBERS_LIMIT_ADD_ON_CODE } = require('./RecurlyEntities')
|
||||
|
||||
async function removeUserFromGroup(subscriptionId, userIdToRemove) {
|
||||
await SubscriptionUpdater.promises.removeUserFromGroup(
|
||||
@@ -57,7 +59,13 @@ async function _replaceInArray(model, property, oldValue, newValue) {
|
||||
|
||||
async function ensureFlexibleLicensingEnabled(plan) {
|
||||
if (!plan?.canUseFlexibleLicensing) {
|
||||
throw new Error('The group plan does not support flexible licencing')
|
||||
throw new Error('The group plan does not support flexible licensing')
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureAddSeatsEnabled(plan) {
|
||||
if (!plan?.membersLimitAddOn) {
|
||||
throw new Error('The group plan does not support adding seats')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,29 +96,60 @@ async function _addSeatsSubscriptionChange(req) {
|
||||
const { recurlySubscription, plan } =
|
||||
await getUsersGroupSubscriptionDetails(req)
|
||||
await ensureFlexibleLicensingEnabled(plan)
|
||||
await ensureAddSeatsEnabled(plan)
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
const currentAddonQuantity =
|
||||
recurlySubscription.addOns.find(
|
||||
addOn => addOn.code === plan.membersLimitAddOn
|
||||
addOn => addOn.code === MEMBERS_LIMIT_ADD_ON_CODE
|
||||
)?.quantity ?? 0
|
||||
// Keeps only the new total quantity of addon
|
||||
const nextAddonQuantity = currentAddonQuantity + adding
|
||||
const changeRequest = recurlySubscription.getRequestForAddOnUpdate(
|
||||
plan.membersLimitAddOn,
|
||||
nextAddonQuantity
|
||||
)
|
||||
|
||||
let changeRequest
|
||||
if (recurlySubscription.hasAddOn(MEMBERS_LIMIT_ADD_ON_CODE)) {
|
||||
// Not providing a custom price as once the subscription is locked
|
||||
// to an add-on at a given price, it will use it for subsequent payments
|
||||
changeRequest = recurlySubscription.getRequestForAddOnUpdate(
|
||||
MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
nextAddonQuantity
|
||||
)
|
||||
} else {
|
||||
let unitPrice
|
||||
const newPlanPricesAppliedAt = new Date('2025-01-08T14:00:00Z')
|
||||
const isLegacyPriceApplicable =
|
||||
new Date(recurlySubscription.createdAt) < newPlanPricesAppliedAt
|
||||
|
||||
if (isLegacyPriceApplicable) {
|
||||
const pattern =
|
||||
/^group_(collaborator|professional)_(5|10|20|50)_(educational|enterprise)$/
|
||||
const [, planCode, size, usage] = plan.planCode.match(pattern)
|
||||
const currency = recurlySubscription.currency
|
||||
const legacyPriceInCents =
|
||||
GroupPlansData[usage][planCode][currency][size]
|
||||
.additional_license_legacy_price_in_cents
|
||||
|
||||
if (legacyPriceInCents > 0) {
|
||||
unitPrice = legacyPriceInCents / 100
|
||||
}
|
||||
}
|
||||
|
||||
changeRequest = recurlySubscription.getRequestForAddOnPurchase(
|
||||
MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
nextAddonQuantity,
|
||||
unitPrice
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
changeRequest,
|
||||
userId,
|
||||
currentAddonQuantity,
|
||||
recurlySubscription,
|
||||
plan,
|
||||
}
|
||||
}
|
||||
|
||||
async function previewAddSeatsSubscriptionChange(req) {
|
||||
const { changeRequest, userId, currentAddonQuantity, plan } =
|
||||
const { changeRequest, userId, currentAddonQuantity } =
|
||||
await _addSeatsSubscriptionChange(req)
|
||||
const paymentMethod = await RecurlyClient.promises.getPaymentMethod(userId)
|
||||
const subscriptionChange =
|
||||
@@ -120,9 +159,9 @@ async function previewAddSeatsSubscriptionChange(req) {
|
||||
{
|
||||
type: 'add-on-update',
|
||||
addOn: {
|
||||
code: plan.membersLimitAddOn,
|
||||
code: MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
quantity: subscriptionChange.nextAddOns.find(
|
||||
addon => addon.code === plan.membersLimitAddOn
|
||||
addon => addon.code === MEMBERS_LIMIT_ADD_ON_CODE
|
||||
).quantity,
|
||||
prevQuantity: currentAddonQuantity,
|
||||
},
|
||||
@@ -216,6 +255,7 @@ module.exports = {
|
||||
removeUserFromGroup: callbackify(removeUserFromGroup),
|
||||
replaceUserReferencesInGroups: callbackify(replaceUserReferencesInGroups),
|
||||
ensureFlexibleLicensingEnabled: callbackify(ensureFlexibleLicensingEnabled),
|
||||
ensureAddSeatsEnabled: callbackify(ensureAddSeatsEnabled),
|
||||
getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
|
||||
isUserPartOfGroup: callbackify(isUserPartOfGroup),
|
||||
getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
|
||||
@@ -224,6 +264,7 @@ module.exports = {
|
||||
removeUserFromGroup,
|
||||
replaceUserReferencesInGroups,
|
||||
ensureFlexibleLicensingEnabled,
|
||||
ensureAddSeatsEnabled,
|
||||
getTotalConfirmedUsersInGroup,
|
||||
isUserPartOfGroup,
|
||||
getUsersGroupSubscriptionDetails,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,318 @@
|
||||
{
|
||||
"educational": {
|
||||
"professional": {
|
||||
"5": {
|
||||
"AUD": 321,
|
||||
"BRL": 699,
|
||||
"CAD": 314,
|
||||
"CHF": 279,
|
||||
"CLP": 168693,
|
||||
"COP": 552930,
|
||||
"DKK": 1665,
|
||||
"EUR": 258,
|
||||
"GBP": 223,
|
||||
"INR": 6719,
|
||||
"MXN": 4129,
|
||||
"NOK": 2008,
|
||||
"NZD": 321,
|
||||
"PEN": 671,
|
||||
"SEK": 2008,
|
||||
"SGD": 363,
|
||||
"USD": 279
|
||||
},
|
||||
"10": {
|
||||
"AUD": 179,
|
||||
"BRL": 389,
|
||||
"CAD": 175,
|
||||
"CHF": 155,
|
||||
"CLP": 93986,
|
||||
"COP": 308061,
|
||||
"DKK": 927,
|
||||
"EUR": 143,
|
||||
"GBP": 124,
|
||||
"INR": 3743,
|
||||
"MXN": 2300,
|
||||
"NOK": 1118,
|
||||
"NZD": 179,
|
||||
"PEN": 374,
|
||||
"SEK": 1118,
|
||||
"SGD": 202,
|
||||
"USD": 155
|
||||
},
|
||||
"20": {
|
||||
"AUD": 165,
|
||||
"BRL": 359,
|
||||
"CAD": 161,
|
||||
"CHF": 143,
|
||||
"CLP": 86756,
|
||||
"COP": 284364,
|
||||
"DKK": 856,
|
||||
"EUR": 132,
|
||||
"GBP": 114,
|
||||
"INR": 3455,
|
||||
"MXN": 2123,
|
||||
"NOK": 1032,
|
||||
"NZD": 165,
|
||||
"PEN": 345,
|
||||
"SEK": 1032,
|
||||
"SGD": 186,
|
||||
"USD": 143
|
||||
},
|
||||
"50": {
|
||||
"AUD": 151,
|
||||
"BRL": 329,
|
||||
"CAD": 148,
|
||||
"CHF": 131,
|
||||
"CLP": 79526,
|
||||
"COP": 260667,
|
||||
"DKK": 785,
|
||||
"EUR": 121,
|
||||
"GBP": 105,
|
||||
"INR": 3167,
|
||||
"MXN": 1946,
|
||||
"NOK": 946,
|
||||
"NZD": 151,
|
||||
"PEN": 316,
|
||||
"SEK": 946,
|
||||
"SGD": 171,
|
||||
"USD": 131
|
||||
}
|
||||
},
|
||||
"collaborator": {
|
||||
"5": {
|
||||
"AUD": 167,
|
||||
"BRL": 349,
|
||||
"CAD": 160,
|
||||
"CHF": 139,
|
||||
"CLP": 77693,
|
||||
"COP": 272930,
|
||||
"DKK": 839,
|
||||
"EUR": 125,
|
||||
"GBP": 111,
|
||||
"INR": 3219,
|
||||
"MXN": 2029,
|
||||
"NOK": 1014,
|
||||
"NZD": 167,
|
||||
"PEN": 321,
|
||||
"SEK": 1014,
|
||||
"SGD": 181,
|
||||
"USD": 139
|
||||
},
|
||||
"10": {
|
||||
"AUD": 93,
|
||||
"BRL": 194,
|
||||
"CAD": 89,
|
||||
"CHF": 77,
|
||||
"CLP": 43286,
|
||||
"COP": 152061,
|
||||
"DKK": 467,
|
||||
"EUR": 69,
|
||||
"GBP": 62,
|
||||
"INR": 1793,
|
||||
"MXN": 1130,
|
||||
"NOK": 565,
|
||||
"NZD": 93,
|
||||
"PEN": 179,
|
||||
"SEK": 565,
|
||||
"SGD": 101,
|
||||
"USD": 77
|
||||
},
|
||||
"20": {
|
||||
"AUD": 86,
|
||||
"BRL": 179,
|
||||
"CAD": 82,
|
||||
"CHF": 71,
|
||||
"CLP": 39956,
|
||||
"COP": 140364,
|
||||
"DKK": 431,
|
||||
"EUR": 64,
|
||||
"GBP": 57,
|
||||
"INR": 1655,
|
||||
"MXN": 1043,
|
||||
"NOK": 521,
|
||||
"NZD": 86,
|
||||
"PEN": 165,
|
||||
"SEK": 521,
|
||||
"SGD": 93,
|
||||
"USD": 71
|
||||
},
|
||||
"50": {
|
||||
"AUD": 78,
|
||||
"BRL": 164,
|
||||
"CAD": 75,
|
||||
"CHF": 65,
|
||||
"CLP": 36626,
|
||||
"COP": 128667,
|
||||
"DKK": 395,
|
||||
"EUR": 59,
|
||||
"GBP": 52,
|
||||
"INR": 1517,
|
||||
"MXN": 956,
|
||||
"NOK": 478,
|
||||
"NZD": 78,
|
||||
"PEN": 151,
|
||||
"SEK": 478,
|
||||
"SGD": 85,
|
||||
"USD": 65
|
||||
}
|
||||
}
|
||||
},
|
||||
"enterprise": {
|
||||
"professional": {
|
||||
"5": {
|
||||
"AUD": 321,
|
||||
"BRL": 699,
|
||||
"CAD": 314,
|
||||
"CHF": 499,
|
||||
"CLP": 168693,
|
||||
"COP": 552930,
|
||||
"DKK": 1665,
|
||||
"EUR": 258,
|
||||
"GBP": 223,
|
||||
"INR": 6719,
|
||||
"MXN": 4129,
|
||||
"NOK": 2008,
|
||||
"NZD": 321,
|
||||
"PEN": 671,
|
||||
"SEK": 2008,
|
||||
"SGD": 363,
|
||||
"USD": 279
|
||||
},
|
||||
"10": {
|
||||
"AUD": 298,
|
||||
"BRL": 649,
|
||||
"CAD": 291,
|
||||
"CHF": 259,
|
||||
"CLP": 156643,
|
||||
"COP": 513435,
|
||||
"DKK": 1546,
|
||||
"EUR": 239,
|
||||
"GBP": 207,
|
||||
"INR": 6239,
|
||||
"MXN": 3834,
|
||||
"NOK": 1864,
|
||||
"NZD": 298,
|
||||
"PEN": 623,
|
||||
"SEK": 1864,
|
||||
"SGD": 337,
|
||||
"USD": 259
|
||||
},
|
||||
"20": {
|
||||
"AUD": 275,
|
||||
"BRL": 599,
|
||||
"CAD": 269,
|
||||
"CHF": 239,
|
||||
"CLP": 144594,
|
||||
"COP": 473940,
|
||||
"DKK": 1427,
|
||||
"EUR": 221,
|
||||
"GBP": 191,
|
||||
"INR": 5759,
|
||||
"MXN": 3539,
|
||||
"NOK": 1721,
|
||||
"NZD": 275,
|
||||
"PEN": 575,
|
||||
"SEK": 1721,
|
||||
"SGD": 311,
|
||||
"USD": 239
|
||||
},
|
||||
"50": {
|
||||
"AUD": 252,
|
||||
"BRL": 549,
|
||||
"CAD": 246,
|
||||
"CHF": 219,
|
||||
"CLP": 132544,
|
||||
"COP": 400000,
|
||||
"DKK": 1308,
|
||||
"EUR": 202,
|
||||
"GBP": 175,
|
||||
"INR": 5279,
|
||||
"MXN": 3244,
|
||||
"NOK": 1577,
|
||||
"NZD": 252,
|
||||
"PEN": 527,
|
||||
"SEK": 1577,
|
||||
"SGD": 285,
|
||||
"USD": 219
|
||||
}
|
||||
},
|
||||
"collaborator": {
|
||||
"5": {
|
||||
"AUD": 167,
|
||||
"BRL": 349,
|
||||
"CAD": 160,
|
||||
"CHF": 139,
|
||||
"CLP": 77693,
|
||||
"COP": 272930,
|
||||
"DKK": 839,
|
||||
"EUR": 125,
|
||||
"GBP": 111,
|
||||
"INR": 3219,
|
||||
"MXN": 2029,
|
||||
"NOK": 1014,
|
||||
"NZD": 167,
|
||||
"PEN": 321,
|
||||
"SEK": 1014,
|
||||
"SGD": 181,
|
||||
"USD": 139
|
||||
},
|
||||
"10": {
|
||||
"AUD": 155,
|
||||
"BRL": 324,
|
||||
"CAD": 148,
|
||||
"CHF": 129,
|
||||
"CLP": 72143,
|
||||
"COP": 253435,
|
||||
"DKK": 779,
|
||||
"EUR": 116,
|
||||
"GBP": 103,
|
||||
"INR": 2989,
|
||||
"MXN": 1884,
|
||||
"NOK": 941,
|
||||
"NZD": 155,
|
||||
"PEN": 298,
|
||||
"SEK": 941,
|
||||
"SGD": 168,
|
||||
"USD": 129
|
||||
},
|
||||
"20": {
|
||||
"AUD": 143,
|
||||
"BRL": 299,
|
||||
"CAD": 137,
|
||||
"CHF": 119,
|
||||
"CLP": 66594,
|
||||
"COP": 233940,
|
||||
"DKK": 719,
|
||||
"EUR": 107,
|
||||
"GBP": 95,
|
||||
"INR": 2759,
|
||||
"MXN": 1739,
|
||||
"NOK": 869,
|
||||
"NZD": 143,
|
||||
"PEN": 275,
|
||||
"SEK": 869,
|
||||
"SGD": 155,
|
||||
"USD": 119
|
||||
},
|
||||
"50": {
|
||||
"AUD": 131,
|
||||
"BRL": 274,
|
||||
"CAD": 125,
|
||||
"CHF": 109,
|
||||
"CLP": 61044,
|
||||
"COP": 214445,
|
||||
"DKK": 659,
|
||||
"EUR": 98,
|
||||
"GBP": 87,
|
||||
"INR": 2529,
|
||||
"MXN": 1594,
|
||||
"NOK": 796,
|
||||
"NZD": 131,
|
||||
"PEN": 252,
|
||||
"SEK": 796,
|
||||
"SGD": 142,
|
||||
"USD": 109
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,16 @@ function generateGroupPlans(workSheetJSON) {
|
||||
)
|
||||
|
||||
const sizes = ['2', '3', '4', '5', '10', '20', '50']
|
||||
const additionalLicenseAddOnLegacyPricesFilePath = path.resolve(
|
||||
__dirname,
|
||||
'additional-license-add-on-legacy-prices.json'
|
||||
)
|
||||
const additionalLicenseAddOnLegacyPricesFile = fs.readFileSync(
|
||||
additionalLicenseAddOnLegacyPricesFilePath
|
||||
)
|
||||
const additionalLicenseAddOnLegacyPrices = JSON.parse(
|
||||
additionalLicenseAddOnLegacyPricesFile
|
||||
)
|
||||
|
||||
const result = {}
|
||||
for (const type1 of ['educational', 'enterprise']) {
|
||||
@@ -152,6 +162,15 @@ function generateGroupPlans(workSheetJSON) {
|
||||
result[type1][type2][currency][size] = {
|
||||
price_in_cents: plan[currency] * 100,
|
||||
}
|
||||
|
||||
const additionalLicenseAddOnLegacyPrice =
|
||||
additionalLicenseAddOnLegacyPrices[type1][type2][size]?.[currency]
|
||||
if (additionalLicenseAddOnLegacyPrice) {
|
||||
Object.assign(result[type1][type2][currency][size], {
|
||||
additional_license_legacy_price_in_cents:
|
||||
additionalLicenseAddOnLegacyPrice * 100,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ describe('RecurlyClient', function () {
|
||||
total: 16.5,
|
||||
periodStart: new Date(),
|
||||
periodEnd: new Date(),
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
this.recurlySubscription = {
|
||||
@@ -79,6 +80,7 @@ describe('RecurlyClient', function () {
|
||||
currency: this.subscription.currency,
|
||||
currentPeriodStartedAt: this.subscription.periodStart,
|
||||
currentPeriodEndsAt: this.subscription.periodEnd,
|
||||
createdAt: this.subscription.createdAt,
|
||||
}
|
||||
|
||||
this.recurlySubscriptionChange = new recurly.SubscriptionChange()
|
||||
|
||||
@@ -185,6 +185,38 @@ describe('RecurlyEntities', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('returns a change request with quantity and unit price specified', function () {
|
||||
const {
|
||||
RecurlySubscriptionChangeRequest,
|
||||
RecurlySubscriptionAddOnUpdate,
|
||||
} = this.RecurlyEntities
|
||||
const quantity = 5
|
||||
const unitPrice = 10
|
||||
const changeRequest = this.subscription.getRequestForAddOnPurchase(
|
||||
'another-add-on',
|
||||
quantity,
|
||||
unitPrice
|
||||
)
|
||||
expect(changeRequest).to.deep.equal(
|
||||
new RecurlySubscriptionChangeRequest({
|
||||
subscription: this.subscription,
|
||||
timeframe: 'now',
|
||||
addOnUpdates: [
|
||||
new RecurlySubscriptionAddOnUpdate({
|
||||
code: this.addOn.code,
|
||||
quantity: this.addOn.quantity,
|
||||
unitPrice: this.addOn.unitPrice,
|
||||
}),
|
||||
new RecurlySubscriptionAddOnUpdate({
|
||||
code: 'another-add-on',
|
||||
quantity,
|
||||
unitPrice,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('throws a DuplicateAddOnError if the subscription already has the add-on', function () {
|
||||
expect(() =>
|
||||
this.subscription.getRequestForAddOnPurchase(this.addOn.code)
|
||||
@@ -346,6 +378,7 @@ describe('RecurlyEntities', function () {
|
||||
total: 11.5,
|
||||
periodStart: new Date(),
|
||||
periodEnd: new Date(),
|
||||
createdAt: new Date(),
|
||||
})
|
||||
const change = new RecurlySubscriptionChange({
|
||||
subscription,
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('SubscriptionGroupController', function () {
|
||||
.stub()
|
||||
.resolves(this.createSubscriptionChangeData),
|
||||
ensureFlexibleLicensingEnabled: sinon.stub().resolves(),
|
||||
ensureAddSeatsEnabled: sinon.stub().resolves(),
|
||||
getGroupPlanUpgradePreview: sinon
|
||||
.stub()
|
||||
.resolves(this.previewSubscriptionChangeData),
|
||||
@@ -336,6 +337,9 @@ describe('SubscriptionGroupController', function () {
|
||||
this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled
|
||||
.calledWith(this.plan)
|
||||
.should.equal(true)
|
||||
this.SubscriptionGroupHandler.promises.ensureAddSeatsEnabled
|
||||
.calledWith(this.plan)
|
||||
.should.equal(true)
|
||||
page.should.equal('subscriptions/add-seats')
|
||||
props.subscriptionId.should.equal(this.subscriptionId)
|
||||
props.groupName.should.equal(this.subscription.teamName)
|
||||
@@ -375,6 +379,21 @@ describe('SubscriptionGroupController', function () {
|
||||
|
||||
this.Controller.addSeatsToGroupSubscription(this.req, res)
|
||||
})
|
||||
|
||||
it('should redirect to subscription page when "add seats" is not enabled', function (done) {
|
||||
this.SubscriptionGroupHandler.promises.ensureAddSeatsEnabled = sinon
|
||||
.stub()
|
||||
.rejects()
|
||||
|
||||
const res = {
|
||||
redirect: url => {
|
||||
url.should.equal('/user/subscription')
|
||||
done()
|
||||
},
|
||||
}
|
||||
|
||||
this.Controller.addSeatsToGroupSubscription(this.req, res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('previewAddSeatsSubscriptionChange', function () {
|
||||
|
||||
@@ -15,9 +15,12 @@ describe('SubscriptionGroupHandler', function () {
|
||||
this.subscription_id = '31DSd1123D'
|
||||
this.adding = 1
|
||||
this.paymentMethod = { cardType: 'Visa', lastFour: '1111' }
|
||||
this.RecurlyEntities = {
|
||||
MEMBERS_LIMIT_ADD_ON_CODE: 'additional-license',
|
||||
}
|
||||
this.localPlanInSettings = {
|
||||
membersLimit: 2,
|
||||
membersLimitAddOn: 'additional-license',
|
||||
membersLimit: 5,
|
||||
membersLimitAddOn: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
}
|
||||
|
||||
this.subscription = {
|
||||
@@ -37,12 +40,21 @@ describe('SubscriptionGroupHandler', function () {
|
||||
id: 123,
|
||||
addOns: [
|
||||
{
|
||||
code: 'additional-license',
|
||||
code: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
getRequestForAddOnUpdate: sinon.stub().returns(this.changeRequest),
|
||||
getRequestForGroupPlanUpgrade: sinon.stub().returns(this.changeRequest),
|
||||
getRequestForAddOnPurchase: sinon.stub().returns(this.changeRequest),
|
||||
getRequestForFlexibleLicensingGroupPlanUpgrade: sinon
|
||||
.stub()
|
||||
.returns(this.changeRequest),
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
currency: 'USD',
|
||||
hasAddOn(code) {
|
||||
return this.addOns.some(addOn => addOn.code === code)
|
||||
},
|
||||
}
|
||||
|
||||
this.SubscriptionLocator = {
|
||||
@@ -81,7 +93,7 @@ describe('SubscriptionGroupHandler', function () {
|
||||
this.previewSubscriptionChange = {
|
||||
nextAddOns: [
|
||||
{
|
||||
code: 'additional-license',
|
||||
code: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
quantity: this.recurlySubscription.addOns[0].quantity + this.adding,
|
||||
},
|
||||
],
|
||||
@@ -115,6 +127,19 @@ describe('SubscriptionGroupHandler', function () {
|
||||
},
|
||||
}
|
||||
|
||||
this.GroupPlansData = {
|
||||
enterprise: {
|
||||
collaborator: {
|
||||
USD: {
|
||||
5: {
|
||||
price_in_cents: 10000,
|
||||
additional_license_legacy_price_in_cents: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
this.Handler = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'./SubscriptionUpdater': this.SubscriptionUpdater,
|
||||
@@ -126,7 +151,9 @@ describe('SubscriptionGroupHandler', function () {
|
||||
},
|
||||
'./RecurlyClient': this.RecurlyClient,
|
||||
'./PlansLocator': this.PlansLocator,
|
||||
'./RecurlyEntities': this.RecurlyEntities,
|
||||
'../Authentication/SessionManager': this.SessionManager,
|
||||
'./GroupPlansData': this.GroupPlansData,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -274,8 +301,8 @@ describe('SubscriptionGroupHandler', function () {
|
||||
expect(data).to.deep.equal({
|
||||
subscription: { groupPlan: true },
|
||||
plan: {
|
||||
membersLimit: 2,
|
||||
membersLimitAddOn: 'additional-license',
|
||||
membersLimit: 5,
|
||||
membersLimitAddOn: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
canUseFlexibleLicensing: true,
|
||||
},
|
||||
recurlySubscription: this.recurlySubscription,
|
||||
@@ -293,60 +320,169 @@ describe('SubscriptionGroupHandler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.recurlySubscription.getRequestForAddOnUpdate
|
||||
.calledWith(
|
||||
'additional-license',
|
||||
this.recurlySubscription.addOns[0].quantity + this.adding
|
||||
describe('has "additional-license" add-on', function () {
|
||||
beforeEach(function () {
|
||||
this.recurlySubscription.addOns = [
|
||||
{
|
||||
code: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
quantity: 6,
|
||||
},
|
||||
]
|
||||
this.prevQuantity = this.recurlySubscription.addOns[0].quantity
|
||||
this.previewSubscriptionChange.nextAddOns = [
|
||||
{
|
||||
code: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
quantity: this.prevQuantity + this.adding,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sinon.assert.notCalled(
|
||||
this.recurlySubscription.getRequestForAddOnPurchase
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
describe('previewAddSeatsSubscriptionChange', function () {
|
||||
it('should return the subscription change preview', async function () {
|
||||
const preview =
|
||||
await this.Handler.promises.previewAddSeatsSubscriptionChange(
|
||||
this.req
|
||||
)
|
||||
|
||||
this.RecurlyClient.promises.getPaymentMethod
|
||||
.calledWith(this.user_id)
|
||||
.should.equal(true)
|
||||
this.RecurlyClient.promises.previewSubscriptionChange
|
||||
.calledWith(this.changeRequest)
|
||||
.should.equal(true)
|
||||
this.SubscriptionController.makeChangePreview
|
||||
this.recurlySubscription.getRequestForAddOnUpdate
|
||||
.calledWith(
|
||||
{
|
||||
type: 'add-on-update',
|
||||
addOn: {
|
||||
code: 'additional-license',
|
||||
quantity:
|
||||
this.recurlySubscription.addOns[0].quantity + this.adding,
|
||||
prevQuantity: this.adding,
|
||||
},
|
||||
},
|
||||
this.previewSubscriptionChange,
|
||||
this.paymentMethod
|
||||
this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
this.recurlySubscription.addOns[0].quantity + this.adding
|
||||
)
|
||||
.should.equal(true)
|
||||
preview.should.equal(this.changePreview)
|
||||
})
|
||||
|
||||
describe('previewAddSeatsSubscriptionChange', function () {
|
||||
it('should return the subscription change preview', async function () {
|
||||
const preview =
|
||||
await this.Handler.promises.previewAddSeatsSubscriptionChange(
|
||||
this.req
|
||||
)
|
||||
this.RecurlyClient.promises.getPaymentMethod
|
||||
.calledWith(this.user_id)
|
||||
.should.equal(true)
|
||||
this.RecurlyClient.promises.previewSubscriptionChange
|
||||
.calledWith(this.changeRequest)
|
||||
.should.equal(true)
|
||||
this.SubscriptionController.makeChangePreview
|
||||
.calledWith(
|
||||
{
|
||||
type: 'add-on-update',
|
||||
addOn: {
|
||||
code: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
quantity:
|
||||
this.previewSubscriptionChange.nextAddOns[0].quantity,
|
||||
prevQuantity: this.prevQuantity,
|
||||
},
|
||||
},
|
||||
this.previewSubscriptionChange,
|
||||
this.paymentMethod
|
||||
)
|
||||
.should.equal(true)
|
||||
preview.should.equal(this.changePreview)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createAddSeatsSubscriptionChange', function () {
|
||||
it('should change the subscription', async function () {
|
||||
const result =
|
||||
await this.Handler.promises.createAddSeatsSubscriptionChange(
|
||||
this.req
|
||||
)
|
||||
|
||||
this.RecurlyClient.promises.applySubscriptionChangeRequest
|
||||
.calledWith(this.changeRequest)
|
||||
.should.equal(true)
|
||||
this.SubscriptionHandler.promises.syncSubscription
|
||||
.calledWith({ uuid: this.recurlySubscription.id }, this.user_id)
|
||||
.should.equal(true)
|
||||
expect(result).to.deep.equal({
|
||||
adding: this.req.body.adding,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createAddSeatsSubscriptionChange', function () {
|
||||
it('should change the subscription', async function () {
|
||||
const result =
|
||||
await this.Handler.promises.createAddSeatsSubscriptionChange(this.req)
|
||||
describe('has no "additional-license" add-on', function () {
|
||||
beforeEach(function () {
|
||||
this.recurlySubscription.addOns = []
|
||||
this.prevQuantity = this.recurlySubscription.addOns[0]?.quantity ?? 0
|
||||
this.previewSubscriptionChange.nextAddOns = [
|
||||
{
|
||||
code: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
quantity: this.prevQuantity + this.adding,
|
||||
},
|
||||
]
|
||||
this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
|
||||
...this.localPlanInSettings,
|
||||
planCode: 'group_collaborator_5_enterprise',
|
||||
canUseFlexibleLicensing: true,
|
||||
})
|
||||
})
|
||||
|
||||
this.RecurlyClient.promises.applySubscriptionChangeRequest
|
||||
.calledWith(this.changeRequest)
|
||||
.should.equal(true)
|
||||
this.SubscriptionHandler.promises.syncSubscription
|
||||
.calledWith({ uuid: this.recurlySubscription.id }, this.user_id)
|
||||
.should.equal(true)
|
||||
expect(result).to.deep.equal({
|
||||
adding: this.req.body.adding,
|
||||
afterEach(function () {
|
||||
sinon.assert.notCalled(
|
||||
this.recurlySubscription.getRequestForAddOnUpdate
|
||||
)
|
||||
})
|
||||
|
||||
describe('previewAddSeatsSubscriptionChange', function () {
|
||||
let preview
|
||||
|
||||
afterEach(function () {
|
||||
this.RecurlyClient.promises.getPaymentMethod
|
||||
.calledWith(this.user_id)
|
||||
.should.equal(true)
|
||||
this.RecurlyClient.promises.previewSubscriptionChange
|
||||
.calledWith(this.changeRequest)
|
||||
.should.equal(true)
|
||||
this.SubscriptionController.makeChangePreview
|
||||
.calledWith(
|
||||
{
|
||||
type: 'add-on-update',
|
||||
addOn: {
|
||||
code: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
quantity:
|
||||
this.previewSubscriptionChange.nextAddOns[0].quantity,
|
||||
prevQuantity: this.prevQuantity,
|
||||
},
|
||||
},
|
||||
this.previewSubscriptionChange,
|
||||
this.paymentMethod
|
||||
)
|
||||
.should.equal(true)
|
||||
preview.should.equal(this.changePreview)
|
||||
})
|
||||
|
||||
it('should return the subscription change preview with legacy add-on price', async function () {
|
||||
this.recurlySubscription.createdAt = '2025-01-01T00:00:00Z'
|
||||
|
||||
preview =
|
||||
await this.Handler.promises.previewAddSeatsSubscriptionChange(
|
||||
this.req
|
||||
)
|
||||
this.recurlySubscription.getRequestForAddOnPurchase
|
||||
.calledWithExactly(
|
||||
this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
this.adding,
|
||||
this.GroupPlansData.enterprise.collaborator.USD[5]
|
||||
.additional_license_legacy_price_in_cents / 100
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the subscription change preview with non-legacy add-on price', async function () {
|
||||
this.recurlySubscription.createdAt = '2030-01-01T00:00:00Z'
|
||||
|
||||
preview =
|
||||
await this.Handler.promises.previewAddSeatsSubscriptionChange(
|
||||
this.req
|
||||
)
|
||||
this.recurlySubscription.getRequestForAddOnPurchase
|
||||
.calledWithExactly(
|
||||
this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
this.adding,
|
||||
undefined
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -358,16 +494,32 @@ describe('SubscriptionGroupHandler', function () {
|
||||
this.Handler.promises.ensureFlexibleLicensingEnabled({
|
||||
canUseFlexibleLicensing: false,
|
||||
})
|
||||
).to.be.rejectedWith('The group plan does not support flexible licencing')
|
||||
).to.be.rejectedWith('The group plan does not support flexible licensing')
|
||||
})
|
||||
|
||||
it('should not throw if the subscription can use flexible licensing', async function () {
|
||||
await expect(
|
||||
this.Handler.promises.ensureFlexibleLicensingEnabled({
|
||||
canUseFlexibleLicensing: true,
|
||||
})
|
||||
).to.not.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
it('should not throw if the subscription can use flexible licensing', async function () {
|
||||
await expect(
|
||||
this.Handler.promises.ensureFlexibleLicensingEnabled({
|
||||
canUseFlexibleLicensing: true,
|
||||
})
|
||||
).to.not.be.rejected
|
||||
describe('ensureAddSeatsEnabled', function () {
|
||||
it('should throw if the subscription can not use the "add seats" feature', async function () {
|
||||
await expect(
|
||||
this.Handler.promises.ensureAddSeatsEnabled({})
|
||||
).to.be.rejectedWith('The group plan does not support adding seats')
|
||||
})
|
||||
|
||||
it('should not throw if the subscription can use the "add seats" feature', async function () {
|
||||
await expect(
|
||||
this.Handler.promises.ensureAddSeatsEnabled({
|
||||
membersLimitAddOn: this.RecurlyEntities.MEMBERS_LIMIT_ADD_ON_CODE,
|
||||
})
|
||||
).to.not.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('upgradeGroupPlan', function () {
|
||||
|
||||
Reference in New Issue
Block a user