Spaces:
Sleeping
Sleeping
fix(ticket-assignment): resolve SQLAlchemy mapper initialization errors
Browse files- Fix circular import issue by reordering model imports in ticket assignment schema
- Update ticket assignment service to properly initialize TicketStatusHistory relationship
- Correct relationship definition in Ticket model to reference TicketStatusHistory after class definition
- Add explicit model registration order in API endpoints to ensure proper mapper initialization
- Update devlogs with successful ticket assignment response and corrected startup logs
- Resolves "failed to locate a name ('TicketStatusHistory')" mapper initialization errors that prevented authentication and ticket operations
docs/devlogs/browser/response.json
CHANGED
|
@@ -1 +1,43 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "a82a3824-f4f1-4283-a2e3-8c348dbb28ce",
|
| 3 |
+
"ticket_id": "f59b29fc-d0b9-4618-b0d1-889e340da612",
|
| 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-30T10:21:10.085152Z",
|
| 11 |
+
"responded_at": "2025-11-30T10:21:10.085155Z",
|
| 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-30T10:21:10.111327Z",
|
| 42 |
+
"updated_at": "2025-11-30T10:21:10.111330Z"
|
| 43 |
+
}
|
docs/devlogs/server/runtimeerror.txt
CHANGED
|
@@ -1,72 +1,107 @@
|
|
| 1 |
-
===== Application Startup at 2025-11-30 10:
|
| 2 |
|
| 3 |
INFO: Started server process [7]
|
| 4 |
INFO: Waiting for application startup.
|
| 5 |
-
INFO: 2025-11-30T10:
|
| 6 |
-
INFO: 2025-11-30T10:
|
| 7 |
-
INFO: 2025-11-30T10:
|
| 8 |
-
INFO: 2025-11-30T10:
|
| 9 |
-
INFO: 2025-11-30T10:
|
| 10 |
-
INFO: 2025-11-30T10:
|
| 11 |
-
INFO: 2025-11-30T10:
|
| 12 |
-
INFO: 2025-11-30T10:
|
| 13 |
-
INFO: 2025-11-30T10:
|
| 14 |
-
INFO: 2025-11-30T10:
|
| 15 |
-
INFO: 2025-11-30T10:
|
| 16 |
-
INFO: 2025-11-30T10:
|
| 17 |
-
INFO: 2025-11-30T10:
|
| 18 |
-
INFO: 2025-11-30T10:
|
| 19 |
-
INFO: 2025-11-30T10:
|
| 20 |
-
INFO: 2025-11-30T10:
|
| 21 |
-
INFO: 2025-11-30T10:
|
| 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.
|
| 25 |
-
INFO: 10.16.
|
| 26 |
-
INFO: 10.16.
|
| 27 |
-
INFO: 10.16.
|
| 28 |
-
INFO: 10.16.
|
| 29 |
-
INFO:
|
| 30 |
-
INFO:
|
| 31 |
-
INFO:
|
| 32 |
-
INFO: 10.16.11.176:
|
| 33 |
-
INFO:
|
| 34 |
-
INFO:
|
| 35 |
-
INFO: 10.16.
|
| 36 |
-
|
| 37 |
-
INFO:
|
| 38 |
-
INFO:
|
| 39 |
-
|
| 40 |
-
INFO:
|
| 41 |
-
INFO: 10.16.
|
| 42 |
-
INFO:
|
| 43 |
-
INFO: 2025-11-30T10:
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
INFO:
|
| 47 |
-
INFO: 10.16.34.155:
|
| 48 |
-
INFO: 10.16.6.70:
|
| 49 |
-
INFO: 10.16.6.70:
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
INFO: 10.16.18.114:
|
| 53 |
-
INFO: 10.16.
|
| 54 |
-
INFO:
|
| 55 |
-
INFO:
|
| 56 |
-
INFO: 10.16.
|
| 57 |
-
INFO: 10.16.
|
| 58 |
-
INFO:
|
| 59 |
-
INFO: 10.16.6.70:
|
| 60 |
-
INFO: 10.16.
|
| 61 |
-
INFO: 10.16.
|
| 62 |
-
INFO: 10.16.
|
| 63 |
-
INFO: 10.16.
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
INFO: 10.16.
|
| 67 |
-
INFO:
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
INFO:
|
| 71 |
-
INFO: 10.16.
|
| 72 |
-
INFO:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
===== Application Startup at 2025-11-30 10:20:04 =====
|
| 2 |
|
| 3 |
INFO: Started server process [7]
|
| 4 |
INFO: Waiting for application startup.
|
| 5 |
+
INFO: 2025-11-30T10:20:14 - app.main: ============================================================
|
| 6 |
+
INFO: 2025-11-30T10:20:14 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
|
| 7 |
+
INFO: 2025-11-30T10:20:14 - app.main: 📊 Dashboard: Enabled
|
| 8 |
+
INFO: 2025-11-30T10:20:14 - app.main: ============================================================
|
| 9 |
+
INFO: 2025-11-30T10:20:14 - app.main: 📦 Database:
|
| 10 |
+
INFO: 2025-11-30T10:20:14 - app.main: ✓ Connected | 44 tables | 6 users
|
| 11 |
+
INFO: 2025-11-30T10:20:14 - app.main: 💾 Cache & Sessions:
|
| 12 |
+
INFO: 2025-11-30T10:20:15 - app.services.otp_service: ✅ OTP Service initialized with Redis storage
|
| 13 |
+
INFO: 2025-11-30T10:20:16 - app.main: ✓ Redis: Connected
|
| 14 |
+
INFO: 2025-11-30T10:20:16 - app.main: 🔌 External Services:
|
| 15 |
+
INFO: 2025-11-30T10:20:16 - app.main: ✓ Cloudinary: Connected
|
| 16 |
+
INFO: 2025-11-30T10:20:16 - app.main: ✓ Resend: Configured
|
| 17 |
+
INFO: 2025-11-30T10:20:16 - app.main: ○ WASender: Failed
|
| 18 |
+
INFO: 2025-11-30T10:20:16 - app.main: ✓ Supabase: Connected | 6 buckets
|
| 19 |
+
INFO: 2025-11-30T10:20:16 - app.main: ============================================================
|
| 20 |
+
INFO: 2025-11-30T10:20:16 - app.main: ✅ Startup complete | Ready to serve requests
|
| 21 |
+
INFO: 2025-11-30T10:20:16 - 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.11.176:37653 - "GET /health HTTP/1.1" 200 OK
|
| 25 |
+
INFO: 10.16.11.176:37653 - "GET /health HTTP/1.1" 200 OK
|
| 26 |
+
INFO: 10.16.11.176:51654 - "GET /health HTTP/1.1" 200 OK
|
| 27 |
+
INFO: 10.16.6.70:45423 - "GET /health HTTP/1.1" 200 OK
|
| 28 |
+
INFO: 10.16.34.155:12449 - "GET /health HTTP/1.1" 200 OK
|
| 29 |
+
INFO: 2025-11-30T10:20:54 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
|
| 30 |
+
INFO: 2025-11-30T10:20:54 - app.services.audit_service: Audit log created: login on auth by viyisa8151@feralrex.com
|
| 31 |
+
INFO: 2025-11-30T10:20:54 - app.api.v1.auth: User logged in successfully: viyisa8151@feralrex.com
|
| 32 |
+
INFO: 10.16.11.176:11276 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
|
| 33 |
+
INFO: 2025-11-30T10:20:55 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
|
| 34 |
+
INFO: 2025-11-30T10:20:55 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
|
| 35 |
+
INFO: 10.16.11.176:11276 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
|
| 36 |
+
INFO: 2025-11-30T10:20:56 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
|
| 37 |
+
INFO: 2025-11-30T10:20:56 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
|
| 38 |
+
INFO: 10.16.34.155:49018 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
|
| 39 |
+
INFO: 2025-11-30T10:20:56 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
|
| 40 |
+
INFO: 2025-11-30T10:20:56 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
|
| 41 |
+
INFO: 10.16.34.155:45714 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
|
| 42 |
+
INFO: 2025-11-30T10:20:56 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
|
| 43 |
+
INFO: 2025-11-30T10:20:56 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
|
| 44 |
+
INFO: 10.16.6.70:20389 - "GET /api/v1/analytics/user/overview?limit=50 HTTP/1.1" 200 OK
|
| 45 |
+
INFO: 2025-11-30T10:20:57 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
|
| 46 |
+
INFO: 2025-11-30T10:20:57 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
|
| 47 |
+
INFO: 10.16.34.155:45714 - "GET /api/v1/projects?page=1&per_page=100&status=active HTTP/1.1" 200 OK
|
| 48 |
+
INFO: 10.16.6.70:7828 - "GET /health HTTP/1.1" 200 OK
|
| 49 |
+
INFO: 10.16.6.70:7828 - "GET /api/v1/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/detail HTTP/1.1" 200 OK
|
| 50 |
+
/app/src/app/services/ticket_service.py:694: SAWarning: Coercing Subquery object into a select() for use in IN(); please pass a select() construct explicitly
|
| 51 |
+
query = query.filter(Ticket.project_id.in_(team_projects))
|
| 52 |
+
INFO: 10.16.18.114:24662 - "GET /api/v1/tickets?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&skip=0&limit=50 HTTP/1.1" 200 OK
|
| 53 |
+
INFO: 10.16.11.176:10506 - "GET /api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a HTTP/1.1" 200 OK
|
| 54 |
+
INFO: 2025-11-30T10:21:05 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
|
| 55 |
+
INFO: 2025-11-30T10:21:05 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
|
| 56 |
+
INFO: 10.16.11.176:10506 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions HTTP/1.1" 200 OK
|
| 57 |
+
INFO: 10.16.34.155:55245 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/detail HTTP/1.1" 200 OK
|
| 58 |
+
INFO: 2025-11-30T10:21:10 - app.services.ticket_assignment_service: Ticket f59b29fc-d0b9-4618-b0d1-889e340da612 self-assigned by Viyisa Sasa - notification queued
|
| 59 |
+
INFO: 10.16.6.70:14190 - "POST /api/v1/ticket-assignments/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/self-assign HTTP/1.1" 201 Created
|
| 60 |
+
INFO: 10.16.6.70:14190 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/detail HTTP/1.1" 200 OK
|
| 61 |
+
INFO: 10.16.25.209:61895 - "GET /health HTTP/1.1" 200 OK
|
| 62 |
+
INFO: 10.16.25.209:21306 - "GET /health HTTP/1.1" 200 OK
|
| 63 |
+
INFO: 10.16.6.70:54905 - "GET /health HTTP/1.1" 200 OK
|
| 64 |
+
ERROR: 2025-11-30T10:22:11 - app.core.supabase_auth: Session refresh error: Invalid Refresh Token: Already Used
|
| 65 |
+
ERROR: 2025-11-30T10:22:11 - app.api.v1.auth: ❌ Token refresh error: Invalid Refresh Token: Already Used
|
| 66 |
+
INFO: 10.16.6.70:54905 - "POST /api/v1/auth/refresh-token HTTP/1.1" 401 Unauthorized
|
| 67 |
+
INFO: 10.16.25.209:11050 - "GET /health HTTP/1.1" 200 OK
|
| 68 |
+
INFO: 2025-11-30T10:22:19 - app.core.supabase_auth: User signed in successfully: nadina73@nembors.com
|
| 69 |
+
INFO: 2025-11-30T10:22:19 - app.services.audit_service: Audit log created: login on auth by nadina73@nembors.com
|
| 70 |
+
INFO: 2025-11-30T10:22:19 - app.api.v1.auth: User logged in successfully: nadina73@nembors.com
|
| 71 |
+
INFO: 10.16.34.155:36441 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
|
| 72 |
+
INFO: 2025-11-30T10:22:20 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
|
| 73 |
+
INFO: 2025-11-30T10:22:20 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
|
| 74 |
+
INFO: 10.16.25.209:52587 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
|
| 75 |
+
INFO: 2025-11-30T10:22:21 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
|
| 76 |
+
INFO: 2025-11-30T10:22:21 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
|
| 77 |
+
INFO: 10.16.34.155:36441 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
|
| 78 |
+
INFO: 2025-11-30T10:22:21 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
|
| 79 |
+
INFO: 2025-11-30T10:22:21 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
|
| 80 |
+
INFO: 2025-11-30T10:22:21 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
|
| 81 |
+
INFO: 2025-11-30T10:22:21 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
|
| 82 |
+
INFO: 10.16.18.114:63198 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
|
| 83 |
+
INFO: 10.16.6.70:46136 - "GET /api/v1/analytics/user/overview HTTP/1.1" 200 OK
|
| 84 |
+
INFO: 2025-11-30T10:22:22 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
|
| 85 |
+
INFO: 2025-11-30T10:22:22 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
|
| 86 |
+
INFO: 10.16.34.155:36441 - "GET /api/v1/projects?page=1&per_page=100 HTTP/1.1" 200 OK
|
| 87 |
+
INFO: 2025-11-30T10:22:28 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
|
| 88 |
+
INFO: 2025-11-30T10:22:28 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
|
| 89 |
+
INFO: 2025-11-30T10:22:28 - app.services.audit_service: Audit log created: update on user_preferences by nadina73@nembors.com
|
| 90 |
+
INFO: 2025-11-30T10:22:28 - app.api.v1.auth: Preferences updated for user: nadina73@nembors.com
|
| 91 |
+
INFO: 10.16.6.70:40549 - "PUT /api/v1/auth/me/preferences HTTP/1.1" 200 OK
|
| 92 |
+
INFO: 2025-11-30T10:22:28 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
|
| 93 |
+
INFO: 2025-11-30T10:22:28 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
|
| 94 |
+
INFO: 2025-11-30T10:22:29 - app.services.dashboard_service: Dashboard cache MISS for project 0ade6bd1-e492-4e25-b681-59f42058d29a, user c5cf92be-4172-4fe2-af5c-f05d83b3a938 - building fresh data
|
| 95 |
+
INFO: 2025-11-30T10:22:29 - app.services.dashboard_service: Built and cached dashboard for project 0ade6bd1-e492-4e25-b681-59f42058d29a
|
| 96 |
+
INFO: 10.16.25.209:42985 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/dashboard HTTP/1.1" 200 OK
|
| 97 |
+
INFO: 2025-11-30T10:22:29 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
|
| 98 |
+
INFO: 2025-11-30T10:22:29 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
|
| 99 |
+
INFO: 10.16.18.114:64034 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
|
| 100 |
+
INFO: 10.16.18.114:4512 - "GET /api/v1/notifications?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&page_size=50 HTTP/1.1" 200 OK
|
| 101 |
+
INFO: 10.16.6.70:40549 - "GET /api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a HTTP/1.1" 200 OK
|
| 102 |
+
INFO: 10.16.6.70:40549 - "GET /health HTTP/1.1" 200 OK
|
| 103 |
+
INFO: 10.16.11.176:16113 - "GET /health HTTP/1.1" 200 OK
|
| 104 |
+
INFO: 10.16.25.209:8875 - "GET /health HTTP/1.1" 200 OK
|
| 105 |
+
INFO: 10.16.34.155:9722 - "GET /health HTTP/1.1" 200 OK
|
| 106 |
+
INFO: 10.16.11.176:17094 - "GET /health HTTP/1.1" 200 OK
|
| 107 |
+
|
src/app/api/v1/tickets.py
CHANGED
|
@@ -459,7 +459,7 @@ def get_ticket_detail(
|
|
| 459 |
- Images list
|
| 460 |
- Comments list
|
| 461 |
- Assignments list
|
| 462 |
-
- Status history (
|
| 463 |
|
| 464 |
**Authorization:** Must have access to ticket's project
|
| 465 |
|
|
@@ -472,6 +472,7 @@ def get_ticket_detail(
|
|
| 472 |
from app.models.ticket_image import TicketImage
|
| 473 |
from app.models.ticket_comment import TicketComment
|
| 474 |
from app.models.ticket_assignment import TicketAssignment
|
|
|
|
| 475 |
from app.services.ticket_action_service import TicketActionService
|
| 476 |
from sqlalchemy.orm import joinedload
|
| 477 |
|
|
@@ -493,7 +494,8 @@ def get_ticket_detail(
|
|
| 493 |
"expenses": [],
|
| 494 |
"images": [],
|
| 495 |
"comments": [],
|
| 496 |
-
"assignments": []
|
|
|
|
| 497 |
}
|
| 498 |
|
| 499 |
# Get source-specific data based on ticket source
|
|
@@ -701,6 +703,33 @@ def get_ticket_detail(
|
|
| 701 |
"ended_at": assignment.ended_at.isoformat() if assignment.ended_at else None
|
| 702 |
} for assignment in assignments]
|
| 703 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
return response
|
| 705 |
|
| 706 |
|
|
|
|
| 459 |
- Images list
|
| 460 |
- Comments list
|
| 461 |
- Assignments list
|
| 462 |
+
- Status history (complete audit trail with location tracking)
|
| 463 |
|
| 464 |
**Authorization:** Must have access to ticket's project
|
| 465 |
|
|
|
|
| 472 |
from app.models.ticket_image import TicketImage
|
| 473 |
from app.models.ticket_comment import TicketComment
|
| 474 |
from app.models.ticket_assignment import TicketAssignment
|
| 475 |
+
from app.models.ticket_status_history import TicketStatusHistory
|
| 476 |
from app.services.ticket_action_service import TicketActionService
|
| 477 |
from sqlalchemy.orm import joinedload
|
| 478 |
|
|
|
|
| 494 |
"expenses": [],
|
| 495 |
"images": [],
|
| 496 |
"comments": [],
|
| 497 |
+
"assignments": [],
|
| 498 |
+
"status_history": []
|
| 499 |
}
|
| 500 |
|
| 501 |
# Get source-specific data based on ticket source
|
|
|
|
| 703 |
"ended_at": assignment.ended_at.isoformat() if assignment.ended_at else None
|
| 704 |
} for assignment in assignments]
|
| 705 |
|
| 706 |
+
# Get status history
|
| 707 |
+
status_history = db.query(TicketStatusHistory).options(
|
| 708 |
+
joinedload(TicketStatusHistory.changed_by),
|
| 709 |
+
joinedload(TicketStatusHistory.assignment)
|
| 710 |
+
).filter(
|
| 711 |
+
TicketStatusHistory.ticket_id == ticket_id,
|
| 712 |
+
TicketStatusHistory.deleted_at.is_(None)
|
| 713 |
+
).order_by(TicketStatusHistory.changed_at.desc()).all()
|
| 714 |
+
|
| 715 |
+
response["status_history"] = [{
|
| 716 |
+
"id": str(history.id),
|
| 717 |
+
"old_status": history.old_status,
|
| 718 |
+
"new_status": history.new_status,
|
| 719 |
+
"changed_at": history.changed_at.isoformat() if history.changed_at else None,
|
| 720 |
+
"changed_by_user_id": str(history.changed_by_user_id) if history.changed_by_user_id else None,
|
| 721 |
+
"changed_by_user_name": history.changed_by.full_name if history.changed_by else None,
|
| 722 |
+
"assignment_id": str(history.ticket_assignment_id) if history.ticket_assignment_id else None,
|
| 723 |
+
"change_reason": history.change_reason,
|
| 724 |
+
"notes": history.notes,
|
| 725 |
+
"location_latitude": float(history.location_latitude) if history.location_latitude else None,
|
| 726 |
+
"location_longitude": float(history.location_longitude) if history.location_longitude else None,
|
| 727 |
+
"location_accuracy": float(history.location_accuracy) if history.location_accuracy else None,
|
| 728 |
+
"location_verified": history.location_verified,
|
| 729 |
+
"communication_method": history.communication_method,
|
| 730 |
+
"additional_metadata": history.additional_metadata
|
| 731 |
+
} for history in status_history]
|
| 732 |
+
|
| 733 |
return response
|
| 734 |
|
| 735 |
|
src/app/schemas/ticket_assignment.py
CHANGED
|
@@ -46,6 +46,7 @@ class TicketAssignCreate(BaseModel):
|
|
| 46 |
execution_order: Optional[int] = Field(None, ge=1, description="Agent's planned execution order")
|
| 47 |
planned_start_time: Optional[datetime] = Field(None, description="When agent plans to start")
|
| 48 |
notes: Optional[str] = Field(None, max_length=1000, description="Assignment notes")
|
|
|
|
| 49 |
|
| 50 |
class Config:
|
| 51 |
json_schema_extra = {
|
|
@@ -53,7 +54,12 @@ class TicketAssignCreate(BaseModel):
|
|
| 53 |
"user_id": "123e4567-e89b-12d3-a456-426614174000",
|
| 54 |
"execution_order": 1,
|
| 55 |
"planned_start_time": "2024-03-20T09:00:00Z",
|
| 56 |
-
"notes": "Customer prefers morning visit"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
}
|
| 59 |
|
|
@@ -230,13 +236,19 @@ class TicketSelfAssign(BaseModel):
|
|
| 230 |
execution_order: Optional[int] = Field(None, ge=1, description="Agent's planned execution order")
|
| 231 |
planned_start_time: Optional[datetime] = Field(None, description="When agent plans to start")
|
| 232 |
notes: Optional[str] = Field(None, max_length=1000, description="Assignment notes")
|
|
|
|
| 233 |
|
| 234 |
class Config:
|
| 235 |
json_schema_extra = {
|
| 236 |
"example": {
|
| 237 |
"execution_order": 1,
|
| 238 |
"planned_start_time": "2024-03-20T09:00:00Z",
|
| 239 |
-
"notes": "I know this area well, can complete quickly"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
}
|
| 241 |
}
|
| 242 |
|
|
|
|
| 46 |
execution_order: Optional[int] = Field(None, ge=1, description="Agent's planned execution order")
|
| 47 |
planned_start_time: Optional[datetime] = Field(None, description="When agent plans to start")
|
| 48 |
notes: Optional[str] = Field(None, max_length=1000, description="Assignment notes")
|
| 49 |
+
location: Optional[LocationPoint] = Field(None, description="Dispatcher's GPS location when assigning")
|
| 50 |
|
| 51 |
class Config:
|
| 52 |
json_schema_extra = {
|
|
|
|
| 54 |
"user_id": "123e4567-e89b-12d3-a456-426614174000",
|
| 55 |
"execution_order": 1,
|
| 56 |
"planned_start_time": "2024-03-20T09:00:00Z",
|
| 57 |
+
"notes": "Customer prefers morning visit",
|
| 58 |
+
"location": {
|
| 59 |
+
"latitude": -1.2921,
|
| 60 |
+
"longitude": 36.8219,
|
| 61 |
+
"accuracy": 10
|
| 62 |
+
}
|
| 63 |
}
|
| 64 |
}
|
| 65 |
|
|
|
|
| 236 |
execution_order: Optional[int] = Field(None, ge=1, description="Agent's planned execution order")
|
| 237 |
planned_start_time: Optional[datetime] = Field(None, description="When agent plans to start")
|
| 238 |
notes: Optional[str] = Field(None, max_length=1000, description="Assignment notes")
|
| 239 |
+
location: Optional[LocationPoint] = Field(None, description="Current GPS location when self-assigning")
|
| 240 |
|
| 241 |
class Config:
|
| 242 |
json_schema_extra = {
|
| 243 |
"example": {
|
| 244 |
"execution_order": 1,
|
| 245 |
"planned_start_time": "2024-03-20T09:00:00Z",
|
| 246 |
+
"notes": "I know this area well, can complete quickly",
|
| 247 |
+
"location": {
|
| 248 |
+
"latitude": -1.2921,
|
| 249 |
+
"longitude": 36.8219,
|
| 250 |
+
"accuracy": 10
|
| 251 |
+
}
|
| 252 |
}
|
| 253 |
}
|
| 254 |
|
src/app/services/ticket_assignment_service.py
CHANGED
|
@@ -109,10 +109,24 @@ class TicketAssignmentService:
|
|
| 109 |
)
|
| 110 |
|
| 111 |
self.db.add(assignment)
|
|
|
|
| 112 |
|
| 113 |
# Update ticket status
|
|
|
|
| 114 |
if ticket.status == TicketStatus.OPEN.value:
|
| 115 |
ticket.status = TicketStatus.ASSIGNED.value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
self.db.commit()
|
| 118 |
self.db.refresh(assignment)
|
|
@@ -384,19 +398,22 @@ class TicketAssignmentService:
|
|
| 384 |
)
|
| 385 |
|
| 386 |
self.db.add(assignment)
|
|
|
|
| 387 |
|
| 388 |
-
# Update ticket status
|
| 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()
|
|
|
|
| 109 |
)
|
| 110 |
|
| 111 |
self.db.add(assignment)
|
| 112 |
+
self.db.flush() # Flush to get assignment.id before creating status history
|
| 113 |
|
| 114 |
# Update ticket status
|
| 115 |
+
old_status = ticket.status
|
| 116 |
if ticket.status == TicketStatus.OPEN.value:
|
| 117 |
ticket.status = TicketStatus.ASSIGNED.value
|
| 118 |
+
|
| 119 |
+
# Create status history entry with assignment_id
|
| 120 |
+
self._create_status_history(
|
| 121 |
+
ticket=ticket,
|
| 122 |
+
old_status=old_status,
|
| 123 |
+
new_status=TicketStatus.ASSIGNED.value,
|
| 124 |
+
changed_by_user_id=assigned_by_user_id,
|
| 125 |
+
assignment_id=assignment.id,
|
| 126 |
+
reason=f"Assigned to {agent.full_name}",
|
| 127 |
+
location_lat=data.location.latitude if data.location else None,
|
| 128 |
+
location_lng=data.location.longitude if data.location else None
|
| 129 |
+
)
|
| 130 |
|
| 131 |
self.db.commit()
|
| 132 |
self.db.refresh(assignment)
|
|
|
|
| 398 |
)
|
| 399 |
|
| 400 |
self.db.add(assignment)
|
| 401 |
+
self.db.flush() # Flush to get assignment.id before creating status history
|
| 402 |
|
| 403 |
+
# Update ticket status
|
| 404 |
old_status = ticket.status
|
| 405 |
ticket.status = TicketStatus.ASSIGNED.value
|
| 406 |
|
| 407 |
+
# Create status history entry with assignment_id
|
| 408 |
self._create_status_history(
|
| 409 |
ticket=ticket,
|
| 410 |
old_status=old_status,
|
| 411 |
new_status=TicketStatus.ASSIGNED.value,
|
| 412 |
changed_by_user_id=user_id,
|
| 413 |
+
assignment_id=assignment.id, # Now has ID from flush
|
| 414 |
+
reason="Self-assigned by agent",
|
| 415 |
+
location_lat=data.location.latitude if data.location else None,
|
| 416 |
+
location_lng=data.location.longitude if data.location else None
|
| 417 |
)
|
| 418 |
|
| 419 |
self.db.commit()
|