Merge pull request #24523 from overleaf/jdt-prevent-bundle-dbl-buys

Redirect bundle purchases when users already have it

GitOrigin-RevId: d8e3c0256db08c08c2be24f38caef91fb26b90e8
This commit is contained in:
Jimmy Domagala-Tang
2025-04-09 12:29:19 -04:00
committed by Copybot
parent f11a6a6b87
commit f7f4a03abb
6 changed files with 65 additions and 6 deletions

View File

@@ -197,6 +197,14 @@ async function doSyncFromV1(v1UserId) {
return refreshFeatures(user._id, 'sync-v1')
}
async function hasFeaturesViaWritefull(userId) {
const user = await UserGetter.promises.getUser(userId, {
_id: 1,
writefull: 1,
})
return Boolean(user?.writefull?.isPremium)
}
module.exports = {
featuresEpochIsCurrent,
computeFeatures: callbackify(computeFeatures),
@@ -209,10 +217,12 @@ module.exports = {
'featuresChanged',
]),
scheduleRefreshFeatures: callbackify(scheduleRefreshFeatures),
hasFeaturesViaWritefull: callbackify(hasFeaturesViaWritefull),
promises: {
computeFeatures,
refreshFeatures,
scheduleRefreshFeatures,
doSyncFromV1,
hasFeaturesViaWritefull,
},
}

View File

@@ -477,6 +477,22 @@ function isStandaloneAiAddOnPlanCode(planCode) {
return STANDALONE_AI_ADD_ON_CODES.includes(planCode)
}
/**
* Returns whether subscription change will have have the ai bundle once the change is processed
*
* @param {RecurlySubscriptionChange} subscriptionChange The subscription change object coming from Recurly
*
* @return {boolean}
*/
function subscriptionChangeIsAiAssistUpgrade(subscriptionChange) {
return Boolean(
isStandaloneAiAddOnPlanCode(subscriptionChange.nextPlanCode) ||
subscriptionChange.nextAddOns?.some(
addOn => addOn.code === AI_ADD_ON_CODE
)
)
}
module.exports = {
AI_ADD_ON_CODE,
MEMBERS_LIMIT_ADD_ON_CODE,
@@ -493,5 +509,6 @@ module.exports = {
RecurlyCoupon,
RecurlyAccount,
isStandaloneAiAddOnPlanCode,
subscriptionChangeIsAiAssistUpgrade,
RecurlyImmediateCharge,
}

View File

@@ -24,6 +24,7 @@ const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const RecurlyClient = require('./RecurlyClient')
const { AI_ADD_ON_CODE } = require('./RecurlyEntities')
const PlansLocator = require('./PlansLocator')
const RecurlyEntities = require('./RecurlyEntities')
/**
* @import { SubscriptionChangeDescription } from '../../../../types/subscription/subscription-change-preview'
@@ -45,7 +46,6 @@ function formatGroupPlansDataForDash() {
async function userSubscriptionPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
await SplitTestHandler.promises.getAssignment(req, res, 'pause-subscription')
const groupPricingDiscount = await SplitTestHandler.promises.getAssignment(
@@ -321,13 +321,19 @@ async function previewAddonPurchase(req, res) {
try {
subscriptionChange =
await SubscriptionHandler.promises.previewAddonPurchase(userId, addOnCode)
const hasBundleViaWritefull =
await FeaturesUpdater.promises.hasFeaturesViaWritefull(userId)
const isAiUpgrade =
RecurlyEntities.subscriptionChangeIsAiAssistUpgrade(subscriptionChange)
if (hasBundleViaWritefull && isAiUpgrade) {
return res.redirect(
'/user/subscription?redirect-reason=writefull-entitled'
)
}
} catch (err) {
if (err instanceof DuplicateAddOnError) {
return HttpErrorHandler.badRequest(
req,
res,
`Subscription already has add-on "${addOnCode}"`
)
return res.redirect('/user/subscription?redirect-reason=double-buy')
}
throw err
}

View File

@@ -670,6 +670,8 @@
"go_to_pdf_location_in_code": "",
"go_to_settings": "",
"go_to_subscriptions": "",
"good_news_you_already_purchased_this_add_on": "",
"good_news_you_are_already_receiving_this_add_on_via_writefull": "",
"group_admin": "",
"group_invitations": "",
"group_invite_has_been_sent_to_email": "",

View File

@@ -33,6 +33,27 @@ function PastDueSubscriptionAlert({
)
}
function RedirectAlerts() {
const queryParams = new URLSearchParams(window.location.search)
const redirectReason = queryParams.get('redirect-reason')
const { t } = useTranslation()
if (!redirectReason) {
return null
}
let warning
if (redirectReason === 'writefull-entitled') {
warning = t('good_news_you_are_already_receiving_this_add_on_via_writefull')
} else if (redirectReason === 'double-buy') {
warning = t('good_news_you_already_purchased_this_add_on')
} else {
return null
}
return <OLNotification type="warning" content={<>{warning}</>} />
}
function PersonalSubscriptionStates({
subscription,
}: {
@@ -75,6 +96,7 @@ function PersonalSubscription() {
return (
<>
<RedirectAlerts />
{personalSubscription.recurly.hasPastDueInvoice && (
<PastDueSubscriptionAlert subscription={personalSubscription} />
)}

View File

@@ -884,6 +884,8 @@
"go_to_previous_page": "Go to previous page",
"go_to_settings": "Go to settings",
"go_to_subscriptions": "Go to Subscriptions",
"good_news_you_already_purchased_this_add_on": "Good news! You already have this add-on, so no need to pay again.",
"good_news_you_are_already_receiving_this_add_on_via_writefull": "Good news! You already have this add-on via your Writefull subscription. No need to pay again.",
"great_for_getting_started": "Great for getting started",
"great_for_small_teams_and_departments": "Great for small teams and departments",
"group": "Group",