Parcourir la source

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

Redirect bundle purchases when users already have it

GitOrigin-RevId: d8e3c0256db08c08c2be24f38caef91fb26b90e8
Jimmy Domagala-Tang il y a 1 an
Parent
commit
f7f4a03abb

+ 10 - 0
services/web/app/src/Features/Subscription/FeaturesUpdater.js

@@ -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,
   },
 }

+ 17 - 0
services/web/app/src/Features/Subscription/RecurlyEntities.js

@@ -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,
 }

+ 12 - 6
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -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
   }

+ 2 - 0
services/web/frontend/extracted-translations.json

@@ -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": "",

+ 22 - 0
services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx

@@ -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} />
       )}

+ 2 - 0
services/web/locales/en.json

@@ -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",