feat: automações de tickets e testes de regressão
This commit is contained in:
parent
9f1a6a7401
commit
8ab510bfe9
18 changed files with 2221 additions and 20 deletions
133
convex/automationsEngine.ts
Normal file
133
convex/automationsEngine.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import type { Doc } from "./_generated/dataModel"
|
||||
|
||||
export type AutomationTrigger = "TICKET_CREATED" | "STATUS_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)
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue