import { and, desc, eq, gte, inArray, lt, ne, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agents, approvals, budgetIncidents, budgetPolicies, companies, costEvents, projects, } from "@paperclipai/db"; import type { BudgetIncident, BudgetIncidentResolutionInput, BudgetMetric, BudgetOverview, BudgetPolicy, BudgetPolicySummary, BudgetPolicyUpsertInput, BudgetScopeType, BudgetThresholdType, BudgetWindowKind, } from "@paperclipai/shared"; import { notFound, unprocessable } from "../errors.js"; import { logActivity } from "./activity-log.js"; type ScopeRecord = { companyId: string; name: string; paused: boolean; pauseReason: "manual" | "budget" | "system" | null; }; type PolicyRow = typeof budgetPolicies.$inferSelect; type IncidentRow = typeof budgetIncidents.$inferSelect; export type BudgetEnforcementScope = { companyId: string; scopeType: BudgetScopeType; scopeId: string; }; export type BudgetServiceHooks = { cancelWorkForScope?: (scope: BudgetEnforcementScope) => Promise; }; function currentUtcMonthWindow(now = new Date()) { const year = now.getUTCFullYear(); const month = now.getUTCMonth(); const start = new Date(Date.UTC(year, month, 1, 0, 0, 0, 0)); const end = new Date(Date.UTC(year, month + 1, 1, 0, 0, 0, 0)); return { start, end }; } function resolveWindow(windowKind: BudgetWindowKind, now = new Date()) { if (windowKind === "lifetime") { return { start: new Date(Date.UTC(1970, 0, 1, 0, 0, 0, 0)), end: new Date(Date.UTC(9999, 0, 1, 0, 0, 0, 0)), }; } return currentUtcMonthWindow(now); } function budgetStatusFromObserved( observedAmount: number, amount: number, warnPercent: number, ): BudgetPolicySummary["status"] { if (amount <= 0) return "ok"; if (observedAmount >= amount) return "hard_stop"; if (observedAmount >= Math.ceil((amount * warnPercent) / 100)) return "warning"; return "ok"; } function normalizeScopeName(scopeType: BudgetScopeType, name: string) { if (scopeType === "company") return name; return name.trim().length > 0 ? name : scopeType; } async function resolveScopeRecord(db: Db, scopeType: BudgetScopeType, scopeId: string): Promise { if (scopeType === "company") { const row = await db .select({ companyId: companies.id, name: companies.name, status: companies.status, pauseReason: companies.pauseReason, pausedAt: companies.pausedAt, }) .from(companies) .where(eq(companies.id, scopeId)) .then((rows) => rows[0] ?? null); if (!row) throw notFound("Company not found"); return { companyId: row.companyId, name: row.name, paused: row.status === "paused" || Boolean(row.pausedAt), pauseReason: (row.pauseReason as ScopeRecord["pauseReason"]) ?? null, }; } if (scopeType === "agent") { const row = await db .select({ companyId: agents.companyId, name: agents.name, status: agents.status, pauseReason: agents.pauseReason, }) .from(agents) .where(eq(agents.id, scopeId)) .then((rows) => rows[0] ?? null); if (!row) throw notFound("Agent not found"); return { companyId: row.companyId, name: row.name, paused: row.status === "paused", pauseReason: (row.pauseReason as ScopeRecord["pauseReason"]) ?? null, }; } const row = await db .select({ companyId: projects.companyId, name: projects.name, pauseReason: projects.pauseReason, pausedAt: projects.pausedAt, }) .from(projects) .where(eq(projects.id, scopeId)) .then((rows) => rows[0] ?? null); if (!row) throw notFound("Project not found"); return { companyId: row.companyId, name: row.name, paused: Boolean(row.pausedAt), pauseReason: (row.pauseReason as ScopeRecord["pauseReason"]) ?? null, }; } async function computeObservedAmount( db: Db, policy: Pick, ) { if (policy.metric !== "billed_cents") return 0; const conditions = [eq(costEvents.companyId, policy.companyId)]; if (policy.scopeType === "agent") conditions.push(eq(costEvents.agentId, policy.scopeId)); if (policy.scopeType === "project") conditions.push(eq(costEvents.projectId, policy.scopeId)); const { start, end } = resolveWindow(policy.windowKind as BudgetWindowKind); if (policy.windowKind === "calendar_month_utc") { conditions.push(gte(costEvents.occurredAt, start)); conditions.push(lt(costEvents.occurredAt, end)); } const [row] = await db .select({ total: sql`coalesce(sum(${costEvents.costCents}), 0)::int`, }) .from(costEvents) .where(and(...conditions)); return Number(row?.total ?? 0); } function buildApprovalPayload(input: { policy: PolicyRow; scopeName: string; thresholdType: BudgetThresholdType; amountObserved: number; windowStart: Date; windowEnd: Date; }) { return { scopeType: input.policy.scopeType, scopeId: input.policy.scopeId, scopeName: input.scopeName, metric: input.policy.metric, windowKind: input.policy.windowKind, thresholdType: input.thresholdType, budgetAmount: input.policy.amount, observedAmount: input.amountObserved, warnPercent: input.policy.warnPercent, windowStart: input.windowStart.toISOString(), windowEnd: input.windowEnd.toISOString(), policyId: input.policy.id, guidance: "Raise the budget and resume the scope, or keep the scope paused.", }; } async function markApprovalStatus( db: Db, approvalId: string | null, status: "approved" | "rejected", decisionNote: string | null | undefined, decidedByUserId: string, ) { if (!approvalId) return; await db .update(approvals) .set({ status, decisionNote: decisionNote ?? null, decidedByUserId, decidedAt: new Date(), updatedAt: new Date(), }) .where(eq(approvals.id, approvalId)); } export function budgetService(db: Db, hooks: BudgetServiceHooks = {}) { async function pauseScopeForBudget(policy: PolicyRow) { const now = new Date(); if (policy.scopeType === "agent") { await db .update(agents) .set({ status: "paused", pauseReason: "budget", pausedAt: now, updatedAt: now, }) .where(and(eq(agents.id, policy.scopeId), inArray(agents.status, ["active", "idle", "running", "error"]))); return; } if (policy.scopeType === "project") { await db .update(projects) .set({ pauseReason: "budget", pausedAt: now, updatedAt: now, }) .where(eq(projects.id, policy.scopeId)); return; } await db .update(companies) .set({ status: "paused", pauseReason: "budget", pausedAt: now, updatedAt: now, }) .where(eq(companies.id, policy.scopeId)); } async function pauseAndCancelScopeForBudget(policy: PolicyRow) { await pauseScopeForBudget(policy); await hooks.cancelWorkForScope?.({ companyId: policy.companyId, scopeType: policy.scopeType as BudgetScopeType, scopeId: policy.scopeId, }); } async function resumeScopeFromBudget(policy: PolicyRow) { const now = new Date(); if (policy.scopeType === "agent") { await db .update(agents) .set({ status: "idle", pauseReason: null, pausedAt: null, updatedAt: now, }) .where(and(eq(agents.id, policy.scopeId), eq(agents.pauseReason, "budget"))); return; } if (policy.scopeType === "project") { await db .update(projects) .set({ pauseReason: null, pausedAt: null, updatedAt: now, }) .where(and(eq(projects.id, policy.scopeId), eq(projects.pauseReason, "budget"))); return; } await db .update(companies) .set({ status: "active", pauseReason: null, pausedAt: null, updatedAt: now, }) .where(and(eq(companies.id, policy.scopeId), eq(companies.pauseReason, "budget"))); } async function getPolicyRow(policyId: string) { const policy = await db .select() .from(budgetPolicies) .where(eq(budgetPolicies.id, policyId)) .then((rows) => rows[0] ?? null); if (!policy) throw notFound("Budget policy not found"); return policy; } async function listPolicyRows(companyId: string) { return db .select() .from(budgetPolicies) .where(eq(budgetPolicies.companyId, companyId)) .orderBy(desc(budgetPolicies.updatedAt)); } async function buildPolicySummary(policy: PolicyRow): Promise { const scope = await resolveScopeRecord(db, policy.scopeType as BudgetScopeType, policy.scopeId); const observedAmount = await computeObservedAmount(db, policy); const { start, end } = resolveWindow(policy.windowKind as BudgetWindowKind); const amount = policy.isActive ? policy.amount : 0; const utilizationPercent = amount > 0 ? Number(((observedAmount / amount) * 100).toFixed(2)) : 0; return { policyId: policy.id, companyId: policy.companyId, scopeType: policy.scopeType as BudgetScopeType, scopeId: policy.scopeId, scopeName: normalizeScopeName(policy.scopeType as BudgetScopeType, scope.name), metric: policy.metric as BudgetMetric, windowKind: policy.windowKind as BudgetWindowKind, amount, observedAmount, remainingAmount: amount > 0 ? Math.max(0, amount - observedAmount) : 0, utilizationPercent, warnPercent: policy.warnPercent, hardStopEnabled: policy.hardStopEnabled, notifyEnabled: policy.notifyEnabled, isActive: policy.isActive, status: policy.isActive ? budgetStatusFromObserved(observedAmount, amount, policy.warnPercent) : "ok", paused: scope.paused, pauseReason: scope.pauseReason, windowStart: start, windowEnd: end, }; } async function createIncidentIfNeeded( policy: PolicyRow, thresholdType: BudgetThresholdType, amountObserved: number, ) { const { start, end } = resolveWindow(policy.windowKind as BudgetWindowKind); const existing = await db .select() .from(budgetIncidents) .where( and( eq(budgetIncidents.policyId, policy.id), eq(budgetIncidents.windowStart, start), eq(budgetIncidents.thresholdType, thresholdType), ne(budgetIncidents.status, "dismissed"), ), ) .then((rows) => rows[0] ?? null); if (existing) return existing; const scope = await resolveScopeRecord(db, policy.scopeType as BudgetScopeType, policy.scopeId); const payload = buildApprovalPayload({ policy, scopeName: normalizeScopeName(policy.scopeType as BudgetScopeType, scope.name), thresholdType, amountObserved, windowStart: start, windowEnd: end, }); const approval = thresholdType === "hard" ? await db .insert(approvals) .values({ companyId: policy.companyId, type: "budget_override_required", requestedByUserId: null, requestedByAgentId: null, status: "pending", payload, }) .returning() .then((rows) => rows[0] ?? null) : null; return db .insert(budgetIncidents) .values({ companyId: policy.companyId, policyId: policy.id, scopeType: policy.scopeType, scopeId: policy.scopeId, metric: policy.metric, windowKind: policy.windowKind, windowStart: start, windowEnd: end, thresholdType, amountLimit: policy.amount, amountObserved, status: "open", approvalId: approval?.id ?? null, }) .returning() .then((rows) => rows[0] ?? null); } async function resolveOpenSoftIncidents(policyId: string) { await db .update(budgetIncidents) .set({ status: "resolved", resolvedAt: new Date(), updatedAt: new Date(), }) .where( and( eq(budgetIncidents.policyId, policyId), eq(budgetIncidents.thresholdType, "soft"), eq(budgetIncidents.status, "open"), ), ); } async function resolveOpenIncidentsForPolicy( policyId: string, approvalStatus: "approved" | "rejected" | null, decidedByUserId: string | null, ) { const openRows = await db .select() .from(budgetIncidents) .where(and(eq(budgetIncidents.policyId, policyId), eq(budgetIncidents.status, "open"))); await db .update(budgetIncidents) .set({ status: "resolved", resolvedAt: new Date(), updatedAt: new Date(), }) .where(and(eq(budgetIncidents.policyId, policyId), eq(budgetIncidents.status, "open"))); if (!approvalStatus || !decidedByUserId) return; for (const row of openRows) { await markApprovalStatus(db, row.approvalId ?? null, approvalStatus, "Resolved via budget update", decidedByUserId); } } async function hydrateIncidentRows(rows: IncidentRow[]): Promise { const approvalIds = rows.map((row) => row.approvalId).filter((value): value is string => Boolean(value)); const approvalRows = approvalIds.length > 0 ? await db .select({ id: approvals.id, status: approvals.status }) .from(approvals) .where(inArray(approvals.id, approvalIds)) : []; const approvalStatusById = new Map(approvalRows.map((row) => [row.id, row.status])); return Promise.all( rows.map(async (row) => { const scope = await resolveScopeRecord(db, row.scopeType as BudgetScopeType, row.scopeId); return { id: row.id, companyId: row.companyId, policyId: row.policyId, scopeType: row.scopeType as BudgetScopeType, scopeId: row.scopeId, scopeName: normalizeScopeName(row.scopeType as BudgetScopeType, scope.name), metric: row.metric as BudgetMetric, windowKind: row.windowKind as BudgetWindowKind, windowStart: row.windowStart, windowEnd: row.windowEnd, thresholdType: row.thresholdType as BudgetThresholdType, amountLimit: row.amountLimit, amountObserved: row.amountObserved, status: row.status as BudgetIncident["status"], approvalId: row.approvalId ?? null, approvalStatus: row.approvalId ? approvalStatusById.get(row.approvalId) ?? null : null, resolvedAt: row.resolvedAt ?? null, createdAt: row.createdAt, updatedAt: row.updatedAt, }; }), ); } return { listPolicies: async (companyId: string): Promise => { const rows = await listPolicyRows(companyId); return rows.map((row) => ({ ...row, scopeType: row.scopeType as BudgetScopeType, metric: row.metric as BudgetMetric, windowKind: row.windowKind as BudgetWindowKind, })); }, upsertPolicy: async ( companyId: string, input: BudgetPolicyUpsertInput, actorUserId: string | null, ): Promise => { const scope = await resolveScopeRecord(db, input.scopeType, input.scopeId); if (scope.companyId !== companyId) { throw unprocessable("Budget scope does not belong to company"); } const metric = input.metric ?? "billed_cents"; const windowKind = input.windowKind ?? (input.scopeType === "project" ? "lifetime" : "calendar_month_utc"); const amount = Math.max(0, Math.floor(input.amount)); const nextIsActive = amount > 0 && (input.isActive ?? true); const existing = await db .select() .from(budgetPolicies) .where( and( eq(budgetPolicies.companyId, companyId), eq(budgetPolicies.scopeType, input.scopeType), eq(budgetPolicies.scopeId, input.scopeId), eq(budgetPolicies.metric, metric), eq(budgetPolicies.windowKind, windowKind), ), ) .then((rows) => rows[0] ?? null); const now = new Date(); const row = existing ? await db .update(budgetPolicies) .set({ amount, warnPercent: input.warnPercent ?? existing.warnPercent, hardStopEnabled: input.hardStopEnabled ?? existing.hardStopEnabled, notifyEnabled: input.notifyEnabled ?? existing.notifyEnabled, isActive: nextIsActive, updatedByUserId: actorUserId, updatedAt: now, }) .where(eq(budgetPolicies.id, existing.id)) .returning() .then((rows) => rows[0]) : await db .insert(budgetPolicies) .values({ companyId, scopeType: input.scopeType, scopeId: input.scopeId, metric, windowKind, amount, warnPercent: input.warnPercent ?? 80, hardStopEnabled: input.hardStopEnabled ?? true, notifyEnabled: input.notifyEnabled ?? true, isActive: nextIsActive, createdByUserId: actorUserId, updatedByUserId: actorUserId, }) .returning() .then((rows) => rows[0]); if (input.scopeType === "company" && windowKind === "calendar_month_utc") { await db .update(companies) .set({ budgetMonthlyCents: amount, updatedAt: now, }) .where(eq(companies.id, input.scopeId)); } if (input.scopeType === "agent" && windowKind === "calendar_month_utc") { await db .update(agents) .set({ budgetMonthlyCents: amount, updatedAt: now, }) .where(eq(agents.id, input.scopeId)); } if (amount > 0) { const observedAmount = await computeObservedAmount(db, row); if (observedAmount < amount) { await resumeScopeFromBudget(row); await resolveOpenIncidentsForPolicy(row.id, actorUserId ? "approved" : null, actorUserId); } else { const softThreshold = Math.ceil((row.amount * row.warnPercent) / 100); if (row.notifyEnabled && observedAmount >= softThreshold) { await createIncidentIfNeeded(row, "soft", observedAmount); } if (row.hardStopEnabled && observedAmount >= row.amount) { await resolveOpenSoftIncidents(row.id); await createIncidentIfNeeded(row, "hard", observedAmount); await pauseAndCancelScopeForBudget(row); } } } else { await resumeScopeFromBudget(row); await resolveOpenIncidentsForPolicy(row.id, actorUserId ? "approved" : null, actorUserId); } await logActivity(db, { companyId, actorType: "user", actorId: actorUserId ?? "board", action: "budget.policy_upserted", entityType: "budget_policy", entityId: row.id, details: { scopeType: row.scopeType, scopeId: row.scopeId, amount: row.amount, windowKind: row.windowKind, }, }); return buildPolicySummary(row); }, overview: async (companyId: string): Promise => { const rows = await listPolicyRows(companyId); const policies = await Promise.all(rows.map((row) => buildPolicySummary(row))); const activeIncidentRows = await db .select() .from(budgetIncidents) .where(and(eq(budgetIncidents.companyId, companyId), eq(budgetIncidents.status, "open"))) .orderBy(desc(budgetIncidents.createdAt)); const activeIncidents = await hydrateIncidentRows(activeIncidentRows); return { companyId, policies, activeIncidents, pausedAgentCount: policies.filter((policy) => policy.scopeType === "agent" && policy.paused).length, pausedProjectCount: policies.filter((policy) => policy.scopeType === "project" && policy.paused).length, pendingApprovalCount: activeIncidents.filter((incident) => incident.approvalStatus === "pending").length, }; }, evaluateCostEvent: async (event: typeof costEvents.$inferSelect) => { const candidatePolicies = await db .select() .from(budgetPolicies) .where( and( eq(budgetPolicies.companyId, event.companyId), eq(budgetPolicies.isActive, true), inArray(budgetPolicies.scopeType, ["company", "agent", "project"]), ), ); const relevantPolicies = candidatePolicies.filter((policy) => { if (policy.scopeType === "company") return policy.scopeId === event.companyId; if (policy.scopeType === "agent") return policy.scopeId === event.agentId; if (policy.scopeType === "project") return Boolean(event.projectId) && policy.scopeId === event.projectId; return false; }); for (const policy of relevantPolicies) { if (policy.metric !== "billed_cents" || policy.amount <= 0) continue; const observedAmount = await computeObservedAmount(db, policy); const softThreshold = Math.ceil((policy.amount * policy.warnPercent) / 100); if (policy.notifyEnabled && observedAmount >= softThreshold) { const softIncident = await createIncidentIfNeeded(policy, "soft", observedAmount); if (softIncident) { await logActivity(db, { companyId: policy.companyId, actorType: "system", actorId: "budget_service", action: "budget.soft_threshold_crossed", entityType: "budget_incident", entityId: softIncident.id, details: { scopeType: policy.scopeType, scopeId: policy.scopeId, amountObserved: observedAmount, amountLimit: policy.amount, }, }); } } if (policy.hardStopEnabled && observedAmount >= policy.amount) { await resolveOpenSoftIncidents(policy.id); const hardIncident = await createIncidentIfNeeded(policy, "hard", observedAmount); await pauseAndCancelScopeForBudget(policy); if (hardIncident) { await logActivity(db, { companyId: policy.companyId, actorType: "system", actorId: "budget_service", action: "budget.hard_threshold_crossed", entityType: "budget_incident", entityId: hardIncident.id, details: { scopeType: policy.scopeType, scopeId: policy.scopeId, amountObserved: observedAmount, amountLimit: policy.amount, approvalId: hardIncident.approvalId ?? null, }, }); } } } }, getInvocationBlock: async ( companyId: string, agentId: string, context?: { issueId?: string | null; projectId?: string | null }, ) => { const agent = await db .select({ status: agents.status, pauseReason: agents.pauseReason, companyId: agents.companyId, name: agents.name, }) .from(agents) .where(eq(agents.id, agentId)) .then((rows) => rows[0] ?? null); if (!agent || agent.companyId !== companyId) throw notFound("Agent not found"); const company = await db .select({ status: companies.status, pauseReason: companies.pauseReason, name: companies.name, }) .from(companies) .where(eq(companies.id, companyId)) .then((rows) => rows[0] ?? null); if (!company) throw notFound("Company not found"); if (company.status === "paused") { return { scopeType: "company" as const, scopeId: companyId, scopeName: company.name, reason: company.pauseReason === "budget" ? "Company is paused because its budget hard-stop was reached." : "Company is paused and cannot start new work.", }; } const companyPolicy = await db .select() .from(budgetPolicies) .where( and( eq(budgetPolicies.companyId, companyId), eq(budgetPolicies.scopeType, "company"), eq(budgetPolicies.scopeId, companyId), eq(budgetPolicies.isActive, true), eq(budgetPolicies.metric, "billed_cents"), ), ) .then((rows) => rows[0] ?? null); if (companyPolicy && companyPolicy.hardStopEnabled && companyPolicy.amount > 0) { const observed = await computeObservedAmount(db, companyPolicy); if (observed >= companyPolicy.amount) { return { scopeType: "company" as const, scopeId: companyId, scopeName: company.name, reason: "Company cannot start new work because its budget hard-stop is exceeded.", }; } } if (agent.status === "paused" && agent.pauseReason === "budget") { return { scopeType: "agent" as const, scopeId: agentId, scopeName: agent.name, reason: "Agent is paused because its budget hard-stop was reached.", }; } const agentPolicy = await db .select() .from(budgetPolicies) .where( and( eq(budgetPolicies.companyId, companyId), eq(budgetPolicies.scopeType, "agent"), eq(budgetPolicies.scopeId, agentId), eq(budgetPolicies.isActive, true), eq(budgetPolicies.metric, "billed_cents"), ), ) .then((rows) => rows[0] ?? null); if (agentPolicy && agentPolicy.hardStopEnabled && agentPolicy.amount > 0) { const observed = await computeObservedAmount(db, agentPolicy); if (observed >= agentPolicy.amount) { return { scopeType: "agent" as const, scopeId: agentId, scopeName: agent.name, reason: "Agent cannot start because its budget hard-stop is still exceeded.", }; } } const candidateProjectId = context?.projectId ?? null; if (!candidateProjectId) return null; const project = await db .select({ id: projects.id, name: projects.name, companyId: projects.companyId, pauseReason: projects.pauseReason, pausedAt: projects.pausedAt, }) .from(projects) .where(eq(projects.id, candidateProjectId)) .then((rows) => rows[0] ?? null); if (!project || project.companyId !== companyId) return null; const projectPolicy = await db .select() .from(budgetPolicies) .where( and( eq(budgetPolicies.companyId, companyId), eq(budgetPolicies.scopeType, "project"), eq(budgetPolicies.scopeId, project.id), eq(budgetPolicies.isActive, true), eq(budgetPolicies.metric, "billed_cents"), ), ) .then((rows) => rows[0] ?? null); if (projectPolicy && projectPolicy.hardStopEnabled && projectPolicy.amount > 0) { const observed = await computeObservedAmount(db, projectPolicy); if (observed >= projectPolicy.amount) { return { scopeType: "project" as const, scopeId: project.id, scopeName: project.name, reason: "Project cannot start work because its budget hard-stop is still exceeded.", }; } } if (!project.pausedAt || project.pauseReason !== "budget") return null; return { scopeType: "project" as const, scopeId: project.id, scopeName: project.name, reason: "Project is paused because its budget hard-stop was reached.", }; }, resolveIncident: async ( companyId: string, incidentId: string, input: BudgetIncidentResolutionInput, actorUserId: string, ): Promise => { const incident = await db .select() .from(budgetIncidents) .where(eq(budgetIncidents.id, incidentId)) .then((rows) => rows[0] ?? null); if (!incident) throw notFound("Budget incident not found"); if (incident.companyId !== companyId) throw notFound("Budget incident not found"); const policy = await getPolicyRow(incident.policyId); if (input.action === "raise_budget_and_resume") { const nextAmount = Math.max(0, Math.floor(input.amount ?? 0)); const currentObserved = await computeObservedAmount(db, policy); if (nextAmount <= currentObserved) { throw unprocessable("New budget must exceed current observed spend"); } const now = new Date(); await db .update(budgetPolicies) .set({ amount: nextAmount, isActive: true, updatedByUserId: actorUserId, updatedAt: now, }) .where(eq(budgetPolicies.id, policy.id)); if (policy.scopeType === "company" && policy.windowKind === "calendar_month_utc") { await db .update(companies) .set({ budgetMonthlyCents: nextAmount, updatedAt: now }) .where(eq(companies.id, policy.scopeId)); } if (policy.scopeType === "agent" && policy.windowKind === "calendar_month_utc") { await db .update(agents) .set({ budgetMonthlyCents: nextAmount, updatedAt: now }) .where(eq(agents.id, policy.scopeId)); } await resumeScopeFromBudget(policy); await db .update(budgetIncidents) .set({ status: "resolved", resolvedAt: now, updatedAt: now, }) .where(and(eq(budgetIncidents.policyId, policy.id), eq(budgetIncidents.status, "open"))); await markApprovalStatus(db, incident.approvalId ?? null, "approved", input.decisionNote, actorUserId); } else { await db .update(budgetIncidents) .set({ status: "dismissed", resolvedAt: new Date(), updatedAt: new Date(), }) .where(eq(budgetIncidents.id, incident.id)); await markApprovalStatus(db, incident.approvalId ?? null, "rejected", input.decisionNote, actorUserId); } await logActivity(db, { companyId: incident.companyId, actorType: "user", actorId: actorUserId, action: "budget.incident_resolved", entityType: "budget_incident", entityId: incident.id, details: { action: input.action, amount: input.amount ?? null, scopeType: incident.scopeType, scopeId: incident.scopeId, }, }); const [updated] = await hydrateIncidentRows([{ ...incident, status: input.action === "raise_budget_and_resume" ? "resolved" : "dismissed", resolvedAt: new Date(), updatedAt: new Date(), }]); return updated!; }, }; }