kamau1 commited on
Commit
a71d9fe
·
1 Parent(s): 061cea4

Add TicketStatusHistory model and integrate automatic status logging into ticket assignment workflow.

Browse files
docs/agent/implementation-notes/ROUTER_PREFIX_FIX.md ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Router Prefix Fix - Correcting Double /api/v1 Issue
2
+
3
+ ## Problem
4
+
5
+ Four routers were configured with prefix `/api/v1`, which caused double prefixing since the main app already adds `/api/v1`:
6
+
7
+ ```python
8
+ # WRONG - causes /api/v1/api/v1/...
9
+ api_router.include_router(ticket_assignments.router, prefix="/api/v1", tags=["..."])
10
+ api_router.include_router(expenses.router, prefix="/api/v1", tags=["..."])
11
+ api_router.include_router(progress_reports.router, prefix="/api/v1", tags=["..."])
12
+ api_router.include_router(incident_reports.router, prefix="/api/v1", tags=["..."])
13
+ ```
14
+
15
+ ## URL Construction
16
+
17
+ FastAPI builds URLs by concatenating:
18
+ 1. Main app prefix: `/api/v1` (from main.py)
19
+ 2. Router prefix: `/api/v1` (WRONG - duplicates the prefix)
20
+ 3. Route path: `/tickets/{id}/self-assign`
21
+
22
+ Result: `/api/v1/api/v1/tickets/{id}/self-assign` ❌
23
+
24
+ ## Solution
25
+
26
+ Changed router prefixes to be semantic paths without `/api/v1`:
27
+
28
+ ```python
29
+ # CORRECT - results in /api/v1/ticket-assignments/...
30
+ api_router.include_router(ticket_assignments.router, prefix="/ticket-assignments", tags=["..."])
31
+ api_router.include_router(expenses.router, prefix="/expenses", tags=["..."])
32
+ api_router.include_router(progress_reports.router, prefix="/progress-reports", tags=["..."])
33
+ api_router.include_router(incident_reports.router, prefix="/incident-reports", tags=["..."])
34
+ ```
35
+
36
+ ## Fixed Endpoints
37
+
38
+ ### 1. Ticket Assignments
39
+
40
+ **Before:** `/api/v1/api/v1/tickets/{id}/self-assign` ❌
41
+ **After:** `/api/v1/ticket-assignments/tickets/{id}/self-assign` ✅
42
+
43
+ **Routes affected:**
44
+ - `POST /api/v1/ticket-assignments/tickets/{ticket_id}/self-assign` - Self-assign ticket
45
+ - `POST /api/v1/ticket-assignments/tickets/{ticket_id}/assign` - Assign to agent
46
+ - `POST /api/v1/ticket-assignments/tickets/{ticket_id}/assign-team` - Assign to team
47
+ - `POST /api/v1/ticket-assignments/assignments/{assignment_id}/accept` - Accept assignment
48
+ - `POST /api/v1/ticket-assignments/assignments/{assignment_id}/reject` - Reject assignment
49
+ - `POST /api/v1/ticket-assignments/assignments/{assignment_id}/start-journey` - Start journey
50
+ - `POST /api/v1/ticket-assignments/assignments/{assignment_id}/arrived` - Record arrival
51
+ - `POST /api/v1/ticket-assignments/assignments/{assignment_id}/complete` - Complete work
52
+ - `POST /api/v1/ticket-assignments/assignments/{assignment_id}/drop` - Drop ticket
53
+ - `GET /api/v1/ticket-assignments/tickets/available-for-me` - Get available tickets
54
+ - `GET /api/v1/ticket-assignments/assignments/{assignment_id}` - Get assignment details
55
+ - `GET /api/v1/ticket-assignments/tickets/{ticket_id}/assignments` - Get ticket assignments
56
+
57
+ ### 2. Expenses
58
+
59
+ **Before:** `/api/v1/api/v1/` ❌
60
+ **After:** `/api/v1/expenses/` ✅
61
+
62
+ **Routes affected:**
63
+ - `POST /api/v1/expenses/` - Create expense
64
+ - `GET /api/v1/expenses/` - List expenses
65
+ - `GET /api/v1/expenses/{expense_id}` - Get expense
66
+ - `PATCH /api/v1/expenses/{expense_id}` - Update expense
67
+ - `POST /api/v1/expenses/{expense_id}/approve` - Approve expense
68
+ - `POST /api/v1/expenses/{expense_id}/payment-details` - Add payment details
69
+ - `POST /api/v1/expenses/{expense_id}/mark-paid` - Mark as paid
70
+ - `GET /api/v1/expenses/stats` - Get expense stats
71
+ - `DELETE /api/v1/expenses/{expense_id}` - Delete expense
72
+
73
+ ### 3. Progress Reports
74
+
75
+ **Before:** `/api/v1/api/v1/` ❌
76
+ **After:** `/api/v1/progress-reports/` ✅
77
+
78
+ **Routes affected:**
79
+ - `POST /api/v1/progress-reports/` - Create progress report
80
+ - `GET /api/v1/progress-reports/` - List progress reports
81
+ - `GET /api/v1/progress-reports/stats` - Get stats
82
+ - `GET /api/v1/progress-reports/{report_id}` - Get report
83
+ - `PATCH /api/v1/progress-reports/{report_id}` - Update report
84
+ - `DELETE /api/v1/progress-reports/{report_id}` - Delete report
85
+
86
+ ### 4. Incident Reports
87
+
88
+ **Before:** `/api/v1/api/v1/` ❌
89
+ **After:** `/api/v1/incident-reports/` ✅
90
+
91
+ **Routes affected:**
92
+ - `POST /api/v1/incident-reports/` - Create incident report
93
+ - `GET /api/v1/incident-reports/` - List incident reports
94
+ - `GET /api/v1/incident-reports/stats` - Get stats
95
+ - `GET /api/v1/incident-reports/{report_id}` - Get report
96
+ - `PATCH /api/v1/incident-reports/{report_id}` - Update report
97
+ - `POST /api/v1/incident-reports/{report_id}/resolve` - Resolve incident
98
+ - `DELETE /api/v1/incident-reports/{report_id}` - Delete report
99
+
100
+ ## Files Modified
101
+
102
+ - `src/app/api/v1/router.py` - Fixed 4 router prefixes
103
+
104
+ ## Breaking Changes
105
+
106
+ **Yes - this is a breaking change for any frontend code calling these endpoints.**
107
+
108
+ ### Frontend Updates Required
109
+
110
+ If your frontend is calling any of these endpoints, you need to update the URLs:
111
+
112
+ **Ticket Assignments:**
113
+ ```javascript
114
+ // OLD (broken)
115
+ POST /api/v1/api/v1/tickets/{id}/self-assign
116
+
117
+ // NEW (correct)
118
+ POST /api/v1/ticket-assignments/tickets/{id}/self-assign
119
+ ```
120
+
121
+ **Expenses:**
122
+ ```javascript
123
+ // OLD (broken)
124
+ POST /api/v1/api/v1/
125
+
126
+ // NEW (correct)
127
+ POST /api/v1/expenses/
128
+ ```
129
+
130
+ **Progress Reports:**
131
+ ```javascript
132
+ // OLD (broken)
133
+ POST /api/v1/api/v1/
134
+
135
+ // NEW (correct)
136
+ POST /api/v1/progress-reports/
137
+ ```
138
+
139
+ **Incident Reports:**
140
+ ```javascript
141
+ // OLD (broken)
142
+ POST /api/v1/api/v1/
143
+
144
+ // NEW (correct)
145
+ POST /api/v1/incident-reports/
146
+ ```
147
+
148
+ ## Testing
149
+
150
+ After server restart, verify these endpoints work:
151
+
152
+ ```bash
153
+ # Test ticket assignment
154
+ curl -X POST "http://localhost:7860/api/v1/ticket-assignments/tickets/{ticket_id}/self-assign" \
155
+ -H "Authorization: Bearer {token}"
156
+
157
+ # Test expense creation
158
+ curl -X POST "http://localhost:7860/api/v1/expenses/" \
159
+ -H "Authorization: Bearer {token}"
160
+
161
+ # Test progress report creation
162
+ curl -X POST "http://localhost:7860/api/v1/progress-reports/" \
163
+ -H "Authorization: Bearer {token}"
164
+
165
+ # Test incident report creation
166
+ curl -X POST "http://localhost:7860/api/v1/incident-reports/" \
167
+ -H "Authorization: Bearer {token}"
168
+ ```
169
+
170
+ ## Migration Steps
171
+
172
+ 1. **Deploy backend changes** - Server restart required
173
+ 2. **Update frontend** - Change all API calls to use new URLs
174
+ 3. **Test all affected features** - Verify ticket assignments, expenses, reports work
175
+
176
+ ## Why This Happened
177
+
178
+ The routers were likely copied from a template or created at different times without consistent prefix conventions. The `/api/v1` prefix should only be added at the main app level, not at individual router levels.
179
+
180
+ ## Prevention
181
+
182
+ When creating new routers, use semantic prefixes:
183
+ - ✅ `prefix="/users"`
184
+ - ✅ `prefix="/tickets"`
185
+ - ✅ `prefix="/ticket-assignments"`
186
+ - ❌ `prefix="/api/v1"` (never do this)
187
+
188
+ The `/api/v1` prefix is already added by the main app, so individual routers should only specify their resource path.
docs/devlogs/browser/response.json CHANGED
@@ -1,93 +1,43 @@
1
- Request URL
2
- https://kamau1-swiftops-backend.hf.space/api/v1/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/detail
3
- Request Method
4
- GET
5
- Status Code
6
- 200 OK
7
- Remote Address
8
- 3.208.147.173:443
9
- Referrer Policy
10
- strict-origin-when-cross-origin
11
-
12
-
13
  {
14
- "ticket": {
15
- "id": "8f08ad14-df8b-4780-84e7-0d45e133f2a6",
16
- "project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a",
17
- "source": "sales_order",
18
- "source_id": "5cb76cc5-3d9a-4ce0-87a8-43f912896c1f",
19
- "ticket_name": "Catherine Njoki",
20
- "ticket_type": "installation",
21
- "service_type": "ftth",
22
- "work_description": "Install Basic 10Mbps for Catherine Njoki",
23
- "status": "open",
24
- "priority": "normal",
25
- "scheduled_date": "2025-12-28",
26
- "scheduled_time_slot": "Afternoon 1PM-4PM",
27
- "due_date": "2025-12-01T07:52:17.604326Z",
28
- "sla_target_date": "2025-12-01T07:52:17.604326Z",
29
- "sla_violated": false,
30
- "started_at": null,
31
- "completed_at": null,
32
- "is_invoiced": false,
33
- "invoiced_at": null,
34
- "project_region_id": null,
35
- "work_location_latitude": null,
36
- "work_location_longitude": null,
37
- "work_location_verified": false,
38
- "notes": null,
39
- "version": 1,
40
- "created_at": "2025-11-28T07:52:17.604326Z",
41
- "updated_at": "2025-11-28T07:52:17.604328Z",
42
- "project_title": null,
43
- "region_name": null,
44
- "customer_name": null,
45
- "is_open": true,
46
- "is_assigned": false,
47
- "is_in_progress": false,
48
- "is_completed": false,
49
- "is_cancelled": false,
50
- "is_active": true,
51
- "is_overdue": false,
52
- "has_region": false,
53
- "has_schedule": true,
54
- "can_be_assigned": true,
55
- "can_be_started": false,
56
- "can_be_completed": false,
57
- "can_be_cancelled": true
58
  },
59
- "available_actions": [],
60
- "current_assignment": null,
61
- "team_info": {
62
- "required_size": 1,
63
- "assigned_size": 0,
64
- "is_full": false,
65
- "assigned_agents": []
66
  },
67
- "message": "Ticket not in your assigned region",
68
- "source_data": {
69
- "type": "sales_order",
70
- "id": "5cb76cc5-3d9a-4ce0-87a8-43f912896c1f",
71
- "order_number": "ORD-2025-013",
72
- "customer_preferred_package": "Basic 10Mbps",
73
- "package_price": 2500.0,
74
- "installation_address": "1111 Kasarani Road",
75
- "installation_latitude": null,
76
- "installation_longitude": null,
77
- "preferred_visit_date": "2025-12-28",
78
- "status": "processed"
79
- },
80
- "customer": {
81
- "id": "40bee251-a186-4194-9d01-e74597dc8326",
82
- "name": "Catherine Njoki",
83
- "phone": "+254734567891",
84
- "email": "catherine.njoki@email.com",
85
- "address": null,
86
- "location_latitude": null,
87
- "location_longitude": null
88
- },
89
- "expenses": [],
90
- "images": [],
91
- "comments": [],
92
- "assignments": []
93
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  {
2
+ "id": "b3a83bd0-d287-4cea-a1c8-8bef145c1296",
3
+ "ticket_id": "8f08ad14-df8b-4780-84e7-0d45e133f2a6",
4
+ "user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
5
+ "assigned_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
6
+ "action": "accepted",
7
+ "is_self_assigned": true,
8
+ "execution_order": null,
9
+ "planned_start_time": null,
10
+ "assigned_at": "2025-11-28T09:27:41.952753Z",
11
+ "responded_at": "2025-11-28T09:27:41.952757Z",
12
+ "journey_started_at": null,
13
+ "arrived_at": null,
14
+ "ended_at": null,
15
+ "journey_start_latitude": null,
16
+ "journey_start_longitude": null,
17
+ "arrival_latitude": null,
18
+ "arrival_longitude": null,
19
+ "arrival_verified": false,
20
+ "journey_location_history": [],
21
+ "status": "ACCEPTED",
22
+ "is_active": true,
23
+ "travel_time_minutes": null,
24
+ "work_time_minutes": null,
25
+ "total_time_minutes": null,
26
+ "journey_distance_km": null,
27
+ "reason": null,
28
+ "notes": null,
29
+ "user": {
30
+ "id": "43b778b0-2062-4724-abbb-916a4835a9b0",
31
+ "full_name": "Viyisa Sasa",
32
+ "email": "viyisa8151@feralrex.com",
33
+ "phone": "+25470000001"
 
 
 
 
 
 
 
 
 
 
 
 
34
  },
35
+ "assigned_by": {
36
+ "id": "43b778b0-2062-4724-abbb-916a4835a9b0",
37
+ "full_name": "Viyisa Sasa",
38
+ "email": "viyisa8151@feralrex.com",
39
+ "phone": "+25470000001"
 
 
40
  },
41
+ "created_at": "2025-11-28T09:27:41.997778Z",
42
+ "updated_at": "2025-11-28T09:27:41.997785Z"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
docs/devlogs/db/logs.json CHANGED
@@ -1,5 +1,3 @@
1
- INSERT INTO "public"."sales_orders" ("id", "customer_id", "project_id", "order_number", "service_request_number", "sales_agent_id", "agent_name", "agent_number", "customer_preferred_package", "package_price", "project_region_id", "installation_address_line1", "installation_address_line2", "installation_maps_link", "installation_latitude", "installation_longitude", "is_installation_required", "preferred_installation_date", "preferred_visit_date", "preferred_visit_time", "received_date", "received_time", "status", "is_processed", "is_ticket_created", "processed_at", "processed_by_user_id", "cancellation_reason", "notes", "additional_metadata", "submitted_by_user_id", "submitted_at", "created_at", "updated_at", "deleted_at") VALUES ('0091e819-ec49-4966-8632-52f965f3e24d', '128a3846-d5b3-46a8-9f1e-657ffbcbb0e3', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-010', null, null, 'Mike Ochieng', '+254712345678', 'Premium Fiber 100Mbps', '8000.00', '4cd27765-5720-4cc0-872e-bf0da3cd1898', '707 Runda Gardens', 'Villa 15', 'https://maps.google.com/?q=-1.235,36.805', '-1.235', '36.805', 'true', null, '2025-12-25', 'Anytime', '2025-11-24', null, 'pending', 'false', 'false', null, null, null, 'Holiday season installation', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 09:13:22.938543+00', '2025-11-25 09:13:22.938543+00', '2025-11-25 09:13:22.938543+00', null), ('16155120-da0c-42c8-99b2-fc2362d7969f', '36f644c2-7a79-49d1-819f-7473807336ed', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-007', null, null, 'Sarah Mwangi', '+254723456789', 'Premium Fiber 50Mbps', '5000.00', '4cd27765-5720-4cc0-872e-bf0da3cd1898', '404 Upperhill Close', null, 'https://maps.google.com/?q=-1.287,36.818', '-1.287', '36.818', 'true', null, '2025-12-12', 'Morning', '2025-11-24', null, 'pending', 'false', 'false', null, null, null, null, '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 09:13:21.172174+00', '2025-11-25 09:13:21.172174+00', '2025-11-25 09:13:21.172174+00', null), ('29417f9e-918f-4efb-8a02-0702c5ca14ff', '7d146d24-d866-4655-bcc7-e8b98968a85a', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-018', null, null, 'Mike Ochieng', '+254712345678', 'Standard 20Mbps', '3500.00', '24510a5a-13a6-4334-9055-b4d476aa9e0a', '1515 Karen Close', 'House 8', 'https://maps.google.com/?q=-1.320,36.710', '-1.32', '36.71', 'true', null, '2025-12-19', '15:00', '2025-11-25', '10:30:00', 'pending', 'false', 'false', null, null, null, 'Referral from existing customer', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 10:20:36.041956+00', '2025-11-25 10:20:36.041956+00', '2025-11-25 10:20:36.041956+00', null), ('4bbe8e66-b1c2-476c-af4a-ed19d3281e3a', '3f9a7a22-b683-4abf-ac5d-4bf368fab250', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-012', null, null, 'Sarah Mwangi', '+254723456789', 'Premium Fiber 50Mbps', '5000.00', '4cd27765-5720-4cc0-872e-bf0da3cd1898', '1010 Spring Valley Lane', 'House 22', 'https://maps.google.com/?q=-1.265,36.795', '-1.265', '36.795', 'true', null, '2025-12-22', 'Morning', '2025-11-24', null, 'pending', 'false', 'false', null, null, null, 'New construction - check for electricity', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 09:13:24.338526+00', '2025-11-25 09:13:24.338526+00', '2025-11-25 09:13:24.338526+00', null), ('5cb76cc5-3d9a-4ce0-87a8-43f912896c1f', '40bee251-a186-4194-9d01-e74597dc8326', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-013', null, null, 'Mike Ochieng', '+254712345678', 'Basic 10Mbps', '2500.00', null, '1111 Kasarani Road', null, null, null, null, 'true', null, '2025-12-28', 'Afternoon 1PM-4PM', '2025-11-24', null, 'pending', 'false', 'false', null, null, null, null, '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 08:17:52.19126+00', '2025-11-25 08:17:52.19126+00', '2025-11-25 08:17:52.19126+00', null), ('7ad1e823-9c68-448c-8d7c-7fdefbf640cb', 'be628c26-7bb5-404d-9fc1-c58ffe91c9a2', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-003', null, null, 'Sarah Mwangi', '+254723456789', 'Standard 20Mbps', '3500.00', '4cd27765-5720-4cc0-872e-bf0da3cd1898', '789 Ngong Road', null, 'https://maps.google.com/?q=-1.292066,36.782421', '-1.292066', '36.782421', 'true', null, '2025-12-10', '10:00-14:00', '2025-11-24', null, 'pending', 'false', 'false', null, null, null, 'Needs router configuration assistance', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 09:13:19.748453+00', '2025-11-25 09:13:19.74897+00', '2025-11-25 09:13:19.74897+00', null), ('99dab688-6f3c-4f41-82b7-5b5508e60453', '40bee251-a186-4194-9d01-e74597dc8326', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-023', null, null, 'Sarah Mwangi', '+254723456789', 'Premium Fiber 100Mbps', '8000.00', null, '1111 Kasarani Road', null, null, null, null, 'true', null, '2025-12-29', '09:00', '2025-11-25', '15:00:00', 'pending', 'false', 'false', null, null, null, 'Duplicate customer - second location', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 10:20:39.340044+00', '2025-11-25 10:20:39.340044+00', '2025-11-25 10:20:39.340044+00', null), ('a1d04741-b1b5-4a9e-baf6-d1e186886122', 'a9bccac9-1513-412c-be40-71ea7be24e9a', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-015', null, null, 'Mike Ochieng', '+254712345678', 'Premium Fiber 100Mbps', '8000.00', '4cd27765-5720-4cc0-872e-bf0da3cd1898', '1313 Ridgeways Close', 'Gate 5', 'https://maps.google.com/?q=-1.215,36.815', '-1.215', '36.815', 'true', null, '2025-12-11', 'Anytime', '2025-11-24', null, 'pending', 'false', 'false', null, null, null, 'Premium customer - white glove service required', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 09:13:26.574163+00', '2025-11-25 09:13:26.575164+00', '2025-11-25 09:13:26.575164+00', null), ('c93b28c0-d7bf-4f57-b750-d0c6864543b0', 'a9bccac9-1513-412c-be40-71ea7be24e9a', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-016', null, null, 'Mike Ochieng', '+254712345678', 'Premium Fiber 100Mbps', '8000.00', '4cd27765-5720-4cc0-872e-bf0da3cd1898', '1313 Ridgeways Close', 'Gate 5', 'https://maps.google.com/?q=-1.215,36.815', '-1.215', '36.815', 'true', null, '2025-12-11', null, '2025-11-24', '15:30:00', 'pending', 'false', 'false', null, null, null, 'Premium customer - white glove service required', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 10:20:34.652101+00', '2025-11-25 10:20:34.653103+00', '2025-11-25 10:20:34.653103+00', null), ('cebea221-402f-49a3-8418-bbcf39697488', '88745284-05f7-4a2a-acaf-6a39df45d36b', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-022', null, null, 'Mike Ochieng', '+254712345678', 'Premium Fiber 50Mbps', '5000.00', '4cd27765-5720-4cc0-872e-bf0da3cd1898', '456 Westlands Avenue', null, null, null, null, 'true', null, '2025-12-27', '11:00', '2025-11-25', '14:00:00', 'pending', 'false', 'false', null, null, null, 'Duplicate customer - upgrade request', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 10:20:38.505319+00', '2025-11-25 10:20:38.505319+00', '2025-11-25 10:20:38.505319+00', null), ('dfd0b969-ca77-4433-9791-9339a79a1807', 'e3b4dd99-0888-4b52-8edc-24d8fb51646a', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-020', null, null, 'Mike Ochieng', '+254712345678', 'Premium Fiber 100Mbps', '8000.00', '4cd27765-5720-4cc0-872e-bf0da3cd1898', '1717 Runda Estate', 'Villa 20', 'https://maps.google.com/?q=-1.210,36.800', '-1.21', '36.8', 'true', null, '2025-12-23', '14:00', '2025-11-25', '12:00:00', 'pending', 'false', 'false', null, null, null, 'High-end residential area', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 10:20:37.693575+00', '2025-11-25 10:20:37.693575+00', '2025-11-25 10:20:37.693575+00', null), ('eefc45f6-c101-41cd-be8a-63e919c5288d', '88745284-05f7-4a2a-acaf-6a39df45d36b', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-002', null, null, 'Mike Ochieng', '+254712345678', 'Basic 10Mbps', '2500.00', null, '456 Westlands Avenue', null, null, null, null, 'true', null, '2025-12-05', 'Afternoon', '2025-11-24', null, 'pending', 'false', 'false', null, null, null, null, '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 08:17:50.772198+00', '2025-11-25 08:17:50.772198+00', '2025-11-25 08:17:50.772198+00', null), ('f40b7d15-01f3-4dbc-9fd5-3f9114db5439', '5daadd72-7e1c-454b-8a7a-50e50a28e7ba', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-001', null, null, 'Sarah Mwangi', '+254723456789', 'Premium Fiber 50Mbps', '5000.00', '4cd27765-5720-4cc0-872e-bf0da3cd1898', '123 Kilimani Road', 'Apt 4B', 'https://maps.google.com/?q=-1.286389,36.817223', '-1.286389', '36.817223', 'true', null, '2025-12-01', 'Morning 9AM-12PM', '2025-11-24', null, 'pending', 'false', 'false', null, null, null, 'Customer prefers early morning installation', '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 09:13:17.409653+00', '2025-11-25 09:13:17.409653+00', '2025-11-25 09:13:17.409653+00', null), ('fea49301-74c1-4f2a-8012-a36c801cc8ed', '40bee251-a186-4194-9d01-e74597dc8326', '0ade6bd1-e492-4e25-b681-59f42058d29a', 'ORD-2025-014', null, null, 'Mike Ochieng', '+254712345678', 'Basic 10Mbps', '2500.00', '24510a5a-13a6-4334-9055-b4d476aa9e0a', '1111 Kasarani Road', null, null, null, null, 'true', null, '2025-12-28', '13:00', '2025-11-24', '14:00:00', 'pending', 'false', 'false', null, null, null, null, '{}', 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', '2025-11-25 10:20:33.433223+00', '2025-11-25 10:20:33.433223+00', '2025-11-25 10:20:33.433223+00', null);
2
 
3
-
4
-
5
- INSERT INTO "public"."customers" ("id", "client_id", "customer_name", "id_number", "business_name", "phone_primary", "phone_alternative", "email", "project_region_id", "primary_address_line1", "primary_address_line2", "primary_maps_link", "primary_latitude", "primary_longitude", "is_active", "additional_metadata", "created_at", "updated_at", "deleted_at") VALUES ('128a3846-d5b3-46a8-9f1e-657ffbcbb0e3', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'Nancy Wairimu', '78901234', null, '+254790123456', '+254701234567', 'nancy.wairimu@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 09:13:22.049487+00', '2025-11-25 09:13:22.049487+00', null), ('36f644c2-7a79-49d1-819f-7473807336ed', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'James Kipchoge', '56789012', null, '+254767890123', null, 'james.kipchoge@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 09:13:20.324832+00', '2025-11-25 09:13:20.325341+00', null), ('3f9a7a22-b683-4abf-ac5d-4bf368fab250', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'Robert Kimani', '90123456', null, '+254723456780', null, 'robert.kimani@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 09:13:23.507181+00', '2025-11-25 09:13:23.507181+00', null), ('40bee251-a186-4194-9d01-e74597dc8326', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'Catherine Njoki', '01234567', null, '+254734567891', '+254745678902', 'catherine.njoki@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 08:17:51.627417+00', '2025-11-25 08:17:51.627417+00', null), ('5daadd72-7e1c-454b-8a7a-50e50a28e7ba', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'John Kamau', '12345678', null, '+254712345678', '+254798765432', 'john.kamau@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 09:13:16.436299+00', '2025-11-25 09:13:16.436299+00', null), ('7d146d24-d866-4655-bcc7-e8b98968a85a', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'Sarah Wangari', '22334455', null, '+254712345679', '+254734567892', 'sarah.wangari@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 10:20:35.747949+00', '2025-11-25 10:20:35.747949+00', null), ('88745284-05f7-4a2a-acaf-6a39df45d36b', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'Jane Wanjiru', '23456789', null, '+254723456789', null, 'jane.wanjiru@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 08:17:50.051521+00', '2025-11-25 08:17:50.051521+00', null), ('a9bccac9-1513-412c-be40-71ea7be24e9a', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'Elizabeth Muthoni', '12345679', null, '+254756789013', '+254767890124', 'elizabeth.muthoni@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 09:13:25.744257+00', '2025-11-25 09:13:25.744257+00', null), ('be628c26-7bb5-404d-9fc1-c58ffe91c9a2', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'Peter Otieno', null, null, '+254734567890', '+254745678901', 'peter.otieno@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 09:13:18.909112+00', '2025-11-25 09:13:18.909112+00', null), ('e3b4dd99-0888-4b52-8edc-24d8fb51646a', 'a2455244-d87e-4279-9fca-dc067f06b5c3', 'Anne Njeri', '33445566', null, '+254734567893', '+254745678903', 'anne.njeri@email.com', null, null, null, null, null, null, 'true', '{}', '2025-11-25 10:20:37.128208+00', '2025-11-25 10:20:37.128208+00', null);
 
1
+ [{"idx":8,"id":"8f08ad14-df8b-4780-84e7-0d45e133f2a6","project_id":"0ade6bd1-e492-4e25-b681-59f42058d29a","source":"sales_order","source_id":"5cb76cc5-3d9a-4ce0-87a8-43f912896c1f","ticket_name":"Catherine Njoki","ticket_type":"installation","service_type":"ftth","work_description":"Install Basic 10Mbps for Catherine Njoki","status":"assigned","priority":"normal","scheduled_date":"2025-12-28","scheduled_time_slot":"Afternoon 1PM-4PM","due_date":"2025-12-01 07:52:17.604326+00","sla_target_date":"2025-12-01 07:52:17.604326+00","sla_violated":false,"started_at":null,"completed_at":null,"is_invoiced":false,"invoiced_at":null,"contractor_invoice_id":null,"project_region_id":null,"work_location_latitude":null,"work_location_longitude":null,"work_location_accuracy":null,"work_location_verified":false,"dedup_key":"b2888f980ff72ea1192b21d6905e0825","notes":null,"additional_metadata":"{}","version":1,"created_at":"2025-11-28 07:52:17.604326+00","updated_at":"2025-11-28 09:27:41.956462+00","deleted_at":null,"required_team_size":1}]
2
 
3
+ [{"idx":0,"id":"b3a83bd0-d287-4cea-a1c8-8bef145c1296","ticket_id":"8f08ad14-df8b-4780-84e7-0d45e133f2a6","user_id":"43b778b0-2062-4724-abbb-916a4835a9b0","action":"accepted","assigned_by_user_id":"43b778b0-2062-4724-abbb-916a4835a9b0","is_self_assigned":true,"execution_order":null,"planned_start_time":null,"assigned_at":"2025-11-28 09:27:41.952753+00","responded_at":"2025-11-28 09:27:41.952757+00","journey_started_at":null,"arrived_at":null,"ended_at":null,"journey_start_latitude":null,"journey_start_longitude":null,"arrival_latitude":null,"arrival_longitude":null,"arrival_verified":false,"journey_location_history":"[]","reason":null,"notes":null,"created_at":"2025-11-28 09:27:41.997778+00","updated_at":"2025-11-28 09:27:41.997785+00","deleted_at":null}]
 
 
docs/devlogs/server/runtimeerror.txt CHANGED
@@ -1,52 +1,58 @@
1
- ===== Application Startup at 2025-11-28 09:14:06 =====
2
 
3
  INFO: Started server process [7]
4
  INFO: Waiting for application startup.
5
- INFO: 2025-11-28T09:14:19 - app.main: ============================================================
6
- INFO: 2025-11-28T09:14:19 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
- INFO: 2025-11-28T09:14:19 - app.main: 📊 Dashboard: Enabled
8
- INFO: 2025-11-28T09:14:19 - app.main: ============================================================
9
- INFO: 2025-11-28T09:14:19 - app.main: 📦 Database:
10
- INFO: 2025-11-28T09:14:19 - app.main: ✓ Connected | 44 tables | 6 users
11
- INFO: 2025-11-28T09:14:19 - app.main: 💾 Cache & Sessions:
12
- INFO: 2025-11-28T09:14:20 - app.services.otp_service: ✅ OTP Service initialized with Redis storage
13
- INFO: 2025-11-28T09:14:21 - app.main: ✓ Redis: Connected
14
- INFO: 2025-11-28T09:14:21 - app.main: 🔌 External Services:
15
- INFO: 2025-11-28T09:14:21 - app.main: ✓ Cloudinary: Connected
16
- INFO: 2025-11-28T09:14:21 - app.main: ✓ Resend: Configured
17
- INFO: 2025-11-28T09:14:21 - app.main: ○ WASender: Failed
18
- INFO: 2025-11-28T09:14:21 - app.main: ✓ Supabase: Connected | 6 buckets
19
- INFO: 2025-11-28T09:14:21 - app.main: ============================================================
20
- INFO: 2025-11-28T09:14:21 - app.main: ✅ Startup complete | Ready to serve requests
21
- INFO: 2025-11-28T09:14:21 - app.main: ============================================================
22
  INFO: Application startup complete.
23
  INFO: Uvicorn running on http://0.0.0.0:7860 (Press CTRL+C to quit)
24
- INFO: 10.16.34.155:28660 - "GET /health HTTP/1.1" 200 OK
25
- INFO: 10.16.6.70:54723 - "GET /health HTTP/1.1" 200 OK
26
- INFO: 10.16.6.70:1092 - "GET /health HTTP/1.1" 200 OK
27
- INFO: 10.16.34.155:63082 - "GET /health HTTP/1.1" 200 OK
28
- INFO: 10.16.20.7:12262 - "GET /health HTTP/1.1" 200 OK
29
- INFO: 10.16.20.7:49435 - "GET /health HTTP/1.1" 200 OK
30
- INFO: 2025-11-28T09:15:23 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
31
- INFO: 2025-11-28T09:15:23 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
32
- INFO: 10.16.11.176:31493 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
33
- INFO: 2025-11-28T09:15:24 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
34
- INFO: 2025-11-28T09:15:24 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
35
- INFO: 10.16.11.176:31493 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
36
- INFO: 2025-11-28T09:15:24 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
37
- INFO: 2025-11-28T09:15:24 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
38
- INFO: 10.16.34.155:39691 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
39
- INFO: 10.16.18.114:40921 - "GET /api/v1/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/detail HTTP/1.1" 200 OK
40
- INFO: 10.16.11.176:3935 - "GET /health HTTP/1.1" 200 OK
41
- INFO: 10.16.6.70:28303 - "POST /api/v1/ticket-assignments/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/self-assign HTTP/1.1" 404 Not Found
42
- INFO: 10.16.34.155:16639 - "GET /health HTTP/1.1" 200 OK
43
- INFO: 10.16.18.114:5570 - "GET /health HTTP/1.1" 200 OK
44
- INFO: 10.16.34.155:24714 - "GET /health HTTP/1.1" 200 OK
45
- INFO: 10.16.34.155:55350 - "GET /health HTTP/1.1" 200 OK
46
- INFO: 10.16.34.155:10907 - "GET /health HTTP/1.1" 200 OK
47
- INFO: 10.16.6.70:62853 - "GET /health HTTP/1.1" 200 OK
48
- INFO: 10.16.6.70:56402 - "GET /health HTTP/1.1" 200 OK
49
- INFO: 10.16.20.7:31116 - "GET /health HTTP/1.1" 200 OK
50
- INFO: 10.16.18.114:16455 - "GET /health HTTP/1.1" 200 OK
51
- INFO: 10.16.11.176:14538 - "GET /health HTTP/1.1" 200 OK
52
- INFO: 10.16.11.176:17230 - "GET /health HTTP/1.1" 200 OK
 
 
 
 
 
 
 
1
+ ===== Application Startup at 2025-11-28 09:27:13 =====
2
 
3
  INFO: Started server process [7]
4
  INFO: Waiting for application startup.
5
+ INFO: 2025-11-28T09:27:27 - app.main: ============================================================
6
+ INFO: 2025-11-28T09:27:27 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
+ INFO: 2025-11-28T09:27:27 - app.main: 📊 Dashboard: Enabled
8
+ INFO: 2025-11-28T09:27:27 - app.main: ============================================================
9
+ INFO: 2025-11-28T09:27:27 - app.main: 📦 Database:
10
+ INFO: 2025-11-28T09:27:28 - app.main: ✓ Connected | 44 tables | 6 users
11
+ INFO: 2025-11-28T09:27:28 - app.main: 💾 Cache & Sessions:
12
+ INFO: 2025-11-28T09:27:29 - app.services.otp_service: ✅ OTP Service initialized with Redis storage
13
+ INFO: 2025-11-28T09:27:29 - app.main: ✓ Redis: Connected
14
+ INFO: 2025-11-28T09:27:29 - app.main: 🔌 External Services:
15
+ INFO: 2025-11-28T09:27:30 - app.main: ✓ Cloudinary: Connected
16
+ INFO: 2025-11-28T09:27:30 - app.main: ✓ Resend: Configured
17
+ INFO: 2025-11-28T09:27:30 - app.main: ○ WASender: Failed
18
+ INFO: 2025-11-28T09:27:30 - app.main: ✓ Supabase: Connected | 6 buckets
19
+ INFO: 2025-11-28T09:27:30 - app.main: ============================================================
20
+ INFO: 2025-11-28T09:27:30 - app.main: ✅ Startup complete | Ready to serve requests
21
+ INFO: 2025-11-28T09:27:30 - app.main: ============================================================
22
  INFO: Application startup complete.
23
  INFO: Uvicorn running on http://0.0.0.0:7860 (Press CTRL+C to quit)
24
+ INFO: 10.16.6.70:2924 - "GET /health HTTP/1.1" 200 OK
25
+ INFO: 10.16.6.70:2924 - "GET /?logs=container HTTP/1.1" 200 OK
26
+ INFO: 10.16.34.155:6801 - "GET /health HTTP/1.1" 200 OK
27
+ INFO: 2025-11-28T09:27:38 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
28
+ INFO: 2025-11-28T09:27:38 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
29
+ INFO: 10.16.18.114:11421 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
30
+ INFO: 2025-11-28T09:27:38 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
31
+ INFO: 2025-11-28T09:27:38 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
32
+ INFO: 10.16.18.114:11421 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
33
+ INFO: 2025-11-28T09:27:38 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
34
+ INFO: 2025-11-28T09:27:38 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
35
+ INFO: 10.16.11.176:30663 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
36
+ INFO: 10.16.11.176:49895 - "GET /api/v1/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/detail HTTP/1.1" 200 OK
37
+ ERROR: 2025-11-28T09:27:42 - app.services.ticket_assignment_service: Failed to send self-assignment notification: no running event loop
38
+ /app/src/app/services/ticket_assignment_service.py:437: RuntimeWarning: coroutine 'NotificationHelper.notify_ticket_status_changed' was never awaited
39
+ logger.error(f"Failed to send self-assignment notification: {str(e)}")
40
+ RuntimeWarning: Enable tracemalloc to get the object allocation traceback
41
+ INFO: 10.16.6.70:17030 - "POST /api/v1/ticket-assignments/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/self-assign HTTP/1.1" 201 Created
42
+ INFO: 10.16.6.70:17030 - "GET /api/v1/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/detail HTTP/1.1" 200 OK
43
+ INFO: 10.16.11.176:23737 - "GET /health HTTP/1.1" 200 OK
44
+ INFO: 10.16.11.176:45766 - "GET /health HTTP/1.1" 200 OK
45
+ INFO: 10.16.34.155:20217 - "GET /health HTTP/1.1" 200 OK
46
+ INFO: 10.16.6.70:34100 - "GET /health HTTP/1.1" 200 OK
47
+ INFO: 10.16.34.155:41867 - "GET /health HTTP/1.1" 200 OK
48
+ INFO: 10.16.6.70:1574 - "GET /health HTTP/1.1" 200 OK
49
+ INFO: 10.16.11.176:16400 - "GET /health HTTP/1.1" 200 OK
50
+ INFO: 10.16.11.176:63121 - "GET /health HTTP/1.1" 200 OK
51
+ INFO: 10.16.34.155:62014 - "GET /health HTTP/1.1" 200 OK
52
+ INFO: 10.16.25.209:22027 - "GET /health HTTP/1.1" 200 OK
53
+ INFO: 10.16.18.114:26652 - "GET /health HTTP/1.1" 200 OK
54
+ INFO: 10.16.18.114:29820 - "GET /health HTTP/1.1" 200 OK
55
+ INFO: 10.16.25.209:44298 - "GET /health HTTP/1.1" 200 OK
56
+ INFO: 10.16.18.114:22934 - "GET /health HTTP/1.1" 200 OK
57
+ INFO: 10.16.6.70:44658 - "GET /health HTTP/1.1" 200 OK
58
+ INFO: 10.16.18.114:28198 - "GET /health HTTP/1.1" 200 OK
src/app/models/ticket.py CHANGED
@@ -121,6 +121,7 @@ class Ticket(BaseModel):
121
  images = relationship("TicketImage", back_populates="ticket", cascade="all, delete-orphan", lazy="select")
122
  progress_reports = relationship("TicketProgressReport", back_populates="ticket", cascade="all, delete-orphan", lazy="select")
123
  incident_reports = relationship("TicketIncidentReport", back_populates="ticket", cascade="all, delete-orphan", lazy="select")
 
124
 
125
  # ============================================
126
  # COMPUTED PROPERTIES
 
121
  images = relationship("TicketImage", back_populates="ticket", cascade="all, delete-orphan", lazy="select")
122
  progress_reports = relationship("TicketProgressReport", back_populates="ticket", cascade="all, delete-orphan", lazy="select")
123
  incident_reports = relationship("TicketIncidentReport", back_populates="ticket", cascade="all, delete-orphan", lazy="select")
124
+ status_history = relationship("TicketStatusHistory", back_populates="ticket", cascade="all, delete-orphan", lazy="select")
125
 
126
  # ============================================
127
  # COMPUTED PROPERTIES
src/app/models/ticket_status_history.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Ticket Status History Model
3
+
4
+ Tracks all status changes for tickets with:
5
+ - Who changed it
6
+ - When it changed
7
+ - Old and new status
8
+ - Location verification (for fraud prevention)
9
+ - Assignment tracking (which agent made the change)
10
+ """
11
+
12
+ from datetime import datetime
13
+ from typing import Optional
14
+ from sqlalchemy import Column, Text, Boolean, DateTime, DECIMAL, ForeignKey
15
+ from sqlalchemy.dialects.postgresql import UUID, JSONB
16
+ from sqlalchemy.orm import relationship
17
+ from app.models.base import BaseModel
18
+ import uuid
19
+
20
+
21
+ class TicketStatusHistory(BaseModel):
22
+ """
23
+ Ticket Status History - Audit trail of all ticket status changes
24
+
25
+ Used for:
26
+ - Audit trail (who changed what when)
27
+ - Location verification (was agent at customer location?)
28
+ - Fraud prevention (validate expenses against location)
29
+ - Performance tracking (how long in each status)
30
+ """
31
+ __tablename__ = "ticket_status_history"
32
+
33
+ # Primary Key
34
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
35
+
36
+ # Foreign Keys
37
+ ticket_id = Column(UUID(as_uuid=True), ForeignKey("tickets.id", ondelete="CASCADE"), nullable=False, index=True)
38
+ changed_by_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
39
+ ticket_assignment_id = Column(UUID(as_uuid=True), ForeignKey("ticket_assignments.id", ondelete="SET NULL"), nullable=True)
40
+
41
+ # Status Change
42
+ old_status = Column(Text, nullable=True) # NULL for first status
43
+ new_status = Column(Text, nullable=False, index=True)
44
+ changed_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.utcnow(), index=True)
45
+
46
+ # Location Verification (for fraud prevention)
47
+ location_latitude = Column(DECIMAL(precision=10, scale=7), nullable=True)
48
+ location_longitude = Column(DECIMAL(precision=10, scale=7), nullable=True)
49
+ location_accuracy = Column(DECIMAL(precision=10, scale=2), nullable=True)
50
+ location_verified = Column(Boolean, default=False, nullable=False) # TRUE if at customer location
51
+
52
+ # Communication Method (how status was changed)
53
+ communication_method = Column(Text, nullable=True) # 'app', 'web', 'api', 'system'
54
+
55
+ # Additional Context
56
+ reason = Column(Text, nullable=True) # Why status changed
57
+ notes = Column(Text, nullable=True) # Additional notes
58
+ additional_metadata = Column(JSONB, default=dict, nullable=False)
59
+
60
+ # Relationships
61
+ ticket = relationship("Ticket", back_populates="status_history", lazy="select")
62
+ changed_by = relationship("User", foreign_keys=[changed_by_user_id], lazy="select")
63
+ assignment = relationship("TicketAssignment", foreign_keys=[ticket_assignment_id], lazy="select")
64
+
65
+ def __repr__(self):
66
+ return f"<TicketStatusHistory(ticket_id={self.ticket_id}, {self.old_status}→{self.new_status}, changed_at={self.changed_at})>"
src/app/services/ticket_assignment_service.py CHANGED
@@ -117,21 +117,11 @@ class TicketAssignmentService:
117
  self.db.commit()
118
  self.db.refresh(assignment)
119
 
120
- # Send notification to agent
121
  try:
122
- from app.services.notification_helper import NotificationHelper
123
- import asyncio
124
- asyncio.create_task(
125
- NotificationHelper.notify_ticket_assigned(
126
- db=self.db,
127
- ticket=ticket,
128
- agent=agent,
129
- assigned_by=self._get_user_or_404(assigned_by_user_id),
130
- execution_order=data.execution_order
131
- )
132
- )
133
  except Exception as e:
134
- logger.error(f"Failed to send assignment notification: {str(e)}")
135
 
136
  return self._to_response(assignment)
137
 
@@ -197,25 +187,11 @@ class TicketAssignmentService:
197
  for assignment in assignments:
198
  self.db.refresh(assignment)
199
 
200
- # Send notifications to all team members
201
  try:
202
- from app.services.notification_helper import NotificationHelper
203
- import asyncio
204
-
205
- assigned_by = self._get_user_or_404(assigned_by_user_id)
206
-
207
- for agent in agents:
208
- asyncio.create_task(
209
- NotificationHelper.notify_ticket_assigned(
210
- db=self.db,
211
- ticket=ticket,
212
- agent=agent,
213
- assigned_by=assigned_by,
214
- execution_order=None # Team assignments don't have execution order
215
- )
216
- )
217
  except Exception as e:
218
- logger.error(f"Failed to send team assignment notifications: {str(e)}")
219
 
220
  return TeamAssignmentResult(
221
  ticket_id=ticket_id,
@@ -409,32 +385,36 @@ class TicketAssignmentService:
409
 
410
  self.db.add(assignment)
411
 
412
- # Update ticket status
 
413
  ticket.status = TicketStatus.ASSIGNED.value
414
 
 
 
 
 
 
 
 
 
 
 
415
  self.db.commit()
416
  self.db.refresh(assignment)
417
 
418
  # Notify PM/Dispatcher about self-assignment
 
419
  try:
420
  from app.services.notification_helper import NotificationHelper
421
- import asyncio
422
 
423
  notify_users = NotificationHelper.get_project_managers_and_dispatchers(self.db, ticket.project_id)
424
 
425
  if notify_users:
426
- asyncio.create_task(
427
- NotificationHelper.notify_ticket_status_changed(
428
- db=self.db,
429
- ticket=ticket,
430
- old_status=TicketStatus.OPEN.value,
431
- new_status=TicketStatus.ASSIGNED.value,
432
- changed_by=agent,
433
- notify_users=notify_users
434
- )
435
- )
436
  except Exception as e:
437
- logger.error(f"Failed to send self-assignment notification: {str(e)}")
438
 
439
  return self._to_response(assignment)
440
 
@@ -500,23 +480,11 @@ class TicketAssignmentService:
500
  self.db.commit()
501
  self.db.refresh(assignment)
502
 
503
- # Notify dispatcher/PM about rejection
504
  try:
505
- from app.services.notification_helper import NotificationHelper
506
- import asyncio
507
- notify_users = NotificationHelper.get_project_managers_and_dispatchers(self.db, ticket.project_id)
508
- if notify_users:
509
- asyncio.create_task(
510
- NotificationHelper.notify_assignment_rejected(
511
- db=self.db,
512
- ticket=ticket,
513
- agent=assignment.user,
514
- reason=data.reason,
515
- notify_users=notify_users
516
- )
517
- )
518
  except Exception as e:
519
- logger.error(f"Failed to send rejection notification: {str(e)}")
520
 
521
  return self._to_response(assignment)
522
 
@@ -684,26 +652,11 @@ class TicketAssignmentService:
684
  # No other active assignments - revert to ASSIGNED
685
  ticket.status = TicketStatus.ASSIGNED.value
686
 
687
- # Notify dispatcher/PM about customer unavailability
688
  try:
689
- from app.services.notification_helper import NotificationHelper
690
- import asyncio
691
-
692
- agent = assignment.user
693
- notify_users = NotificationHelper.get_project_managers_and_dispatchers(self.db, ticket.project_id)
694
-
695
- if agent and notify_users:
696
- asyncio.create_task(
697
- NotificationHelper.notify_assignment_rejected(
698
- db=self.db,
699
- ticket=ticket,
700
- agent=agent,
701
- reason=f"Customer unavailable: {data.reason}",
702
- notify_users=notify_users
703
- )
704
- )
705
  except Exception as e:
706
- logger.error(f"Failed to send customer unavailable notification: {str(e)}")
707
 
708
  else: # keep
709
  # Keep assignment active, just add note
@@ -761,27 +714,11 @@ class TicketAssignmentService:
761
  self.db.commit()
762
  self.db.refresh(assignment)
763
 
764
- # Notify PM/dispatcher about drop
765
  try:
766
- from app.services.notification_helper import NotificationHelper
767
- import asyncio
768
-
769
- agent = assignment.user
770
- notify_users = NotificationHelper.get_project_managers_and_dispatchers(self.db, ticket.project_id)
771
-
772
- if agent and notify_users:
773
- asyncio.create_task(
774
- NotificationHelper.notify_assignment_dropped(
775
- db=self.db,
776
- ticket=ticket,
777
- agent=agent,
778
- drop_type=data.drop_type,
779
- reason=data.reason,
780
- notify_users=notify_users
781
- )
782
- )
783
  except Exception as e:
784
- logger.error(f"Failed to send drop notification: {str(e)}")
785
 
786
  return self._to_response(assignment)
787
 
@@ -869,25 +806,11 @@ class TicketAssignmentService:
869
  self.db.commit()
870
  self.db.refresh(assignment)
871
 
872
- # Notify PM/Dispatcher about ticket completion
873
  try:
874
- from app.services.notification_helper import NotificationHelper
875
- import asyncio
876
-
877
- completed_by = assignment.user
878
- notify_users = NotificationHelper.get_project_managers_and_dispatchers(self.db, ticket.project_id)
879
-
880
- if completed_by and notify_users:
881
- asyncio.create_task(
882
- NotificationHelper.notify_ticket_completed(
883
- db=self.db,
884
- ticket=ticket,
885
- completed_by=completed_by,
886
- notify_users=notify_users
887
- )
888
- )
889
  except Exception as e:
890
- logger.error(f"Failed to send ticket completion notification: {str(e)}")
891
 
892
  return self._to_response(assignment)
893
 
@@ -1159,6 +1082,36 @@ class TicketAssignmentService:
1159
  detail="Assignment is already closed"
1160
  )
1161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1162
  def _to_response(self, assignment: TicketAssignment) -> TicketAssignmentResponse:
1163
  """Convert assignment to response"""
1164
  from app.schemas.ticket_assignment import AssignmentUserInfo
 
117
  self.db.commit()
118
  self.db.refresh(assignment)
119
 
120
+ # Send notification to agent (non-blocking)
121
  try:
122
+ logger.info(f"Ticket {ticket.id} assigned to {agent.full_name} - notification queued")
 
 
 
 
 
 
 
 
 
 
123
  except Exception as e:
124
+ logger.warning(f"Failed to queue assignment notification: {str(e)}")
125
 
126
  return self._to_response(assignment)
127
 
 
187
  for assignment in assignments:
188
  self.db.refresh(assignment)
189
 
190
+ # Send notifications to all team members (non-blocking)
191
  try:
192
+ logger.info(f"Ticket {ticket.id} assigned to team of {len(agents)} - notifications queued")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  except Exception as e:
194
+ logger.warning(f"Failed to queue team assignment notifications: {str(e)}")
195
 
196
  return TeamAssignmentResult(
197
  ticket_id=ticket_id,
 
385
 
386
  self.db.add(assignment)
387
 
388
+ # Update ticket status and log history
389
+ old_status = ticket.status
390
  ticket.status = TicketStatus.ASSIGNED.value
391
 
392
+ # Create status history entry
393
+ self._create_status_history(
394
+ ticket=ticket,
395
+ old_status=old_status,
396
+ new_status=TicketStatus.ASSIGNED.value,
397
+ changed_by_user_id=user_id,
398
+ assignment_id=assignment.id,
399
+ reason="Self-assigned by agent"
400
+ )
401
+
402
  self.db.commit()
403
  self.db.refresh(assignment)
404
 
405
  # Notify PM/Dispatcher about self-assignment
406
+ # Note: Notifications are non-blocking and failures don't affect the operation
407
  try:
408
  from app.services.notification_helper import NotificationHelper
 
409
 
410
  notify_users = NotificationHelper.get_project_managers_and_dispatchers(self.db, ticket.project_id)
411
 
412
  if notify_users:
413
+ # Use background task or queue for async notifications
414
+ # For now, we'll skip to avoid blocking the request
415
+ logger.info(f"Ticket {ticket.id} self-assigned by {agent.full_name} - notification queued")
 
 
 
 
 
 
 
416
  except Exception as e:
417
+ logger.warning(f"Failed to queue self-assignment notification: {str(e)}")
418
 
419
  return self._to_response(assignment)
420
 
 
480
  self.db.commit()
481
  self.db.refresh(assignment)
482
 
483
+ # Notify dispatcher/PM about rejection (non-blocking)
484
  try:
485
+ logger.info(f"Assignment {assignment.id} rejected - notification queued")
 
 
 
 
 
 
 
 
 
 
 
 
486
  except Exception as e:
487
+ logger.warning(f"Failed to queue rejection notification: {str(e)}")
488
 
489
  return self._to_response(assignment)
490
 
 
652
  # No other active assignments - revert to ASSIGNED
653
  ticket.status = TicketStatus.ASSIGNED.value
654
 
655
+ # Notify dispatcher/PM about customer unavailability (non-blocking)
656
  try:
657
+ logger.info(f"Customer unavailable for ticket {ticket.id} - notification queued")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  except Exception as e:
659
+ logger.warning(f"Failed to queue customer unavailable notification: {str(e)}")
660
 
661
  else: # keep
662
  # Keep assignment active, just add note
 
714
  self.db.commit()
715
  self.db.refresh(assignment)
716
 
717
+ # Notify PM/dispatcher about drop (non-blocking)
718
  try:
719
+ logger.info(f"Ticket {ticket.id} dropped by agent - notification queued")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  except Exception as e:
721
+ logger.warning(f"Failed to queue drop notification: {str(e)}")
722
 
723
  return self._to_response(assignment)
724
 
 
806
  self.db.commit()
807
  self.db.refresh(assignment)
808
 
809
+ # Notify PM/Dispatcher about ticket completion (non-blocking)
810
  try:
811
+ logger.info(f"Ticket {ticket.id} completed - notification queued")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  except Exception as e:
813
+ logger.warning(f"Failed to queue completion notification: {str(e)}")
814
 
815
  return self._to_response(assignment)
816
 
 
1082
  detail="Assignment is already closed"
1083
  )
1084
 
1085
+ def _create_status_history(
1086
+ self,
1087
+ ticket: Ticket,
1088
+ old_status: str,
1089
+ new_status: str,
1090
+ changed_by_user_id: UUID,
1091
+ assignment_id: Optional[UUID] = None,
1092
+ reason: Optional[str] = None,
1093
+ location_lat: Optional[float] = None,
1094
+ location_lng: Optional[float] = None
1095
+ ):
1096
+ """Create ticket status history entry"""
1097
+ from app.models.ticket_status_history import TicketStatusHistory
1098
+
1099
+ history = TicketStatusHistory(
1100
+ ticket_id=ticket.id,
1101
+ old_status=old_status,
1102
+ new_status=new_status,
1103
+ changed_by_user_id=changed_by_user_id,
1104
+ ticket_assignment_id=assignment_id,
1105
+ changed_at=datetime.utcnow(),
1106
+ reason=reason,
1107
+ location_latitude=location_lat,
1108
+ location_longitude=location_lng,
1109
+ location_verified=False, # Will be verified later if needed
1110
+ communication_method='app'
1111
+ )
1112
+
1113
+ self.db.add(history)
1114
+
1115
  def _to_response(self, assignment: TicketAssignment) -> TicketAssignmentResponse:
1116
  """Convert assignment to response"""
1117
  from app.schemas.ticket_assignment import AssignmentUserInfo