| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- import Settings from '@overleaf/settings'
- import { AI_ADD_ON_CODE, isStandaloneAiAddOnPlanCode } from './AiHelper.mjs'
- import FeaturesHelper from './FeaturesHelper.mjs'
- const INACTIVE_NEXT_RENEWAL_DATE_STATES = new Set([
- 'canceled',
- 'cancelled',
- 'expired',
- ])
- const PENDING_CANCELLATION_STATES = new Set(['canceled', 'cancelled'])
- function getSubscriptionState(subscription) {
- return (
- subscription?.recurlyStatus?.state || subscription?.paymentProvider?.state
- )
- }
- function toUnixTimestamp(dateValue) {
- if (!dateValue) {
- return null
- }
- const date = new Date(dateValue)
- if (Number.isNaN(date.getTime())) {
- return null
- }
- return Math.floor(date.getTime() / 1000)
- }
- function normalizePlanType(bestSubscription) {
- if (!bestSubscription) {
- return null
- }
- if (['standalone-ai-add-on', 'commons'].includes(bestSubscription.type)) {
- return bestSubscription.type
- }
- const planCode = bestSubscription.plan?.planCode
- const isGroupPlan = bestSubscription.plan?.groupPlan === true
- if (!planCode) {
- return bestSubscription.type || null
- }
- if (planCode.startsWith('v1_')) {
- return 'v1'
- }
- if (planCode.includes('student')) {
- return 'student'
- }
- if (planCode.includes('professional')) {
- return isGroupPlan ? 'group-professional' : 'professional'
- }
- if (planCode.includes('collaborator')) {
- return isGroupPlan ? 'group-standard' : 'standard'
- }
- if (planCode.includes('personal')) {
- return 'personal'
- }
- if (isGroupPlan) {
- return 'group-standard'
- }
- return planCode
- }
- function getFriendlyPlanName(planType) {
- if (!planType) {
- return null
- }
- const friendlyPlanNames = {
- free: 'Free',
- personal: 'Personal',
- standard: 'Standard',
- professional: 'Pro',
- student: 'Student',
- commons: 'Commons',
- 'group-standard': 'Group Standard',
- 'group-professional': 'Group Pro',
- 'standalone-ai-add-on': 'AI Assist add-on',
- v1: 'Legacy',
- }
- if (friendlyPlanNames[planType]) {
- return friendlyPlanNames[planType]
- }
- return planType
- }
- function getPlanCadence(bestSubscription) {
- if (!bestSubscription?.plan) {
- return null
- }
- return bestSubscription.plan.annual ? 'annual' : 'monthly'
- }
- function getPlanCadenceFromPlanCode(planCode) {
- if (!planCode) {
- return null
- }
- const plan = Settings.plans.find(candidate => candidate.planCode === planCode)
- if (plan) {
- return plan.annual ? 'annual' : 'monthly'
- }
- if (planCode.includes('annual')) {
- return 'annual'
- }
- if (isStandaloneAiAddOnPlanCode(planCode)) {
- return 'monthly'
- }
- return null
- }
- function getNextRenewalDateFromPaymentRecord(paymentRecord) {
- const subscriptionState = paymentRecord?.subscription?.state
- if (INACTIVE_NEXT_RENEWAL_DATE_STATES.has(subscriptionState)) {
- return null
- }
- return toUnixTimestamp(paymentRecord?.subscription?.periodEnd)
- }
- function shouldClearNextRenewalDate(subscription) {
- if (!subscription) {
- return true
- }
- return INACTIVE_NEXT_RENEWAL_DATE_STATES.has(
- getSubscriptionState(subscription)
- )
- }
- function getExpiryDateFromPaymentRecord(paymentRecord) {
- const subscriptionState = paymentRecord?.subscription?.state
- if (!PENDING_CANCELLATION_STATES.has(subscriptionState)) {
- return null
- }
- const expiryDate = toUnixTimestamp(paymentRecord?.subscription?.periodEnd)
- if (expiryDate == null) {
- return null
- }
- return expiryDate > Math.floor(Date.now() / 1000) ? expiryDate : null
- }
- function shouldClearExpiryDate(subscription) {
- if (!subscription) {
- return true
- }
- return !PENDING_CANCELLATION_STATES.has(getSubscriptionState(subscription))
- }
- function getTrialEndDate(individualSubscription) {
- const trialEndsAt =
- individualSubscription?.recurlyStatus?.trialEndsAt ||
- individualSubscription?.paymentProvider?.trialEndsAt
- return toUnixTimestamp(trialEndsAt)
- }
- function hasIndividualAiAssistAddOn(individualSubscription, paymentRecord) {
- if (
- !individualSubscription ||
- individualSubscription.groupPlan ||
- isStandaloneAiAddOnPlanCode(individualSubscription.planCode)
- ) {
- return false
- }
- return Boolean(
- paymentRecord?.subscription?.addOns?.some(
- addOn => addOn.code === AI_ADD_ON_CODE
- )
- )
- }
- function getAiPlanType(
- bestSubscription,
- individualSubscription,
- paymentRecord,
- writefullData
- ) {
- if (
- bestSubscription?.type === 'standalone-ai-add-on' ||
- isStandaloneAiAddOnPlanCode(individualSubscription?.planCode)
- ) {
- return 'ai-assist-standalone'
- }
- if (hasIndividualAiAssistAddOn(individualSubscription, paymentRecord)) {
- return 'ai-assist-add-on'
- }
- if (writefullData?.isPremium) {
- return 'writefull-premium'
- }
- return 'none'
- }
- function getAiPlanCadence(
- aiPlan,
- bestSubscription,
- individualSubscription,
- paymentRecord
- ) {
- if (aiPlan === 'ai-assist-standalone') {
- return (
- getPlanCadenceFromPlanCode(individualSubscription?.planCode) ||
- getPlanCadenceFromPlanCode(paymentRecord?.subscription?.planCode)
- )
- }
- if (aiPlan === 'ai-assist-add-on') {
- return (
- getPlanCadence(bestSubscription) ||
- getPlanCadenceFromPlanCode(individualSubscription?.planCode)
- )
- }
- return null
- }
- function hasPlanAiEnabled(plan) {
- if (!plan?.features) {
- return false
- }
- return (
- plan.features.aiUsageQuota === Settings.aiFeatures.unlimitedQuota ||
- plan.features.aiErrorAssistant === true
- )
- }
- function getGroupAiEnabled(
- memberGroupSubscriptions = [],
- managedGroupSubscriptions = [],
- userIsMemberOfGroupSubscription
- ) {
- if (!userIsMemberOfGroupSubscription) {
- return null
- }
- const allGroupSubscriptions = [
- ...memberGroupSubscriptions,
- ...managedGroupSubscriptions,
- ]
- return allGroupSubscriptions.some(subscription => {
- const plan = Settings.plans.find(
- candidate => candidate.planCode === subscription.planCode
- )
- return hasPlanAiEnabled(plan)
- })
- }
- function getGroupSize(
- bestSubscription,
- memberGroupSubscriptions = [],
- managedGroupSubscriptions = []
- ) {
- const allGroupSubscriptions = [
- ...memberGroupSubscriptions,
- ...managedGroupSubscriptions,
- ]
- if (allGroupSubscriptions.length === 0) {
- return null
- }
- const matchingBestGroupSubscription =
- bestSubscription?.type === 'group'
- ? allGroupSubscriptions.find(
- subscription =>
- subscription.planCode === bestSubscription.plan?.planCode &&
- subscription.teamName === bestSubscription.subscription?.teamName
- )
- : null
- if (matchingBestGroupSubscription?.membersLimit != null) {
- return matchingBestGroupSubscription.membersLimit
- }
- if (bestSubscription?.subscription?.membersLimit != null) {
- return bestSubscription.subscription.membersLimit
- }
- if (
- bestSubscription?.plan?.groupPlan &&
- bestSubscription.plan.membersLimit != null
- ) {
- return bestSubscription.plan.membersLimit
- }
- return allGroupSubscriptions.reduce((largestGroupSize, subscription) => {
- const plan = Settings.plans.find(
- candidate => candidate.planCode === subscription.planCode
- )
- const groupSize = subscription.membersLimit ?? plan?.membersLimit ?? 0
- return Math.max(largestGroupSize, groupSize)
- }, 0)
- }
- function getPaymentProvider(
- individualSubscription,
- memberGroupSubscriptions = [],
- managedGroupSubscriptions = []
- ) {
- const candidates = [
- individualSubscription,
- ...memberGroupSubscriptions,
- ...managedGroupSubscriptions,
- ].filter(Boolean)
- if (candidates.length === 0) {
- return null
- }
- for (const candidate of candidates) {
- const service = candidate.paymentProvider?.service
- if (service) {
- return service.includes('stripe') ? 'stripe' : 'recurly'
- }
- }
- return 'recurly'
- }
- function shouldUseCommonsBestSubscription(
- hasCommons,
- bestSubscription,
- commonsPlan
- ) {
- if (!hasCommons) {
- return false
- }
- if (bestSubscription == null) {
- return true
- }
- return FeaturesHelper.isFeatureSetBetter(
- commonsPlan?.features || {},
- bestSubscription.plan?.features || {}
- )
- }
- /**
- * Compute plan-related user properties for sending to customer.io.
- */
- function getPlanProperties({
- bestSubscription,
- individualSubscription,
- individualPaymentRecord,
- memberGroupSubscriptions,
- managedGroupSubscriptions,
- userIsMemberOfGroupSubscription,
- hasCommons,
- writefullData,
- }) {
- const planType = normalizePlanType(bestSubscription)
- const displayPlanType = getFriendlyPlanName(planType)
- const planTermLabel = getPlanCadence(bestSubscription)
- const aiPlan = getAiPlanType(
- bestSubscription,
- individualSubscription,
- individualPaymentRecord,
- writefullData
- )
- const aiPlanTermLabel = getAiPlanCadence(
- aiPlan,
- bestSubscription,
- individualSubscription,
- individualPaymentRecord
- )
- const groupAiEnabled = getGroupAiEnabled(
- memberGroupSubscriptions,
- managedGroupSubscriptions,
- userIsMemberOfGroupSubscription
- )
- const nextRenewalDate = getNextRenewalDateFromPaymentRecord(
- individualPaymentRecord
- )
- const expiryDate = getExpiryDateFromPaymentRecord(individualPaymentRecord)
- const groupSizeValue = getGroupSize(
- bestSubscription,
- memberGroupSubscriptions,
- managedGroupSubscriptions
- )
- const nextRenewalDateTrait =
- nextRenewalDate ??
- (shouldClearNextRenewalDate(individualSubscription) ? '' : undefined)
- const expiryDateTrait =
- expiryDate ??
- (shouldClearExpiryDate(individualSubscription) ? '' : undefined)
- const trialEndDate = getTrialEndDate(individualSubscription)
- const properties = {
- ai_plan: aiPlan,
- group: userIsMemberOfGroupSubscription,
- commons: Boolean(hasCommons),
- individual_subscription: Boolean(
- individualSubscription && !individualSubscription.groupPlan
- ),
- }
- if (trialEndDate != null) properties.trial_end_date = trialEndDate
- const paymentProvider = getPaymentProvider(
- individualSubscription,
- memberGroupSubscriptions,
- managedGroupSubscriptions
- )
- if (paymentProvider) properties.payment_provider = paymentProvider
- if (planType) properties.plan_type = planType
- if (displayPlanType) properties.display_plan_type = displayPlanType
- if (planTermLabel) properties.plan_term_label = planTermLabel
- if (aiPlanTermLabel) properties.ai_plan_term_label = aiPlanTermLabel
- if (groupAiEnabled !== null) properties.group_ai_enabled = groupAiEnabled
- if (nextRenewalDateTrait !== undefined)
- properties.next_renewal_date = nextRenewalDateTrait
- if (expiryDateTrait !== undefined) properties.expiry_date = expiryDateTrait
- if (groupSizeValue !== null) properties.group_size = groupSizeValue
- return properties
- }
- export default {
- normalizePlanType,
- getFriendlyPlanName,
- getNextRenewalDateFromPaymentRecord,
- getExpiryDateFromPaymentRecord,
- getAiPlanType,
- getAiPlanCadence,
- hasPlanAiEnabled,
- shouldUseCommonsBestSubscription,
- getPlanProperties,
- }
|