Configure Email Usage Alert Logic (#278)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user