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

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

GitOrigin-RevId: f5e905eba598cfcd146803c6ccc36a2304021544
This commit is contained in:
Kristina
2025-06-06 11:20:22 +02:00
committed by Copybot
parent a8df91e91b
commit 7a449f4686
30 changed files with 477 additions and 147 deletions

View File

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

View File

@@ -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 || []),
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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 () {

View File

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

View File

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

View File

@@ -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)',

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: [],
}
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
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
)
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
)

View File

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

View File

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

View File

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

View File

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