Эх сурвалжийг харах

Merge pull request #26014 from overleaf/kh-remaining-references-to-recurly-fields

[web] update remaining references to `recurlyStatus` and `recurlySubscription_id`

GitOrigin-RevId: f5e905eba598cfcd146803c6ccc36a2304021544
Kristina 1 жил өмнө
parent
commit
7a449f4686
30 өөрчлөгдсөн 476 нэмэгдсэн , 146 устгасан
  1. 4 4
      services/web/app/src/Features/Project/ProjectController.js
  2. 7 6
      services/web/app/src/Features/Project/ProjectListController.mjs
  3. 5 1
      services/web/app/src/Features/Subscription/FeaturesUpdater.js
  4. 3 1
      services/web/app/src/Features/Subscription/SubscriptionController.js
  5. 2 1
      services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js
  6. 13 8
      services/web/app/src/Features/Subscription/SubscriptionHandler.js
  7. 59 0
      services/web/app/src/Features/Subscription/SubscriptionHelper.js
  8. 13 21
      services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
  9. 6 7
      services/web/app/src/Features/Subscription/TeamInvitesController.mjs
  10. 1 1
      services/web/app/views/project/list-react.pug
  11. 1 1
      services/web/app/views/subscriptions/team/invite.pug
  12. 2 1
      services/web/frontend/js/features/project-list/components/current-plan-widget/current-plan-widget.tsx
  13. 4 4
      services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/hooks/use-group-invitation-notification.tsx
  14. 14 0
      services/web/frontend/js/features/project-list/util/user.ts
  15. 4 4
      services/web/frontend/js/features/subscription/components/group-invite/group-invite.tsx
  16. 1 1
      services/web/frontend/js/utils/meta.ts
  17. 1 1
      services/web/frontend/stories/project-list/notifications.stories.tsx
  18. 1 4
      services/web/test/frontend/components/project-list/notifications/group-invitation.spec.tsx
  19. 1 1
      services/web/test/frontend/features/project-list/components/notifications.test.tsx
  20. 2 8
      services/web/test/frontend/features/subscription/components/group-invite/group-invite.test.tsx
  21. 0 12
      services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
  22. 0 3
      services/web/test/unit/src/Project/ProjectControllerTests.js
  23. 2 2
      services/web/test/unit/src/Subscription/SubscriptionControllerTests.js
  24. 2 0
      services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js
  25. 202 0
      services/web/test/unit/src/Subscription/SubscriptionHelperTests.js
  26. 116 47
      services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js
  27. 4 4
      services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs
  28. 5 1
      services/web/types/project/dashboard/subscription.ts
  29. 0 1
      services/web/types/subscription/dashboard/subscription.ts
  30. 1 1
      services/web/types/user.ts

+ 4 - 4
services/web/app/src/Features/Project/ProjectController.js

@@ -14,6 +14,7 @@ const ProjectHelper = require('./ProjectHelper')
 const metrics = require('@overleaf/metrics')
 const { User } = require('../../models/User')
 const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
+const { isPaidSubscription } = require('../Subscription/SubscriptionHelper')
 const LimitationsManager = require('../Subscription/LimitationsManager')
 const Settings = require('@overleaf/settings')
 const AuthorizationManager = require('../Authorization/AuthorizationManager')
@@ -655,12 +656,11 @@ const _ProjectController = {
         }
       }
 
-      const hasNonRecurlySubscription =
-        subscription && !subscription.recurlySubscription_id
+      const hasPaidSubscription = isPaidSubscription(subscription)
       const hasManuallyCollectedSubscription =
         subscription?.collectionMethod === 'manual'
       const canPurchaseAddons = !(
-        hasNonRecurlySubscription || hasManuallyCollectedSubscription
+        hasPaidSubscription || hasManuallyCollectedSubscription
       )
       const assistantDisabled = user.aiErrorAssistant?.enabled === false // the assistant has been manually disabled by the user
       const canUseErrorAssistant =
@@ -792,7 +792,7 @@ const _ProjectController = {
           referal_id: user.referal_id,
           signUpDate: user.signUpDate,
           allowedFreeTrial,
-          hasRecurlySubscription: subscription?.recurlySubscription_id != null,
+          hasPaidSubscription,
           featureSwitches: user.featureSwitches,
           features: fullFeatureSet,
           featureUsage,

+ 7 - 6
services/web/app/src/Features/Project/ProjectListController.mjs

@@ -26,6 +26,7 @@ import GeoIpLookup from '../../infrastructure/GeoIpLookup.js'
 import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
 import SplitTestSessionHandler from '../SplitTests/SplitTestSessionHandler.js'
 import TutorialHandler from '../Tutorial/TutorialHandler.js'
+import SubscriptionHelper from '../Subscription/SubscriptionHelper.js'
 
 /**
  * @import { GetProjectsRequest, GetProjectsResponse, AllUsersProjects, MongoProject } from "./types"
@@ -388,13 +389,13 @@ async function projectListPage(req, res, next) {
     }
   }
 
-  let hasIndividualRecurlySubscription = false
+  let hasIndividualPaidSubscription = false
 
   try {
-    hasIndividualRecurlySubscription =
-      usersIndividualSubscription?.groupPlan === false &&
-      usersIndividualSubscription?.recurlyStatus?.state !== 'canceled' &&
-      usersIndividualSubscription?.recurlySubscription_id !== ''
+    hasIndividualPaidSubscription =
+      SubscriptionHelper.isIndividualActivePaidSubscription(
+        usersIndividualSubscription
+      )
   } catch (error) {
     logger.error({ err: error }, 'Failed to get individual subscription')
   }
@@ -437,7 +438,7 @@ async function projectListPage(req, res, next) {
         groupId: subscription._id,
         groupName: subscription.teamName,
       })),
-    hasIndividualRecurlySubscription,
+    hasIndividualPaidSubscription,
     userRestrictions: Array.from(req.userRestrictions || []),
   })
 }

+ 5 - 1
services/web/app/src/Features/Subscription/FeaturesUpdater.js

@@ -3,6 +3,7 @@ const { callbackify } = require('util')
 const { callbackifyMultiResult } = require('@overleaf/promise-utils')
 const PlansLocator = require('./PlansLocator')
 const SubscriptionLocator = require('./SubscriptionLocator')
+const SubscriptionHelper = require('./SubscriptionHelper')
 const UserFeaturesUpdater = require('./UserFeaturesUpdater')
 const FeaturesHelper = require('./FeaturesHelper')
 const Settings = require('@overleaf/settings')
@@ -117,7 +118,10 @@ async function computeFeatures(userId) {
 async function _getIndividualFeatures(userId) {
   const subscription =
     await SubscriptionLocator.promises.getUsersSubscription(userId)
-  if (subscription == null || subscription?.recurlyStatus?.state === 'paused') {
+  if (
+    subscription == null ||
+    SubscriptionHelper.getPaidSubscriptionState(subscription) === 'paused'
+  ) {
     return {}
   }
 

+ 3 - 1
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -2,6 +2,7 @@
 
 const SessionManager = require('../Authentication/SessionManager')
 const SubscriptionHandler = require('./SubscriptionHandler')
+const SubscriptionHelper = require('./SubscriptionHelper')
 const SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder')
 const LimitationsManager = require('./LimitationsManager')
 const RecurlyWrapper = require('./RecurlyWrapper')
@@ -262,7 +263,8 @@ async function pauseSubscription(req, res, next) {
       {
         pause_length: pauseCycles,
         plan_code: subscription?.planCode,
-        subscriptionId: subscription?.recurlySubscription_id,
+        subscriptionId:
+          SubscriptionHelper.getPaymentProviderSubscriptionId(subscription),
       }
     )
 

+ 2 - 1
services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js

@@ -4,6 +4,7 @@ const OError = require('@overleaf/o-error')
 const SubscriptionUpdater = require('./SubscriptionUpdater')
 const SubscriptionLocator = require('./SubscriptionLocator')
 const SubscriptionController = require('./SubscriptionController')
+const SubscriptionHelper = require('./SubscriptionHelper')
 const { Subscription } = require('../../models/Subscription')
 const { User } = require('../../models/User')
 const RecurlyClient = require('./RecurlyClient')
@@ -77,7 +78,7 @@ async function ensureFlexibleLicensingEnabled(plan) {
 }
 
 async function ensureSubscriptionIsActive(subscription) {
-  if (subscription?.recurlyStatus?.state !== 'active') {
+  if (SubscriptionHelper.getPaidSubscriptionState(subscription) !== 'active') {
     throw new InactiveError('The subscription is not active', {
       subscriptionId: subscription._id.toString(),
     })

+ 13 - 8
services/web/app/src/Features/Subscription/SubscriptionHandler.js

@@ -4,6 +4,7 @@ const RecurlyWrapper = require('./RecurlyWrapper')
 const RecurlyClient = require('./RecurlyClient')
 const { User } = require('../../models/User')
 const logger = require('@overleaf/logger')
+const SubscriptionHelper = require('./SubscriptionHelper')
 const SubscriptionUpdater = require('./SubscriptionUpdater')
 const SubscriptionLocator = require('./SubscriptionLocator')
 const LimitationsManager = require('./LimitationsManager')
@@ -101,8 +102,7 @@ async function updateSubscription(user, planCode) {
   if (
     !hasSubscription ||
     subscription == null ||
-    (subscription.recurlySubscription_id == null &&
-      subscription.paymentProvider?.subscriptionId == null)
+    SubscriptionHelper.getPaymentProviderSubscriptionId(subscription) == null
   ) {
     return
   }
@@ -299,7 +299,10 @@ async function pauseSubscription(user, pauseCycles) {
   // only allow pausing on monthly plans not in a trial
   const { subscription } =
     await LimitationsManager.promises.userHasSubscription(user)
-  if (!subscription || !subscription.recurlyStatus) {
+  if (
+    !subscription ||
+    !SubscriptionHelper.getPaidSubscriptionState(subscription)
+  ) {
     throw new Error('No active subscription to pause')
   }
 
@@ -310,10 +313,9 @@ async function pauseSubscription(user, pauseCycles) {
   ) {
     throw new Error('Can only pause monthly individual plans')
   }
-  if (
-    subscription.recurlyStatus.trialEndsAt &&
-    subscription.recurlyStatus.trialEndsAt > new Date()
-  ) {
+  const trialEndsAt =
+    SubscriptionHelper.getSubscriptionTrialEndsAt(subscription)
+  if (trialEndsAt && trialEndsAt > new Date()) {
     throw new Error('Cannot pause a subscription in a trial')
   }
   if (subscription.addOns?.length) {
@@ -329,7 +331,10 @@ async function pauseSubscription(user, pauseCycles) {
 async function resumeSubscription(user) {
   const { subscription } =
     await LimitationsManager.promises.userHasSubscription(user)
-  if (!subscription || !subscription.recurlyStatus) {
+  if (
+    !subscription ||
+    !SubscriptionHelper.getPaidSubscriptionState(subscription)
+  ) {
     throw new Error('No active subscription to resume')
   }
   await RecurlyClient.promises.resumeSubscriptionByUuid(

+ 59 - 0
services/web/app/src/Features/Subscription/SubscriptionHelper.js

@@ -86,7 +86,66 @@ function generateInitialLocalizedGroupPrice(recommendedCurrency, locale) {
   }
 }
 
+function isPaidSubscription(subscription) {
+  const hasRecurlySubscription =
+    subscription?.recurlySubscription_id &&
+    subscription?.recurlySubscription_id !== ''
+  const hasStripeSubscription =
+    subscription?.paymentProvider?.subscriptionId &&
+    subscription?.paymentProvider?.subscriptionId !== ''
+  return !!(subscription && (hasRecurlySubscription || hasStripeSubscription))
+}
+
+function isIndividualActivePaidSubscription(subscription) {
+  return (
+    isPaidSubscription(subscription) &&
+    subscription?.groupPlan === false &&
+    subscription?.recurlyStatus?.state !== 'canceled' &&
+    subscription?.paymentProvider?.state !== 'canceled'
+  )
+}
+
+function getPaymentProviderSubscriptionId(subscription) {
+  if (subscription?.recurlySubscription_id) {
+    return subscription.recurlySubscription_id
+  }
+  if (subscription?.paymentProvider?.subscriptionId) {
+    return subscription.paymentProvider.subscriptionId
+  }
+  return null
+}
+
+function getPaidSubscriptionState(subscription) {
+  if (subscription?.recurlyStatus?.state) {
+    return subscription.recurlyStatus.state
+  }
+  if (subscription?.paymentProvider?.state) {
+    return subscription.paymentProvider.state
+  }
+  return null
+}
+
+function getSubscriptionTrialStartedAt(subscription) {
+  if (subscription?.recurlyStatus) {
+    return subscription.recurlyStatus?.trialStartedAt
+  }
+  return subscription?.paymentProvider?.trialStartedAt
+}
+
+function getSubscriptionTrialEndsAt(subscription) {
+  if (subscription?.recurlyStatus) {
+    return subscription.recurlyStatus?.trialEndsAt
+  }
+  return subscription?.paymentProvider?.trialEndsAt
+}
+
 module.exports = {
   shouldPlanChangeAtTermEnd,
   generateInitialLocalizedGroupPrice,
+  isPaidSubscription,
+  isIndividualActivePaidSubscription,
+  getPaymentProviderSubscriptionId,
+  getPaidSubscriptionState,
+  getSubscriptionTrialStartedAt,
+  getSubscriptionTrialEndsAt,
 }

+ 13 - 21
services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js

@@ -1,6 +1,5 @@
 // ts-check
 const Settings = require('@overleaf/settings')
-const RecurlyWrapper = require('./RecurlyWrapper')
 const PlansLocator = require('./PlansLocator')
 const {
   isStandaloneAiAddOnPlanCode,
@@ -8,7 +7,6 @@ const {
 } = require('./PaymentProviderEntities')
 const SubscriptionFormatters = require('./SubscriptionFormatters')
 const SubscriptionLocator = require('./SubscriptionLocator')
-const SubscriptionUpdater = require('./SubscriptionUpdater')
 const InstitutionsGetter = require('../Institutions/InstitutionsGetter')
 const InstitutionsManager = require('../Institutions/InstitutionsManager')
 const PublishersGetter = require('../Publishers/PublishersGetter')
@@ -227,6 +225,7 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
     // don't return subscription payment information
     delete personalSubscription.paymentProvider
     delete personalSubscription.recurly
+    delete personalSubscription.recurlySubscription_id
 
     const tax = paymentRecord.subscription.taxAmount || 0
     // Some plans allow adding more seats than the base plan provides.
@@ -374,15 +373,6 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
   }
 }
 
-/**
- * @param {{_id: string}} user
- * @returns {Promise<Subscription>}
- */
-async function getBestSubscription(user) {
-  const { bestSubscription } = await getUsersSubscriptionDetails(user)
-  return bestSubscription
-}
-
 /**
  * @param {{_id: string}} user
  * @returns {Promise<{bestSubscription:Subscription,individualSubscription:DBSubscription|null,memberGroupSubscriptions:DBSubscription[]}>}
@@ -400,15 +390,18 @@ async function getUsersSubscriptionDetails(user) {
   if (
     individualSubscription &&
     !individualSubscription.customAccount &&
-    individualSubscription.recurlySubscription_id &&
-    !individualSubscription.recurlyStatus?.state
+    SubscriptionHelper.getPaymentProviderSubscriptionId(
+      individualSubscription
+    ) &&
+    !SubscriptionHelper.getPaidSubscriptionState(individualSubscription)
   ) {
-    const recurlySubscription = await RecurlyWrapper.promises.getSubscription(
-      individualSubscription.recurlySubscription_id,
-      { includeAccount: true }
+    const paymentResults = await Modules.promises.hooks.fire(
+      'getPaymentFromRecordPromise',
+      individualSubscription
     )
-    await SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
-      recurlySubscription,
+    await Modules.promises.hooks.fire(
+      'syncSubscription',
+      paymentResults[0]?.subscription,
       individualSubscription
     )
     individualSubscription =
@@ -540,7 +533,8 @@ function _isPlanEqualOrBetter(planA, planB) {
 
 function _getRemainingTrialDays(subscription) {
   const now = new Date()
-  const trialEndDate = subscription.recurlyStatus?.trialEndsAt
+  const trialEndDate =
+    SubscriptionHelper.getSubscriptionTrialEndsAt(subscription)
   return trialEndDate && trialEndDate > now
     ? Math.ceil(
         (trialEndDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
@@ -605,10 +599,8 @@ module.exports = {
   buildUsersSubscriptionViewModel: callbackify(buildUsersSubscriptionViewModel),
   buildPlansList,
   buildPlansListForSubscriptionDash,
-  getBestSubscription: callbackify(getBestSubscription),
   promises: {
     buildUsersSubscriptionViewModel,
-    getBestSubscription,
     getUsersSubscriptionDetails,
   },
 }

+ 6 - 7
services/web/app/src/Features/Subscription/TeamInvitesController.mjs

@@ -4,6 +4,7 @@ import OError from '@overleaf/o-error'
 import TeamInvitesHandler from './TeamInvitesHandler.js'
 import SessionManager from '../Authentication/SessionManager.js'
 import SubscriptionLocator from './SubscriptionLocator.js'
+import SubscriptionHelper from './SubscriptionHelper.js'
 import ErrorController from '../Errors/ErrorController.js'
 import EmailHelper from '../Helpers/EmailHelper.js'
 import UserGetter from '../User/UserGetter.js'
@@ -87,12 +88,10 @@ async function viewInvite(req, res, next) {
     const personalSubscription =
       await SubscriptionLocator.promises.getUsersSubscription(userId)
 
-    const hasIndividualRecurlySubscription =
-      personalSubscription &&
-      personalSubscription.groupPlan === false &&
-      personalSubscription.recurlyStatus?.state !== 'canceled' &&
-      personalSubscription.recurlySubscription_id &&
-      personalSubscription.recurlySubscription_id !== ''
+    const hasIndividualPaidSubscription =
+      SubscriptionHelper.isIndividualActivePaidSubscription(
+        personalSubscription
+      )
 
     if (subscription?.managedUsersEnabled) {
       if (!subscription.populated('groupPolicy')) {
@@ -155,7 +154,7 @@ async function viewInvite(req, res, next) {
       return res.render('subscriptions/team/invite', {
         inviterName: invite.inviterName,
         inviteToken: invite.token,
-        hasIndividualRecurlySubscription,
+        hasIndividualPaidSubscription,
         expired: req.query.expired,
         userRestrictions: Array.from(req.userRestrictions || []),
         currentManagedUserAdminEmail,

+ 1 - 1
services/web/app/views/project/list-react.pug

@@ -34,7 +34,7 @@ block append meta
 	meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency)
 	meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
 	meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
-	meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription)
+	meta(name="ol-hasIndividualPaidSubscription" data-type="boolean" content=hasIndividualPaidSubscription)
 	meta(name="ol-groupSsoSetupSuccess" data-type="boolean" content=groupSsoSetupSuccess)
 	meta(name="ol-showUSGovBanner" data-type="boolean" content=showUSGovBanner)
 	meta(name="ol-usGovBannerVariant" data-type="string" content=usGovBannerVariant)

+ 1 - 1
services/web/app/views/subscriptions/team/invite.pug

@@ -4,7 +4,7 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/invite'
 
 block append meta
-	meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription)
+	meta(name="ol-hasIndividualPaidSubscription" data-type="boolean" content=hasIndividualPaidSubscription)
 	meta(name="ol-inviterName" data-type="string" content=inviterName)
 	meta(name="ol-inviteToken" data-type="string" content=inviteToken)
 	meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail)

+ 2 - 1
services/web/frontend/js/features/project-list/components/current-plan-widget/current-plan-widget.tsx

@@ -4,6 +4,7 @@ import GroupPlan from './group-plan'
 import CommonsPlan from './commons-plan'
 import PausedPlan from './paused-plan'
 import getMeta from '../../../../utils/meta'
+import { getUserSubscriptionState } from '../../util/user'
 
 function CurrentPlanWidget() {
   const usersBestSubscription = getMeta('ol-usersBestSubscription')
@@ -19,7 +20,7 @@ function CurrentPlanWidget() {
   const isCommonsPlan = type === 'commons'
   const isPaused =
     isIndividualPlan &&
-    usersBestSubscription.subscription?.recurlyStatus?.state === 'paused'
+    getUserSubscriptionState(usersBestSubscription) === 'paused'
 
   const featuresPageURL = '/learn/how-to/Overleaf_premium_features'
   const subscriptionPageUrl = '/user/subscription'

+ 4 - 4
services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/hooks/use-group-invitation-notification.tsx

@@ -57,19 +57,19 @@ export function useGroupInvitationNotification(
   const location = useLocation()
   const { handleDismiss } = useAsyncDismiss()
 
-  const hasIndividualRecurlySubscription = getMeta(
-    'ol-hasIndividualRecurlySubscription'
+  const hasIndividualPaidSubscription = getMeta(
+    'ol-hasIndividualPaidSubscription'
   )
 
   useEffect(() => {
-    if (hasIndividualRecurlySubscription) {
+    if (hasIndividualPaidSubscription) {
       setGroupInvitationStatus(
         GroupInvitationStatus.CancelIndividualSubscription
       )
     } else {
       setGroupInvitationStatus(GroupInvitationStatus.AskToJoin)
     }
-  }, [hasIndividualRecurlySubscription])
+  }, [hasIndividualPaidSubscription])
 
   const acceptGroupInvite = useCallback(() => {
     if (managedUsersEnabled) {

+ 14 - 0
services/web/frontend/js/features/project-list/util/user.ts

@@ -1,4 +1,5 @@
 import { UserRef } from '../../../../../types/project/dashboard/api'
+import { Subscription } from '../../../../../types/project/dashboard/subscription'
 import getMeta from '@/utils/meta'
 
 export function getUserName(user: UserRef) {
@@ -20,3 +21,16 @@ export function getUserName(user: UserRef) {
 
   return 'None'
 }
+
+export function getUserSubscriptionState(subscription: Subscription) {
+  if ('subscription' in subscription) {
+    if (subscription.subscription.recurlyStatus) {
+      return subscription.subscription.recurlyStatus.state
+    }
+    if (subscription.subscription.paymentProvider) {
+      return subscription.subscription.paymentProvider.state
+    }
+  }
+
+  return null
+}

+ 4 - 4
services/web/frontend/js/features/subscription/components/group-invite/group-invite.tsx

@@ -19,20 +19,20 @@ export type InviteViewTypes =
   | undefined
 
 function GroupInviteViews() {
-  const hasIndividualRecurlySubscription = getMeta(
-    'ol-hasIndividualRecurlySubscription'
+  const hasIndividualPaidSubscription = getMeta(
+    'ol-hasIndividualPaidSubscription'
   )
   const cannotJoinSubscription = getMeta('ol-cannot-join-subscription')
 
   useEffect(() => {
     if (cannotJoinSubscription) {
       setView('managed-user-cannot-join')
-    } else if (hasIndividualRecurlySubscription) {
+    } else if (hasIndividualPaidSubscription) {
       setView('cancel-personal-subscription')
     } else {
       setView('invite')
     }
-  }, [cannotJoinSubscription, hasIndividualRecurlySubscription])
+  }, [cannotJoinSubscription, hasIndividualPaidSubscription])
   const [view, setView] = useState<InviteViewTypes>(undefined)
 
   if (!view) {

+ 1 - 1
services/web/frontend/js/utils/meta.ts

@@ -127,7 +127,7 @@ export interface Meta {
   'ol-groupsAndEnterpriseBannerVariant': GroupsAndEnterpriseBannerVariant
   'ol-hasAiAssistViaWritefull': boolean
   'ol-hasGroupSSOFeature': boolean
-  'ol-hasIndividualRecurlySubscription': boolean
+  'ol-hasIndividualPaidSubscription': boolean
   'ol-hasManagedUsersFeature': boolean
   'ol-hasPassword': boolean
   'ol-hasSubscription': boolean

+ 1 - 1
services/web/frontend/stories/project-list/notifications.stories.tsx

@@ -186,7 +186,7 @@ export const NotificationGroupInvitationCancelSubscription = (args: any) => {
     },
   })
 
-  window.metaAttributesCache.set('ol-hasIndividualRecurlySubscription', true)
+  window.metaAttributesCache.set('ol-hasIndividualPaidSubscription', true)
 
   return (
     <ProjectListProvider>

+ 1 - 4
services/web/test/frontend/components/project-list/notifications/group-invitation.spec.tsx

@@ -62,10 +62,7 @@ describe('<GroupInvitationNotification />', function () {
 
   describe('user with existing personal subscription', function () {
     beforeEach(function () {
-      window.metaAttributesCache.set(
-        'ol-hasIndividualRecurlySubscription',
-        true
-      )
+      window.metaAttributesCache.set('ol-hasIndividualPaidSubscription', true)
     })
 
     it('is able to join group successfully without cancelling personal subscription', function () {

+ 1 - 1
services/web/test/frontend/features/project-list/components/notifications.test.tsx

@@ -441,7 +441,7 @@ describe('<UserNotifications />', function () {
               ),
             ])
             window.metaAttributesCache.set(
-              'ol-hasIndividualRecurlySubscription',
+              'ol-hasIndividualPaidSubscription',
               true
             )
 

+ 2 - 8
services/web/test/frontend/features/subscription/components/group-invite/group-invite.test.tsx

@@ -18,10 +18,7 @@ describe('group invite', function () {
 
   describe('when user has personal subscription', function () {
     beforeEach(function () {
-      window.metaAttributesCache.set(
-        'ol-hasIndividualRecurlySubscription',
-        true
-      )
+      window.metaAttributesCache.set('ol-hasIndividualPaidSubscription', true)
     })
 
     it('renders cancel personal subscription view', async function () {
@@ -55,10 +52,7 @@ describe('group invite', function () {
 
   describe('when user does not have a personal subscription', function () {
     beforeEach(function () {
-      window.metaAttributesCache.set(
-        'ol-hasIndividualRecurlySubscription',
-        false
-      )
+      window.metaAttributesCache.set('ol-hasIndividualPaidSubscription', false)
       window.metaAttributesCache.set('ol-inviteToken', 'token123')
     })
 

+ 0 - 12
services/web/test/frontend/features/subscription/fixtures/subscriptions.ts

@@ -25,7 +25,6 @@ export const annualActiveSubscription: PaidSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'collaborator-annual',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'collaborator-annual',
     name: 'Standard (Collaborator) Annual',
@@ -68,7 +67,6 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'collaborator-annual',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'collaborator-annual',
     name: 'Standard (Collaborator) Annual',
@@ -111,7 +109,6 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'professional',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'professional',
     name: 'Professional',
@@ -153,7 +150,6 @@ export const pastDueExpiredSubscription: PaidSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'collaborator-annual',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'collaborator-annual',
     name: 'Standard (Collaborator) Annual',
@@ -196,7 +192,6 @@ export const canceledSubscription: PaidSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'collaborator-annual',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'collaborator-annual',
     name: 'Standard (Collaborator) Annual',
@@ -239,7 +234,6 @@ export const pendingSubscriptionChange: PaidSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'collaborator-annual',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'collaborator-annual',
     name: 'Standard (Collaborator) Annual',
@@ -290,7 +284,6 @@ export const groupActiveSubscription: GroupSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'group_collaborator_10_enterprise',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'group_collaborator_10_enterprise',
     name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise',
@@ -338,7 +331,6 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
     admin_id: 'abc123',
     teamInvites: [],
     planCode: 'group_collaborator_10_enterprise',
-    recurlySubscription_id: 'ghi789',
     plan: {
       planCode: 'group_collaborator_10_enterprise',
       name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise',
@@ -396,7 +388,6 @@ export const trialSubscription: PaidSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'paid-personal_free_trial_7_days',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'paid-personal_free_trial_7_days',
     name: 'Personal',
@@ -439,7 +430,6 @@ export const customSubscription: CustomSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'collaborator-annual',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'collaborator-annual',
     name: 'Standard (Collaborator) Annual',
@@ -460,7 +450,6 @@ export const trialCollaboratorSubscription: PaidSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'collaborator_free_trial_7_days',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'collaborator_free_trial_7_days',
     name: 'Standard (Collaborator)',
@@ -503,7 +492,6 @@ export const monthlyActiveCollaborator: PaidSubscription = {
   admin_id: 'abc123',
   teamInvites: [],
   planCode: 'collaborator',
-  recurlySubscription_id: 'ghi789',
   plan: {
     planCode: 'collaborator',
     name: 'Standard (Collaborator)',

+ 0 - 3
services/web/test/unit/src/Project/ProjectControllerTests.js

@@ -201,9 +201,6 @@ describe('ProjectController', function () {
         getCurrentAffiliations: sinon.stub().resolves([]),
       },
     }
-    this.SubscriptionViewModelBuilder = {
-      getBestSubscription: sinon.stub().yields(null, { type: 'free' }),
-    }
     this.SurveyHandler = {
       getSurvey: sinon.stub().yields(null, {}),
     }

+ 2 - 2
services/web/test/unit/src/Subscription/SubscriptionControllerTests.js

@@ -6,6 +6,7 @@ const MockResponse = require('../helpers/MockResponse')
 const modulePath =
   '../../../../app/src/Features/Subscription/SubscriptionController'
 const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
+const SubscriptionHelper = require('../../../../app/src/Features/Subscription/SubscriptionHelper')
 
 const mockSubscriptions = {
   'subscription-123-active': {
@@ -77,7 +78,6 @@ describe('SubscriptionController', function () {
       buildPlansList: sinon.stub(),
       promises: {
         buildUsersSubscriptionViewModel: sinon.stub().resolves({}),
-        getBestSubscription: sinon.stub().resolves({}),
       },
       buildPlansListForSubscriptionDash: sinon
         .stub()
@@ -146,7 +146,7 @@ describe('SubscriptionController', function () {
         '../SplitTests/SplitTestHandler': this.SplitTestV2Hander,
         '../Authentication/SessionManager': this.SessionManager,
         './SubscriptionHandler': this.SubscriptionHandler,
-        './SubscriptionHelper': this.SubscriptionHelper,
+        './SubscriptionHelper': SubscriptionHelper,
         './SubscriptionViewModelBuilder': this.SubscriptionViewModelBuilder,
         './LimitationsManager': this.LimitationsManager,
         '../../infrastructure/GeoIpLookup': this.GeoIpLookup,

+ 2 - 0
services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js

@@ -5,6 +5,7 @@ const { expect } = chai
 const {
   PaymentProviderSubscription,
 } = require('../../../../app/src/Features/Subscription/PaymentProviderEntities')
+const SubscriptionHelper = require('../../../../app/src/Features/Subscription/SubscriptionHelper')
 
 const MODULE_PATH =
   '../../../../app/src/Features/Subscription/SubscriptionHandler'
@@ -149,6 +150,7 @@ describe('SubscriptionHandler', function () {
         '../../models/User': {
           User: this.User,
         },
+        './SubscriptionHelper': SubscriptionHelper,
         './SubscriptionUpdater': this.SubscriptionUpdater,
         './SubscriptionLocator': this.SubscriptionLocator,
         './LimitationsManager': this.LimitationsManager,

+ 202 - 0
services/web/test/unit/src/Subscription/SubscriptionHelperTests.js

@@ -267,4 +267,206 @@ describe('SubscriptionHelper', function () {
       })
     })
   })
+
+  describe('isPaidSubscription', function () {
+    it('should return true for a subscription with a recurly subscription id', function () {
+      const result = this.SubscriptionHelper.isPaidSubscription({
+        recurlySubscription_id: 'some-id',
+      })
+      expect(result).to.be.true
+    })
+
+    it('should return true for a subscription with a stripe subscription id', function () {
+      const result = this.SubscriptionHelper.isPaidSubscription({
+        paymentProvider: { subscriptionId: 'some-id' },
+      })
+      expect(result).to.be.true
+    })
+
+    it('should return false for a free subscription', function () {
+      const result = this.SubscriptionHelper.isPaidSubscription({})
+      expect(result).to.be.false
+    })
+
+    it('should return false for a missing subscription', function () {
+      const result = this.SubscriptionHelper.isPaidSubscription()
+      expect(result).to.be.false
+    })
+  })
+
+  describe('isIndividualActivePaidSubscription', function () {
+    it('should return true for an active recurly subscription', function () {
+      const result = this.SubscriptionHelper.isIndividualActivePaidSubscription(
+        {
+          groupPlan: false,
+          recurlyStatus: { state: 'active' },
+          recurlySubscription_id: 'some-id',
+        }
+      )
+      expect(result).to.be.true
+    })
+
+    it('should return true for an active stripe subscription', function () {
+      const result = this.SubscriptionHelper.isIndividualActivePaidSubscription(
+        {
+          groupPlan: false,
+          paymentProvider: { subscriptionId: 'sub_123', state: 'active' },
+        }
+      )
+      expect(result).to.be.true
+    })
+
+    it('should return false for a canceled recurly subscription', function () {
+      const result = this.SubscriptionHelper.isIndividualActivePaidSubscription(
+        {
+          groupPlan: false,
+          recurlyStatus: { state: 'canceled' },
+          recurlySubscription_id: 'some-id',
+        }
+      )
+      expect(result).to.be.false
+    })
+
+    it('should return false for a canceled stripe subscription', function () {
+      const result = this.SubscriptionHelper.isIndividualActivePaidSubscription(
+        {
+          groupPlan: false,
+          paymentProvider: { state: 'canceled', subscriptionId: 'sub_123' },
+        }
+      )
+      expect(result).to.be.false
+    })
+
+    it('should return false for a group plan subscription', function () {
+      const result = this.SubscriptionHelper.isIndividualActivePaidSubscription(
+        {
+          groupPlan: true,
+          recurlyStatus: { state: 'active' },
+          recurlySubscription_id: 'some-id',
+        }
+      )
+      expect(result).to.be.false
+    })
+
+    it('should return false for a free subscription', function () {
+      const result = this.SubscriptionHelper.isIndividualActivePaidSubscription(
+        {}
+      )
+      expect(result).to.be.false
+    })
+
+    it('should return false for a subscription with an empty string for recurlySubscription_id', function () {
+      const result = this.SubscriptionHelper.isIndividualActivePaidSubscription(
+        {
+          groupPlan: false,
+          recurlySubscription_id: '',
+          recurlyStatus: { state: 'active' },
+        }
+      )
+      expect(result).to.be.false
+    })
+
+    it('should return false for a subscription with an empty string for paymentProvider.subscriptionId', function () {
+      const result = this.SubscriptionHelper.isIndividualActivePaidSubscription(
+        {
+          groupPlan: false,
+          paymentProvider: { state: 'active', subscriptionId: '' },
+        }
+      )
+      expect(result).to.be.false
+    })
+
+    it('should return false for a missing subscription', function () {
+      const result = this.SubscriptionHelper.isPaidSubscription()
+      expect(result).to.be.false
+    })
+  })
+
+  describe('getPaymentProviderSubscriptionId', function () {
+    it('should return the recurly subscription id if it exists', function () {
+      const result = this.SubscriptionHelper.getPaymentProviderSubscriptionId({
+        recurlySubscription_id: 'some-id',
+      })
+      expect(result).to.equal('some-id')
+    })
+
+    it('should return the payment provider subscription id if it exists', function () {
+      const result = this.SubscriptionHelper.getPaymentProviderSubscriptionId({
+        paymentProvider: { subscriptionId: 'sub_123' },
+      })
+      expect(result).to.equal('sub_123')
+    })
+
+    it('should return null if no subscription id exists', function () {
+      const result = this.SubscriptionHelper.getPaymentProviderSubscriptionId(
+        {}
+      )
+      expect(result).to.be.null
+    })
+  })
+
+  describe('getPaidSubscriptionState', function () {
+    it('should return the recurly state if it exists', function () {
+      const result = this.SubscriptionHelper.getPaidSubscriptionState({
+        recurlyStatus: { state: 'active' },
+      })
+      expect(result).to.equal('active')
+    })
+
+    it('should return the payment provider state if it exists', function () {
+      const result = this.SubscriptionHelper.getPaidSubscriptionState({
+        paymentProvider: { state: 'active' },
+      })
+      expect(result).to.equal('active')
+    })
+
+    it('should return null if no state exists', function () {
+      const result = this.SubscriptionHelper.getPaidSubscriptionState({})
+      expect(result).to.be.null
+    })
+  })
+
+  describe('getSubscriptionTrialStartedAt', function () {
+    it('should return the recurly trial start date if it exists', function () {
+      const result = this.SubscriptionHelper.getSubscriptionTrialStartedAt({
+        recurlySubscription_id: 'some-id',
+        recurlyStatus: { trialStartedAt: new Date('2023-01-01') },
+      })
+      expect(result).to.deep.equal(new Date('2023-01-01'))
+    })
+
+    it('should return the payment provider trial start date if it exists', function () {
+      const result = this.SubscriptionHelper.getSubscriptionTrialStartedAt({
+        paymentProvider: { trialStartedAt: new Date('2023-01-01') },
+      })
+      expect(result).to.deep.equal(new Date('2023-01-01'))
+    })
+
+    it('should return undefined if no trial start date exists', function () {
+      const result = this.SubscriptionHelper.getSubscriptionTrialStartedAt({})
+      expect(result).to.be.undefined
+    })
+  })
+
+  describe('getSubscriptionTrialEndsAt', function () {
+    it('should return the recurly trial end date if it exists', function () {
+      const result = this.SubscriptionHelper.getSubscriptionTrialEndsAt({
+        recurlySubscription_id: 'some-id',
+        recurlyStatus: { trialEndsAt: new Date('2023-01-01') },
+      })
+      expect(result).to.deep.equal(new Date('2023-01-01'))
+    })
+
+    it('should return the payment provider trial end date if it exists', function () {
+      const result = this.SubscriptionHelper.getSubscriptionTrialEndsAt({
+        paymentProvider: { trialEndsAt: new Date('2023-01-01') },
+      })
+      expect(result).to.deep.equal(new Date('2023-01-01'))
+    })
+
+    it('should return undefined if no trial end date exists', function () {
+      const result = this.SubscriptionHelper.getSubscriptionTrialEndsAt({})
+      expect(result).to.be.undefined
+    })
+  })
 })

+ 116 - 47
services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js

@@ -7,6 +7,7 @@ const {
   PaymentProviderSubscriptionAddOn,
   PaymentProviderSubscriptionChange,
 } = require('../../../../app/src/Features/Subscription/PaymentProviderEntities')
+const SubscriptionHelper = require('../../../../app/src/Features/Subscription/SubscriptionHelper')
 
 const modulePath =
   '../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder'
@@ -159,13 +160,14 @@ describe('SubscriptionViewModelBuilder', function () {
         './SubscriptionUpdater': this.SubscriptionUpdater,
         './PlansLocator': this.PlansLocator,
         '../../infrastructure/Modules': (this.Modules = {
+          promises: { hooks: { fire: sinon.stub().resolves([]) } },
           hooks: {
             fire: sinon.stub().yields(null, []),
           },
         }),
         './V1SubscriptionManager': {},
         '../Publishers/PublishersGetter': this.PublishersGetter,
-        './SubscriptionHelper': {},
+        './SubscriptionHelper': SubscriptionHelper,
       },
     })
 
@@ -180,10 +182,10 @@ describe('SubscriptionViewModelBuilder', function () {
       .returns(this.commonsPlan)
   })
 
-  describe('getBestSubscription', function () {
+  describe('getUsersSubscriptionDetails', function () {
     it('should return a free plan when user has no subscription or affiliation', async function () {
-      const usersBestSubscription =
-        await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+      const { bestSubscription: usersBestSubscription } =
+        await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
           this.user
         )
       assert.deepEqual(usersBestSubscription, { type: 'free' })
@@ -195,8 +197,8 @@ describe('SubscriptionViewModelBuilder', function () {
           .withArgs(this.user)
           .resolves(this.individualCustomSubscription)
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 
@@ -213,8 +215,8 @@ describe('SubscriptionViewModelBuilder', function () {
           .withArgs(this.user)
           .resolves(this.individualSubscription)
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 
@@ -234,8 +236,8 @@ describe('SubscriptionViewModelBuilder', function () {
           .withArgs(this.user)
           .resolves(this.individualSubscription)
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 
@@ -255,8 +257,8 @@ describe('SubscriptionViewModelBuilder', function () {
           .withArgs(this.user)
           .resolves(this.individualSubscription)
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 
@@ -268,8 +270,8 @@ describe('SubscriptionViewModelBuilder', function () {
         })
       })
 
-      it('should update subscription if recurly data is missing', async function () {
-        this.individualSubscriptionWithoutRecurly = {
+      it('should update subscription if recurly payment state is missing', async function () {
+        this.individualSubscriptionWithoutPaymentState = {
           planCode: this.planCode,
           plan: this.plan,
           recurlySubscription_id: this.recurlySubscription_id,
@@ -280,37 +282,104 @@ describe('SubscriptionViewModelBuilder', function () {
         this.SubscriptionLocator.promises.getUsersSubscription
           .withArgs(this.user)
           .onCall(0)
-          .resolves(this.individualSubscriptionWithoutRecurly)
+          .resolves(this.individualSubscriptionWithoutPaymentState)
           .withArgs(this.user)
           .onCall(1)
           .resolves(this.individualSubscription)
-        this.RecurlyWrapper.promises.getSubscription
-          .withArgs(this.individualSubscription.recurlySubscription_id, {
-            includeAccount: true,
-          })
-          .resolves(this.paymentRecord)
+        const payment = {
+          subscription: this.paymentRecord,
+          account: new PaymentProviderAccount({}),
+          coupons: [],
+        }
+
+        this.Modules.promises.hooks.fire
+          .withArgs(
+            'getPaymentFromRecordPromise',
+            this.individualSubscriptionWithoutPaymentState
+          )
+          .resolves([payment])
+        this.Modules.promises.hooks.fire
+          .withArgs(
+            'syncSubscription',
+            payment,
+            this.individualSubscriptionWithoutPaymentState
+          )
+          .resolves([])
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 
-        sinon.assert.calledWith(
-          this.RecurlyWrapper.promises.getSubscription,
-          this.individualSubscriptionWithoutRecurly.recurlySubscription_id,
-          { includeAccount: true }
-        )
-        sinon.assert.calledWith(
-          this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly,
-          this.paymentRecord,
-          this.individualSubscriptionWithoutRecurly
+        assert.deepEqual(usersBestSubscription, {
+          type: 'individual',
+          subscription: this.individualSubscription,
+          plan: this.plan,
+          remainingTrialDays: -1,
+        })
+        assert.isTrue(
+          this.Modules.promises.hooks.fire.withArgs(
+            'getPaymentFromRecordPromise',
+            this.individualSubscriptionWithoutPaymentState
+          ).calledOnce
         )
+      })
+
+      it('should update subscription if stripe payment state is missing', async function () {
+        this.individualSubscriptionWithoutPaymentState = {
+          planCode: this.planCode,
+          plan: this.plan,
+          paymentProvider: {
+            subscriptionId: this.recurlySubscription_id,
+          },
+        }
+        this.paymentRecord = {
+          state: 'active',
+        }
+        this.SubscriptionLocator.promises.getUsersSubscription
+          .withArgs(this.user)
+          .onCall(0)
+          .resolves(this.individualSubscriptionWithoutPaymentState)
+          .withArgs(this.user)
+          .onCall(1)
+          .resolves(this.individualSubscription)
+        const payment = {
+          subscription: this.paymentRecord,
+          account: new PaymentProviderAccount({}),
+          coupons: [],
+        }
+
+        this.Modules.promises.hooks.fire
+          .withArgs(
+            'getPaymentFromRecordPromise',
+            this.individualSubscriptionWithoutPaymentState
+          )
+          .resolves([payment])
+        this.Modules.promises.hooks.fire
+          .withArgs(
+            'syncSubscription',
+            payment,
+            this.individualSubscriptionWithoutPaymentState
+          )
+          .resolves([])
+
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
+            this.user
+          )
+
         assert.deepEqual(usersBestSubscription, {
           type: 'individual',
           subscription: this.individualSubscription,
           plan: this.plan,
           remainingTrialDays: -1,
         })
+        assert.isTrue(
+          this.Modules.promises.hooks.fire.withArgs(
+            'getPaymentFromRecordPromise',
+            this.individualSubscriptionWithoutPaymentState
+          ).calledOnce
+        )
       })
     })
 
@@ -318,8 +387,8 @@ describe('SubscriptionViewModelBuilder', function () {
       this.SubscriptionLocator.promises.getMemberSubscriptions
         .withArgs(this.user)
         .resolves([this.groupSubscription])
-      const usersBestSubscription =
-        await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+      const { bestSubscription: usersBestSubscription } =
+        await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
           this.user
         )
       assert.deepEqual(usersBestSubscription, {
@@ -336,8 +405,8 @@ describe('SubscriptionViewModelBuilder', function () {
         .resolves([
           Object.assign({}, this.groupSubscription, { teamName: 'test team' }),
         ])
-      const usersBestSubscription =
-        await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+      const { bestSubscription: usersBestSubscription } =
+        await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
           this.user
         )
       assert.deepEqual(usersBestSubscription, {
@@ -353,8 +422,8 @@ describe('SubscriptionViewModelBuilder', function () {
         .withArgs(this.user._id)
         .resolves([this.commonsSubscription])
 
-      const usersBestSubscription =
-        await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+      const { bestSubscription: usersBestSubscription } =
+        await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
           this.user
         )
 
@@ -385,8 +454,8 @@ describe('SubscriptionViewModelBuilder', function () {
           compileTimeout: 60,
         }
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 
@@ -410,8 +479,8 @@ describe('SubscriptionViewModelBuilder', function () {
           compileTimeout: 60,
         }
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 
@@ -440,8 +509,8 @@ describe('SubscriptionViewModelBuilder', function () {
           compileTimeout: 240,
         }
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 
@@ -469,8 +538,8 @@ describe('SubscriptionViewModelBuilder', function () {
           compileTimeout: 240,
         }
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 
@@ -499,8 +568,8 @@ describe('SubscriptionViewModelBuilder', function () {
           compileTimeout: 240,
         }
 
-        const usersBestSubscription =
-          await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
+        const { bestSubscription: usersBestSubscription } =
+          await this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails(
             this.user
           )
 

+ 4 - 4
services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs

@@ -175,7 +175,7 @@ describe('TeamInvitesController', function () {
       },
     }
 
-    describe('hasIndividualRecurlySubscription', function () {
+    describe('hasIndividualPaidSubscription', function () {
       it('is true for personal subscription', function (ctx) {
         return new Promise(resolve => {
           ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({
@@ -184,7 +184,7 @@ describe('TeamInvitesController', function () {
           })
           const res = {
             render: (template, data) => {
-              expect(data.hasIndividualRecurlySubscription).to.be.true
+              expect(data.hasIndividualPaidSubscription).to.be.true
               resolve()
             },
           }
@@ -200,7 +200,7 @@ describe('TeamInvitesController', function () {
           })
           const res = {
             render: (template, data) => {
-              expect(data.hasIndividualRecurlySubscription).to.be.false
+              expect(data.hasIndividualPaidSubscription).to.be.false
               resolve()
             },
           }
@@ -219,7 +219,7 @@ describe('TeamInvitesController', function () {
           })
           const res = {
             render: (template, data) => {
-              expect(data.hasIndividualRecurlySubscription).to.be.false
+              expect(data.hasIndividualPaidSubscription).to.be.false
               resolve()
             },
           }

+ 5 - 1
services/web/types/project/dashboard/subscription.ts

@@ -1,4 +1,7 @@
-import { SubscriptionState } from '../../subscription/dashboard/subscription'
+import {
+  SubscriptionState,
+  PaymentProvider,
+} from '../../subscription/dashboard/subscription'
 
 type SubscriptionBase = {
   featuresPageURL: string
@@ -22,6 +25,7 @@ type PaidSubscriptionBase = {
     teamName?: string
     name: string
     recurlyStatus?: RecurlyStatus
+    paymentProvider?: PaymentProvider
   }
 } & SubscriptionBase
 

+ 0 - 1
services/web/types/subscription/dashboard/subscription.ts

@@ -64,7 +64,6 @@ export type Subscription = {
   membersLimit: number
   teamInvites: object[]
   planCode: string
-  recurlySubscription_id: string
   plan: Plan
   pendingPlan?: PendingPaymentProviderPlan
   addOns?: AddOn[]

+ 1 - 1
services/web/types/user.ts

@@ -39,7 +39,7 @@ export type User = {
   isAdmin?: boolean
   email: string
   allowedFreeTrial?: boolean
-  hasRecurlySubscription?: boolean
+  hasPaidSubscription?: boolean
   first_name?: string
   last_name?: string
   alphaProgram?: boolean