import { PLAN_LIMITS, LimitReason } from "~/lib/constants/plans"; import { env } from "~/env"; import { getThisMonthUsage } from "./usage-service"; import { TeamService } from "./team-service"; import { withCache } from "../redis"; import { db } from "../db"; import { logger } from "../logger/log"; function isLimitExceeded(current: number, limit: number): boolean { if (limit === -1) return false; // unlimited return current >= limit; } export class LimitService { static async checkDomainLimit(teamId: number): Promise<{ isLimitReached: boolean; limit: number; reason?: LimitReason; }> { // Limits only apply in cloud mode if (!env.NEXT_PUBLIC_IS_CLOUD) { return { isLimitReached: false, limit: -1 }; } const team = await TeamService.getTeamCached(teamId); const currentCount = await db.domain.count({ where: { teamId } }); const limit = PLAN_LIMITS[team.plan].domains; if (isLimitExceeded(currentCount, limit)) { return { isLimitReached: true, limit, reason: LimitReason.DOMAIN, }; } return { isLimitReached: false, limit, }; } static async checkContactBookLimit(teamId: number): Promise<{ isLimitReached: boolean; limit: number; reason?: LimitReason; }> { // Limits only apply in cloud mode if (!env.NEXT_PUBLIC_IS_CLOUD) { return { isLimitReached: false, limit: -1 }; } const team = await TeamService.getTeamCached(teamId); const currentCount = await db.contactBook.count({ where: { teamId } }); const limit = PLAN_LIMITS[team.plan].contactBooks; if (isLimitExceeded(currentCount, limit)) { return { isLimitReached: true, limit, reason: LimitReason.CONTACT_BOOK, }; } return { isLimitReached: false, limit, }; } static async checkTeamMemberLimit(teamId: number): Promise<{ isLimitReached: boolean; limit: number; reason?: LimitReason; }> { // Limits only apply in cloud mode if (!env.NEXT_PUBLIC_IS_CLOUD) { return { isLimitReached: false, limit: -1 }; } const team = await TeamService.getTeamCached(teamId); const currentCount = await db.teamUser.count({ where: { teamId } }); const limit = PLAN_LIMITS[team.plan].teamMembers; if (isLimitExceeded(currentCount, limit)) { return { isLimitReached: true, limit, reason: LimitReason.TEAM_MEMBER, }; } return { isLimitReached: false, limit, }; } // Checks email sending limits and also triggers usage notifications. // 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) static async checkEmailLimit(teamId: number): Promise<{ isLimitReached: boolean; limit: number; reason?: LimitReason; available?: number; }> { // Limits only apply in cloud mode if (!env.NEXT_PUBLIC_IS_CLOUD) { return { isLimitReached: false, limit: -1 }; } const team = await TeamService.getTeamCached(teamId); // In cloud, enforce verification and block flags first if (team.isBlocked) { return { isLimitReached: true, limit: 0, reason: LimitReason.EMAIL_BLOCKED, }; } // Enforce daily sending limit (team-specific) const usage = await withCache( `usage:this-month:${teamId}`, () => getThisMonthUsage(teamId), { ttlSeconds: 60 } ); const dailyUsage = usage.day.reduce((acc, curr) => acc + curr.sent, 0); const dailyLimit = team.plan !== "FREE" ? team.dailyEmailLimit : PLAN_LIMITS[team.plan].emailsPerDay; logger.info( { dailyUsage, dailyLimit, team }, `[LimitService]: Daily usage and limit` ); if (isLimitExceeded(dailyUsage, dailyLimit)) { // Notify: daily limit reached try { await TeamService.maybeNotifyEmailLimitReached( teamId, dailyLimit, LimitReason.EMAIL_DAILY_LIMIT_REACHED ); } catch (e) { logger.warn( { err: e }, "Failed to send daily limit reached notification" ); } return { isLimitReached: true, limit: dailyLimit, reason: LimitReason.EMAIL_DAILY_LIMIT_REACHED, available: dailyLimit - dailyUsage, }; } if (team.plan === "FREE") { const monthlyUsage = usage.month.reduce( (acc, curr) => acc + curr.sent, 0 ); const monthlyLimit = PLAN_LIMITS[team.plan].emailsPerMonth; logger.info( { monthlyUsage, monthlyLimit, team }, `[LimitService]: Monthly usage and limit` ); if (monthlyUsage / monthlyLimit > 0.8 && monthlyUsage < monthlyLimit) { await TeamService.sendWarningEmail( teamId, monthlyUsage, monthlyLimit, LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED ); } logger.info( { monthlyUsage, monthlyLimit, team }, `[LimitService]: Monthly usage and limit` ); if (isLimitExceeded(monthlyUsage, monthlyLimit)) { // Notify: monthly (free plan) limit reached try { await TeamService.maybeNotifyEmailLimitReached( teamId, monthlyLimit, LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED ); } catch (e) { logger.warn( { err: e }, "Failed to send monthly limit reached notification" ); } return { isLimitReached: true, limit: monthlyLimit, reason: LimitReason.EMAIL_FREE_PLAN_MONTHLY_LIMIT_REACHED, available: monthlyLimit - monthlyUsage, }; } } // Warn: nearing daily limit (e.g., < 20% available) if ( dailyLimit !== -1 && dailyLimit > 0 && dailyLimit - dailyUsage > 0 && (dailyLimit - dailyUsage) / dailyLimit < 0.2 ) { try { await TeamService.sendWarningEmail( teamId, dailyUsage, dailyLimit, LimitReason.EMAIL_DAILY_LIMIT_REACHED ); } catch (e) { logger.warn({ err: e }, "Failed to send daily warning email"); } } return { isLimitReached: false, limit: dailyLimit, available: dailyLimit - dailyUsage, }; } }