2023-10-26 15:41:51 +02:00
import { Plan , PrismaClient , WorkspaceRole } from '@typebot.io/prisma'
2023-10-17 08:03:30 +02:00
import { isDefined , isEmpty } from '@typebot.io/lib'
2024-05-23 10:42:23 +02:00
import { getChatsLimit } from '@typebot.io/billing/helpers/getChatsLimit'
2023-06-06 13:25:13 +02:00
import { promptAndSetEnvironment } from './utils'
import { Workspace } from '@typebot.io/schemas'
import { sendAlmostReachedChatsLimitEmail } from '@typebot.io/emails/src/emails/AlmostReachedChatsLimitEmail'
import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry'
2024-03-15 16:32:29 +01:00
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
2023-10-17 08:03:30 +02:00
import Stripe from 'stripe'
import { createId } from '@paralleldrive/cuid2'
2023-06-06 13:25:13 +02:00
const prisma = new PrismaClient ( )
2023-10-17 08:03:30 +02:00
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.75
2023-06-06 13:25:13 +02:00
2023-10-17 08:03:30 +02:00
export const checkAndReportChatsUsage = async ( ) = > {
2023-06-06 13:25:13 +02:00
await promptAndSetEnvironment ( 'production' )
console . log ( 'Get collected results from the last hour...' )
2023-10-17 08:03:30 +02:00
const zeroedMinutesHour = new Date ( )
zeroedMinutesHour . setUTCMinutes ( 0 , 0 , 0 )
const hourAgo = new Date ( zeroedMinutesHour . getTime ( ) - 1000 * 60 * 60 )
2023-06-06 13:25:13 +02:00
const results = await prisma . result . groupBy ( {
by : [ 'typebotId' ] ,
_count : {
_all : true ,
} ,
where : {
hasStarted : true ,
createdAt : {
2023-10-17 08:03:30 +02:00
lt : zeroedMinutesHour ,
2023-06-06 13:25:13 +02:00
gte : hourAgo ,
} ,
} ,
} )
console . log (
` Found ${ results . reduce (
( total , result ) = > total + result . _count . _all ,
0
) } results collected for the last hour . `
)
const workspaces = await prisma . workspace . findMany ( {
where : {
typebots : {
some : {
id : { in : results . map ( ( result ) = > result . typebotId ) } ,
} ,
} ,
} ,
select : {
id : true ,
2023-10-17 08:03:30 +02:00
name : true ,
2023-06-06 13:25:13 +02:00
typebots : { select : { id : true } } ,
members : {
select : { user : { select : { id : true , email : true } } , role : true } ,
} ,
additionalStorageIndex : true ,
customChatsLimit : true ,
customStorageLimit : true ,
plan : true ,
isQuarantined : true ,
chatsLimitFirstEmailSentAt : true ,
chatsLimitSecondEmailSentAt : true ,
2023-10-17 08:03:30 +02:00
stripeId : true ,
2023-06-06 13:25:13 +02:00
} ,
} )
2023-10-17 08:53:09 +02:00
if ( isEmpty ( process . env . STRIPE_SECRET_KEY ) )
throw new Error ( 'Missing STRIPE_SECRET_KEY env variable' )
2023-10-17 08:03:30 +02:00
2023-10-17 08:53:09 +02:00
const stripe = new Stripe ( process . env . STRIPE_SECRET_KEY , {
apiVersion : '2022-11-15' ,
} )
2023-06-06 13:25:13 +02:00
2023-10-17 08:53:09 +02:00
const quarantineEvents : TelemetryEvent [ ] = [ ]
2023-11-14 16:00:10 +01:00
const autoUpgradeEvents : TelemetryEvent [ ] = [ ]
2023-06-06 13:25:13 +02:00
2023-10-26 15:41:51 +02:00
for ( const workspace of workspaces ) {
if ( workspace . isQuarantined ) continue
const chatsLimit = getChatsLimit ( workspace )
const subscription = await getSubscription ( workspace , { stripe } )
2023-10-17 08:53:09 +02:00
const { totalChatsUsed } = await getUsage ( prisma ) ( {
2023-10-26 15:41:51 +02:00
workspaceId : workspace.id ,
2023-10-17 08:53:09 +02:00
subscription ,
} )
2023-10-25 17:57:13 +02:00
if ( chatsLimit === 'inf' ) continue
2023-06-06 13:25:13 +02:00
if (
chatsLimit > 0 &&
totalChatsUsed >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT &&
totalChatsUsed < chatsLimit &&
2023-10-26 15:41:51 +02:00
! workspace . chatsLimitFirstEmailSentAt
2023-06-06 13:25:13 +02:00
) {
2023-10-26 15:41:51 +02:00
const to = workspace . members
2023-08-31 10:19:32 +02:00
. filter ( ( member ) = > member . role === WorkspaceRole . ADMIN )
2023-06-06 13:25:13 +02:00
. map ( ( member ) = > member . user . email )
. filter ( isDefined )
console . log (
` Send almost reached chats limit email to ${ to . join ( ', ' ) } ... `
)
2023-06-26 09:40:26 +02:00
try {
await sendAlmostReachedChatsLimitEmail ( {
to ,
usagePercent : Math.round ( ( totalChatsUsed / chatsLimit ) * 100 ) ,
chatsLimit ,
2023-10-26 15:41:51 +02:00
workspaceName : workspace.name ,
2023-06-26 09:40:26 +02:00
} )
2023-07-16 18:52:30 +02:00
await prisma . workspace . updateMany ( {
2023-10-26 15:41:51 +02:00
where : { id : workspace.id } ,
2023-06-26 09:40:26 +02:00
data : { chatsLimitFirstEmailSentAt : new Date ( ) } ,
} )
} catch ( err ) {
console . error ( err )
}
2023-06-06 13:25:13 +02:00
}
2023-10-17 08:53:09 +02:00
const isUsageBasedSubscription = isDefined (
subscription ? . items . data . find (
( item ) = >
item . price . id === process . env . STRIPE_STARTER_PRICE_ID ||
item . price . id === process . env . STRIPE_PRO_PRICE_ID
)
)
if (
isUsageBasedSubscription &&
subscription &&
2023-10-26 15:41:51 +02:00
( workspace . plan === 'STARTER' || workspace . plan === 'PRO' )
2023-10-17 08:53:09 +02:00
) {
2023-10-26 15:41:51 +02:00
if ( workspace . plan === 'STARTER' && totalChatsUsed >= 4000 ) {
2023-10-17 08:53:09 +02:00
console . log (
'Workspace has more than 4000 chats, automatically upgrading to PRO plan'
)
const newSubscription = await autoUpgradeToPro ( subscription , {
stripe ,
2023-10-26 15:41:51 +02:00
workspaceId : workspace.id ,
2023-10-17 08:53:09 +02:00
} )
2023-11-14 16:00:10 +01:00
autoUpgradeEvents . push (
. . . workspace . members
. filter ( ( member ) = > member . role === WorkspaceRole . ADMIN )
. map (
( member ) = >
( {
name : 'Subscription automatically updated' ,
userId : member.user.id ,
workspaceId : workspace.id ,
data : {
plan : 'PRO' ,
} ,
} satisfies TelemetryEvent )
)
)
2023-10-17 08:53:09 +02:00
await reportUsageToStripe ( totalChatsUsed , {
stripe ,
subscription : newSubscription ,
} )
} else {
await reportUsageToStripe ( totalChatsUsed , { stripe , subscription } )
}
}
2024-09-12 13:59:22 +02:00
// if (totalChatsUsed > chatsLimit * 1.5 && workspace.plan === Plan.FREE) { //hä ist der Chatlimit mal 1,5?
if ( totalChatsUsed > chatsLimit &&
( workspace . plan === Plan . FREE || workspace . plan === Plan . STARTER || workspace . plan === Plan . PRO || workspace . plan === Plan . CUSTOM ) ) { //ich hab den faktor entfernt und es für den Plan FREE, STARTER, PRO und CUSTOM gesetzt, sodass bei erreichen des Limits der Bot offline geschlatet wird (quarantäne)
2023-10-26 15:41:51 +02:00
console . log ( ` Automatically quarantine workspace ${ workspace . id } ... ` )
2023-07-16 18:52:30 +02:00
await prisma . workspace . updateMany ( {
2023-10-26 15:41:51 +02:00
where : { id : workspace.id } ,
2023-06-06 13:25:13 +02:00
data : { isQuarantined : true } ,
} )
2023-10-17 08:53:09 +02:00
quarantineEvents . push (
2023-10-26 15:41:51 +02:00
. . . workspace . members
2023-06-06 13:25:13 +02:00
. filter ( ( member ) = > member . role === WorkspaceRole . ADMIN )
. map (
( member ) = >
( {
name : 'Workspace automatically quarantined' ,
userId : member.user.id ,
2023-10-26 15:41:51 +02:00
workspaceId : workspace.id ,
2023-06-06 13:25:13 +02:00
data : {
totalChatsUsed ,
chatsLimit ,
} ,
} satisfies TelemetryEvent )
)
)
}
}
2023-10-17 08:53:09 +02:00
2023-10-26 15:41:51 +02:00
const resultsWithWorkspaces = results
. flatMap ( ( result ) = > {
const workspace = workspaces . find ( ( workspace ) = >
workspace . typebots . some ( ( typebot ) = > typebot . id === result . typebotId )
)
if ( ! workspace ) return
return workspace . members
. filter ( ( member ) = > member . role !== WorkspaceRole . GUEST )
. map ( ( member , memberIndex ) = > ( {
userId : member.user.id ,
workspace : workspace ,
typebotId : result.typebotId ,
totalResultsLastHour : result._count._all ,
isFirstOfKind : memberIndex === 0 ? ( true as const ) : undefined ,
} ) )
} )
. filter ( isDefined )
2023-10-17 08:53:09 +02:00
const newResultsCollectedEvents = resultsWithWorkspaces . map (
( result ) = >
( {
name : 'New results collected' ,
userId : result.userId ,
workspaceId : result.workspace.id ,
typebotId : result.typebotId ,
data : {
total : result.totalResultsLastHour ,
isFirstOfKind : result.isFirstOfKind ,
} ,
} satisfies TelemetryEvent )
)
console . log (
` Send ${ newResultsCollectedEvents . length } new results events and ${ quarantineEvents . length } auto quarantine events... `
)
2024-02-01 14:19:24 +01:00
await trackEvents ( quarantineEvents . concat ( newResultsCollectedEvents ) )
2023-10-17 08:53:09 +02:00
}
const getSubscription = async (
workspace : Pick < Workspace , 'stripeId' | 'plan' > ,
{ stripe } : { stripe : Stripe }
) = > {
if (
! workspace . stripeId ||
( workspace . plan !== 'STARTER' && workspace . plan !== 'PRO' )
)
return
const subscriptions = await stripe . subscriptions . list ( {
customer : workspace.stripeId ,
} )
const currentSubscription = subscriptions . data
. filter ( ( sub ) = > [ 'past_due' , 'active' ] . includes ( sub . status ) )
. sort ( ( a , b ) = > a . created - b . created )
. shift ( )
return currentSubscription
2023-06-06 13:25:13 +02:00
}
2023-10-17 08:03:30 +02:00
const reportUsageToStripe = async (
2023-10-17 08:53:09 +02:00
totalResultsLastHour : number ,
{
stripe ,
subscription ,
} : { stripe : Stripe ; subscription : Stripe.Subscription }
2023-10-17 08:03:30 +02:00
) = > {
2023-10-17 08:53:09 +02:00
if (
! process . env . STRIPE_STARTER_CHATS_PRICE_ID ||
! process . env . STRIPE_PRO_CHATS_PRICE_ID
)
throw new Error (
'Missing STRIPE_STARTER_CHATS_PRICE_ID or STRIPE_PRO_CHATS_PRICE_ID env variable'
)
const subscriptionItem = subscription . items . data . find (
( item ) = >
item . price . id === process . env . STRIPE_STARTER_CHATS_PRICE_ID ||
item . price . id === process . env . STRIPE_PRO_CHATS_PRICE_ID
)
2023-10-17 08:03:30 +02:00
2023-10-17 08:53:09 +02:00
if ( ! subscriptionItem )
throw new Error ( ` Could not find subscription item for workspace ` )
2023-10-17 08:03:30 +02:00
2023-10-17 08:53:09 +02:00
const idempotencyKey = createId ( )
return stripe . subscriptionItems . createUsageRecord (
subscriptionItem . id ,
{
quantity : totalResultsLastHour ,
timestamp : 'now' ,
} ,
{
idempotencyKey ,
}
)
}
const getUsage =
( prisma : PrismaClient ) = >
async ( {
workspaceId ,
subscription ,
} : {
workspaceId : string
subscription : Stripe.Subscription | undefined
} ) = > {
const typebots = await prisma . typebot . findMany ( {
where : {
workspaceId ,
} ,
select : {
id : true ,
} ,
2023-10-17 08:03:30 +02:00
} )
2023-10-17 08:53:09 +02:00
const now = new Date ( )
const firstDayOfMonth = new Date ( now . getFullYear ( ) , now . getMonth ( ) , 1 )
2023-10-17 08:03:30 +02:00
2023-10-17 08:53:09 +02:00
const totalChatsUsed = await prisma . result . count ( {
where : {
typebotId : { in : typebots . map ( ( typebot ) = > typebot . id ) } ,
hasStarted : true ,
createdAt : {
gte : subscription
? new Date ( subscription . current_period_start * 1000 )
: firstDayOfMonth ,
} ,
} ,
} )
2023-10-17 08:03:30 +02:00
2023-10-17 08:53:09 +02:00
return {
totalChatsUsed ,
}
}
2023-10-17 08:03:30 +02:00
2023-10-17 08:53:09 +02:00
const autoUpgradeToPro = async (
subscription : Stripe.Subscription ,
{ stripe , workspaceId } : { stripe : Stripe ; workspaceId : string }
) = > {
if (
! process . env . STRIPE_STARTER_CHATS_PRICE_ID ||
! process . env . STRIPE_PRO_CHATS_PRICE_ID ||
! process . env . STRIPE_PRO_PRICE_ID ||
! process . env . STRIPE_STARTER_PRICE_ID
)
throw new Error (
'Missing STRIPE_STARTER_CHATS_PRICE_ID or STRIPE_PRO_CHATS_PRICE_ID env variable'
)
const currentPlanItemId = subscription ? . items . data . find ( ( item ) = >
[
process . env . STRIPE_PRO_PRICE_ID ,
process . env . STRIPE_STARTER_PRICE_ID ,
] . includes ( item . price . id )
) ? . id
2023-10-17 08:03:30 +02:00
2023-10-17 08:53:09 +02:00
if ( ! currentPlanItemId )
throw new Error ( ` Could not find current plan item ID for workspace ` )
2023-10-17 08:03:30 +02:00
2023-10-17 08:53:09 +02:00
const newSubscription = stripe . subscriptions . update ( subscription . id , {
items : [
2023-10-17 08:03:30 +02:00
{
2023-10-17 08:53:09 +02:00
id : currentPlanItemId ,
price : process.env.STRIPE_PRO_PRICE_ID ,
quantity : 1 ,
2023-10-17 08:03:30 +02:00
} ,
{
2023-10-17 08:53:09 +02:00
id : subscription.items.data.find (
( item ) = >
item . price . id === process . env . STRIPE_STARTER_CHATS_PRICE_ID ||
item . price . id === process . env . STRIPE_PRO_CHATS_PRICE_ID
) ? . id ,
price : process.env.STRIPE_PRO_CHATS_PRICE_ID ,
} ,
] ,
proration_behavior : 'always_invoice' ,
} )
await prisma . workspace . update ( {
where : { id : workspaceId } ,
data : {
plan : 'PRO' ,
} ,
} )
return newSubscription
2023-10-17 08:03:30 +02:00
}
checkAndReportChatsUsage ( ) . then ( )