From 1c9056ba75d9765af86b6bd3cc1cbf4a239acc34 Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Sat, 25 Oct 2025 05:37:46 +1100 Subject: [PATCH] Configure Email Usage Alert Logic (#278) Co-authored-by: Claude --- apps/web/src/server/service/limit-service.ts | 46 ++++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/apps/web/src/server/service/limit-service.ts b/apps/web/src/server/service/limit-service.ts index 8aa5d1e..45f0fcb 100644 --- a/apps/web/src/server/service/limit-service.ts +++ b/apps/web/src/server/service/limit-service.ts @@ -5,12 +5,17 @@ import { TeamService } from "./team-service"; import { withCache } from "../redis"; import { db } from "../db"; import { logger } from "../logger/log"; +import { Plan } from "@prisma/client"; function isLimitExceeded(current: number, limit: number): boolean { if (limit === -1) return false; // unlimited return current >= limit; } +function getActivePlan(team: { plan: string; isActive: boolean }): Plan { + return team.isActive ? team.plan : "FREE"; +} + export class LimitService { static async checkDomainLimit(teamId: number): Promise<{ isLimitReached: boolean; @@ -25,7 +30,7 @@ export class LimitService { const team = await TeamService.getTeamCached(teamId); const currentCount = await db.domain.count({ where: { teamId } }); - const limit = PLAN_LIMITS[team.plan].domains; + const limit = PLAN_LIMITS[getActivePlan(team)].domains; if (isLimitExceeded(currentCount, limit)) { return { isLimitReached: true, @@ -53,7 +58,7 @@ export class LimitService { const team = await TeamService.getTeamCached(teamId); const currentCount = await db.contactBook.count({ where: { teamId } }); - const limit = PLAN_LIMITS[team.plan].contactBooks; + const limit = PLAN_LIMITS[getActivePlan(team)].contactBooks; if (isLimitExceeded(currentCount, limit)) { return { isLimitReached: true, @@ -81,7 +86,7 @@ export class LimitService { const team = await TeamService.getTeamCached(teamId); const currentCount = await db.teamUser.count({ where: { teamId } }); - const limit = PLAN_LIMITS[team.plan].teamMembers; + const limit = PLAN_LIMITS[getActivePlan(team)].teamMembers; if (isLimitExceeded(currentCount, limit)) { return { isLimitReached: true, @@ -100,6 +105,7 @@ export class LimitService { // Side effects: // - Sends "warning" emails when nearing daily/monthly limits (rate-limited in TeamService) // - Sends "limit reached" notifications when limits are exceeded (rate-limited in TeamService) + // - Teams with inactive subscriptions are treated like FREE plans for monthly limit alerts static async checkEmailLimit(teamId: number): Promise<{ isLimitReached: boolean; limit: number; @@ -126,7 +132,7 @@ export class LimitService { const usage = await withCache( `usage:this-month:${teamId}`, () => getThisMonthUsage(teamId), - { ttlSeconds: 60 } + { ttlSeconds: 60 }, ); const dailyUsage = usage.day.reduce((acc, curr) => acc + curr.sent, 0); @@ -137,7 +143,7 @@ export class LimitService { logger.info( { dailyUsage, dailyLimit, team }, - `[LimitService]: Daily usage and limit` + `[LimitService]: Daily usage and limit`, ); if (isLimitExceeded(dailyUsage, dailyLimit)) { @@ -146,12 +152,12 @@ export class LimitService { await TeamService.maybeNotifyEmailLimitReached( teamId, dailyLimit, - LimitReason.EMAIL_DAILY_LIMIT_REACHED + LimitReason.EMAIL_DAILY_LIMIT_REACHED, ); } catch (e) { logger.warn( { err: e }, - "Failed to send daily limit reached notification" + "Failed to send daily limit reached notification", ); } @@ -163,16 +169,18 @@ export class LimitService { }; } - if (team.plan === "FREE") { + // Apply monthly limit logic for FREE plan or inactive subscriptions + if (getActivePlan(team) === "FREE") { const monthlyUsage = usage.month.reduce( (acc, curr) => acc + curr.sent, - 0 + 0, ); - const monthlyLimit = PLAN_LIMITS[team.plan].emailsPerMonth; + // Use FREE plan limits for inactive subscriptions + const monthlyLimit = PLAN_LIMITS.FREE.emailsPerMonth; logger.info( - { monthlyUsage, monthlyLimit, team }, - `[LimitService]: Monthly usage and limit` + { monthlyUsage, monthlyLimit, team, isActive: team.isActive }, + `[LimitService]: Monthly usage and limit (FREE plan or inactive subscription)`, ); if (monthlyUsage / monthlyLimit > 0.8 && monthlyUsage < monthlyLimit) { @@ -180,27 +188,27 @@ export class LimitService { teamId, monthlyUsage, monthlyLimit, - LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED + LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED, ); } logger.info( - { monthlyUsage, monthlyLimit, team }, - `[LimitService]: Monthly usage and limit` + { monthlyUsage, monthlyLimit, team, isActive: team.isActive }, + `[LimitService]: Monthly usage and limit (FREE plan or inactive subscription)`, ); if (isLimitExceeded(monthlyUsage, monthlyLimit)) { - // Notify: monthly (free plan) limit reached + // Notify: monthly (free plan or inactive subscription) limit reached try { await TeamService.maybeNotifyEmailLimitReached( teamId, monthlyLimit, - LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED + LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED, ); } catch (e) { logger.warn( { err: e }, - "Failed to send monthly limit reached notification" + "Failed to send monthly limit reached notification", ); } @@ -225,7 +233,7 @@ export class LimitService { teamId, dailyUsage, dailyLimit, - LimitReason.EMAIL_DAILY_LIMIT_REACHED + LimitReason.EMAIL_DAILY_LIMIT_REACHED, ); } catch (e) { logger.warn({ err: e }, "Failed to send daily warning email");