Configure Email Usage Alert Logic (#278)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
KM Koushik
2025-10-25 05:37:46 +11:00
committed by GitHub
parent 374f173a09
commit 1c9056ba75
+27 -19
View File
@@ -5,12 +5,17 @@ import { TeamService } from "./team-service";
import { withCache } from "../redis"; import { withCache } from "../redis";
import { db } from "../db"; import { db } from "../db";
import { logger } from "../logger/log"; import { logger } from "../logger/log";
import { Plan } from "@prisma/client";
function isLimitExceeded(current: number, limit: number): boolean { function isLimitExceeded(current: number, limit: number): boolean {
if (limit === -1) return false; // unlimited if (limit === -1) return false; // unlimited
return current >= limit; return current >= limit;
} }
function getActivePlan(team: { plan: string; isActive: boolean }): Plan {
return team.isActive ? team.plan : "FREE";
}
export class LimitService { export class LimitService {
static async checkDomainLimit(teamId: number): Promise<{ static async checkDomainLimit(teamId: number): Promise<{
isLimitReached: boolean; isLimitReached: boolean;
@@ -25,7 +30,7 @@ export class LimitService {
const team = await TeamService.getTeamCached(teamId); const team = await TeamService.getTeamCached(teamId);
const currentCount = await db.domain.count({ where: { 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)) { if (isLimitExceeded(currentCount, limit)) {
return { return {
isLimitReached: true, isLimitReached: true,
@@ -53,7 +58,7 @@ export class LimitService {
const team = await TeamService.getTeamCached(teamId); const team = await TeamService.getTeamCached(teamId);
const currentCount = await db.contactBook.count({ where: { 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)) { if (isLimitExceeded(currentCount, limit)) {
return { return {
isLimitReached: true, isLimitReached: true,
@@ -81,7 +86,7 @@ export class LimitService {
const team = await TeamService.getTeamCached(teamId); const team = await TeamService.getTeamCached(teamId);
const currentCount = await db.teamUser.count({ where: { 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)) { if (isLimitExceeded(currentCount, limit)) {
return { return {
isLimitReached: true, isLimitReached: true,
@@ -100,6 +105,7 @@ export class LimitService {
// Side effects: // Side effects:
// - Sends "warning" emails when nearing daily/monthly limits (rate-limited in TeamService) // - Sends "warning" emails when nearing daily/monthly limits (rate-limited in TeamService)
// - Sends "limit reached" notifications when limits are exceeded (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<{ static async checkEmailLimit(teamId: number): Promise<{
isLimitReached: boolean; isLimitReached: boolean;
limit: number; limit: number;
@@ -126,7 +132,7 @@ export class LimitService {
const usage = await withCache( const usage = await withCache(
`usage:this-month:${teamId}`, `usage:this-month:${teamId}`,
() => getThisMonthUsage(teamId), () => getThisMonthUsage(teamId),
{ ttlSeconds: 60 } { ttlSeconds: 60 },
); );
const dailyUsage = usage.day.reduce((acc, curr) => acc + curr.sent, 0); const dailyUsage = usage.day.reduce((acc, curr) => acc + curr.sent, 0);
@@ -137,7 +143,7 @@ export class LimitService {
logger.info( logger.info(
{ dailyUsage, dailyLimit, team }, { dailyUsage, dailyLimit, team },
`[LimitService]: Daily usage and limit` `[LimitService]: Daily usage and limit`,
); );
if (isLimitExceeded(dailyUsage, dailyLimit)) { if (isLimitExceeded(dailyUsage, dailyLimit)) {
@@ -146,12 +152,12 @@ export class LimitService {
await TeamService.maybeNotifyEmailLimitReached( await TeamService.maybeNotifyEmailLimitReached(
teamId, teamId,
dailyLimit, dailyLimit,
LimitReason.EMAIL_DAILY_LIMIT_REACHED LimitReason.EMAIL_DAILY_LIMIT_REACHED,
); );
} catch (e) { } catch (e) {
logger.warn( logger.warn(
{ err: e }, { 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( const monthlyUsage = usage.month.reduce(
(acc, curr) => acc + curr.sent, (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( logger.info(
{ monthlyUsage, monthlyLimit, team }, { monthlyUsage, monthlyLimit, team, isActive: team.isActive },
`[LimitService]: Monthly usage and limit` `[LimitService]: Monthly usage and limit (FREE plan or inactive subscription)`,
); );
if (monthlyUsage / monthlyLimit > 0.8 && monthlyUsage < monthlyLimit) { if (monthlyUsage / monthlyLimit > 0.8 && monthlyUsage < monthlyLimit) {
@@ -180,27 +188,27 @@ export class LimitService {
teamId, teamId,
monthlyUsage, monthlyUsage,
monthlyLimit, monthlyLimit,
LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED,
); );
} }
logger.info( logger.info(
{ monthlyUsage, monthlyLimit, team }, { monthlyUsage, monthlyLimit, team, isActive: team.isActive },
`[LimitService]: Monthly usage and limit` `[LimitService]: Monthly usage and limit (FREE plan or inactive subscription)`,
); );
if (isLimitExceeded(monthlyUsage, monthlyLimit)) { if (isLimitExceeded(monthlyUsage, monthlyLimit)) {
// Notify: monthly (free plan) limit reached // Notify: monthly (free plan or inactive subscription) limit reached
try { try {
await TeamService.maybeNotifyEmailLimitReached( await TeamService.maybeNotifyEmailLimitReached(
teamId, teamId,
monthlyLimit, monthlyLimit,
LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED,
); );
} catch (e) { } catch (e) {
logger.warn( logger.warn(
{ err: e }, { err: e },
"Failed to send monthly limit reached notification" "Failed to send monthly limit reached notification",
); );
} }
@@ -225,7 +233,7 @@ export class LimitService {
teamId, teamId,
dailyUsage, dailyUsage,
dailyLimit, dailyLimit,
LimitReason.EMAIL_DAILY_LIMIT_REACHED LimitReason.EMAIL_DAILY_LIMIT_REACHED,
); );
} catch (e) { } catch (e) {
logger.warn({ err: e }, "Failed to send daily warning email"); logger.warn({ err: e }, "Failed to send daily warning email");