kamau1 commited on
Commit
24bb10d
·
1 Parent(s): fd7c25c

feat: updated the pm ticket detail endpoint to give back proper actions

Browse files
docs/devlogs/browser/browserconsole.txt CHANGED
@@ -1,24 +1,96 @@
1
- view?token=Vuds_hyGrwV7qRNL56QdPD_1zQNE3_NDvtdatIe2MiM:1 Access to fetch at 'https://swiftops-backend-2025-egqez.ondigitalocean.app/api/v1/invoices/view?token=Vuds_hyGrwV7qRNL56QdPD_1zQNE3_NDvtdatIe2MiM' from origin 'https://swiftops.atomio.tech' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
2
- index-CnJZslQ6.js:72 GET https://swiftops-backend-2025-egqez.ondigitalocean.app/api/v1/invoices/view?token=Vuds_hyGrwV7qRNL56QdPD_1zQNE3_NDvtdatIe2MiM net::ERR_FAILED 500 (Internal Server Error)
3
- request @ index-CnJZslQ6.js:72
4
- get @ index-CnJZslQ6.js:72
5
- getPublicInvoice @ invoice.service-DeG-yCJ7.js:1
6
- (anonymous) @ InvoiceViewPage-Cx3k4kE1.js:1
7
- (anonymous) @ InvoiceViewPage-Cx3k4kE1.js:1
8
- wl @ vendor-BWBiZtUN.js:32
9
- en @ vendor-BWBiZtUN.js:32
10
- (anonymous) @ vendor-BWBiZtUN.js:32
11
- k @ vendor-BWBiZtUN.js:17
12
- mn @ vendor-BWBiZtUN.js:17
13
- InvoiceViewPage-Cx3k4kE1.js:1 Failed to load invoice APIError: Failed to fetch
14
- at wv.request (index-CnJZslQ6.js:72:10536)
15
- at async Object.getPublicInvoice (invoice.service-DeG-yCJ7.js:1:590)
16
- at async InvoiceViewPage-Cx3k4kE1.js:1:2442
17
- (anonymous) @ InvoiceViewPage-Cx3k4kE1.js:1
18
- await in (anonymous)
19
- (anonymous) @ InvoiceViewPage-Cx3k4kE1.js:1
20
- wl @ vendor-BWBiZtUN.js:32
21
- en @ vendor-BWBiZtUN.js:32
22
- (anonymous) @ vendor-BWBiZtUN.js:32
23
- k @ vendor-BWBiZtUN.js:17
24
- mn @ vendor-BWBiZtUN.js:17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chunk-276SZO74.js?v=bb94d704:21551 Download the React DevTools for a better development experience: https://reactjs.org/link/react-devtools
2
+ react-router-dom.js?v=bb94d704:4393 ⚠️ React Router Future Flag Warning: React Router will begin wrapping state updates in `React.startTransition` in v7. You can use the `v7_startTransition` future flag to opt-in early. For more information, see https://reactrouter.com/v6/upgrading/future#v7_starttransition.
3
+ warnOnce @ react-router-dom.js?v=bb94d704:4393
4
+ logDeprecation @ react-router-dom.js?v=bb94d704:4396
5
+ logV6DeprecationWarnings @ react-router-dom.js?v=bb94d704:4399
6
+ (anonymous) @ react-router-dom.js?v=bb94d704:5271
7
+ commitHookEffectListMount @ chunk-276SZO74.js?v=bb94d704:16915
8
+ commitPassiveMountOnFiber @ chunk-276SZO74.js?v=bb94d704:18156
9
+ commitPassiveMountEffects_complete @ chunk-276SZO74.js?v=bb94d704:18129
10
+ commitPassiveMountEffects_begin @ chunk-276SZO74.js?v=bb94d704:18119
11
+ commitPassiveMountEffects @ chunk-276SZO74.js?v=bb94d704:18109
12
+ flushPassiveEffectsImpl @ chunk-276SZO74.js?v=bb94d704:19490
13
+ flushPassiveEffects @ chunk-276SZO74.js?v=bb94d704:19447
14
+ (anonymous) @ chunk-276SZO74.js?v=bb94d704:19328
15
+ workLoop @ chunk-276SZO74.js?v=bb94d704:197
16
+ flushWork @ chunk-276SZO74.js?v=bb94d704:176
17
+ performWorkUntilDeadline @ chunk-276SZO74.js?v=bb94d704:384
18
+ react-router-dom.js?v=bb94d704:4393 ⚠️ React Router Future Flag Warning: Relative route resolution within Splat routes is changing in v7. You can use the `v7_relativeSplatPath` future flag to opt-in early. For more information, see https://reactrouter.com/v6/upgrading/future#v7_relativesplatpath.
19
+ warnOnce @ react-router-dom.js?v=bb94d704:4393
20
+ logDeprecation @ react-router-dom.js?v=bb94d704:4396
21
+ logV6DeprecationWarnings @ react-router-dom.js?v=bb94d704:4402
22
+ (anonymous) @ react-router-dom.js?v=bb94d704:5271
23
+ commitHookEffectListMount @ chunk-276SZO74.js?v=bb94d704:16915
24
+ commitPassiveMountOnFiber @ chunk-276SZO74.js?v=bb94d704:18156
25
+ commitPassiveMountEffects_complete @ chunk-276SZO74.js?v=bb94d704:18129
26
+ commitPassiveMountEffects_begin @ chunk-276SZO74.js?v=bb94d704:18119
27
+ commitPassiveMountEffects @ chunk-276SZO74.js?v=bb94d704:18109
28
+ flushPassiveEffectsImpl @ chunk-276SZO74.js?v=bb94d704:19490
29
+ flushPassiveEffects @ chunk-276SZO74.js?v=bb94d704:19447
30
+ (anonymous) @ chunk-276SZO74.js?v=bb94d704:19328
31
+ workLoop @ chunk-276SZO74.js?v=bb94d704:197
32
+ flushWork @ chunk-276SZO74.js?v=bb94d704:176
33
+ performWorkUntilDeadline @ chunk-276SZO74.js?v=bb94d704:384
34
+ core.ts:169 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/invoices/view?token=Vuds_hyGrwV7qRNL56QdPD_1zQNE3_NDvtdatIe2MiM
35
+ core.ts:169 GET https://kamau1-swiftops-backend.hf.space/api/v1/invoices/view?token=Vuds_hyGrwV7qRNL56QdPD_1zQNE3_NDvtdatIe2MiM → 200 (346ms)
36
+ core.ts:169 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail
37
+ api-client.ts:124 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail 403 (Forbidden)
38
+ request @ api-client.ts:124
39
+ get @ api-client.ts:202
40
+ fetchTicketDetail @ tickets.service.ts:70
41
+ queryFn @ useTicketDetail.ts:7
42
+ fetchFn @ @tanstack_react-query.js?v=bb94d704:881
43
+ run @ @tanstack_react-query.js?v=bb94d704:513
44
+ start @ @tanstack_react-query.js?v=bb94d704:555
45
+ fetch @ @tanstack_react-query.js?v=bb94d704:969
46
+ executeFetch_fn @ @tanstack_react-query.js?v=bb94d704:2280
47
+ onSubscribe @ @tanstack_react-query.js?v=bb94d704:1983
48
+ subscribe @ @tanstack_react-query.js?v=bb94d704:24
49
+ (anonymous) @ @tanstack_react-query.js?v=bb94d704:3147
50
+ subscribeToStore @ chunk-276SZO74.js?v=bb94d704:11984
51
+ commitHookEffectListMount @ chunk-276SZO74.js?v=bb94d704:16915
52
+ commitPassiveMountOnFiber @ chunk-276SZO74.js?v=bb94d704:18156
53
+ commitPassiveMountEffects_complete @ chunk-276SZO74.js?v=bb94d704:18129
54
+ commitPassiveMountEffects_begin @ chunk-276SZO74.js?v=bb94d704:18119
55
+ commitPassiveMountEffects @ chunk-276SZO74.js?v=bb94d704:18109
56
+ flushPassiveEffectsImpl @ chunk-276SZO74.js?v=bb94d704:19490
57
+ flushPassiveEffects @ chunk-276SZO74.js?v=bb94d704:19447
58
+ commitRootImpl @ chunk-276SZO74.js?v=bb94d704:19416
59
+ commitRoot @ chunk-276SZO74.js?v=bb94d704:19277
60
+ performSyncWorkOnRoot @ chunk-276SZO74.js?v=bb94d704:18895
61
+ flushSyncCallbacks @ chunk-276SZO74.js?v=bb94d704:9119
62
+ (anonymous) @ chunk-276SZO74.js?v=bb94d704:18627
63
+ core.ts:169 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail → 403 (281ms)
64
+ core.ts:169 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail
65
+ api-client.ts:124 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail 403 (Forbidden)
66
+ request @ api-client.ts:124
67
+ get @ api-client.ts:202
68
+ fetchTicketDetail @ tickets.service.ts:70
69
+ queryFn @ useTicketDetail.ts:7
70
+ fetchFn @ @tanstack_react-query.js?v=bb94d704:881
71
+ run @ @tanstack_react-query.js?v=bb94d704:513
72
+ (anonymous) @ @tanstack_react-query.js?v=bb94d704:538
73
+ Promise.then
74
+ (anonymous) @ @tanstack_react-query.js?v=bb94d704:534
75
+ Promise.catch
76
+ run @ @tanstack_react-query.js?v=bb94d704:517
77
+ start @ @tanstack_react-query.js?v=bb94d704:555
78
+ fetch @ @tanstack_react-query.js?v=bb94d704:969
79
+ executeFetch_fn @ @tanstack_react-query.js?v=bb94d704:2280
80
+ onSubscribe @ @tanstack_react-query.js?v=bb94d704:1983
81
+ subscribe @ @tanstack_react-query.js?v=bb94d704:24
82
+ (anonymous) @ @tanstack_react-query.js?v=bb94d704:3147
83
+ subscribeToStore @ chunk-276SZO74.js?v=bb94d704:11984
84
+ commitHookEffectListMount @ chunk-276SZO74.js?v=bb94d704:16915
85
+ commitPassiveMountOnFiber @ chunk-276SZO74.js?v=bb94d704:18156
86
+ commitPassiveMountEffects_complete @ chunk-276SZO74.js?v=bb94d704:18129
87
+ commitPassiveMountEffects_begin @ chunk-276SZO74.js?v=bb94d704:18119
88
+ commitPassiveMountEffects @ chunk-276SZO74.js?v=bb94d704:18109
89
+ flushPassiveEffectsImpl @ chunk-276SZO74.js?v=bb94d704:19490
90
+ flushPassiveEffects @ chunk-276SZO74.js?v=bb94d704:19447
91
+ commitRootImpl @ chunk-276SZO74.js?v=bb94d704:19416
92
+ commitRoot @ chunk-276SZO74.js?v=bb94d704:19277
93
+ performSyncWorkOnRoot @ chunk-276SZO74.js?v=bb94d704:18895
94
+ flushSyncCallbacks @ chunk-276SZO74.js?v=bb94d704:9119
95
+ (anonymous) @ chunk-276SZO74.js?v=bb94d704:18627
96
+ core.ts:169 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail → 403 (288ms)
docs/devlogs/browser/response.json CHANGED
@@ -1,290 +1,93 @@
1
  {
2
- "ticket": {
3
- "id": "2de41ce7-dff1-4151-9710-87958d18b5c4",
 
 
 
 
4
  "project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a",
5
- "source": "sales_order",
6
- "source_id": "a1d04741-b1b5-4a9e-baf6-d1e186886122",
7
- "ticket_name": "Elizabeth Muthoni",
8
- "ticket_type": "installation",
9
- "service_type": "ftth",
10
- "work_description": "Install Premium Fiber 100Mbps for Elizabeth Muthoni",
11
- "status": "completed",
12
- "priority": "normal",
13
- "scheduled_date": "2025-12-11",
14
- "scheduled_time_slot": "Anytime",
15
- "due_date": "2025-11-29T09:33:10.216625Z",
16
- "sla_target_date": "2025-11-29T09:33:10.216625Z",
17
- "sla_violated": true,
18
- "started_at": "2025-12-03T11:26:46.746725Z",
19
- "completed_at": "2025-12-10T12:36:25.444896Z",
20
- "is_invoiced": true,
21
- "invoiced_at": "2025-12-10T20:08:01.097828Z",
22
- "project_region_id": "4cd27765-5720-4cc0-872e-bf0da3cd1898",
23
- "work_location_latitude": "-1.2198880",
24
- "work_location_longitude": "36.8770130",
25
- "work_location_verified": true,
26
- "notes": "[COMPLETION] done",
27
- "version": 1,
28
- "created_at": "2025-11-26T09:33:10.216625Z",
29
- "updated_at": "2025-12-10T20:08:01.113908Z",
30
- "project_title": null,
31
- "region_name": null,
32
- "customer_name": null,
33
- "is_open": false,
34
- "is_assigned": false,
35
- "is_in_progress": false,
36
- "is_completed": true,
37
- "is_cancelled": false,
38
- "is_active": false,
39
- "is_overdue": false,
40
- "has_region": true,
41
- "has_schedule": true,
42
- "can_be_assigned": false,
43
- "can_be_started": false,
44
- "can_be_completed": false,
45
- "can_be_cancelled": false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  },
47
- "available_actions": [],
48
- "current_assignment": null,
49
- "team_info": {
50
- "required_size": 1,
51
- "assigned_size": 0,
52
- "is_full": false,
53
- "assigned_agents": []
54
- },
55
- "message": "Ticket TicketStatus.COMPLETED",
56
- "source_data": {
57
- "type": "sales_order",
58
- "id": "a1d04741-b1b5-4a9e-baf6-d1e186886122",
59
- "order_number": "ORD-2025-015",
60
- "customer_preferred_package": "Premium Fiber 100Mbps",
61
- "package_price": 8000.0,
62
- "installation_address": "1313 Ridgeways Close, Gate 5",
63
- "installation_latitude": -1.215,
64
- "installation_longitude": 36.815,
65
- "preferred_visit_date": "2025-12-11",
66
- "status": "processed"
67
- },
68
- "customer": {
69
- "id": "a9bccac9-1513-412c-be40-71ea7be24e9a",
70
- "name": "Elizabeth Muthoni",
71
- "phone": "+254756789013",
72
- "email": "elizabeth.muthoni@email.com",
73
- "address": null,
74
- "location_latitude": null,
75
- "location_longitude": null
76
- },
77
- "expenses": [
78
- {
79
- "id": "35db9201-3c05-4853-be91-5eb6f30782d1",
80
- "ticket_assignment_id": "34c2a077-5630-4af8-843d-e125c2497267",
81
- "incurred_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
82
- "incurred_by_user_name": "Viyisa Sasa",
83
- "category": "transport",
84
- "description": "nganya",
85
- "expense_date": "2025-12-10",
86
- "quantity": 1.0,
87
- "unit": "unit",
88
- "unit_cost": 60.0,
89
- "total_cost": 60.0,
90
- "receipt_document_id": null,
91
- "location_verified": false,
92
- "verification_notes": "Not verified: No GPS location found for 2025-12-10. Manual review required.",
93
- "is_approved": true,
94
- "approved_by_user_id": "c5cf92be-4172-4fe2-af5c-f05d83b3a938",
95
- "approved_by_user_name": "Project Manager",
96
- "approved_at": "2025-12-10T12:49:31.240803+00:00",
97
- "rejection_reason": null,
98
- "is_paid": true,
99
- "paid_to_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
100
- "paid_to_user_name": "Viyisa Sasa",
101
- "paid_at": "2025-12-10T13:23:41.579606+00:00",
102
- "payment_reference": "CSV_EXPORT_20251210_132341_c5cf92be-4172-4fe2-af5c-f05d83b3a938",
103
- "payment_recipient_type": "agent",
104
- "payment_method": "send_money",
105
- "payment_details": {
106
- "phone_number": "+254799459782",
107
- "recipient_name": "Viyisa"
108
- },
109
- "notes": "",
110
- "created_at": "2025-12-10T12:48:01.978430+00:00",
111
- "updated_at": "2025-12-10T13:23:41.581979+00:00"
112
- },
113
- {
114
- "id": "d73cb843-e4fb-4200-aa3e-243887714422",
115
- "ticket_assignment_id": "34c2a077-5630-4af8-843d-e125c2497267",
116
- "incurred_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
117
- "incurred_by_user_name": "Viyisa Sasa",
118
- "category": "transport",
119
- "description": "matatu",
120
- "expense_date": "2025-12-10",
121
- "quantity": 1.0,
122
- "unit": "unit",
123
- "unit_cost": 30.0,
124
- "total_cost": 30.0,
125
- "receipt_document_id": null,
126
- "location_verified": false,
127
- "verification_notes": "Not verified: No GPS location found for 2025-12-10. Manual review required.",
128
- "is_approved": true,
129
- "approved_by_user_id": "c5cf92be-4172-4fe2-af5c-f05d83b3a938",
130
- "approved_by_user_name": "Project Manager",
131
- "approved_at": "2025-12-10T12:49:31.240794+00:00",
132
- "rejection_reason": null,
133
- "is_paid": true,
134
- "paid_to_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
135
- "paid_to_user_name": "Viyisa Sasa",
136
- "paid_at": "2025-12-10T13:23:41.579593+00:00",
137
- "payment_reference": "CSV_EXPORT_20251210_132341_c5cf92be-4172-4fe2-af5c-f05d83b3a938",
138
- "payment_recipient_type": "agent",
139
- "payment_method": "send_money",
140
- "payment_details": {
141
- "phone_number": "+254799459782",
142
- "recipient_name": "Viyisa"
143
- },
144
- "notes": "",
145
- "created_at": "2025-12-10T12:47:37.968268+00:00",
146
- "updated_at": "2025-12-10T13:23:41.581981+00:00"
147
- },
148
- {
149
- "id": "b8a0bac2-69c0-4218-a565-e1e2b6ce0d4d",
150
- "ticket_assignment_id": "34c2a077-5630-4af8-843d-e125c2497267",
151
- "incurred_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
152
- "incurred_by_user_name": "Viyisa Sasa",
153
- "category": "transport",
154
- "description": "boda boda",
155
- "expense_date": "2025-12-10",
156
- "quantity": 1.0,
157
- "unit": "unit",
158
- "unit_cost": 100.0,
159
- "total_cost": 100.0,
160
- "receipt_document_id": null,
161
- "location_verified": false,
162
- "verification_notes": "Not verified: No GPS location found for 2025-12-10. Manual review required.",
163
- "is_approved": true,
164
- "approved_by_user_id": "c5cf92be-4172-4fe2-af5c-f05d83b3a938",
165
- "approved_by_user_name": "Project Manager",
166
- "approved_at": "2025-12-10T12:49:31.240785+00:00",
167
- "rejection_reason": null,
168
- "is_paid": true,
169
- "paid_to_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
170
- "paid_to_user_name": "Viyisa Sasa",
171
- "paid_at": "2025-12-10T13:48:43.261303+00:00",
172
- "payment_reference": "CSV_EXPORT_20251210_134843_c5cf92be-4172-4fe2-af5c-f05d83b3a938",
173
- "payment_recipient_type": "agent",
174
- "payment_method": "send_money",
175
- "payment_details": {
176
- "phone_number": "+254799459782",
177
- "recipient_name": "Viyisa"
178
- },
179
- "notes": "",
180
- "created_at": "2025-12-10T12:32:23.362852+00:00",
181
- "updated_at": "2025-12-10T13:48:43.265156+00:00"
182
- }
183
- ],
184
- "images": [
185
- {
186
- "id": "e37b665d-fd6d-4c77-b39e-a6064cb49d64",
187
- "image_url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1765370174/ticket_2de41ce7_ticket_photo_jcc_jcc_20251210_123614_screenshot_2025-09-04_093459.webp",
188
- "image_type": "completion",
189
- "description": "[JCC] Completion photo for ticket",
190
- "captured_at": "2025-12-10T12:36:14.781724+00:00",
191
- "uploaded_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
192
- "created_at": "2025-12-10T12:36:14.782564+00:00"
193
- },
194
- {
195
- "id": "ecfc24fc-6be0-4888-a65e-7acf83088c2e",
196
- "image_url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1765370173/ticket_2de41ce7_ticket_photo_odu_outdoor_image_odu_outdoor_image_20251210_123613_screenshot_2025-09-01_163321.webp",
197
- "image_type": "completion",
198
- "description": "[ODU outdoor image] Completion photo for ticket",
199
- "captured_at": "2025-12-10T12:36:14.128316+00:00",
200
- "uploaded_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
201
- "created_at": "2025-12-10T12:36:14.129146+00:00"
202
- },
203
- {
204
- "id": "cbbec871-af75-4364-ab34-55272764a415",
205
- "image_url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1765370172/ticket_2de41ce7_ticket_photo_speedtest_speedtest_20251210_123611_screenshot_2025-08-19_155834.webp",
206
- "image_type": "completion",
207
- "description": "[Speedtest] Completion photo for ticket",
208
- "captured_at": "2025-12-10T12:36:13.395865+00:00",
209
- "uploaded_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
210
- "created_at": "2025-12-10T12:36:13.396676+00:00"
211
- },
212
- {
213
- "id": "07674999-1fcd-46e9-86ad-8dae1846a218",
214
- "image_url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1765370171/ticket_2de41ce7_ticket_photo_airtel_network_airtel_network_20251210_123610_screenshot_2025-08-25_090418.webp",
215
- "image_type": "completion",
216
- "description": "[Airtel network] Completion photo for ticket",
217
- "captured_at": "2025-12-10T12:36:11.763055+00:00",
218
- "uploaded_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
219
- "created_at": "2025-12-10T12:36:11.814756+00:00"
220
- }
221
- ],
222
- "comments": [],
223
- "assignments": [
224
- {
225
- "id": "34c2a077-5630-4af8-843d-e125c2497267",
226
- "user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
227
- "user_name": "Viyisa Sasa",
228
- "action": "completed",
229
- "status": "CLOSED",
230
- "assigned_at": "2025-12-03T11:26:09.556998+00:00",
231
- "responded_at": "2025-12-03T11:26:09.557001+00:00",
232
- "journey_started_at": "2025-12-03T11:26:46.746692+00:00",
233
- "arrived_at": "2025-12-03T12:18:09.241003+00:00",
234
- "ended_at": "2025-12-10T12:36:25.458584+00:00"
235
- }
236
- ],
237
- "status_history": [
238
- {
239
- "id": "faf96f42-b676-4c03-bf71-895aaace1ca4",
240
- "old_status": "in_progress",
241
- "new_status": "completed",
242
- "changed_at": "2025-12-10T12:36:25.466040+00:00",
243
- "changed_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
244
- "changed_by_user_name": "viya",
245
- "assignment_id": "34c2a077-5630-4af8-843d-e125c2497267",
246
- "change_reason": "Ticket completed with all requirements satisfied",
247
- "notes": "done",
248
- "location_latitude": null,
249
- "location_longitude": null,
250
- "location_accuracy": null,
251
- "location_verified": false,
252
- "communication_method": "app",
253
- "additional_metadata": {}
254
- },
255
- {
256
- "id": "df7dc5a2-273a-4256-8203-5b42ef347da3",
257
- "old_status": "assigned",
258
- "new_status": "in_progress",
259
- "changed_at": "2025-12-03T11:26:46.747547+00:00",
260
- "changed_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
261
- "changed_by_user_name": "viya",
262
- "assignment_id": "34c2a077-5630-4af8-843d-e125c2497267",
263
- "change_reason": "Agent started journey to site",
264
- "notes": null,
265
- "location_latitude": -1.219959,
266
- "location_longitude": 36.8769753,
267
- "location_accuracy": null,
268
- "location_verified": false,
269
- "communication_method": "app",
270
- "additional_metadata": {}
271
- },
272
- {
273
- "id": "2920a339-4a41-4806-bdc4-2a4d846fefb9",
274
- "old_status": "open",
275
- "new_status": "assigned",
276
- "changed_at": "2025-12-03T11:26:09.599984+00:00",
277
- "changed_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
278
- "changed_by_user_name": "viya",
279
- "assignment_id": "34c2a077-5630-4af8-843d-e125c2497267",
280
- "change_reason": "Self-assigned by agent",
281
- "notes": null,
282
- "location_latitude": null,
283
- "location_longitude": null,
284
- "location_accuracy": null,
285
- "location_verified": false,
286
- "communication_method": "app",
287
- "additional_metadata": {}
288
- }
289
- ]
290
  }
 
1
  {
2
+ "invoice": {
3
+ "id": "81c96213-485f-4170-92a0-23c08332923b",
4
+ "invoice_number": "INV-TEL-2025-00001",
5
+ "invoice_title": "Invoice for 1 tickets",
6
+ "contractor_id": "1af9fb24-e5bb-40ac-a748-0997580b4c32",
7
+ "client_id": "a2455244-d87e-4279-9fca-dc067f06b5c3",
8
  "project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a",
9
+ "issue_date": "2025-12-10",
10
+ "due_date": "2026-01-09",
11
+ "subtotal": 0.0,
12
+ "tax_rate": 0.0,
13
+ "tax_amount": 0.0,
14
+ "discount_amount": 0.0,
15
+ "total_amount": 0.0,
16
+ "amount_paid": 0.0,
17
+ "amount_due": 0.0,
18
+ "currency": "KES",
19
+ "status": "draft",
20
+ "notes": null,
21
+ "terms_and_conditions": null,
22
+ "line_items": [
23
+ {
24
+ "id": "uqtB0XJIJAviIUWq-Ez7dA",
25
+ "type": "ticket",
26
+ "ticket_id": "2de41ce7-dff1-4151-9710-87958d18b5c4",
27
+ "sales_order_id": "a1d04741-b1b5-4a9e-baf6-d1e186886122",
28
+ "sales_order_number": "ORD-2025-015",
29
+ "description": "Installation - Elizabeth Muthoni",
30
+ "ticket_type": "installation",
31
+ "completed_at": "2025-12-10T12:36:25.444896+00:00",
32
+ "quantity": 1.0,
33
+ "unit_price": 0.0,
34
+ "total": 0.0,
35
+ "ticket_details": {
36
+ "id": "2de41ce7-dff1-4151-9710-87958d18b5c4",
37
+ "ticket_name": "Elizabeth Muthoni",
38
+ "ticket_type": "installation",
39
+ "work_description": "Install Premium Fiber 100Mbps for Elizabeth Muthoni",
40
+ "completed_at": "2025-12-10T12:36:25.444896+00:00",
41
+ "scheduled_date": "2025-12-11",
42
+ "work_location": {
43
+ "latitude": -1.219888,
44
+ "longitude": 36.877013
45
+ },
46
+ "completion_data": {
47
+ "odu_serial": "wetert",
48
+ "ont_serial": "ewrwet",
49
+ "odu_imei_number": "sdfgreh",
50
+ "activated_number": "12345678"
51
+ },
52
+ "images": [
53
+ {
54
+ "id": "07674999-1fcd-46e9-86ad-8dae1846a218",
55
+ "image_type": "completion",
56
+ "description": "[Airtel network] Completion photo for ticket",
57
+ "url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1765370171/ticket_2de41ce7_ticket_photo_airtel_network_airtel_network_20251210_123610_screenshot_2025-08-25_090418.webp",
58
+ "file_name": "ticket_2de41ce7_ticket_photo_airtel_network_airtel_network_20251210_123610_screenshot_2025-08-25_090418.png",
59
+ "captured_at": "2025-12-10T12:36:11.763055+00:00"
60
+ },
61
+ {
62
+ "id": "cbbec871-af75-4364-ab34-55272764a415",
63
+ "image_type": "completion",
64
+ "description": "[Speedtest] Completion photo for ticket",
65
+ "url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1765370172/ticket_2de41ce7_ticket_photo_speedtest_speedtest_20251210_123611_screenshot_2025-08-19_155834.webp",
66
+ "file_name": "ticket_2de41ce7_ticket_photo_speedtest_speedtest_20251210_123611_screenshot_2025-08-19_155834.png",
67
+ "captured_at": "2025-12-10T12:36:13.395865+00:00"
68
+ },
69
+ {
70
+ "id": "ecfc24fc-6be0-4888-a65e-7acf83088c2e",
71
+ "image_type": "completion",
72
+ "description": "[ODU outdoor image] Completion photo for ticket",
73
+ "url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1765370173/ticket_2de41ce7_ticket_photo_odu_outdoor_image_odu_outdoor_image_20251210_123613_screenshot_2025-09-01_163321.webp",
74
+ "file_name": "ticket_2de41ce7_ticket_photo_odu_outdoor_image_odu_outdoor_image_20251210_123613_screenshot_2025-09-01_163321.png",
75
+ "captured_at": "2025-12-10T12:36:14.128316+00:00"
76
+ },
77
+ {
78
+ "id": "e37b665d-fd6d-4c77-b39e-a6064cb49d64",
79
+ "image_type": "completion",
80
+ "description": "[JCC] Completion photo for ticket",
81
+ "url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1765370174/ticket_2de41ce7_ticket_photo_jcc_jcc_20251210_123614_screenshot_2025-09-04_093459.webp",
82
+ "file_name": "ticket_2de41ce7_ticket_photo_jcc_jcc_20251210_123614_screenshot_2025-09-04_093459.png",
83
+ "captured_at": "2025-12-10T12:36:14.781724+00:00"
84
+ }
85
+ ],
86
+ "images_count": 4
87
+ }
88
+ }
89
+ ]
90
  },
91
+ "token_expires_at": "2026-01-09T20:08:01.097861+00:00",
92
+ "times_viewed": 18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
docs/devlogs/server/runtimeerror.txt CHANGED
@@ -1,65 +1,398 @@
 
1
 
2
- Dec 10 21:16:30
3
- File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 718, in __call__
4
- Dec 10 21:16:30
5
- await route.handle(scope, receive, send)
6
- Dec 10 21:16:30
7
- File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 276, in handle
8
- Dec 10 21:16:30
9
- await self.app(scope, receive, send)
10
- Dec 10 21:16:30
11
- File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 66, in app
12
- Dec 10 21:16:30
13
- response = await func(request)
14
- Dec 10 21:16:30
15
- ^^^^^^^^^^^^^^^^^^^
16
- Dec 10 21:16:30
17
- File "/usr/local/lib/python3.11/site-packages/fastapi/routing.py", line 274, in app
18
- Dec 10 21:16:30
19
- raw_response = await run_endpoint_function(
20
- Dec 10 21:16:30
21
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
22
- Dec 10 21:16:30
23
- File "/usr/local/lib/python3.11/site-packages/fastapi/routing.py", line 193, in run_endpoint_function
24
- Dec 10 21:16:30
25
- return await run_in_threadpool(dependant.call, **values)
26
- Dec 10 21:16:30
27
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28
- Dec 10 21:16:30
29
- File "/usr/local/lib/python3.11/site-packages/starlette/concurrency.py", line 41, in run_in_threadpool
30
- Dec 10 21:16:30
31
- return await anyio.to_thread.run_sync(func, *args)
32
- Dec 10 21:16:30
33
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34
- Dec 10 21:16:30
35
- File "/usr/local/lib/python3.11/site-packages/anyio/to_thread.py", line 33, in run_sync
36
- Dec 10 21:16:30
37
- return await get_asynclib().run_sync_in_worker_thread(
38
- Dec 10 21:16:30
39
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40
- Dec 10 21:16:30
41
- File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 877, in run_sync_in_worker_thread
42
- Dec 10 21:16:30
43
- return await future
44
- Dec 10 21:16:30
45
- ^^^^^^^^^^^^
46
- Dec 10 21:16:30
47
- File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 807, in run
48
- Dec 10 21:16:30
49
- result = context.run(func, *args)
50
- Dec 10 21:16:30
51
- ^^^^^^^^^^^^^^^^^^^^^^^^
52
- Dec 10 21:16:30
53
- File "/app/src/app/api/v1/invoice_viewing.py", line 38, in view_invoice
54
- Dec 10 21:16:30
55
- invoice_data = InvoiceViewingService.get_invoice_by_token(
56
- Dec 10 21:16:30
57
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58
- Dec 10 21:16:30
59
- File "/app/src/app/services/invoice_viewing_service.py", line 51, in get_invoice_by_token
60
- Dec 10 21:16:30
61
- if invoice.viewing_token_expires_at and invoice.viewing_token_expires_at < datetime.utcnow():
62
- Dec 10 21:16:30
63
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
64
- Dec 10 21:16:30
65
- TypeError: can't compare offset-naive and offset-aware datetimes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ===== Application Startup at 2025-12-10 21:53:44 =====
2
 
3
+ INFO: Started server process [7]
4
+ INFO: Waiting for application startup.
5
+ INFO: 2025-12-10T21:54:05 - app.main: ============================================================
6
+ INFO: 2025-12-10T21:54:05 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
+ INFO: 2025-12-10T21:54:05 - app.main: 📊 Dashboard: Enabled
8
+ INFO: 2025-12-10T21:54:05 - app.main: ============================================================
9
+ INFO: 2025-12-10T21:54:05 - app.main: 📦 Database:
10
+ INFO: 2025-12-10T21:54:05 - app.main: ✓ Connected | 47 tables | 6 users
11
+ INFO: 2025-12-10T21:54:05 - app.main: 💾 Cache & Sessions:
12
+ INFO: 2025-12-10T21:54:06 - app.services.otp_service: OTP Service initialized with Redis storage
13
+ INFO: 2025-12-10T21:54:07 - app.main: ✓ Redis: Connected
14
+ INFO: 2025-12-10T21:54:07 - app.main: 🔌 External Services:
15
+ INFO: 2025-12-10T21:54:07 - app.main: ✓ Cloudinary: Connected
16
+ INFO: 2025-12-10T21:54:07 - app.main: ✓ Resend: Configured
17
+ INFO: 2025-12-10T21:54:07 - app.main: ○ WASender: Disconnected
18
+ INFO: 2025-12-10T21:54:07 - app.main: ✓ Supabase: Connected | 6 buckets
19
+ INFO: 2025-12-10T21:54:07 - app.main: ⏰ Scheduler:
20
+ INFO: 2025-12-10T21:54:07 - apscheduler.scheduler: Adding job tentatively -- it will be properly scheduled when the scheduler starts
21
+ INFO: 2025-12-10T21:54:07 - apscheduler.scheduler: Added job "Daily Field Agent Reconciliation" to job store "default"
22
+ INFO: 2025-12-10T21:54:07 - apscheduler.scheduler: Scheduler started
23
+ INFO: 2025-12-10T21:54:07 - app.tasks.scheduler: Reconciliation scheduler started (runs at 10 PM Africa/Nairobi)
24
+ INFO: 2025-12-10T21:54:07 - app.main: ✓ Daily reconciliation scheduler started (runs at midnight)
25
+ INFO: 2025-12-10T21:54:07 - app.main: ============================================================
26
+ INFO: 2025-12-10T21:54:07 - app.main: ✅ Startup complete | Ready to serve requests
27
+ INFO: 2025-12-10T21:54:07 - app.main: ============================================================
28
+ INFO: Application startup complete.
29
+ INFO: Uvicorn running on http://0.0.0.0:7860 (Press CTRL+C to quit)
30
+ INFO: 2025-12-10T22:00:00 - apscheduler.executors.default: Running job "Daily Field Agent Reconciliation (trigger: cron[hour='22', minute='0'], next run at: 2025-12-10 22:00:00 UTC)" (scheduled at 2025-12-10 22:00:00+00:00)
31
+ INFO: 2025-12-10T22:00:00 - app.tasks.scheduler: Starting scheduled validation for 2025-12-10
32
+ INFO: 2025-12-10T22:00:00 - app.tasks.scheduler: Validating 1 projects for 2025-12-10
33
+ INFO: 2025-12-10T22:00:00 - app.services.reconciliation.reconciliation_service: Starting reconciliation: project=0ade6bd1-e492-4e25-b681-59f42058d29a, date=2025-12-10, type=scheduled
34
+ INFO: 2025-12-10T22:00:00 - app.services.reconciliation.reconciliation_service: Aggregated 1 agents in 71ms
35
+ INFO: 2025-12-10T22:00:00 - app.services.reconciliation.anomaly_detector: Detected 0 anomalies across 1 agents
36
+ ERROR: 2025-12-10T22:00:00 - app.services.reconciliation.reconciliation_service: Reconciliation failed: (psycopg2.ProgrammingError) can't adapt type 'dict'
37
+ [SQL:
38
+ UPDATE reconciliation_runs
39
+ SET
40
+ status = 'completed',
41
+ completed_at = NOW(),
42
+ agents_processed = %(agents_processed)s,
43
+ timesheets_created = %(timesheets_created)s,
44
+ timesheets_updated = %(timesheets_updated)s,
45
+ assignments_processed = %(assignments_processed)s,
46
+ expenses_processed = %(expenses_processed)s,
47
+ summary_stats = %(summary_stats)s,
48
+ anomalies_detected = %(anomalies)s,
49
+ execution_time_ms = %(execution_time_ms)s,
50
+ query_time_ms = %(query_time_ms)s,
51
+ updated_at = NOW()
52
+ WHERE id = %(run_id)s
53
+ ]
54
+ [parameters: {'agents_processed': 1, 'timesheets_created': 0, 'timesheets_updated': 1, 'assignments_processed': 1, 'expenses_processed': 3, 'summary_stats': {'total_agents': 1, 'total_tickets_assigned': 1, 'total_tickets_completed': 2, 'total_tickets_rejected': 0, 'total_tickets_cancelled': 0, 'total_expen ... (37 characters truncated) ... es': 2370.0, 'total_pending_expenses': 7650.0, 'total_expense_claims': 3, 'avg_tickets_per_agent': 2.0, 'avg_expenses_per_agent': Decimal('10020.00')}, 'anomalies': [], 'execution_time_ms': 288, 'query_time_ms': 71, 'run_id': 'ade18d69-39da-40da-92bf-5ff92beaf9ea'}]
55
+ (Background on this error at: https://sqlalche.me/e/20/f405)
56
+ Traceback (most recent call last):
57
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
58
+ self.dialect.do_execute(
59
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
60
+ cursor.execute(statement, parameters)
61
+ psycopg2.ProgrammingError: can't adapt type 'dict'
62
+
63
+ The above exception was the direct cause of the following exception:
64
+
65
+ Traceback (most recent call last):
66
+ File "/app/src/app/services/reconciliation/reconciliation_service.py", line 117, in reconcile_project_day
67
+ self._complete_run(
68
+ File "/app/src/app/services/reconciliation/reconciliation_service.py", line 482, in _complete_run
69
+ self.db.execute(query, {
70
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
71
+ return self._execute_internal(
72
+ ^^^^^^^^^^^^^^^^^^^^^^^
73
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2199, in _execute_internal
74
+ result = conn.execute(
75
+ ^^^^^^^^^^^^^
76
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
77
+ return meth(
78
+ ^^^^^
79
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
80
+ return connection._execute_clauseelement(
81
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
83
+ ret = self._execute_context(
84
+ ^^^^^^^^^^^^^^^^^^^^^^
85
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
86
+ return self._exec_single_context(
87
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
88
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
89
+ self._handle_dbapi_exception(
90
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
91
+ raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
92
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
93
+ self.dialect.do_execute(
94
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
95
+ cursor.execute(statement, parameters)
96
+ sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) can't adapt type 'dict'
97
+ [SQL:
98
+ UPDATE reconciliation_runs
99
+ SET
100
+ status = 'completed',
101
+ completed_at = NOW(),
102
+ agents_processed = %(agents_processed)s,
103
+ timesheets_created = %(timesheets_created)s,
104
+ timesheets_updated = %(timesheets_updated)s,
105
+ assignments_processed = %(assignments_processed)s,
106
+ expenses_processed = %(expenses_processed)s,
107
+ summary_stats = %(summary_stats)s,
108
+ anomalies_detected = %(anomalies)s,
109
+ execution_time_ms = %(execution_time_ms)s,
110
+ query_time_ms = %(query_time_ms)s,
111
+ updated_at = NOW()
112
+ WHERE id = %(run_id)s
113
+ ]
114
+ [parameters: {'agents_processed': 1, 'timesheets_created': 0, 'timesheets_updated': 1, 'assignments_processed': 1, 'expenses_processed': 3, 'summary_stats': {'total_agents': 1, 'total_tickets_assigned': 1, 'total_tickets_completed': 2, 'total_tickets_rejected': 0, 'total_tickets_cancelled': 0, 'total_expen ... (37 characters truncated) ... es': 2370.0, 'total_pending_expenses': 7650.0, 'total_expense_claims': 3, 'avg_tickets_per_agent': 2.0, 'avg_expenses_per_agent': Decimal('10020.00')}, 'anomalies': [], 'execution_time_ms': 288, 'query_time_ms': 71, 'run_id': 'ade18d69-39da-40da-92bf-5ff92beaf9ea'}]
115
+ (Background on this error at: https://sqlalche.me/e/20/f405)
116
+ ERROR: 2025-12-10T22:00:00 - app.tasks.scheduler: Failed to validate project 0ade6bd1-e492-4e25-b681-59f42058d29a (Atomio Fttx): Reconciliation failed: (psycopg2.ProgrammingError) can't adapt type 'dict'
117
+ [SQL:
118
+ UPDATE reconciliation_runs
119
+ SET
120
+ status = 'completed',
121
+ completed_at = NOW(),
122
+ agents_processed = %(agents_processed)s,
123
+ timesheets_created = %(timesheets_created)s,
124
+ timesheets_updated = %(timesheets_updated)s,
125
+ assignments_processed = %(assignments_processed)s,
126
+ expenses_processed = %(expenses_processed)s,
127
+ summary_stats = %(summary_stats)s,
128
+ anomalies_detected = %(anomalies)s,
129
+ execution_time_ms = %(execution_time_ms)s,
130
+ query_time_ms = %(query_time_ms)s,
131
+ updated_at = NOW()
132
+ WHERE id = %(run_id)s
133
+ ]
134
+ [parameters: {'agents_processed': 1, 'timesheets_created': 0, 'timesheets_updated': 1, 'assignments_processed': 1, 'expenses_processed': 3, 'summary_stats': {'total_agents': 1, 'total_tickets_assigned': 1, 'total_tickets_completed': 2, 'total_tickets_rejected': 0, 'total_tickets_cancelled': 0, 'total_expen ... (37 characters truncated) ... es': 2370.0, 'total_pending_expenses': 7650.0, 'total_expense_claims': 3, 'avg_tickets_per_agent': 2.0, 'avg_expenses_per_agent': Decimal('10020.00')}, 'anomalies': [], 'execution_time_ms': 288, 'query_time_ms': 71, 'run_id': 'ade18d69-39da-40da-92bf-5ff92beaf9ea'}]
135
+ (Background on this error at: https://sqlalche.me/e/20/f405)
136
+ Traceback (most recent call last):
137
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
138
+ self.dialect.do_execute(
139
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
140
+ cursor.execute(statement, parameters)
141
+ psycopg2.ProgrammingError: can't adapt type 'dict'
142
+
143
+ The above exception was the direct cause of the following exception:
144
+
145
+ Traceback (most recent call last):
146
+ File "/app/src/app/services/reconciliation/reconciliation_service.py", line 117, in reconcile_project_day
147
+ self._complete_run(
148
+ File "/app/src/app/services/reconciliation/reconciliation_service.py", line 482, in _complete_run
149
+ self.db.execute(query, {
150
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
151
+ return self._execute_internal(
152
+ ^^^^^^^^^^^^^^^^^^^^^^^
153
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2199, in _execute_internal
154
+ result = conn.execute(
155
+ ^^^^^^^^^^^^^
156
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
157
+ return meth(
158
+ ^^^^^
159
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
160
+ return connection._execute_clauseelement(
161
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
162
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
163
+ ret = self._execute_context(
164
+ ^^^^^^^^^^^^^^^^^^^^^^
165
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
166
+ return self._exec_single_context(
167
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
168
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
169
+ self._handle_dbapi_exception(
170
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
171
+ raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
172
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
173
+ self.dialect.do_execute(
174
+ File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
175
+ cursor.execute(statement, parameters)
176
+ sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) can't adapt type 'dict'
177
+ [SQL:
178
+ UPDATE reconciliation_runs
179
+ SET
180
+ status = 'completed',
181
+ completed_at = NOW(),
182
+ agents_processed = %(agents_processed)s,
183
+ timesheets_created = %(timesheets_created)s,
184
+ timesheets_updated = %(timesheets_updated)s,
185
+ assignments_processed = %(assignments_processed)s,
186
+ expenses_processed = %(expenses_processed)s,
187
+ summary_stats = %(summary_stats)s,
188
+ anomalies_detected = %(anomalies)s,
189
+ execution_time_ms = %(execution_time_ms)s,
190
+ query_time_ms = %(query_time_ms)s,
191
+ updated_at = NOW()
192
+ WHERE id = %(run_id)s
193
+ ]
194
+ [parameters: {'agents_processed': 1, 'timesheets_created': 0, 'timesheets_updated': 1, 'assignments_processed': 1, 'expenses_processed': 3, 'summary_stats': {'total_agents': 1, 'total_tickets_assigned': 1, 'total_tickets_completed': 2, 'total_tickets_rejected': 0, 'total_tickets_cancelled': 0, 'total_expen ... (37 characters truncated) ... es': 2370.0, 'total_pending_expenses': 7650.0, 'total_expense_claims': 3, 'avg_tickets_per_agent': 2.0, 'avg_expenses_per_agent': Decimal('10020.00')}, 'anomalies': [], 'execution_time_ms': 288, 'query_time_ms': 71, 'run_id': 'ade18d69-39da-40da-92bf-5ff92beaf9ea'}]
195
+ (Background on this error at: https://sqlalche.me/e/20/f405)
196
+
197
+ The above exception was the direct cause of the following exception:
198
+
199
+ Traceback (most recent call last):
200
+ File "/app/src/app/tasks/scheduler.py", line 132, in validate_all_projects
201
+ run_id = service.reconcile_project_day(
202
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
203
+ File "/app/src/app/services/reconciliation/reconciliation_service.py", line 149, in reconcile_project_day
204
+ raise ReconciliationError(f"Reconciliation failed: {str(e)}") from e
205
+ app.services.reconciliation.reconciliation_service.ReconciliationError: Reconciliation failed: (psycopg2.ProgrammingError) can't adapt type 'dict'
206
+ [SQL:
207
+ UPDATE reconciliation_runs
208
+ SET
209
+ status = 'completed',
210
+ completed_at = NOW(),
211
+ agents_processed = %(agents_processed)s,
212
+ timesheets_created = %(timesheets_created)s,
213
+ timesheets_updated = %(timesheets_updated)s,
214
+ assignments_processed = %(assignments_processed)s,
215
+ expenses_processed = %(expenses_processed)s,
216
+ summary_stats = %(summary_stats)s,
217
+ anomalies_detected = %(anomalies)s,
218
+ execution_time_ms = %(execution_time_ms)s,
219
+ query_time_ms = %(query_time_ms)s,
220
+ updated_at = NOW()
221
+ WHERE id = %(run_id)s
222
+ ]
223
+ [parameters: {'agents_processed': 1, 'timesheets_created': 0, 'timesheets_updated': 1, 'assignments_processed': 1, 'expenses_processed': 3, 'summary_stats': {'total_agents': 1, 'total_tickets_assigned': 1, 'total_tickets_completed': 2, 'total_tickets_rejected': 0, 'total_tickets_cancelled': 0, 'total_expen ... (37 characters truncated) ... es': 2370.0, 'total_pending_expenses': 7650.0, 'total_expense_claims': 3, 'avg_tickets_per_agent': 2.0, 'avg_expenses_per_agent': Decimal('10020.00')}, 'anomalies': [], 'execution_time_ms': 288, 'query_time_ms': 71, 'run_id': 'ade18d69-39da-40da-92bf-5ff92beaf9ea'}]
224
+ (Background on this error at: https://sqlalche.me/e/20/f405)
225
+ INFO: 2025-12-10T22:00:00 - app.tasks.scheduler: Validation summary: 0/1 projects succeeded
226
+ WARNING: 2025-12-10T22:00:00 - app.tasks.scheduler: Failed projects: ['Atomio Fttx']
227
+ INFO: 2025-12-10T22:00:00 - app.tasks.scheduler: Scheduled validation completed for 2025-12-10
228
+ INFO: 2025-12-10T22:00:00 - apscheduler.executors.default: Job "Daily Field Agent Reconciliation (trigger: cron[hour='22', minute='0'], next run at: 2025-12-11 22:00:00 UTC)" executed successfully
229
+ INFO: 2025-12-10T22:00:00 - app.tasks.scheduler: Job daily_validation completed successfully
230
+ INFO: 10.16.13.79:30150 - "GET / HTTP/1.1" 200 OK
231
+ INFO: 10.16.13.79:44122 - "GET / HTTP/1.1" 200 OK
232
+ INFO: 10.16.13.79:55137 - "GET /health HTTP/1.1" 200 OK
233
+ INFO: 2025-12-11T06:07:25 - app.core.supabase_auth: Session refreshed successfully
234
+ INFO: 2025-12-11T06:07:29 - app.api.v1.auth: ✅ Token refreshed successfully for: nadina73@nembors.com
235
+ INFO: 10.16.37.13:60071 - "POST /api/v1/auth/refresh-token HTTP/1.1" 200 OK
236
+ INFO: 2025-12-11T06:07:30 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
237
+ INFO: 2025-12-11T06:07:30 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
238
+ INFO: 10.16.13.79:32694 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
239
+ INFO: 2025-12-11T06:07:30 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
240
+ INFO: 2025-12-11T06:07:30 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
241
+ INFO: 10.16.37.13:60071 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
242
+ INFO: 2025-12-11T06:07:30 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
243
+ INFO: 2025-12-11T06:07:30 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
244
+ INFO: 10.16.13.79:32694 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
245
+ INFO: 2025-12-11T06:07:31 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
246
+ INFO: 2025-12-11T06:07:31 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
247
+ INFO: 10.16.37.13:60071 - "GET /api/v1/analytics/user/overview HTTP/1.1" 200 OK
248
+ INFO: 2025-12-11T06:07:32 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
249
+ INFO: 2025-12-11T06:07:32 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
250
+ INFO: 2025-12-11T06:07:32 - app.services.project_service: Listed 1 projects (total: 1) for user c5cf92be-4172-4fe2-af5c-f05d83b3a938
251
+ INFO: 10.16.13.79:32694 - "GET /api/v1/projects?page=1&per_page=100 HTTP/1.1" 200 OK
252
+ INFO: 10.16.13.79:50368 - "GET /health HTTP/1.1" 200 OK
253
+ INFO: 10.16.13.79:63963 - "GET /health HTTP/1.1" 200 OK
254
+ INFO: 10.16.13.79:14550 - "GET /health HTTP/1.1" 200 OK
255
+ INFO: 10.16.37.13:47705 - "GET /health HTTP/1.1" 200 OK
256
+ INFO: 2025-12-11T06:19:31 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
257
+ INFO: 2025-12-11T06:19:31 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
258
+ INFO: 10.16.13.79:29766 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
259
+ INFO: 2025-12-11T06:19:32 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
260
+ INFO: 2025-12-11T06:19:32 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
261
+ INFO: 10.16.13.79:29766 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
262
+ INFO: 2025-12-11T06:19:32 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
263
+ INFO: 2025-12-11T06:19:32 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
264
+ INFO: 10.16.37.13:47705 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
265
+ INFO: 2025-12-11T06:19:32 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
266
+ INFO: 2025-12-11T06:19:32 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
267
+ INFO: 10.16.37.13:47705 - "GET /api/v1/analytics/user/overview HTTP/1.1" 200 OK
268
+ INFO: 2025-12-11T06:19:33 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
269
+ INFO: 2025-12-11T06:19:33 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
270
+ INFO: 2025-12-11T06:19:33 - app.services.project_service: Listed 1 projects (total: 1) for user c5cf92be-4172-4fe2-af5c-f05d83b3a938
271
+ INFO: 10.16.37.13:47705 - "GET /api/v1/projects?page=1&per_page=100 HTTP/1.1" 200 OK
272
+ INFO: 10.16.37.13:10471 - "GET /health HTTP/1.1" 200 OK
273
+ INFO: 2025-12-11T06:19:46 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
274
+ INFO: 2025-12-11T06:19:46 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
275
+ INFO: 2025-12-11T06:19:46 - app.services.audit_service: Audit log created: update on user_preferences by nadina73@nembors.com
276
+ INFO: 2025-12-11T06:19:46 - app.api.v1.auth: Preferences updated for user: nadina73@nembors.com
277
+ INFO: 10.16.13.79:25924 - "PUT /api/v1/auth/me/preferences HTTP/1.1" 200 OK
278
+ INFO: 2025-12-11T06:19:47 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
279
+ INFO: 2025-12-11T06:19:47 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
280
+ INFO: 10.16.37.13:10471 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
281
+ INFO: 2025-12-11T06:19:47 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
282
+ INFO: 2025-12-11T06:19:47 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
283
+ INFO: 2025-12-11T06:19:47 - app.services.dashboard_service: Dashboard cache MISS for project 0ade6bd1-e492-4e25-b681-59f42058d29a, user c5cf92be-4172-4fe2-af5c-f05d83b3a938 - building fresh data
284
+ INFO: 2025-12-11T06:19:47 - app.services.dashboard_service: Built and cached dashboard for project 0ade6bd1-e492-4e25-b681-59f42058d29a
285
+ INFO: 10.16.13.79:25924 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/dashboard HTTP/1.1" 200 OK
286
+ INFO: 10.16.13.79:25924 - "GET /api/v1/notifications?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&page_size=50&is_read=false HTTP/1.1" 200 OK
287
+ INFO: 10.16.37.13:10471 - "GET /api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a HTTP/1.1" 200 OK
288
+ INFO: 10.16.13.79:39294 - "GET /api/v1/contractor-invoices?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a HTTP/1.1" 200 OK
289
+ INFO: 10.16.37.13:56789 - "GET /api/v1/contractor-invoices/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a HTTP/1.1" 200 OK
290
+ INFO: 10.16.13.79:54124 - "GET /api/v1/contractor-invoices/81c96213-485f-4170-92a0-23c08332923b HTTP/1.1" 200 OK
291
+ INFO: 10.16.13.79:54124 - "GET /api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail HTTP/1.1" 200 OK
292
+ INFO: 10.16.37.13:5569 - "GET /health HTTP/1.1" 200 OK
293
+ INFO: 10.16.37.13:15098 - "GET /health HTTP/1.1" 200 OK
294
+ INFO: 10.16.13.79:36865 - "GET /health HTTP/1.1" 200 OK
295
+ INFO: 10.16.13.79:32070 - "GET /health HTTP/1.1" 200 OK
296
+ INFO: 10.16.37.13:16706 - "GET /health HTTP/1.1" 200 OK
297
+ INFO: 2025-12-11T06:23:08 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
298
+ INFO: 2025-12-11T06:23:08 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
299
+ INFO: 10.16.13.79:5103 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
300
+ INFO: 2025-12-11T06:23:09 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
301
+ INFO: 2025-12-11T06:23:09 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
302
+ INFO: 10.16.13.79:5103 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
303
+ INFO: 2025-12-11T06:23:09 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
304
+ INFO: 2025-12-11T06:23:09 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
305
+ INFO: 10.16.37.13:16706 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
306
+ INFO: 10.16.37.13:18227 - "GET /api/v1/contractor-invoices/81c96213-485f-4170-92a0-23c08332923b HTTP/1.1" 200 OK
307
+ INFO: 10.16.13.79:5103 - "GET /health HTTP/1.1" 200 OK
308
+ INFO: 2025-12-11T06:23:16 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
309
+ INFO: 2025-12-11T06:23:16 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
310
+ INFO: 10.16.37.13:17230 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
311
+ INFO: 10.16.13.79:33301 - "GET /health HTTP/1.1" 200 OK
312
+ INFO: 2025-12-11T06:23:16 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
313
+ INFO: 2025-12-11T06:23:16 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
314
+ INFO: 10.16.37.13:17230 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
315
+ INFO: 2025-12-11T06:23:16 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
316
+ INFO: 2025-12-11T06:23:16 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
317
+ INFO: 10.16.13.79:33301 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
318
+ INFO: 10.16.37.13:17230 - "GET /api/v1/contractor-invoices/81c96213-485f-4170-92a0-23c08332923b HTTP/1.1" 200 OK
319
+ INFO: 10.16.13.79:33301 - "GET /api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail HTTP/1.1" 200 OK
320
+ INFO: 10.16.37.13:30569 - "GET /health HTTP/1.1" 200 OK
321
+ INFO: 2025-12-11T06:25:50 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
322
+ INFO: 2025-12-11T06:25:50 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
323
+ INFO: 10.16.13.79:29711 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
324
+ INFO: 10.16.37.13:39487 - "GET /health HTTP/1.1" 200 OK
325
+ INFO: 2025-12-11T06:25:51 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
326
+ INFO: 2025-12-11T06:25:51 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
327
+ INFO: 10.16.37.13:39487 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
328
+ INFO: 2025-12-11T06:25:51 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
329
+ INFO: 2025-12-11T06:25:51 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
330
+ INFO: 10.16.13.79:29711 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
331
+ INFO: 10.16.37.13:53350 - "GET /api/v1/contractor-invoices/81c96213-485f-4170-92a0-23c08332923b HTTP/1.1" 200 OK
332
+ INFO: 2025-12-11T06:25:59 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
333
+ INFO: 2025-12-11T06:25:59 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
334
+ INFO: 10.16.37.13:52474 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
335
+ INFO: 10.16.13.79:62628 - "GET /health HTTP/1.1" 200 OK
336
+ INFO: 2025-12-11T06:25:59 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
337
+ INFO: 2025-12-11T06:25:59 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
338
+ INFO: 10.16.37.13:52474 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
339
+ INFO: 2025-12-11T06:25:59 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
340
+ INFO: 2025-12-11T06:25:59 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
341
+ INFO: 10.16.13.79:62628 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
342
+ INFO: 10.16.37.13:23782 - "GET /api/v1/contractor-invoices/81c96213-485f-4170-92a0-23c08332923b HTTP/1.1" 200 OK
343
+ INFO: 10.16.37.13:16935 - "GET /health HTTP/1.1" 200 OK
344
+ INFO: 2025-12-11T06:26:23 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
345
+ INFO: 2025-12-11T06:26:23 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
346
+ INFO: 10.16.13.79:7127 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
347
+ INFO: 2025-12-11T06:26:23 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
348
+ INFO: 2025-12-11T06:26:23 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
349
+ INFO: 10.16.13.79:7127 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
350
+ INFO: 2025-12-11T06:26:23 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
351
+ INFO: 2025-12-11T06:26:23 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
352
+ INFO: 10.16.37.13:16935 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
353
+ INFO: 10.16.13.79:63911 - "GET /api/v1/contractor-invoices/81c96213-485f-4170-92a0-23c08332923b HTTP/1.1" 200 OK
354
+ INFO: 10.16.37.13:62825 - "GET /health HTTP/1.1" 200 OK
355
+ INFO: 10.16.37.13:62825 - "GET /api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail HTTP/1.1" 200 OK
356
+ INFO: 10.16.13.79:59155 - "GET /health HTTP/1.1" 200 OK
357
+ INFO: 10.16.37.13:27183 - "GET /health HTTP/1.1" 200 OK
358
+ INFO: 2025-12-11T06:27:46 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
359
+ INFO: 2025-12-11T06:27:46 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
360
+ INFO: 10.16.13.79:51476 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
361
+ INFO: 2025-12-11T06:27:46 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
362
+ INFO: 2025-12-11T06:27:46 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
363
+ INFO: 10.16.13.79:51476 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
364
+ INFO: 2025-12-11T06:27:46 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
365
+ INFO: 2025-12-11T06:27:46 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
366
+ INFO: 10.16.37.13:27183 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
367
+ INFO: 10.16.37.13:64873 - "GET /api/v1/contractor-invoices/81c96213-485f-4170-92a0-23c08332923b HTTP/1.1" 200 OK
368
+ INFO: 10.16.13.79:33275 - "GET /health HTTP/1.1" 200 OK
369
+ INFO: 10.16.13.79:33275 - "GET /api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail HTTP/1.1" 200 OK
370
+ INFO: 10.16.37.13:23023 - "GET /health HTTP/1.1" 200 OK
371
+ INFO: 2025-12-11T06:28:31 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
372
+ INFO: 2025-12-11T06:28:31 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
373
+ INFO: 10.16.13.79:31206 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
374
+ INFO: 2025-12-11T06:28:31 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
375
+ INFO: 2025-12-11T06:28:31 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
376
+ INFO: 10.16.13.79:31206 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
377
+ INFO: 2025-12-11T06:28:31 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
378
+ INFO: 2025-12-11T06:28:31 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
379
+ INFO: 10.16.37.13:23023 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
380
+ INFO: 10.16.13.79:49321 - "GET /api/v1/contractor-invoices/81c96213-485f-4170-92a0-23c08332923b HTTP/1.1" 200 OK
381
+ INFO: 10.16.37.13:23023 - "GET /health HTTP/1.1" 200 OK
382
+ INFO: 10.16.37.13:22536 - "GET /api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail HTTP/1.1" 200 OK
383
+ INFO: 10.16.37.13:22536 - "GET /health HTTP/1.1" 200 OK
384
+ INFO: 2025-12-11T06:28:37 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
385
+ INFO: 2025-12-11T06:28:37 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
386
+ INFO: 10.16.13.79:20306 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
387
+ INFO: 2025-12-11T06:28:37 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
388
+ INFO: 2025-12-11T06:28:37 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
389
+ INFO: 10.16.13.79:20306 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
390
+ INFO: 2025-12-11T06:28:38 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
391
+ INFO: 2025-12-11T06:28:38 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
392
+ INFO: 10.16.37.13:22536 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
393
+ INFO: 10.16.13.79:35880 - "GET /api/v1/contractor-invoices/81c96213-485f-4170-92a0-23c08332923b HTTP/1.1" 200 OK
394
+ INFO: 10.16.13.79:35880 - "GET /api/v1/tickets/2de41ce7-dff1-4151-9710-87958d18b5c4/detail HTTP/1.1" 200 OK
395
+ INFO: 10.16.37.13:4657 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions/4cd27765-5720-4cc0-872e-bf0da3cd1898 HTTP/1.1" 405 Method Not Allowed
396
+ INFO: 10.16.13.79:11727 - "GET /health HTTP/1.1" 200 OK
397
+ INFO: 10.16.37.13:10176 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions/4cd27765-5720-4cc0-872e-bf0da3cd1898 HTTP/1.1" 405 Method Not Allowed
398
+ INFO: 10.16.37.13:19693 - "GET /health HTTP/1.1" 200 OK
src/app/api/v1/invoice_viewing.py CHANGED
@@ -6,6 +6,7 @@ No authentication required - token provides access.
6
  """
7
  from fastapi import APIRouter, Depends, HTTPException, Query, Response
8
  from sqlalchemy.orm import Session
 
9
 
10
  from app.api.deps import get_db
11
  from app.services.invoice_viewing_service import InvoiceViewingService
@@ -16,9 +17,10 @@ from app.schemas.invoice_viewing import PublicInvoiceResponse
16
  router = APIRouter(prefix="/invoices/view", tags=["Invoice Viewing (Public)"])
17
 
18
 
19
- @router.get("", response_model=PublicInvoiceResponse)
20
  def view_invoice(
21
  token: str = Query(..., description="Viewing token from invoice link"),
 
22
  db: Session = Depends(get_db)
23
  ):
24
  """
@@ -26,20 +28,56 @@ def view_invoice(
26
 
27
  **No authentication required.**
28
 
29
- Returns:
30
- - Complete invoice details
31
- - Ticket information with images
32
- - Completion data
33
- - Work locations
 
 
34
 
35
  The token is validated and view is tracked.
36
  If token is expired, returns 403 error.
37
  """
 
 
38
  invoice_data = InvoiceViewingService.get_invoice_by_token(
39
  db=db,
40
  token=token
41
  )
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  return PublicInvoiceResponse(**invoice_data)
44
 
45
 
 
6
  """
7
  from fastapi import APIRouter, Depends, HTTPException, Query, Response
8
  from sqlalchemy.orm import Session
9
+ from typing import Optional
10
 
11
  from app.api.deps import get_db
12
  from app.services.invoice_viewing_service import InvoiceViewingService
 
17
  router = APIRouter(prefix="/invoices/view", tags=["Invoice Viewing (Public)"])
18
 
19
 
20
+ @router.get("")
21
  def view_invoice(
22
  token: str = Query(..., description="Viewing token from invoice link"),
23
+ ticket_id: Optional[str] = Query(None, description="Optional: Filter to show only specific ticket details"),
24
  db: Session = Depends(get_db)
25
  ):
26
  """
 
28
 
29
  **No authentication required.**
30
 
31
+ **Query Parameters:**
32
+ - `token`: Viewing token from invoice link (required)
33
+ - `ticket_id`: Optional - if provided, returns only that ticket's details instead of full invoice
34
+
35
+ **Returns:**
36
+ - If `ticket_id` provided: Single ticket details with images and completion data
37
+ - If no `ticket_id`: Complete invoice with all tickets
38
 
39
  The token is validated and view is tracked.
40
  If token is expired, returns 403 error.
41
  """
42
+ from uuid import UUID
43
+
44
  invoice_data = InvoiceViewingService.get_invoice_by_token(
45
  db=db,
46
  token=token
47
  )
48
 
49
+ # If ticket_id provided, filter to return only that ticket's details
50
+ if ticket_id:
51
+ try:
52
+ ticket_uuid = UUID(ticket_id)
53
+
54
+ # Find the ticket in line items
55
+ for line_item in invoice_data["invoice"]["line_items"]:
56
+ if (line_item.get("type") == "ticket" and
57
+ line_item.get("ticket_id") == str(ticket_uuid) and
58
+ line_item.get("ticket_details")):
59
+
60
+ # Return just the ticket details
61
+ return {
62
+ "ticket": line_item["ticket_details"],
63
+ "invoice_number": invoice_data["invoice"]["invoice_number"],
64
+ "invoice_id": invoice_data["invoice"]["id"],
65
+ "sales_order_number": line_item.get("sales_order_number"),
66
+ "token_expires_at": invoice_data["token_expires_at"]
67
+ }
68
+
69
+ # Ticket not found in invoice
70
+ raise HTTPException(
71
+ status_code=404,
72
+ detail="Ticket not found in this invoice"
73
+ )
74
+ except ValueError:
75
+ raise HTTPException(
76
+ status_code=400,
77
+ detail="Invalid ticket_id format"
78
+ )
79
+
80
+ # Return full invoice
81
  return PublicInvoiceResponse(**invoice_data)
82
 
83
 
src/app/api/v1/projects.py CHANGED
@@ -1723,6 +1723,54 @@ async def list_project_regions(
1723
  )
1724
 
1725
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1726
  @router.patch("/{project_id}/regions/{region_id}", response_model=ProjectRegionResponse)
1727
  async def update_project_region(
1728
  project_id: UUID,
 
1723
  )
1724
 
1725
 
1726
+ @router.get("/{project_id}/regions/{region_id}", response_model=ProjectRegionResponse)
1727
+ async def get_project_region(
1728
+ project_id: UUID,
1729
+ region_id: UUID,
1730
+ db: Session = Depends(get_db),
1731
+ current_user: User = Depends(get_current_user)
1732
+ ):
1733
+ """
1734
+ Get a specific project region by ID.
1735
+
1736
+ **Authorization:**
1737
+ - PM, Dispatcher, Field Agent: Can view regions in their projects
1738
+ - Platform Admin: Can view any region
1739
+ """
1740
+ from app.models.project_region import ProjectRegion
1741
+
1742
+ # Get region
1743
+ region = db.query(ProjectRegion).filter(
1744
+ ProjectRegion.id == region_id,
1745
+ ProjectRegion.project_id == project_id,
1746
+ ProjectRegion.deleted_at.is_(None)
1747
+ ).first()
1748
+
1749
+ if not region:
1750
+ raise HTTPException(
1751
+ status_code=status.HTTP_404_NOT_FOUND,
1752
+ detail="Region not found"
1753
+ )
1754
+
1755
+ # Authorization check
1756
+ if current_user.role != AppRole.PLATFORM_ADMIN.value:
1757
+ # Check if user has access to this project
1758
+ from app.models.project_team import ProjectTeam
1759
+ has_access = db.query(ProjectTeam).filter(
1760
+ ProjectTeam.project_id == project_id,
1761
+ ProjectTeam.user_id == current_user.id,
1762
+ ProjectTeam.deleted_at.is_(None)
1763
+ ).first() is not None
1764
+
1765
+ if not has_access:
1766
+ raise HTTPException(
1767
+ status_code=status.HTTP_403_FORBIDDEN,
1768
+ detail="Not authorized to view this region"
1769
+ )
1770
+
1771
+ return region
1772
+
1773
+
1774
  @router.patch("/{project_id}/regions/{region_id}", response_model=ProjectRegionResponse)
1775
  async def update_project_region(
1776
  project_id: UUID,
src/app/services/ticket_action_service.py CHANGED
@@ -114,6 +114,70 @@ class TicketActionService:
114
  actions = []
115
  message = None
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  # State: Ticket completed or cancelled (view only)
118
  if ticket.status in [TicketStatus.COMPLETED.value, TicketStatus.CANCELLED.value]:
119
  message = f"Ticket {ticket.status}"
 
114
  actions = []
115
  message = None
116
 
117
+ # Check if user is PM/Dispatcher (management role)
118
+ is_management = current_user.role in [
119
+ AppRole.PROJECT_MANAGER.value,
120
+ AppRole.DISPATCHER.value,
121
+ AppRole.PLATFORM_ADMIN.value
122
+ ]
123
+
124
+ # PM/Dispatcher actions (different from field agent actions)
125
+ if is_management:
126
+ if ticket.status == TicketStatus.OPEN.value:
127
+ actions = [
128
+ {"action": "assign", "label": "Assign to Agent", "type": "primary"},
129
+ {"action": "call_customer", "label": "Call Customer", "type": "secondary"},
130
+ {"action": "cancel", "label": "Cancel Ticket", "type": "danger"}
131
+ ]
132
+ message = "Ticket awaiting assignment"
133
+
134
+ elif ticket.status == TicketStatus.ASSIGNED.value:
135
+ agent_names = [a["name"] for a in team_info["assigned_agents"] if a["name"]]
136
+ actions = [
137
+ {"action": "reassign", "label": "Reassign", "type": "primary"},
138
+ {"action": "call_customer", "label": "Call Customer", "type": "secondary"},
139
+ {"action": "cancel", "label": "Cancel Ticket", "type": "danger"}
140
+ ]
141
+ if agent_names:
142
+ message = f"Assigned to {', '.join(agent_names)}"
143
+ else:
144
+ message = "Assigned to agents"
145
+
146
+ elif ticket.status == TicketStatus.IN_PROGRESS.value:
147
+ actions = [
148
+ {"action": "call_customer", "label": "Call Customer", "type": "secondary"},
149
+ {"action": "cancel", "label": "Cancel Ticket", "type": "danger"}
150
+ ]
151
+ message = "Work in progress"
152
+
153
+ elif ticket.status == TicketStatus.PENDING_REVIEW.value:
154
+ actions = [
155
+ {"action": "reopen", "label": "Reopen & Reassign", "type": "primary"},
156
+ {"action": "cancel", "label": "Cancel Ticket", "type": "danger"},
157
+ {"action": "call_customer", "label": "Call Customer", "type": "secondary"}
158
+ ]
159
+ message = "Awaiting review - agent dropped ticket"
160
+
161
+ elif ticket.status == TicketStatus.COMPLETED.value:
162
+ actions = [
163
+ {"action": "reopen", "label": "Reopen Ticket", "type": "warning"}
164
+ ]
165
+ message = "Ticket completed"
166
+
167
+ elif ticket.status == TicketStatus.CANCELLED.value:
168
+ actions = [
169
+ {"action": "reopen", "label": "Reopen Ticket", "type": "warning"}
170
+ ]
171
+ message = "Ticket cancelled"
172
+
173
+ return {
174
+ "available_actions": actions,
175
+ "current_assignment": current_assignment,
176
+ "team_info": team_info,
177
+ "message": message
178
+ }
179
+
180
+ # Field Agent actions (existing logic)
181
  # State: Ticket completed or cancelled (view only)
182
  if ticket.status in [TicketStatus.COMPLETED.value, TicketStatus.CANCELLED.value]:
183
  message = f"Ticket {ticket.status}"