Spaces:
Sleeping
Sleeping
Add TicketStatusHistory model and integrate automatic status logging into ticket assignment workflow.
Browse files- docs/agent/implementation-notes/ROUTER_PREFIX_FIX.md +188 -0
- docs/devlogs/browser/response.json +39 -89
- docs/devlogs/db/logs.json +2 -4
- docs/devlogs/server/runtimeerror.txt +53 -47
- src/app/models/ticket.py +1 -0
- src/app/models/ticket_status_history.py +66 -0
- src/app/services/ticket_assignment_service.py +65 -112
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 |
-
"
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
| 45 |
-
"
|
| 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 |
-
"
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
"
|
| 63 |
-
"
|
| 64 |
-
"is_full": false,
|
| 65 |
-
"assigned_agents": []
|
| 66 |
},
|
| 67 |
-
"
|
| 68 |
-
"
|
| 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 |
-
|
| 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:
|
| 2 |
|
| 3 |
INFO: Started server process [7]
|
| 4 |
INFO: Waiting for application startup.
|
| 5 |
-
INFO: 2025-11-28T09:
|
| 6 |
-
INFO: 2025-11-28T09:
|
| 7 |
-
INFO: 2025-11-28T09:
|
| 8 |
-
INFO: 2025-11-28T09:
|
| 9 |
-
INFO: 2025-11-28T09:
|
| 10 |
-
INFO: 2025-11-28T09:
|
| 11 |
-
INFO: 2025-11-28T09:
|
| 12 |
-
INFO: 2025-11-28T09:
|
| 13 |
-
INFO: 2025-11-28T09:
|
| 14 |
-
INFO: 2025-11-28T09:
|
| 15 |
-
INFO: 2025-11-28T09:
|
| 16 |
-
INFO: 2025-11-28T09:
|
| 17 |
-
INFO: 2025-11-28T09:
|
| 18 |
-
INFO: 2025-11-28T09:
|
| 19 |
-
INFO: 2025-11-28T09:
|
| 20 |
-
INFO: 2025-11-28T09:
|
| 21 |
-
INFO: 2025-11-28T09:
|
| 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.6.70:
|
| 26 |
-
INFO: 10.16.
|
| 27 |
-
INFO:
|
| 28 |
-
INFO:
|
| 29 |
-
INFO: 10.16.
|
| 30 |
-
INFO: 2025-11-28T09:
|
| 31 |
-
INFO: 2025-11-28T09:
|
| 32 |
-
INFO: 10.16.
|
| 33 |
-
INFO: 2025-11-28T09:
|
| 34 |
-
INFO: 2025-11-28T09:
|
| 35 |
-
INFO: 10.16.11.176:
|
| 36 |
-
INFO:
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
INFO: 10.16.6.70:
|
| 42 |
-
INFO: 10.16.
|
| 43 |
-
INFO: 10.16.
|
| 44 |
-
INFO: 10.16.
|
| 45 |
-
INFO: 10.16.34.155:
|
| 46 |
-
INFO: 10.16.
|
| 47 |
-
INFO: 10.16.
|
| 48 |
-
INFO: 10.16.6.70:
|
| 49 |
-
INFO: 10.16.
|
| 50 |
-
INFO: 10.16.
|
| 51 |
-
INFO: 10.16.
|
| 52 |
-
INFO: 10.16.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 427 |
-
|
| 428 |
-
|
| 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.
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 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.
|
| 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
|