diff --git a/packages/backend/src/api/webhooks/stripe.ts b/packages/backend/src/api/webhooks/stripe.ts index 9d95f76d..c51ca029 100644 --- a/packages/backend/src/api/webhooks/stripe.ts +++ b/packages/backend/src/api/webhooks/stripe.ts @@ -70,7 +70,7 @@ const updateSubscription = async (object: Stripe.Subscription) => { const { customer, cancel_at_period_end, metadata, id, cancellation_details } = object - const plan = metadata.plan + const plan = metadata.plan || "team" const period = metadata.period const canceled = cancel_at_period_end diff --git a/packages/backend/src/jobs/resetUsage.ts b/packages/backend/src/jobs/resetUsage.ts index 8cbf0c95..dfd3d86a 100644 --- a/packages/backend/src/jobs/resetUsage.ts +++ b/packages/backend/src/jobs/resetUsage.ts @@ -1,9 +1,11 @@ import sql from "@/src/utils/db" import { sendTelegramMessage } from "@/src/utils/notifications" +import { sendEmail } from "../utils/sendEmail" +import { LIMITED_EMAIL } from "../utils/emails" async function updateLimitedStatus() { // set limited = false for all users that have been under the limit - // for the last 3 days + // for the last 30 days const alreadyLimited = await sql`UPDATE "public"."org" p SET limited = false WHERE limited = true AND p.id NOT IN ( @@ -11,35 +13,26 @@ async function updateLimitedStatus() { FROM "public"."org" o JOIN "public"."project" p ON p.org_id = o.id JOIN "public"."run" r ON r.project_id = p.id - WHERE r.created_at >= CURRENT_DATE - INTERVAL '3 days' + WHERE r.created_at >= CURRENT_DATE - INTERVAL '30 days' GROUP BY o.id - HAVING COUNT(r.id) > 1000 + HAVING COUNT(r.id) > 10000 ) RETURNING *; ` - // get all free users with more than 1000 runs 2 out of the last 3 days + // get all free users with more than 10000 runs in the last 30 days // and set their `limited` to true - const orgsToLimit = await sql`WITH over_limit_days AS ( + const orgsToLimit = await sql`WITH over_limit_users AS ( SELECT o.id, o.name, - DATE(r.created_at) AS run_date, - COUNT(r.id) AS daily_runs + COUNT(r.id) AS total_runs FROM "public"."org" o JOIN "public"."project" p ON p.org_id = o.id JOIN "public"."run" r ON r.project_id = p.id - WHERE r.created_at >= CURRENT_DATE - INTERVAL '3 days' - GROUP BY o.id, o.name, run_date - HAVING COUNT(r.id) > 1000 AND o.plan = 'free' -), -over_limit_users AS ( - SELECT - id, - COUNT(run_date) AS limit_exceeded_days - FROM over_limit_days - GROUP BY id - HAVING COUNT(run_date) >= 2 + WHERE r.created_at >= CURRENT_DATE - INTERVAL '30 days' AND o.plan = 'free' AND o.limited = false + GROUP BY o.id, o.name + HAVING COUNT(r.id) > 10000 ) UPDATE "public"."org" SET limited = TRUE @@ -47,20 +40,33 @@ WHERE id IN (SELECT id FROM over_limit_users) RETURNING *;` for (const org of orgsToLimit) { - // send telegram message to user - if (alreadyLimited.find((u) => u.id === org.id)) continue + try { + // send telegram message to user + if (alreadyLimited.find((u) => u.id === org.id)) continue + + await sendTelegramMessage( + `⛔ limited ${org.name} because too many events`, + "users", + ) + + // send email to user + + const users = + await sql`SELECT email, name FROM account WHERE org_id = ${org.id};` - await sendTelegramMessage( - `⛔ limited ${org.name} because too many events`, - "users", - ) + for (const user of users) { + await sendEmail(LIMITED_EMAIL(user.email, user.name)) + } + } catch (error) { + console.error(error) + } } } // reset playground allowance async function resetPlaygroundAllowance() { await sql`UPDATE "public"."org" o SET play_allowance = 3 WHERE o.plan = 'free';` - await sql`UPDATE "public"."org" o SET play_allowance = 1000 WHERE o.plan = 'pro';` + await sql`UPDATE "public"."org" o SET play_allowance = 1000 WHERE o.plan = 'pro' OR o.plan = 'team';` await sql`UPDATE "public"."org" o SET play_allowance = 1000 WHERE o.plan = 'unlimited' OR o.plan = 'custom';` } diff --git a/packages/backend/src/utils/emails.ts b/packages/backend/src/utils/emails.ts index 6dd141ea..f9fbc42e 100644 --- a/packages/backend/src/utils/emails.ts +++ b/packages/backend/src/utils/emails.ts @@ -144,7 +144,7 @@ Would you mind telling us why you canceled? We're always looking to improve. Thank you for trying Lunary. -Vince & Hugh - co-founders of Lunary`, +Vince & Hugh - founders of Lunary`, } } @@ -156,7 +156,7 @@ export function FULLY_CANCELED_EMAIL(email: string, name: string) { from: process.env.GENERIC_SENDER, text: `Hi ${extractFirstName(name)}, -Your account has been downgraded to the free plan +Your account has been downgraded to the free plan. Would you mind telling us why you canceled? We're always looking to improve. @@ -172,6 +172,28 @@ If this was a mistake, you can upgrade again at any time here: https://app.lunar Thank you for trying Lunary. -Vince & Hugh - co-founders of Lunary`, +Vince & Hugh - founders of Lunary`, + } +} + +export function LIMITED_EMAIL(email: string, name: string) { + return { + subject: `Action Required: Events limit reached`, + to: [email], + reply_to: "hello@lunary.ai", + from: process.env.GENERIC_SENDER, + text: `Hi ${extractFirstName(name)}, + +Congratulations! You've reached your free ingested event limit for the month, which means you're making great use of Lunary. + +As a result, your account has been temporarily limited (don't worry, your data is safe and sound). + +To continue enjoying our services without interruption, please consider upgrading your account here: https://app.lunary.ai/billing + +If you have any questions, feel free to reply to this email. + +Thank you for being a part of Lunary. + +- The Lunary team`, } }