feat: checklists em tickets + automações
- Adiciona checklist no ticket (itens obrigatórios/opcionais) e bloqueia encerramento com pendências\n- Cria templates de checklist (globais/por empresa) + tela em /settings/checklists\n- Nova ação de automação: aplicar template de checklist\n- Corrige crash do Select (value vazio), warnings de Dialog e dimensionamento de charts\n- Ajusta SMTP (STARTTLS) e melhora teste de integração
This commit is contained in:
parent
4306b0504d
commit
88a9ef454e
27 changed files with 2685 additions and 226 deletions
|
|
@ -51,18 +51,34 @@ describe("sendSmtpMail - integracao", () => {
|
|||
// Para rodar: SMTP_INTEGRATION_TEST=true bun test tests/email-smtp.test.ts
|
||||
const { sendSmtpMail } = await import("@/server/email-smtp")
|
||||
|
||||
const host = process.env.SMTP_HOST
|
||||
const port = process.env.SMTP_PORT
|
||||
const username = process.env.SMTP_USER
|
||||
const password = process.env.SMTP_PASS
|
||||
const fromEmail = process.env.SMTP_FROM_EMAIL
|
||||
const fromName = process.env.SMTP_FROM_NAME ?? "Sistema de Chamados"
|
||||
|
||||
if (!host || !port || !username || !password || !fromEmail) {
|
||||
throw new Error(
|
||||
"Variáveis SMTP ausentes. Defina SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM_EMAIL (e opcionalmente SMTP_FROM_NAME, SMTP_SECURE, SMTP_TEST_TO)."
|
||||
)
|
||||
}
|
||||
|
||||
const config = {
|
||||
host: process.env.SMTP_HOST ?? "smtp.c.inova.com.br",
|
||||
port: Number(process.env.SMTP_PORT ?? 587),
|
||||
username: process.env.SMTP_USER ?? "envio@rever.com.br",
|
||||
password: process.env.SMTP_PASS ?? "CAAJQm6ZT6AUdhXRTDYu",
|
||||
from: process.env.SMTP_FROM_EMAIL ?? "Sistema de Chamados <envio@rever.com.br>",
|
||||
host,
|
||||
port: Number(port),
|
||||
username,
|
||||
password,
|
||||
from: `"${fromName}" <${fromEmail}>`,
|
||||
tls: (process.env.SMTP_SECURE ?? "false").toLowerCase() === "true",
|
||||
starttls: (process.env.SMTP_SECURE ?? "false").toLowerCase() !== "true",
|
||||
timeoutMs: 30000,
|
||||
}
|
||||
|
||||
// Enviar email de teste
|
||||
const to = process.env.SMTP_TEST_TO ?? fromEmail
|
||||
|
||||
await expect(
|
||||
sendSmtpMail(config, "envio@rever.com.br", "Teste automatico do sistema", "<p>Este e um teste automatico.</p>")
|
||||
sendSmtpMail(config, to, "Teste automático do sistema", "<p>Este é um teste automático.</p>")
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
}, { timeout: 60000 })
|
||||
})
|
||||
|
|
|
|||
77
tests/ticket-checklist.test.ts
Normal file
77
tests/ticket-checklist.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { describe, expect, it } from "bun:test"
|
||||
|
||||
import type { Id } from "../convex/_generated/dataModel"
|
||||
import { applyChecklistTemplateToItems, checklistBlocksResolution } from "../convex/ticketChecklist"
|
||||
|
||||
describe("convex.ticketChecklist", () => {
|
||||
it("aplica template no checklist de forma idempotente", () => {
|
||||
const now = 123
|
||||
const template = {
|
||||
_id: "tpl_1" as Id<"ticketChecklistTemplates">,
|
||||
items: [
|
||||
{ id: "i1", text: "Validar backup", required: true },
|
||||
{ id: "i2", text: "Reiniciar serviço" }, // required default true
|
||||
],
|
||||
}
|
||||
|
||||
const generatedIds = ["c1", "c2"]
|
||||
let idx = 0
|
||||
const first = applyChecklistTemplateToItems([], template, {
|
||||
now,
|
||||
actorId: "user_1" as Id<"users">,
|
||||
generateId: () => generatedIds[idx++]!,
|
||||
})
|
||||
|
||||
expect(first.added).toBe(2)
|
||||
expect(first.checklist).toHaveLength(2)
|
||||
expect(first.checklist[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "c1",
|
||||
text: "Validar backup",
|
||||
done: false,
|
||||
required: true,
|
||||
templateId: template._id,
|
||||
templateItemId: "i1",
|
||||
createdAt: now,
|
||||
createdBy: "user_1",
|
||||
})
|
||||
)
|
||||
expect(first.checklist[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "c2",
|
||||
text: "Reiniciar serviço",
|
||||
required: true,
|
||||
templateItemId: "i2",
|
||||
})
|
||||
)
|
||||
|
||||
const second = applyChecklistTemplateToItems(first.checklist, template, {
|
||||
now: now + 1,
|
||||
actorId: "user_2" as Id<"users">,
|
||||
generateId: () => "should_not_add",
|
||||
})
|
||||
expect(second.added).toBe(0)
|
||||
expect(second.checklist).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("bloqueia encerramento quando há item obrigatório pendente", () => {
|
||||
expect(
|
||||
checklistBlocksResolution([
|
||||
{ id: "1", text: "Obrigatório", done: false, required: true },
|
||||
])
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
checklistBlocksResolution([
|
||||
{ id: "1", text: "Opcional", done: false, required: false },
|
||||
])
|
||||
).toBe(false)
|
||||
|
||||
expect(
|
||||
checklistBlocksResolution([
|
||||
{ id: "1", text: "Obrigatório", done: true, required: true },
|
||||
])
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -199,6 +199,38 @@ describe("convex.tickets.resolveTicketHandler", () => {
|
|||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("impede encerrar ticket quando há checklist obrigatório pendente", async () => {
|
||||
const ticketMain = buildTicket({
|
||||
checklist: [{ id: "c1", text: "Validar backup", done: false, required: true }] as unknown as TicketDoc["checklist"],
|
||||
})
|
||||
|
||||
const { ctx, patch } = createResolveCtx({
|
||||
[String(ticketMain._id)]: ticketMain,
|
||||
})
|
||||
|
||||
mockedRequireStaff.mockResolvedValue({
|
||||
user: {
|
||||
_id: "user_agent" as Id<"users">,
|
||||
_creationTime: NOW - 5_000,
|
||||
tenantId: "tenant-1",
|
||||
name: "Agente",
|
||||
email: "agente@example.com",
|
||||
role: "ADMIN",
|
||||
teams: [],
|
||||
},
|
||||
role: "ADMIN",
|
||||
})
|
||||
|
||||
await expect(
|
||||
resolveTicketHandler(ctx, {
|
||||
ticketId: ticketMain._id,
|
||||
actorId: "user_agent" as Id<"users">,
|
||||
})
|
||||
).rejects.toThrow("checklist")
|
||||
|
||||
expect(patch).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("convex.tickets.reopenTicketHandler", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue