kamau1 commited on
Commit
a9afcba
·
1 Parent(s): de77159

Fix: Update ON CONFLICT to use (user_id, work_date) and align reconciliation logic with deployed migrations

Browse files
docs/devlogs/browser/response.json CHANGED
@@ -1,135 +1,945 @@
1
- {
2
- "basic_info": {
3
- "id": "43b778b0-2062-4724-abbb-916a4835a9b0",
4
- "name": "Viyisa Sasa",
5
- "phone": "+25470000001",
6
- "phone_alternate": "+25470000002",
7
- "email": "viyisa8151@feralrex.com",
8
- "id_number": "12437583",
9
- "display_name": "viya",
10
- "emergency_contact_name": "poa sana",
11
- "emergency_contact_phone": "+25470000003",
12
- "role": "field_agent",
13
- "status": "active",
14
- "is_active": true,
15
- "client_id": null,
16
- "contractor_id": "1af9fb24-e5bb-40ac-a748-0997580b4c32",
17
- "profile_photo_url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1764665304/user_43b778b0_profile_photo_20251202_084824_gemini_generated_image_anof4fa.png",
18
- "created_at": "2025-11-24T09:55:41.602235Z",
19
- "updated_at": "2025-12-02T07:41:55.682479Z"
20
- },
21
- "health_info": {
22
- "blood_type": "B+",
23
- "allergies": "nuts",
24
- "chronic_conditions": "asthma",
25
- "medications": "insulin",
26
- "last_medical_check": "2025-12-01",
27
- "medical_notes": ""
28
- },
29
- "ppe_sizes": {
30
- "height": "177",
31
- "weight": "62kg",
32
- "waist": "35",
33
- "shoe_size": "43",
34
- "helmet_size": "XXL",
35
- "shirt_size": "M",
36
- "pants_size": "XL",
37
- "glove_size": "M",
38
- "vest_size": "M"
39
- },
40
- "location": {
41
- "current_location_name": "Nairobi",
42
- "current_country": "Kenya",
43
- "current_region": "Nai",
44
- "current_city": "Nairobi",
45
- "current_address_line1": "area one, road 2",
46
- "current_address_line2": "",
47
- "current_maps_link": "https://www.google.com/maps?q=-1.2199098171149145,36.87698126601467",
48
- "current_latitude": -1.21990981711491,
49
- "current_longitude": 36.8769812660147,
50
- "current_location_updated_at": "2025-12-02T07:35:02.083495Z"
51
- },
52
- "completion_status": {
53
- "basic_info": true,
54
- "health_info": true,
55
- "ppe_sizes": true,
56
- "financial_accounts": true,
57
- "documents": true,
58
- "location": true,
59
- "completion_percentage": 100
60
- },
61
- "financial_accounts_count": 1,
62
- "documents_count": 2,
63
- "asset_assignments_count": 1,
64
- "documents": [
65
- {
66
- "id": "aad31648-7bb6-4721-8a39-18fc38676ce0",
67
- "document_type": "national_id",
68
- "document_category": "identity",
69
- "file_name": "user_43b778b0_national_id_20251209_081904_annie-spratt-oty0mkqc2yk-unspl.jpg",
70
- "file_url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1765268345/user_43b778b0_national_id_20251209_081904_annie-spratt-oty0mkqc2yk-unspl.webp",
71
- "file_size": 252216,
72
- "file_type": "image/webp",
73
- "storage_provider": "cloudinary",
74
- "version": 1,
75
- "is_latest_version": true,
76
- "description": "front",
77
- "tags": [],
78
- "created_at": "2025-12-09T08:19:05.525069+00:00"
79
- },
80
- {
81
- "id": "72d8048e-2fd8-49ed-b833-3ad6d464baf2",
82
- "document_type": "profile_photo",
83
- "document_category": "profile",
84
- "file_name": "user_43b778b0_profile_photo_20251202_084824_gemini_generated_image_anof4fa.png",
85
- "file_url": "https://res.cloudinary.com/dnhajmziu/image/upload/v1764665304/user_43b778b0_profile_photo_20251202_084824_gemini_generated_image_anof4fa.png",
86
- "file_size": 101003,
87
- "file_type": "image/png",
88
- "storage_provider": "cloudinary",
89
- "version": 1,
90
- "is_latest_version": true,
91
- "description": "User profile photo",
92
- "tags": [
93
- "profile",
94
- "photo"
95
- ],
96
- "created_at": "2025-12-02T08:48:25.091753+00:00"
97
- }
98
- ],
99
- "financial_accounts": [
100
- {
101
- "id": "c6d350c7-1b90-4e48-b7e5-ce80d1bac22a",
102
- "account_name": "M-Pesa - +2547994597824",
103
- "payout_method": "mobile_money",
104
- "mobile_money_provider": "M-Pesa",
105
- "mobile_money_phone": "+2547994597823",
106
- "bank_name": "",
107
- "bank_account_number": "",
108
- "is_primary": true,
109
- "is_active": true,
110
- "is_verified": false
111
- }
112
- ],
113
- "asset_assignments": [
114
- {
115
- "id": "5f1bbe56-baa7-4429-a1ac-5a5b935aaf55",
116
- "asset_type": "Tool",
117
- "asset_name": "Mobigo",
118
- "asset_description": null,
119
- "serial_number": "0799930258345",
120
- "asset_value": 239999.0,
121
- "assigned_at": "2025-12-09T20:51:26.341180Z",
122
- "is_active": true,
123
- "condition_on_assign": "New"
124
- }
125
- ],
126
- "projects": [
127
- {
128
- "id": "0ade6bd1-e492-4e25-b681-59f42058d29a",
129
- "title": "Atomio Fttx",
130
- "role": null,
131
- "is_lead": false,
132
- "assigned_at": "2025-11-24T09:55:41.668630+00:00"
133
- }
134
- ]
135
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- 1. Get all columns in timesheets table
2
+ SELECT
3
+ column_name,
4
+ data_type,
5
+ is_nullable,
6
+ column_default,
7
+ character_maximum_length
8
+ FROM information_schema.columns
9
+ WHERE table_name = 'timesheets'
10
+ ORDER BY ordinal_position;
11
+
12
+ [
13
+ {
14
+ "column_name": "id",
15
+ "data_type": "uuid",
16
+ "is_nullable": "NO",
17
+ "column_default": "gen_random_uuid()",
18
+ "character_maximum_length": null
19
+ },
20
+ {
21
+ "column_name": "user_id",
22
+ "data_type": "uuid",
23
+ "is_nullable": "NO",
24
+ "column_default": null,
25
+ "character_maximum_length": null
26
+ },
27
+ {
28
+ "column_name": "project_id",
29
+ "data_type": "uuid",
30
+ "is_nullable": "YES",
31
+ "column_default": null,
32
+ "character_maximum_length": null
33
+ },
34
+ {
35
+ "column_name": "work_date",
36
+ "data_type": "date",
37
+ "is_nullable": "NO",
38
+ "column_default": null,
39
+ "character_maximum_length": null
40
+ },
41
+ {
42
+ "column_name": "status",
43
+ "data_type": "USER-DEFINED",
44
+ "is_nullable": "NO",
45
+ "column_default": "'present'::timesheetstatus",
46
+ "character_maximum_length": null
47
+ },
48
+ {
49
+ "column_name": "check_in_time",
50
+ "data_type": "timestamp with time zone",
51
+ "is_nullable": "YES",
52
+ "column_default": null,
53
+ "character_maximum_length": null
54
+ },
55
+ {
56
+ "column_name": "check_out_time",
57
+ "data_type": "timestamp with time zone",
58
+ "is_nullable": "YES",
59
+ "column_default": null,
60
+ "character_maximum_length": null
61
+ },
62
+ {
63
+ "column_name": "hours_worked",
64
+ "data_type": "numeric",
65
+ "is_nullable": "YES",
66
+ "column_default": null,
67
+ "character_maximum_length": null
68
+ },
69
+ {
70
+ "column_name": "leave_reason",
71
+ "data_type": "text",
72
+ "is_nullable": "YES",
73
+ "column_default": null,
74
+ "character_maximum_length": null
75
+ },
76
+ {
77
+ "column_name": "leave_approved_by_user_id",
78
+ "data_type": "uuid",
79
+ "is_nullable": "YES",
80
+ "column_default": null,
81
+ "character_maximum_length": null
82
+ },
83
+ {
84
+ "column_name": "notes",
85
+ "data_type": "text",
86
+ "is_nullable": "YES",
87
+ "column_default": null,
88
+ "character_maximum_length": null
89
+ },
90
+ {
91
+ "column_name": "additional_metadata",
92
+ "data_type": "jsonb",
93
+ "is_nullable": "YES",
94
+ "column_default": "'{}'::jsonb",
95
+ "character_maximum_length": null
96
+ },
97
+ {
98
+ "column_name": "created_at",
99
+ "data_type": "timestamp with time zone",
100
+ "is_nullable": "YES",
101
+ "column_default": "timezone('utc'::text, now())",
102
+ "character_maximum_length": null
103
+ },
104
+ {
105
+ "column_name": "updated_at",
106
+ "data_type": "timestamp with time zone",
107
+ "is_nullable": "YES",
108
+ "column_default": "timezone('utc'::text, now())",
109
+ "character_maximum_length": null
110
+ },
111
+ {
112
+ "column_name": "deleted_at",
113
+ "data_type": "timestamp with time zone",
114
+ "is_nullable": "YES",
115
+ "column_default": null,
116
+ "character_maximum_length": null
117
+ },
118
+ {
119
+ "column_name": "tickets_assigned",
120
+ "data_type": "integer",
121
+ "is_nullable": "NO",
122
+ "column_default": "0",
123
+ "character_maximum_length": null
124
+ },
125
+ {
126
+ "column_name": "tickets_completed",
127
+ "data_type": "integer",
128
+ "is_nullable": "NO",
129
+ "column_default": "0",
130
+ "character_maximum_length": null
131
+ },
132
+ {
133
+ "column_name": "tickets_rescheduled",
134
+ "data_type": "integer",
135
+ "is_nullable": "NO",
136
+ "column_default": "0",
137
+ "character_maximum_length": null
138
+ },
139
+ {
140
+ "column_name": "tickets_cancelled",
141
+ "data_type": "integer",
142
+ "is_nullable": "NO",
143
+ "column_default": "0",
144
+ "character_maximum_length": null
145
+ },
146
+ {
147
+ "column_name": "tickets_rejected",
148
+ "data_type": "integer",
149
+ "is_nullable": "NO",
150
+ "column_default": "0",
151
+ "character_maximum_length": null
152
+ },
153
+ {
154
+ "column_name": "is_payroll_generated",
155
+ "data_type": "boolean",
156
+ "is_nullable": "NO",
157
+ "column_default": "false",
158
+ "character_maximum_length": null
159
+ },
160
+ {
161
+ "column_name": "payroll_id",
162
+ "data_type": "uuid",
163
+ "is_nullable": "YES",
164
+ "column_default": null,
165
+ "character_maximum_length": null
166
+ },
167
+ {
168
+ "column_name": "total_expenses",
169
+ "data_type": "numeric",
170
+ "is_nullable": "YES",
171
+ "column_default": "0",
172
+ "character_maximum_length": null
173
+ },
174
+ {
175
+ "column_name": "approved_expenses",
176
+ "data_type": "numeric",
177
+ "is_nullable": "YES",
178
+ "column_default": "0",
179
+ "character_maximum_length": null
180
+ },
181
+ {
182
+ "column_name": "pending_expenses",
183
+ "data_type": "numeric",
184
+ "is_nullable": "YES",
185
+ "column_default": "0",
186
+ "character_maximum_length": null
187
+ },
188
+ {
189
+ "column_name": "rejected_expenses",
190
+ "data_type": "numeric",
191
+ "is_nullable": "YES",
192
+ "column_default": "0",
193
+ "character_maximum_length": null
194
+ },
195
+ {
196
+ "column_name": "expense_claims_count",
197
+ "data_type": "integer",
198
+ "is_nullable": "YES",
199
+ "column_default": "0",
200
+ "character_maximum_length": null
201
+ },
202
+ {
203
+ "column_name": "reconciliation_run_id",
204
+ "data_type": "uuid",
205
+ "is_nullable": "YES",
206
+ "column_default": null,
207
+ "character_maximum_length": null
208
+ },
209
+ {
210
+ "column_name": "last_reconciled_at",
211
+ "data_type": "timestamp with time zone",
212
+ "is_nullable": "YES",
213
+ "column_default": null,
214
+ "character_maximum_length": null
215
+ },
216
+ {
217
+ "column_name": "update_source",
218
+ "data_type": "text",
219
+ "is_nullable": "YES",
220
+ "column_default": "'realtime'::text",
221
+ "character_maximum_length": null
222
+ },
223
+ {
224
+ "column_name": "last_realtime_update_at",
225
+ "data_type": "timestamp with time zone",
226
+ "is_nullable": "YES",
227
+ "column_default": null,
228
+ "character_maximum_length": null
229
+ },
230
+ {
231
+ "column_name": "last_validated_at",
232
+ "data_type": "timestamp with time zone",
233
+ "is_nullable": "YES",
234
+ "column_default": null,
235
+ "character_maximum_length": null
236
+ },
237
+ {
238
+ "column_name": "needs_review",
239
+ "data_type": "boolean",
240
+ "is_nullable": "YES",
241
+ "column_default": "false",
242
+ "character_maximum_length": null
243
+ },
244
+ {
245
+ "column_name": "discrepancy_notes",
246
+ "data_type": "text",
247
+ "is_nullable": "YES",
248
+ "column_default": null,
249
+ "character_maximum_length": null
250
+ },
251
+ {
252
+ "column_name": "version",
253
+ "data_type": "integer",
254
+ "is_nullable": "NO",
255
+ "column_default": "1",
256
+ "character_maximum_length": null
257
+ },
258
+ {
259
+ "column_name": "inventory_issued_count",
260
+ "data_type": "integer",
261
+ "is_nullable": "YES",
262
+ "column_default": "0",
263
+ "character_maximum_length": null
264
+ },
265
+ {
266
+ "column_name": "inventory_issued_value",
267
+ "data_type": "numeric",
268
+ "is_nullable": "YES",
269
+ "column_default": "0",
270
+ "character_maximum_length": null
271
+ },
272
+ {
273
+ "column_name": "inventory_installed_count",
274
+ "data_type": "integer",
275
+ "is_nullable": "YES",
276
+ "column_default": "0",
277
+ "character_maximum_length": null
278
+ },
279
+ {
280
+ "column_name": "inventory_consumed_count",
281
+ "data_type": "integer",
282
+ "is_nullable": "YES",
283
+ "column_default": "0",
284
+ "character_maximum_length": null
285
+ },
286
+ {
287
+ "column_name": "inventory_returned_count",
288
+ "data_type": "integer",
289
+ "is_nullable": "YES",
290
+ "column_default": "0",
291
+ "character_maximum_length": null
292
+ },
293
+ {
294
+ "column_name": "inventory_returned_value",
295
+ "data_type": "numeric",
296
+ "is_nullable": "YES",
297
+ "column_default": "0",
298
+ "character_maximum_length": null
299
+ },
300
+ {
301
+ "column_name": "inventory_lost_count",
302
+ "data_type": "integer",
303
+ "is_nullable": "YES",
304
+ "column_default": "0",
305
+ "character_maximum_length": null
306
+ },
307
+ {
308
+ "column_name": "inventory_damaged_count",
309
+ "data_type": "integer",
310
+ "is_nullable": "YES",
311
+ "column_default": "0",
312
+ "character_maximum_length": null
313
+ },
314
+ {
315
+ "column_name": "inventory_loss_value",
316
+ "data_type": "numeric",
317
+ "is_nullable": "YES",
318
+ "column_default": "0",
319
+ "character_maximum_length": null
320
+ },
321
+ {
322
+ "column_name": "inventory_on_hand_count",
323
+ "data_type": "integer",
324
+ "is_nullable": "YES",
325
+ "column_default": "0",
326
+ "character_maximum_length": null
327
+ },
328
+ {
329
+ "column_name": "inventory_on_hand_value",
330
+ "data_type": "numeric",
331
+ "is_nullable": "YES",
332
+ "column_default": "0",
333
+ "character_maximum_length": null
334
+ },
335
+ {
336
+ "column_name": "inventory_details",
337
+ "data_type": "jsonb",
338
+ "is_nullable": "YES",
339
+ "column_default": "'[]'::jsonb",
340
+ "character_maximum_length": null
341
+ }
342
+ ]
343
+
344
+ -- 2. Get all columns in reconciliation_runs table
345
+ SELECT
346
+ column_name,
347
+ data_type,
348
+ is_nullable,
349
+ column_default,
350
+ character_maximum_length
351
+ FROM information_schema.columns
352
+ WHERE table_name = 'reconciliation_runs'
353
+ ORDER BY ordinal_position;
354
+
355
+ [
356
+ {
357
+ "column_name": "id",
358
+ "data_type": "uuid",
359
+ "is_nullable": "NO",
360
+ "column_default": "gen_random_uuid()",
361
+ "character_maximum_length": null
362
+ },
363
+ {
364
+ "column_name": "project_id",
365
+ "data_type": "uuid",
366
+ "is_nullable": "NO",
367
+ "column_default": null,
368
+ "character_maximum_length": null
369
+ },
370
+ {
371
+ "column_name": "reconciliation_date",
372
+ "data_type": "date",
373
+ "is_nullable": "NO",
374
+ "column_default": null,
375
+ "character_maximum_length": null
376
+ },
377
+ {
378
+ "column_name": "run_type",
379
+ "data_type": "text",
380
+ "is_nullable": "NO",
381
+ "column_default": null,
382
+ "character_maximum_length": null
383
+ },
384
+ {
385
+ "column_name": "started_at",
386
+ "data_type": "timestamp with time zone",
387
+ "is_nullable": "NO",
388
+ "column_default": "timezone('utc'::text, now())",
389
+ "character_maximum_length": null
390
+ },
391
+ {
392
+ "column_name": "completed_at",
393
+ "data_type": "timestamp with time zone",
394
+ "is_nullable": "YES",
395
+ "column_default": null,
396
+ "character_maximum_length": null
397
+ },
398
+ {
399
+ "column_name": "status",
400
+ "data_type": "text",
401
+ "is_nullable": "NO",
402
+ "column_default": "'running'::text",
403
+ "character_maximum_length": null
404
+ },
405
+ {
406
+ "column_name": "user_ids",
407
+ "data_type": "ARRAY",
408
+ "is_nullable": "YES",
409
+ "column_default": null,
410
+ "character_maximum_length": null
411
+ },
412
+ {
413
+ "column_name": "agents_processed",
414
+ "data_type": "integer",
415
+ "is_nullable": "YES",
416
+ "column_default": "0",
417
+ "character_maximum_length": null
418
+ },
419
+ {
420
+ "column_name": "timesheets_created",
421
+ "data_type": "integer",
422
+ "is_nullable": "YES",
423
+ "column_default": "0",
424
+ "character_maximum_length": null
425
+ },
426
+ {
427
+ "column_name": "timesheets_updated",
428
+ "data_type": "integer",
429
+ "is_nullable": "YES",
430
+ "column_default": "0",
431
+ "character_maximum_length": null
432
+ },
433
+ {
434
+ "column_name": "assignments_processed",
435
+ "data_type": "integer",
436
+ "is_nullable": "YES",
437
+ "column_default": "0",
438
+ "character_maximum_length": null
439
+ },
440
+ {
441
+ "column_name": "expenses_processed",
442
+ "data_type": "integer",
443
+ "is_nullable": "YES",
444
+ "column_default": "0",
445
+ "character_maximum_length": null
446
+ },
447
+ {
448
+ "column_name": "execution_time_ms",
449
+ "data_type": "integer",
450
+ "is_nullable": "YES",
451
+ "column_default": null,
452
+ "character_maximum_length": null
453
+ },
454
+ {
455
+ "column_name": "query_time_ms",
456
+ "data_type": "integer",
457
+ "is_nullable": "YES",
458
+ "column_default": null,
459
+ "character_maximum_length": null
460
+ },
461
+ {
462
+ "column_name": "summary_stats",
463
+ "data_type": "jsonb",
464
+ "is_nullable": "YES",
465
+ "column_default": "'{}'::jsonb",
466
+ "character_maximum_length": null
467
+ },
468
+ {
469
+ "column_name": "anomalies_detected",
470
+ "data_type": "jsonb",
471
+ "is_nullable": "YES",
472
+ "column_default": "'[]'::jsonb",
473
+ "character_maximum_length": null
474
+ },
475
+ {
476
+ "column_name": "error_message",
477
+ "data_type": "text",
478
+ "is_nullable": "YES",
479
+ "column_default": null,
480
+ "character_maximum_length": null
481
+ },
482
+ {
483
+ "column_name": "error_details",
484
+ "data_type": "jsonb",
485
+ "is_nullable": "YES",
486
+ "column_default": null,
487
+ "character_maximum_length": null
488
+ },
489
+ {
490
+ "column_name": "triggered_by_user_id",
491
+ "data_type": "uuid",
492
+ "is_nullable": "YES",
493
+ "column_default": null,
494
+ "character_maximum_length": null
495
+ },
496
+ {
497
+ "column_name": "created_at",
498
+ "data_type": "timestamp with time zone",
499
+ "is_nullable": "YES",
500
+ "column_default": "timezone('utc'::text, now())",
501
+ "character_maximum_length": null
502
+ },
503
+ {
504
+ "column_name": "updated_at",
505
+ "data_type": "timestamp with time zone",
506
+ "is_nullable": "YES",
507
+ "column_default": "timezone('utc'::text, now())",
508
+ "character_maximum_length": null
509
+ },
510
+ {
511
+ "column_name": "deleted_at",
512
+ "data_type": "timestamp with time zone",
513
+ "is_nullable": "YES",
514
+ "column_default": null,
515
+ "character_maximum_length": null
516
+ },
517
+ {
518
+ "column_name": "discrepancies_found",
519
+ "data_type": "integer",
520
+ "is_nullable": "YES",
521
+ "column_default": "0",
522
+ "character_maximum_length": null
523
+ },
524
+ {
525
+ "column_name": "orphaned_records_found",
526
+ "data_type": "integer",
527
+ "is_nullable": "YES",
528
+ "column_default": "0",
529
+ "character_maximum_length": null
530
+ },
531
+ {
532
+ "column_name": "corrections_made",
533
+ "data_type": "integer",
534
+ "is_nullable": "YES",
535
+ "column_default": "0",
536
+ "character_maximum_length": null
537
+ },
538
+ {
539
+ "column_name": "discrepancy_details",
540
+ "data_type": "jsonb",
541
+ "is_nullable": "YES",
542
+ "column_default": "'[]'::jsonb",
543
+ "character_maximum_length": null
544
+ }
545
+ ]
546
+
547
+ -- 3. Get all columns in timesheet_updates table
548
+ SELECT
549
+ column_name,
550
+ data_type,
551
+ is_nullable,
552
+ column_default,
553
+ character_maximum_length
554
+ FROM information_schema.columns
555
+ WHERE table_name = 'timesheet_updates'
556
+ ORDER BY ordinal_position;
557
+
558
+ [
559
+ {
560
+ "column_name": "id",
561
+ "data_type": "uuid",
562
+ "is_nullable": "NO",
563
+ "column_default": "gen_random_uuid()",
564
+ "character_maximum_length": null
565
+ },
566
+ {
567
+ "column_name": "timesheet_id",
568
+ "data_type": "uuid",
569
+ "is_nullable": "NO",
570
+ "column_default": null,
571
+ "character_maximum_length": null
572
+ },
573
+ {
574
+ "column_name": "trigger_type",
575
+ "data_type": "text",
576
+ "is_nullable": "NO",
577
+ "column_default": null,
578
+ "character_maximum_length": null
579
+ },
580
+ {
581
+ "column_name": "trigger_entity_type",
582
+ "data_type": "text",
583
+ "is_nullable": "YES",
584
+ "column_default": null,
585
+ "character_maximum_length": null
586
+ },
587
+ {
588
+ "column_name": "trigger_entity_id",
589
+ "data_type": "uuid",
590
+ "is_nullable": "YES",
591
+ "column_default": null,
592
+ "character_maximum_length": null
593
+ },
594
+ {
595
+ "column_name": "fields_changed",
596
+ "data_type": "jsonb",
597
+ "is_nullable": "NO",
598
+ "column_default": "'{}'::jsonb",
599
+ "character_maximum_length": null
600
+ },
601
+ {
602
+ "column_name": "updated_by_user_id",
603
+ "data_type": "uuid",
604
+ "is_nullable": "YES",
605
+ "column_default": null,
606
+ "character_maximum_length": null
607
+ },
608
+ {
609
+ "column_name": "updated_at",
610
+ "data_type": "timestamp with time zone",
611
+ "is_nullable": "YES",
612
+ "column_default": "now()",
613
+ "character_maximum_length": null
614
+ },
615
+ {
616
+ "column_name": "additional_metadata",
617
+ "data_type": "jsonb",
618
+ "is_nullable": "YES",
619
+ "column_default": "'{}'::jsonb",
620
+ "character_maximum_length": null
621
+ },
622
+ {
623
+ "column_name": "created_at",
624
+ "data_type": "timestamp with time zone",
625
+ "is_nullable": "YES",
626
+ "column_default": "now()",
627
+ "character_maximum_length": null
628
+ }
629
+ ]
630
+
631
+ -- 4. Get all indexes on timesheets
632
+ SELECT
633
+ indexname,
634
+ indexdef
635
+ FROM pg_indexes
636
+ WHERE tablename = 'timesheets'
637
+ ORDER BY indexname;
638
+
639
+ [
640
+ {
641
+ "indexname": "idx_timesheets_inventory_details_gin",
642
+ "indexdef": "CREATE INDEX idx_timesheets_inventory_details_gin ON public.timesheets USING gin (inventory_details)"
643
+ },
644
+ {
645
+ "indexname": "idx_timesheets_inventory_loss",
646
+ "indexdef": "CREATE INDEX idx_timesheets_inventory_loss ON public.timesheets USING btree (work_date, inventory_lost_count, inventory_damaged_count) WHERE (((inventory_lost_count > 0) OR (inventory_damaged_count > 0)) AND (deleted_at IS NULL))"
647
+ },
648
+ {
649
+ "indexname": "idx_timesheets_inventory_on_hand",
650
+ "indexdef": "CREATE INDEX idx_timesheets_inventory_on_hand ON public.timesheets USING btree (user_id, work_date, inventory_on_hand_count) WHERE ((inventory_on_hand_count > 0) AND (deleted_at IS NULL))"
651
+ },
652
+ {
653
+ "indexname": "idx_timesheets_metadata_gin",
654
+ "indexdef": "CREATE INDEX idx_timesheets_metadata_gin ON public.timesheets USING gin (additional_metadata)"
655
+ },
656
+ {
657
+ "indexname": "idx_timesheets_needs_review",
658
+ "indexdef": "CREATE INDEX idx_timesheets_needs_review ON public.timesheets USING btree (needs_review, last_validated_at) WHERE ((needs_review = true) AND (deleted_at IS NULL))"
659
+ },
660
+ {
661
+ "indexname": "idx_timesheets_payroll",
662
+ "indexdef": "CREATE INDEX idx_timesheets_payroll ON public.timesheets USING btree (payroll_id) WHERE (payroll_id IS NOT NULL)"
663
+ },
664
+ {
665
+ "indexname": "idx_timesheets_payroll_status",
666
+ "indexdef": "CREATE INDEX idx_timesheets_payroll_status ON public.timesheets USING btree (user_id, is_payroll_generated, work_date DESC)"
667
+ },
668
+ {
669
+ "indexname": "idx_timesheets_project",
670
+ "indexdef": "CREATE INDEX idx_timesheets_project ON public.timesheets USING btree (project_id, work_date DESC) WHERE (deleted_at IS NULL)"
671
+ },
672
+ {
673
+ "indexname": "idx_timesheets_project_date",
674
+ "indexdef": "CREATE INDEX idx_timesheets_project_date ON public.timesheets USING btree (project_id, work_date DESC) WHERE (deleted_at IS NULL)"
675
+ },
676
+ {
677
+ "indexname": "idx_timesheets_realtime_updates",
678
+ "indexdef": "CREATE INDEX idx_timesheets_realtime_updates ON public.timesheets USING btree (user_id, work_date, last_realtime_update_at DESC) WHERE (deleted_at IS NULL)"
679
+ },
680
+ {
681
+ "indexname": "idx_timesheets_reconciliation_run",
682
+ "indexdef": "CREATE INDEX idx_timesheets_reconciliation_run ON public.timesheets USING btree (reconciliation_run_id) WHERE (deleted_at IS NULL)"
683
+ },
684
+ {
685
+ "indexname": "idx_timesheets_ticket_metrics",
686
+ "indexdef": "CREATE INDEX idx_timesheets_ticket_metrics ON public.timesheets USING btree (work_date, tickets_completed) WHERE (tickets_completed > 0)"
687
+ },
688
+ {
689
+ "indexname": "idx_timesheets_unique",
690
+ "indexdef": "CREATE UNIQUE INDEX idx_timesheets_unique ON public.timesheets USING btree (user_id, project_id, work_date) WHERE (deleted_at IS NULL)"
691
+ },
692
+ {
693
+ "indexname": "idx_timesheets_user",
694
+ "indexdef": "CREATE INDEX idx_timesheets_user ON public.timesheets USING btree (user_id, work_date DESC) WHERE (deleted_at IS NULL)"
695
+ },
696
+ {
697
+ "indexname": "idx_timesheets_user_date_unique",
698
+ "indexdef": "CREATE UNIQUE INDEX idx_timesheets_user_date_unique ON public.timesheets USING btree (user_id, work_date) WHERE (deleted_at IS NULL)"
699
+ },
700
+ {
701
+ "indexname": "timesheets_pkey",
702
+ "indexdef": "CREATE UNIQUE INDEX timesheets_pkey ON public.timesheets USING btree (id)"
703
+ }
704
+ ]
705
+
706
+ -- 5. Get all indexes on reconciliation_runs
707
+ SELECT
708
+ indexname,
709
+ indexdef
710
+ FROM pg_indexes
711
+ WHERE tablename = 'reconciliation_runs'
712
+ ORDER BY indexname;
713
+
714
+ [
715
+ {
716
+ "indexname": "idx_reconciliation_runs_anomalies_gin",
717
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_anomalies_gin ON public.reconciliation_runs USING gin (anomalies_detected)"
718
+ },
719
+ {
720
+ "indexname": "idx_reconciliation_runs_discrepancies",
721
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_discrepancies ON public.reconciliation_runs USING btree (discrepancies_found, reconciliation_date DESC) WHERE (discrepancies_found > 0)"
722
+ },
723
+ {
724
+ "indexname": "idx_reconciliation_runs_project_date",
725
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_project_date ON public.reconciliation_runs USING btree (project_id, reconciliation_date DESC) WHERE (deleted_at IS NULL)"
726
+ },
727
+ {
728
+ "indexname": "idx_reconciliation_runs_status",
729
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_status ON public.reconciliation_runs USING btree (status, started_at DESC) WHERE (deleted_at IS NULL)"
730
+ },
731
+ {
732
+ "indexname": "idx_reconciliation_runs_summary_gin",
733
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_summary_gin ON public.reconciliation_runs USING gin (summary_stats)"
734
+ },
735
+ {
736
+ "indexname": "idx_reconciliation_runs_triggered_by",
737
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_triggered_by ON public.reconciliation_runs USING btree (triggered_by_user_id, started_at DESC) WHERE (deleted_at IS NULL)"
738
+ },
739
+ {
740
+ "indexname": "idx_reconciliation_runs_unique_active_run",
741
+ "indexdef": "CREATE UNIQUE INDEX idx_reconciliation_runs_unique_active_run ON public.reconciliation_runs USING btree (project_id, reconciliation_date, status) WHERE ((status = 'running'::text) AND (deleted_at IS NULL))"
742
+ },
743
+ {
744
+ "indexname": "reconciliation_runs_pkey",
745
+ "indexdef": "CREATE UNIQUE INDEX reconciliation_runs_pkey ON public.reconciliation_runs USING btree (id)"
746
+ }
747
+ ]
748
+
749
+ -- 6. Get all indexes on timesheet_updates
750
+ SELECT
751
+ indexname,
752
+ indexdef
753
+ FROM pg_indexes
754
+ WHERE tablename = 'timesheet_updates'
755
+ ORDER BY indexname;
756
+
757
+ [
758
+ {
759
+ "indexname": "idx_timesheet_updates_entity",
760
+ "indexdef": "CREATE INDEX idx_timesheet_updates_entity ON public.timesheet_updates USING btree (trigger_entity_type, trigger_entity_id) WHERE (trigger_entity_id IS NOT NULL)"
761
+ },
762
+ {
763
+ "indexname": "idx_timesheet_updates_fields_gin",
764
+ "indexdef": "CREATE INDEX idx_timesheet_updates_fields_gin ON public.timesheet_updates USING gin (fields_changed)"
765
+ },
766
+ {
767
+ "indexname": "idx_timesheet_updates_timesheet",
768
+ "indexdef": "CREATE INDEX idx_timesheet_updates_timesheet ON public.timesheet_updates USING btree (timesheet_id, updated_at DESC)"
769
+ },
770
+ {
771
+ "indexname": "idx_timesheet_updates_trigger",
772
+ "indexdef": "CREATE INDEX idx_timesheet_updates_trigger ON public.timesheet_updates USING btree (trigger_type, updated_at DESC)"
773
+ },
774
+ {
775
+ "indexname": "timesheet_updates_pkey",
776
+ "indexdef": "CREATE UNIQUE INDEX timesheet_updates_pkey ON public.timesheet_updates USING btree (id)"
777
+ }
778
+ ]
779
+
780
+ -- 7. Get all constraints on timesheets
781
+ SELECT
782
+ conname AS constraint_name,
783
+ contype AS constraint_type,
784
+ pg_get_constraintdef(oid) AS constraint_definition
785
+ FROM pg_constraint
786
+ WHERE conrelid = 'timesheets'::regclass
787
+ ORDER BY conname;
788
+
789
+ [
790
+ {
791
+ "constraint_name": "chk_check_times",
792
+ "constraint_type": "c",
793
+ "constraint_definition": "CHECK (((check_out_time IS NULL) OR (check_in_time IS NULL) OR (check_out_time >= check_in_time)))"
794
+ },
795
+ {
796
+ "constraint_name": "chk_valid_hours",
797
+ "constraint_type": "c",
798
+ "constraint_definition": "CHECK (((hours_worked IS NULL) OR ((hours_worked >= (0)::numeric) AND (hours_worked <= (24)::numeric))))"
799
+ },
800
+ {
801
+ "constraint_name": "timesheets_approved_expenses_check",
802
+ "constraint_type": "c",
803
+ "constraint_definition": "CHECK ((approved_expenses >= (0)::numeric))"
804
+ },
805
+ {
806
+ "constraint_name": "timesheets_expense_claims_count_check",
807
+ "constraint_type": "c",
808
+ "constraint_definition": "CHECK ((expense_claims_count >= 0))"
809
+ },
810
+ {
811
+ "constraint_name": "timesheets_inventory_consumed_count_check",
812
+ "constraint_type": "c",
813
+ "constraint_definition": "CHECK ((inventory_consumed_count >= 0))"
814
+ },
815
+ {
816
+ "constraint_name": "timesheets_inventory_damaged_count_check",
817
+ "constraint_type": "c",
818
+ "constraint_definition": "CHECK ((inventory_damaged_count >= 0))"
819
+ },
820
+ {
821
+ "constraint_name": "timesheets_inventory_installed_count_check",
822
+ "constraint_type": "c",
823
+ "constraint_definition": "CHECK ((inventory_installed_count >= 0))"
824
+ },
825
+ {
826
+ "constraint_name": "timesheets_inventory_issued_count_check",
827
+ "constraint_type": "c",
828
+ "constraint_definition": "CHECK ((inventory_issued_count >= 0))"
829
+ },
830
+ {
831
+ "constraint_name": "timesheets_inventory_issued_value_check",
832
+ "constraint_type": "c",
833
+ "constraint_definition": "CHECK ((inventory_issued_value >= (0)::numeric))"
834
+ },
835
+ {
836
+ "constraint_name": "timesheets_inventory_loss_value_check",
837
+ "constraint_type": "c",
838
+ "constraint_definition": "CHECK ((inventory_loss_value >= (0)::numeric))"
839
+ },
840
+ {
841
+ "constraint_name": "timesheets_inventory_lost_count_check",
842
+ "constraint_type": "c",
843
+ "constraint_definition": "CHECK ((inventory_lost_count >= 0))"
844
+ },
845
+ {
846
+ "constraint_name": "timesheets_inventory_on_hand_count_check",
847
+ "constraint_type": "c",
848
+ "constraint_definition": "CHECK ((inventory_on_hand_count >= 0))"
849
+ },
850
+ {
851
+ "constraint_name": "timesheets_inventory_on_hand_value_check",
852
+ "constraint_type": "c",
853
+ "constraint_definition": "CHECK ((inventory_on_hand_value >= (0)::numeric))"
854
+ },
855
+ {
856
+ "constraint_name": "timesheets_inventory_returned_count_check",
857
+ "constraint_type": "c",
858
+ "constraint_definition": "CHECK ((inventory_returned_count >= 0))"
859
+ },
860
+ {
861
+ "constraint_name": "timesheets_inventory_returned_value_check",
862
+ "constraint_type": "c",
863
+ "constraint_definition": "CHECK ((inventory_returned_value >= (0)::numeric))"
864
+ },
865
+ {
866
+ "constraint_name": "timesheets_leave_approved_by_user_id_fkey",
867
+ "constraint_type": "f",
868
+ "constraint_definition": "FOREIGN KEY (leave_approved_by_user_id) REFERENCES users(id) ON DELETE SET NULL"
869
+ },
870
+ {
871
+ "constraint_name": "timesheets_payroll_id_fkey",
872
+ "constraint_type": "f",
873
+ "constraint_definition": "FOREIGN KEY (payroll_id) REFERENCES user_payroll(id) ON DELETE SET NULL"
874
+ },
875
+ {
876
+ "constraint_name": "timesheets_pending_expenses_check",
877
+ "constraint_type": "c",
878
+ "constraint_definition": "CHECK ((pending_expenses >= (0)::numeric))"
879
+ },
880
+ {
881
+ "constraint_name": "timesheets_pkey",
882
+ "constraint_type": "p",
883
+ "constraint_definition": "PRIMARY KEY (id)"
884
+ },
885
+ {
886
+ "constraint_name": "timesheets_project_id_fkey",
887
+ "constraint_type": "f",
888
+ "constraint_definition": "FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE"
889
+ },
890
+ {
891
+ "constraint_name": "timesheets_reconciliation_run_id_fkey",
892
+ "constraint_type": "f",
893
+ "constraint_definition": "FOREIGN KEY (reconciliation_run_id) REFERENCES reconciliation_runs(id) ON DELETE SET NULL"
894
+ },
895
+ {
896
+ "constraint_name": "timesheets_rejected_expenses_check",
897
+ "constraint_type": "c",
898
+ "constraint_definition": "CHECK ((rejected_expenses >= (0)::numeric))"
899
+ },
900
+ {
901
+ "constraint_name": "timesheets_total_expenses_check",
902
+ "constraint_type": "c",
903
+ "constraint_definition": "CHECK ((total_expenses >= (0)::numeric))"
904
+ },
905
+ {
906
+ "constraint_name": "timesheets_update_source_check",
907
+ "constraint_type": "c",
908
+ "constraint_definition": "CHECK ((update_source = ANY (ARRAY['realtime'::text, 'scheduled'::text, 'manual'::text])))"
909
+ },
910
+ {
911
+ "constraint_name": "timesheets_user_id_fkey",
912
+ "constraint_type": "f",
913
+ "constraint_definition": "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"
914
+ }
915
+ ]
916
+
917
+ -- 8. Check if specific columns exist (quick verification)
918
+ SELECT
919
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'total_expenses') as has_total_expenses,
920
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'approved_expenses') as has_approved_expenses,
921
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'pending_expenses') as has_pending_expenses,
922
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'rejected_expenses') as has_rejected_expenses,
923
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'expense_claims_count') as has_expense_claims_count,
924
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'update_source') as has_update_source,
925
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'last_realtime_update_at') as has_last_realtime_update_at,
926
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'check_in_time') as has_check_in_time,
927
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'clock_in_time') as has_clock_in_time,
928
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'inventory_on_hand_count') as has_inventory_on_hand_count,
929
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'inventory_issued_count') as has_inventory_issued_count;
930
+
931
+ [
932
+ {
933
+ "has_total_expenses": true,
934
+ "has_approved_expenses": true,
935
+ "has_pending_expenses": true,
936
+ "has_rejected_expenses": true,
937
+ "has_expense_claims_count": true,
938
+ "has_update_source": true,
939
+ "has_last_realtime_update_at": true,
940
+ "has_check_in_time": true,
941
+ "has_clock_in_time": false,
942
+ "has_inventory_on_hand_count": true,
943
+ "has_inventory_issued_count": true
944
+ }
945
+ ]
docs/devlogs/db/logs.json CHANGED
@@ -1 +1,945 @@
1
- [{"idx":0,"id":"20772cb1-ec31-41dc-9cb0-73649dc6ac55","ticket_id":"70090c47-e9c1-4b0a-add4-69bec53d92f9","user_id":"43b778b0-2062-4724-abbb-916a4835a9b0","action":"completed","assigned_by_user_id":"43b778b0-2062-4724-abbb-916a4835a9b0","is_self_assigned":true,"execution_order":null,"planned_start_time":null,"assigned_at":"2025-12-03 08:04:10.106149+00","responded_at":"2025-12-03 08:04:10.106152+00","journey_started_at":"2025-12-03 08:04:25.130305+00","arrived_at":"2025-12-03 08:05:00.662165+00","ended_at":"2025-12-09 06:11:34.948803+00","journey_start_latitude":-1.2200548,"journey_start_longitude":36.876972,"arrival_latitude":-1.2200534,"arrival_longitude":36.876971,"arrival_verified":false,"journey_location_history":"[{\"lat\": -1.2200548, \"lng\": 36.8769751, \"speed\": 0.0, \"battery\": 62, \"network\": \"3g\", \"accuracy\": 20.0, \"timestamp\": \"2025-12-03T08:04:29.012678\"}, {\"lat\": -1.2200534, \"lng\": 36.876971, \"speed\": 0.0, \"battery\": 62, \"network\": \"3g\", \"accuracy\": 20.0, \"timestamp\": \"2025-12-03T08:05:00.310663\"}]","reason":null,"notes":"[COMPLETED] well done\n","created_at":"2025-12-03 08:04:10.113266+00","updated_at":"2025-12-09 06:11:35.012778+00","deleted_at":null},{"idx":1,"id":"34c2a077-5630-4af8-843d-e125c2497267","ticket_id":"2de41ce7-dff1-4151-9710-87958d18b5c4","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-12-03 11:26:09.556998+00","responded_at":"2025-12-03 11:26:09.557001+00","journey_started_at":"2025-12-03 11:26:46.746692+00","arrived_at":"2025-12-03 12:18:09.241003+00","ended_at":null,"journey_start_latitude":-1.21995899256506,"journey_start_longitude":36.8769753048327,"arrival_latitude":-1.219888,"arrival_longitude":36.877013,"arrival_verified":false,"journey_location_history":"[{\"lat\": -1.2199589925650558, \"lng\": 36.87697530483272, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 85.0, \"timestamp\": \"2025-12-03T11:26:48.178288\"}, {\"lat\": -1.2199589925650558, \"lng\": 36.87697530483272, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 85.0, \"timestamp\": \"2025-12-03T11:27:21.327980\"}, {\"lat\": -1.2200231002547879, \"lng\": 36.8771629705233, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 68.0, \"timestamp\": \"2025-12-03T11:27:50.652389\"}, {\"lat\": -1.2200231002547879, \"lng\": 36.8771629705233, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 68.0, \"timestamp\": \"2025-12-03T11:28:22.992295\"}, {\"lat\": -1.220109, \"lng\": 36.8774065, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 99.0, \"timestamp\": \"2025-12-03T11:43:06.954957\"}, {\"lat\": -1.220109, \"lng\": 36.8774065, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 99.0, \"timestamp\": \"2025-12-03T11:43:38.608213\"}, {\"lat\": -1.220109, \"lng\": 36.8774065, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 99.0, \"timestamp\": \"2025-12-03T11:44:10.707565\"}, {\"lat\": -1.220109, \"lng\": 36.8774065, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 99.0, \"timestamp\": \"2025-12-03T11:44:38.555332\"}, {\"lat\": -1.219888, \"lng\": 36.877013, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 149.0, \"timestamp\": \"2025-12-03T11:45:08.446621\"}, {\"lat\": -1.2200648441710604, \"lng\": 36.8773285837727, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 92.0, \"timestamp\": \"2025-12-03T11:45:38.963983\"}, {\"lat\": -1.2200648441710604, \"lng\": 36.8773285837727, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 92.0, \"timestamp\": \"2025-12-03T11:46:08.085772\"}, {\"lat\": -1.2200648441710604, \"lng\": 36.8773285837727, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 92.0, \"timestamp\": \"2025-12-03T11:46:41.028458\"}, {\"lat\": -1.2200648441710604, \"lng\": 36.8773285837727, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 92.0, \"timestamp\": \"2025-12-03T11:47:18.306843\"}, {\"lat\": -1.2198873388753058, \"lng\": 36.877013, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 114.0, \"timestamp\": \"2025-12-03T11:48:18.534400\"}, {\"lat\": -1.22001920394653, \"lng\": 36.87720764186818, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 92.0, \"timestamp\": \"2025-12-03T11:49:19.458742\"}, {\"lat\": -1.22003489286718, \"lng\": 36.8771952558782, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 92.0, \"timestamp\": \"2025-12-03T11:50:18.361195\"}, {\"lat\": -1.22003489286718, \"lng\": 36.8771952558782, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 92.0, \"timestamp\": \"2025-12-03T11:51:00.540478\"}, {\"lat\": -1.219888, \"lng\": 36.877013, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 149.0, \"timestamp\": \"2025-12-03T12:05:23.087122\"}, {\"lat\": -1.2201036936940906, \"lng\": 36.87739493481981, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 81.0, \"timestamp\": \"2025-12-03T12:05:50.078766\"}, {\"lat\": -1.2201036936940906, \"lng\": 36.87739493481981, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 81.0, \"timestamp\": \"2025-12-03T12:06:20.217655\"}, {\"lat\": -1.2201036936940906, \"lng\": 36.87739493481981, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 81.0, \"timestamp\": \"2025-12-03T12:06:52.207598\"}, {\"lat\": -1.2201036936940906, \"lng\": 36.87739493481981, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 81.0, \"timestamp\": \"2025-12-03T12:07:19.342098\"}, {\"lat\": -1.2201036936940906, \"lng\": 36.87739493481981, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 81.0, \"timestamp\": \"2025-12-03T12:07:48.987004\"}, {\"lat\": -1.2200341085574573, \"lng\": 36.87726984694376, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 114.0, \"timestamp\": \"2025-12-03T12:08:19.276546\"}, {\"lat\": -1.2200341085574573, \"lng\": 36.87726984694376, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 114.0, \"timestamp\": \"2025-12-03T12:08:49.316274\"}, {\"lat\": -1.2201656877323421, \"lng\": 36.87751371747212, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 85.0, \"timestamp\": \"2025-12-03T12:10:01.727248\"}, {\"lat\": -1.2201656877323421, \"lng\": 36.87751371747212, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 85.0, \"timestamp\": \"2025-12-03T12:10:32.749087\"}, {\"lat\": -1.2201656877323421, \"lng\": 36.87751371747212, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 85.0, \"timestamp\": \"2025-12-03T12:10:49.035657\"}, {\"lat\": -1.2200968236467173, \"lng\": 36.8773399129426, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 79.0, \"timestamp\": \"2025-12-03T12:11:19.244816\"}, {\"lat\": -1.2200968236467173, \"lng\": 36.8773399129426, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 79.0, \"timestamp\": \"2025-12-03T12:11:49.325134\"}, {\"lat\": -1.2200968236467173, \"lng\": 36.8773399129426, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 79.0, \"timestamp\": \"2025-12-03T12:12:22.640713\"}, {\"lat\": -1.220083681913554, \"lng\": 36.877307709819554, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 87.0, \"timestamp\": \"2025-12-03T12:12:49.255484\"}, {\"lat\": -1.220195055324658, \"lng\": 36.877557804952716, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 85.0, \"timestamp\": \"2025-12-03T12:13:27.145805\"}, {\"lat\": -1.220195055324658, \"lng\": 36.877557804952716, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 85.0, \"timestamp\": \"2025-12-03T12:13:57.449517\"}, {\"lat\": -1.2200767183872938, \"lng\": 36.87734831582162, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 107.0, \"timestamp\": \"2025-12-03T12:14:30.021486\"}, {\"lat\": -1.2200767183872938, \"lng\": 36.87734831582162, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 107.0, \"timestamp\": \"2025-12-03T12:14:57.364315\"}, {\"lat\": -1.219888, \"lng\": 36.877013, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 149.0, \"timestamp\": \"2025-12-03T12:15:27.675161\"}, {\"lat\": -1.219888, \"lng\": 36.877013, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 149.0, \"timestamp\": \"2025-12-03T12:15:57.396948\"}, {\"lat\": -1.219888, \"lng\": 36.877013, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 149.0, \"timestamp\": \"2025-12-03T12:16:27.477932\"}, {\"lat\": -1.219888, \"lng\": 36.877013, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 149.0, \"timestamp\": \"2025-12-03T12:16:59.376149\"}, {\"lat\": -1.219888, \"lng\": 36.877013, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 149.0, \"timestamp\": \"2025-12-03T12:17:26.486481\"}, {\"lat\": -1.219888, \"lng\": 36.877013, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 149.0, \"timestamp\": \"2025-12-03T12:17:57.103096\"}]","reason":null,"notes":null,"created_at":"2025-12-03 11:26:09.562514+00","updated_at":"2025-12-03 12:18:09.242009+00","deleted_at":null},{"idx":2,"id":"69a2a4f6-f72a-425c-9f90-4088e074c13b","ticket_id":"1f807cf8-f139-421b-86e3-38c2f8bc7070","user_id":"43b778b0-2062-4724-abbb-916a4835a9b0","action":"dropped","assigned_by_user_id":"43b778b0-2062-4724-abbb-916a4835a9b0","is_self_assigned":true,"execution_order":null,"planned_start_time":null,"assigned_at":"2025-12-02 14:12:10.05599+00","responded_at":"2025-12-02 14:12:10.055992+00","journey_started_at":"2025-12-02 14:12:27.929722+00","arrived_at":"2025-12-02 14:45:50.181161+00","ended_at":"2025-12-03 11:59:40.294207+00","journey_start_latitude":-1.2200122,"journey_start_longitude":36.8770178,"arrival_latitude":-1.2192414,"arrival_longitude":36.8907296,"arrival_verified":false,"journey_location_history":"[{\"lat\": -1.2194135, \"lng\": 36.8775907, \"speed\": 0.0, \"battery\": 56, \"network\": \"4g\", \"accuracy\": 34.61399841308594, \"timestamp\": \"2025-12-02T14:12:31.527583\"}, {\"lat\": -1.2194928, \"lng\": 36.8775066, \"speed\": 0.0, \"battery\": 55, \"network\": \"4g\", \"accuracy\": 26.85700035095215, \"timestamp\": \"2025-12-02T14:13:00.622699\"}, {\"lat\": -1.2195889, \"lng\": 36.8776923, \"speed\": 0.0, \"battery\": 55, \"network\": \"4g\", \"accuracy\": 30.481000900268555, \"timestamp\": \"2025-12-02T14:13:30.323994\"}, {\"lat\": -1.2199054, \"lng\": 36.8783171, \"speed\": 0.0, \"battery\": 55, \"network\": \"4g\", \"accuracy\": 36.08300018310547, \"timestamp\": \"2025-12-02T14:14:00.533225\"}, {\"lat\": -1.2200001, \"lng\": 36.8784442, \"speed\": 0.0, \"battery\": 55, \"network\": \"4g\", \"accuracy\": 18.518999099731445, \"timestamp\": \"2025-12-02T14:14:33.007440\"}, {\"lat\": -1.2201898, \"lng\": 36.8806725, \"speed\": 0.0, \"battery\": 55, \"network\": \"4g\", \"accuracy\": 20.899999618530273, \"timestamp\": \"2025-12-02T14:19:29.029786\"}, {\"lat\": -1.2202622, \"lng\": 36.8808323, \"speed\": 0.0, \"battery\": 55, \"network\": \"4g\", \"accuracy\": 12.71399974822998, \"timestamp\": \"2025-12-02T14:20:21.598297\"}, {\"lat\": -1.2204048, \"lng\": 36.8814195, \"speed\": 1.7677345275878906, \"battery\": 55, \"network\": \"4g\", \"accuracy\": 12.756999969482422, \"timestamp\": \"2025-12-02T14:20:51.373114\"}, {\"lat\": -1.2203779, \"lng\": 36.881459, \"speed\": 0.0, \"battery\": 55, \"network\": \"4g\", \"accuracy\": 30.0, \"timestamp\": \"2025-12-02T14:21:21.198022\"}, {\"lat\": -1.2187483, \"lng\": 36.8871833, \"speed\": 0.0, \"battery\": 54, \"network\": \"4g\", \"accuracy\": 12.199999809265137, \"timestamp\": \"2025-12-02T14:35:01.049929\"}, {\"lat\": -1.2188454, \"lng\": 36.8875974, \"speed\": 0.0, \"battery\": 54, \"network\": \"4g\", \"accuracy\": 18.856000900268555, \"timestamp\": \"2025-12-02T14:35:24.327865\"}, {\"lat\": -1.2188761, \"lng\": 36.8876456, \"speed\": 0.0, \"battery\": 54, \"network\": \"4g\", \"accuracy\": 15.873000144958496, \"timestamp\": \"2025-12-02T14:35:53.867957\"}, {\"lat\": -1.2188681, \"lng\": 36.8876746, \"speed\": 0.0, \"battery\": 54, \"network\": \"4g\", \"accuracy\": 19.760000228881836, \"timestamp\": \"2025-12-02T14:36:23.507366\"}, {\"lat\": -1.2192414, \"lng\": 36.8907296, \"speed\": 3.572157597541809, \"battery\": 53, \"network\": \"4g\", \"accuracy\": 72.9000015258789, \"timestamp\": \"2025-12-02T14:45:50.097141\"}]","reason":"[cancellation] opted for another installer","notes":null,"created_at":"2025-12-02 14:12:10.058135+00","updated_at":"2025-12-03 11:59:40.358261+00","deleted_at":null},{"idx":3,"id":"a82a3824-f4f1-4283-a2e3-8c348dbb28ce","ticket_id":"f59b29fc-d0b9-4618-b0d1-889e340da612","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-30 10:21:10.085152+00","responded_at":"2025-11-30 10:21:10.085155+00","journey_started_at":"2025-11-30 10:55:39.250857+00","arrived_at":"2025-11-30 10:56:11.929093+00","ended_at":"2025-12-01 05:43:37.183538+00","journey_start_latitude":-1.10052333333333,"journey_start_longitude":37.0092266666667,"arrival_latitude":-1.10056144256674,"arrival_longitude":37.0092363654176,"arrival_verified":false,"journey_location_history":"[{\"lat\": -1.100523333333333, \"lng\": 37.00922666666666, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 64.0, \"timestamp\": \"2025-11-30T10:55:40.399050\"}, {\"lat\": -1.1005614425667403, \"lng\": 37.009236365417586, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 65.0, \"timestamp\": \"2025-11-30T10:56:11.682201\"}]","reason":null,"notes":null,"created_at":"2025-11-30 10:21:10.111327+00","updated_at":"2025-11-30 10:56:11.929936+00","deleted_at":null},{"idx":4,"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":"2025-11-28 10:04:37.590417+00","arrived_at":"2025-11-28 10:45:04.045672+00","ended_at":"2025-12-01 05:45:37.183538+00","journey_start_latitude":-1.22005074013012,"journey_start_longitude":36.8772529852395,"arrival_latitude":-1.22018863567581,"arrival_longitude":36.8775512279948,"arrival_verified":false,"journey_location_history":"[{\"lat\": -1.2201086694376528, \"lng\": 36.87740484718826, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 87.0, \"timestamp\": \"2025-11-28T10:34:41.638069\"}, {\"lat\": -1.2201371818782953, \"lng\": 36.87746013754867, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 68.0, \"timestamp\": \"2025-11-28T10:36:56.645310\"}, {\"lat\": -1.220034831373305, \"lng\": 36.877274425758436, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 81.0, \"timestamp\": \"2025-11-28T10:38:04.419568\"}, {\"lat\": -1.2200648441710604, \"lng\": 36.8773285837727, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 92.0, \"timestamp\": \"2025-11-28T10:44:42.461237\"}, {\"lat\": -1.220188635675814, \"lng\": 36.87755122799482, \"speed\": 0.0, \"battery\": 100, \"network\": null, \"accuracy\": 75.0, \"timestamp\": \"2025-11-28T10:44:57.478127\"}]","reason":null,"notes":null,"created_at":"2025-11-28 09:27:41.997778+00","updated_at":"2025-11-28 10:45:04.049575+00","deleted_at":null},{"idx":5,"id":"d6f25868-5117-4b85-8bb3-44314144ef6e","ticket_id":"0fd3ee15-5e7d-465a-b377-155f9bdb7e70","user_id":"43b778b0-2062-4724-abbb-916a4835a9b0","action":"completed","assigned_by_user_id":"43b778b0-2062-4724-abbb-916a4835a9b0","is_self_assigned":true,"execution_order":null,"planned_start_time":null,"assigned_at":"2025-12-01 05:50:51.219042+00","responded_at":"2025-12-01 05:50:51.219044+00","journey_started_at":"2025-12-01 05:52:03.358665+00","arrived_at":"2025-12-01 05:52:35.847093+00","ended_at":"2025-12-01 05:54:37.183538+00","journey_start_latitude":-1.2200188,"journey_start_longitude":36.8770193,"arrival_latitude":-1.2200285,"arrival_longitude":36.8770248,"arrival_verified":false,"journey_location_history":"[{\"lat\": -1.2200188, \"lng\": 36.8770193, \"speed\": 0.0, \"battery\": 81, \"network\": \"4g\", \"accuracy\": 15.255999565124512, \"timestamp\": \"2025-12-01T05:52:07.294621\"}, {\"lat\": -1.2200285, \"lng\": 36.8770248, \"speed\": 0.0, \"battery\": 81, \"network\": \"4g\", \"accuracy\": 19.003999710083008, \"timestamp\": \"2025-12-01T05:52:35.782705\"}]","reason":null,"notes":"[COMPLETED] Hhhdg\n","created_at":"2025-12-01 05:50:51.230657+00","updated_at":"2025-12-01 05:54:37.241104+00","deleted_at":null},{"idx":6,"id":"f5b40f0c-bc9c-4904-9ec1-7e570bda34eb","ticket_id":"169eec08-654d-4ffe-bdb3-45fad1101637","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-12-03 13:21:09.471388+00","responded_at":"2025-12-03 13:21:09.471392+00","journey_started_at":"2025-12-03 13:21:46.868278+00","arrived_at":"2025-12-03 13:22:20.932051+00","ended_at":null,"journey_start_latitude":-1.2200555,"journey_start_longitude":36.876972,"arrival_latitude":-1.2200712,"arrival_longitude":36.8770138,"arrival_verified":false,"journey_location_history":"[{\"lat\": -1.2200661, \"lng\": 36.8769748, \"speed\": 0.0, \"battery\": 46, \"network\": \"3g\", \"accuracy\": 38.72999954223633, \"timestamp\": \"2025-12-03T13:21:50.999645\"}, {\"lat\": -1.2200712, \"lng\": 36.8770138, \"speed\": 0.0, \"battery\": 46, \"network\": \"3g\", \"accuracy\": 23.055999755859375, \"timestamp\": \"2025-12-03T13:22:20.715580\"}]","reason":null,"notes":null,"created_at":"2025-12-03 13:21:09.472355+00","updated_at":"2025-12-03 13:22:20.932686+00","deleted_at":null}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- 1. Get all columns in timesheets table
2
+ SELECT
3
+ column_name,
4
+ data_type,
5
+ is_nullable,
6
+ column_default,
7
+ character_maximum_length
8
+ FROM information_schema.columns
9
+ WHERE table_name = 'timesheets'
10
+ ORDER BY ordinal_position;
11
+
12
+ [
13
+ {
14
+ "column_name": "id",
15
+ "data_type": "uuid",
16
+ "is_nullable": "NO",
17
+ "column_default": "gen_random_uuid()",
18
+ "character_maximum_length": null
19
+ },
20
+ {
21
+ "column_name": "user_id",
22
+ "data_type": "uuid",
23
+ "is_nullable": "NO",
24
+ "column_default": null,
25
+ "character_maximum_length": null
26
+ },
27
+ {
28
+ "column_name": "project_id",
29
+ "data_type": "uuid",
30
+ "is_nullable": "YES",
31
+ "column_default": null,
32
+ "character_maximum_length": null
33
+ },
34
+ {
35
+ "column_name": "work_date",
36
+ "data_type": "date",
37
+ "is_nullable": "NO",
38
+ "column_default": null,
39
+ "character_maximum_length": null
40
+ },
41
+ {
42
+ "column_name": "status",
43
+ "data_type": "USER-DEFINED",
44
+ "is_nullable": "NO",
45
+ "column_default": "'present'::timesheetstatus",
46
+ "character_maximum_length": null
47
+ },
48
+ {
49
+ "column_name": "check_in_time",
50
+ "data_type": "timestamp with time zone",
51
+ "is_nullable": "YES",
52
+ "column_default": null,
53
+ "character_maximum_length": null
54
+ },
55
+ {
56
+ "column_name": "check_out_time",
57
+ "data_type": "timestamp with time zone",
58
+ "is_nullable": "YES",
59
+ "column_default": null,
60
+ "character_maximum_length": null
61
+ },
62
+ {
63
+ "column_name": "hours_worked",
64
+ "data_type": "numeric",
65
+ "is_nullable": "YES",
66
+ "column_default": null,
67
+ "character_maximum_length": null
68
+ },
69
+ {
70
+ "column_name": "leave_reason",
71
+ "data_type": "text",
72
+ "is_nullable": "YES",
73
+ "column_default": null,
74
+ "character_maximum_length": null
75
+ },
76
+ {
77
+ "column_name": "leave_approved_by_user_id",
78
+ "data_type": "uuid",
79
+ "is_nullable": "YES",
80
+ "column_default": null,
81
+ "character_maximum_length": null
82
+ },
83
+ {
84
+ "column_name": "notes",
85
+ "data_type": "text",
86
+ "is_nullable": "YES",
87
+ "column_default": null,
88
+ "character_maximum_length": null
89
+ },
90
+ {
91
+ "column_name": "additional_metadata",
92
+ "data_type": "jsonb",
93
+ "is_nullable": "YES",
94
+ "column_default": "'{}'::jsonb",
95
+ "character_maximum_length": null
96
+ },
97
+ {
98
+ "column_name": "created_at",
99
+ "data_type": "timestamp with time zone",
100
+ "is_nullable": "YES",
101
+ "column_default": "timezone('utc'::text, now())",
102
+ "character_maximum_length": null
103
+ },
104
+ {
105
+ "column_name": "updated_at",
106
+ "data_type": "timestamp with time zone",
107
+ "is_nullable": "YES",
108
+ "column_default": "timezone('utc'::text, now())",
109
+ "character_maximum_length": null
110
+ },
111
+ {
112
+ "column_name": "deleted_at",
113
+ "data_type": "timestamp with time zone",
114
+ "is_nullable": "YES",
115
+ "column_default": null,
116
+ "character_maximum_length": null
117
+ },
118
+ {
119
+ "column_name": "tickets_assigned",
120
+ "data_type": "integer",
121
+ "is_nullable": "NO",
122
+ "column_default": "0",
123
+ "character_maximum_length": null
124
+ },
125
+ {
126
+ "column_name": "tickets_completed",
127
+ "data_type": "integer",
128
+ "is_nullable": "NO",
129
+ "column_default": "0",
130
+ "character_maximum_length": null
131
+ },
132
+ {
133
+ "column_name": "tickets_rescheduled",
134
+ "data_type": "integer",
135
+ "is_nullable": "NO",
136
+ "column_default": "0",
137
+ "character_maximum_length": null
138
+ },
139
+ {
140
+ "column_name": "tickets_cancelled",
141
+ "data_type": "integer",
142
+ "is_nullable": "NO",
143
+ "column_default": "0",
144
+ "character_maximum_length": null
145
+ },
146
+ {
147
+ "column_name": "tickets_rejected",
148
+ "data_type": "integer",
149
+ "is_nullable": "NO",
150
+ "column_default": "0",
151
+ "character_maximum_length": null
152
+ },
153
+ {
154
+ "column_name": "is_payroll_generated",
155
+ "data_type": "boolean",
156
+ "is_nullable": "NO",
157
+ "column_default": "false",
158
+ "character_maximum_length": null
159
+ },
160
+ {
161
+ "column_name": "payroll_id",
162
+ "data_type": "uuid",
163
+ "is_nullable": "YES",
164
+ "column_default": null,
165
+ "character_maximum_length": null
166
+ },
167
+ {
168
+ "column_name": "total_expenses",
169
+ "data_type": "numeric",
170
+ "is_nullable": "YES",
171
+ "column_default": "0",
172
+ "character_maximum_length": null
173
+ },
174
+ {
175
+ "column_name": "approved_expenses",
176
+ "data_type": "numeric",
177
+ "is_nullable": "YES",
178
+ "column_default": "0",
179
+ "character_maximum_length": null
180
+ },
181
+ {
182
+ "column_name": "pending_expenses",
183
+ "data_type": "numeric",
184
+ "is_nullable": "YES",
185
+ "column_default": "0",
186
+ "character_maximum_length": null
187
+ },
188
+ {
189
+ "column_name": "rejected_expenses",
190
+ "data_type": "numeric",
191
+ "is_nullable": "YES",
192
+ "column_default": "0",
193
+ "character_maximum_length": null
194
+ },
195
+ {
196
+ "column_name": "expense_claims_count",
197
+ "data_type": "integer",
198
+ "is_nullable": "YES",
199
+ "column_default": "0",
200
+ "character_maximum_length": null
201
+ },
202
+ {
203
+ "column_name": "reconciliation_run_id",
204
+ "data_type": "uuid",
205
+ "is_nullable": "YES",
206
+ "column_default": null,
207
+ "character_maximum_length": null
208
+ },
209
+ {
210
+ "column_name": "last_reconciled_at",
211
+ "data_type": "timestamp with time zone",
212
+ "is_nullable": "YES",
213
+ "column_default": null,
214
+ "character_maximum_length": null
215
+ },
216
+ {
217
+ "column_name": "update_source",
218
+ "data_type": "text",
219
+ "is_nullable": "YES",
220
+ "column_default": "'realtime'::text",
221
+ "character_maximum_length": null
222
+ },
223
+ {
224
+ "column_name": "last_realtime_update_at",
225
+ "data_type": "timestamp with time zone",
226
+ "is_nullable": "YES",
227
+ "column_default": null,
228
+ "character_maximum_length": null
229
+ },
230
+ {
231
+ "column_name": "last_validated_at",
232
+ "data_type": "timestamp with time zone",
233
+ "is_nullable": "YES",
234
+ "column_default": null,
235
+ "character_maximum_length": null
236
+ },
237
+ {
238
+ "column_name": "needs_review",
239
+ "data_type": "boolean",
240
+ "is_nullable": "YES",
241
+ "column_default": "false",
242
+ "character_maximum_length": null
243
+ },
244
+ {
245
+ "column_name": "discrepancy_notes",
246
+ "data_type": "text",
247
+ "is_nullable": "YES",
248
+ "column_default": null,
249
+ "character_maximum_length": null
250
+ },
251
+ {
252
+ "column_name": "version",
253
+ "data_type": "integer",
254
+ "is_nullable": "NO",
255
+ "column_default": "1",
256
+ "character_maximum_length": null
257
+ },
258
+ {
259
+ "column_name": "inventory_issued_count",
260
+ "data_type": "integer",
261
+ "is_nullable": "YES",
262
+ "column_default": "0",
263
+ "character_maximum_length": null
264
+ },
265
+ {
266
+ "column_name": "inventory_issued_value",
267
+ "data_type": "numeric",
268
+ "is_nullable": "YES",
269
+ "column_default": "0",
270
+ "character_maximum_length": null
271
+ },
272
+ {
273
+ "column_name": "inventory_installed_count",
274
+ "data_type": "integer",
275
+ "is_nullable": "YES",
276
+ "column_default": "0",
277
+ "character_maximum_length": null
278
+ },
279
+ {
280
+ "column_name": "inventory_consumed_count",
281
+ "data_type": "integer",
282
+ "is_nullable": "YES",
283
+ "column_default": "0",
284
+ "character_maximum_length": null
285
+ },
286
+ {
287
+ "column_name": "inventory_returned_count",
288
+ "data_type": "integer",
289
+ "is_nullable": "YES",
290
+ "column_default": "0",
291
+ "character_maximum_length": null
292
+ },
293
+ {
294
+ "column_name": "inventory_returned_value",
295
+ "data_type": "numeric",
296
+ "is_nullable": "YES",
297
+ "column_default": "0",
298
+ "character_maximum_length": null
299
+ },
300
+ {
301
+ "column_name": "inventory_lost_count",
302
+ "data_type": "integer",
303
+ "is_nullable": "YES",
304
+ "column_default": "0",
305
+ "character_maximum_length": null
306
+ },
307
+ {
308
+ "column_name": "inventory_damaged_count",
309
+ "data_type": "integer",
310
+ "is_nullable": "YES",
311
+ "column_default": "0",
312
+ "character_maximum_length": null
313
+ },
314
+ {
315
+ "column_name": "inventory_loss_value",
316
+ "data_type": "numeric",
317
+ "is_nullable": "YES",
318
+ "column_default": "0",
319
+ "character_maximum_length": null
320
+ },
321
+ {
322
+ "column_name": "inventory_on_hand_count",
323
+ "data_type": "integer",
324
+ "is_nullable": "YES",
325
+ "column_default": "0",
326
+ "character_maximum_length": null
327
+ },
328
+ {
329
+ "column_name": "inventory_on_hand_value",
330
+ "data_type": "numeric",
331
+ "is_nullable": "YES",
332
+ "column_default": "0",
333
+ "character_maximum_length": null
334
+ },
335
+ {
336
+ "column_name": "inventory_details",
337
+ "data_type": "jsonb",
338
+ "is_nullable": "YES",
339
+ "column_default": "'[]'::jsonb",
340
+ "character_maximum_length": null
341
+ }
342
+ ]
343
+
344
+ -- 2. Get all columns in reconciliation_runs table
345
+ SELECT
346
+ column_name,
347
+ data_type,
348
+ is_nullable,
349
+ column_default,
350
+ character_maximum_length
351
+ FROM information_schema.columns
352
+ WHERE table_name = 'reconciliation_runs'
353
+ ORDER BY ordinal_position;
354
+
355
+ [
356
+ {
357
+ "column_name": "id",
358
+ "data_type": "uuid",
359
+ "is_nullable": "NO",
360
+ "column_default": "gen_random_uuid()",
361
+ "character_maximum_length": null
362
+ },
363
+ {
364
+ "column_name": "project_id",
365
+ "data_type": "uuid",
366
+ "is_nullable": "NO",
367
+ "column_default": null,
368
+ "character_maximum_length": null
369
+ },
370
+ {
371
+ "column_name": "reconciliation_date",
372
+ "data_type": "date",
373
+ "is_nullable": "NO",
374
+ "column_default": null,
375
+ "character_maximum_length": null
376
+ },
377
+ {
378
+ "column_name": "run_type",
379
+ "data_type": "text",
380
+ "is_nullable": "NO",
381
+ "column_default": null,
382
+ "character_maximum_length": null
383
+ },
384
+ {
385
+ "column_name": "started_at",
386
+ "data_type": "timestamp with time zone",
387
+ "is_nullable": "NO",
388
+ "column_default": "timezone('utc'::text, now())",
389
+ "character_maximum_length": null
390
+ },
391
+ {
392
+ "column_name": "completed_at",
393
+ "data_type": "timestamp with time zone",
394
+ "is_nullable": "YES",
395
+ "column_default": null,
396
+ "character_maximum_length": null
397
+ },
398
+ {
399
+ "column_name": "status",
400
+ "data_type": "text",
401
+ "is_nullable": "NO",
402
+ "column_default": "'running'::text",
403
+ "character_maximum_length": null
404
+ },
405
+ {
406
+ "column_name": "user_ids",
407
+ "data_type": "ARRAY",
408
+ "is_nullable": "YES",
409
+ "column_default": null,
410
+ "character_maximum_length": null
411
+ },
412
+ {
413
+ "column_name": "agents_processed",
414
+ "data_type": "integer",
415
+ "is_nullable": "YES",
416
+ "column_default": "0",
417
+ "character_maximum_length": null
418
+ },
419
+ {
420
+ "column_name": "timesheets_created",
421
+ "data_type": "integer",
422
+ "is_nullable": "YES",
423
+ "column_default": "0",
424
+ "character_maximum_length": null
425
+ },
426
+ {
427
+ "column_name": "timesheets_updated",
428
+ "data_type": "integer",
429
+ "is_nullable": "YES",
430
+ "column_default": "0",
431
+ "character_maximum_length": null
432
+ },
433
+ {
434
+ "column_name": "assignments_processed",
435
+ "data_type": "integer",
436
+ "is_nullable": "YES",
437
+ "column_default": "0",
438
+ "character_maximum_length": null
439
+ },
440
+ {
441
+ "column_name": "expenses_processed",
442
+ "data_type": "integer",
443
+ "is_nullable": "YES",
444
+ "column_default": "0",
445
+ "character_maximum_length": null
446
+ },
447
+ {
448
+ "column_name": "execution_time_ms",
449
+ "data_type": "integer",
450
+ "is_nullable": "YES",
451
+ "column_default": null,
452
+ "character_maximum_length": null
453
+ },
454
+ {
455
+ "column_name": "query_time_ms",
456
+ "data_type": "integer",
457
+ "is_nullable": "YES",
458
+ "column_default": null,
459
+ "character_maximum_length": null
460
+ },
461
+ {
462
+ "column_name": "summary_stats",
463
+ "data_type": "jsonb",
464
+ "is_nullable": "YES",
465
+ "column_default": "'{}'::jsonb",
466
+ "character_maximum_length": null
467
+ },
468
+ {
469
+ "column_name": "anomalies_detected",
470
+ "data_type": "jsonb",
471
+ "is_nullable": "YES",
472
+ "column_default": "'[]'::jsonb",
473
+ "character_maximum_length": null
474
+ },
475
+ {
476
+ "column_name": "error_message",
477
+ "data_type": "text",
478
+ "is_nullable": "YES",
479
+ "column_default": null,
480
+ "character_maximum_length": null
481
+ },
482
+ {
483
+ "column_name": "error_details",
484
+ "data_type": "jsonb",
485
+ "is_nullable": "YES",
486
+ "column_default": null,
487
+ "character_maximum_length": null
488
+ },
489
+ {
490
+ "column_name": "triggered_by_user_id",
491
+ "data_type": "uuid",
492
+ "is_nullable": "YES",
493
+ "column_default": null,
494
+ "character_maximum_length": null
495
+ },
496
+ {
497
+ "column_name": "created_at",
498
+ "data_type": "timestamp with time zone",
499
+ "is_nullable": "YES",
500
+ "column_default": "timezone('utc'::text, now())",
501
+ "character_maximum_length": null
502
+ },
503
+ {
504
+ "column_name": "updated_at",
505
+ "data_type": "timestamp with time zone",
506
+ "is_nullable": "YES",
507
+ "column_default": "timezone('utc'::text, now())",
508
+ "character_maximum_length": null
509
+ },
510
+ {
511
+ "column_name": "deleted_at",
512
+ "data_type": "timestamp with time zone",
513
+ "is_nullable": "YES",
514
+ "column_default": null,
515
+ "character_maximum_length": null
516
+ },
517
+ {
518
+ "column_name": "discrepancies_found",
519
+ "data_type": "integer",
520
+ "is_nullable": "YES",
521
+ "column_default": "0",
522
+ "character_maximum_length": null
523
+ },
524
+ {
525
+ "column_name": "orphaned_records_found",
526
+ "data_type": "integer",
527
+ "is_nullable": "YES",
528
+ "column_default": "0",
529
+ "character_maximum_length": null
530
+ },
531
+ {
532
+ "column_name": "corrections_made",
533
+ "data_type": "integer",
534
+ "is_nullable": "YES",
535
+ "column_default": "0",
536
+ "character_maximum_length": null
537
+ },
538
+ {
539
+ "column_name": "discrepancy_details",
540
+ "data_type": "jsonb",
541
+ "is_nullable": "YES",
542
+ "column_default": "'[]'::jsonb",
543
+ "character_maximum_length": null
544
+ }
545
+ ]
546
+
547
+ -- 3. Get all columns in timesheet_updates table
548
+ SELECT
549
+ column_name,
550
+ data_type,
551
+ is_nullable,
552
+ column_default,
553
+ character_maximum_length
554
+ FROM information_schema.columns
555
+ WHERE table_name = 'timesheet_updates'
556
+ ORDER BY ordinal_position;
557
+
558
+ [
559
+ {
560
+ "column_name": "id",
561
+ "data_type": "uuid",
562
+ "is_nullable": "NO",
563
+ "column_default": "gen_random_uuid()",
564
+ "character_maximum_length": null
565
+ },
566
+ {
567
+ "column_name": "timesheet_id",
568
+ "data_type": "uuid",
569
+ "is_nullable": "NO",
570
+ "column_default": null,
571
+ "character_maximum_length": null
572
+ },
573
+ {
574
+ "column_name": "trigger_type",
575
+ "data_type": "text",
576
+ "is_nullable": "NO",
577
+ "column_default": null,
578
+ "character_maximum_length": null
579
+ },
580
+ {
581
+ "column_name": "trigger_entity_type",
582
+ "data_type": "text",
583
+ "is_nullable": "YES",
584
+ "column_default": null,
585
+ "character_maximum_length": null
586
+ },
587
+ {
588
+ "column_name": "trigger_entity_id",
589
+ "data_type": "uuid",
590
+ "is_nullable": "YES",
591
+ "column_default": null,
592
+ "character_maximum_length": null
593
+ },
594
+ {
595
+ "column_name": "fields_changed",
596
+ "data_type": "jsonb",
597
+ "is_nullable": "NO",
598
+ "column_default": "'{}'::jsonb",
599
+ "character_maximum_length": null
600
+ },
601
+ {
602
+ "column_name": "updated_by_user_id",
603
+ "data_type": "uuid",
604
+ "is_nullable": "YES",
605
+ "column_default": null,
606
+ "character_maximum_length": null
607
+ },
608
+ {
609
+ "column_name": "updated_at",
610
+ "data_type": "timestamp with time zone",
611
+ "is_nullable": "YES",
612
+ "column_default": "now()",
613
+ "character_maximum_length": null
614
+ },
615
+ {
616
+ "column_name": "additional_metadata",
617
+ "data_type": "jsonb",
618
+ "is_nullable": "YES",
619
+ "column_default": "'{}'::jsonb",
620
+ "character_maximum_length": null
621
+ },
622
+ {
623
+ "column_name": "created_at",
624
+ "data_type": "timestamp with time zone",
625
+ "is_nullable": "YES",
626
+ "column_default": "now()",
627
+ "character_maximum_length": null
628
+ }
629
+ ]
630
+
631
+ -- 4. Get all indexes on timesheets
632
+ SELECT
633
+ indexname,
634
+ indexdef
635
+ FROM pg_indexes
636
+ WHERE tablename = 'timesheets'
637
+ ORDER BY indexname;
638
+
639
+ [
640
+ {
641
+ "indexname": "idx_timesheets_inventory_details_gin",
642
+ "indexdef": "CREATE INDEX idx_timesheets_inventory_details_gin ON public.timesheets USING gin (inventory_details)"
643
+ },
644
+ {
645
+ "indexname": "idx_timesheets_inventory_loss",
646
+ "indexdef": "CREATE INDEX idx_timesheets_inventory_loss ON public.timesheets USING btree (work_date, inventory_lost_count, inventory_damaged_count) WHERE (((inventory_lost_count > 0) OR (inventory_damaged_count > 0)) AND (deleted_at IS NULL))"
647
+ },
648
+ {
649
+ "indexname": "idx_timesheets_inventory_on_hand",
650
+ "indexdef": "CREATE INDEX idx_timesheets_inventory_on_hand ON public.timesheets USING btree (user_id, work_date, inventory_on_hand_count) WHERE ((inventory_on_hand_count > 0) AND (deleted_at IS NULL))"
651
+ },
652
+ {
653
+ "indexname": "idx_timesheets_metadata_gin",
654
+ "indexdef": "CREATE INDEX idx_timesheets_metadata_gin ON public.timesheets USING gin (additional_metadata)"
655
+ },
656
+ {
657
+ "indexname": "idx_timesheets_needs_review",
658
+ "indexdef": "CREATE INDEX idx_timesheets_needs_review ON public.timesheets USING btree (needs_review, last_validated_at) WHERE ((needs_review = true) AND (deleted_at IS NULL))"
659
+ },
660
+ {
661
+ "indexname": "idx_timesheets_payroll",
662
+ "indexdef": "CREATE INDEX idx_timesheets_payroll ON public.timesheets USING btree (payroll_id) WHERE (payroll_id IS NOT NULL)"
663
+ },
664
+ {
665
+ "indexname": "idx_timesheets_payroll_status",
666
+ "indexdef": "CREATE INDEX idx_timesheets_payroll_status ON public.timesheets USING btree (user_id, is_payroll_generated, work_date DESC)"
667
+ },
668
+ {
669
+ "indexname": "idx_timesheets_project",
670
+ "indexdef": "CREATE INDEX idx_timesheets_project ON public.timesheets USING btree (project_id, work_date DESC) WHERE (deleted_at IS NULL)"
671
+ },
672
+ {
673
+ "indexname": "idx_timesheets_project_date",
674
+ "indexdef": "CREATE INDEX idx_timesheets_project_date ON public.timesheets USING btree (project_id, work_date DESC) WHERE (deleted_at IS NULL)"
675
+ },
676
+ {
677
+ "indexname": "idx_timesheets_realtime_updates",
678
+ "indexdef": "CREATE INDEX idx_timesheets_realtime_updates ON public.timesheets USING btree (user_id, work_date, last_realtime_update_at DESC) WHERE (deleted_at IS NULL)"
679
+ },
680
+ {
681
+ "indexname": "idx_timesheets_reconciliation_run",
682
+ "indexdef": "CREATE INDEX idx_timesheets_reconciliation_run ON public.timesheets USING btree (reconciliation_run_id) WHERE (deleted_at IS NULL)"
683
+ },
684
+ {
685
+ "indexname": "idx_timesheets_ticket_metrics",
686
+ "indexdef": "CREATE INDEX idx_timesheets_ticket_metrics ON public.timesheets USING btree (work_date, tickets_completed) WHERE (tickets_completed > 0)"
687
+ },
688
+ {
689
+ "indexname": "idx_timesheets_unique",
690
+ "indexdef": "CREATE UNIQUE INDEX idx_timesheets_unique ON public.timesheets USING btree (user_id, project_id, work_date) WHERE (deleted_at IS NULL)"
691
+ },
692
+ {
693
+ "indexname": "idx_timesheets_user",
694
+ "indexdef": "CREATE INDEX idx_timesheets_user ON public.timesheets USING btree (user_id, work_date DESC) WHERE (deleted_at IS NULL)"
695
+ },
696
+ {
697
+ "indexname": "idx_timesheets_user_date_unique",
698
+ "indexdef": "CREATE UNIQUE INDEX idx_timesheets_user_date_unique ON public.timesheets USING btree (user_id, work_date) WHERE (deleted_at IS NULL)"
699
+ },
700
+ {
701
+ "indexname": "timesheets_pkey",
702
+ "indexdef": "CREATE UNIQUE INDEX timesheets_pkey ON public.timesheets USING btree (id)"
703
+ }
704
+ ]
705
+
706
+ -- 5. Get all indexes on reconciliation_runs
707
+ SELECT
708
+ indexname,
709
+ indexdef
710
+ FROM pg_indexes
711
+ WHERE tablename = 'reconciliation_runs'
712
+ ORDER BY indexname;
713
+
714
+ [
715
+ {
716
+ "indexname": "idx_reconciliation_runs_anomalies_gin",
717
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_anomalies_gin ON public.reconciliation_runs USING gin (anomalies_detected)"
718
+ },
719
+ {
720
+ "indexname": "idx_reconciliation_runs_discrepancies",
721
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_discrepancies ON public.reconciliation_runs USING btree (discrepancies_found, reconciliation_date DESC) WHERE (discrepancies_found > 0)"
722
+ },
723
+ {
724
+ "indexname": "idx_reconciliation_runs_project_date",
725
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_project_date ON public.reconciliation_runs USING btree (project_id, reconciliation_date DESC) WHERE (deleted_at IS NULL)"
726
+ },
727
+ {
728
+ "indexname": "idx_reconciliation_runs_status",
729
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_status ON public.reconciliation_runs USING btree (status, started_at DESC) WHERE (deleted_at IS NULL)"
730
+ },
731
+ {
732
+ "indexname": "idx_reconciliation_runs_summary_gin",
733
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_summary_gin ON public.reconciliation_runs USING gin (summary_stats)"
734
+ },
735
+ {
736
+ "indexname": "idx_reconciliation_runs_triggered_by",
737
+ "indexdef": "CREATE INDEX idx_reconciliation_runs_triggered_by ON public.reconciliation_runs USING btree (triggered_by_user_id, started_at DESC) WHERE (deleted_at IS NULL)"
738
+ },
739
+ {
740
+ "indexname": "idx_reconciliation_runs_unique_active_run",
741
+ "indexdef": "CREATE UNIQUE INDEX idx_reconciliation_runs_unique_active_run ON public.reconciliation_runs USING btree (project_id, reconciliation_date, status) WHERE ((status = 'running'::text) AND (deleted_at IS NULL))"
742
+ },
743
+ {
744
+ "indexname": "reconciliation_runs_pkey",
745
+ "indexdef": "CREATE UNIQUE INDEX reconciliation_runs_pkey ON public.reconciliation_runs USING btree (id)"
746
+ }
747
+ ]
748
+
749
+ -- 6. Get all indexes on timesheet_updates
750
+ SELECT
751
+ indexname,
752
+ indexdef
753
+ FROM pg_indexes
754
+ WHERE tablename = 'timesheet_updates'
755
+ ORDER BY indexname;
756
+
757
+ [
758
+ {
759
+ "indexname": "idx_timesheet_updates_entity",
760
+ "indexdef": "CREATE INDEX idx_timesheet_updates_entity ON public.timesheet_updates USING btree (trigger_entity_type, trigger_entity_id) WHERE (trigger_entity_id IS NOT NULL)"
761
+ },
762
+ {
763
+ "indexname": "idx_timesheet_updates_fields_gin",
764
+ "indexdef": "CREATE INDEX idx_timesheet_updates_fields_gin ON public.timesheet_updates USING gin (fields_changed)"
765
+ },
766
+ {
767
+ "indexname": "idx_timesheet_updates_timesheet",
768
+ "indexdef": "CREATE INDEX idx_timesheet_updates_timesheet ON public.timesheet_updates USING btree (timesheet_id, updated_at DESC)"
769
+ },
770
+ {
771
+ "indexname": "idx_timesheet_updates_trigger",
772
+ "indexdef": "CREATE INDEX idx_timesheet_updates_trigger ON public.timesheet_updates USING btree (trigger_type, updated_at DESC)"
773
+ },
774
+ {
775
+ "indexname": "timesheet_updates_pkey",
776
+ "indexdef": "CREATE UNIQUE INDEX timesheet_updates_pkey ON public.timesheet_updates USING btree (id)"
777
+ }
778
+ ]
779
+
780
+ -- 7. Get all constraints on timesheets
781
+ SELECT
782
+ conname AS constraint_name,
783
+ contype AS constraint_type,
784
+ pg_get_constraintdef(oid) AS constraint_definition
785
+ FROM pg_constraint
786
+ WHERE conrelid = 'timesheets'::regclass
787
+ ORDER BY conname;
788
+
789
+ [
790
+ {
791
+ "constraint_name": "chk_check_times",
792
+ "constraint_type": "c",
793
+ "constraint_definition": "CHECK (((check_out_time IS NULL) OR (check_in_time IS NULL) OR (check_out_time >= check_in_time)))"
794
+ },
795
+ {
796
+ "constraint_name": "chk_valid_hours",
797
+ "constraint_type": "c",
798
+ "constraint_definition": "CHECK (((hours_worked IS NULL) OR ((hours_worked >= (0)::numeric) AND (hours_worked <= (24)::numeric))))"
799
+ },
800
+ {
801
+ "constraint_name": "timesheets_approved_expenses_check",
802
+ "constraint_type": "c",
803
+ "constraint_definition": "CHECK ((approved_expenses >= (0)::numeric))"
804
+ },
805
+ {
806
+ "constraint_name": "timesheets_expense_claims_count_check",
807
+ "constraint_type": "c",
808
+ "constraint_definition": "CHECK ((expense_claims_count >= 0))"
809
+ },
810
+ {
811
+ "constraint_name": "timesheets_inventory_consumed_count_check",
812
+ "constraint_type": "c",
813
+ "constraint_definition": "CHECK ((inventory_consumed_count >= 0))"
814
+ },
815
+ {
816
+ "constraint_name": "timesheets_inventory_damaged_count_check",
817
+ "constraint_type": "c",
818
+ "constraint_definition": "CHECK ((inventory_damaged_count >= 0))"
819
+ },
820
+ {
821
+ "constraint_name": "timesheets_inventory_installed_count_check",
822
+ "constraint_type": "c",
823
+ "constraint_definition": "CHECK ((inventory_installed_count >= 0))"
824
+ },
825
+ {
826
+ "constraint_name": "timesheets_inventory_issued_count_check",
827
+ "constraint_type": "c",
828
+ "constraint_definition": "CHECK ((inventory_issued_count >= 0))"
829
+ },
830
+ {
831
+ "constraint_name": "timesheets_inventory_issued_value_check",
832
+ "constraint_type": "c",
833
+ "constraint_definition": "CHECK ((inventory_issued_value >= (0)::numeric))"
834
+ },
835
+ {
836
+ "constraint_name": "timesheets_inventory_loss_value_check",
837
+ "constraint_type": "c",
838
+ "constraint_definition": "CHECK ((inventory_loss_value >= (0)::numeric))"
839
+ },
840
+ {
841
+ "constraint_name": "timesheets_inventory_lost_count_check",
842
+ "constraint_type": "c",
843
+ "constraint_definition": "CHECK ((inventory_lost_count >= 0))"
844
+ },
845
+ {
846
+ "constraint_name": "timesheets_inventory_on_hand_count_check",
847
+ "constraint_type": "c",
848
+ "constraint_definition": "CHECK ((inventory_on_hand_count >= 0))"
849
+ },
850
+ {
851
+ "constraint_name": "timesheets_inventory_on_hand_value_check",
852
+ "constraint_type": "c",
853
+ "constraint_definition": "CHECK ((inventory_on_hand_value >= (0)::numeric))"
854
+ },
855
+ {
856
+ "constraint_name": "timesheets_inventory_returned_count_check",
857
+ "constraint_type": "c",
858
+ "constraint_definition": "CHECK ((inventory_returned_count >= 0))"
859
+ },
860
+ {
861
+ "constraint_name": "timesheets_inventory_returned_value_check",
862
+ "constraint_type": "c",
863
+ "constraint_definition": "CHECK ((inventory_returned_value >= (0)::numeric))"
864
+ },
865
+ {
866
+ "constraint_name": "timesheets_leave_approved_by_user_id_fkey",
867
+ "constraint_type": "f",
868
+ "constraint_definition": "FOREIGN KEY (leave_approved_by_user_id) REFERENCES users(id) ON DELETE SET NULL"
869
+ },
870
+ {
871
+ "constraint_name": "timesheets_payroll_id_fkey",
872
+ "constraint_type": "f",
873
+ "constraint_definition": "FOREIGN KEY (payroll_id) REFERENCES user_payroll(id) ON DELETE SET NULL"
874
+ },
875
+ {
876
+ "constraint_name": "timesheets_pending_expenses_check",
877
+ "constraint_type": "c",
878
+ "constraint_definition": "CHECK ((pending_expenses >= (0)::numeric))"
879
+ },
880
+ {
881
+ "constraint_name": "timesheets_pkey",
882
+ "constraint_type": "p",
883
+ "constraint_definition": "PRIMARY KEY (id)"
884
+ },
885
+ {
886
+ "constraint_name": "timesheets_project_id_fkey",
887
+ "constraint_type": "f",
888
+ "constraint_definition": "FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE"
889
+ },
890
+ {
891
+ "constraint_name": "timesheets_reconciliation_run_id_fkey",
892
+ "constraint_type": "f",
893
+ "constraint_definition": "FOREIGN KEY (reconciliation_run_id) REFERENCES reconciliation_runs(id) ON DELETE SET NULL"
894
+ },
895
+ {
896
+ "constraint_name": "timesheets_rejected_expenses_check",
897
+ "constraint_type": "c",
898
+ "constraint_definition": "CHECK ((rejected_expenses >= (0)::numeric))"
899
+ },
900
+ {
901
+ "constraint_name": "timesheets_total_expenses_check",
902
+ "constraint_type": "c",
903
+ "constraint_definition": "CHECK ((total_expenses >= (0)::numeric))"
904
+ },
905
+ {
906
+ "constraint_name": "timesheets_update_source_check",
907
+ "constraint_type": "c",
908
+ "constraint_definition": "CHECK ((update_source = ANY (ARRAY['realtime'::text, 'scheduled'::text, 'manual'::text])))"
909
+ },
910
+ {
911
+ "constraint_name": "timesheets_user_id_fkey",
912
+ "constraint_type": "f",
913
+ "constraint_definition": "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"
914
+ }
915
+ ]
916
+
917
+ -- 8. Check if specific columns exist (quick verification)
918
+ SELECT
919
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'total_expenses') as has_total_expenses,
920
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'approved_expenses') as has_approved_expenses,
921
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'pending_expenses') as has_pending_expenses,
922
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'rejected_expenses') as has_rejected_expenses,
923
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'expense_claims_count') as has_expense_claims_count,
924
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'update_source') as has_update_source,
925
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'last_realtime_update_at') as has_last_realtime_update_at,
926
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'check_in_time') as has_check_in_time,
927
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'clock_in_time') as has_clock_in_time,
928
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'inventory_on_hand_count') as has_inventory_on_hand_count,
929
+ EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'timesheets' AND column_name = 'inventory_issued_count') as has_inventory_issued_count;
930
+
931
+ [
932
+ {
933
+ "has_total_expenses": true,
934
+ "has_approved_expenses": true,
935
+ "has_pending_expenses": true,
936
+ "has_rejected_expenses": true,
937
+ "has_expense_claims_count": true,
938
+ "has_update_source": true,
939
+ "has_last_realtime_update_at": true,
940
+ "has_check_in_time": true,
941
+ "has_clock_in_time": false,
942
+ "has_inventory_on_hand_count": true,
943
+ "has_inventory_issued_count": true
944
+ }
945
+ ]
docs/devlogs/reconciliation_deployment_status.md ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Reconciliation System - Deployment Status
2
+
3
+ **Date:** 2024-12-10
4
+ **Status:** ✅ FULLY DEPLOYED AND READY FOR TESTING
5
+
6
+ ## Database Schema Status
7
+
8
+ ### ✅ All Migrations Deployed
9
+
10
+ 1. **20241209_add_reconciliation_system.sql** - DEPLOYED
11
+ - `reconciliation_runs` table created
12
+ - Expense tracking columns added to timesheets
13
+ - All indexes and RLS policies in place
14
+
15
+ 2. **20241210_add_realtime_reconciliation.sql** - DEPLOYED
16
+ - `timesheet_updates` audit table created
17
+ - Real-time tracking columns added to timesheets
18
+ - Helper functions created (`log_timesheet_update`, `increment_timesheet_version`)
19
+
20
+ 3. **20241210_add_inventory_to_timesheets.sql** - DEPLOYED
21
+ - All inventory tracking columns added
22
+ - Check in/out time columns exist (using correct naming: `check_in_time`/`check_out_time`)
23
+ - Inventory calculation function created
24
+
25
+ ### Timesheets Table - Complete Column List (47 columns)
26
+
27
+ **Core Fields:**
28
+ - id, user_id, project_id, work_date, status
29
+ - check_in_time, check_out_time, hours_worked
30
+ - created_at, updated_at, deleted_at
31
+
32
+ **Ticket Metrics:**
33
+ - tickets_assigned, tickets_completed, tickets_rejected
34
+ - tickets_cancelled, tickets_rescheduled
35
+
36
+ **Expense Tracking:**
37
+ - total_expenses, approved_expenses, pending_expenses, rejected_expenses
38
+ - expense_claims_count
39
+
40
+ **Inventory Tracking:**
41
+ - inventory_issued_count, inventory_issued_value
42
+ - inventory_installed_count, inventory_consumed_count
43
+ - inventory_returned_count, inventory_returned_value
44
+ - inventory_lost_count, inventory_damaged_count, inventory_loss_value
45
+ - inventory_on_hand_count, inventory_on_hand_value
46
+ - inventory_details (JSONB)
47
+
48
+ **Reconciliation Tracking:**
49
+ - reconciliation_run_id, last_reconciled_at
50
+ - update_source, last_realtime_update_at
51
+ - last_validated_at, needs_review, discrepancy_notes
52
+ - version (for optimistic locking)
53
+
54
+ **Leave Management:**
55
+ - leave_reason, leave_approved_by_user_id
56
+
57
+ **Payroll:**
58
+ - is_payroll_generated, payroll_id
59
+
60
+ **Metadata:**
61
+ - notes, additional_metadata (JSONB)
62
+
63
+ ## Code Implementation Status
64
+
65
+ ### ✅ Completed
66
+
67
+ 1. **ReconciliationService** (`src/app/services/reconciliation/reconciliation_service.py`)
68
+ - Sync implementation (no async)
69
+ - Main reconciliation method: `reconcile_project_day()`
70
+ - Real-time update method: `update_user_timesheet_realtime()`
71
+ - Fixed ON CONFLICT to use `(user_id, work_date)` instead of constraint name
72
+ - All expense parameters included in real-time updates
73
+
74
+ 2. **Real-Time Triggers Integrated:**
75
+ - ✅ `ticket_assignments.py` - self-assign, complete assignment
76
+ - ✅ `ticket_expenses.py` - create expense, approve/reject expense
77
+ - ✅ `ticket_completion.py` - complete ticket
78
+ - ⏳ `inventory.py` - NOT YET INTEGRATED (next step)
79
+
80
+ 3. **Scheduler** (`src/app/tasks/scheduler.py`)
81
+ - Configured for midnight runs (Africa/Nairobi timezone)
82
+ - Currently runs full reconciliation
83
+ - ⏳ TODO: Update to validation mode (find orphans/discrepancies)
84
+
85
+ 4. **API Endpoints** (`src/app/api/endpoints/reconciliation.py`)
86
+ - Manual reconciliation trigger
87
+ - View reconciliation runs
88
+ - View reconciliation stats
89
+
90
+ 5. **Timesheet Endpoints** (`src/app/api/v1/timesheets.py`)
91
+ - ✅ `/me/timesheets` - agents view their own timesheets
92
+ - ✅ `/apply-leave` - agents apply for leave
93
+ - ✅ `/approve-leave` - managers approve leave
94
+ - ✅ `/stats` - timesheet statistics
95
+ - ✅ `/users/{user_id}/performance` - manager view team performance
96
+
97
+ ## Critical Fixes Applied
98
+
99
+ 1. **Constraint Name:** Changed from `ON CONFLICT ON CONSTRAINT uq_timesheets_user_date` to `ON CONFLICT (user_id, work_date)`
100
+ 2. **Expense Parameters:** Added all expense parameters to real-time update query execution
101
+ 3. **Field Naming:** Confirmed using `check_in_time`/`check_out_time` (not `clock_*`)
102
+ 4. **Relationship Names:** Fixed `expense.ticket` (not `expense.ticket_assignment.ticket`)
103
+
104
+ ## Next Steps
105
+
106
+ ### 1. Test Real-Time Updates (Priority: HIGH)
107
+ Test these scenarios to verify real-time reconciliation works:
108
+
109
+ ```bash
110
+ # Test 1: Self-assign ticket
111
+ POST /api/v1/ticket-assignments/self-assign
112
+ # Expected: Creates timesheet with tickets_assigned=1
113
+
114
+ # Test 2: Create expense
115
+ POST /api/v1/ticket-expenses
116
+ # Expected: Updates timesheet with total_expenses, pending_expenses
117
+
118
+ # Test 3: Approve expense
119
+ PUT /api/v1/ticket-expenses/{id}/approve
120
+ # Expected: Updates approved_expenses, reduces pending_expenses
121
+
122
+ # Test 4: Complete ticket
123
+ POST /api/v1/tickets/{id}/complete
124
+ # Expected: Updates tickets_completed
125
+
126
+ # Test 5: View timesheet
127
+ GET /api/v1/timesheets/me/timesheets
128
+ # Expected: Shows all updates in real-time
129
+ ```
130
+
131
+ ### 2. Add Inventory Real-Time Updates (Priority: MEDIUM)
132
+ File: `src/app/api/v1/inventory.py`
133
+
134
+ Add calls to `reconciliation_service.update_user_timesheet_realtime()` for:
135
+ - Inventory issued
136
+ - Inventory installed/consumed
137
+ - Inventory returned
138
+ - Inventory lost/damaged
139
+
140
+ ### 3. Update Scheduled Job to Validation Mode (Priority: MEDIUM)
141
+ File: `src/app/tasks/scheduler.py`
142
+
143
+ Change from "create all timesheets" to "validate and find orphans":
144
+ - Check for assignments/expenses without timesheet updates
145
+ - Detect discrepancies between real-time and actual data
146
+ - Mark timesheets with `needs_review=TRUE` if issues found
147
+
148
+ ### 4. Monitor Performance (Priority: LOW)
149
+ - Check `reconciliation_runs.execution_time_ms` for performance
150
+ - Target: <30 seconds for 500 agents
151
+ - Optimize queries if needed
152
+
153
+ ## Testing Checklist
154
+
155
+ - [ ] Self-assign ticket creates timesheet
156
+ - [ ] Complete assignment updates timesheet
157
+ - [ ] Create expense updates expense fields
158
+ - [ ] Approve expense updates approved_expenses
159
+ - [ ] Reject expense updates rejected_expenses
160
+ - [ ] Complete ticket updates tickets_completed
161
+ - [ ] Apply leave creates timesheet with status
162
+ - [ ] View own timesheets shows all data
163
+ - [ ] Stats endpoint returns correct metrics
164
+ - [ ] Scheduled job runs at midnight
165
+ - [ ] Audit trail in timesheet_updates table
166
+
167
+ ## Known Issues
168
+
169
+ None - system is ready for testing!
170
+
171
+ ## Database Constraints
172
+
173
+ **Unique Constraint:** `idx_timesheets_user_date_unique` on `(user_id, work_date)`
174
+ - Ensures one timesheet per user per day
175
+ - Used in ON CONFLICT clause for upserts
176
+
177
+ **Check Constraints:**
178
+ - All expense amounts >= 0
179
+ - All inventory counts >= 0
180
+ - hours_worked between 0 and 24
181
+ - check_out_time >= check_in_time
182
+ - update_source in ('realtime', 'scheduled', 'manual')
docs/devlogs/server/runtimeerror.txt CHANGED
@@ -1,160 +1,96 @@
1
- ===== Application Startup at 2025-12-10 11:52:59 =====
2
 
3
  INFO: Started server process [7]
4
  INFO: Waiting for application startup.
5
- INFO: 2025-12-10T11:53:11 - app.main: ============================================================
6
- INFO: 2025-12-10T11:53:11 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
- INFO: 2025-12-10T11:53:11 - app.main: 📊 Dashboard: Enabled
8
- INFO: 2025-12-10T11:53:11 - app.main: ============================================================
9
- INFO: 2025-12-10T11:53:11 - app.main: 📦 Database:
10
- INFO: 2025-12-10T11:53:11 - app.main: ✓ Connected | 47 tables | 6 users
11
- INFO: 2025-12-10T11:53:11 - app.main: 💾 Cache & Sessions:
12
- INFO: 2025-12-10T11:53:12 - app.services.otp_service: ✅ OTP Service initialized with Redis storage
13
- INFO: 2025-12-10T11:53:12 - app.main: ✓ Redis: Connected
14
- INFO: 2025-12-10T11:53:12 - app.main: 🔌 External Services:
15
- INFO: 2025-12-10T11:53:13 - app.main: ✓ Cloudinary: Connected
16
- INFO: 2025-12-10T11:53:13 - app.main: ✓ Resend: Configured
17
- INFO: 2025-12-10T11:53:13 - app.main: ○ WASender: Disconnected
18
- INFO: 2025-12-10T11:53:13 - app.main: ✓ Supabase: Connected | 6 buckets
19
- INFO: 2025-12-10T11:53:13 - app.main: ⏰ Scheduler:
20
- INFO: 2025-12-10T11:53:13 - apscheduler.scheduler: Adding job tentatively -- it will be properly scheduled when the scheduler starts
21
- INFO: 2025-12-10T11:53:13 - apscheduler.scheduler: Added job "Daily Field Agent Reconciliation" to job store "default"
22
- INFO: 2025-12-10T11:53:13 - apscheduler.scheduler: Scheduler started
23
- INFO: 2025-12-10T11:53:13 - app.tasks.scheduler: Reconciliation scheduler started
24
- INFO: 2025-12-10T11:53:13 - app.main: ✓ Daily reconciliation scheduler started (runs at midnight)
25
- INFO: 2025-12-10T11:53:13 - app.main: ============================================================
26
- INFO: 2025-12-10T11:53:13 - app.main: ✅ Startup complete | Ready to serve requests
27
- INFO: 2025-12-10T11:53:13 - 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: 10.16.37.13:12366 - "GET /health HTTP/1.1" 200 OK
31
- INFO: 10.16.37.13:15678 - "GET /health HTTP/1.1" 200 OK
32
- INFO: 2025-12-10T11:54:00 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
33
- INFO: 2025-12-10T11:54:00 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
34
- INFO: 2025-12-10T11:54:00 - app.services.dashboard_service: Dashboard cache MISS for project 0ade6bd1-e492-4e25-b681-59f42058d29a, user 43b778b0-2062-4724-abbb-916a4835a9b0 - building fresh data
35
- INFO: 2025-12-10T11:54:01 - app.services.dashboard_service: Built and cached dashboard for project 0ade6bd1-e492-4e25-b681-59f42058d29a
36
- INFO: 10.16.37.13:61239 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/dashboard HTTP/1.1" 200 OK
37
- INFO: 10.16.37.13:61684 - "GET /api/v1/tickets/1e622599-1909-49b9-9d8b-4c5cb483b29e/detail HTTP/1.1" 200 OK
38
- INFO: 2025-12-10T11:54:30 - app.services.ticket_expense_service: Auto-populated mobile money payment details for user 43b778b0-2062-4724-abbb-916a4835a9b0: phone=+2547994597823
39
- INFO: 2025-12-10T11:54:30 - app.services.ticket_expense_service: Expense created: db836b45-5d6b-4e67-a12a-4619c0fb3f38 by user 43b778b0-2062-4724-abbb-916a4835a9b0 for ticket 1e622599-1909-49b9-9d8b-4c5cb483b29e, amount: 80.00, location_verified: False
40
- ERROR: 2025-12-10T11:54:30 - app.services.reconciliation.reconciliation_service: Real-time timesheet update failed: user=43b778b0-2062-4724-abbb-916a4835a9b0, date=2025-12-10, trigger=expense_created, error=(psycopg2.errors.InvalidTextRepresentation) invalid input value for enum assignmentaction: "unassigned"
41
- LINE 67: ... WHERE da.action IN ('dropped', 'unassigne...
42
- ^
43
 
44
  [SQL:
45
- WITH daily_assignments AS (
46
- -- All assignments for this project/date
47
- SELECT
48
- ta.id as assignment_id,
49
- ta.user_id,
50
- ta.ticket_id,
51
- ta.action,
52
- ta.assigned_at,
53
- ta.ended_at,
54
- t.status as ticket_status,
55
- t.completed_at as ticket_completed_at
56
- FROM ticket_assignments ta
57
- JOIN tickets t ON ta.ticket_id = t.id
58
- WHERE t.project_id = %(project_id)s
59
- AND t.deleted_at IS NULL
60
- AND ta.deleted_at IS NULL
61
- AND DATE(ta.assigned_at) = %(target_date)s
62
- AND ta.user_id IN (%(user_id_0)s)
63
- ),
64
- daily_expenses AS (
65
- -- All expenses for this project/date
66
- SELECT
67
- te.ticket_assignment_id,
68
- te.incurred_by_user_id as user_id,
69
- te.total_cost,
70
- te.is_approved,
71
- te.rejection_reason,
72
- CASE
73
- WHEN te.is_approved = TRUE THEN 'approved'
74
- WHEN te.is_approved = FALSE AND te.rejection_reason IS NOT NULL THEN 'rejected'
75
- ELSE 'pending'
76
- END as approval_status
77
- FROM ticket_expenses te
78
- JOIN ticket_assignments ta ON te.ticket_assignment_id = ta.id
79
- JOIN tickets t ON ta.ticket_id = t.id
80
- WHERE t.project_id = %(project_id)s
81
- AND te.deleted_at IS NULL
82
- AND DATE(te.created_at) = %(target_date)s
83
- AND te.incurred_by_user_id IN (%(user_id_0)s)
84
- )
85
- SELECT
86
- COALESCE(da.user_id, de.user_id) as user_id,
87
-
88
- -- Check in/out times (first and last activity)
89
- MIN(da.assigned_at) as check_in_time,
90
- MAX(COALESCE(da.ended_at, da.assigned_at)) as check_out_time,
91
-
92
- -- Assignment counts by action type
93
- COUNT(DISTINCT da.assignment_id) as tickets_assigned,
94
-
95
- COUNT(DISTINCT da.assignment_id) FILTER (
96
- WHERE da.action = 'accepted'
97
- ) as tickets_accepted,
98
-
99
- COUNT(DISTINCT da.ticket_id) FILTER (
100
- WHERE da.action = 'accepted'
101
- AND da.ticket_status = 'completed'
102
- AND DATE(da.ended_at) = %(target_date)s
103
- ) as tickets_completed,
104
-
105
- COUNT(DISTINCT da.assignment_id) FILTER (
106
- WHERE da.action = 'rejected'
107
- ) as tickets_rejected,
108
-
109
- COUNT(DISTINCT da.assignment_id) FILTER (
110
- WHERE da.action IN ('dropped', 'unassigned')
111
- ) as tickets_cancelled,
112
-
113
- COUNT(DISTINCT da.assignment_id) FILTER (
114
- WHERE da.action = 'reassigned'
115
- ) as tickets_rescheduled,
116
-
117
- -- Expense aggregations by approval status
118
- COALESCE(SUM(de.total_cost), 0) as total_expenses,
119
- COALESCE(SUM(de.total_cost) FILTER (
120
- WHERE de.approval_status = 'approved'
121
- ), 0) as approved_expenses,
122
- COALESCE(SUM(de.total_cost) FILTER (
123
- WHERE de.approval_status = 'pending'
124
- ), 0) as pending_expenses,
125
- COALESCE(SUM(de.total_cost) FILTER (
126
- WHERE de.approval_status = 'rejected'
127
- ), 0) as rejected_expenses,
128
- COUNT(DISTINCT de.ticket_assignment_id) as expense_claims_count
129
-
130
- FROM daily_assignments da
131
- FULL OUTER JOIN daily_expenses de ON da.user_id = de.user_id
132
- GROUP BY COALESCE(da.user_id, de.user_id)
133
- HAVING COUNT(DISTINCT da.assignment_id) > 0
134
- OR COUNT(DISTINCT de.ticket_assignment_id) > 0
135
- ORDER BY tickets_completed DESC
136
- ]
137
- [parameters: {'project_id': '0ade6bd1-e492-4e25-b681-59f42058d29a', 'target_date': datetime.date(2025, 12, 10), 'user_id_0': '43b778b0-2062-4724-abbb-916a4835a9b0'}]
138
- (Background on this error at: https://sqlalche.me/e/20/9h9h)
139
  Traceback (most recent call last):
140
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
141
  self.dialect.do_execute(
142
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
143
  cursor.execute(statement, parameters)
144
- psycopg2.errors.InvalidTextRepresentation: invalid input value for enum assignmentaction: "unassigned"
145
- LINE 67: ... WHERE da.action IN ('dropped', 'unassigne...
146
- ^
147
 
148
 
149
  The above exception was the direct cause of the following exception:
150
 
151
  Traceback (most recent call last):
152
- File "/app/src/app/services/reconciliation/reconciliation_service.py", line 531, in update_user_timesheet_realtime
153
- agent_stats = self._aggregate_agent_activity(
154
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
155
- File "/app/src/app/services/reconciliation/reconciliation_service.py", line 321, in _aggregate_agent_activity
156
- result = self.db.execute(query, params)
157
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
158
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
159
  return self._execute_internal(
160
  ^^^^^^^^^^^^^^^^^^^^^^^
@@ -181,104 +117,48 @@ Traceback (most recent call last):
181
  self.dialect.do_execute(
182
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
183
  cursor.execute(statement, parameters)
184
- sqlalchemy.exc.DataError: (psycopg2.errors.InvalidTextRepresentation) invalid input value for enum assignmentaction: "unassigned"
185
- LINE 67: ... WHERE da.action IN ('dropped', 'unassigne...
186
- ^
187
 
188
  [SQL:
189
- WITH daily_assignments AS (
190
- -- All assignments for this project/date
191
- SELECT
192
- ta.id as assignment_id,
193
- ta.user_id,
194
- ta.ticket_id,
195
- ta.action,
196
- ta.assigned_at,
197
- ta.ended_at,
198
- t.status as ticket_status,
199
- t.completed_at as ticket_completed_at
200
- FROM ticket_assignments ta
201
- JOIN tickets t ON ta.ticket_id = t.id
202
- WHERE t.project_id = %(project_id)s
203
- AND t.deleted_at IS NULL
204
- AND ta.deleted_at IS NULL
205
- AND DATE(ta.assigned_at) = %(target_date)s
206
- AND ta.user_id IN (%(user_id_0)s)
207
- ),
208
- daily_expenses AS (
209
- -- All expenses for this project/date
210
- SELECT
211
- te.ticket_assignment_id,
212
- te.incurred_by_user_id as user_id,
213
- te.total_cost,
214
- te.is_approved,
215
- te.rejection_reason,
216
- CASE
217
- WHEN te.is_approved = TRUE THEN 'approved'
218
- WHEN te.is_approved = FALSE AND te.rejection_reason IS NOT NULL THEN 'rejected'
219
- ELSE 'pending'
220
- END as approval_status
221
- FROM ticket_expenses te
222
- JOIN ticket_assignments ta ON te.ticket_assignment_id = ta.id
223
- JOIN tickets t ON ta.ticket_id = t.id
224
- WHERE t.project_id = %(project_id)s
225
- AND te.deleted_at IS NULL
226
- AND DATE(te.created_at) = %(target_date)s
227
- AND te.incurred_by_user_id IN (%(user_id_0)s)
228
- )
229
- SELECT
230
- COALESCE(da.user_id, de.user_id) as user_id,
231
-
232
- -- Check in/out times (first and last activity)
233
- MIN(da.assigned_at) as check_in_time,
234
- MAX(COALESCE(da.ended_at, da.assigned_at)) as check_out_time,
235
-
236
- -- Assignment counts by action type
237
- COUNT(DISTINCT da.assignment_id) as tickets_assigned,
238
-
239
- COUNT(DISTINCT da.assignment_id) FILTER (
240
- WHERE da.action = 'accepted'
241
- ) as tickets_accepted,
242
-
243
- COUNT(DISTINCT da.ticket_id) FILTER (
244
- WHERE da.action = 'accepted'
245
- AND da.ticket_status = 'completed'
246
- AND DATE(da.ended_at) = %(target_date)s
247
- ) as tickets_completed,
248
-
249
- COUNT(DISTINCT da.assignment_id) FILTER (
250
- WHERE da.action = 'rejected'
251
- ) as tickets_rejected,
252
-
253
- COUNT(DISTINCT da.assignment_id) FILTER (
254
- WHERE da.action IN ('dropped', 'unassigned')
255
- ) as tickets_cancelled,
256
-
257
- COUNT(DISTINCT da.assignment_id) FILTER (
258
- WHERE da.action = 'reassigned'
259
- ) as tickets_rescheduled,
260
-
261
- -- Expense aggregations by approval status
262
- COALESCE(SUM(de.total_cost), 0) as total_expenses,
263
- COALESCE(SUM(de.total_cost) FILTER (
264
- WHERE de.approval_status = 'approved'
265
- ), 0) as approved_expenses,
266
- COALESCE(SUM(de.total_cost) FILTER (
267
- WHERE de.approval_status = 'pending'
268
- ), 0) as pending_expenses,
269
- COALESCE(SUM(de.total_cost) FILTER (
270
- WHERE de.approval_status = 'rejected'
271
- ), 0) as rejected_expenses,
272
- COUNT(DISTINCT de.ticket_assignment_id) as expense_claims_count
273
-
274
- FROM daily_assignments da
275
- FULL OUTER JOIN daily_expenses de ON da.user_id = de.user_id
276
- GROUP BY COALESCE(da.user_id, de.user_id)
277
- HAVING COUNT(DISTINCT da.assignment_id) > 0
278
- OR COUNT(DISTINCT de.ticket_assignment_id) > 0
279
- ORDER BY tickets_completed DESC
280
- ]
281
- [parameters: {'project_id': '0ade6bd1-e492-4e25-b681-59f42058d29a', 'target_date': datetime.date(2025, 12, 10), 'user_id_0': '43b778b0-2062-4724-abbb-916a4835a9b0'}]
282
- (Background on this error at: https://sqlalche.me/e/20/9h9h)
283
- INFO: 10.16.37.13:38994 - "POST /api/v1/ticket-expenses HTTP/1.1" 201 Created
284
- INFO: 10.16.13.79:8146 - "GET /api/v1/tickets/1e622599-1909-49b9-9d8b-4c5cb483b29e/detail HTTP/1.1" 200 OK
 
1
+ ===== Application Startup at 2025-12-10 12:01:23 =====
2
 
3
  INFO: Started server process [7]
4
  INFO: Waiting for application startup.
5
+ INFO: 2025-12-10T12:01:35 - app.main: ============================================================
6
+ INFO: 2025-12-10T12:01:35 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
+ INFO: 2025-12-10T12:01:35 - app.main: 📊 Dashboard: Enabled
8
+ INFO: 2025-12-10T12:01:35 - app.main: ============================================================
9
+ INFO: 2025-12-10T12:01:35 - app.main: 📦 Database:
10
+ INFO: 2025-12-10T12:01:35 - app.main: ✓ Connected | 47 tables | 6 users
11
+ INFO: 2025-12-10T12:01:35 - app.main: 💾 Cache & Sessions:
12
+ INFO: 2025-12-10T12:01:36 - app.services.otp_service: ✅ OTP Service initialized with Redis storage
13
+ INFO: 2025-12-10T12:01:37 - app.main: ✓ Redis: Connected
14
+ INFO: 2025-12-10T12:01:37 - app.main: 🔌 External Services:
15
+ INFO: 2025-12-10T12:01:37 - app.main: ✓ Cloudinary: Connected
16
+ INFO: 2025-12-10T12:01:37 - app.main: ✓ Resend: Configured
17
+ INFO: 2025-12-10T12:01:37 - app.main: ○ WASender: Disconnected
18
+ INFO: 2025-12-10T12:01:37 - app.main: ✓ Supabase: Connected | 6 buckets
19
+ INFO: 2025-12-10T12:01:37 - app.main: ⏰ Scheduler:
20
+ INFO: 2025-12-10T12:01:37 - apscheduler.scheduler: Adding job tentatively -- it will be properly scheduled when the scheduler starts
21
+ INFO: 2025-12-10T12:01:37 - apscheduler.scheduler: Added job "Daily Field Agent Reconciliation" to job store "default"
22
+ INFO: 2025-12-10T12:01:37 - apscheduler.scheduler: Scheduler started
23
+ INFO: 2025-12-10T12:01:37 - app.tasks.scheduler: Reconciliation scheduler started
24
+ INFO: 2025-12-10T12:01:37 - app.main: ✓ Daily reconciliation scheduler started (runs at midnight)
25
+ INFO: 2025-12-10T12:01:37 - app.main: ============================================================
26
+ INFO: 2025-12-10T12:01:37 - app.main: ✅ Startup complete | Ready to serve requests
27
+ INFO: 2025-12-10T12:01:37 - 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: 10.16.13.79:55428 - "GET /health HTTP/1.1" 200 OK
31
+ INFO: 2025-12-10T12:02:07 - app.services.ticket_expense_service: Expense updated: db836b45-5d6b-4e67-a12a-4619c0fb3f38 by user 43b778b0-2062-4724-abbb-916a4835a9b0
32
+ INFO: 10.16.13.79:61819 - "PUT /api/v1/ticket-expenses/db836b45-5d6b-4e67-a12a-4619c0fb3f38 HTTP/1.1" 200 OK
33
+ INFO: 10.16.13.79:61819 - "GET /api/v1/tickets/1e622599-1909-49b9-9d8b-4c5cb483b29e/detail HTTP/1.1" 200 OK
34
+ INFO: 10.16.37.13:37986 - "GET /health HTTP/1.1" 200 OK
35
+ INFO: 2025-12-10T12:02:36 - app.services.ticket_expense_service: Auto-populated mobile money payment details for user 43b778b0-2062-4724-abbb-916a4835a9b0: phone=+2547994597823
36
+ INFO: 2025-12-10T12:02:36 - app.services.ticket_expense_service: Expense created: 5d0a1d44-0eb4-4b32-90ec-5cbdda883238 by user 43b778b0-2062-4724-abbb-916a4835a9b0 for ticket 1e622599-1909-49b9-9d8b-4c5cb483b29e, amount: 150.00, location_verified: False
37
+ ERROR: 2025-12-10T12:02:36 - app.services.reconciliation.reconciliation_service: Real-time timesheet update failed: user=43b778b0-2062-4724-abbb-916a4835a9b0, date=2025-12-10, trigger=expense_created, error=(psycopg2.errors.InvalidColumnReference) there is no unique or exclusion constraint matching the ON CONFLICT specification
 
 
 
 
 
38
 
39
  [SQL:
40
+ INSERT INTO timesheets (
41
+ user_id, project_id, work_date,
42
+ check_in_time, check_out_time,
43
+ tickets_assigned, tickets_completed, tickets_rejected,
44
+ tickets_cancelled, tickets_rescheduled,
45
+ total_expenses, approved_expenses, pending_expenses, rejected_expenses,
46
+ expense_claims_count,
47
+ update_source, last_realtime_update_at,
48
+ status, created_at, updated_at
49
+ ) VALUES (
50
+ %(user_id)s, %(project_id)s, %(work_date)s,
51
+ %(check_in_time)s, %(check_out_time)s,
52
+ %(tickets_assigned)s, %(tickets_completed)s, %(tickets_rejected)s,
53
+ %(tickets_cancelled)s, %(tickets_rescheduled)s,
54
+ %(total_expenses)s, %(approved_expenses)s, %(pending_expenses)s, %(rejected_expenses)s,
55
+ %(expense_claims_count)s,
56
+ 'realtime', NOW(),
57
+ 'present', NOW(), NOW()
58
+ )
59
+ ON CONFLICT (user_id, work_date)
60
+ DO UPDATE SET
61
+ check_in_time = EXCLUDED.check_in_time,
62
+ check_out_time = EXCLUDED.check_out_time,
63
+ tickets_assigned = EXCLUDED.tickets_assigned,
64
+ tickets_completed = EXCLUDED.tickets_completed,
65
+ tickets_rejected = EXCLUDED.tickets_rejected,
66
+ tickets_cancelled = EXCLUDED.tickets_cancelled,
67
+ tickets_rescheduled = EXCLUDED.tickets_rescheduled,
68
+ total_expenses = EXCLUDED.total_expenses,
69
+ approved_expenses = EXCLUDED.approved_expenses,
70
+ pending_expenses = EXCLUDED.pending_expenses,
71
+ rejected_expenses = EXCLUDED.rejected_expenses,
72
+ expense_claims_count = EXCLUDED.expense_claims_count,
73
+ update_source = 'realtime',
74
+ last_realtime_update_at = NOW(),
75
+ updated_at = NOW()
76
+ RETURNING id
77
+ ]
78
+ [parameters: {'user_id': '43b778b0-2062-4724-abbb-916a4835a9b0', 'project_id': '0ade6bd1-e492-4e25-b681-59f42058d29a', 'work_date': datetime.date(2025, 12, 10), 'check_in_time': datetime.datetime(2025, 12, 10, 11, 5, 29, 209659, tzinfo=datetime.timezone.utc), 'check_out_time': datetime.datetime(2025, 12, 10, 11, 5, 29, 209659, tzinfo=datetime.timezone.utc), 'tickets_assigned': 1, 'tickets_completed': 0, 'tickets_rejected': 0, 'tickets_cancelled': 0, 'tickets_rescheduled': 0, 'total_expenses': 2550.0, 'approved_expenses': 0.0, 'pending_expenses': 2550.0, 'rejected_expenses': 0.0, 'expense_claims_count': 2}]
79
+ (Background on this error at: https://sqlalche.me/e/20/f405)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  Traceback (most recent call last):
81
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
82
  self.dialect.do_execute(
83
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
84
  cursor.execute(statement, parameters)
85
+ psycopg2.errors.InvalidColumnReference: there is no unique or exclusion constraint matching the ON CONFLICT specification
 
 
86
 
87
 
88
  The above exception was the direct cause of the following exception:
89
 
90
  Traceback (most recent call last):
91
+ File "/app/src/app/services/reconciliation/reconciliation_service.py", line 587, in update_user_timesheet_realtime
92
+ result = self.db.execute(query, {
93
+ ^^^^^^^^^^^^^^^^^^^^^^^^
 
 
 
94
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
95
  return self._execute_internal(
96
  ^^^^^^^^^^^^^^^^^^^^^^^
 
117
  self.dialect.do_execute(
118
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
119
  cursor.execute(statement, parameters)
120
+ sqlalchemy.exc.ProgrammingError: (psycopg2.errors.InvalidColumnReference) there is no unique or exclusion constraint matching the ON CONFLICT specification
 
 
121
 
122
  [SQL:
123
+ INSERT INTO timesheets (
124
+ user_id, project_id, work_date,
125
+ check_in_time, check_out_time,
126
+ tickets_assigned, tickets_completed, tickets_rejected,
127
+ tickets_cancelled, tickets_rescheduled,
128
+ total_expenses, approved_expenses, pending_expenses, rejected_expenses,
129
+ expense_claims_count,
130
+ update_source, last_realtime_update_at,
131
+ status, created_at, updated_at
132
+ ) VALUES (
133
+ %(user_id)s, %(project_id)s, %(work_date)s,
134
+ %(check_in_time)s, %(check_out_time)s,
135
+ %(tickets_assigned)s, %(tickets_completed)s, %(tickets_rejected)s,
136
+ %(tickets_cancelled)s, %(tickets_rescheduled)s,
137
+ %(total_expenses)s, %(approved_expenses)s, %(pending_expenses)s, %(rejected_expenses)s,
138
+ %(expense_claims_count)s,
139
+ 'realtime', NOW(),
140
+ 'present', NOW(), NOW()
141
+ )
142
+ ON CONFLICT (user_id, work_date)
143
+ DO UPDATE SET
144
+ check_in_time = EXCLUDED.check_in_time,
145
+ check_out_time = EXCLUDED.check_out_time,
146
+ tickets_assigned = EXCLUDED.tickets_assigned,
147
+ tickets_completed = EXCLUDED.tickets_completed,
148
+ tickets_rejected = EXCLUDED.tickets_rejected,
149
+ tickets_cancelled = EXCLUDED.tickets_cancelled,
150
+ tickets_rescheduled = EXCLUDED.tickets_rescheduled,
151
+ total_expenses = EXCLUDED.total_expenses,
152
+ approved_expenses = EXCLUDED.approved_expenses,
153
+ pending_expenses = EXCLUDED.pending_expenses,
154
+ rejected_expenses = EXCLUDED.rejected_expenses,
155
+ expense_claims_count = EXCLUDED.expense_claims_count,
156
+ update_source = 'realtime',
157
+ last_realtime_update_at = NOW(),
158
+ updated_at = NOW()
159
+ RETURNING id
160
+ ]
161
+ [parameters: {'user_id': '43b778b0-2062-4724-abbb-916a4835a9b0', 'project_id': '0ade6bd1-e492-4e25-b681-59f42058d29a', 'work_date': datetime.date(2025, 12, 10), 'check_in_time': datetime.datetime(2025, 12, 10, 11, 5, 29, 209659, tzinfo=datetime.timezone.utc), 'check_out_time': datetime.datetime(2025, 12, 10, 11, 5, 29, 209659, tzinfo=datetime.timezone.utc), 'tickets_assigned': 1, 'tickets_completed': 0, 'tickets_rejected': 0, 'tickets_cancelled': 0, 'tickets_rescheduled': 0, 'total_expenses': 2550.0, 'approved_expenses': 0.0, 'pending_expenses': 2550.0, 'rejected_expenses': 0.0, 'expense_claims_count': 2}]
162
+ (Background on this error at: https://sqlalche.me/e/20/f405)
163
+ INFO: 10.16.37.13:6184 - "POST /api/v1/ticket-expenses HTTP/1.1" 201 Created
164
+ INFO: 10.16.13.79:20609 - "GET /api/v1/tickets/1e622599-1909-49b9-9d8b-4c5cb483b29e/detail HTTP/1.1" 200 OK
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/services/reconciliation/reconciliation_service.py CHANGED
@@ -543,7 +543,7 @@ class ReconciliationService:
543
 
544
  stats = agent_stats[0]
545
 
546
- # Step 2: Upsert timesheet with real-time source
547
  query = text("""
548
  INSERT INTO timesheets (
549
  user_id, project_id, work_date,
@@ -564,7 +564,7 @@ class ReconciliationService:
564
  'realtime', NOW(),
565
  'present', NOW(), NOW()
566
  )
567
- ON CONFLICT (user_id, work_date)
568
  DO UPDATE SET
569
  check_in_time = EXCLUDED.check_in_time,
570
  check_out_time = EXCLUDED.check_out_time,
 
543
 
544
  stats = agent_stats[0]
545
 
546
+ # Step 2: Upsert timesheet with all columns (migrations confirmed run)
547
  query = text("""
548
  INSERT INTO timesheets (
549
  user_id, project_id, work_date,
 
564
  'realtime', NOW(),
565
  'present', NOW(), NOW()
566
  )
567
+ ON CONFLICT (user_id, work_date)
568
  DO UPDATE SET
569
  check_in_time = EXCLUDED.check_in_time,
570
  check_out_time = EXCLUDED.check_out_time,
supabase/migrations/20241210_add_inventory_to_timesheets.sql CHANGED
@@ -104,20 +104,21 @@ ALTER TABLE timesheet_updates
104
  ));
105
 
106
  -- =====================================================
107
- -- 3. ADD CLOCK IN/OUT TIMES TO TIMESHEETS
108
  -- =====================================================
109
 
110
  -- Track when agent started and ended work for the day
 
111
  ALTER TABLE timesheets
112
- ADD COLUMN IF NOT EXISTS clock_in_time TIMESTAMP WITH TIME ZONE,
113
- ADD COLUMN IF NOT EXISTS clock_out_time TIMESTAMP WITH TIME ZONE;
114
 
115
- COMMENT ON COLUMN timesheets.clock_in_time IS 'First activity of the day (earliest assignment start or journey start)';
116
- COMMENT ON COLUMN timesheets.clock_out_time IS 'Last activity of the day (latest assignment completion or last assignment start)';
117
 
118
- -- Add index for clock time queries
119
- CREATE INDEX IF NOT EXISTS idx_timesheets_clock_times
120
- ON timesheets(user_id, work_date, clock_in_time, clock_out_time)
121
  WHERE deleted_at IS NULL;
122
 
123
  -- =====================================================
 
104
  ));
105
 
106
  -- =====================================================
107
+ -- 3. ADD CHECK IN/OUT TIMES TO TIMESHEETS
108
  -- =====================================================
109
 
110
  -- Track when agent started and ended work for the day
111
+ -- Note: Using check_in/check_out (not clock_in/clock_out) for modern terminology
112
  ALTER TABLE timesheets
113
+ ADD COLUMN IF NOT EXISTS check_in_time TIMESTAMP WITH TIME ZONE,
114
+ ADD COLUMN IF NOT EXISTS check_out_time TIMESTAMP WITH TIME ZONE;
115
 
116
+ COMMENT ON COLUMN timesheets.check_in_time IS 'First activity of the day (earliest assignment start or journey start)';
117
+ COMMENT ON COLUMN timesheets.check_out_time IS 'Last activity of the day (latest assignment completion or last assignment start)';
118
 
119
+ -- Add index for check time queries
120
+ CREATE INDEX IF NOT EXISTS idx_timesheets_check_times
121
+ ON timesheets(user_id, work_date, check_in_time, check_out_time)
122
  WHERE deleted_at IS NULL;
123
 
124
  -- =====================================================