import { afterAll, beforeAll, describe, expect, it, vi } from "bun:test" vi.mock("../convex/rbac", () => ({ requireStaff: vi.fn(), })) import type { Doc, Id } from "../convex/_generated/dataModel" import { csatOverviewHandler, hoursByClientHandler, openedResolvedByDayHandler, } from "../convex/reports" import { requireStaff } from "../convex/rbac" import { createReportsCtx } from "./utils/report-test-helpers" const TENANT_ID = "tenant-1" const VIEWER_ID = "user-reports" as Id<"users"> function buildTicket(overrides: Partial>): Doc<"tickets"> { const base: Record = { _id: "ticket_base" as Id<"tickets">, tenantId: TENANT_ID, reference: 50000, subject: "Chamado", summary: null, status: "PENDING", priority: "MEDIUM", channel: "EMAIL", queueId: undefined, requesterId: "user_req" as Id<"users">, requesterSnapshot: { name: "Alice", email: "alice@example.com", avatarUrl: undefined, teams: [] }, assigneeId: "user_assignee" as Id<"users">, assigneeSnapshot: { name: "Bob", email: "bob@example.com", avatarUrl: undefined, teams: [] }, companyId: undefined, companySnapshot: undefined, machineId: undefined, machineSnapshot: undefined, working: false, dueAt: undefined, firstResponseAt: undefined, resolvedAt: undefined, closedAt: undefined, updatedAt: Date.now(), createdAt: Date.now(), tags: [], customFields: [], totalWorkedMs: 0, internalWorkedMs: 0, externalWorkedMs: 0, activeSessionId: undefined, } return { ...(base as Doc<"tickets">), ...overrides } } function buildCompany(overrides: Partial>): Doc<"companies"> { const base: Record = { _id: "company_base" as Id<"companies">, tenantId: TENANT_ID, name: "Empresa", slug: "empresa", createdAt: Date.now(), updatedAt: Date.now(), isAvulso: false, contractedHoursPerMonth: 40, } return { ...(base as Doc<"companies">), ...overrides } } describe("convex.reports.openedResolvedByDay", () => { const requireStaffMock = vi.mocked(requireStaff) const FIXED_NOW = Date.UTC(2024, 4, 15, 12, 0, 0) beforeAll(() => { vi.useFakeTimers() vi.setSystemTime(FIXED_NOW) }) afterAll(() => { vi.useRealTimers() }) it("counts opened and resolved tickets per day within the range", async () => { requireStaffMock.mockResolvedValue({ role: "ADMIN", user: { companyId: undefined }, } as unknown as Awaited>) const tickets = [ buildTicket({ _id: "ticket_open" as Id<"tickets">, createdAt: Date.UTC(2024, 4, 13, 9, 0, 0), status: "PENDING", }), buildTicket({ _id: "ticket_resolved" as Id<"tickets">, createdAt: Date.UTC(2024, 4, 12, 10, 0, 0), resolvedAt: Date.UTC(2024, 4, 14, 8, 0, 0), status: "RESOLVED", }), buildTicket({ _id: "ticket_old" as Id<"tickets">, createdAt: Date.UTC(2024, 3, 30, 10, 0, 0), status: "PENDING", }), ] const ctx = createReportsCtx({ tickets }) as Parameters[0] const result = await openedResolvedByDayHandler(ctx, { tenantId: TENANT_ID, viewerId: VIEWER_ID, range: "7d", }) const opened13 = result.series.find((point) => point.date === "2024-05-13") const resolved14 = result.series.find((point) => point.date === "2024-05-14") expect(result.rangeDays).toBe(7) expect(opened13).toMatchObject({ opened: 1, resolved: 0 }) expect(resolved14).toMatchObject({ opened: 0, resolved: 1 }) }) }) describe("convex.reports.csatOverview", () => { const requireStaffMock = vi.mocked(requireStaff) const FIXED_NOW = Date.UTC(2024, 4, 20, 12, 0, 0) beforeAll(() => { vi.useFakeTimers() vi.setSystemTime(FIXED_NOW) }) afterAll(() => { vi.useRealTimers() }) it("summarizes survey averages and distribution", async () => { requireStaffMock.mockResolvedValue({ role: "ADMIN", user: { companyId: undefined }, } as unknown as Awaited>) const ticketA = buildTicket({ _id: "ticket_a" as Id<"tickets">, createdAt: Date.UTC(2024, 4, 18, 9, 0, 0), }) const ticketB = buildTicket({ _id: "ticket_b" as Id<"tickets">, createdAt: Date.UTC(2024, 4, 17, 15, 0, 0), }) const eventsByTicket = new Map>([ ["ticket_a", [{ type: "CSAT_RECEIVED", payload: { score: 5 }, createdAt: Date.UTC(2024, 4, 19, 8, 0, 0) }]], [ "ticket_b", [ { type: "CSAT_RECEIVED", payload: { score: 3 }, createdAt: Date.UTC(2024, 4, 18, 14, 0, 0) }, { type: "COMMENT", payload: {}, createdAt: Date.UTC(2024, 4, 18, 15, 0, 0) }, ], ], ]) const ctx = createReportsCtx({ tickets: [ticketA, ticketB], ticketEventsByTicket: eventsByTicket }) as Parameters[0] const result = await csatOverviewHandler(ctx, { tenantId: TENANT_ID, viewerId: VIEWER_ID, range: "7d", }) expect(result.rangeDays).toBe(7) expect(result.totalSurveys).toBe(2) expect(result.averageScore).toBe(4) expect(result.distribution.find((entry) => entry.score === 5)?.total).toBe(1) expect(result.distribution.find((entry) => entry.score === 3)?.total).toBe(1) expect(result.recent[0]?.ticketId).toBe("ticket_a" as Id<"tickets">) }) }) describe("convex.reports.hoursByClient", () => { const requireStaffMock = vi.mocked(requireStaff) const FIXED_NOW = Date.UTC(2024, 5, 5, 12, 0, 0) beforeAll(() => { vi.useFakeTimers() vi.setSystemTime(FIXED_NOW) }) afterAll(() => { vi.useRealTimers() }) it("aggregates worked hours by company", async () => { requireStaffMock.mockResolvedValue({ role: "ADMIN", user: { companyId: undefined }, } as unknown as Awaited>) const companyA = buildCompany({ _id: "company_a" as Id<"companies">, name: "Empresa A", contractedHoursPerMonth: 60 }) const companyB = buildCompany({ _id: "company_b" as Id<"companies">, name: "Empresa B", isAvulso: true }) const tickets = [ buildTicket({ _id: "ticket_a" as Id<"tickets">, companyId: companyA._id, updatedAt: Date.UTC(2024, 5, 4, 10, 0, 0), internalWorkedMs: 3 * 3600000, externalWorkedMs: 1 * 3600000, }), buildTicket({ _id: "ticket_b" as Id<"tickets">, companyId: companyB._id, updatedAt: Date.UTC(2024, 5, 3, 15, 0, 0), internalWorkedMs: 3600000, externalWorkedMs: 2 * 3600000, }), buildTicket({ _id: "ticket_old" as Id<"tickets">, companyId: companyA._id, updatedAt: Date.UTC(2024, 4, 20, 12, 0, 0), internalWorkedMs: 5 * 3600000, externalWorkedMs: 0, }), ] const companies = new Map>([ [String(companyA._id), companyA], [String(companyB._id), companyB], ]) const ctx = createReportsCtx({ tickets, companies }) as Parameters[0] const result = await hoursByClientHandler(ctx, { tenantId: TENANT_ID, viewerId: VIEWER_ID, range: "7d", }) expect(result.rangeDays).toBe(7) expect(result.items).toHaveLength(2) expect(result.items).toEqual( expect.arrayContaining([ expect.objectContaining({ companyId: companyA._id, internalMs: 3 * 3600000, externalMs: 3600000, totalMs: 4 * 3600000, contractedHoursPerMonth: 60, }), expect.objectContaining({ companyId: companyB._id, internalMs: 3600000, externalMs: 2 * 3600000, totalMs: 3 * 3600000, isAvulso: true, }), ]) ) }) })