Skip to content

Commit 66b9563

Browse files
committed
feat: Add ticket models and reminder functionality for "Assigned without Assignee"
Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent 5f4cf4e commit 66b9563

4 files changed

Lines changed: 842 additions & 0 deletions

File tree

backend/test_tickets.py

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
"""
2+
Test build_reminder_candidate with real support ticket data.
3+
4+
This script validates the Pydantic models and calculation functions
5+
against sample data from the support-tickets MCP service.
6+
"""
7+
8+
from datetime import datetime, timezone
9+
10+
from tickets import (
11+
PRIORITY_SLA_MINUTES,
12+
Ticket,
13+
WorkLog,
14+
build_reminder_candidate,
15+
is_assigned_without_assignee,
16+
)
17+
18+
# Sample ticket data from MCP service (10 tickets with varying priorities/statuses)
19+
SAMPLE_TICKETS = [
20+
{
21+
"id": "f4b5d39f-51cb-46af-b551-6b2054b82e84",
22+
"summary": "Teams: Anrufe gehen nicht",
23+
"description": "Teams calls not working",
24+
"status": "in_progress",
25+
"priority": "high",
26+
"assignee": "Agent Quinn",
27+
"assigned_group": "SDE - Service Desk",
28+
"requester_name": "Taylor Anderson",
29+
"requester_email": "taylor.anderson@demo.org",
30+
"city": "Lausanne",
31+
"service": "Communication",
32+
"created_at": "2025-12-17T10:14:03.160125+00:00",
33+
"updated_at": "2025-12-17T10:14:03.160125+00:00",
34+
"work_logs": [
35+
{
36+
"id": "00231b87-f522-4679-9229-4f8dd09fb8af",
37+
"ticket_id": "f4b5d39f-51cb-46af-b551-6b2054b82e84",
38+
"created_at": "2025-12-17T10:14:03.232136+00:00",
39+
"log_type": "creation",
40+
"summary": "Ticket created",
41+
"author": "System",
42+
"time_spent_minutes": 0,
43+
}
44+
],
45+
},
46+
{
47+
"id": "1b144d12-2ea2-4be5-ace1-169a36b3bba9",
48+
"summary": "Adobe Acrobat Pro installation request",
49+
"description": "Need Adobe Acrobat Pro on new workstation",
50+
"status": "in_progress",
51+
"priority": "high",
52+
"assignee": "Agent Finley",
53+
"assigned_group": "SEC - Security Operations",
54+
"requester_name": "Drew Williams",
55+
"requester_email": "drew.williams@test.net",
56+
"city": "Geneva",
57+
"service": "Business Applications",
58+
"created_at": "2025-12-17T05:15:58.864873+00:00",
59+
"updated_at": "2025-12-17T05:15:58.864873+00:00",
60+
"work_logs": [
61+
{
62+
"id": "19874b93-9142-43d8-a026-b85c505095aa",
63+
"ticket_id": "1b144d12-2ea2-4be5-ace1-169a36b3bba9",
64+
"created_at": "2025-12-17T05:15:58.929231+00:00",
65+
"log_type": "creation",
66+
"summary": "Ticket created",
67+
"author": "System",
68+
"time_spent_minutes": 0,
69+
}
70+
],
71+
},
72+
# Critical priority - no assignee (should trigger reminder)
73+
{
74+
"id": "aaaaaaaa-1111-1111-1111-111111111111",
75+
"summary": "CRITICAL: Production database down",
76+
"description": "Database server unreachable",
77+
"status": "assigned",
78+
"priority": "critical",
79+
"assignee": None,
80+
"assigned_group": "DBA Team",
81+
"requester_name": "System Admin",
82+
"requester_email": "admin@company.org",
83+
"city": "Zürich",
84+
"service": "Infrastructure",
85+
"created_at": "2025-12-17T11:00:00+00:00", # 30+ mins ago → overdue
86+
"updated_at": "2025-12-17T11:00:00+00:00",
87+
"work_logs": [],
88+
},
89+
# Medium priority - with reminder already sent
90+
{
91+
"id": "bbbbbbbb-2222-2222-2222-222222222222",
92+
"summary": "VPN connection issues",
93+
"description": "Cannot connect to VPN from home",
94+
"status": "assigned",
95+
"priority": "medium",
96+
"assignee": None,
97+
"assigned_group": "Network Team",
98+
"requester_name": "Remote Worker",
99+
"requester_email": "remote@company.org",
100+
"city": "Bern",
101+
"service": "Network Services",
102+
"created_at": "2025-12-17T06:00:00+00:00",
103+
"updated_at": "2025-12-17T08:00:00+00:00",
104+
"work_logs": [
105+
{
106+
"id": "cc111111-1111-1111-1111-111111111111",
107+
"ticket_id": "bbbbbbbb-2222-2222-2222-222222222222",
108+
"created_at": "2025-12-17T08:00:00+00:00",
109+
"log_type": "reminder",
110+
"summary": "Reminder sent to SGL",
111+
"author": "Incident Manager",
112+
"time_spent_minutes": 0,
113+
}
114+
],
115+
},
116+
# Low priority - new, within SLA
117+
{
118+
"id": "cccccccc-3333-3333-3333-333333333333",
119+
"summary": "Request for second monitor",
120+
"description": "Would like additional monitor for productivity",
121+
"status": "new",
122+
"priority": "low",
123+
"assignee": None,
124+
"assigned_group": "Workplace Services",
125+
"requester_name": "Office Worker",
126+
"requester_email": "worker@company.org",
127+
"city": "Basel",
128+
"service": "Workplace",
129+
"created_at": "2025-12-17T11:30:00+00:00", # Recent → within SLA
130+
"updated_at": "2025-12-17T11:30:00+00:00",
131+
"work_logs": [],
132+
},
133+
# High priority - overdue, multiple reminders
134+
{
135+
"id": "dddddddd-4444-4444-4444-444444444444",
136+
"summary": "Email not syncing on mobile",
137+
"description": "Outlook mobile app stuck syncing",
138+
"status": "assigned",
139+
"priority": "high",
140+
"assignee": None,
141+
"assigned_group": "SDE - Service Desk",
142+
"requester_name": "Mobile User",
143+
"requester_email": "mobile@company.org",
144+
"city": "Luzern",
145+
"service": "Communication",
146+
"created_at": "2025-12-17T04:00:00+00:00", # 8+ hours ago
147+
"updated_at": "2025-12-17T10:00:00+00:00",
148+
"work_logs": [
149+
{
150+
"id": "dd111111-1111-1111-1111-111111111111",
151+
"ticket_id": "dddddddd-4444-4444-4444-444444444444",
152+
"created_at": "2025-12-17T06:00:00+00:00",
153+
"log_type": "reminder",
154+
"summary": "First reminder sent",
155+
"author": "Incident Manager",
156+
"time_spent_minutes": 0,
157+
},
158+
{
159+
"id": "dd222222-2222-2222-2222-222222222222",
160+
"ticket_id": "dddddddd-4444-4444-4444-444444444444",
161+
"created_at": "2025-12-17T09:00:00+00:00",
162+
"log_type": "reminder",
163+
"summary": "Second reminder sent",
164+
"author": "Incident Manager",
165+
"time_spent_minutes": 0,
166+
},
167+
],
168+
},
169+
# Resolved ticket - should not need reminder
170+
{
171+
"id": "eeeeeeee-5555-5555-5555-555555555555",
172+
"summary": "Password reset request",
173+
"description": "User forgot password",
174+
"status": "resolved",
175+
"priority": "medium",
176+
"assignee": "Agent Smith",
177+
"assigned_group": "SDE - Service Desk",
178+
"requester_name": "Forgetful User",
179+
"requester_email": "forgot@company.org",
180+
"city": "Winterthur",
181+
"service": "Security",
182+
"created_at": "2025-12-17T07:00:00+00:00",
183+
"updated_at": "2025-12-17T07:30:00+00:00",
184+
"work_logs": [
185+
{
186+
"id": "ee111111-1111-1111-1111-111111111111",
187+
"ticket_id": "eeeeeeee-5555-5555-5555-555555555555",
188+
"created_at": "2025-12-17T07:30:00+00:00",
189+
"log_type": "resolution",
190+
"summary": "Password reset completed",
191+
"author": "Agent Smith",
192+
"time_spent_minutes": 15,
193+
}
194+
],
195+
},
196+
# Critical with assignee - no reminder needed
197+
{
198+
"id": "ffffffff-6666-6666-6666-666666666666",
199+
"summary": "CRITICAL: Network switch failure",
200+
"description": "Core switch in DC1 unresponsive",
201+
"status": "in_progress",
202+
"priority": "critical",
203+
"assignee": "Network Admin",
204+
"assigned_group": "Network Team",
205+
"requester_name": "NOC",
206+
"requester_email": "noc@company.org",
207+
"city": "Zürich",
208+
"service": "Infrastructure",
209+
"created_at": "2025-12-17T11:45:00+00:00",
210+
"updated_at": "2025-12-17T11:50:00+00:00",
211+
"work_logs": [],
212+
},
213+
# Pending ticket - waiting on user
214+
{
215+
"id": "11111111-7777-7777-7777-777777777777",
216+
"summary": "Software license clarification",
217+
"description": "Need to verify license type",
218+
"status": "pending",
219+
"priority": "low",
220+
"assignee": "Agent Brown",
221+
"assigned_group": "Licensing Team",
222+
"requester_name": "License Requester",
223+
"requester_email": "license@company.org",
224+
"city": "St. Gallen",
225+
"service": "Business Applications",
226+
"created_at": "2025-12-16T14:00:00+00:00",
227+
"updated_at": "2025-12-17T09:00:00+00:00",
228+
"work_logs": [],
229+
},
230+
# New ticket, no group assigned yet
231+
{
232+
"id": "22222222-8888-8888-8888-888888888888",
233+
"summary": "General inquiry about training",
234+
"description": "When is the next Excel training?",
235+
"status": "new",
236+
"priority": "low",
237+
"assignee": None,
238+
"assigned_group": None,
239+
"requester_name": "Curious Employee",
240+
"requester_email": "curious@company.org",
241+
"city": "Emmen",
242+
"service": "Workplace",
243+
"created_at": "2025-12-17T11:55:00+00:00",
244+
"updated_at": "2025-12-17T11:55:00+00:00",
245+
"work_logs": [],
246+
},
247+
]
248+
249+
250+
def parse_ticket_with_worklogs(data: dict) -> tuple[Ticket, list[WorkLog]]:
251+
"""Parse raw ticket data into Ticket and WorkLog models."""
252+
work_logs_data = data.pop("work_logs", [])
253+
ticket = Ticket.model_validate(data)
254+
work_logs = [WorkLog.model_validate(wl) for wl in work_logs_data]
255+
return ticket, work_logs
256+
257+
258+
def main():
259+
print("=" * 70)
260+
print("Testing build_reminder_candidate with 10 support tickets")
261+
print("=" * 70)
262+
print()
263+
264+
# Use a fixed "now" for consistent testing
265+
test_now = datetime(2025, 12, 17, 12, 0, 0, tzinfo=timezone.utc)
266+
print(f"Test time (now): {test_now.isoformat()}")
267+
print()
268+
269+
# Show SLA deadlines
270+
print("SLA Deadlines by Priority:")
271+
for priority, minutes in PRIORITY_SLA_MINUTES.items():
272+
print(f" {priority.value:10s}: {minutes:4d} min ({minutes // 60}h {minutes % 60}m)")
273+
print()
274+
275+
# Process each ticket
276+
print("-" * 70)
277+
print(f"{'#':>2} {'Priority':10s} {'Status':12s} {'Elapsed':>8s} {'SLA':>6s} {'Overdue':>8s} {'Reminded':>9s} {'NeedsReminder':>13s}")
278+
print("-" * 70)
279+
280+
candidates_needing_reminder = []
281+
282+
for i, raw_data in enumerate(SAMPLE_TICKETS, 1):
283+
# Make a copy to avoid mutating original
284+
data = {**raw_data, "work_logs": raw_data.get("work_logs", [])}
285+
work_logs_data = data.pop("work_logs")
286+
287+
ticket = Ticket.model_validate(data)
288+
work_logs = [WorkLog.model_validate(wl) for wl in work_logs_data]
289+
290+
# Build reminder candidate
291+
candidate = build_reminder_candidate(ticket, work_logs, now=test_now)
292+
293+
# Check if needs reminder (assigned without assignee + overdue)
294+
needs_reminder = is_assigned_without_assignee(ticket) and candidate.is_overdue
295+
296+
elapsed_str = f"{candidate.minutes_since_creation}m"
297+
sla_str = f"{candidate.sla_deadline_minutes}m"
298+
299+
print(
300+
f"{i:2d} "
301+
f"{ticket.priority.value:10s} "
302+
f"{ticket.status.value:12s} "
303+
f"{elapsed_str:>8s} "
304+
f"{sla_str:>6s} "
305+
f"{'YES' if candidate.is_overdue else 'no':>8s} "
306+
f"{candidate.reminder_count:>9d} "
307+
f"{'>>> YES <<<' if needs_reminder else 'no':>13s}"
308+
)
309+
310+
if needs_reminder:
311+
candidates_needing_reminder.append((i, ticket, candidate))
312+
313+
print("-" * 70)
314+
print()
315+
316+
# Summary
317+
print("=" * 70)
318+
print("TICKETS NEEDING REMINDER:")
319+
print("=" * 70)
320+
321+
if not candidates_needing_reminder:
322+
print(" None")
323+
else:
324+
for idx, ticket, candidate in candidates_needing_reminder:
325+
reminded_msg = f" (reminded {candidate.reminder_count}x)" if candidate.was_reminded_before else ""
326+
print(f" #{idx}: {ticket.summary}")
327+
print(f" ID: {ticket.id}")
328+
print(f" Priority: {ticket.priority.value} | Group: {ticket.assigned_group}")
329+
print(f" Overdue by: {candidate.minutes_since_creation - candidate.sla_deadline_minutes} minutes{reminded_msg}")
330+
print()
331+
332+
print("=" * 70)
333+
print("TEST PASSED: All tickets parsed and analyzed successfully!")
334+
print("=" * 70)
335+
336+
337+
if __name__ == "__main__":
338+
main()

0 commit comments

Comments
 (0)