mirror of
https://github.com/hanzoai/helpdesk.git
synced 2026-10-12 16:56:02 +00:00
test: add tests
This commit is contained in:
@@ -152,10 +152,26 @@ import { computed, provide, ref } from "vue";
|
||||
import ChartItem from "./components/ChartItem.vue";
|
||||
import { __ } from "@/translation";
|
||||
|
||||
type LayoutItem = {
|
||||
chart: string;
|
||||
data: Record<string, unknown>;
|
||||
layout: {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
i: string;
|
||||
minW?: number;
|
||||
minH?: number;
|
||||
maxW?: number;
|
||||
maxH?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const { userName, userId } = storeToRefs(useAuthStore());
|
||||
const editing = ref(false);
|
||||
const layout = ref([]);
|
||||
const oldLayout = ref([]);
|
||||
const layout = ref<LayoutItem[]>([]);
|
||||
const oldLayout = ref<LayoutItem[]>([]);
|
||||
|
||||
const isDirty = computed(() => {
|
||||
return JSON.stringify(layout.value) !== JSON.stringify(oldLayout.value);
|
||||
@@ -167,9 +183,13 @@ const isLoading = computed(() => {
|
||||
});
|
||||
|
||||
const agentDashboard = createResource({
|
||||
url: "helpdesk.api.agent_dashboard.get_dashboard",
|
||||
url: "helpdesk.api.agent_home.agent_home.get_dashboard",
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
onSuccess(data: {
|
||||
layout: LayoutItem[];
|
||||
default_layout: string;
|
||||
dashboard_id?: string;
|
||||
}) {
|
||||
layout.value = data.layout;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ const chartConfig = computed(() => {
|
||||
});
|
||||
|
||||
const getAgentTicketsResource = createResource({
|
||||
url: "helpdesk.api.agent_dashboard.get_agent_tickets",
|
||||
url: "helpdesk.api.agent_home.agent_home.get_agent_tickets",
|
||||
type: "GET",
|
||||
makeParams: () => {
|
||||
return {
|
||||
|
||||
@@ -58,7 +58,7 @@ const chartConfig = computed(() => {
|
||||
});
|
||||
|
||||
const getAvgFirstResponseTimeResource = createResource({
|
||||
url: "helpdesk.api.agent_dashboard.get_avg_first_response_time",
|
||||
url: "helpdesk.api.agent_home.agent_home.get_avg_first_response_time",
|
||||
type: "GET",
|
||||
makeParams: () => {
|
||||
return {
|
||||
|
||||
@@ -57,7 +57,7 @@ const chartConfig = computed(() => {
|
||||
});
|
||||
|
||||
const getAvgResolutionTimeResource = createResource({
|
||||
url: "helpdesk.api.agent_dashboard.get_avg_resolution_time",
|
||||
url: "helpdesk.api.agent_home.agent_home.get_avg_resolution_time",
|
||||
type: "GET",
|
||||
makeParams: () => {
|
||||
return {
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, markRaw, onMounted, ref } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import EChart from "./EChart.vue";
|
||||
import { EChartsOption } from "echarts";
|
||||
import { createResource, TabButtons } from "frappe-ui";
|
||||
@@ -84,7 +84,7 @@ const props = defineProps({
|
||||
const currentDuration = ref("6m");
|
||||
|
||||
const getAvgTimeMetricsResource = createResource({
|
||||
url: "helpdesk.api.agent_dashboard.get_avg_time_metrics",
|
||||
url: "helpdesk.api.agent_home.agent_home.get_avg_time_metrics",
|
||||
type: "GET",
|
||||
makeParams: () => {
|
||||
return {
|
||||
@@ -132,8 +132,12 @@ const chartConfig = computed<EChartsOption>(() => {
|
||||
color: "#000",
|
||||
},
|
||||
formatter: (params) => {
|
||||
const [category, firstResponse, resolution] = params.data;
|
||||
if (params.seriesIndex === 0) {
|
||||
const p = params as unknown as {
|
||||
data: [string, number, number];
|
||||
seriesIndex: number;
|
||||
};
|
||||
const [category, firstResponse, resolution] = p.data;
|
||||
if (p.seriesIndex === 0) {
|
||||
return `<span>${category}</span><br/>${__(
|
||||
"Avg. First Response"
|
||||
)}: <b>${formatTime(firstResponse, {
|
||||
@@ -162,7 +166,7 @@ const chartConfig = computed<EChartsOption>(() => {
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: (value) => {
|
||||
formatter: (value: number) => {
|
||||
if (value < 3600) {
|
||||
return (value / 60).toFixed(0) + "m";
|
||||
} else if (value < 86400) {
|
||||
|
||||
@@ -163,7 +163,7 @@ const chartConfig = computed(() => {
|
||||
});
|
||||
|
||||
const getPendingTicketsResource = createResource({
|
||||
url: "helpdesk.api.agent_dashboard.get_pending_tickets",
|
||||
url: "helpdesk.api.agent_home.agent_home.get_pending_tickets",
|
||||
});
|
||||
|
||||
function getPriorityBadgeColor(integerValue: number) {
|
||||
|
||||
@@ -412,7 +412,7 @@ const getRatingColor = (rating: number) => {
|
||||
};
|
||||
|
||||
const getRecentFeedbackResource = createResource({
|
||||
url: "helpdesk.api.agent_dashboard.get_recent_feedback",
|
||||
url: "helpdesk.api.agent_home.agent_home.get_recent_feedback",
|
||||
makeParams: () => ({
|
||||
period: currentPeriod.value,
|
||||
sort_order: currentSort.value,
|
||||
|
||||
@@ -119,7 +119,7 @@ const goToAllTickets = () => {
|
||||
};
|
||||
|
||||
const getRecentlyAssignedTickets = createResource({
|
||||
url: "helpdesk.api.agent_dashboard.get_recently_assigned_tickets",
|
||||
url: "helpdesk.api.agent_home.agent_home.get_recently_assigned_tickets",
|
||||
type: "GET",
|
||||
});
|
||||
|
||||
|
||||
@@ -222,17 +222,44 @@ import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import TimerIcon from "~icons/lucide/timer";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
interface SlaTicket {
|
||||
name: string;
|
||||
subject: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
integer_value: number;
|
||||
agent_group: string;
|
||||
first_responded_on: string | null;
|
||||
response_by: string;
|
||||
resolution_date: string | null;
|
||||
resolution_by: string;
|
||||
}
|
||||
|
||||
interface SlaViolationsData {
|
||||
total_sla_violations_count: number;
|
||||
upcoming_sla_violations: SlaTicket[];
|
||||
min_priority: number;
|
||||
max_priority: number;
|
||||
}
|
||||
|
||||
interface SortByFieldname {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface SortBy {
|
||||
direction: "asc" | "desc";
|
||||
fieldname: SortByFieldname;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
data: SlaViolationsData;
|
||||
}>();
|
||||
|
||||
const { getStatus } = useTicketStatusStore();
|
||||
const router = useRouter();
|
||||
const priorityFilter = ref("");
|
||||
const sortBy = ref({
|
||||
const sortBy = ref<SortBy>({
|
||||
direction: "asc",
|
||||
fieldname: {
|
||||
label: __("First Response"),
|
||||
@@ -245,7 +272,7 @@ const getPriorityListResource = createListResource({
|
||||
doctype: "HD Ticket Priority",
|
||||
fields: ["name"],
|
||||
auto: true,
|
||||
transform(data) {
|
||||
transform(data: { name: string }[]) {
|
||||
return data.map((d) => d.name);
|
||||
},
|
||||
});
|
||||
@@ -302,7 +329,7 @@ const sortDropdownOptions = computed(() => {
|
||||
});
|
||||
|
||||
const upcomingSlaViolations = createResource({
|
||||
url: "helpdesk.api.agent_dashboard.get_upcoming_sla_violations",
|
||||
url: "helpdesk.api.agent_home.agent_home.get_upcoming_sla_violations",
|
||||
});
|
||||
|
||||
function getPriorityBadgeColor(integerValue: number) {
|
||||
@@ -317,7 +344,7 @@ function getPriorityBadgeColor(integerValue: number) {
|
||||
return "gray";
|
||||
}
|
||||
|
||||
const goToTicket = (ticket: any) => {
|
||||
const goToTicket = (ticket: SlaTicket) => {
|
||||
router.push({
|
||||
name: "TicketAgent",
|
||||
params: { ticketId: ticket.name },
|
||||
|
||||
@@ -47,9 +47,9 @@ def get_dashboard(reset_layout=False):
|
||||
|
||||
for chart in layout:
|
||||
method_name = f"get_{chart['chart']}"
|
||||
if hasattr(frappe.get_attr("helpdesk.api.agent_dashboard"), method_name):
|
||||
if hasattr(frappe.get_attr("helpdesk.api.agent_home.agent_home"), method_name):
|
||||
method = getattr(
|
||||
frappe.get_attr("helpdesk.api.agent_dashboard"), method_name
|
||||
frappe.get_attr("helpdesk.api.agent_home.agent_home"), method_name
|
||||
)
|
||||
chart["data"] = method()
|
||||
else:
|
||||
@@ -232,79 +232,6 @@ def get_avg_resolution_time(period="last month"):
|
||||
return _get_avg_time_metric(period, "resolution_time")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@agent_only
|
||||
def get_unresolved_tickets():
|
||||
allowed_statuses = frappe.get_all(
|
||||
"HD Ticket Status", filters={"category": ["!=", "Resolved"]}, pluck="name"
|
||||
)
|
||||
count = frappe.db.count(
|
||||
"HD Ticket",
|
||||
filters=[
|
||||
["_assign", "like", f"%{frappe.session.user}%"],
|
||||
["status", "in", allowed_statuses],
|
||||
],
|
||||
)
|
||||
return {"total": count}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@agent_only
|
||||
def get_recently_assigned_tickets():
|
||||
one_week_ago = frappe.utils.add_days(frappe.utils.nowdate(), -7)
|
||||
|
||||
todo = DocType("ToDo")
|
||||
assigned_tickets = (
|
||||
frappe.qb.from_(todo)
|
||||
.select(todo.reference_name)
|
||||
.distinct()
|
||||
.where(todo.reference_type == "HD Ticket")
|
||||
.where(todo.allocated_to == frappe.session.user)
|
||||
.where(todo.creation >= one_week_ago)
|
||||
.where(todo.status == "Open")
|
||||
.run(as_dict=False)
|
||||
)
|
||||
ticket_names = [row[0] for row in assigned_tickets]
|
||||
|
||||
if not ticket_names:
|
||||
return {"count": 0, "tickets": []}
|
||||
|
||||
allowed_statuses = frappe.get_all(
|
||||
"HD Ticket Status", filters={"category": ["!=", "Resolved"]}, pluck="name"
|
||||
)
|
||||
|
||||
# Count tickets assigned in past week that are still assigned and not resolved
|
||||
count = frappe.db.count(
|
||||
"HD Ticket",
|
||||
filters=[
|
||||
["name", "in", ticket_names],
|
||||
["_assign", "like", f"%{frappe.session.user}%"],
|
||||
["status", "in", allowed_statuses],
|
||||
],
|
||||
)
|
||||
|
||||
tickets = frappe.get_list(
|
||||
"HD Ticket",
|
||||
fields=[
|
||||
"name",
|
||||
"subject",
|
||||
"status",
|
||||
"priority",
|
||||
"modified",
|
||||
"creation",
|
||||
],
|
||||
filters=[
|
||||
["name", "in", ticket_names],
|
||||
["_assign", "like", f"%{frappe.session.user}%"],
|
||||
["status", "in", allowed_statuses],
|
||||
],
|
||||
order_by="modified desc",
|
||||
limit=5,
|
||||
)
|
||||
|
||||
return {"count": count, "tickets": tickets}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@agent_only
|
||||
def get_recent_feedback(period="all_time", sort_order="positive_first"):
|
||||
@@ -776,53 +703,3 @@ def get_pending_tickets(ticket_type="all"):
|
||||
"min_priority": min_priority,
|
||||
"max_priority": max_priority,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@agent_only
|
||||
def get_upcoming_sla_violations(priority=None, order_by="response_by asc"):
|
||||
filters = [
|
||||
["sla", "!=", ""],
|
||||
["agreement_status", "in", ["First Response Due", "Resolution Due"]],
|
||||
["status_category", "!=", "Closed"],
|
||||
["_assign", "like", f"%{frappe.session.user}%"],
|
||||
["resolution_by", ">", frappe.utils.now()],
|
||||
["response_by", ">", frappe.utils.now()],
|
||||
]
|
||||
if priority:
|
||||
filters.append(["priority", "=", priority])
|
||||
|
||||
# Get total count of tickets about to breach SLA
|
||||
total_sla_violations_count = frappe.db.count("HD Ticket", filters=filters)
|
||||
|
||||
upcoming_sla_violations = frappe.get_list(
|
||||
"HD Ticket",
|
||||
fields=[
|
||||
"name",
|
||||
"subject",
|
||||
"status",
|
||||
"priority",
|
||||
"priority.integer_value",
|
||||
"agent_group",
|
||||
"response_by",
|
||||
"resolution_by",
|
||||
"resolution_date",
|
||||
"agreement_status",
|
||||
"status_category",
|
||||
"first_responded_on",
|
||||
],
|
||||
filters=filters,
|
||||
order_by=order_by,
|
||||
limit=5,
|
||||
)
|
||||
|
||||
priorities = frappe.get_all("HD Ticket Priority", fields="integer_value")
|
||||
min_priority = min(priorities, key=lambda x: x["integer_value"])["integer_value"]
|
||||
max_priority = max(priorities, key=lambda x: x["integer_value"])["integer_value"]
|
||||
|
||||
return {
|
||||
"upcoming_sla_violations": upcoming_sla_violations,
|
||||
"total_sla_violations_count": total_sla_violations_count,
|
||||
"min_priority": min_priority,
|
||||
"max_priority": max_priority,
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
# Copyright (c) 2024, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from helpdesk.api.agent_home.agent_home import (
|
||||
get_agent_tickets,
|
||||
get_avg_first_response_time,
|
||||
get_avg_resolution_time,
|
||||
get_avg_time_metrics,
|
||||
get_dashboard,
|
||||
get_default_agent_dashboard,
|
||||
get_pending_tickets,
|
||||
get_recent_feedback,
|
||||
)
|
||||
from helpdesk.test_utils import make_agent, make_sla, make_ticket
|
||||
|
||||
agent_email = "[email protected]"
|
||||
|
||||
|
||||
def set_ticket_timestamps(
|
||||
ticket_name: str,
|
||||
creation=None,
|
||||
modified=None,
|
||||
first_response_time=None,
|
||||
resolution_time=None,
|
||||
):
|
||||
"""
|
||||
Updates ticket timestamps directly in the database.
|
||||
Useful for creating test tickets with specific dates.
|
||||
"""
|
||||
if creation:
|
||||
frappe.db.set_value("HD Ticket", ticket_name, "creation", creation)
|
||||
|
||||
if modified:
|
||||
frappe.db.sql(
|
||||
"UPDATE `tabHD Ticket` SET modified=%s WHERE name=%s",
|
||||
(modified, ticket_name),
|
||||
)
|
||||
|
||||
if first_response_time:
|
||||
frappe.db.set_value(
|
||||
"HD Ticket", ticket_name, "first_response_time", first_response_time
|
||||
)
|
||||
|
||||
if resolution_time:
|
||||
frappe.db.set_value(
|
||||
"HD Ticket", ticket_name, "resolution_time", resolution_time
|
||||
)
|
||||
|
||||
|
||||
def create_ticket_with_agent(
|
||||
agent: str = None,
|
||||
creation=None,
|
||||
modified=None,
|
||||
first_response_time=None,
|
||||
resolution_time=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Creates a ticket with flexible options including agent assignment and timestamps.
|
||||
Combines make_ticket functionality with agent assignment and timestamp setting.
|
||||
"""
|
||||
ticket = make_ticket(**kwargs)
|
||||
|
||||
if agent:
|
||||
ticket.assign_agent(agent)
|
||||
|
||||
if creation or modified or first_response_time or resolution_time:
|
||||
set_ticket_timestamps(
|
||||
ticket.name,
|
||||
creation=creation,
|
||||
modified=modified,
|
||||
first_response_time=first_response_time,
|
||||
resolution_time=resolution_time,
|
||||
)
|
||||
|
||||
return ticket
|
||||
|
||||
|
||||
class TestAgentHome(IntegrationTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
make_agent(agent_email, "Test Agent")
|
||||
frappe.set_user(agent_email)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.set_user("Administrator")
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user(agent_email)
|
||||
# Clean up tickets before each test
|
||||
frappe.db.delete("HD Ticket")
|
||||
frappe.db.delete("HD Field Layout", {"user": agent_email})
|
||||
frappe.db.commit()
|
||||
|
||||
def _create_ticket(
|
||||
self,
|
||||
agent=None,
|
||||
creation=None,
|
||||
modified=None,
|
||||
first_response_time=None,
|
||||
resolution_time=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Helper to create a ticket with flexible options"""
|
||||
if agent is None:
|
||||
agent = agent_email
|
||||
|
||||
# Use default values if not provided in kwargs
|
||||
if "subject" not in kwargs:
|
||||
kwargs["subject"] = "Test Ticket"
|
||||
if "description" not in kwargs:
|
||||
kwargs["description"] = "Test Description"
|
||||
|
||||
return create_ticket_with_agent(
|
||||
agent=agent,
|
||||
creation=creation,
|
||||
modified=modified,
|
||||
first_response_time=first_response_time,
|
||||
resolution_time=resolution_time,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def test_returns_valid_json(self):
|
||||
"""Test that default dashboard returns valid JSON string"""
|
||||
result = get_default_agent_dashboard()
|
||||
self.assertIsInstance(result, str)
|
||||
|
||||
# Should be parseable as JSON
|
||||
parsed = json.loads(result)
|
||||
self.assertIsInstance(parsed, list)
|
||||
|
||||
def test_get_dashboard_with_existing_layout(self):
|
||||
"""Test dashboard retrieval when layout exists"""
|
||||
# Create a layout for the user
|
||||
layout_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "HD Field Layout",
|
||||
"user": agent_email,
|
||||
"layout": get_default_agent_dashboard(),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
result = get_dashboard()
|
||||
|
||||
self.assertEqual(result["dashboard_id"], layout_doc.name)
|
||||
|
||||
def test_reset_layout(self):
|
||||
"""Test dashboard reset functionality"""
|
||||
# Create a custom layout
|
||||
custom_layout = json.dumps([{"chart": "custom_chart", "layout": {}}])
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "HD Field Layout",
|
||||
"user": agent_email,
|
||||
"layout": custom_layout,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
result = get_dashboard(reset_layout=True)
|
||||
|
||||
# Should return default layout structure
|
||||
default_layout = json.loads(get_default_agent_dashboard())
|
||||
self.assertEqual(len(result["layout"]), len(default_layout))
|
||||
|
||||
def test_get_agent_tickets_different_periods(self):
|
||||
"""Test different period options"""
|
||||
now = frappe.utils.nowdate()
|
||||
|
||||
# T1: 2 days ago (matches all periods: week, month, 3 months)
|
||||
self._create_ticket(
|
||||
creation=frappe.utils.add_days(now, -2),
|
||||
subject="Ticket 2 days ago",
|
||||
)
|
||||
|
||||
# T2: 10 days ago (matches: month, 3 months)
|
||||
self._create_ticket(
|
||||
creation=frappe.utils.add_days(now, -10),
|
||||
subject="Ticket 10 days ago",
|
||||
)
|
||||
|
||||
# T3: 40 days ago (matches: 3 months only)
|
||||
self._create_ticket(
|
||||
creation=frappe.utils.add_days(now, -40),
|
||||
subject="Ticket 40 days ago",
|
||||
)
|
||||
|
||||
# Test last week (7 days) - Should see 1 ticket (T1)
|
||||
result = get_agent_tickets(period="last week")
|
||||
self.assertIn("data", result)
|
||||
self.assertIn("total", result)
|
||||
self.assertIn("percentage_change", result)
|
||||
self.assertEqual(result["total"], 1)
|
||||
# Verify data is a list with date entries
|
||||
self.assertIsInstance(result["data"], list)
|
||||
self.assertEqual(len(result["data"]), 7) # 7 days of data
|
||||
|
||||
# Test last month (30 days) - Should see 2 tickets (T1, T2)
|
||||
result = get_agent_tickets(period="last month")
|
||||
self.assertEqual(result["total"], 2)
|
||||
self.assertEqual(len(result["data"]), 30) # 30 days of data
|
||||
|
||||
# Test last 3 months (90 days) - Should see 3 tickets (T1, T2, T3)
|
||||
result = get_agent_tickets(period="last 3 months")
|
||||
self.assertEqual(result["total"], 3)
|
||||
self.assertEqual(len(result["data"]), 90) # 90 days of data
|
||||
|
||||
def test_get_agent_tickets_count_per_agent(self):
|
||||
"""Test that ticket count is retrieved correctly for each agent"""
|
||||
# Create a second agent
|
||||
second_agent_email = "[email protected]"
|
||||
make_agent(second_agent_email, "Test Agent 2")
|
||||
|
||||
# Create 2 tickets assigned to the first agent ([email protected])
|
||||
self._create_ticket()
|
||||
self._create_ticket()
|
||||
|
||||
# Create 3 tickets assigned to the second agent
|
||||
for _ in range(3):
|
||||
self._create_ticket(
|
||||
agent=second_agent_email,
|
||||
subject="Test Ticket for Agent 2",
|
||||
)
|
||||
|
||||
# Verify first agent sees only their 2 tickets
|
||||
frappe.set_user(agent_email)
|
||||
result = get_agent_tickets()
|
||||
self.assertEqual(result["total"], 2)
|
||||
|
||||
# Verify second agent sees only their 3 tickets
|
||||
frappe.set_user(second_agent_email)
|
||||
result = get_agent_tickets()
|
||||
self.assertEqual(result["total"], 3)
|
||||
|
||||
# Switch back to original agent
|
||||
frappe.set_user(agent_email)
|
||||
|
||||
def _test_avg_metric(self, method, field_name):
|
||||
"""Helper to test average time metric calculations"""
|
||||
|
||||
# Create tickets for "Current" period (Last 30 days)
|
||||
# Using 2 days ago
|
||||
current_date_1 = frappe.utils.add_days(frappe.utils.nowdate(), -2)
|
||||
self._create_ticket(
|
||||
creation=current_date_1, **{field_name: 30}
|
||||
) # 30 mins/hours
|
||||
self._create_ticket(
|
||||
creation=current_date_1, **{field_name: 60}
|
||||
) # 60 mins/hours
|
||||
# Current Avg = (30 + 60) / 2 = 45
|
||||
|
||||
# Create tickets for "Previous" period (30-60 days ago)
|
||||
# Using 35 days ago
|
||||
previous_date_1 = frappe.utils.add_days(frappe.utils.nowdate(), -35)
|
||||
self._create_ticket(
|
||||
creation=previous_date_1, **{field_name: 20}
|
||||
) # 20 mins/hours
|
||||
self._create_ticket(
|
||||
creation=previous_date_1, **{field_name: 40}
|
||||
) # 40 mins/hours
|
||||
# Previous Avg = (20 + 40) / 2 = 30
|
||||
|
||||
# Calculate Expected Percentage Change
|
||||
# ((Current - Previous) / Previous) * 100
|
||||
# ((45 - 30) / 30) * 100 = (15 / 30) * 100 = 50.0
|
||||
|
||||
result = method(period="last month")
|
||||
|
||||
self.assertEqual(result["average"], 45.0)
|
||||
self.assertEqual(result["percentage_change"], 50.0)
|
||||
|
||||
def test_get_avg_first_response_time(self):
|
||||
"""Test average first response time calculation"""
|
||||
self._test_avg_metric(get_avg_first_response_time, "first_response_time")
|
||||
|
||||
def test_get_avg_resolution_time(self):
|
||||
"""Test average resolution time calculation"""
|
||||
self._test_avg_metric(get_avg_resolution_time, "resolution_time")
|
||||
|
||||
def test_get_recent_feedback_rating_distribution(self):
|
||||
"""Test average and distribution calculations with multiple feedbacks"""
|
||||
# Create multiple tickets with different ratings
|
||||
self._create_ticket(feedback_rating=1.0) # 5 stars
|
||||
self._create_ticket(feedback_rating=0.6) # 3 stars
|
||||
self._create_ticket(feedback_rating=0.2) # 1 star
|
||||
|
||||
# Avg = (5 + 3 + 1) / 3 = 3.0
|
||||
|
||||
result = get_recent_feedback()
|
||||
|
||||
self.assertEqual(result["total_feedbacks"], 3)
|
||||
self.assertEqual(result["average_rating"], 3.0)
|
||||
|
||||
dist = result["rating_distribution"]
|
||||
self.assertEqual(dist[5], 1)
|
||||
self.assertEqual(dist[3], 1)
|
||||
self.assertEqual(dist[1], 1)
|
||||
self.assertEqual(dist[2], 0)
|
||||
self.assertEqual(dist[4], 0)
|
||||
|
||||
def test_get_recent_feedback_different_periods(self):
|
||||
"""Test different period options"""
|
||||
# Create tickets with different ages
|
||||
now = frappe.utils.now_datetime()
|
||||
|
||||
# T1: Today (matches all periods)
|
||||
self._create_ticket(feedback_rating=1.0, modified=now)
|
||||
|
||||
# T2: 10 days ago (matches last month, last 3 months, all time)
|
||||
self._create_ticket(
|
||||
feedback_rating=0.8, modified=frappe.utils.add_days(now, -10)
|
||||
)
|
||||
|
||||
# T3: 40 days ago (matches last 3 months, all time)
|
||||
self._create_ticket(
|
||||
feedback_rating=0.6, modified=frappe.utils.add_days(now, -40)
|
||||
)
|
||||
|
||||
# T4: 100 days ago (only matches all time)
|
||||
self._create_ticket(
|
||||
feedback_rating=0.4, modified=frappe.utils.add_days(now, -100)
|
||||
)
|
||||
|
||||
# Test last_week (7 days) - Should see 1 ticket
|
||||
result = get_recent_feedback(period="last_week")
|
||||
self.assertEqual(result["total_feedbacks"], 1)
|
||||
self.assertEqual(result["average_rating"], 5.0)
|
||||
|
||||
# Test last_month (30 days) - Should see 2 tickets (T1, T2)
|
||||
result = get_recent_feedback(period="last_month")
|
||||
self.assertEqual(result["total_feedbacks"], 2)
|
||||
# Avg = (5 + 4) / 2 = 4.5
|
||||
self.assertEqual(result["average_rating"], 4.5)
|
||||
|
||||
# Test last_3_months (90 days) - Should see 3 tickets (T1, T2, T3)
|
||||
result = get_recent_feedback(period="last_3_months")
|
||||
self.assertEqual(result["total_feedbacks"], 3)
|
||||
# Avg = (5 + 4 + 3) / 3 = 4.0
|
||||
self.assertEqual(result["average_rating"], 4.0)
|
||||
|
||||
# Test all_time - Should see 4 tickets
|
||||
result = get_recent_feedback(period="all_time")
|
||||
self.assertEqual(result["total_feedbacks"], 4)
|
||||
# Avg = (5 + 4 + 3 + 2) / 4 = 3.5
|
||||
self.assertEqual(result["average_rating"], 3.5)
|
||||
|
||||
def test_get_recent_feedback_sort_order(self):
|
||||
"""Test sort order options"""
|
||||
# Create tickets with different ratings
|
||||
self._create_ticket(feedback_rating=1.0) # 5 stars
|
||||
self._create_ticket(feedback_rating=0.6) # 3 stars
|
||||
self._create_ticket(feedback_rating=0.2) # 1 star
|
||||
|
||||
# Test positive_first (Descending)
|
||||
result = get_recent_feedback(sort_order="positive_first")
|
||||
feedbacks = result["recent_feedbacks"]
|
||||
self.assertEqual(len(feedbacks), 3)
|
||||
self.assertEqual(feedbacks[0]["feedback_rating"], 1.0)
|
||||
self.assertEqual(feedbacks[1]["feedback_rating"], 0.6)
|
||||
self.assertEqual(feedbacks[2]["feedback_rating"], 0.2)
|
||||
|
||||
# Test negative_first (Ascending)
|
||||
result = get_recent_feedback(sort_order="negative_first")
|
||||
feedbacks = result["recent_feedbacks"]
|
||||
self.assertEqual(len(feedbacks), 3)
|
||||
self.assertEqual(feedbacks[0]["feedback_rating"], 0.2)
|
||||
self.assertEqual(feedbacks[1]["feedback_rating"], 0.6)
|
||||
self.assertEqual(feedbacks[2]["feedback_rating"], 1.0)
|
||||
|
||||
def test_get_avg_time_metrics_different_periods(self):
|
||||
"""Test different period options with actual data verification"""
|
||||
now = frappe.utils.now_datetime()
|
||||
|
||||
# Create tickets spread across the last year
|
||||
# T1: 1 month ago (matches 3m, 6m, 1y)
|
||||
self._create_ticket(
|
||||
creation=frappe.utils.add_months(now, -1),
|
||||
first_response_time=10,
|
||||
resolution_time=20,
|
||||
)
|
||||
|
||||
# T2: 4 months ago (matches 6m, 1y)
|
||||
self._create_ticket(
|
||||
creation=frappe.utils.add_months(now, -4),
|
||||
first_response_time=20,
|
||||
resolution_time=40,
|
||||
)
|
||||
|
||||
# T3: 8 months ago (matches 1y)
|
||||
self._create_ticket(
|
||||
creation=frappe.utils.add_months(now, -8),
|
||||
first_response_time=30,
|
||||
resolution_time=60,
|
||||
)
|
||||
|
||||
# Test 3m period (Should see T1 only)
|
||||
# Avg First = 10, Avg Res = 20
|
||||
result = get_avg_time_metrics(period="3m")
|
||||
self.assertEqual(result["averages"]["first_response"], 10.0)
|
||||
self.assertEqual(result["averages"]["resolution"], 20.0)
|
||||
|
||||
# Verify data points exist for the months
|
||||
month_labels = [row[0] for row in result["data"]]
|
||||
t1_month = frappe.utils.add_months(now, -1).strftime("%b")
|
||||
self.assertIn(t1_month, month_labels)
|
||||
|
||||
# Test 6m period (Should see T1, T2)
|
||||
# Avg First = (10 + 20) / 2 = 15
|
||||
# Avg Res = (20 + 40) / 2 = 30
|
||||
result = get_avg_time_metrics(period="6m")
|
||||
self.assertEqual(result["averages"]["first_response"], 15.0)
|
||||
self.assertEqual(result["averages"]["resolution"], 30.0)
|
||||
|
||||
# Test 1y period (Should see T1, T2, T3)
|
||||
# Avg First = (10 + 20 + 30) / 3 = 20
|
||||
# Avg Res = (20 + 40 + 60) / 3 = 40
|
||||
result = get_avg_time_metrics(period="1y")
|
||||
self.assertEqual(result["averages"]["first_response"], 20.0)
|
||||
self.assertEqual(result["averages"]["resolution"], 40.0)
|
||||
|
||||
def _create_ticket_with_sla(self, priority="High", agent=None):
|
||||
"""Helper to create a ticket with SLA"""
|
||||
# Create SLA with condition for the priority
|
||||
condition = f"doc.priority == '{priority}'"
|
||||
sla = make_sla(f"Test SLA {priority}", condition)
|
||||
|
||||
# Configure SLA with priority settings
|
||||
sla.reload()
|
||||
|
||||
# Clear and set support days to avoid duplicate workday validation errors
|
||||
sla.support_and_resolution = []
|
||||
for day in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]:
|
||||
sla.append(
|
||||
"support_and_resolution",
|
||||
{
|
||||
"workday": day,
|
||||
"start_time": "10:00:00",
|
||||
"end_time": "18:00:00",
|
||||
},
|
||||
)
|
||||
|
||||
sla.priorities = []
|
||||
sla.append(
|
||||
"priorities",
|
||||
{
|
||||
"priority": priority,
|
||||
"default_priority": 1,
|
||||
"response_time": 60 * 60, # 1 hour
|
||||
"resolution_time": 60 * 60 * 4, # 4 hours
|
||||
},
|
||||
)
|
||||
sla.save()
|
||||
|
||||
# Create ticket with the specified priority
|
||||
ticket = self._create_ticket(
|
||||
subject="SLA Test Ticket", priority=priority, agent=agent
|
||||
)
|
||||
|
||||
# Ensure SLA fields are set for upcoming_sla tests
|
||||
ticket.reload()
|
||||
return ticket
|
||||
|
||||
def test_get_pending_tickets_upcoming_sla_type(self):
|
||||
"""Test getting upcoming SLA violation tickets"""
|
||||
# Create a second agent for non-matching tickets
|
||||
other_agent_email = "[email protected]"
|
||||
make_agent(other_agent_email, "Other Agent SLA")
|
||||
|
||||
# Create SLA ticket for current agent
|
||||
ticket = self._create_ticket_with_sla(priority="High")
|
||||
|
||||
# Create SLA ticket for OTHER agent (should NOT be returned)
|
||||
other_ticket = self._create_ticket_with_sla(
|
||||
priority="High", agent=other_agent_email
|
||||
)
|
||||
|
||||
result = get_pending_tickets(ticket_type="upcoming_sla")
|
||||
|
||||
# Verify our SLA ticket is in the result
|
||||
ticket_names = [t["name"] for t in result["tickets"]]
|
||||
self.assertIn(ticket.name, ticket_names)
|
||||
# Other agent's ticket should NOT be in the result
|
||||
self.assertNotIn(other_ticket.name, ticket_names)
|
||||
|
||||
def test_get_pending_tickets_new_tickets_type(self):
|
||||
"""Test getting newly assigned tickets"""
|
||||
# Create a second agent for non-matching tickets
|
||||
other_agent_email = "[email protected]"
|
||||
make_agent(other_agent_email, "Other Agent New")
|
||||
|
||||
# Create new tickets for current agent
|
||||
ticket1 = self._create_ticket(subject="New Test Ticket 1")
|
||||
ticket2 = self._create_ticket(subject="New Test Ticket 2")
|
||||
ticket3 = self._create_ticket(subject="New Test Ticket 3")
|
||||
|
||||
# Create tickets assigned to OTHER agent (should NOT be returned)
|
||||
self._create_ticket(agent=other_agent_email, subject="Other Agent New Ticket 1")
|
||||
self._create_ticket(agent=other_agent_email, subject="Other Agent New Ticket 2")
|
||||
|
||||
result = get_pending_tickets(ticket_type="new_tickets")
|
||||
|
||||
self.assertIn("tickets", result)
|
||||
self.assertIn("total_pending_tickets", result)
|
||||
# Only current agent's new tickets should be returned
|
||||
self.assertEqual(result["total_pending_tickets"], 3)
|
||||
|
||||
# Verify all new tickets are in the result
|
||||
ticket_names = [t["name"] for t in result["tickets"]]
|
||||
self.assertIn(ticket1.name, ticket_names)
|
||||
self.assertIn(ticket2.name, ticket_names)
|
||||
self.assertIn(ticket3.name, ticket_names)
|
||||
|
||||
# Verify reason format
|
||||
for t in result["tickets"]:
|
||||
self.assertEqual(t["reason"]["type"], "new_tickets")
|
||||
self.assertEqual(t["reason"]["text"], "Recently assigned")
|
||||
|
||||
def test_get_pending_tickets_pending_type(self):
|
||||
"""Test getting tickets pending response"""
|
||||
# Create a second agent for non-matching tickets
|
||||
other_agent_email = "[email protected]"
|
||||
make_agent(other_agent_email, "Other Agent Pending")
|
||||
|
||||
# Create tickets pending response for current agent
|
||||
ticket1 = self._create_ticket(subject="Pending Response Ticket 1")
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"reference_doctype": "HD Ticket",
|
||||
"reference_name": ticket1.name,
|
||||
"sent_or_received": "Received",
|
||||
"content": "Customer message 1",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
ticket2 = self._create_ticket(subject="Pending Response Ticket 2")
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"reference_doctype": "HD Ticket",
|
||||
"reference_name": ticket2.name,
|
||||
"sent_or_received": "Received",
|
||||
"content": "Customer message 2",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
# Create ticket for OTHER agent with communication (should NOT be returned)
|
||||
other_ticket = self._create_ticket(
|
||||
agent=other_agent_email, subject="Other Agent Pending Ticket"
|
||||
)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"reference_doctype": "HD Ticket",
|
||||
"reference_name": other_ticket.name,
|
||||
"sent_or_received": "Received",
|
||||
"content": "Other agent customer message",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
result = get_pending_tickets(ticket_type="pending")
|
||||
|
||||
# Verify current agent's pending tickets are returned
|
||||
self.assertGreaterEqual(result["total_pending_tickets"], 2)
|
||||
|
||||
# Verify pending tickets are in the result
|
||||
ticket_names = [t["name"] for t in result["tickets"]]
|
||||
self.assertIn(ticket1.name, ticket_names)
|
||||
self.assertIn(ticket2.name, ticket_names)
|
||||
|
||||
# Verify reason format
|
||||
for t in result["tickets"]:
|
||||
self.assertEqual(t["reason"]["type"], "pending")
|
||||
self.assertIn("Pending for", t["reason"]["text"])
|
||||
|
||||
def test_get_pending_tickets_all_combines_types(self):
|
||||
"""Test that 'all' type combines different ticket types without duplicates"""
|
||||
# Create a second agent for non-matching tickets
|
||||
other_agent_email = "[email protected]"
|
||||
make_agent(other_agent_email, "Other Agent Combined")
|
||||
|
||||
# Create tickets of different types for current agent
|
||||
new_ticket1 = self._create_ticket(subject="New Test Ticket 1")
|
||||
new_ticket2 = self._create_ticket(subject="New Test Ticket 2")
|
||||
|
||||
pending_ticket = self._create_ticket(subject="Pending Response Ticket")
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"reference_doctype": "HD Ticket",
|
||||
"reference_name": pending_ticket.name,
|
||||
"sent_or_received": "Received",
|
||||
"content": "Customer message",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
# Create tickets for OTHER agent (should NOT be returned)
|
||||
self._create_ticket(agent=other_agent_email, subject="Other Agent New Ticket")
|
||||
other_pending = self._create_ticket(
|
||||
agent=other_agent_email, subject="Other Agent Pending Ticket"
|
||||
)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"reference_doctype": "HD Ticket",
|
||||
"reference_name": other_pending.name,
|
||||
"sent_or_received": "Received",
|
||||
"content": "Other agent customer message",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
result = get_pending_tickets(ticket_type="all")
|
||||
|
||||
# Get ticket names from results
|
||||
ticket_names = [t["name"] for t in result["tickets"]]
|
||||
|
||||
# Verify no duplicates
|
||||
self.assertEqual(len(ticket_names), len(set(ticket_names)))
|
||||
|
||||
# Verify only current agent's tickets are returned
|
||||
self.assertIn(new_ticket1.name, ticket_names)
|
||||
self.assertIn(new_ticket2.name, ticket_names)
|
||||
self.assertIn(pending_ticket.name, ticket_names)
|
||||
|
||||
# Verify total count is correct for current agent
|
||||
self.assertGreaterEqual(result["total_pending_tickets"], 3)
|
||||
@@ -207,3 +207,21 @@ def make_status(name: str = "Test Status", category: str = "Open"):
|
||||
}
|
||||
)
|
||||
return doc.insert(ignore_if_duplicate=True)
|
||||
|
||||
|
||||
def make_agent(email: str, first_name: str = "Test Agent"):
|
||||
"""
|
||||
Creates a test user and HD Agent if they don't exist.
|
||||
Returns the user email.
|
||||
"""
|
||||
if not frappe.db.exists("User", email):
|
||||
frappe.get_doc(
|
||||
{"doctype": "User", "first_name": first_name, "email": email}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
if not frappe.db.exists("HD Agent", {"user": email}):
|
||||
frappe.get_doc({"doctype": "HD Agent", "user": email}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
|
||||
return email
|
||||
|
||||
Reference in New Issue
Block a user