import { afterEach, describe, expect, it, vi } from "bun:test" import type { Doc, Id } from "../convex/_generated/dataModel" import { submitCsatHandler } from "../convex/tickets" import { requireUser } from "../convex/rbac" vi.mock("../convex/rbac", () => ({ requireUser: vi.fn(), requireStaff: vi.fn(), requireAdmin: vi.fn(), })) type SubmitCsatCtx = Parameters[0] const mockedRequireUser = vi.mocked(requireUser) const FIXED_NOW = 1_706_500_000_000 function makeTicket(overrides: Partial> = {}): Doc<"tickets"> { const ticket = { _id: "ticket_1" as Id<"tickets">, _creationTime: FIXED_NOW - 10_000, tenantId: "tenant-1", reference: 42_100, subject: "Computador não liga", summary: undefined, status: "RESOLVED", priority: "MEDIUM", channel: "EMAIL", queueId: undefined, requesterId: "user_requester" as Id<"users">, requesterSnapshot: { name: "Cliente", email: "cliente@example.com" }, assigneeId: undefined, assigneeSnapshot: undefined, companyId: undefined, companySnapshot: undefined, machineId: undefined, machineSnapshot: undefined, slaPolicyId: undefined, dueAt: undefined, firstResponseAt: undefined, resolvedAt: FIXED_NOW - 1000, createdAt: FIXED_NOW - 20_000, updatedAt: FIXED_NOW - 100, tags: [], customFields: [], activeSessionId: undefined, totalWorkedMs: 0, internalWorkedMs: 0, externalWorkedMs: 0, csatScore: undefined, csatMaxScore: undefined, csatComment: undefined, csatRatedAt: undefined, csatRatedBy: undefined, } satisfies Partial> return { ...(ticket as Doc<"tickets">), ...overrides } } function createCtx(ticket: Doc<"tickets">, events: Doc<"ticketEvents">[] = []) { const collect = vi.fn(async () => events) const query = vi.fn(() => ({ withIndex: vi.fn((_index: string, cb: (builder: { eq: (field: string, value: unknown) => { collect: typeof collect } }) => { collect: typeof collect }) => { const cursor = { eq: vi.fn(() => ({ collect, })), } const result = cb(cursor as { eq: (field: string, value: unknown) => { collect: typeof collect } }) return result ?? { collect } }), })) const patch = vi.fn(async () => undefined) const insert = vi.fn(async () => undefined) const deleteFn = vi.fn(async () => undefined) const ctx: SubmitCsatCtx = { db: { get: vi.fn(async (id: Id<"tickets"> | Id<"users">) => { if (id === ticket._id) return ticket return null }), patch, query, delete: deleteFn, insert, }, } as unknown as SubmitCsatCtx return { ctx, patch, insert, deleteFn, query } } describe("convex.tickets.submitCsat", () => { afterEach(() => { vi.clearAllMocks() }) it("permite que o solicitante avalie um chamado resolvido", async () => { const ticket = makeTicket() const { ctx, patch, insert, deleteFn } = createCtx(ticket) mockedRequireUser.mockResolvedValue({ user: { _id: "user_requester" as Id<"users">, _creationTime: FIXED_NOW - 30_000, tenantId: "tenant-1", name: "Cliente", email: "cliente@example.com", role: "COLLABORATOR", companyId: undefined, teams: [], }, role: "COLLABORATOR", }) const result = await submitCsatHandler(ctx, { ticketId: ticket._id, actorId: "user_requester" as Id<"users">, score: 4, maxScore: 5, comment: "Atendimento excelente!", }) expect(result.score).toBe(4) expect(result.comment).toBe("Atendimento excelente!") expect(patch).toHaveBeenCalledWith( ticket._id, expect.objectContaining({ csatScore: 4, csatMaxScore: 5, csatComment: "Atendimento excelente!", csatRatedBy: "user_requester", }) ) expect(insert).toHaveBeenCalledWith( "ticketEvents", expect.objectContaining({ ticketId: ticket._id, type: "CSAT_RATED", payload: expect.objectContaining({ score: 4, maxScore: 5, comment: "Atendimento excelente!" }), }) ) expect(deleteFn).not.toHaveBeenCalled() }) it("bloqueia avaliações antes do encerramento do chamado para colaboradores", async () => { const ticket = makeTicket({ status: "PENDING" }) const { ctx, patch, insert } = createCtx(ticket) mockedRequireUser.mockResolvedValue({ user: { _id: "user_requester" as Id<"users">, _creationTime: FIXED_NOW - 30_000, tenantId: "tenant-1", name: "Cliente", email: "cliente@example.com", role: "COLLABORATOR", companyId: undefined, teams: [], }, role: "COLLABORATOR", }) await expect( submitCsatHandler(ctx, { ticketId: ticket._id, actorId: "user_requester" as Id<"users">, score: 5, comment: "Perfeito", }) ).rejects.toThrow("Avaliações só são permitidas após o encerramento do chamado") expect(patch).not.toHaveBeenCalled() expect(insert).not.toHaveBeenCalled() }) it("não permite registrar uma segunda avaliação", async () => { const ticket = makeTicket({ csatScore: 4, csatMaxScore: 5, csatRatedAt: FIXED_NOW - 2000, csatRatedBy: "user_requester" as Id<"users">, }) const { ctx, patch, insert, deleteFn } = createCtx(ticket) mockedRequireUser.mockResolvedValue({ user: { _id: "user_requester" as Id<"users">, _creationTime: FIXED_NOW - 30_000, tenantId: "tenant-1", name: "Cliente", email: "cliente@example.com", role: "COLLABORATOR", companyId: undefined, teams: [], }, role: "COLLABORATOR", }) await expect( submitCsatHandler(ctx, { ticketId: ticket._id, actorId: "user_requester" as Id<"users">, score: 5, comment: "Vou tentar atualizar", }) ).rejects.toThrow("Este chamado já possui uma avaliação registrada") expect(patch).not.toHaveBeenCalled() expect(insert).not.toHaveBeenCalled() expect(deleteFn).not.toHaveBeenCalled() }) it("impede que administradores registrem avaliação", async () => { const ticket = makeTicket() const { ctx, patch, insert } = createCtx(ticket) mockedRequireUser.mockResolvedValue({ user: { _id: "user_admin" as Id<"users">, _creationTime: FIXED_NOW - 40_000, tenantId: "tenant-1", name: "Administrador", email: "admin@example.com", role: "ADMIN", companyId: undefined, teams: [], }, role: "ADMIN", }) await expect( submitCsatHandler(ctx, { ticketId: ticket._id, actorId: "user_admin" as Id<"users">, score: 4, comment: "somente testes", }) ).rejects.toThrow("Somente o solicitante pode avaliar o chamado") expect(patch).not.toHaveBeenCalled() expect(insert).not.toHaveBeenCalled() }) })