Merge pull request #24412 from overleaf/ii-flexible-licensing-manually-collected-2
[web] Add seats feature for manually collected subscriptions GitOrigin-RevId: f7cc6f8ce17163f10e175a06bb471de6e3a96e3c
This commit is contained in:
@@ -30,6 +30,8 @@ class PaymentProviderSubscription {
|
||||
* @param {Date} props.periodStart
|
||||
* @param {Date} props.periodEnd
|
||||
* @param {string} props.collectionMethod
|
||||
* @param {string} [props.poNumber]
|
||||
* @param {string} [props.termsAndConditions]
|
||||
* @param {PaymentProviderSubscriptionChange} [props.pendingChange]
|
||||
* @param {PaymentProvider['service']} [props.service]
|
||||
* @param {string} [props.state]
|
||||
@@ -53,6 +55,8 @@ class PaymentProviderSubscription {
|
||||
this.periodStart = props.periodStart
|
||||
this.periodEnd = props.periodEnd
|
||||
this.collectionMethod = props.collectionMethod
|
||||
this.poNumber = props.poNumber ?? ''
|
||||
this.termsAndConditions = props.termsAndConditions ?? ''
|
||||
this.pendingChange = props.pendingChange ?? null
|
||||
this.service = props.service ?? 'recurly'
|
||||
this.state = props.state ?? 'active'
|
||||
@@ -263,6 +267,39 @@ class PaymentProviderSubscription {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the "PO number" and "Terms and conditions" in a subscription
|
||||
*
|
||||
* @param {string} poNumber
|
||||
* @param {string} termsAndConditions
|
||||
* @return {PaymentProviderSubscriptionUpdateRequest} - the update request to send to
|
||||
* Recurly
|
||||
*/
|
||||
getRequestForPoNumberAndTermsAndConditionsUpdate(
|
||||
poNumber,
|
||||
termsAndConditions
|
||||
) {
|
||||
return new PaymentProviderSubscriptionUpdateRequest({
|
||||
subscription: this,
|
||||
poNumber,
|
||||
termsAndConditions,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the "Terms and conditions" in a subscription
|
||||
*
|
||||
* @param {string} termsAndConditions
|
||||
* @return {PaymentProviderSubscriptionUpdateRequest} - the update request to send to
|
||||
* Recurly
|
||||
*/
|
||||
getRequestForTermsAndConditionsUpdate(termsAndConditions) {
|
||||
return new PaymentProviderSubscriptionUpdateRequest({
|
||||
subscription: this,
|
||||
termsAndConditions,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this subscription is manually collected
|
||||
*
|
||||
@@ -304,6 +341,20 @@ class PaymentProviderSubscriptionAddOn {
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentProviderSubscriptionUpdateRequest {
|
||||
/**
|
||||
* @param {object} props
|
||||
* @param {PaymentProviderSubscription} props.subscription
|
||||
* @param {string} [props.poNumber]
|
||||
* @param {string} [props.termsAndConditions]
|
||||
*/
|
||||
constructor(props) {
|
||||
this.subscription = props.subscription
|
||||
this.poNumber = props.poNumber ?? ''
|
||||
this.termsAndConditions = props.termsAndConditions ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentProviderSubscriptionChangeRequest {
|
||||
/**
|
||||
* @param {object} props
|
||||
@@ -514,6 +565,7 @@ module.exports = {
|
||||
PaymentProviderSubscriptionAddOn,
|
||||
PaymentProviderSubscriptionChange,
|
||||
PaymentProviderSubscriptionChangeRequest,
|
||||
PaymentProviderSubscriptionUpdateRequest,
|
||||
PaymentProviderSubscriptionAddOnUpdate,
|
||||
PaypalPaymentMethod,
|
||||
CreditCardPaymentMethod,
|
||||
|
||||
@@ -25,6 +25,7 @@ const {
|
||||
|
||||
/**
|
||||
* @import { PaymentProviderSubscriptionChangeRequest } from './PaymentProviderEntities'
|
||||
* @import { PaymentProviderSubscriptionUpdateRequest } from './PaymentProviderEntities'
|
||||
* @import { PaymentMethod } from './types'
|
||||
*/
|
||||
|
||||
@@ -188,7 +189,29 @@ async function getSubscriptionForUser(userId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a susbcription change from Recurly
|
||||
* Request a subscription update from Recurly
|
||||
*
|
||||
* @param {PaymentProviderSubscriptionUpdateRequest} updateRequest
|
||||
*/
|
||||
async function updateSubscriptionDetails(updateRequest) {
|
||||
const body = subscriptionUpdateRequestToApi(updateRequest)
|
||||
|
||||
const updatedSubscription = await client.updateSubscription(
|
||||
`uuid-${updateRequest.subscription.id}`,
|
||||
body
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
{
|
||||
subscriptionId: updateRequest.subscription.id,
|
||||
updateId: updatedSubscription.id,
|
||||
},
|
||||
'updated subscription'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a subscription change from Recurly
|
||||
*
|
||||
* @param {PaymentProviderSubscriptionChangeRequest} changeRequest
|
||||
*/
|
||||
@@ -361,6 +384,25 @@ async function getPlan(planCode) {
|
||||
return planFromApi(plan)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the country code for given user
|
||||
*
|
||||
* @param {string} userId
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
async function getCountryCode(userId) {
|
||||
const account = await client.getAccount(`code-${userId}`)
|
||||
const countryCode = account.address?.country
|
||||
|
||||
if (!countryCode) {
|
||||
throw new OError('Country code not found', {
|
||||
userId,
|
||||
})
|
||||
}
|
||||
|
||||
return countryCode
|
||||
}
|
||||
|
||||
function subscriptionIsCanceledOrExpired(subscription) {
|
||||
const state = subscription?.recurlyStatus?.state
|
||||
return state === 'canceled' || state === 'expired'
|
||||
@@ -424,7 +466,10 @@ function subscriptionFromApi(apiSubscription) {
|
||||
apiSubscription.currency == null ||
|
||||
apiSubscription.currentPeriodStartedAt == null ||
|
||||
apiSubscription.currentPeriodEndsAt == null ||
|
||||
apiSubscription.collectionMethod == null
|
||||
apiSubscription.collectionMethod == null ||
|
||||
// The values below could be null initially if the subscription has never updated
|
||||
!('poNumber' in apiSubscription) ||
|
||||
!('termsAndConditions' in apiSubscription)
|
||||
) {
|
||||
throw new OError('Invalid Recurly subscription', {
|
||||
subscription: apiSubscription,
|
||||
@@ -446,6 +491,8 @@ function subscriptionFromApi(apiSubscription) {
|
||||
periodStart: apiSubscription.currentPeriodStartedAt,
|
||||
periodEnd: apiSubscription.currentPeriodEndsAt,
|
||||
collectionMethod: apiSubscription.collectionMethod,
|
||||
poNumber: apiSubscription.poNumber ?? '',
|
||||
termsAndConditions: apiSubscription.termsAndConditions ?? '',
|
||||
service: 'recurly',
|
||||
state: apiSubscription.state ?? 'active',
|
||||
trialPeriodStart: apiSubscription.trialStartedAt,
|
||||
@@ -639,6 +686,22 @@ function subscriptionChangeRequestToApi(changeRequest) {
|
||||
return requestBody
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an API request from a PaymentProviderSubscriptionUpdateRequest
|
||||
*
|
||||
* @param {PaymentProviderSubscriptionUpdateRequest} updateRequest
|
||||
*/
|
||||
function subscriptionUpdateRequestToApi(updateRequest) {
|
||||
const requestBody = {}
|
||||
if (updateRequest.poNumber) {
|
||||
requestBody.poNumber = updateRequest.poNumber
|
||||
}
|
||||
if (updateRequest.termsAndConditions) {
|
||||
requestBody.termsAndConditions = updateRequest.termsAndConditions
|
||||
}
|
||||
return requestBody
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
errors: recurly.errors,
|
||||
|
||||
@@ -648,6 +711,7 @@ module.exports = {
|
||||
getSubscription: callbackify(getSubscription),
|
||||
getSubscriptionForUser: callbackify(getSubscriptionForUser),
|
||||
previewSubscriptionChange: callbackify(previewSubscriptionChange),
|
||||
updateSubscriptionDetails: callbackify(updateSubscriptionDetails),
|
||||
applySubscriptionChangeRequest: callbackify(applySubscriptionChangeRequest),
|
||||
removeSubscriptionChange: callbackify(removeSubscriptionChange),
|
||||
removeSubscriptionChangeByUuid: callbackify(removeSubscriptionChangeByUuid),
|
||||
@@ -656,6 +720,7 @@ module.exports = {
|
||||
getPaymentMethod: callbackify(getPaymentMethod),
|
||||
getAddOn: callbackify(getAddOn),
|
||||
getPlan: callbackify(getPlan),
|
||||
getCountryCode: callbackify(getCountryCode),
|
||||
subscriptionIsCanceledOrExpired,
|
||||
pauseSubscriptionByUuid: callbackify(pauseSubscriptionByUuid),
|
||||
resumeSubscriptionByUuid: callbackify(resumeSubscriptionByUuid),
|
||||
@@ -668,6 +733,7 @@ module.exports = {
|
||||
getActiveCouponsForUserId,
|
||||
getCustomerManagementLink,
|
||||
previewSubscriptionChange,
|
||||
updateSubscriptionDetails,
|
||||
applySubscriptionChangeRequest,
|
||||
removeSubscriptionChange,
|
||||
removeSubscriptionChangeByUuid,
|
||||
@@ -678,5 +744,6 @@ module.exports = {
|
||||
getPaymentMethod,
|
||||
getAddOn,
|
||||
getPlan,
|
||||
getCountryCode,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -723,7 +723,7 @@ function getPlanNameForDisplay(planName, planCode) {
|
||||
*
|
||||
* @param {SubscriptionChangeDescription} subscriptionChangeDescription A description of the change for the frontend
|
||||
* @param {PaymentProviderSubscriptionChange} subscriptionChange The subscription change object coming from Recurly
|
||||
* @param {PaymentMethod} paymentMethod The payment method associated to the user
|
||||
* @param {PaymentMethod} [paymentMethod] The payment method associated to the user
|
||||
* @return {SubscriptionChangePreview}
|
||||
*/
|
||||
function makeChangePreview(
|
||||
@@ -739,7 +739,7 @@ function makeChangePreview(
|
||||
change: subscriptionChangeDescription,
|
||||
currency: subscription.currency,
|
||||
immediateCharge: { ...subscriptionChange.immediateCharge },
|
||||
paymentMethod: paymentMethod.toString(),
|
||||
paymentMethod: paymentMethod?.toString(),
|
||||
nextPlan: {
|
||||
annual: nextPlan.annual ?? false,
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import SessionManager from '../Authentication/SessionManager.js'
|
||||
import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import Modules from '../../infrastructure/Modules.js'
|
||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import { Subscription } from '../../models/Subscription.js'
|
||||
import { isProfessionalGroupPlan } from './PlansHelper.mjs'
|
||||
@@ -135,23 +136,39 @@ async function addSeatsToGroupSubscription(req, res) {
|
||||
userId
|
||||
)
|
||||
await SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled(plan)
|
||||
await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual(
|
||||
recurlySubscription
|
||||
)
|
||||
await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges(
|
||||
recurlySubscription
|
||||
)
|
||||
// Check if the user has missing billing details
|
||||
await RecurlyClient.promises.getPaymentMethod(userId)
|
||||
await SubscriptionGroupHandler.promises.ensureSubscriptionIsActive(
|
||||
subscription
|
||||
)
|
||||
|
||||
const { variant: flexibleLicensingForManuallyBilledSubscriptionsVariant } =
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'flexible-group-licensing-for-manually-billed-subscriptions'
|
||||
)
|
||||
|
||||
if (flexibleLicensingForManuallyBilledSubscriptionsVariant === 'enabled') {
|
||||
await SubscriptionGroupHandler.promises.checkBillingInfoExistence(
|
||||
recurlySubscription,
|
||||
userId
|
||||
)
|
||||
} else {
|
||||
await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual(
|
||||
recurlySubscription
|
||||
)
|
||||
// Check if the user has missing billing details
|
||||
await RecurlyClient.promises.getPaymentMethod(userId)
|
||||
}
|
||||
|
||||
res.render('subscriptions/add-seats', {
|
||||
subscriptionId: subscription._id,
|
||||
groupName: subscription.teamName,
|
||||
totalLicenses: subscription.membersLimit,
|
||||
isProfessional: isProfessionalGroupPlan(subscription),
|
||||
isCollectionMethodManual: recurlySubscription.isCollectionMethodManual,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof MissingBillingInfoError) {
|
||||
@@ -231,7 +248,8 @@ async function createAddSeatsSubscriptionChange(req, res) {
|
||||
const create =
|
||||
await SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange(
|
||||
userId,
|
||||
req.body.adding
|
||||
req.body.adding,
|
||||
req.body.poNumber
|
||||
)
|
||||
|
||||
res.json(create)
|
||||
@@ -264,11 +282,27 @@ async function createAddSeatsSubscriptionChange(req, res) {
|
||||
async function submitForm(req, res) {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
const userEmail = await UserGetter.promises.getUserEmail(userId)
|
||||
const { adding } = req.body
|
||||
const { adding, poNumber } = req.body
|
||||
|
||||
const { recurlySubscription } =
|
||||
await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
|
||||
userId
|
||||
)
|
||||
|
||||
if (recurlySubscription.isCollectionMethodManual) {
|
||||
await SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms(
|
||||
userId,
|
||||
recurlySubscription,
|
||||
poNumber
|
||||
)
|
||||
}
|
||||
|
||||
const messageLines = [`\n**Overleaf Sales Contact Form:**`]
|
||||
messageLines.push('**Subject:** Self-Serve Group User Increase Request')
|
||||
messageLines.push(`**Estimated Number of Users:** ${adding}`)
|
||||
if (poNumber) {
|
||||
messageLines.push(`**PO Number:** ${poNumber}`)
|
||||
}
|
||||
messageLines.push(
|
||||
`**Message:** This email has been generated on behalf of user with email **${userEmail}** ` +
|
||||
'to request an increase in the total number of users for their subscription.'
|
||||
|
||||
@@ -7,6 +7,7 @@ const RecurlyClient = require('./RecurlyClient')
|
||||
const PlansLocator = require('./PlansLocator')
|
||||
const SubscriptionHandler = require('./SubscriptionHandler')
|
||||
const GroupPlansData = require('./GroupPlansData')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
const { MEMBERS_LIMIT_ADD_ON_CODE } = require('./PaymentProviderEntities')
|
||||
const {
|
||||
ManuallyCollectedError,
|
||||
@@ -122,13 +123,21 @@ async function getUsersGroupSubscriptionDetails(userId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkBillingInfoExistence(recurlySubscription, userId) {
|
||||
// Verify the billing info only if the collection method is not manual (e.g. automatic)
|
||||
if (!recurlySubscription.isCollectionMethodManual) {
|
||||
// Check if the user has missing billing details
|
||||
await RecurlyClient.promises.getPaymentMethod(userId)
|
||||
}
|
||||
}
|
||||
|
||||
async function _addSeatsSubscriptionChange(userId, adding) {
|
||||
const { subscription, recurlySubscription, plan } =
|
||||
await getUsersGroupSubscriptionDetails(userId)
|
||||
await ensureFlexibleLicensingEnabled(plan)
|
||||
await ensureSubscriptionIsActive(subscription)
|
||||
await ensureSubscriptionCollectionMethodIsNotManual(recurlySubscription)
|
||||
await ensureSubscriptionHasNoPendingChanges(recurlySubscription)
|
||||
await checkBillingInfoExistence(recurlySubscription, userId)
|
||||
|
||||
const currentAddonQuantity =
|
||||
recurlySubscription.addOns.find(
|
||||
@@ -202,7 +211,6 @@ function _shouldUseLegacyPricing(
|
||||
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 =
|
||||
@@ -217,16 +225,20 @@ async function previewAddSeatsSubscriptionChange(userId, adding) {
|
||||
prevQuantity: currentAddonQuantity,
|
||||
},
|
||||
},
|
||||
subscriptionChange,
|
||||
paymentMethod
|
||||
subscriptionChange
|
||||
)
|
||||
|
||||
return subscriptionChangePreview
|
||||
}
|
||||
|
||||
async function createAddSeatsSubscriptionChange(userId, adding) {
|
||||
async function createAddSeatsSubscriptionChange(userId, adding, poNumber) {
|
||||
const { changeRequest, recurlySubscription } =
|
||||
await _addSeatsSubscriptionChange(userId, adding)
|
||||
|
||||
if (recurlySubscription.isCollectionMethodManual) {
|
||||
await updateSubscriptionPaymentTerms(userId, recurlySubscription, poNumber)
|
||||
}
|
||||
|
||||
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
|
||||
await SubscriptionHandler.promises.syncSubscription(
|
||||
{ uuid: recurlySubscription.id },
|
||||
@@ -236,6 +248,29 @@ async function createAddSeatsSubscriptionChange(userId, adding) {
|
||||
return { adding }
|
||||
}
|
||||
|
||||
async function updateSubscriptionPaymentTerms(
|
||||
userId,
|
||||
recurlySubscription,
|
||||
poNumber
|
||||
) {
|
||||
const countryCode = await RecurlyClient.promises.getCountryCode(userId)
|
||||
const [termsAndConditions] = await Modules.promises.hooks.fire(
|
||||
'generateTermsAndConditions',
|
||||
{ countryCode, poNumber }
|
||||
)
|
||||
|
||||
const updateRequest = poNumber
|
||||
? recurlySubscription.getRequestForPoNumberAndTermsAndConditionsUpdate(
|
||||
poNumber,
|
||||
termsAndConditions
|
||||
)
|
||||
: recurlySubscription.getRequestForTermsAndConditionsUpdate(
|
||||
termsAndConditions
|
||||
)
|
||||
|
||||
await RecurlyClient.promises.updateSubscriptionDetails(updateRequest)
|
||||
}
|
||||
|
||||
async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
|
||||
if (
|
||||
subscription.planCode.includes('professional') ||
|
||||
@@ -308,6 +343,7 @@ module.exports = {
|
||||
isUserPartOfGroup: callbackify(isUserPartOfGroup),
|
||||
getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
|
||||
upgradeGroupPlan: callbackify(upgradeGroupPlan),
|
||||
checkBillingInfoExistence: callbackify(checkBillingInfoExistence),
|
||||
promises: {
|
||||
removeUserFromGroup,
|
||||
replaceUserReferencesInGroups,
|
||||
@@ -320,7 +356,9 @@ module.exports = {
|
||||
getUsersGroupSubscriptionDetails,
|
||||
previewAddSeatsSubscriptionChange,
|
||||
createAddSeatsSubscriptionChange,
|
||||
updateSubscriptionPaymentTerms,
|
||||
getGroupPlanUpgradePreview,
|
||||
upgradeGroupPlan,
|
||||
checkBillingInfoExistence,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ const MAX_NUMBER_OF_USERS = 20
|
||||
const addSeatsValidateSchema = {
|
||||
body: Joi.object({
|
||||
adding: Joi.number().integer().min(1).max(MAX_NUMBER_OF_USERS).required(),
|
||||
poNumber: Joi.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -90,6 +91,7 @@ export default {
|
||||
validate({
|
||||
body: Joi.object({
|
||||
adding: Joi.number().integer().min(MAX_NUMBER_OF_USERS).required(),
|
||||
poNumber: Joi.string(),
|
||||
}),
|
||||
}),
|
||||
RateLimiterMiddleware.rateLimit(subscriptionRateLimiter),
|
||||
|
||||
@@ -4,11 +4,11 @@ block entrypointVar
|
||||
- entrypoint = 'pages/user/subscription/group-management/add-seats'
|
||||
|
||||
block append meta
|
||||
meta(name="ol-subscriptionData" data-type="json" content=subscriptionData)
|
||||
meta(name="ol-groupName", data-type="string", content=groupName)
|
||||
meta(name="ol-subscriptionId", data-type="string", content=subscriptionId)
|
||||
meta(name="ol-totalLicenses", data-type="number", content=totalLicenses)
|
||||
meta(name="ol-isProfessional", data-type="boolean", content=isProfessional)
|
||||
meta(name="ol-isCollectionMethodManual", data-type="boolean", content=isCollectionMethodManual)
|
||||
|
||||
block content
|
||||
main.content.content-alt#main-content
|
||||
|
||||
@@ -757,6 +757,7 @@
|
||||
"how_we_use_your_data": "",
|
||||
"how_we_use_your_data_explanation": "",
|
||||
"i_confirm_am_student": "",
|
||||
"i_want_to_add_a_po_number": "",
|
||||
"i_want_to_stay": "",
|
||||
"id": "",
|
||||
"identify_errors_with_your_compile": "",
|
||||
@@ -1226,6 +1227,7 @@
|
||||
"plus_additional_collaborators_document_history_track_changes_and_more": "",
|
||||
"plus_more": "",
|
||||
"plus_x_additional_licenses_for_a_total_of_y_licenses": "",
|
||||
"po_number": "",
|
||||
"postal_code": "",
|
||||
"premium": "",
|
||||
"premium_feature": "",
|
||||
|
||||
+16
-1
@@ -16,6 +16,7 @@ import {
|
||||
} from 'react-bootstrap-5'
|
||||
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
import Button from '@/features/ui/components/bootstrap-5/button'
|
||||
import PoNumber from '@/features/group-management/components/add-seats/po-number'
|
||||
import CostSummary from '@/features/group-management/components/add-seats/cost-summary'
|
||||
import RequestStatus from '@/features/group-management/components/request-status'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
} from '../../../../../../types/subscription/subscription-change-preview'
|
||||
import { MergeAndOverride, Nullable } from '../../../../../../types/utils'
|
||||
import { sendMB } from '../../../../infrastructure/event-tracking'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
export const MAX_NUMBER_OF_USERS = 20
|
||||
|
||||
@@ -43,8 +45,12 @@ function AddSeats() {
|
||||
const subscriptionId = getMeta('ol-subscriptionId')
|
||||
const totalLicenses = Number(getMeta('ol-totalLicenses'))
|
||||
const isProfessional = getMeta('ol-isProfessional')
|
||||
const isCollectionMethodManual = getMeta('ol-isCollectionMethodManual')
|
||||
const [addSeatsInputError, setAddSeatsInputError] = useState<string>()
|
||||
const [shouldContactSales, setShouldContactSales] = useState(false)
|
||||
const isFlexibleGroupLicensingForManuallyBilledSubscriptions = useFeatureFlag(
|
||||
'flexible-group-licensing-for-manually-billed-subscriptions'
|
||||
)
|
||||
const controller = useAbortController()
|
||||
const { signal: addSeatsSignal } = useAbortController()
|
||||
const { signal: contactSalesSignal } = useAbortController()
|
||||
@@ -151,6 +157,9 @@ function AddSeats() {
|
||||
formData.get('seats') === ''
|
||||
? undefined
|
||||
: (formData.get('seats') as string)
|
||||
const poNumber = !formData.get('po_number')
|
||||
? undefined
|
||||
: (formData.get('po_number') as string)
|
||||
|
||||
if (!(await validateSeats(rawSeats))) {
|
||||
return
|
||||
@@ -166,6 +175,7 @@ function AddSeats() {
|
||||
signal: contactSalesSignal,
|
||||
body: {
|
||||
adding: rawSeats,
|
||||
poNumber,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -176,7 +186,10 @@ function AddSeats() {
|
||||
})
|
||||
const post = postJSON('/user/subscription/group/add-users/create', {
|
||||
signal: addSeatsSignal,
|
||||
body: { adding: Number(rawSeats) },
|
||||
body: {
|
||||
adding: Number(rawSeats),
|
||||
poNumber,
|
||||
},
|
||||
})
|
||||
runAsyncAddSeats(post)
|
||||
.then(() => {
|
||||
@@ -323,6 +336,8 @@ function AddSeats() {
|
||||
<FormText type="error">{addSeatsInputError}</FormText>
|
||||
)}
|
||||
</FormGroup>
|
||||
{isFlexibleGroupLicensingForManuallyBilledSubscriptions &&
|
||||
isCollectionMethodManual && <PoNumber />}
|
||||
</div>
|
||||
<CostSummarySection
|
||||
isLoadingCostSummary={isLoadingCostSummary}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormControl, FormGroup, FormLabel } from 'react-bootstrap-5'
|
||||
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
|
||||
|
||||
function PoNumber() {
|
||||
const { t } = useTranslation()
|
||||
const [isPoNumberChecked, setIsPoNumberChecked] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup className="mt-3">
|
||||
<OLFormCheckbox
|
||||
label={t('i_want_to_add_a_po_number')}
|
||||
id="po-number-checkbox"
|
||||
checked={isPoNumberChecked}
|
||||
onChange={e => setIsPoNumberChecked(e.target.checked)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{isPoNumberChecked && (
|
||||
<FormGroup className="mt-2" controlId="po-number">
|
||||
<FormLabel>{t('po_number')}</FormLabel>
|
||||
<FormControl type="text" required className="w-25" name="po_number" />
|
||||
</FormGroup>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PoNumber
|
||||
@@ -1,5 +1,6 @@
|
||||
import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
|
||||
import AddSeats from '@/features/group-management/components/add-seats/add-seats'
|
||||
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||
|
||||
function Root() {
|
||||
const { isReady } = useWaitForI18n()
|
||||
@@ -8,7 +9,11 @@ function Root() {
|
||||
return null
|
||||
}
|
||||
|
||||
return <AddSeats />
|
||||
return (
|
||||
<SplitTestProvider>
|
||||
<AddSeats />
|
||||
</SplitTestProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Root
|
||||
|
||||
@@ -129,6 +129,7 @@ export interface Meta {
|
||||
'ol-institutionLinked': InstitutionLink | undefined
|
||||
'ol-inviteToken': string
|
||||
'ol-inviterName': string
|
||||
'ol-isCollectionMethodManual': boolean
|
||||
'ol-isExternalAuthenticationSystemUsed': boolean
|
||||
'ol-isManagedAccount': boolean
|
||||
'ol-isPaywallChangeCompileTimeoutEnabled': boolean
|
||||
|
||||
@@ -982,6 +982,7 @@
|
||||
"how_we_use_your_data_explanation": "<0>Please help us continue to improve Overleaf by answering a few quick questions. Your answers will help us and our corporate group understand more about our user base. We may use this information to improve your Overleaf experience, for example by providing personalized onboarding, upgrade prompts, help suggestions, and tailored marketing communications (if you’ve opted-in to receive them).</0><1>For more details on how we use your personal data, please see our <0>Privacy Notice</0>.</1>",
|
||||
"hundreds_templates_info": "Produce beautiful documents starting from our gallery of LaTeX templates for journals, conferences, theses, reports, CVs and much more.",
|
||||
"i_confirm_am_student": "I confirm that I am currently a student.",
|
||||
"i_want_to_add_a_po_number": "I want to add a PO number",
|
||||
"i_want_to_stay": "I want to stay",
|
||||
"id": "ID",
|
||||
"identify_errors_with_your_compile": "Identify errors with your compile",
|
||||
@@ -1632,6 +1633,7 @@
|
||||
"plus_additional_collaborators_document_history_track_changes_and_more": "(plus additional collaborators, document history, track changes, and more).",
|
||||
"plus_more": "plus more",
|
||||
"plus_x_additional_licenses_for_a_total_of_y_licenses": "Plus <0>__additionalLicenses__</0> additional license(s) for a total of <1>__count__ licenses</1>",
|
||||
"po_number": "PO Number",
|
||||
"popular_tags": "Popular Tags",
|
||||
"portal_add_affiliation_to_join": "It looks like you are already logged in to __appName__. If you have a __portalTitle__ email you can add it now.",
|
||||
"position": "Position",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AddSeats, {
|
||||
MAX_NUMBER_OF_USERS,
|
||||
} from '@/features/group-management/components/add-seats/add-seats'
|
||||
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||
|
||||
describe('<AddSeats />', function () {
|
||||
beforeEach(function () {
|
||||
@@ -11,9 +12,17 @@ describe('<AddSeats />', function () {
|
||||
win.metaAttributesCache.set('ol-subscriptionId', '123')
|
||||
win.metaAttributesCache.set('ol-totalLicenses', this.totalLicenses)
|
||||
win.metaAttributesCache.set('ol-isProfessional', false)
|
||||
win.metaAttributesCache.set('ol-isCollectionMethodManual', true)
|
||||
win.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'flexible-group-licensing-for-manually-billed-subscriptions': 'enabled',
|
||||
})
|
||||
})
|
||||
|
||||
cy.mount(<AddSeats />)
|
||||
cy.mount(
|
||||
<SplitTestProvider>
|
||||
<AddSeats />
|
||||
</SplitTestProvider>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: /buy licenses/i })
|
||||
cy.findByTestId('add-more-users-group-form')
|
||||
@@ -68,6 +77,28 @@ describe('<AddSeats />', function () {
|
||||
)
|
||||
})
|
||||
|
||||
describe('PO number', function () {
|
||||
it('should not render the PO checkbox and PO input if collection method is not manual', function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-isCollectionMethodManual', false)
|
||||
})
|
||||
cy.mount(
|
||||
<SplitTestProvider>
|
||||
<AddSeats />
|
||||
</SplitTestProvider>
|
||||
)
|
||||
|
||||
cy.findByLabelText(/i want to add a po number/i).should('not.exist')
|
||||
cy.findByLabelText(/^po number$/i).should('not.exist')
|
||||
})
|
||||
|
||||
it('should check the PO checkbox in order to activate the PO input field', function () {
|
||||
cy.findByLabelText(/^po number$/i).should('not.exist')
|
||||
cy.findByLabelText(/i want to add a po number/i).check()
|
||||
cy.findByLabelText(/^po number$/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('"Upgrade my plan" link', function () {
|
||||
it('shows the link', function () {
|
||||
cy.findByRole('link', { name: /upgrade my plan/i }).should(
|
||||
@@ -82,7 +113,11 @@ describe('<AddSeats />', function () {
|
||||
win.metaAttributesCache.set('ol-isProfessional', true)
|
||||
})
|
||||
|
||||
cy.mount(<AddSeats />)
|
||||
cy.mount(
|
||||
<SplitTestProvider>
|
||||
<AddSeats />
|
||||
</SplitTestProvider>
|
||||
)
|
||||
|
||||
cy.findByRole('link', { name: /upgrade my plan/i }).should('not.exist')
|
||||
})
|
||||
@@ -325,16 +360,23 @@ describe('<AddSeats />', function () {
|
||||
})
|
||||
|
||||
function makeRequest(statusCode: number, adding: string) {
|
||||
const PO_NUMBER = 'PO123456789'
|
||||
cy.intercept('POST', '/user/subscription/group/add-users/create', {
|
||||
statusCode,
|
||||
}).as('addUsersRequest')
|
||||
cy.get('@input').type(adding)
|
||||
cy.findByLabelText(/i want to add a po number/i).check()
|
||||
cy.findByLabelText(/^po number$/i).type(PO_NUMBER)
|
||||
cy.get('@addUsersBtn').click()
|
||||
|
||||
const body = {
|
||||
adding: Number(adding),
|
||||
poNumber: PO_NUMBER,
|
||||
}
|
||||
cy.get('@addUsersRequest')
|
||||
.its('request.body')
|
||||
.should('deep.equal', {
|
||||
adding: Number(adding),
|
||||
})
|
||||
.should('deep.equal', body)
|
||||
.and('have.keys', Object.keys(body))
|
||||
cy.findByTestId('add-more-users-group-form').should('not.exist')
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const Errors = require('../../../../app/src/Features/Subscription/Errors')
|
||||
const {
|
||||
AI_ADD_ON_CODE,
|
||||
PaymentProviderSubscriptionChangeRequest,
|
||||
PaymentProviderSubscriptionUpdateRequest,
|
||||
PaymentProviderSubscriptionChange,
|
||||
PaymentProviderSubscription,
|
||||
PaymentProviderSubscriptionAddOnUpdate,
|
||||
@@ -307,6 +308,36 @@ describe('PaymentProviderEntities', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRequestForPoNumberAndTermsAndConditionsUpdate()', function () {
|
||||
it('returns a correct update request', function () {
|
||||
const updateRequest =
|
||||
this.subscription.getRequestForPoNumberAndTermsAndConditionsUpdate(
|
||||
'O12345',
|
||||
'T&C copy'
|
||||
)
|
||||
expect(updateRequest).to.deep.equal(
|
||||
new PaymentProviderSubscriptionUpdateRequest({
|
||||
subscription: this.subscription,
|
||||
poNumber: 'O12345',
|
||||
termsAndConditions: 'T&C copy',
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRequestForTermsAndConditionsUpdate()', function () {
|
||||
it('returns a correct update request', function () {
|
||||
const updateRequest =
|
||||
this.subscription.getRequestForTermsAndConditionsUpdate('T&C copy')
|
||||
expect(updateRequest).to.deep.equal(
|
||||
new PaymentProviderSubscriptionUpdateRequest({
|
||||
subscription: this.subscription,
|
||||
termsAndConditions: 'T&C copy',
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without add-ons', function () {
|
||||
beforeEach(function () {
|
||||
const { PaymentProviderSubscription } = this.PaymentProviderEntities
|
||||
@@ -381,6 +412,8 @@ describe('PaymentProviderEntities', function () {
|
||||
periodStart: new Date(),
|
||||
periodEnd: new Date(),
|
||||
collectionMethod: 'automatic',
|
||||
poNumber: '012345',
|
||||
termsAndConditions: 'T&C copy',
|
||||
})
|
||||
const change = new PaymentProviderSubscriptionChange({
|
||||
subscription,
|
||||
|
||||
@@ -5,6 +5,7 @@ const SandboxedModule = require('sandboxed-module')
|
||||
const {
|
||||
PaymentProviderSubscription,
|
||||
PaymentProviderSubscriptionChangeRequest,
|
||||
PaymentProviderSubscriptionUpdateRequest,
|
||||
PaymentProviderSubscriptionAddOnUpdate,
|
||||
PaymentProviderAccount,
|
||||
PaymentProviderCoupon,
|
||||
@@ -57,6 +58,8 @@ describe('RecurlyClient', function () {
|
||||
periodStart: new Date(),
|
||||
periodEnd: new Date(),
|
||||
collectionMethod: 'automatic',
|
||||
poNumber: '',
|
||||
termsAndConditions: '',
|
||||
})
|
||||
|
||||
this.recurlySubscription = {
|
||||
@@ -87,6 +90,8 @@ describe('RecurlyClient', function () {
|
||||
currentPeriodStartedAt: this.subscription.periodStart,
|
||||
currentPeriodEndsAt: this.subscription.periodEnd,
|
||||
collectionMethod: this.subscription.collectionMethod,
|
||||
poNumber: this.subscription.poNumber,
|
||||
termsAndConditions: this.subscription.termsAndConditions,
|
||||
}
|
||||
|
||||
this.recurlySubscriptionChange = new recurly.SubscriptionChange()
|
||||
@@ -444,6 +449,37 @@ describe('RecurlyClient', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSubscriptionDetails', function () {
|
||||
beforeEach(function () {
|
||||
this.client.updateSubscription = sinon
|
||||
.stub()
|
||||
.resolves({ id: this.subscription.id })
|
||||
})
|
||||
|
||||
it('handles subscription update', async function () {
|
||||
await this.RecurlyClient.promises.updateSubscriptionDetails(
|
||||
new PaymentProviderSubscriptionUpdateRequest({
|
||||
subscription: this.subscription,
|
||||
poNumber: '012345',
|
||||
termsAndConditions: 'T&C',
|
||||
})
|
||||
)
|
||||
expect(this.client.updateSubscription).to.be.calledWith(
|
||||
'uuid-subscription-id',
|
||||
{ poNumber: '012345', termsAndConditions: 'T&C' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw any API errors', async function () {
|
||||
this.client.updateSubscription = sinon.stub().throws()
|
||||
await expect(
|
||||
this.RecurlyClient.promises.updateSubscriptionDetails({
|
||||
subscription: this.subscription,
|
||||
})
|
||||
).to.eventually.be.rejectedWith(Error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeSubscriptionChange', function () {
|
||||
beforeEach(function () {
|
||||
this.client.removeSubscriptionChange = sinon.stub().resolves()
|
||||
@@ -654,4 +690,29 @@ describe('RecurlyClient', function () {
|
||||
).to.be.rejectedWith(Error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCountryCode', function () {
|
||||
it('should return the country code from the account info', async function () {
|
||||
this.client.getAccount = sinon.stub().resolves({
|
||||
address: {
|
||||
country: 'GB',
|
||||
},
|
||||
})
|
||||
const countryCode = await this.RecurlyClient.promises.getCountryCode(
|
||||
this.user._id
|
||||
)
|
||||
expect(countryCode).to.equal('GB')
|
||||
})
|
||||
|
||||
it('should throw if country code doesn’t exist', async function () {
|
||||
this.client.getAccount = sinon.stub().resolves({
|
||||
address: {
|
||||
country: '',
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
this.RecurlyClient.promises.getCountryCode(this.user._id)
|
||||
).to.be.rejectedWith(Error, 'Country code not found')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,6 +34,12 @@ describe('SubscriptionGroupController', function () {
|
||||
canUseFlexibleLicensing: true,
|
||||
}
|
||||
|
||||
this.recurlySubscription = {
|
||||
get isCollectionMethodManual() {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
this.previewSubscriptionChangeData = {
|
||||
change: {},
|
||||
currency: 'USD',
|
||||
@@ -41,13 +47,15 @@ describe('SubscriptionGroupController', function () {
|
||||
|
||||
this.createSubscriptionChangeData = { adding: 1 }
|
||||
|
||||
this.paymentMethod = { cardType: 'Visa', lastFour: '1111' }
|
||||
|
||||
this.SubscriptionGroupHandler = {
|
||||
promises: {
|
||||
removeUserFromGroup: sinon.stub().resolves(),
|
||||
getUsersGroupSubscriptionDetails: sinon.stub().resolves({
|
||||
subscription: this.subscription,
|
||||
plan: this.plan,
|
||||
recurlySubscription: {},
|
||||
recurlySubscription: this.recurlySubscription,
|
||||
}),
|
||||
previewAddSeatsSubscriptionChange: sinon
|
||||
.stub()
|
||||
@@ -62,6 +70,8 @@ describe('SubscriptionGroupController', function () {
|
||||
getGroupPlanUpgradePreview: sinon
|
||||
.stub()
|
||||
.resolves(this.previewSubscriptionChangeData),
|
||||
checkBillingInfoExistence: sinon.stub().resolves(this.paymentMethod),
|
||||
updateSubscriptionPaymentTerms: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,7 +106,7 @@ describe('SubscriptionGroupController', function () {
|
||||
|
||||
this.SplitTestHandler = {
|
||||
promises: {
|
||||
getAssignment: sinon.stub().resolves(),
|
||||
getAssignment: sinon.stub().resolves({ variant: 'enabled' }),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -111,7 +121,6 @@ describe('SubscriptionGroupController', function () {
|
||||
this.RecurlyClient = {
|
||||
promises: {
|
||||
getPaymentMethod: sinon.stub().resolves(this.paymentMethod),
|
||||
// getSubscription: sinon.stub().resolves(this.subscription),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -355,14 +364,21 @@ describe('SubscriptionGroupController', function () {
|
||||
this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled
|
||||
.calledWith(this.plan)
|
||||
.should.equal(true)
|
||||
this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges
|
||||
.calledWith(this.recurlySubscription)
|
||||
.should.equal(true)
|
||||
this.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive
|
||||
.calledWith(this.subscription)
|
||||
.should.equal(true)
|
||||
this.SubscriptionGroupHandler.promises.checkBillingInfoExistence
|
||||
.calledWith(this.recurlySubscription, this.adminUserId)
|
||||
.should.equal(true)
|
||||
page.should.equal('subscriptions/add-seats')
|
||||
props.subscriptionId.should.equal(this.subscriptionId)
|
||||
props.groupName.should.equal(this.subscription.teamName)
|
||||
props.totalLicenses.should.equal(this.subscription.membersLimit)
|
||||
props.isProfessional.should.equal(false)
|
||||
props.isCollectionMethodManual.should.equal(true)
|
||||
done()
|
||||
},
|
||||
}
|
||||
@@ -399,7 +415,7 @@ describe('SubscriptionGroupController', function () {
|
||||
})
|
||||
|
||||
it('should redirect to missing billing information page when billing information is missing', function (done) {
|
||||
this.RecurlyClient.promises.getPaymentMethod = sinon
|
||||
this.SubscriptionGroupHandler.promises.checkBillingInfoExistence = sinon
|
||||
.stub()
|
||||
.throws(new this.Errors.MissingBillingInfoError())
|
||||
|
||||
@@ -415,22 +431,6 @@ describe('SubscriptionGroupController', function () {
|
||||
this.Controller.addSeatsToGroupSubscription(this.req, res)
|
||||
})
|
||||
|
||||
it('should redirect to manually collected subscription error page when collection method is manual', function (done) {
|
||||
this.SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual =
|
||||
sinon.stub().throws(new this.Errors.ManuallyCollectedError())
|
||||
|
||||
const res = {
|
||||
redirect: url => {
|
||||
url.should.equal(
|
||||
'/user/subscription/group/manually-collected-subscription'
|
||||
)
|
||||
done()
|
||||
},
|
||||
}
|
||||
|
||||
this.Controller.addSeatsToGroupSubscription(this.req, res)
|
||||
})
|
||||
|
||||
it('should redirect to subscription page when there is a pending change', function (done) {
|
||||
this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges =
|
||||
sinon.stub().throws(new this.Errors.PendingChangeError())
|
||||
@@ -586,10 +586,16 @@ describe('SubscriptionGroupController', function () {
|
||||
describe('submitForm', function () {
|
||||
it('should build and pass the request body to the sales submit handler', function (done) {
|
||||
const adding = 100
|
||||
this.req.body = { adding }
|
||||
const poNumber = 'PO123456'
|
||||
this.req.body = { adding, poNumber }
|
||||
|
||||
const res = {
|
||||
sendStatus: code => {
|
||||
this.SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms(
|
||||
this.adminUserId,
|
||||
this.recurlySubscription,
|
||||
poNumber
|
||||
)
|
||||
this.Modules.promises.hooks.fire
|
||||
.calledWith('sendSupportRequest', {
|
||||
email: this.user.email,
|
||||
@@ -602,6 +608,8 @@ describe('SubscriptionGroupController', function () {
|
||||
'\n' +
|
||||
`**Estimated Number of Users:** ${adding}\n` +
|
||||
'\n' +
|
||||
`**PO Number:** ${poNumber}\n` +
|
||||
'\n' +
|
||||
`**Message:** This email has been generated on behalf of user with email **${this.user.email}** to request an increase in the total number of users for their subscription.`,
|
||||
inbox: 'sales',
|
||||
})
|
||||
|
||||
@@ -36,6 +36,15 @@ describe('SubscriptionGroupHandler', function () {
|
||||
},
|
||||
}
|
||||
|
||||
this.termsAndConditionsUpdate = {
|
||||
termsAndConditions: 'T&C copy',
|
||||
}
|
||||
|
||||
this.poNumberAndTermsAndConditionsUpdate = {
|
||||
poNumber: '4444',
|
||||
...this.termsAndConditionsUpdate,
|
||||
}
|
||||
|
||||
this.recurlySubscription = {
|
||||
id: 123,
|
||||
addOns: [
|
||||
@@ -50,10 +59,19 @@ describe('SubscriptionGroupHandler', function () {
|
||||
getRequestForFlexibleLicensingGroupPlanUpgrade: sinon
|
||||
.stub()
|
||||
.returns(this.changeRequest),
|
||||
getRequestForPoNumberAndTermsAndConditionsUpdate: sinon
|
||||
.stub()
|
||||
.returns(this.poNumberAndTermsAndConditionsUpdate),
|
||||
getRequestForTermsAndConditionsUpdate: sinon
|
||||
.stub()
|
||||
.returns(this.termsAndConditionsUpdate),
|
||||
currency: 'USD',
|
||||
hasAddOn(code) {
|
||||
return this.addOns.some(addOn => addOn.code === code)
|
||||
},
|
||||
get isCollectionMethodManual() {
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
this.SubscriptionLocator = {
|
||||
@@ -119,6 +137,8 @@ describe('SubscriptionGroupHandler', function () {
|
||||
applySubscriptionChangeRequest: sinon
|
||||
.stub()
|
||||
.resolves(this.applySubscriptionChange),
|
||||
getCountryCode: sinon.stub().resolves('BG'),
|
||||
updateSubscriptionDetails: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -155,6 +175,19 @@ describe('SubscriptionGroupHandler', function () {
|
||||
},
|
||||
}
|
||||
|
||||
this.Modules = {
|
||||
promises: {
|
||||
hooks: {
|
||||
fire: sinon.stub().callsFake(hookName => {
|
||||
if (hookName === 'generateTermsAndConditions') {
|
||||
return Promise.resolve(['T&Cs'])
|
||||
}
|
||||
return Promise.resolve()
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
this.Handler = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'./SubscriptionUpdater': this.SubscriptionUpdater,
|
||||
@@ -169,6 +202,7 @@ describe('SubscriptionGroupHandler', function () {
|
||||
'./PaymentProviderEntities': this.PaymentProviderEntities,
|
||||
'../Authentication/SessionManager': this.SessionManager,
|
||||
'./GroupPlansData': this.GroupPlansData,
|
||||
'../../infrastructure/Modules': this.Modules,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -398,8 +432,7 @@ describe('SubscriptionGroupHandler', function () {
|
||||
prevQuantity: this.prevQuantity,
|
||||
},
|
||||
},
|
||||
this.previewSubscriptionChange,
|
||||
this.paymentMethod
|
||||
this.previewSubscriptionChange
|
||||
)
|
||||
.should.equal(true)
|
||||
preview.should.equal(this.changePreview)
|
||||
@@ -408,12 +441,30 @@ describe('SubscriptionGroupHandler', function () {
|
||||
|
||||
describe('createAddSeatsSubscriptionChange', function () {
|
||||
it('should change the subscription', async function () {
|
||||
this.recurlySubscription = {
|
||||
...this.recurlySubscription,
|
||||
get isCollectionMethodManual() {
|
||||
return true
|
||||
},
|
||||
}
|
||||
this.RecurlyClient.promises.getSubscription = sinon
|
||||
.stub()
|
||||
.resolves(this.recurlySubscription)
|
||||
|
||||
const result =
|
||||
await this.Handler.promises.createAddSeatsSubscriptionChange(
|
||||
this.adminUser_id,
|
||||
this.adding
|
||||
this.adding,
|
||||
'123'
|
||||
)
|
||||
|
||||
this.RecurlyClient.promises.updateSubscriptionDetails
|
||||
.calledWith(
|
||||
sinon.match
|
||||
.has('poNumber')
|
||||
.and(sinon.match.has('termsAndConditions'))
|
||||
)
|
||||
.should.equal(true)
|
||||
this.RecurlyClient.promises.applySubscriptionChangeRequest
|
||||
.calledWith(this.changeRequest)
|
||||
.should.equal(true)
|
||||
@@ -430,6 +481,48 @@ describe('SubscriptionGroupHandler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSubscriptionPaymentTerms', function () {
|
||||
describe('accounts with PO number', function () {
|
||||
it('should update the subscription PO number and T&C', async function () {
|
||||
this.RecurlyClient.promises.getCountryCode = sinon
|
||||
.stub()
|
||||
.resolves('GB')
|
||||
await this.Handler.promises.updateSubscriptionPaymentTerms(
|
||||
this.adminUser_id,
|
||||
this.recurlySubscription,
|
||||
this.poNumberAndTermsAndConditionsUpdate.poNumber
|
||||
)
|
||||
this.recurlySubscription.getRequestForPoNumberAndTermsAndConditionsUpdate
|
||||
.calledWithMatch(
|
||||
this.poNumberAndTermsAndConditionsUpdate.poNumber,
|
||||
'T&Cs'
|
||||
)
|
||||
.should.equal(true)
|
||||
this.RecurlyClient.promises.updateSubscriptionDetails
|
||||
.calledWith(this.poNumberAndTermsAndConditionsUpdate)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('accounts with no PO number', function () {
|
||||
it('should update the subscription T&C only', async function () {
|
||||
this.RecurlyClient.promises.getCountryCode = sinon
|
||||
.stub()
|
||||
.resolves('GB')
|
||||
await this.Handler.promises.updateSubscriptionPaymentTerms(
|
||||
this.adminUser_id,
|
||||
this.recurlySubscription
|
||||
)
|
||||
this.recurlySubscription.getRequestForTermsAndConditionsUpdate
|
||||
.calledWithMatch('T&Cs')
|
||||
.should.equal(true)
|
||||
this.RecurlyClient.promises.updateSubscriptionDetails
|
||||
.calledWith(this.termsAndConditionsUpdate)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('has no "additional-license" add-on', function () {
|
||||
beforeEach(function () {
|
||||
this.recurlySubscription.addOns = []
|
||||
@@ -474,8 +567,7 @@ describe('SubscriptionGroupHandler', function () {
|
||||
prevQuantity: this.prevQuantity,
|
||||
},
|
||||
},
|
||||
this.previewSubscriptionChange,
|
||||
this.paymentMethod
|
||||
this.previewSubscriptionChange
|
||||
)
|
||||
.should.equal(true)
|
||||
preview.should.equal(this.changePreview)
|
||||
@@ -743,4 +835,30 @@ describe('SubscriptionGroupHandler', function () {
|
||||
result.should.equal(this.changePreview)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkBillingInfoExistence', function () {
|
||||
it('should invoke the payment method function when collection method is "automatic"', async function () {
|
||||
await this.Handler.promises.checkBillingInfoExistence(
|
||||
this.recurlySubscription,
|
||||
this.adminUser_id
|
||||
)
|
||||
this.RecurlyClient.promises.getPaymentMethod
|
||||
.calledWith(this.adminUser_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('shouldn’t invoke the payment method function when collection method is "manual"', async function () {
|
||||
const recurlySubscription = {
|
||||
...this.recurlySubscription,
|
||||
get isCollectionMethodManual() {
|
||||
return true
|
||||
},
|
||||
}
|
||||
await this.Handler.promises.checkBillingInfoExistence(
|
||||
recurlySubscription,
|
||||
this.adminUser_id
|
||||
)
|
||||
this.RecurlyClient.promises.getPaymentMethod.should.not.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type SubscriptionChangePreview = {
|
||||
change: SubscriptionChangeDescription
|
||||
currency: string
|
||||
paymentMethod: string
|
||||
paymentMethod: string | undefined
|
||||
nextPlan: {
|
||||
annual: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user