import { mutation, query } from "./_generated/server"; import type { MutationCtx, QueryCtx } from "./_generated/server"; import { ConvexError, v } from "convex/values"; import type { Id } from "./_generated/dataModel"; import { requireAdmin } from "./rbac"; function normalizeName(value: string) { return value.trim(); } function normalizeMode(value?: string): "business" | "calendar" { if (value === "business") return "business"; return "calendar"; } function normalizeThreshold(value?: number): number { if (value === undefined || value === null) return 0.8; if (value < 0.1) return 0.1; if (value > 0.95) return 0.95; return value; } const VALID_PAUSE_STATUSES = ["PAUSED", "PENDING", "AWAITING_ATTENDANCE"] as const; function normalizePauseStatuses(statuses?: string[]): string[] { if (!statuses || statuses.length === 0) return ["PAUSED"]; const filtered = statuses.filter((s) => VALID_PAUSE_STATUSES.includes(s as typeof VALID_PAUSE_STATUSES[number])); return filtered.length > 0 ? filtered : ["PAUSED"]; } type AnyCtx = QueryCtx | MutationCtx; async function ensureUniqueName(ctx: AnyCtx, tenantId: string, name: string, excludeId?: Id<"slaPolicies">) { const existing = await ctx.db .query("slaPolicies") .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId).eq("name", name)) .first(); if (existing && (!excludeId || existing._id !== excludeId)) { throw new ConvexError("Já existe uma política SLA com este nome"); } } export const list = query({ args: { tenantId: v.string(), viewerId: v.id("users") }, handler: async (ctx, { tenantId, viewerId }) => { await requireAdmin(ctx, viewerId, tenantId); const items = await ctx.db .query("slaPolicies") .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId)) .take(50); return items.map((policy) => ({ id: policy._id, name: policy.name, description: policy.description ?? "", timeToFirstResponse: policy.timeToFirstResponse ?? null, responseMode: policy.responseMode ?? "calendar", timeToResolution: policy.timeToResolution ?? null, solutionMode: policy.solutionMode ?? "calendar", alertThreshold: policy.alertThreshold ?? 0.8, pauseStatuses: policy.pauseStatuses ?? ["PAUSED"], })); }, }); export const create = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), name: v.string(), description: v.optional(v.string()), timeToFirstResponse: v.optional(v.number()), responseMode: v.optional(v.string()), timeToResolution: v.optional(v.number()), solutionMode: v.optional(v.string()), alertThreshold: v.optional(v.number()), pauseStatuses: v.optional(v.array(v.string())), }, handler: async (ctx, args) => { const { tenantId, actorId, name, description, timeToFirstResponse, responseMode, timeToResolution, solutionMode, alertThreshold, pauseStatuses } = args; await requireAdmin(ctx, actorId, tenantId); const trimmed = normalizeName(name); if (trimmed.length < 2) { throw new ConvexError("Informe um nome válido para a política"); } await ensureUniqueName(ctx, tenantId, trimmed); if (timeToFirstResponse !== undefined && timeToFirstResponse < 0) { throw new ConvexError("Tempo para primeira resposta deve ser positivo"); } if (timeToResolution !== undefined && timeToResolution < 0) { throw new ConvexError("Tempo para resolução deve ser positivo"); } const id = await ctx.db.insert("slaPolicies", { tenantId, name: trimmed, description, timeToFirstResponse, responseMode: normalizeMode(responseMode), timeToResolution, solutionMode: normalizeMode(solutionMode), alertThreshold: normalizeThreshold(alertThreshold), pauseStatuses: normalizePauseStatuses(pauseStatuses), }); return id; }, }); export const update = mutation({ args: { policyId: v.id("slaPolicies"), tenantId: v.string(), actorId: v.id("users"), name: v.string(), description: v.optional(v.string()), timeToFirstResponse: v.optional(v.number()), responseMode: v.optional(v.string()), timeToResolution: v.optional(v.number()), solutionMode: v.optional(v.string()), alertThreshold: v.optional(v.number()), pauseStatuses: v.optional(v.array(v.string())), }, handler: async (ctx, args) => { const { policyId, tenantId, actorId, name, description, timeToFirstResponse, responseMode, timeToResolution, solutionMode, alertThreshold, pauseStatuses } = args; await requireAdmin(ctx, actorId, tenantId); const policy = await ctx.db.get(policyId); if (!policy || policy.tenantId !== tenantId) { throw new ConvexError("Política não encontrada"); } const trimmed = normalizeName(name); if (trimmed.length < 2) { throw new ConvexError("Informe um nome válido para a política"); } if (timeToFirstResponse !== undefined && timeToFirstResponse < 0) { throw new ConvexError("Tempo para primeira resposta deve ser positivo"); } if (timeToResolution !== undefined && timeToResolution < 0) { throw new ConvexError("Tempo para resolução deve ser positivo"); } await ensureUniqueName(ctx, tenantId, trimmed, policyId); await ctx.db.patch(policyId, { name: trimmed, description, timeToFirstResponse, responseMode: normalizeMode(responseMode), timeToResolution, solutionMode: normalizeMode(solutionMode), alertThreshold: normalizeThreshold(alertThreshold), pauseStatuses: normalizePauseStatuses(pauseStatuses), }); }, }); export const remove = mutation({ args: { policyId: v.id("slaPolicies"), tenantId: v.string(), actorId: v.id("users"), }, handler: async (ctx, { policyId, tenantId, actorId }) => { await requireAdmin(ctx, actorId, tenantId); const policy = await ctx.db.get(policyId); if (!policy || policy.tenantId !== tenantId) { throw new ConvexError("Política não encontrada"); } const ticketLinked = await ctx.db .query("tickets") .withIndex("by_tenant_sla_policy", (q) => q.eq("tenantId", tenantId).eq("slaPolicyId", policyId)) .first(); if (ticketLinked) { throw new ConvexError("Remova a associação de tickets antes de excluir a política"); } await ctx.db.delete(policyId); }, });