1/** 2 * apps/worker — per-workspace automation email daily cap (TASK-0112 §8). 3 * 4 * Redis counter `auto:email:{wsId}:{utc-date}` (shared `emailCapKey`). The 5 * SEND_EMAIL executor INCRs before enqueueing; if the result exceeds the cap 6 * (AUTOMATION_EMAIL_DAILY_CAP = 500) the email is NOT sent and the executor 7 * throws the FATAL EMAIL_DAILY_CAP (no retry — the quota won't change by 8 * retrying). The counter has a 24 h TTL so it rolls over on its own. 9 */ 10import { AUTOMATION_EMAIL_DAILY_CAP, emailCapKey } from "@factory/shared"; 11import type { Redis } from "ioredis"; 12import { FatalError } from "./classify.js"; 13 14/** 15 * Reserve one email slot for the workspace today. Throws FatalError 16 * EMAIL_DAILY_CAP when the cap is exceeded. Returns the new counter value. 17 */ 18export async function reserveEmailSlot(redis: Redis, workspaceId: string): Promise<number> { 19 const key = emailCapKey(workspaceId); 20 const n = await redis.incr(key); 21 if (n === 1) { 22 // First send today: arm the 24 h TTL. 23 await redis.expire(key, 24 * 60 * 60); 24 } 25 if (n > AUTOMATION_EMAIL_DAILY_CAP) { 26 throw new FatalError("EMAIL_DAILY_CAP", `workspace ${workspaceId} exceeded ${AUTOMATION_EMAIL_DAILY_CAP} automation emails today`); 27 } 28 return n; 29} 30