260 lines
7.8 KiB
TypeScript
260 lines
7.8 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"
|
|
|
|
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">>): Doc<"tickets"> {
|
|
const base: Record<string, unknown> = {
|
|
_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">>): Doc<"companies"> {
|
|
const base: Record<string, unknown> = {
|
|
_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<ReturnType<typeof requireStaff>>)
|
|
|
|
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<typeof openedResolvedByDayHandler>[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<ReturnType<typeof requireStaff>>)
|
|
|
|
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<string, Array<{ type: string; payload?: unknown; createdAt: number }>>([
|
|
["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<typeof csatOverviewHandler>[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")
|
|
})
|
|
})
|
|
|
|
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<ReturnType<typeof requireStaff>>)
|
|
|
|
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, Doc<"companies">>([
|
|
[String(companyA._id), companyA],
|
|
[String(companyB._id), companyB],
|
|
])
|
|
|
|
const ctx = createReportsCtx({ tickets, companies }) as Parameters<typeof hoursByClientHandler>[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,
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
})
|