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 { 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");
|
||||||
|
|||||||
Reference in New Issue
Block a user