kamau1 commited on
Commit
94e7f53
·
1 Parent(s): bb1483f

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
- Internal Server Error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:02:22 =====
2
 
3
  INFO: Started server process [7]
4
  INFO: Waiting for application startup.
5
- INFO: 2025-11-30T10:02:36 - app.main: ============================================================
6
- INFO: 2025-11-30T10:02:36 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
- INFO: 2025-11-30T10:02:36 - app.main: 📊 Dashboard: Enabled
8
- INFO: 2025-11-30T10:02:36 - app.main: ============================================================
9
- INFO: 2025-11-30T10:02:36 - app.main: 📦 Database:
10
- INFO: 2025-11-30T10:02:36 - app.main: ✓ Connected | 44 tables | 6 users
11
- INFO: 2025-11-30T10:02:36 - app.main: 💾 Cache & Sessions:
12
- INFO: 2025-11-30T10:02:37 - app.services.otp_service: ✅ OTP Service initialized with Redis storage
13
- INFO: 2025-11-30T10:02:38 - app.main: ✓ Redis: Connected
14
- INFO: 2025-11-30T10:02:38 - app.main: 🔌 External Services:
15
- INFO: 2025-11-30T10:02:38 - app.main: ✓ Cloudinary: Connected
16
- INFO: 2025-11-30T10:02:38 - app.main: ✓ Resend: Configured
17
- INFO: 2025-11-30T10:02:38 - app.main: ○ WASender: Failed
18
- INFO: 2025-11-30T10:02:38 - app.main: ✓ Supabase: Connected | 6 buckets
19
- INFO: 2025-11-30T10:02:38 - app.main: ============================================================
20
- INFO: 2025-11-30T10:02:38 - app.main: ✅ Startup complete | Ready to serve requests
21
- INFO: 2025-11-30T10:02:38 - 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.18.114:55928 - "GET /health HTTP/1.1" 200 OK
25
- INFO: 10.16.6.70:56128 - "GET /health HTTP/1.1" 200 OK
26
- INFO: 10.16.6.70:56561 - "GET /health HTTP/1.1" 200 OK
27
- INFO: 10.16.18.114:10241 - "GET /health HTTP/1.1" 200 OK
28
- INFO: 10.16.11.176:46093 - "GET /health HTTP/1.1" 200 OK
29
- INFO: 10.16.11.176:25317 - "GET /health HTTP/1.1" 200 OK
30
- INFO: 10.16.6.70:58656 - "GET /health HTTP/1.1" 200 OK
31
- INFO: 10.16.11.176:46518 - "GET /health HTTP/1.1" 200 OK
32
- INFO: 10.16.11.176:52236 - "GET /health HTTP/1.1" 200 OK
33
- INFO: 10.16.6.70:15409 - "GET /health HTTP/1.1" 200 OK
34
- INFO: 10.16.6.70:62819 - "GET /health HTTP/1.1" 200 OK
35
- INFO: 10.16.25.209:50726 - "GET /health HTTP/1.1" 200 OK
36
- ERROR: 2025-11-30T10:07:42 - app.api.deps: Authentication error: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
37
- INFO: 10.16.25.209:50726 - "GET /api/v1/auth/me HTTP/1.1" 401 Unauthorized
38
- INFO: 2025-11-30T10:07:43 - app.core.supabase_auth: Session refreshed successfully
39
- ERROR: 2025-11-30T10:07:43 - app.api.v1.auth: Token refresh error: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper[Ticket(tickets)]'. Original exception was: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
40
- INFO: 10.16.6.70:13712 - "POST /api/v1/auth/refresh-token HTTP/1.1" 401 Unauthorized
41
- INFO: 10.16.18.114:1142 - "GET /health HTTP/1.1" 200 OK
42
- INFO: 10.16.6.70:23975 - "GET /health HTTP/1.1" 200 OK
43
- INFO: 2025-11-30T10:08:39 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
44
- ERROR: 2025-11-30T10:08:39 - app.api.v1.auth: Login error: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper[Ticket(tickets)]'. Original exception was: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
45
- ERROR: 2025-11-30T10:08:39 - app.services.audit_service: Failed to create audit log: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper[Ticket(tickets)]'. Original exception was: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
46
- INFO: 10.16.25.209:28616 - "POST /api/v1/auth/login HTTP/1.1" 401 Unauthorized
47
- INFO: 10.16.34.155:12295 - "GET /health HTTP/1.1" 200 OK
48
- INFO: 10.16.6.70:18122 - "GET /health HTTP/1.1" 200 OK
49
- INFO: 10.16.6.70:5588 - "GET /health HTTP/1.1" 200 OK
50
- INFO: 10.16.25.209:12555 - "GET /health HTTP/1.1" 200 OK
51
- INFO: 10.16.6.70:58652 - "GET /health HTTP/1.1" 200 OK
52
- INFO: 10.16.18.114:8697 - "GET /health HTTP/1.1" 200 OK
53
- INFO: 10.16.6.70:7897 - "GET /health HTTP/1.1" 200 OK
54
- INFO: 10.16.6.70:39538 - "GET /health HTTP/1.1" 200 OK
55
- INFO: 10.16.11.176:6264 - "GET /health HTTP/1.1" 200 OK
56
- INFO: 10.16.6.70:5580 - "GET /health HTTP/1.1" 200 OK
57
- INFO: 10.16.25.209:28827 - "GET /health HTTP/1.1" 200 OK
58
- INFO: 10.16.34.155:27217 - "GET /health HTTP/1.1" 200 OK
59
- INFO: 10.16.6.70:44523 - "GET /health HTTP/1.1" 200 OK
60
- INFO: 10.16.34.155:25939 - "GET /health HTTP/1.1" 200 OK
61
- INFO: 10.16.34.155:3936 - "GET /health HTTP/1.1" 200 OK
62
- INFO: 10.16.6.70:62519 - "GET /health HTTP/1.1" 200 OK
63
- INFO: 10.16.11.176:51920 - "GET /health HTTP/1.1" 200 OK
64
- INFO: 10.16.18.114:28609 - "GET /health HTTP/1.1" 200 OK
65
- INFO: 10.16.11.176:8729 - "GET / HTTP/1.1" 200 OK
66
- INFO: 10.16.34.155:40138 - "GET /health HTTP/1.1" 200 OK
67
- INFO: 2025-11-30T10:16:27 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
68
- ERROR: 2025-11-30T10:16:27 - app.api.v1.auth: Login error: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper[Ticket(tickets)]'. Original exception was: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
69
- ERROR: 2025-11-30T10:16:27 - app.services.audit_service: Failed to create audit log: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper[Ticket(tickets)]'. Original exception was: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
70
- INFO: 10.16.6.70:22674 - "POST /api/v1/auth/login HTTP/1.1" 401 Unauthorized
71
- INFO: 10.16.6.70:30069 - "GET /health HTTP/1.1" 200 OK
72
- INFO: 10.16.25.209:49805 - "GET /health HTTP/1.1" 200 OK
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 (if available)
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 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()
 
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()