| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- const { callbackify } = require('util')
- const SubscriptionUpdater = require('./SubscriptionUpdater')
- const SubscriptionLocator = require('./SubscriptionLocator')
- const SubscriptionController = require('./SubscriptionController')
- const { Subscription } = require('../../models/Subscription')
- 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(
- subscriptionId,
- userIdToRemove
- )
- }
- async function replaceUserReferencesInGroups(oldId, newId) {
- await Subscription.updateOne({ admin_id: oldId }, { admin_id: newId }).exec()
- await _replaceInArray(Subscription, 'manager_ids', oldId, newId)
- await _replaceInArray(Subscription, 'member_ids', oldId, newId)
- }
- async function isUserPartOfGroup(userId, subscriptionId) {
- const subscription =
- await SubscriptionLocator.promises.getSubscriptionByMemberIdAndId(
- userId,
- subscriptionId
- )
- return !!subscription
- }
- async function getTotalConfirmedUsersInGroup(subscriptionId) {
- const subscription =
- await SubscriptionLocator.promises.getSubscription(subscriptionId)
- return subscription?.member_ids?.length
- }
- async function _replaceInArray(model, property, oldValue, newValue) {
- // Mongo won't let us pull and addToSet in the same query, so do it in
- // two. Note we need to add first, since the query is based on the old user.
- const query = {}
- query[property] = oldValue
- const setNewValue = {}
- setNewValue[property] = newValue
- const setOldValue = {}
- setOldValue[property] = oldValue
- await model.updateMany(query, { $addToSet: setNewValue })
- await model.updateMany(query, { $pull: setOldValue })
- }
- async function ensureFlexibleLicensingEnabled(plan) {
- if (!plan?.canUseFlexibleLicensing) {
- throw new Error('The group plan does not support flexible licensing')
- }
- }
- async function getUsersGroupSubscriptionDetails(userId) {
- const subscription =
- await SubscriptionLocator.promises.getUsersSubscription(userId)
- if (!subscription) {
- throw new Error('No subscription was found')
- }
- if (!subscription.groupPlan) {
- throw new Error('User subscription is not a group plan')
- }
- const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
- const recurlySubscription = await RecurlyClient.promises.getSubscription(
- subscription.recurlySubscription_id
- )
- return {
- subscription,
- recurlySubscription,
- plan,
- }
- }
- async function _addSeatsSubscriptionChange(userId, adding) {
- const { recurlySubscription, plan } =
- await getUsersGroupSubscriptionDetails(userId)
- await ensureFlexibleLicensingEnabled(plan)
- const currentAddonQuantity =
- recurlySubscription.addOns.find(
- addOn => addOn.code === MEMBERS_LIMIT_ADD_ON_CODE
- )?.quantity ?? 0
- // Keeps only the new total quantity of addon
- const nextAddonQuantity = currentAddonQuantity + adding
- 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)_(2|3|4|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,
- currentAddonQuantity,
- recurlySubscription,
- }
- }
- async function previewAddSeatsSubscriptionChange(userId, adding) {
- const { changeRequest, currentAddonQuantity } =
- await _addSeatsSubscriptionChange(userId, adding)
- const paymentMethod = await RecurlyClient.promises.getPaymentMethod(userId)
- const subscriptionChange =
- await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
- const subscriptionChangePreview =
- await SubscriptionController.makeChangePreview(
- {
- type: 'add-on-update',
- addOn: {
- code: MEMBERS_LIMIT_ADD_ON_CODE,
- quantity: subscriptionChange.nextAddOns.find(
- addon => addon.code === MEMBERS_LIMIT_ADD_ON_CODE
- ).quantity,
- prevQuantity: currentAddonQuantity,
- },
- },
- subscriptionChange,
- paymentMethod
- )
- return subscriptionChangePreview
- }
- async function createAddSeatsSubscriptionChange(userId, adding) {
- const { changeRequest, recurlySubscription } =
- await _addSeatsSubscriptionChange(userId, adding)
- await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
- await SubscriptionHandler.promises.syncSubscription(
- { uuid: recurlySubscription.id },
- userId
- )
- return { adding }
- }
- async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
- if (
- subscription.planCode.includes('professional') ||
- !subscription.groupPlan
- ) {
- throw new Error('Not eligible for group plan upgrade')
- }
- return subscription.planCode.replace('collaborator', 'professional')
- }
- async function _getGroupPlanUpgradeChangeRequest(ownerId) {
- const olSubscription =
- await SubscriptionLocator.promises.getUsersSubscription(ownerId)
- const newPlanCode = await _getUpgradeTargetPlanCodeMaybeThrow(olSubscription)
- const recurlySubscription = await RecurlyClient.promises.getSubscription(
- olSubscription.recurlySubscription_id
- )
- return recurlySubscription.getRequestForGroupPlanUpgrade(newPlanCode)
- }
- async function getGroupPlanUpgradePreview(ownerId) {
- const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
- const subscriptionChange =
- await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
- const paymentMethod = await RecurlyClient.promises.getPaymentMethod(ownerId)
- return SubscriptionController.makeChangePreview(
- {
- type: 'group-plan-upgrade',
- prevPlan: {
- name: SubscriptionController.getPlanNameForDisplay(
- subscriptionChange.subscription.planName,
- subscriptionChange.subscription.planCode
- ),
- },
- },
- subscriptionChange,
- paymentMethod
- )
- }
- async function upgradeGroupPlan(ownerId) {
- const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
- await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
- await SubscriptionHandler.promises.syncSubscription(
- { uuid: changeRequest.subscription.id },
- ownerId
- )
- }
- module.exports = {
- removeUserFromGroup: callbackify(removeUserFromGroup),
- replaceUserReferencesInGroups: callbackify(replaceUserReferencesInGroups),
- ensureFlexibleLicensingEnabled: callbackify(ensureFlexibleLicensingEnabled),
- getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
- isUserPartOfGroup: callbackify(isUserPartOfGroup),
- getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
- upgradeGroupPlan: callbackify(upgradeGroupPlan),
- promises: {
- removeUserFromGroup,
- replaceUserReferencesInGroups,
- ensureFlexibleLicensingEnabled,
- getTotalConfirmedUsersInGroup,
- isUserPartOfGroup,
- getUsersGroupSubscriptionDetails,
- previewAddSeatsSubscriptionChange,
- createAddSeatsSubscriptionChange,
- getGroupPlanUpgradePreview,
- upgradeGroupPlan,
- },
- }
|