sistema-de-chamados/convex/automationsEngine.ts

138 lines
4.3 KiB
TypeScript

import type { Doc } from "./_generated/dataModel"
export type AutomationTrigger =
| "TICKET_CREATED"
| "STATUS_CHANGED"
| "PRIORITY_CHANGED"
| "QUEUE_CHANGED"
| "COMMENT_ADDED"
| "TICKET_RESOLVED"
export type AutomationConditionOperator = "AND" | "OR"
export type AutomationConditionField =
| "companyId"
| "queueId"
| "categoryId"
| "subcategoryId"
| "priority"
| "status"
| "channel"
| "formTemplate"
| "chatEnabled"
| "tag"
export type AutomationConditionComparator =
| "eq"
| "neq"
| "in"
| "not_in"
| "contains"
| "not_contains"
| "is_true"
| "is_false"
export type AutomationCondition = {
field: AutomationConditionField
op: AutomationConditionComparator
value?: unknown
}
export type AutomationConditionGroup = {
op: AutomationConditionOperator
conditions: AutomationCondition[]
}
export type TicketForAutomation = Pick<
Doc<"tickets">,
| "tenantId"
| "status"
| "priority"
| "channel"
| "queueId"
| "companyId"
| "categoryId"
| "subcategoryId"
| "tags"
| "formTemplate"
| "chatEnabled"
>
function normalizeId(value: unknown): string | null {
if (!value) return null
return String(value)
}
function normalizeString(value: unknown): string | null {
if (typeof value !== "string") return null
const trimmed = value.trim()
return trimmed.length > 0 ? trimmed : null
}
function normalizeStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return []
return value.map((item) => normalizeString(item)).filter((item): item is string => Boolean(item))
}
function normalizeBoolean(value: unknown): boolean | null {
if (typeof value === "boolean") return value
return null
}
function compareValue(actual: string | null, op: AutomationConditionComparator, expected: unknown): boolean {
if (op === "eq") return actual === normalizeId(expected)
if (op === "neq") return actual !== normalizeId(expected)
if (op === "in") {
const list = normalizeStringArray(expected).map((v) => v)
return actual !== null && list.includes(actual)
}
if (op === "not_in") {
const list = normalizeStringArray(expected).map((v) => v)
return actual === null || !list.includes(actual)
}
return false
}
export function evaluateAutomationConditions(
ticket: TicketForAutomation,
group: AutomationConditionGroup | null | undefined
): boolean {
if (!group || !Array.isArray(group.conditions) || group.conditions.length === 0) return true
const op = group.op === "OR" ? "OR" : "AND"
const results = group.conditions.map((condition) => {
const field = condition.field
const operator = condition.op
if (field === "companyId") return compareValue(normalizeId(ticket.companyId), operator, condition.value)
if (field === "queueId") return compareValue(normalizeId(ticket.queueId), operator, condition.value)
if (field === "categoryId") return compareValue(normalizeId(ticket.categoryId), operator, condition.value)
if (field === "subcategoryId") return compareValue(normalizeId(ticket.subcategoryId), operator, condition.value)
if (field === "priority") return compareValue(normalizeString(ticket.priority), operator, condition.value)
if (field === "status") return compareValue(normalizeString(ticket.status), operator, condition.value)
if (field === "channel") return compareValue(normalizeString(ticket.channel), operator, condition.value)
if (field === "formTemplate") return compareValue(normalizeString(ticket.formTemplate), operator, condition.value)
if (field === "chatEnabled") {
const expectedBool = normalizeBoolean(condition.value)
if (operator === "is_true") return ticket.chatEnabled === true
if (operator === "is_false") return ticket.chatEnabled !== true
if (operator === "eq") return expectedBool !== null ? Boolean(ticket.chatEnabled) === expectedBool : false
if (operator === "neq") return expectedBool !== null ? Boolean(ticket.chatEnabled) !== expectedBool : false
return false
}
if (field === "tag") {
const tag = normalizeString(condition.value)
if (!tag) return false
const has = (ticket.tags ?? []).includes(tag)
if (operator === "contains" || operator === "eq") return has
if (operator === "not_contains" || operator === "neq") return !has
return false
}
return false
})
return op === "OR" ? results.some(Boolean) : results.every(Boolean)
}