diff --git a/packages/server/billing/helpers/teamLimitsCheck.ts b/packages/server/billing/helpers/teamLimitsCheck.ts deleted file mode 100644 index fb16cfdca80..00000000000 --- a/packages/server/billing/helpers/teamLimitsCheck.ts +++ /dev/null @@ -1,169 +0,0 @@ -import ms from 'ms' -import {Threshold} from 'parabol-client/types/constEnums' -// Uncomment for easier testing -// import { ThresholdTest as Threshold } from "~/types/constEnums"; -import scheduleTeamLimitsJobs from '../../database/types/scheduleTeamLimitsJobs' -import generateUID from '../../generateUID' -import {DataLoaderWorker} from '../../graphql/graphql' -import publishNotification from '../../graphql/public/mutations/helpers/publishNotification' -import getActiveTeamCountByTeamIds from '../../graphql/public/types/helpers/getActiveTeamCountByTeamIds' -import {getFeatureTier} from '../../graphql/types/helpers/getFeatureTier' -import {domainHasActiveDeals} from '../../hubSpot/hubSpotApi' -import getKysely from '../../postgres/getKysely' -import getTeamIdsByOrgIds from '../../postgres/queries/getTeamIdsByOrgIds' -import {Organization} from '../../postgres/types' -import {getBillingLeadersByOrgId} from '../../utils/getBillingLeadersByOrgId' -import sendToSentry from '../../utils/sendToSentry' -import removeTeamsLimitObjects from './removeTeamsLimitObjects' -import sendTeamsLimitEmail from './sendTeamsLimitEmail' - -const enableUsageStats = async (userIds: string[], orgId: string) => { - const pg = getKysely() - await pg - .updateTable('OrganizationUser') - .set({suggestedTier: 'team'}) - .where('orgId', '=', orgId) - .where('userId', 'in', userIds) - .where('removedAt', 'is', null) - .execute() - const featureFlag = await pg - .selectFrom('FeatureFlag') - .select(['id']) - .where('featureName', '=', 'insights') - .executeTakeFirst() - if (featureFlag) { - const values = [...userIds.map((userId) => ({userId, featureFlagId: featureFlag.id}))] - await pg - .insertInto('FeatureFlagOwner') - .values(values) - .onConflict((oc) => oc.doNothing()) - .execute() - } -} - -const sendWebsiteNotifications = async ( - organization: Organization, - userIds: string[], - dataLoader: DataLoaderWorker -) => { - const pg = getKysely() - const {id: orgId, name: orgName, picture: orgPicture} = organization - const operationId = dataLoader.share() - const subOptions = {operationId} - const notificationsToInsert = userIds.map((userId) => ({ - id: generateUID(), - type: 'TEAMS_LIMIT_EXCEEDED' as const, - userId, - orgId, - orgName, - orgPicture - })) - - await pg.insertInto('Notification').values(notificationsToInsert).execute() - notificationsToInsert.forEach((notification) => { - publishNotification(notification, subOptions) - }) -} - -// Warning: the function might be expensive -const isLimitExceeded = async (orgId: string) => { - const teamIds = await getTeamIdsByOrgIds([orgId]) - if (teamIds.length <= Threshold.MAX_STARTER_TIER_TEAMS) { - return false - } - - const activeTeamCount = await getActiveTeamCountByTeamIds(teamIds) - - return activeTeamCount >= Threshold.MAX_STARTER_TIER_TEAMS -} - -// Warning: the function might be expensive -export const maybeRemoveRestrictions = async (orgId: string, dataLoader: DataLoaderWorker) => { - const organization = await dataLoader.get('organizations').loadNonNull(orgId) - - if (!organization.tierLimitExceededAt) { - return - } - - if (!(await isLimitExceeded(orgId))) { - const billingLeadersIds = await dataLoader.get('billingLeadersIdsByOrgId').load(orgId) - const pg = getKysely() - await Promise.all([ - pg - .updateTable('Organization') - .set({tierLimitExceededAt: null, scheduledLockAt: null, lockedAt: null}) - .where('id', '=', orgId) - .execute(), - pg - .updateTable('OrganizationUser') - .set({suggestedTier: 'starter'}) - .where('orgId', '=', orgId) - .where('userId', 'in', billingLeadersIds) - .where('removedAt', 'is', null) - .execute(), - removeTeamsLimitObjects(orgId, dataLoader) - ]) - dataLoader.get('organizations').clear(orgId) - } -} - -// Warning: the function might be expensive -export const checkTeamsLimit = async (orgId: string, dataLoader: DataLoaderWorker) => { - const organization = await dataLoader.get('organizations').loadNonNull(orgId) - const {tierLimitExceededAt, tier, trialStartDate, name: orgName} = organization - - const hasTeamsLimitFlag = await dataLoader - .get('featureFlagByOwnerId') - .load({ownerId: orgId, featureName: 'teamsLimit'}) - if (!hasTeamsLimitFlag) return - - if (tierLimitExceededAt || getFeatureTier({tier, trialStartDate}) !== 'starter') return - - // if an org is using a free provider, e.g. gmail.com, we can't show them usage stats, so don't send notifications/emails directing them there for now. Issue to fix this here: https://github.com/ParabolInc/parabol/issues/7723 - if (!organization.activeDomain) return - - if (!(await isLimitExceeded(orgId))) return - - const hasActiveDeals = await domainHasActiveDeals(organization.activeDomain) - - if (hasActiveDeals) { - if (hasActiveDeals instanceof Error) { - sendToSentry(hasActiveDeals) - } - - return - } - - const now = new Date() - const scheduledLockAt = new Date(now.getTime() + ms(`${Threshold.STARTER_TIER_LOCK_AFTER_DAYS}d`)) - const pg = getKysely() - await Promise.all([ - pg - .updateTable('Organization') - .set({ - tierLimitExceededAt: now, - scheduledLockAt - }) - .where('id', '=', orgId) - .execute() - ]) - dataLoader.get('organizations').clear(orgId) - - const billingLeaders = await getBillingLeadersByOrgId(orgId, dataLoader) - const billingLeadersIds = billingLeaders.map((billingLeader) => billingLeader.id) - - // wait for usage stats to be enabled as we dont want to send notifications before it's available - await enableUsageStats(billingLeadersIds, orgId) - await Promise.all([ - sendWebsiteNotifications(organization, billingLeadersIds, dataLoader), - billingLeaders.map((billingLeader) => - sendTeamsLimitEmail({ - user: billingLeader, - orgId, - orgName, - emailType: 'thirtyDayWarning' - }) - ), - scheduleTeamLimitsJobs(scheduledLockAt, orgId) - ]) -} diff --git a/packages/server/graphql/mutations/archiveTeam.ts b/packages/server/graphql/mutations/archiveTeam.ts index c9ccd474666..c4f0945c5ae 100644 --- a/packages/server/graphql/mutations/archiveTeam.ts +++ b/packages/server/graphql/mutations/archiveTeam.ts @@ -1,7 +1,6 @@ import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import TeamMemberId from '../../../client/shared/gqlIds/TeamMemberId' -import {maybeRemoveRestrictions} from '../../billing/helpers/teamLimitsCheck' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' import removeMeetingTemplatesForTeam from '../../postgres/queries/removeMeetingTemplatesForTeam' @@ -89,8 +88,6 @@ export default { publish(SubscriptionChannel.NOTIFICATION, id, 'AuthTokenPayload', {tms}) }) - await maybeRemoveRestrictions(team.orgId, dataLoader) - return data } } diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index 500ebccf76e..5a9792b70e2 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -5,7 +5,6 @@ import {AGENDA_ITEMS, LAST_CALL} from 'parabol-client/utils/constants' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' import findStageById from 'parabol-client/utils/meetings/findStageById' import {positionAfter} from '../../../client/shared/sortOrder' -import {checkTeamsLimit} from '../../billing/helpers/teamLimitsCheck' import TimelineEventCheckinComplete from '../../database/types/TimelineEventCheckinComplete' import {DataLoaderInstance} from '../../dataloader/RootDataLoader' import generateUID from '../../generateUID' @@ -210,7 +209,6 @@ export default { analytics.checkInEnd(completedCheckIn, meetingMembers, dataLoader) sendNewMeetingSummary(completedCheckIn, context).catch(Logger.log) - checkTeamsLimit(team.orgId, dataLoader) const events = teamMembers.map( (teamMember) => diff --git a/packages/server/graphql/mutations/endSprintPoker.ts b/packages/server/graphql/mutations/endSprintPoker.ts index 56e6f402cda..9d197ee8af5 100644 --- a/packages/server/graphql/mutations/endSprintPoker.ts +++ b/packages/server/graphql/mutations/endSprintPoker.ts @@ -3,7 +3,6 @@ import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' import findStageById from 'parabol-client/utils/meetings/findStageById' -import {checkTeamsLimit} from '../../billing/helpers/teamLimitsCheck' import TimelineEventPokerComplete from '../../database/types/TimelineEventPokerComplete' import getKysely from '../../postgres/getKysely' import {Logger} from '../../utils/Logger' @@ -108,7 +107,6 @@ export default { const isKill = !!(phase && phase.phaseType !== 'ESTIMATE') if (!isKill) { sendNewMeetingSummary(completedMeeting, context).catch(Logger.log) - checkTeamsLimit(team.orgId, dataLoader) } const events = teamMembers.map( (teamMember) => diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index a3da6220a4e..b6082f4d5ec 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -3,7 +3,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {DISCUSS} from 'parabol-client/utils/constants' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' import findStageById from 'parabol-client/utils/meetings/findStageById' -import {checkTeamsLimit} from '../../../billing/helpers/teamLimitsCheck' import TimelineEventRetroComplete from '../../../database/types/TimelineEventRetroComplete' import getKysely from '../../../postgres/getKysely' import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' @@ -140,7 +139,6 @@ const safeEndRetrospective = async ({ // don't await for the OpenAI response or it'll hang for a while when ending the retro summarizeRetroMeeting(completedRetrospective, context) analytics.retrospectiveEnd(completedRetrospective, meetingMembers, template, dataLoader) - checkTeamsLimit(team.orgId, dataLoader) const events = teamMembers.map( (teamMember) => new TimelineEventRetroComplete({ diff --git a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts index 9994a58b5b6..ae049b6c0b2 100644 --- a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts @@ -1,6 +1,5 @@ import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import {checkTeamsLimit} from '../../../billing/helpers/teamLimitsCheck' import TimelineEventTeamPromptComplete from '../../../database/types/TimelineEventTeamPromptComplete' import getKysely from '../../../postgres/getKysely' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' @@ -88,7 +87,6 @@ const safeEndTeamPrompt = async ({ await pg.insertInto('TimelineEvent').values(events).execute() summarizeTeamPrompt(meeting, context) analytics.teamPromptEnd(completedTeamPrompt, meetingMembers, responses, dataLoader) - checkTeamsLimit(team.orgId, dataLoader) dataLoader.get('newMeetings').clear(meetingId) const data = {