kamau1 commited on
Commit
d12a170
·
1 Parent(s): b00cab2

refactor: remove reconciliation system and all related code, tasks, and docs

Browse files
docs/agent/implementation-notes/RECONCILIATION_REMOVAL_COMPLETE.md ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ✅ Reconciliation System Removal - COMPLETE
2
+
3
+ **Date:** 2025-12-12
4
+ **Status:** Code changes complete, database migration pending
5
+
6
+ ---
7
+
8
+ ## Summary
9
+
10
+ The reconciliation system has been **completely removed** from the codebase. All that remains is to run the database migration in Supabase.
11
+
12
+ ---
13
+
14
+ ## What Was Removed
15
+
16
+ ### 1. ✅ Service Files (Deleted)
17
+ - `src/app/services/reconciliation/__init__.py`
18
+ - `src/app/services/reconciliation/reconciliation_service.py`
19
+ - `src/app/services/reconciliation/anomaly_detector.py`
20
+ - `src/app/services/reconciliation/models.py`
21
+
22
+ ### 2. ✅ API Endpoints (Deleted)
23
+ - `src/app/api/endpoints/reconciliation.py`
24
+
25
+ ### 3. ✅ Scheduler (Deleted)
26
+ - `src/app/tasks/scheduler.py`
27
+
28
+ ### 4. ✅ API Router Integration (Removed)
29
+ - Removed reconciliation import from `src/app/api/v1/router.py`
30
+ - Removed reconciliation router registration
31
+
32
+ ### 5. ✅ Scheduler Integration (Removed)
33
+ - Removed scheduler imports from `src/app/main.py`
34
+ - Removed scheduler startup/shutdown calls
35
+ - Updated `src/app/tasks/__init__.py`
36
+
37
+ ### 6. ✅ Real-time Update Calls (Removed)
38
+ **File:** `src/app/api/v1/ticket_assignments.py`
39
+ - Removed ReconciliationService import
40
+ - Removed 3 real-time update calls (assign, self-assign, complete)
41
+
42
+ **File:** `src/app/api/v1/ticket_expenses.py`
43
+ - Removed ReconciliationService import
44
+ - Removed 3 real-time update calls (create, approve, bulk approve)
45
+
46
+ **File:** `src/app/api/v1/ticket_completion.py`
47
+ - Removed ReconciliationService import
48
+ - Removed 1 real-time update call (ticket completed)
49
+
50
+ ### 7. ✅ Documentation Comments (Cleaned)
51
+ - Updated leave approval comment in `src/app/api/v1/timesheets.py`
52
+
53
+ ---
54
+
55
+ ## Verification
56
+
57
+ ### Python Code ✅
58
+ - **No remaining imports** of ReconciliationService
59
+ - **No remaining calls** to reconciliation methods
60
+ - **No remaining references** to reconciliation in active code
61
+ - Only references are in documentation files (expected)
62
+
63
+ ### Files Modified
64
+ 1. `src/app/api/v1/router.py` - Removed reconciliation router
65
+ 2. `src/app/main.py` - Removed scheduler startup/shutdown
66
+ 3. `src/app/tasks/__init__.py` - Removed scheduler exports
67
+ 4. `src/app/api/v1/ticket_assignments.py` - Removed 3 reconciliation calls
68
+ 5. `src/app/api/v1/ticket_expenses.py` - Removed 3 reconciliation calls
69
+ 6. `src/app/api/v1/ticket_completion.py` - Removed 1 reconciliation call
70
+ 7. `src/app/api/v1/timesheets.py` - Updated comment
71
+
72
+ ### Files Deleted
73
+ 1. `src/app/services/reconciliation/__init__.py`
74
+ 2. `src/app/services/reconciliation/reconciliation_service.py`
75
+ 3. `src/app/services/reconciliation/anomaly_detector.py`
76
+ 4. `src/app/services/reconciliation/models.py`
77
+ 5. `src/app/api/endpoints/reconciliation.py`
78
+ 6. `src/app/tasks/scheduler.py`
79
+
80
+ ---
81
+
82
+ ## 🔴 NEXT STEP: Database Migration
83
+
84
+ ### Run This Migration in Supabase SQL Editor
85
+
86
+ **File:** `supabase/migrations/20241212_remove_reconciliation_system.sql`
87
+
88
+ **What it does:**
89
+ 1. Drops `reconciliation_runs` table
90
+ 2. Drops `timesheet_updates` table
91
+ 3. Removes reconciliation columns from `timesheets` table:
92
+ - `reconciliation_run_id`
93
+ - `last_reconciled_at`
94
+ - `update_source`
95
+ - `last_realtime_update_at`
96
+ - `last_validated_at`
97
+ - `needs_review`
98
+ - `discrepancy_notes`
99
+ - `version`
100
+ 4. Drops all reconciliation-related indexes
101
+ 5. Drops reconciliation helper functions
102
+
103
+ ### How to Run
104
+
105
+ 1. Open Supabase Dashboard
106
+ 2. Go to SQL Editor
107
+ 3. Copy the contents of `supabase/migrations/20241212_remove_reconciliation_system.sql`
108
+ 4. Paste and execute
109
+ 5. Verify success message: "Reconciliation system successfully removed"
110
+
111
+ ### Post-Migration Verification
112
+
113
+ Run these queries in Supabase SQL Editor to verify cleanup:
114
+
115
+ ```sql
116
+ -- 1. Check for any remaining reconciliation columns in timesheets
117
+ SELECT column_name
118
+ FROM information_schema.columns
119
+ WHERE table_name = 'timesheets'
120
+ AND column_name LIKE '%reconcil%';
121
+ -- Expected: 0 rows
122
+
123
+ -- 2. Check for any remaining reconciliation indexes
124
+ SELECT indexname
125
+ FROM pg_indexes
126
+ WHERE indexname LIKE '%reconcil%';
127
+ -- Expected: 0 rows
128
+
129
+ -- 3. Check for any remaining reconciliation functions
130
+ SELECT routine_name
131
+ FROM information_schema.routines
132
+ WHERE routine_name LIKE '%reconcil%';
133
+ -- Expected: 0 rows
134
+
135
+ -- 4. Verify tables are dropped
136
+ SELECT table_name
137
+ FROM information_schema.tables
138
+ WHERE table_name IN ('reconciliation_runs', 'timesheet_updates');
139
+ -- Expected: 0 rows
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Impact Assessment
145
+
146
+ ### ✅ Zero Risk
147
+ - Reconciliation service was already broken (failing nightly)
148
+ - Real-time updates were redundant (not used for business logic)
149
+ - Audit tables were never queried
150
+ - No frontend dependencies on reconciliation endpoints
151
+
152
+ ### ✅ No Functionality Loss
153
+ - Timesheets still exist and work normally
154
+ - All ticket metrics still tracked
155
+ - All expense metrics still tracked
156
+ - Payroll integration unaffected
157
+
158
+ ### ✅ Benefits
159
+ - Removed 1000+ lines of unused code
160
+ - Eliminated nightly job failures
161
+ - Simplified codebase
162
+ - Reduced database overhead
163
+ - Cleaner architecture
164
+
165
+ ---
166
+
167
+ ## Rollback Plan (If Needed)
168
+
169
+ If you need to rollback:
170
+
171
+ 1. **Restore from Git:**
172
+ ```bash
173
+ git checkout HEAD~1 -- src/app/services/reconciliation/
174
+ git checkout HEAD~1 -- src/app/api/endpoints/reconciliation.py
175
+ git checkout HEAD~1 -- src/app/tasks/scheduler.py
176
+ git checkout HEAD~1 -- src/app/api/v1/router.py
177
+ git checkout HEAD~1 -- src/app/main.py
178
+ git checkout HEAD~1 -- src/app/tasks/__init__.py
179
+ git checkout HEAD~1 -- src/app/api/v1/ticket_assignments.py
180
+ git checkout HEAD~1 -- src/app/api/v1/ticket_expenses.py
181
+ git checkout HEAD~1 -- src/app/api/v1/ticket_completion.py
182
+ ```
183
+
184
+ 2. **Restore Database:**
185
+ - Restore from Supabase backup taken before migration
186
+ - Or re-run original migrations:
187
+ - `20241209_add_reconciliation_system.sql`
188
+ - `20241210_add_realtime_reconciliation.sql`
189
+
190
+ ---
191
+
192
+ ## Documentation
193
+
194
+ The following documentation files remain for historical reference:
195
+ - `docs/features/reconciliation-system-analysis.md` - Analysis of what was removed
196
+ - `docs/features/reconciliation-removal-plan.md` - Original removal plan
197
+ - `docs/features/realtime-timesheet-updates.md` - Real-time update design
198
+ - `docs/features/timesheets/RECONCILIATION_SYSTEM.md` - Original system docs
199
+
200
+ These can be moved to an archive folder or deleted if not needed.
201
+
202
+ ---
203
+
204
+ ## ✅ Completion Checklist
205
+
206
+ - [x] Remove reconciliation service files
207
+ - [x] Remove reconciliation API endpoints
208
+ - [x] Remove scheduler integration
209
+ - [x] Remove reconciliation from API router
210
+ - [x] Remove reconciliation calls from ticket_assignments.py
211
+ - [x] Remove reconciliation calls from ticket_expenses.py
212
+ - [x] Remove reconciliation calls from ticket_completion.py
213
+ - [x] Clean up documentation comments
214
+ - [x] Verify no remaining Python references
215
+ - [x] Create database migration script
216
+ - [ ] **Run database migration in Supabase** ⬅️ YOU ARE HERE
217
+ - [ ] Verify migration success with post-migration queries
218
+ - [ ] Test application startup (no scheduler errors)
219
+ - [ ] Test ticket assignment creation (no reconciliation errors)
220
+ - [ ] Test expense creation (no reconciliation errors)
221
+ - [ ] Commit changes to Git
222
+
223
+ ---
224
+
225
+ **Ready to proceed with database migration!**
226
+
docs/devlogs/browser/browserconsole.txt CHANGED
@@ -1,391 +0,0 @@
1
- ===== Application Startup at 2025-12-11 11:40:56 =====
2
-
3
- INFO: Started server process [7]
4
- INFO: Waiting for application startup.
5
- INFO: 2025-12-11T11:41:07 - app.main: ============================================================
6
- INFO: 2025-12-11T11:41:07 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
- INFO: 2025-12-11T11:41:07 - app.main: 📊 Dashboard: Enabled
8
- INFO: 2025-12-11T11:41:07 - app.main: ============================================================
9
- INFO: 2025-12-11T11:41:07 - app.main: 📦 Database:
10
- INFO: 2025-12-11T11:41:07 - app.main: ✓ Connected | 47 tables | 6 users
11
- INFO: 2025-12-11T11:41:07 - app.main: 💾 Cache & Sessions:
12
- INFO: 2025-12-11T11:41:08 - app.services.otp_service: ✅ OTP Service initialized with Redis storage
13
- INFO: 2025-12-11T11:41:09 - app.main: ✓ Redis: Connected
14
- INFO: 2025-12-11T11:41:09 - app.main: 🔌 External Services:
15
- INFO: 2025-12-11T11:41:09 - app.main: ✓ Cloudinary: Connected
16
- INFO: 2025-12-11T11:41:09 - app.main: ✓ Resend: Configured
17
- INFO: 2025-12-11T11:41:09 - app.main: ○ WASender: Disconnected
18
- INFO: 2025-12-11T11:41:09 - app.main: ✓ Supabase: Connected | 6 buckets
19
- INFO: 2025-12-11T11:41:09 - app.main: ⏰ Scheduler:
20
- INFO: 2025-12-11T11:41:09 - apscheduler.scheduler: Adding job tentatively -- it will be properly scheduled when the scheduler starts
21
- INFO: 2025-12-11T11:41:09 - apscheduler.scheduler: Added job "Daily Field Agent Reconciliation" to job store "default"
22
- INFO: 2025-12-11T11:41:09 - apscheduler.scheduler: Scheduler started
23
- INFO: 2025-12-11T11:41:09 - app.tasks.scheduler: Reconciliation scheduler started (runs at 10 PM Africa/Nairobi)
24
- INFO: 2025-12-11T11:41:09 - app.main: ✓ Daily reconciliation scheduler started (runs at midnight)
25
- INFO: 2025-12-11T11:41:09 - app.main: ============================================================
26
- INFO: 2025-12-11T11:41:09 - app.main: ✅ Startup complete | Ready to serve requests
27
- INFO: 2025-12-11T11:41:09 - 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:37570 - "GET /health HTTP/1.1" 200 OK
31
- INFO: 2025-12-11T11:42:59 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
32
- INFO: 2025-12-11T11:42:59 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
33
- INFO: 2025-12-11T11:42:59 - app.services.project_service: Updated role 7e0f7731-737c-4fb3-8b1a-fc98e54680b4
34
- INFO: 2025-12-11T11:42:59 - app.services.audit_service: Audit log created: update on project_role by nadina73@nembors.com
35
- INFO: 10.16.37.13:49324 - "PATCH /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/project-roles/7e0f7731-737c-4fb3-8b1a-fc98e54680b4 HTTP/1.1" 200 OK
36
- INFO: 2025-12-11T11:43:00 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
37
- INFO: 2025-12-11T11:43:00 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
38
- INFO: 2025-12-11T11:43:00 - app.services.dashboard_service: Overview cache MISS for project 0ade6bd1-e492-4e25-b681-59f42058d29a, user c5cf92be-4172-4fe2-af5c-f05d83b3a938 - building fresh data
39
- INFO: 2025-12-11T11:43:00 - app.services.dashboard_service: Built and cached overview for project 0ade6bd1-e492-4e25-b681-59f42058d29a
40
- INFO: 10.16.13.79:43025 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/overview?refresh=true HTTP/1.1" 200 OK
41
- INFO: 10.16.13.79:16533 - "GET /health HTTP/1.1" 200 OK
42
- INFO: 10.16.13.79:58696 - "GET /health HTTP/1.1" 200 OK
43
- INFO: 2025-12-11T11:50:28 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
44
- INFO: 2025-12-11T11:50:28 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
45
- INFO: 10.16.37.13:15635 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
46
- INFO: 2025-12-11T11:50:29 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
47
- INFO: 2025-12-11T11:50:29 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
48
- INFO: 10.16.13.79:58696 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
49
- INFO: 2025-12-11T11:50:29 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
50
- INFO: 2025-12-11T11:50:29 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
51
- INFO: 10.16.37.13:15635 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
52
- INFO: 10.16.37.13:7153 - "GET /health HTTP/1.1" 200 OK
53
- INFO: 10.16.37.13:9912 - "GET /health HTTP/1.1" 200 OK
54
- INFO: 10.16.13.79:59040 - "GET /health HTTP/1.1" 200 OK
55
- INFO: 10.16.13.79:26106 - "GET /health HTTP/1.1" 200 OK
56
- INFO: 2025-12-11T12:01:31 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
57
- INFO: 2025-12-11T12:01:31 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
58
- INFO: 10.16.37.13:47452 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
59
- INFO: 2025-12-11T12:01:32 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
60
- INFO: 2025-12-11T12:01:32 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
61
- INFO: 10.16.37.13:47452 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
62
- INFO: 2025-12-11T12:01:32 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
63
- INFO: 2025-12-11T12:01:32 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
64
- INFO: 10.16.13.79:26106 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
65
- INFO: 2025-12-11T12:01:33 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
66
- INFO: 2025-12-11T12:01:33 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
67
- INFO: 2025-12-11T12:01:33 - app.services.dashboard_service: Overview cache HIT for project 0ade6bd1-e492-4e25-b681-59f42058d29a, user c5cf92be-4172-4fe2-af5c-f05d83b3a938
68
- INFO: 10.16.13.79:26106 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/overview HTTP/1.1" 200 OK
69
- INFO: 10.16.37.13:47452 - "GET /api/v1/notifications/stats HTTP/1.1" 200 OK
70
- INFO: 2025-12-11T12:01:36 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
71
- INFO: 2025-12-11T12:01:36 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
72
- INFO: 2025-12-11T12:01:36 - app.services.audit_service: Audit log created: logout on auth by system
73
- INFO: 2025-12-11T12:01:36 - app.api.v1.auth: User logged out: nadina73@nembors.com
74
- INFO: 10.16.37.13:47452 - "POST /api/v1/auth/logout HTTP/1.1" 200 OK
75
- INFO: 2025-12-11T12:01:38 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
76
- INFO: 2025-12-11T12:01:39 - app.services.audit_service: Audit log created: login on auth by viyisa8151@feralrex.com
77
- INFO: 2025-12-11T12:01:39 - app.api.v1.auth: User logged in successfully: viyisa8151@feralrex.com
78
- INFO: 10.16.37.13:47452 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
79
- INFO: 2025-12-11T12:01:39 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
80
- INFO: 2025-12-11T12:01:39 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
81
- INFO: 10.16.13.79:31245 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
82
- INFO: 2025-12-11T12:01:40 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
83
- INFO: 2025-12-11T12:01:40 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
84
- INFO: 10.16.13.79:31245 - "GET /api/v1/analytics/user/overview?limit=50 HTTP/1.1" 200 OK
85
- INFO: 2025-12-11T12:01:41 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
86
- INFO: 2025-12-11T12:01:41 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
87
- INFO: 10.16.37.13:47452 - "GET /api/v1/profile/me/validation HTTP/1.1" 200 OK
88
- INFO: 2025-12-11T12:01:41 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
89
- INFO: 2025-12-11T12:01:41 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
90
- INFO: 2025-12-11T12:01:41 - app.services.project_service: Listed 1 projects (total: 1) for user 43b778b0-2062-4724-abbb-916a4835a9b0
91
- INFO: 10.16.13.79:31245 - "GET /api/v1/projects?page=1&per_page=100&status=active HTTP/1.1" 200 OK
92
- INFO: 2025-12-11T12:01:45 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
93
- INFO: 2025-12-11T12:01:45 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
94
- INFO: 2025-12-11T12:01:45 - app.services.dashboard_service: Dashboard cache MISS for project 0ade6bd1-e492-4e25-b681-59f42058d29a, user 43b778b0-2062-4724-abbb-916a4835a9b0 - building fresh data
95
- INFO: 2025-12-11T12:01:45 - app.services.dashboard_service: Built and cached dashboard for project 0ade6bd1-e492-4e25-b681-59f42058d29a
96
- INFO: 10.16.13.79:13245 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/dashboard HTTP/1.1" 200 OK
97
- INFO: 10.16.37.13:24130 - "GET /api/v1/notifications/stats HTTP/1.1" 200 OK
98
- INFO: 2025-12-11T12:01:50 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
99
- INFO: 2025-12-11T12:01:50 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
100
- INFO: 10.16.13.79:23012 - "GET /api/v1/profile/me HTTP/1.1" 200 OK
101
- INFO: 2025-12-11T12:01:50 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
102
- INFO: 2025-12-11T12:01:50 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
103
- INFO: 10.16.13.79:23012 - "GET /api/v1/financial-accounts/me HTTP/1.1" 200 OK
104
- INFO: 2025-12-11T12:01:50 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
105
- INFO: 2025-12-11T12:01:50 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
106
- INFO: 10.16.13.79:53097 - "GET /api/v1/asset-assignments/me HTTP/1.1" 200 OK
107
- INFO: 2025-12-11T12:01:50 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
108
- INFO: 2025-12-11T12:01:50 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
109
- INFO: 10.16.37.13:18966 - "GET /api/v1/documents/users/me HTTP/1.1" 200 OK
110
- INFO: 10.16.13.79:6842 - "GET /health HTTP/1.1" 200 OK
111
- INFO: 10.16.13.79:23528 - "GET /health HTTP/1.1" 200 OK
112
- INFO: 10.16.37.13:12768 - "GET /health HTTP/1.1" 200 OK
113
- INFO: 10.16.37.13:43554 - "GET /health HTTP/1.1" 200 OK
114
- INFO: 10.16.37.13:18140 - "GET /health HTTP/1.1" 200 OK
115
- INFO: 10.16.37.13:38376 - "GET /health HTTP/1.1" 200 OK
116
- INFO: 10.16.13.79:56523 - "GET /health HTTP/1.1" 200 OK
117
- INFO: 10.16.13.79:25504 - "GET /health HTTP/1.1" 200 OK
118
- INFO: 10.16.13.79:37327 - "GET /health HTTP/1.1" 200 OK
119
- INFO: 10.16.13.79:25409 - "GET /health HTTP/1.1" 200 OK
120
- INFO: 10.16.13.79:30231 - "GET /health HTTP/1.1" 200 OK
121
- INFO: 10.16.13.79:50418 - "GET /health HTTP/1.1" 200 OK
122
- INFO: 10.16.37.13:16612 - "GET /health HTTP/1.1" 200 OK
123
- INFO: 2025-12-11T12:13:26 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
124
- INFO: 2025-12-11T12:13:26 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
125
- INFO: 10.16.13.79:63272 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
126
- INFO: 2025-12-11T12:13:27 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
127
- INFO: 2025-12-11T12:13:27 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
128
- INFO: 10.16.37.13:16612 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
129
- INFO: 2025-12-11T12:13:27 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
130
- INFO: 2025-12-11T12:13:27 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
131
- INFO: 10.16.13.79:63272 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
132
- INFO: 2025-12-11T12:13:28 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
133
- INFO: 2025-12-11T12:13:28 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
134
- INFO: 10.16.37.13:16612 - "GET /api/v1/profile/me HTTP/1.1" 200 OK
135
- INFO: 2025-12-11T12:13:28 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
136
- INFO: 2025-12-11T12:13:28 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
137
- INFO: 10.16.13.79:63272 - "GET /api/v1/financial-accounts/me HTTP/1.1" 200 OK
138
- INFO: 2025-12-11T12:13:28 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
139
- INFO: 2025-12-11T12:13:28 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
140
- INFO: 10.16.37.13:16612 - "GET /api/v1/documents/users/me HTTP/1.1" 200 OK
141
- INFO: 2025-12-11T12:13:29 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
142
- INFO: 2025-12-11T12:13:29 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
143
- INFO: 10.16.37.13:41220 - "GET /api/v1/asset-assignments/me HTTP/1.1" 200 OK
144
- INFO: 10.16.37.13:63963 - "GET /health HTTP/1.1" 200 OK
145
- INFO: 10.16.37.13:57954 - "GET /health HTTP/1.1" 200 OK
146
- INFO: 10.16.37.13:39080 - "GET /health HTTP/1.1" 200 OK
147
- INFO: 10.16.13.79:23343 - "GET /health HTTP/1.1" 200 OK
148
- INFO: 10.16.37.13:46519 - "GET /health HTTP/1.1" 200 OK
149
- INFO: 10.16.13.79:42831 - "GET /health HTTP/1.1" 200 OK
150
- INFO: 10.16.37.13:52232 - "GET /health HTTP/1.1" 200 OK
151
- INFO: 10.16.13.79:32566 - "GET /health HTTP/1.1" 200 OK
152
- INFO: 10.16.13.79:9200 - "GET /health HTTP/1.1" 200 OK
153
- INFO: 10.16.37.13:10009 - "GET /health HTTP/1.1" 200 OK
154
- INFO: 10.16.13.79:4234 - "GET /health HTTP/1.1" 200 OK
155
- INFO: 10.16.13.79:19428 - "GET /health HTTP/1.1" 200 OK
156
- INFO: 10.16.13.79:27008 - "GET /health HTTP/1.1" 200 OK
157
- INFO: 10.16.13.79:4669 - "GET /health HTTP/1.1" 200 OK
158
- INFO: 10.16.37.13:11633 - "GET /health HTTP/1.1" 200 OK
159
- INFO: 10.16.13.79:23786 - "GET /health HTTP/1.1" 200 OK
160
- INFO: 10.16.37.13:37721 - "GET /health HTTP/1.1" 200 OK
161
- INFO: 10.16.13.79:7300 - "GET /health HTTP/1.1" 200 OK
162
- INFO: 10.16.37.13:61636 - "GET /health HTTP/1.1" 200 OK
163
- INFO: 10.16.37.13:61636 - "GET /health HTTP/1.1" 200 OK
164
- INFO: 10.16.13.79:6908 - "GET /health HTTP/1.1" 200 OK
165
- INFO: 10.16.37.13:57945 - "GET /health HTTP/1.1" 200 OK
166
- INFO: 10.16.37.13:59629 - "GET /health HTTP/1.1" 200 OK
167
- INFO: 10.16.13.79:45774 - "GET /health HTTP/1.1" 200 OK
168
- INFO: 10.16.37.13:25544 - "GET /health HTTP/1.1" 200 OK
169
- INFO: 10.16.13.79:57214 - "GET /health HTTP/1.1" 200 OK
170
- INFO: 10.16.13.79:60769 - "GET /health HTTP/1.1" 200 OK
171
- INFO: 10.16.37.13:49370 - "GET /health HTTP/1.1" 200 OK
172
- INFO: 10.16.37.13:51919 - "GET /health HTTP/1.1" 200 OK
173
- INFO: 10.16.13.79:27898 - "GET /health HTTP/1.1" 200 OK
174
- INFO: 10.16.37.13:57822 - "GET /health HTTP/1.1" 200 OK
175
- INFO: 10.16.37.13:57822 - "GET /health HTTP/1.1" 200 OK
176
- INFO: 2025-12-11T12:42:20 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
177
- INFO: 2025-12-11T12:42:20 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
178
- INFO: 10.16.13.79:7805 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
179
- INFO: 2025-12-11T12:42:21 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
180
- INFO: 2025-12-11T12:42:21 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
181
- INFO: 10.16.13.79:7805 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
182
- INFO: 2025-12-11T12:42:21 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
183
- INFO: 2025-12-11T12:42:21 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
184
- INFO: 10.16.37.13:57822 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
185
- INFO: 2025-12-11T12:42:21 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
186
- INFO: 2025-12-11T12:42:21 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
187
- INFO: 10.16.13.79:7805 - "GET /api/v1/profile/me HTTP/1.1" 200 OK
188
- INFO: 2025-12-11T12:42:22 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
189
- INFO: 2025-12-11T12:42:22 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
190
- INFO: 10.16.13.79:7805 - "GET /api/v1/asset-assignments/me HTTP/1.1" 200 OK
191
- INFO: 2025-12-11T12:42:22 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
192
- INFO: 2025-12-11T12:42:22 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
193
- INFO: 10.16.37.13:57822 - "GET /api/v1/financial-accounts/me HTTP/1.1" 200 OK
194
- INFO: 2025-12-11T12:42:22 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
195
- INFO: 2025-12-11T12:42:22 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
196
- INFO: 10.16.37.13:62459 - "GET /api/v1/documents/users/me HTTP/1.1" 200 OK
197
- INFO: 10.16.13.79:5210 - "GET /health HTTP/1.1" 200 OK
198
- INFO: 2025-12-11T12:43:11 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
199
- INFO: 2025-12-11T12:43:11 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
200
- INFO: 10.16.37.13:46661 - "GET /api/v1/analytics/user/overview?limit=50 HTTP/1.1" 200 OK
201
- INFO: 2025-12-11T12:43:11 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
202
- INFO: 2025-12-11T12:43:11 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
203
- INFO: 10.16.37.13:46661 - "GET /api/v1/profile/me/validation HTTP/1.1" 200 OK
204
- INFO: 2025-12-11T12:43:11 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
205
- INFO: 2025-12-11T12:43:11 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
206
- INFO: 2025-12-11T12:43:11 - app.services.project_service: Listed 1 projects (total: 1) for user 43b778b0-2062-4724-abbb-916a4835a9b0
207
- INFO: 10.16.13.79:16665 - "GET /api/v1/projects?page=1&per_page=100&status=active HTTP/1.1" 200 OK
208
- INFO: 10.16.13.79:16665 - "GET /api/v1/tickets/1e622599-1909-49b9-9d8b-4c5cb483b29e/detail HTTP/1.1" 200 OK
209
- INFO: 10.16.13.79:16665 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions/24510a5a-13a6-4334-9055-b4d476aa9e0a HTTP/1.1" 200 OK
210
- INFO: 10.16.37.13:19552 - "GET /health HTTP/1.1" 200 OK
211
- INFO: 2025-12-11T12:45:27 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
212
- INFO: 2025-12-11T12:45:27 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
213
- INFO: 10.16.13.79:9416 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
214
- INFO: 10.16.13.79:30442 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions/24510a5a-13a6-4334-9055-b4d476aa9e0a HTTP/1.1" 200 OK
215
- INFO: 2025-12-11T12:45:27 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
216
- INFO: 2025-12-11T12:45:27 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
217
- INFO: 10.16.37.13:34059 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
218
- INFO: 10.16.13.79:30442 - "GET /health HTTP/1.1" 200 OK
219
- INFO: 10.16.13.79:1845 - "GET /health HTTP/1.1" 200 OK
220
- INFO: 10.16.13.79:62309 - "GET /health HTTP/1.1" 200 OK
221
- INFO: 10.16.13.79:37510 - "GET /api/v1/notifications/stats HTTP/1.1" 200 OK
222
- INFO: 10.16.13.79:43132 - "GET /health HTTP/1.1" 200 OK
223
- INFO: 10.16.37.13:21960 - "GET /health HTTP/1.1" 200 OK
224
- INFO: 10.16.13.79:28571 - "GET /api/v1/notifications/stats HTTP/1.1" 200 OK
225
- INFO: 10.16.13.79:19861 - "GET /health HTTP/1.1" 200 OK
226
- INFO: 10.16.37.13:64518 - "GET /health HTTP/1.1" 200 OK
227
- INFO: 10.16.13.79:20993 - "GET /health HTTP/1.1" 200 OK
228
- INFO: 2025-12-11T12:56:40 - app.core.supabase_auth: Session refreshed successfully
229
- INFO: 2025-12-11T12:56:40 - app.api.v1.auth: ✅ Token refreshed successfully for: viyisa8151@feralrex.com
230
- INFO: 10.16.37.13:4884 - "POST /api/v1/auth/refresh-token HTTP/1.1" 200 OK
231
- INFO: 10.16.13.79:10477 - "GET /health HTTP/1.1" 200 OK
232
- INFO: 10.16.37.13:31019 - "GET /health HTTP/1.1" 200 OK
233
- INFO: 10.16.13.79:18495 - "GET /health HTTP/1.1" 200 OK
234
- INFO: 10.16.37.13:55756 - "GET /health HTTP/1.1" 200 OK
235
- INFO: 10.16.13.79:41717 - "GET /health HTTP/1.1" 200 OK
236
- INFO: 10.16.37.13:16937 - "GET /health HTTP/1.1" 200 OK
237
- INFO: 10.16.37.13:16656 - "GET /health HTTP/1.1" 200 OK
238
- INFO: 10.16.37.13:1448 - "GET /health HTTP/1.1" 200 OK
239
- INFO: 10.16.37.13:12240 - "GET /health HTTP/1.1" 200 OK
240
- INFO: 10.16.37.13:47305 - "GET /health HTTP/1.1" 200 OK
241
- INFO: 10.16.13.79:44584 - "GET /health HTTP/1.1" 200 OK
242
- INFO: 10.16.37.13:21320 - "GET /health HTTP/1.1" 200 OK
243
- INFO: 10.16.37.13:31527 - "GET /health HTTP/1.1" 200 OK
244
- INFO: 10.16.37.13:53757 - "GET /health HTTP/1.1" 200 OK
245
- INFO: 10.16.37.13:61345 - "GET /health HTTP/1.1" 200 OK
246
- INFO: 10.16.37.13:3040 - "GET /health HTTP/1.1" 200 OK
247
- INFO: 10.16.13.79:14881 - "GET /health HTTP/1.1" 200 OK
248
- INFO: 10.16.13.79:9104 - "GET /health HTTP/1.1" 200 OK
249
- INFO: 10.16.13.79:9104 - "GET /health HTTP/1.1" 200 OK
250
- INFO: 2025-12-11T13:16:39 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
251
- INFO: 2025-12-11T13:16:39 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
252
- INFO: 10.16.13.79:9104 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
253
- INFO: 2025-12-11T13:16:40 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
254
- INFO: 2025-12-11T13:16:40 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
255
- INFO: 10.16.37.13:40874 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
256
- INFO: 2025-12-11T13:16:40 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
257
- INFO: 2025-12-11T13:16:40 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
258
- INFO: 10.16.13.79:13160 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
259
- INFO: 10.16.37.13:35507 - "GET /health HTTP/1.1" 200 OK
260
- INFO: 10.16.37.13:61859 - "GET /health HTTP/1.1" 200 OK
261
- INFO: 10.16.13.79:48674 - "GET /health HTTP/1.1" 200 OK
262
- INFO: 2025-12-11T13:19:59 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
263
- INFO: 2025-12-11T13:19:59 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
264
- ERROR: 2025-12-11T13:19:59 - app.core.supabase_auth: Sign in error: Invalid login credentials
265
- ERROR: 2025-12-11T13:19:59 - app.api.v1.auth: Password change error: Invalid login credentials
266
- ERROR: 2025-12-11T13:19:59 - app.services.audit_service: Failed to create audit log: (psycopg2.errors.InvalidTextRepresentation) invalid input value for enum auditaction: "password_change_failed"
267
- LINE 1: ...::UUID, 'viyisa8151@feralrex.com', 'field_agent', 'password_...
268
- ^
269
-
270
- [SQL: INSERT INTO audit_logs (id, user_id, user_email, user_role, action, entity_type, entity_id, description, changes, ip_address, user_agent, request_id, latitude, longitude, additional_metadata, created_at) VALUES (%(id)s::UUID, %(user_id)s::UUID, %(user_email)s, %(user_role)s, %(action)s, %(entity_type)s, %(entity_id)s::UUID, %(description)s, %(changes)s, %(ip_address)s, %(user_agent)s, %(request_id)s, %(latitude)s, %(longitude)s, %(additional_metadata)s, %(created_at)s)]
271
- [parameters: {'id': UUID('6ace3072-e216-49a0-8a18-93738e1aedf8'), 'user_id': UUID('43b778b0-2062-4724-abbb-916a4835a9b0'), 'user_email': 'viyisa8151@feralrex.com', 'user_role': 'field_agent', 'action': 'password_change_failed', 'entity_type': 'user', 'entity_id': '43b778b0-2062-4724-abbb-916a4835a9b0', 'description': 'Failed password change attempt: viyisa8151@feralrex.com', 'changes': '{}', 'ip_address': '10.16.37.13', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', 'request_id': None, 'latitude': None, 'longitude': None, 'additional_metadata': '{"reason": "Invalid login credentials"}', 'created_at': datetime.datetime(2025, 12, 11, 13, 19, 59, 460418)}]
272
- (Background on this error at: https://sqlalche.me/e/20/9h9h)
273
- INFO: 10.16.37.13:55242 - "POST /api/v1/auth/change-password HTTP/1.1" 400 Bad Request
274
- INFO: 10.16.13.79:1073 - "GET /api/v1/notifications/stats HTTP/1.1" 200 OK
275
- INFO: 10.16.37.13:39647 - "GET /health HTTP/1.1" 200 OK
276
- INFO: 2025-12-11T13:20:31 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
277
- INFO: 2025-12-11T13:20:31 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
278
- INFO: 2025-12-11T13:20:31 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
279
- ERROR: 2025-12-11T13:20:31 - app.services.audit_service: Failed to create audit log: (psycopg2.errors.InvalidTextRepresentation) invalid input value for enum auditaction: "password_change"
280
- LINE 1: ...::UUID, 'viyisa8151@feralrex.com', 'field_agent', 'password_...
281
- ^
282
-
283
- [SQL: INSERT INTO audit_logs (id, user_id, user_email, user_role, action, entity_type, entity_id, description, changes, ip_address, user_agent, request_id, latitude, longitude, additional_metadata, created_at) VALUES (%(id)s::UUID, %(user_id)s::UUID, %(user_email)s, %(user_role)s, %(action)s, %(entity_type)s, %(entity_id)s::UUID, %(description)s, %(changes)s, %(ip_address)s, %(user_agent)s, %(request_id)s, %(latitude)s, %(longitude)s, %(additional_metadata)s, %(created_at)s)]
284
- [parameters: {'id': UUID('470c7b83-1082-41ad-87af-b92a3ebb28a9'), 'user_id': UUID('43b778b0-2062-4724-abbb-916a4835a9b0'), 'user_email': 'viyisa8151@feralrex.com', 'user_role': 'field_agent', 'action': 'password_change', 'entity_type': 'user', 'entity_id': '43b778b0-2062-4724-abbb-916a4835a9b0', 'description': 'User changed password: viyisa8151@feralrex.com', 'changes': '{}', 'ip_address': '10.16.37.13', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', 'request_id': None, 'latitude': None, 'longitude': None, 'additional_metadata': '{}', 'created_at': datetime.datetime(2025, 12, 11, 13, 20, 31, 573524)}]
285
- (Background on this error at: https://sqlalche.me/e/20/9h9h)
286
- INFO: 2025-12-11T13:20:31 - app.api.v1.auth: Password changed for user: viyisa8151@feralrex.com
287
- INFO: 10.16.37.13:26075 - "POST /api/v1/auth/change-password HTTP/1.1" 200 OK
288
- INFO: 2025-12-11T13:20:32 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
289
- INFO: 2025-12-11T13:20:32 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
290
- INFO: 2025-12-11T13:20:32 - app.services.audit_service: Audit log created: logout on auth by system
291
- INFO: 2025-12-11T13:20:32 - app.api.v1.auth: User logged out: viyisa8151@feralrex.com
292
- INFO: 10.16.37.13:26075 - "POST /api/v1/auth/logout HTTP/1.1" 200 OK
293
- INFO: 10.16.13.79:20819 - "GET /health HTTP/1.1" 200 OK
294
- INFO: 10.16.13.79:8764 - "GET /health HTTP/1.1" 200 OK
295
- INFO: 2025-12-11T13:21:00 - app.core.supabase_auth: User signed in successfully: nadina73@nembors.com
296
- INFO: 2025-12-11T13:21:00 - app.services.audit_service: Audit log created: login on auth by nadina73@nembors.com
297
- INFO: 2025-12-11T13:21:00 - app.api.v1.auth: User logged in successfully: nadina73@nembors.com
298
- INFO: 10.16.37.13:28703 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
299
- INFO: 2025-12-11T13:21:01 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
300
- INFO: 2025-12-11T13:21:01 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
301
- INFO: 10.16.37.13:28703 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
302
- INFO: 2025-12-11T13:21:01 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
303
- INFO: 2025-12-11T13:21:01 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
304
- INFO: 10.16.13.79:41537 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
305
- INFO: 2025-12-11T13:21:01 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
306
- INFO: 2025-12-11T13:21:01 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
307
- INFO: 10.16.37.13:28703 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
308
- INFO: 2025-12-11T13:21:02 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
309
- INFO: 2025-12-11T13:21:02 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
310
- INFO: 10.16.13.79:41537 - "GET /api/v1/analytics/user/overview HTTP/1.1" 200 OK
311
- INFO: 2025-12-11T13:21:03 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
312
- INFO: 2025-12-11T13:21:03 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
313
- INFO: 2025-12-11T13:21:03 - app.services.project_service: Listed 1 projects (total: 1) for user c5cf92be-4172-4fe2-af5c-f05d83b3a938
314
- INFO: 10.16.37.13:28703 - "GET /api/v1/projects?page=1&per_page=100 HTTP/1.1" 200 OK
315
- INFO: 10.16.37.13:28703 - "GET /api/v1/notifications/stats HTTP/1.1" 200 OK
316
- INFO: 10.16.13.79:52348 - "GET /health HTTP/1.1" 200 OK
317
- INFO: 2025-12-11T13:21:20 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
318
- INFO: 2025-12-11T13:21:20 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
319
- INFO: 2025-12-11T13:21:20 - app.core.supabase_auth: User signed in successfully: nadina73@nembors.com
320
- ERROR: 2025-12-11T13:21:20 - app.services.audit_service: Failed to create audit log: (psycopg2.errors.InvalidTextRepresentation) invalid input value for enum auditaction: "password_change"
321
- LINE 1: ...:UUID, 'nadina73@nembors.com', 'project_manager', 'password_...
322
- ^
323
-
324
- [SQL: INSERT INTO audit_logs (id, user_id, user_email, user_role, action, entity_type, entity_id, description, changes, ip_address, user_agent, request_id, latitude, longitude, additional_metadata, created_at) VALUES (%(id)s::UUID, %(user_id)s::UUID, %(user_email)s, %(user_role)s, %(action)s, %(entity_type)s, %(entity_id)s::UUID, %(description)s, %(changes)s, %(ip_address)s, %(user_agent)s, %(request_id)s, %(latitude)s, %(longitude)s, %(additional_metadata)s, %(created_at)s)]
325
- [parameters: {'id': UUID('4bd3616b-c618-4a1e-8f6b-6fa7166a7e02'), 'user_id': UUID('c5cf92be-4172-4fe2-af5c-f05d83b3a938'), 'user_email': 'nadina73@nembors.com', 'user_role': 'project_manager', 'action': 'password_change', 'entity_type': 'user', 'entity_id': 'c5cf92be-4172-4fe2-af5c-f05d83b3a938', 'description': 'User changed password: nadina73@nembors.com', 'changes': '{}', 'ip_address': '10.16.13.79', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', 'request_id': None, 'latitude': None, 'longitude': None, 'additional_metadata': '{}', 'created_at': datetime.datetime(2025, 12, 11, 13, 21, 20, 361145)}]
326
- (Background on this error at: https://sqlalche.me/e/20/9h9h)
327
- INFO: 2025-12-11T13:21:20 - app.api.v1.auth: Password changed for user: nadina73@nembors.com
328
- INFO: 10.16.13.79:52348 - "POST /api/v1/auth/change-password HTTP/1.1" 200 OK
329
- INFO: 2025-12-11T13:21:20 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
330
- INFO: 2025-12-11T13:21:20 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
331
- INFO: 2025-12-11T13:21:20 - app.services.audit_service: Audit log created: logout on auth by system
332
- INFO: 2025-12-11T13:21:20 - app.api.v1.auth: User logged out: nadina73@nembors.com
333
- INFO: 10.16.37.13:10900 - "POST /api/v1/auth/logout HTTP/1.1" 200 OK
334
- INFO: 10.16.13.79:52348 - "GET /health HTTP/1.1" 200 OK
335
- ERROR: 2025-12-11T13:21:25 - app.core.supabase_auth: Sign in error: Invalid login credentials
336
- ERROR: 2025-12-11T13:21:25 - app.api.v1.auth: Login error: Invalid login credentials
337
- INFO: 2025-12-11T13:21:25 - app.services.audit_service: Audit log created: login_failed on auth by system
338
- INFO: 10.16.13.79:11746 - "POST /api/v1/auth/login HTTP/1.1" 401 Unauthorized
339
- INFO: 10.16.13.79:59438 - "GET /health HTTP/1.1" 200 OK
340
- INFO: 10.16.37.13:44376 - "GET /health HTTP/1.1" 200 OK
341
- INFO: 2025-12-11T13:21:39 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
342
- INFO: 2025-12-11T13:21:39 - app.services.audit_service: Audit log created: login on auth by viyisa8151@feralrex.com
343
- INFO: 2025-12-11T13:21:39 - app.api.v1.auth: User logged in successfully: viyisa8151@feralrex.com
344
- INFO: 10.16.37.13:44376 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
345
- INFO: 2025-12-11T13:21:40 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
346
- INFO: 2025-12-11T13:21:40 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
347
- INFO: 10.16.13.79:61902 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
348
- INFO: 2025-12-11T13:21:40 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
349
- INFO: 2025-12-11T13:21:40 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
350
- INFO: 10.16.37.13:44376 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
351
- INFO: 2025-12-11T13:21:40 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
352
- INFO: 2025-12-11T13:21:40 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
353
- INFO: 10.16.13.79:61902 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
354
- INFO: 2025-12-11T13:21:41 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
355
- INFO: 2025-12-11T13:21:41 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
356
- INFO: 10.16.13.79:61902 - "GET /api/v1/analytics/user/overview?limit=50 HTTP/1.1" 200 OK
357
- INFO: 2025-12-11T13:21:42 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
358
- INFO: 2025-12-11T13:21:42 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
359
- INFO: 10.16.13.79:61902 - "GET /api/v1/profile/me/validation HTTP/1.1" 200 OK
360
- INFO: 2025-12-11T13:21:42 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
361
- INFO: 2025-12-11T13:21:42 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
362
- INFO: 2025-12-11T13:21:42 - app.services.project_service: Listed 1 projects (total: 1) for user 43b778b0-2062-4724-abbb-916a4835a9b0
363
- INFO: 10.16.37.13:44376 - "GET /api/v1/projects?page=1&per_page=100&status=active HTTP/1.1" 200 OK
364
- INFO: 10.16.37.13:14135 - "GET /health HTTP/1.1" 200 OK
365
- INFO: 10.16.13.79:16317 - "GET /health HTTP/1.1" 200 OK
366
- INFO: 10.16.13.79:14574 - "GET /health HTTP/1.1" 200 OK
367
- INFO: 10.16.13.79:14574 - "GET /api/v1/notifications/stats HTTP/1.1" 200 OK
368
- INFO: 2025-12-11T13:22:42 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
369
- INFO: 2025-12-11T13:22:42 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
370
- INFO: 2025-12-11T13:22:42 - app.services.dashboard_service: Dashboard cache MISS for project 0ade6bd1-e492-4e25-b681-59f42058d29a, user 43b778b0-2062-4724-abbb-916a4835a9b0 - building fresh data
371
- INFO: 2025-12-11T13:22:42 - app.services.dashboard_service: Built and cached dashboard for project 0ade6bd1-e492-4e25-b681-59f42058d29a
372
- INFO: 10.16.13.79:35674 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/dashboard HTTP/1.1" 200 OK
373
- INFO: 10.16.13.79:55862 - "GET /health HTTP/1.1" 200 OK
374
- INFO: 2025-12-11T13:23:25 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
375
- INFO: 2025-12-11T13:23:25 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
376
- INFO: 2025-12-11T13:23:25 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
377
- ERROR: 2025-12-11T13:23:25 - app.services.audit_service: Failed to create audit log: (psycopg2.errors.InvalidTextRepresentation) invalid input value for enum auditaction: "password_change"
378
- LINE 1: ...::UUID, 'viyisa8151@feralrex.com', 'field_agent', 'password_...
379
- ^
380
-
381
- [SQL: INSERT INTO audit_logs (id, user_id, user_email, user_role, action, entity_type, entity_id, description, changes, ip_address, user_agent, request_id, latitude, longitude, additional_metadata, created_at) VALUES (%(id)s::UUID, %(user_id)s::UUID, %(user_email)s, %(user_role)s, %(action)s, %(entity_type)s, %(entity_id)s::UUID, %(description)s, %(changes)s, %(ip_address)s, %(user_agent)s, %(request_id)s, %(latitude)s, %(longitude)s, %(additional_metadata)s, %(created_at)s)]
382
- [parameters: {'id': UUID('be0c01e0-0098-4b8c-8b95-215baf2f4262'), 'user_id': UUID('43b778b0-2062-4724-abbb-916a4835a9b0'), 'user_email': 'viyisa8151@feralrex.com', 'user_role': 'field_agent', 'action': 'password_change', 'entity_type': 'user', 'entity_id': '43b778b0-2062-4724-abbb-916a4835a9b0', 'description': 'User changed password: viyisa8151@feralrex.com', 'changes': '{}', 'ip_address': '10.16.37.13', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', 'request_id': None, 'latitude': None, 'longitude': None, 'additional_metadata': '{}', 'created_at': datetime.datetime(2025, 12, 11, 13, 23, 25, 955228)}]
383
- (Background on this error at: https://sqlalche.me/e/20/9h9h)
384
- INFO: 2025-12-11T13:23:25 - app.api.v1.auth: Password changed for user: viyisa8151@feralrex.com
385
- INFO: 10.16.37.13:49418 - "POST /api/v1/auth/change-password HTTP/1.1" 200 OK
386
- INFO: 2025-12-11T13:23:26 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
387
- INFO: 2025-12-11T13:23:26 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
388
- INFO: 2025-12-11T13:23:26 - app.services.audit_service: Audit log created: logout on auth by system
389
- INFO: 2025-12-11T13:23:26 - app.api.v1.auth: User logged out: viyisa8151@feralrex.com
390
- INFO: 10.16.13.79:59590 - "POST /api/v1/auth/logout HTTP/1.1" 200 OK
391
- INFO: 10.16.13.79:59590 - "GET /health HTTP/1.1" 200 OK
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/devlogs/server/builderrors.txt CHANGED
@@ -1,91 +1,228 @@
1
- ===== Application Startup at 2025-12-11 13:29:30 =====
2
-
3
- INFO: Started server process [7]
4
- INFO: Waiting for application startup.
5
- INFO: 2025-12-11T13:29:43 - app.main: ============================================================
6
- INFO: 2025-12-11T13:29:43 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
- INFO: 2025-12-11T13:29:43 - app.main: 📊 Dashboard: Enabled
8
- INFO: 2025-12-11T13:29:43 - app.main: ============================================================
9
- INFO: 2025-12-11T13:29:43 - app.main: 📦 Database:
10
- INFO: 2025-12-11T13:29:43 - app.main: Connected | 47 tables | 6 users
11
- INFO: 2025-12-11T13:29:43 - app.main: 💾 Cache & Sessions:
12
- INFO: 2025-12-11T13:29:44 - app.services.otp_service: OTP Service initialized with Redis storage
13
- INFO: 2025-12-11T13:29:44 - app.main: ✓ Redis: Connected
14
- INFO: 2025-12-11T13:29:44 - app.main: 🔌 External Services:
15
- INFO: 2025-12-11T13:29:45 - app.main: Cloudinary: Connected
16
- INFO: 2025-12-11T13:29:45 - app.main: ✓ Resend: Configured
17
- INFO: 2025-12-11T13:29:45 - app.main: WASender: Disconnected
18
- INFO: 2025-12-11T13:29:45 - app.main: Supabase: Connected | 6 buckets
19
- INFO: 2025-12-11T13:29:45 - app.main: Scheduler:
20
- INFO: 2025-12-11T13:29:45 - apscheduler.scheduler: Adding job tentatively -- it will be properly scheduled when the scheduler starts
21
- INFO: 2025-12-11T13:29:45 - apscheduler.scheduler: Added job "Daily Field Agent Reconciliation" to job store "default"
22
- INFO: 2025-12-11T13:29:45 - apscheduler.scheduler: Scheduler started
23
- INFO: 2025-12-11T13:29:45 - app.tasks.scheduler: Reconciliation scheduler started (runs at 10 PM Africa/Nairobi)
24
- INFO: 2025-12-11T13:29:45 - app.main: Daily reconciliation scheduler started (runs at midnight)
25
- INFO: 2025-12-11T13:29:45 - app.main: ============================================================
26
- INFO: 2025-12-11T13:29:45 - app.main: ✅ Startup complete | Ready to serve requests
27
- INFO: 2025-12-11T13:29:45 - 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:51336 - "GET /health HTTP/1.1" 200 OK
31
- INFO: 2025-12-11T13:31:22 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
32
- INFO: 2025-12-11T13:31:23 - app.services.audit_service: Audit log created: login on auth by viyisa8151@feralrex.com
33
- INFO: 2025-12-11T13:31:23 - app.api.v1.auth: User logged in successfully: viyisa8151@feralrex.com
34
- INFO: 10.16.37.13:10585 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
35
- INFO: 2025-12-11T13:31:24 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
36
- INFO: 2025-12-11T13:31:24 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
37
- INFO: 10.16.13.79:49175 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
38
- INFO: 2025-12-11T13:31:24 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
39
- INFO: 2025-12-11T13:31:24 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
40
- INFO: 10.16.13.79:49175 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
41
- INFO: 2025-12-11T13:31:25 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
42
- INFO: 2025-12-11T13:31:25 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
43
- INFO: 10.16.37.13:10585 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
44
- INFO: 2025-12-11T13:31:25 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
45
- INFO: 2025-12-11T13:31:25 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
46
- INFO: 10.16.13.79:49175 - "GET /api/v1/analytics/user/overview?limit=50 HTTP/1.1" 200 OK
47
- INFO: 2025-12-11T13:31:26 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
48
- INFO: 2025-12-11T13:31:26 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
49
- INFO: 10.16.37.13:10585 - "GET /api/v1/profile/me/validation HTTP/1.1" 200 OK
50
- INFO: 2025-12-11T13:31:26 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
51
- INFO: 2025-12-11T13:31:26 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
52
- INFO: 2025-12-11T13:31:26 - app.services.project_service: Listed 1 projects (total: 1) for user 43b778b0-2062-4724-abbb-916a4835a9b0
53
- INFO: 10.16.13.79:49175 - "GET /api/v1/projects?page=1&per_page=100&status=active HTTP/1.1" 200 OK
54
- INFO: 10.16.13.79:49175 - "GET /api/v1/notifications/stats HTTP/1.1" 200 OK
55
- INFO: 10.16.37.13:46935 - "GET /health HTTP/1.1" 200 OK
56
- INFO: 10.16.13.79:56259 - "GET /health HTTP/1.1" 200 OK
57
- INFO: 2025-12-11T13:31:45 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
58
- INFO: 2025-12-11T13:31:45 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
59
- INFO: 2025-12-11T13:31:45 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
60
- INFO: 2025-12-11T13:31:45 - app.services.audit_service: Audit log created: update on user by viyisa8151@feralrex.com
61
- INFO: 2025-12-11T13:31:45 - app.api.v1.auth: Password changed for user: viyisa8151@feralrex.com
62
- INFO: 10.16.37.13:26858 - "POST /api/v1/auth/change-password HTTP/1.1" 200 OK
63
- ERROR: 2025-12-11T13:31:46 - app.core.supabase_auth: Get user error: Session from session_id claim in JWT does not exist
64
- WARNING: 2025-12-11T13:31:46 - app.api.deps: Invalid or expired token
65
- INFO: 10.16.13.79:21485 - "POST /api/v1/auth/logout HTTP/1.1" 401 Unauthorized
66
- INFO: 10.16.37.13:26858 - "GET /health HTTP/1.1" 200 OK
67
- INFO: 10.16.13.79:32829 - "GET /health HTTP/1.1" 200 OK
68
- INFO: 2025-12-11T13:32:07 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
69
- INFO: 2025-12-11T13:32:07 - app.services.audit_service: Audit log created: login on auth by viyisa8151@feralrex.com
70
- INFO: 2025-12-11T13:32:07 - app.api.v1.auth: User logged in successfully: viyisa8151@feralrex.com
71
- INFO: 10.16.13.79:32829 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
72
- INFO: 2025-12-11T13:32:07 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
73
- INFO: 2025-12-11T13:32:07 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
74
- INFO: 10.16.13.79:32829 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
75
- INFO: 2025-12-11T13:32:08 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
76
- INFO: 2025-12-11T13:32:08 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
77
- INFO: 10.16.37.13:40934 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
78
- INFO: 2025-12-11T13:32:08 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
79
- INFO: 2025-12-11T13:32:08 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
80
- INFO: 10.16.13.79:32829 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
81
- INFO: 2025-12-11T13:32:09 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
82
- INFO: 2025-12-11T13:32:09 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
83
- INFO: 10.16.13.79:32829 - "GET /api/v1/analytics/user/overview?limit=50 HTTP/1.1" 200 OK
84
- INFO: 2025-12-11T13:32:09 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
85
- INFO: 2025-12-11T13:32:09 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
86
- INFO: 10.16.13.79:32829 - "GET /api/v1/profile/me/validation HTTP/1.1" 200 OK
87
- INFO: 2025-12-11T13:32:09 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
88
- INFO: 2025-12-11T13:32:09 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
89
- INFO: 2025-12-11T13:32:09 - app.services.project_service: Listed 1 projects (total: 1) for user 43b778b0-2062-4724-abbb-916a4835a9b0
90
- INFO: 10.16.37.13:40934 - "GET /api/v1/projects?page=1&per_page=100&status=active HTTP/1.1" 200 OK
91
- INFO: 10.16.37.13:23450 - "GET /health HTTP/1.1" 200 OK
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Dec 11 20:12:11 INFO: Started server process [2]
2
+ Dec 11 20:12:11 INFO: Waiting for application startup.
3
+ Dec 11 20:12:11 INFO: 2025-12-11T20:12:11 - app.main: ============================================================
4
+ Dec 11 20:12:11 INFO: 2025-12-11T20:12:11 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
5
+ Dec 11 20:12:11 INFO: 2025-12-11T20:12:11 - app.main: 📊 Dashboard: Enabled
6
+ Dec 11 20:12:11 INFO: 2025-12-11T20:12:11 - app.main: ============================================================
7
+ Dec 11 20:12:11 INFO: 2025-12-11T20:12:11 - app.main: 📦 Database:
8
+ Dec 11 20:12:12 INFO: 2025-12-11T20:12:12 - app.main: Connected | 47 tables | 6 users
9
+ Dec 11 20:12:12 INFO: 2025-12-11T20:12:12 - app.main: 💾 Cache & Sessions:
10
+ Dec 11 20:12:13 INFO: 2025-12-11T20:12:13 - app.services.otp_service: OTP Service initialized with Redis storage
11
+ Dec 11 20:12:13 INFO: 2025-12-11T20:12:13 - app.main: Redis: Connected
12
+ Dec 11 20:12:13 INFO: 2025-12-11T20:12:13 - app.main: 🔌 External Services:
13
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.main: ✓ Cloudinary: Connected
14
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.main: Resend: Configured
15
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.main: WASender: Disconnected
16
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.main: ✓ Supabase: Connected | 6 buckets
17
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.main: Scheduler:
18
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - apscheduler.scheduler: Adding job tentatively -- it will be properly scheduled when the scheduler starts
19
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - apscheduler.scheduler: Added job "Daily Field Agent Reconciliation" to job store "default"
20
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - apscheduler.scheduler: Scheduler started
21
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.tasks.scheduler: Reconciliation scheduler started (runs at 10 PM Africa/Nairobi)
22
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.main: Daily reconciliation scheduler started (runs at midnight)
23
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.main: ============================================================
24
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.main: Startup complete | Ready to serve requests
25
+ Dec 11 20:12:14 INFO: 2025-12-11T20:12:14 - app.main: ============================================================
26
+ Dec 11 20:12:14 INFO: Application startup complete.
27
+ Dec 11 20:12:14 INFO: Uvicorn running on http://0.0.0.0:7860 (Press CTRL+C to quit)
28
+ Dec 11 20:12:20 INFO: 10.244.45.245:40870 - "GET /health HTTP/1.1" 200 OK
29
+ Dec 11 22:00:00 INFO: 2025-12-11T22:00:00 - apscheduler.executors.default: Running job "Daily Field Agent Reconciliation (trigger: cron[hour='22', minute='0'], next run at: 2025-12-11 22:00:00 UTC)" (scheduled at 2025-12-11 22:00:00+00:00)
30
+ Dec 11 22:00:00 INFO: 2025-12-11T22:00:00 - app.tasks.scheduler: Starting scheduled validation for 2025-12-11
31
+ Dec 11 22:00:00 INFO: 2025-12-11T22:00:00 - app.tasks.scheduler: Validating 1 projects for 2025-12-11
32
+ Dec 11 22:00:00 INFO: 2025-12-11T22:00:00 - app.services.reconciliation.reconciliation_service: Starting reconciliation: project=0ade6bd1-e492-4e25-b681-59f42058d29a, date=2025-12-11, type=scheduled
33
+ Dec 11 22:00:00 INFO: 2025-12-11T22:00:00 - app.services.reconciliation.reconciliation_service: Aggregated 0 agents in 210ms
34
+ Dec 11 22:00:00 ERROR: 2025-12-11T22:00:00 - app.services.reconciliation.reconciliation_service: Reconciliation failed: (psycopg2.ProgrammingError) can't adapt type 'dict'
35
+ Dec 11 22:00:00 [SQL:
36
+ Dec 11 22:00:00 UPDATE reconciliation_runs
37
+ Dec 11 22:00:00 SET
38
+ Dec 11 22:00:00 status = 'completed',
39
+ Dec 11 22:00:00 completed_at = NOW(),
40
+ Dec 11 22:00:00 agents_processed = %(agents_processed)s,
41
+ Dec 11 22:00:00 timesheets_created = %(timesheets_created)s,
42
+ Dec 11 22:00:00 timesheets_updated = %(timesheets_updated)s,
43
+ Dec 11 22:00:00 assignments_processed = %(assignments_processed)s,
44
+ Dec 11 22:00:00 expenses_processed = %(expenses_processed)s,
45
+ Dec 11 22:00:00 summary_stats = %(summary_stats)s,
46
+ Dec 11 22:00:00 anomalies_detected = %(anomalies)s,
47
+ Dec 11 22:00:00 execution_time_ms = %(execution_time_ms)s,
48
+ Dec 11 22:00:00 query_time_ms = %(query_time_ms)s,
49
+ Dec 11 22:00:00 updated_at = NOW()
50
+ Dec 11 22:00:00 WHERE id = %(run_id)s
51
+ Dec 11 22:00:00 ]
52
+ Dec 11 22:00:00 [parameters: {'agents_processed': 0, 'timesheets_created': 0, 'timesheets_updated': 0, 'assignments_processed': 0, 'expenses_processed': 0, 'summary_stats': {}, 'anomalies': [], 'execution_time_ms': 572, 'query_time_ms': 209, 'run_id': '2746d0ae-b21c-4161-bb5b-95e96a114142'}]
53
+ Dec 11 22:00:00 (Background on this error at: https://sqlalche.me/e/20/f405)
54
+ Dec 11 22:00:00 Traceback (most recent call last):
55
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
56
+ Dec 11 22:00:00 self.dialect.do_execute(
57
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
58
+ Dec 11 22:00:00 cursor.execute(statement, parameters)
59
+ Dec 11 22:00:00 psycopg2.ProgrammingError: can't adapt type 'dict'
60
+ Dec 11 22:00:00
61
+ Dec 11 22:00:00 The above exception was the direct cause of the following exception:
62
+ Dec 11 22:00:00
63
+ Dec 11 22:00:00 Traceback (most recent call last):
64
+ Dec 11 22:00:00 File "/app/src/app/services/reconciliation/reconciliation_service.py", line 117, in reconcile_project_day
65
+ Dec 11 22:00:00 self._complete_run(
66
+ Dec 11 22:00:00 File "/app/src/app/services/reconciliation/reconciliation_service.py", line 482, in _complete_run
67
+ Dec 11 22:00:00 self.db.execute(query, {
68
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
69
+ Dec 11 22:00:00 return self._execute_internal(
70
+ Dec 11 22:00:00 ^^^^^^^^^^^^^^^^^^^^^^^
71
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2199, in _execute_internal
72
+ Dec 11 22:00:00 result = conn.execute(
73
+ Dec 11 22:00:00 ^^^^^^^^^^^^^
74
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
75
+ Dec 11 22:00:00 return meth(
76
+ Dec 11 22:00:00 ^^^^^
77
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
78
+ Dec 11 22:00:00 return connection._execute_clauseelement(
79
+ Dec 11 22:00:00 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
81
+ Dec 11 22:00:00 ret = self._execute_context(
82
+ Dec 11 22:00:00 ^^^^^^^^^^^^^^^^^^^^^^
83
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
84
+ Dec 11 22:00:00 return self._exec_single_context(
85
+ Dec 11 22:00:00 ^^^^^^^^^^^^^^^^^^^^^^^^^^
86
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
87
+ Dec 11 22:00:00 self._handle_dbapi_exception(
88
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
89
+ Dec 11 22:00:00 raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
90
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
91
+ Dec 11 22:00:00 self.dialect.do_execute(
92
+ Dec 11 22:00:00 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
93
+ Dec 11 22:00:00 cursor.execute(statement, parameters)
94
+ Dec 11 22:00:00 sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) can't adapt type 'dict'
95
+ Dec 11 22:00:00 [SQL:
96
+ Dec 11 22:00:00 UPDATE reconciliation_runs
97
+ Dec 11 22:00:00 SET
98
+ Dec 11 22:00:00 status = 'completed',
99
+ Dec 11 22:00:00 completed_at = NOW(),
100
+ Dec 11 22:00:00 agents_processed = %(agents_processed)s,
101
+ Dec 11 22:00:00 timesheets_created = %(timesheets_created)s,
102
+ Dec 11 22:00:00 timesheets_updated = %(timesheets_updated)s,
103
+ Dec 11 22:00:00 assignments_processed = %(assignments_processed)s,
104
+ Dec 11 22:00:00 expenses_processed = %(expenses_processed)s,
105
+ Dec 11 22:00:00 summary_stats = %(summary_stats)s,
106
+ Dec 11 22:00:00 anomalies_detected = %(anomalies)s,
107
+ Dec 11 22:00:00 execution_time_ms = %(execution_time_ms)s,
108
+ Dec 11 22:00:00 query_time_ms = %(query_time_ms)s,
109
+ Dec 11 22:00:00 updated_at = NOW()
110
+ Dec 11 22:00:00 WHERE id = %(run_id)s
111
+ Dec 11 22:00:00 ]
112
+ Dec 11 22:00:00 [parameters: {'agents_processed': 0, 'timesheets_created': 0, 'timesheets_updated': 0, 'assignments_processed': 0, 'expenses_processed': 0, 'summary_stats': {}, 'anomalies': [], 'execution_time_ms': 572, 'query_time_ms': 209, 'run_id': '2746d0ae-b21c-4161-bb5b-95e96a114142'}]
113
+ Dec 11 22:00:00 (Background on this error at: https://sqlalche.me/e/20/f405)
114
+ Dec 11 22:00:01 ERROR: 2025-12-11T22:00:01 - app.tasks.scheduler: Failed to validate project 0ade6bd1-e492-4e25-b681-59f42058d29a (Atomio Fttx): Reconciliation failed: (psycopg2.ProgrammingError) can't adapt type 'dict'
115
+ Dec 11 22:00:01 [SQL:
116
+ Dec 11 22:00:01 UPDATE reconciliation_runs
117
+ Dec 11 22:00:01 SET
118
+ Dec 11 22:00:01 status = 'completed',
119
+ Dec 11 22:00:01 completed_at = NOW(),
120
+ Dec 11 22:00:01 agents_processed = %(agents_processed)s,
121
+ Dec 11 22:00:01 timesheets_created = %(timesheets_created)s,
122
+ Dec 11 22:00:01 timesheets_updated = %(timesheets_updated)s,
123
+ Dec 11 22:00:01 assignments_processed = %(assignments_processed)s,
124
+ Dec 11 22:00:01 expenses_processed = %(expenses_processed)s,
125
+ Dec 11 22:00:01 summary_stats = %(summary_stats)s,
126
+ Dec 11 22:00:01 anomalies_detected = %(anomalies)s,
127
+ Dec 11 22:00:01 execution_time_ms = %(execution_time_ms)s,
128
+ Dec 11 22:00:01 query_time_ms = %(query_time_ms)s,
129
+ Dec 11 22:00:01 updated_at = NOW()
130
+ Dec 11 22:00:01 WHERE id = %(run_id)s
131
+ Dec 11 22:00:01 ]
132
+ Dec 11 22:00:01 [parameters: {'agents_processed': 0, 'timesheets_created': 0, 'timesheets_updated': 0, 'assignments_processed': 0, 'expenses_processed': 0, 'summary_stats': {}, 'anomalies': [], 'execution_time_ms': 572, 'query_time_ms': 209, 'run_id': '2746d0ae-b21c-4161-bb5b-95e96a114142'}]
133
+ Dec 11 22:00:01 (Background on this error at: https://sqlalche.me/e/20/f405)
134
+ Dec 11 22:00:01 Traceback (most recent call last):
135
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
136
+ Dec 11 22:00:01 self.dialect.do_execute(
137
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
138
+ Dec 11 22:00:01 cursor.execute(statement, parameters)
139
+ Dec 11 22:00:01 psycopg2.ProgrammingError: can't adapt type 'dict'
140
+ Dec 11 22:00:01
141
+ Dec 11 22:00:01 The above exception was the direct cause of the following exception:
142
+ Dec 11 22:00:01
143
+ Dec 11 22:00:01 Traceback (most recent call last):
144
+ Dec 11 22:00:01 File "/app/src/app/services/reconciliation/reconciliation_service.py", line 117, in reconcile_project_day
145
+ Dec 11 22:00:01 self._complete_run(
146
+ Dec 11 22:00:01 File "/app/src/app/services/reconciliation/reconciliation_service.py", line 482, in _complete_run
147
+ Dec 11 22:00:01 self.db.execute(query, {
148
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
149
+ Dec 11 22:00:01 return self._execute_internal(
150
+ Dec 11 22:00:01 ^^^^^^^^^^^^^^^^^^^^^^^
151
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2199, in _execute_internal
152
+ Dec 11 22:00:01 result = conn.execute(
153
+ Dec 11 22:00:01 ^^^^^^^^^^^^^
154
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
155
+ Dec 11 22:00:01 return meth(
156
+ Dec 11 22:00:01 ^^^^^
157
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
158
+ Dec 11 22:00:01 return connection._execute_clauseelement(
159
+ Dec 11 22:00:01 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
160
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
161
+ Dec 11 22:00:01 ret = self._execute_context(
162
+ Dec 11 22:00:01 ^^^^^^^^^^^^^^^^^^^^^^
163
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
164
+ Dec 11 22:00:01 return self._exec_single_context(
165
+ Dec 11 22:00:01 ^^^^^^^^^^^^^^^^^^^^^^^^^^
166
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
167
+ Dec 11 22:00:01 self._handle_dbapi_exception(
168
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
169
+ Dec 11 22:00:01 raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
170
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
171
+ Dec 11 22:00:01 self.dialect.do_execute(
172
+ Dec 11 22:00:01 File "/usr/local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
173
+ Dec 11 22:00:01 cursor.execute(statement, parameters)
174
+ Dec 11 22:00:01 sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) can't adapt type 'dict'
175
+ Dec 11 22:00:01 [SQL:
176
+ Dec 11 22:00:01 UPDATE reconciliation_runs
177
+ Dec 11 22:00:01 SET
178
+ Dec 11 22:00:01 status = 'completed',
179
+ Dec 11 22:00:01 completed_at = NOW(),
180
+ Dec 11 22:00:01 agents_processed = %(agents_processed)s,
181
+ Dec 11 22:00:01 timesheets_created = %(timesheets_created)s,
182
+ Dec 11 22:00:01 timesheets_updated = %(timesheets_updated)s,
183
+ Dec 11 22:00:01 assignments_processed = %(assignments_processed)s,
184
+ Dec 11 22:00:01 expenses_processed = %(expenses_processed)s,
185
+ Dec 11 22:00:01 summary_stats = %(summary_stats)s,
186
+ Dec 11 22:00:01 anomalies_detected = %(anomalies)s,
187
+ Dec 11 22:00:01 execution_time_ms = %(execution_time_ms)s,
188
+ Dec 11 22:00:01 query_time_ms = %(query_time_ms)s,
189
+ Dec 11 22:00:01 updated_at = NOW()
190
+ Dec 11 22:00:01 WHERE id = %(run_id)s
191
+ Dec 11 22:00:01 ]
192
+ Dec 11 22:00:01 [parameters: {'agents_processed': 0, 'timesheets_created': 0, 'timesheets_updated': 0, 'assignments_processed': 0, 'expenses_processed': 0, 'summary_stats': {}, 'anomalies': [], 'execution_time_ms': 572, 'query_time_ms': 209, 'run_id': '2746d0ae-b21c-4161-bb5b-95e96a114142'}]
193
+ Dec 11 22:00:01 (Background on this error at: https://sqlalche.me/e/20/f405)
194
+ Dec 11 22:00:01
195
+ Dec 11 22:00:01 The above exception was the direct cause of the following exception:
196
+ Dec 11 22:00:01
197
+ Dec 11 22:00:01 Traceback (most recent call last):
198
+ Dec 11 22:00:01 File "/app/src/app/tasks/scheduler.py", line 132, in validate_all_projects
199
+ Dec 11 22:00:01 run_id = service.reconcile_project_day(
200
+ Dec 11 22:00:01 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
201
+ Dec 11 22:00:01 File "/app/src/app/services/reconciliation/reconciliation_service.py", line 149, in reconcile_project_day
202
+ Dec 11 22:00:01 raise ReconciliationError(f"Reconciliation failed: {str(e)}") from e
203
+ Dec 11 22:00:01 app.services.reconciliation.reconciliation_service.ReconciliationError: Reconciliation failed: (psycopg2.ProgrammingError) can't adapt type 'dict'
204
+ Dec 11 22:00:01 [SQL:
205
+ Dec 11 22:00:01 UPDATE reconciliation_runs
206
+ Dec 11 22:00:01 SET
207
+ Dec 11 22:00:01 status = 'completed',
208
+ Dec 11 22:00:01 completed_at = NOW(),
209
+ Dec 11 22:00:01 agents_processed = %(agents_processed)s,
210
+ Dec 11 22:00:01 timesheets_created = %(timesheets_created)s,
211
+ Dec 11 22:00:01 timesheets_updated = %(timesheets_updated)s,
212
+ Dec 11 22:00:01 assignments_processed = %(assignments_processed)s,
213
+ Dec 11 22:00:01 expenses_processed = %(expenses_processed)s,
214
+ Dec 11 22:00:01 summary_stats = %(summary_stats)s,
215
+ Dec 11 22:00:01 anomalies_detected = %(anomalies)s,
216
+ Dec 11 22:00:01 execution_time_ms = %(execution_time_ms)s,
217
+ Dec 11 22:00:01 query_time_ms = %(query_time_ms)s,
218
+ Dec 11 22:00:01 updated_at = NOW()
219
+ Dec 11 22:00:01 WHERE id = %(run_id)s
220
+ Dec 11 22:00:01 ]
221
+ Dec 11 22:00:01 [parameters: {'agents_processed': 0, 'timesheets_created': 0, 'timesheets_updated': 0, 'assignments_processed': 0, 'expenses_processed': 0, 'summary_stats': {}, 'anomalies': [], 'execution_time_ms': 572, 'query_time_ms': 209, 'run_id': '2746d0ae-b21c-4161-bb5b-95e96a114142'}]
222
+ Dec 11 22:00:01 (Background on this error at: https://sqlalche.me/e/20/f405)
223
+ Dec 11 22:00:01 INFO: 2025-12-11T22:00:01 - app.tasks.scheduler: Validation summary: 0/1 projects succeeded
224
+ Dec 11 22:00:01 WARNING: 2025-12-11T22:00:01 - app.tasks.scheduler: Failed projects: ['Atomio Fttx']
225
+ Dec 11 22:00:01 INFO: 2025-12-11T22:00:01 - app.tasks.scheduler: Scheduled validation completed for 2025-12-11
226
+ Dec 11 22:00:01 INFO: 2025-12-11T22:00:01 - apscheduler.executors.default: Job "Daily Field Agent Reconciliation (trigger: cron[hour='22', minute='0'], next run at: 2025-12-12 22:00:00 UTC)" executed successfully
227
+ Dec 11 22:00:01 INFO: 2025-12-11T22:00:01 - app.tasks.scheduler: Job daily_validation completed successfully
228
+ Dec 12 06:27:33 INFO: 10.244.16.189:59402 - "GET /health HTTP/1.1" 200 OK
docs/features/realtime-timesheet-updates.md ADDED
@@ -0,0 +1,954 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Real-Time Timesheet Updates & Reporting System
2
+
3
+ **Version:** 1.0
4
+ **Status:** Design Phase
5
+ **Created:** 2025-12-11
6
+ **Priority:** High
7
+
8
+ ---
9
+
10
+ ## Executive Summary
11
+
12
+ This document describes the Real-Time Timesheet Updates system - a replacement for the broken reconciliation service. The system maintains timesheets as immutable daily snapshots that aggregate field agent activity (tickets, expenses, inventory) in real-time as events occur.
13
+
14
+ **Key Principles:**
15
+ - Timesheets are immutable daily snapshots (historical records)
16
+ - Updates happen in real-time as events occur
17
+ - Only current day's timesheet is updated
18
+ - Past timesheets never change (preserves historical accuracy)
19
+ - Timesheets serve as a performance cache to avoid scanning source tables
20
+ - Failed updates are tracked for manual reconciliation
21
+
22
+ ---
23
+
24
+ ## Problem Statement
25
+
26
+ ### Current Issues
27
+ 1. **Broken reconciliation service** - Scheduled job fails with PostgreSQL adapter errors
28
+ 2. **Over-engineered solution** - Complex batch processing when real-time updates already work
29
+ 3. **Redundant validation** - Scheduled job validates what real-time updates already did
30
+ 4. **Unnecessary complexity** - 1000+ lines of code for audit trails nobody uses
31
+
32
+ ### Business Requirements
33
+ - Track daily agent activity (tickets, expenses, inventory)
34
+ - Support payroll calculations from timesheet data
35
+ - Enable performance reporting without scanning all assignments
36
+ - Provide real-time visibility into agent productivity
37
+ - Support multi-project work (agents can work multiple projects per day)
38
+
39
+ ---
40
+
41
+ ## Solution Overview
42
+
43
+ ### Core Concept: Timesheets as Immutable Snapshots
44
+
45
+ Timesheets represent **what happened on a specific date** - like a photograph that doesn't change once taken.
46
+
47
+ **Example:**
48
+ - December 20th: Agent completes 5 tickets, submits 3 expenses
49
+ - December 20th timesheet: `tickets_completed = 5`, `expense_claims_count = 3`
50
+ - December 21st: Admin deletes one of those tickets
51
+ - December 20th timesheet: **UNCHANGED** (still shows 5 tickets)
52
+ - Reason: The agent DID complete 5 tickets on Dec 20th - that's historical fact
53
+
54
+ ### Real-Time Update Flow
55
+
56
+ **When event occurs:**
57
+ 1. Main operation completes (ticket completed, expense created, etc.)
58
+ 2. System attempts to update today's timesheet
59
+ 3. If successful: Mark source record as `timesheet_synced = TRUE, timesheet_synced_at = NOW()`
60
+ 4. If failed: Leave `timesheet_synced = FALSE` for later reconciliation
61
+ 5. Main operation succeeds regardless of timesheet update result
62
+
63
+ **Key Design Decision:** Timesheet update failures don't block user operations.
64
+
65
+ ---
66
+
67
+ ## Architecture
68
+
69
+ ### Components
70
+
71
+ **1. Timesheet Update Service** (NEW)
72
+ - Single focused service for real-time updates
73
+ - Aggregates current counts from source tables
74
+ - Upserts timesheet with fresh data
75
+ - Handles optimistic locking conflicts
76
+ - Marks source records as reconciled
77
+
78
+ **2. Source Tables** (MODIFIED)
79
+ - `ticket_assignments` - Add `timesheet_synced` and `timesheet_synced_at` columns
80
+ - `ticket_expenses` - Add `timesheet_synced` and `timesheet_synced_at` columns
81
+ - `tickets` - Add `timesheet_synced` and `timesheet_synced_at` columns
82
+ - `inventory_assignments` - Add `timesheet_synced` and `timesheet_synced_at` columns (future)
83
+
84
+ **3. Timesheets Table** (MODIFIED)
85
+ - Fix unique constraint to support multi-project per day
86
+ - Add `version` column for optimistic locking
87
+ - Keep all existing metric columns
88
+
89
+ **4. Manual Reconciliation API** (NEW)
90
+ - Endpoint to find orphaned records (NULL `timesheet_reconciled_at`)
91
+ - Endpoint to recalculate specific timesheet
92
+ - Admin tool for fixing failed updates
93
+
94
+ ---
95
+
96
+ ## Database Schema Changes
97
+
98
+ ### 1. Fix Timesheets Unique Constraint
99
+
100
+ **Current Problem:** Constraint prevents multiple projects per day
101
+
102
+
103
+ **Current Constraint:**
104
+ - `UNIQUE (user_id, work_date)`
105
+ - Allows only one timesheet per user per day
106
+
107
+ **New Constraint:**
108
+ - `UNIQUE (user_id, project_id, work_date)`
109
+ - Allows multiple timesheets per day (one per project)
110
+
111
+ **Migration Required:** Drop old constraint, add new one
112
+
113
+ ### 2. Add Optimistic Locking to Timesheets
114
+
115
+ **Purpose:** Prevent concurrent update conflicts
116
+
117
+ **Changes:**
118
+ - Add `version` column (INTEGER, default 1)
119
+ - Add trigger to auto-increment version on update
120
+ - Update logic checks version before writing
121
+
122
+ **Behavior:**
123
+ - Each update increments version
124
+ - Concurrent updates detected and retried
125
+ - Prevents lost updates in high-concurrency scenarios
126
+
127
+ ### 3. Add Timesheet Sync Tracking to Source Tables
128
+
129
+ **Purpose:** Track which records successfully updated timesheets
130
+
131
+ **Tables to Modify:**
132
+ - `ticket_assignments`
133
+ - `ticket_expenses`
134
+ - `tickets`
135
+ - `inventory_assignments` (future)
136
+
137
+ **Columns to Add:**
138
+ - `timesheet_synced` (BOOLEAN, default FALSE, not null)
139
+ - `timesheet_synced_at` (TIMESTAMP WITH TIME ZONE, nullable)
140
+
141
+ **Usage:**
142
+ - `timesheet_synced = FALSE` → Not yet synced (failed or pending)
143
+ - `timesheet_synced = TRUE` → Successfully synced to timesheet
144
+ - `timesheet_synced_at` → Timestamp when sync succeeded
145
+
146
+ **Benefits:**
147
+ - Find unsynced records easily with boolean check
148
+ - Support manual reconciliation
149
+ - Audit trail of sync success/failure
150
+ - Fast queries (boolean index more efficient than NULL checks)
151
+
152
+ ### 4. Existing Columns (No Changes)
153
+
154
+ **Timesheets already has all needed columns:**
155
+
156
+ **Ticket Metrics:**
157
+ - `tickets_assigned` - Assignments created this day
158
+ - `tickets_completed` - Tickets completed this day
159
+ - `tickets_rejected` - Assignments rejected this day
160
+ - `tickets_cancelled` - Assignments dropped this day
161
+ - `tickets_rescheduled` - Assignments rescheduled this day
162
+
163
+ **Expense Metrics:**
164
+ - `total_expenses` - Total expense amount claimed
165
+ - `approved_expenses` - Approved expense amount
166
+ - `pending_expenses` - Pending approval amount
167
+ - `rejected_expenses` - Rejected expense amount
168
+ - `expense_claims_count` - Number of expense claims
169
+
170
+ **Inventory Metrics:**
171
+ - `inventory_issued_count` - Items issued to agent
172
+ - `inventory_installed_count` - Items installed at sites
173
+ - `inventory_consumed_count` - Consumables used
174
+ - `inventory_returned_count` - Items returned to hub
175
+ - `inventory_lost_count` - Items reported lost
176
+ - `inventory_damaged_count` - Items reported damaged
177
+ - `inventory_on_hand_count` - Items in agent possession
178
+ - `inventory_on_hand_value` - Value of inventory on hand
179
+ - `inventory_details` - JSONB breakdown by equipment type
180
+
181
+ **Time Tracking:**
182
+ - `check_in_time` - First activity of the day
183
+ - `check_out_time` - Last activity of the day
184
+ - `hours_worked` - Total hours (check_out - check_in)
185
+
186
+ **Payroll Linkage:**
187
+ - `is_payroll_generated` - Whether used in payroll
188
+ - `payroll_id` - Link to payroll record
189
+
190
+ ---
191
+
192
+ ## Integration Points
193
+
194
+ ### Events That Trigger Timesheet Updates
195
+
196
+ **Rule:** Only update timesheet if event date equals today's date
197
+
198
+ #### Ticket Assignments (6 events)
199
+
200
+ **1. Assignment Created**
201
+ - When: Dispatcher assigns ticket to agent
202
+ - Updates: `tickets_assigned` on assignment date
203
+ - Date: `assignment.assigned_at.date()`
204
+
205
+ **2. Assignment Accepted**
206
+ - When: Agent accepts assignment
207
+ - Updates: No metric change (just status)
208
+ - Action: Update timesheet to refresh check-in time
209
+
210
+ **3. Assignment Rejected**
211
+ - When: Agent rejects assignment
212
+ - Updates: `tickets_rejected` on assignment date
213
+ - Date: `assignment.assigned_at.date()`
214
+
215
+ **4. Assignment Dropped**
216
+ - When: Agent drops ticket (can't complete)
217
+ - Updates: `tickets_cancelled` on drop date
218
+ - Date: `assignment.ended_at.date()`
219
+
220
+ **5. Assignment Completed**
221
+ - When: Agent completes work
222
+ - Updates: `tickets_completed` on completion date
223
+ - Date: `assignment.ended_at.date()`
224
+
225
+ **6. Self-Assignment**
226
+ - When: Agent picks ticket from available pool
227
+ - Updates: `tickets_assigned` on assignment date
228
+ - Date: `assignment.assigned_at.date()`
229
+
230
+ #### Ticket Completion (1 event)
231
+
232
+ **1. Ticket Marked Complete**
233
+ - When: Ticket status changes to completed
234
+ - Updates: All assignments for this ticket
235
+ - Date: `ticket.completed_at.date()`
236
+ - Note: May update multiple agents' timesheets
237
+
238
+ #### Ticket Expenses (4 events)
239
+
240
+ **1. Expense Created**
241
+ - When: Agent submits expense claim
242
+ - Updates: `total_expenses`, `pending_expenses`, `expense_claims_count`
243
+ - Date: `expense.expense_date`
244
+ - Constraint: Can only create expenses for today's work
245
+
246
+ **2. Expense Updated**
247
+ - When: Agent edits expense amount before approval
248
+ - Updates: Recalculate expense totals
249
+ - Date: `expense.expense_date`
250
+
251
+ **3. Expense Approved**
252
+ - When: Manager approves expense
253
+ - Updates: Move from `pending_expenses` to `approved_expenses`
254
+ - Date: `expense.expense_date`
255
+
256
+ **4. Expense Rejected**
257
+ - When: Manager rejects expense
258
+ - Updates: Move from `pending_expenses` to `rejected_expenses`
259
+ - Date: `expense.expense_date`
260
+
261
+ #### Inventory (Future - 6 events)
262
+
263
+ **1. Inventory Issued**
264
+ - When: Agent collects equipment from hub
265
+ - Updates: `inventory_issued_count`, `inventory_on_hand_count`
266
+ - Date: `assignment.issued_at.date()`
267
+
268
+ **2. Inventory Installed**
269
+ - When: Equipment installed at customer site
270
+ - Updates: `inventory_installed_count`, `inventory_on_hand_count`
271
+ - Date: `assignment.installed_at.date()`
272
+
273
+ **3. Inventory Consumed**
274
+ - When: Consumables used up
275
+ - Updates: `inventory_consumed_count`, `inventory_on_hand_count`
276
+ - Date: `assignment.consumed_at.date()`
277
+
278
+ **4. Inventory Returned**
279
+ - When: Tools returned to hub
280
+ - Updates: `inventory_returned_count`, `inventory_on_hand_count`
281
+ - Date: `assignment.returned_at.date()`
282
+
283
+ **5. Inventory Lost**
284
+ - When: Equipment reported lost
285
+ - Updates: `inventory_lost_count`, `inventory_on_hand_count`
286
+ - Date: Today
287
+
288
+ **6. Inventory Damaged**
289
+ - When: Equipment reported damaged
290
+ - Updates: `inventory_damaged_count`, `inventory_on_hand_count`
291
+ - Date: Today
292
+
293
+ **Total Integration Points:** 17 events (11 active now, 6 future)
294
+
295
+ ---
296
+
297
+ ## Edge Cases & Handling
298
+
299
+ ### 1. Cross-Day Activities
300
+
301
+ **Scenario:** Assignment created Dec 20, completed Dec 21
302
+
303
+ **Handling:**
304
+ - Dec 20 timesheet: `tickets_assigned += 1`
305
+ - Dec 21 timesheet: `tickets_completed += 1`
306
+ - Both days get credit for different metrics
307
+
308
+ **Implementation:** Call update twice with different dates
309
+
310
+ ### 2. Multi-Project Work Same Day
311
+
312
+ **Scenario:** Agent works Project A in morning, Project B in afternoon
313
+
314
+ **Handling:**
315
+ - Create two separate timesheets
316
+ - Timesheet 1: `user_id=X, project_id=A, work_date=Dec 20`
317
+ - Timesheet 2: `user_id=X, project_id=B, work_date=Dec 20`
318
+
319
+ **Database Support:** Unique constraint includes `project_id`
320
+
321
+ ### 3. Backdated Activities
322
+
323
+ **Scenario:** Admin creates assignment with past date
324
+
325
+ **Handling:**
326
+ - Check if `work_date < today`
327
+ - If yes: Skip timesheet update (past is immutable)
328
+ - Log warning for audit trail
329
+
330
+ **Rationale:** Timesheets are snapshots of what was known at end of day
331
+
332
+ ### 4. Deleted Records
333
+
334
+ **Scenario:** Admin deletes expense that was counted in timesheet
335
+
336
+ **Handling:**
337
+ - Past timesheets: No change (historical accuracy preserved)
338
+ - Current day: Recalculate if deletion happens today
339
+ - Future: Record stays deleted, timesheet reflects historical state
340
+
341
+ **Philosophy:** Timesheet shows "what happened that day" not "current state of records"
342
+
343
+ ### 5. Failed Timesheet Updates
344
+
345
+ **Scenario:** Database error during timesheet update
346
+
347
+ **Handling:**
348
+ - Main operation (ticket completion) succeeds
349
+ - Timesheet update fails
350
+ - Source record's `timesheet_synced` stays FALSE
351
+ - Log error for monitoring
352
+ - Manual reconciliation API can fix later
353
+
354
+ **Rationale:** Don't block user operations due to reporting failures
355
+
356
+ ### 6. Concurrent Updates
357
+
358
+ **Scenario:** Two API requests update same timesheet simultaneously
359
+
360
+ **Handling:**
361
+ - Optimistic locking with `version` column
362
+ - First update succeeds, increments version
363
+ - Second update detects version mismatch
364
+ - Second update retries with fresh data
365
+ - Both updates eventually succeed
366
+
367
+ **Implementation:** Check version before update, retry on conflict
368
+
369
+ ### 7. Expense Date Constraints
370
+
371
+ **Scenario:** Agent tries to add expense for yesterday's work
372
+
373
+ **Handling:**
374
+ - Validation prevents this at API level
375
+ - Expenses can only be created for today's assignments
376
+ - Constraint: `expense.expense_date = today`
377
+
378
+ **Rationale:** Prevents backdating, simplifies timesheet logic
379
+
380
+ ### 8. Check-In/Out Time Calculation
381
+
382
+ **Scenario:** Agent has multiple assignments same day
383
+
384
+ **Handling:**
385
+ - Check-in: Earliest timestamp of the day (assigned_at, journey_started_at, arrived_at)
386
+ - Check-out: Latest timestamp of the day (ended_at, arrived_at)
387
+ - Hours worked: check_out - check_in
388
+
389
+ **Source:** Calculated from ticket_assignments table
390
+
391
+ ### 9. Payroll Already Generated
392
+
393
+ **Scenario:** Timesheet updated after payroll generated for that week
394
+
395
+ **Handling:**
396
+ - If payroll unpaid: Auto-recalculate payroll
397
+ - If payroll paid: Allow timesheet update, log discrepancy
398
+ - Payroll service already handles this
399
+
400
+ **Note:** Existing payroll recalculation logic remains unchanged
401
+
402
+ ### 10. Zero Activity Days
403
+
404
+ **Scenario:** Agent assigned to project but does nothing all day
405
+
406
+ **Handling:**
407
+ - No timesheet created (only created when events occur)
408
+ - Absence tracking is separate concern (future feature)
409
+ - Timesheets track activity, not attendance
410
+
411
+ **Future:** Background job can create empty timesheets for attendance tracking
412
+
413
+ ---
414
+
415
+ ## Data Flow
416
+
417
+ ### Update Sequence
418
+
419
+ **1. Event Occurs**
420
+ - User action triggers event (complete ticket, create expense, etc.)
421
+ - Main business logic executes
422
+ - Database transaction commits
423
+
424
+ **2. Timesheet Update Triggered**
425
+ - Extract: user_id, project_id, work_date from event
426
+ - Check: Is work_date == today? If no, skip update
427
+ - Call: Timesheet update service
428
+
429
+ **3. Aggregation**
430
+ - Query ticket_assignments for ticket metrics
431
+ - Query ticket_expenses for expense metrics
432
+ - Query inventory_assignments for inventory metrics (future)
433
+ - Calculate check-in/out times from assignments
434
+
435
+ **4. Upsert Timesheet**
436
+ - Check optimistic lock (version)
437
+ - INSERT or UPDATE timesheet with aggregated data
438
+ - Increment version on update
439
+
440
+ **5. Mark Synced**
441
+ - Update source record: `timesheet_synced = TRUE, timesheet_synced_at = NOW()`
442
+ - Commit transaction
443
+
444
+ **6. Error Handling**
445
+ - If any step fails: Log error, leave `timesheet_synced = FALSE`
446
+ - Main operation already succeeded (not rolled back)
447
+
448
+ ### Aggregation Queries
449
+
450
+ **Ticket Metrics Query:**
451
+ - Count assignments by action type (assigned, accepted, rejected, dropped)
452
+ - Count completed tickets by completion date
453
+ - Filter: user_id, project_id, work_date, deleted_at IS NULL
454
+
455
+ **Expense Metrics Query:**
456
+ - Sum expenses by approval status (approved, pending, rejected)
457
+ - Count expense claims
458
+ - Filter: user_id, project_id, expense_date, deleted_at IS NULL
459
+
460
+ **Inventory Metrics Query:**
461
+ - Count issued, installed, consumed, returned, lost, damaged
462
+ - Calculate on-hand count and value
463
+ - Filter: user_id, project_id, date, deleted_at IS NULL
464
+
465
+ **Time Tracking Query:**
466
+ - MIN(assigned_at, journey_started_at, arrived_at) as check_in
467
+ - MAX(ended_at, arrived_at) as check_out
468
+ - Filter: user_id, project_id, work_date, deleted_at IS NULL
469
+
470
+ ---
471
+
472
+ ## API Endpoints
473
+
474
+ ### Real-Time Update (Internal)
475
+
476
+ **Purpose:** Called internally by event handlers
477
+
478
+ **Function:** `update_timesheet_realtime(user_id, project_id, work_date, event_type)`
479
+
480
+ **Returns:** Timesheet ID or None
481
+
482
+ **Error Handling:** Logs errors, doesn't raise exceptions
483
+
484
+ ### Manual Reconciliation
485
+
486
+ **Endpoint:** `POST /api/v1/timesheets/reconcile-unsynced`
487
+
488
+ **Purpose:** Find and reconcile records with `timesheet_synced = FALSE`
489
+
490
+ **Parameters:**
491
+ - `project_id` (optional) - Filter by project
492
+ - `user_id` (optional) - Filter by user
493
+ - `since` (optional) - Only records created after this time
494
+ - `limit` (optional) - Max records to process
495
+
496
+ **Returns:**
497
+ - Count of records found
498
+ - Count of records successfully synced
499
+ - List of errors
500
+
501
+ **Authorization:** Project Manager, Dispatcher, Platform Admin
502
+
503
+ ### Recalculate Timesheet
504
+
505
+ **Endpoint:** `POST /api/v1/timesheets/{timesheet_id}/recalculate`
506
+
507
+ **Purpose:** Manually recalculate specific timesheet
508
+
509
+ **Use Case:** Admin needs to fix incorrect timesheet
510
+
511
+ **Behavior:**
512
+ - Re-runs aggregation queries
513
+ - Updates timesheet with fresh data
514
+ - Marks all related records as synced (`timesheet_synced = TRUE`)
515
+
516
+ **Authorization:** Project Manager, Platform Admin
517
+
518
+ ### Find Unsynced Records
519
+
520
+ **Endpoint:** `GET /api/v1/timesheets/unsynced-records`
521
+
522
+ **Purpose:** List records that failed to sync to timesheets
523
+
524
+ **Parameters:**
525
+ - `entity_type` - 'assignment', 'expense', 'ticket'
526
+ - `project_id` (optional)
527
+ - `since` (optional)
528
+
529
+ **Returns:** List of records with `timesheet_synced = FALSE`
530
+
531
+ **Authorization:** Project Manager, Dispatcher, Platform Admin
532
+
533
+ ---
534
+
535
+ ## Payroll Integration
536
+
537
+ ### Current Payroll Flow (Unchanged)
538
+
539
+ **Payroll service already reads from timesheets:**
540
+ - Function: `aggregate_from_timesheets(user_id, project_id, start_date, end_date)`
541
+ - Sums ticket metrics across date range
542
+ - Calculates compensation based on tickets completed
543
+ - Links timesheets to payroll record
544
+
545
+ **No changes needed to payroll service** - it already works with timesheets
546
+
547
+ ### Real-Time Payroll Calculation
548
+
549
+ **Current Week Payroll:**
550
+ - Calculate on-demand from current week's timesheets
551
+ - Don't save to `user_payroll` table until week ends
552
+ - Provides live view of earnings
553
+
554
+ **Week-End Payroll:**
555
+ - Generate payroll record when week completes
556
+ - Saves to `user_payroll` table
557
+ - Links timesheets via `payroll_id`
558
+
559
+ **Payroll Recalculation:**
560
+ - If timesheet updated and payroll unpaid: Auto-recalculate
561
+ - If timesheet updated and payroll paid: Log discrepancy, don't recalculate
562
+ - Existing logic handles this
563
+
564
+ ---
565
+
566
+ ## Performance Considerations
567
+
568
+ ### Database Load
569
+
570
+ **Expected Load:**
571
+ - 500 agents × 10 events/day = 5,000 timesheet updates/day
572
+ - ~3-5 updates/minute average
573
+ - ~50 updates/minute peak hours
574
+
575
+ **Assessment:** This is negligible load for PostgreSQL
576
+
577
+ **Optimizations:**
578
+ - All queries use indexes (already exist)
579
+ - Simple aggregation queries (no complex joins)
580
+ - Upsert operations (efficient)
581
+ - No table scans
582
+
583
+ ### Query Performance
584
+
585
+ **Aggregation queries are fast because:**
586
+ - Filtered by user_id, project_id, work_date (all indexed)
587
+ - Small result sets (one user, one day)
588
+ - No joins to large tables
589
+ - Indexes on foreign keys
590
+
591
+ **Monitoring:**
592
+ - Log slow queries (> 100ms)
593
+ - Track timesheet update failures
594
+ - Monitor orphaned record count
595
+
596
+ ### Scalability
597
+
598
+ **Current design scales to:**
599
+ - 1,000+ agents
600
+ - 100+ projects
601
+ - 10,000+ updates/day
602
+
603
+ **Future optimizations if needed:**
604
+ - Batch updates for bulk operations
605
+ - Caching for read-heavy scenarios
606
+ - Async processing for non-critical updates
607
+
608
+ ---
609
+
610
+ ## Testing Strategy
611
+
612
+ ### Unit Tests
613
+
614
+ **Timesheet Update Service:**
615
+ - Test aggregation queries return correct counts
616
+ - Test upsert creates new timesheet
617
+ - Test upsert updates existing timesheet
618
+ - Test optimistic locking conflict handling
619
+ - Test date validation (skip past dates)
620
+ - Test multi-project support
621
+
622
+ **Edge Cases:**
623
+ - Cross-day activities
624
+ - Concurrent updates
625
+ - Failed updates
626
+ - Zero activity
627
+ - Deleted records
628
+
629
+ ### Integration Tests
630
+
631
+ **End-to-End Flows:**
632
+ - Create assignment → Verify timesheet updated
633
+ - Complete ticket → Verify timesheet updated
634
+ - Create expense → Verify timesheet updated
635
+ - Approve expense → Verify timesheet updated
636
+ - Multiple events same day → Verify counts accurate
637
+
638
+ **Multi-Project:**
639
+ - Agent works two projects → Verify two timesheets created
640
+ - Verify correct metrics on each timesheet
641
+
642
+ **Failure Scenarios:**
643
+ - Database error → Verify main operation succeeds
644
+ - Verify `timesheet_reconciled_at` stays NULL
645
+ - Verify manual reconciliation fixes it
646
+
647
+ ### Manual Testing
648
+
649
+ **Pre-Deployment:**
650
+ - Create test project with test agents
651
+ - Perform various activities
652
+ - Verify timesheet counts match actual activity
653
+ - Test payroll calculation from timesheets
654
+ - Test manual reconciliation API
655
+
656
+ **Post-Deployment:**
657
+ - Monitor error logs for 24 hours
658
+ - Spot-check timesheet accuracy for 10 agents
659
+ - Verify no performance degradation
660
+ - Check orphaned record count
661
+
662
+ ---
663
+
664
+ ## Migration Plan
665
+
666
+ ### Phase 1: Database Changes
667
+
668
+ **1. Add timesheet sync tracking columns**
669
+ - Add `timesheet_synced` (BOOLEAN, default FALSE) to source tables
670
+ - Add `timesheet_synced_at` (TIMESTAMP) to source tables
671
+ - Add indexes on `timesheet_synced` for fast queries
672
+ - No data migration needed (starts FALSE/NULL)
673
+
674
+ **2. Fix timesheets unique constraint**
675
+ - Drop old constraint
676
+ - Add new constraint with project_id
677
+ - Verify no duplicate violations
678
+
679
+ **3. Add optimistic locking**
680
+ - Add `version` column to timesheets
681
+ - Add auto-increment trigger
682
+ - Set existing rows to version=1
683
+
684
+ **Estimated Time:** 30 minutes
685
+
686
+ ### Phase 2: Create Update Service
687
+
688
+ **1. Create new service file**
689
+ - `src/app/services/timesheet_realtime_service.py`
690
+ - Implement aggregation logic
691
+ - Implement upsert logic
692
+ - Add error handling
693
+
694
+ **2. Write unit tests**
695
+ - Test all aggregation queries
696
+ - Test upsert logic
697
+ - Test edge cases
698
+
699
+ **Estimated Time:** 2-3 hours
700
+
701
+ ### Phase 3: Replace Integration Points
702
+
703
+ **1. Update ticket assignment endpoints**
704
+ - Replace reconciliation service calls
705
+ - Add timesheet sync tracking
706
+ - Test each endpoint
707
+
708
+ **2. Update ticket completion endpoints**
709
+ - Replace reconciliation service calls
710
+ - Add timesheet sync tracking
711
+ - Test
712
+
713
+ **3. Update expense endpoints**
714
+ - Replace reconciliation service calls
715
+ - Add timesheet sync tracking
716
+ - Test
717
+
718
+ **Estimated Time:** 2 hours
719
+
720
+ ### Phase 4: Add Manual Tools
721
+
722
+ **1. Create reconciliation API endpoints**
723
+ - Unsynced records finder
724
+ - Manual sync trigger
725
+ - Timesheet recalculation
726
+
727
+ **2. Test manual tools**
728
+ - Verify unsynced records found
729
+ - Verify manual sync works
730
+ - Verify recalculation accurate
731
+
732
+ **Estimated Time:** 1 hour
733
+
734
+ ### Phase 5: Remove Old System
735
+
736
+ **1. Delete reconciliation service**
737
+ - Remove service files
738
+ - Remove API endpoints
739
+ - Remove router registration
740
+
741
+ **2. Remove scheduled job**
742
+ - Remove from scheduler
743
+ - Verify no errors at 10 PM
744
+
745
+ **3. Drop database tables**
746
+ - Drop `reconciliation_runs`
747
+ - Drop `timesheet_updates`
748
+ - Drop helper functions
749
+
750
+ **Estimated Time:** 30 minutes
751
+
752
+ **Total Estimated Time:** 6-7 hours
753
+
754
+ ---
755
+
756
+ ## Monitoring & Observability
757
+
758
+ ### Metrics to Track
759
+
760
+ **Success Metrics:**
761
+ - Timesheet updates per day
762
+ - Average update time
763
+ - Success rate (%)
764
+
765
+ **Error Metrics:**
766
+ - Failed updates per day
767
+ - Unsynced records count
768
+ - Optimistic lock conflicts
769
+
770
+ **Performance Metrics:**
771
+ - Aggregation query time
772
+ - Upsert operation time
773
+ - 95th percentile latency
774
+
775
+ ### Logging
776
+
777
+ **Info Level:**
778
+ - Timesheet updated successfully
779
+ - Unsynced record reconciled
780
+ - Manual recalculation triggered
781
+
782
+ **Warning Level:**
783
+ - Skipped past date update
784
+ - Optimistic lock conflict (before retry)
785
+ - High unsynced record count
786
+
787
+ **Error Level:**
788
+ - Timesheet update failed
789
+ - Aggregation query failed
790
+ - Database error
791
+
792
+ ### Alerts
793
+
794
+ **Critical:**
795
+ - Unsynced records > 100
796
+ - Error rate > 5%
797
+ - Update failures > 50/hour
798
+
799
+ **Warning:**
800
+ - Unsynced records > 10
801
+ - Error rate > 1%
802
+ - Slow queries > 500ms
803
+
804
+ ---
805
+
806
+ ## Rollback Plan
807
+
808
+ ### If Issues Arise
809
+
810
+ **Scenario 1: High error rate**
811
+ - Investigate logs for root cause
812
+ - Fix bug if identified
813
+ - If unfixable: Revert code changes
814
+ - Redeploy previous version
815
+
816
+ **Scenario 2: Performance issues**
817
+ - Check slow query logs
818
+ - Add missing indexes if needed
819
+ - Optimize aggregation queries
820
+ - Consider caching
821
+
822
+ **Scenario 3: Data accuracy issues**
823
+ - Run manual reconciliation for affected dates
824
+ - Verify aggregation queries correct
825
+ - Fix queries if needed
826
+ - Re-run reconciliation
827
+
828
+ **Scenario 4: Database migration issues**
829
+ - Restore database from backup
830
+ - Fix migration script
831
+ - Re-test on staging
832
+ - Re-run migration
833
+
834
+ ### Rollback Steps
835
+
836
+ **1. Code Rollback**
837
+ - Git revert to previous commit
838
+ - Redeploy application
839
+ - Verify old system working
840
+
841
+ **2. Database Rollback**
842
+ - Keep new columns (no harm)
843
+ - Or restore from backup if needed
844
+ - Verify data integrity
845
+
846
+ **3. Communication**
847
+ - Notify team of rollback
848
+ - Document issues encountered
849
+ - Plan fix and retry
850
+
851
+ ---
852
+
853
+ ## Success Criteria
854
+
855
+ ### Must Have
856
+
857
+ 1. ✅ No more reconciliation errors in logs
858
+ 2. ✅ Timesheets update in real-time when events occur
859
+ 3. ✅ Timesheet counts match actual activity
860
+ 4. ✅ Multi-project support works
861
+ 5. ✅ Payroll calculations still work
862
+ 6. ✅ No performance degradation
863
+
864
+ ### Nice to Have
865
+
866
+ 1. 🎯 Faster response times (less overhead)
867
+ 2. 🎯 Simpler codebase (easier maintenance)
868
+ 3. 🎯 Better error messages
869
+ 4. 🎯 Monitoring dashboard for timesheet accuracy
870
+ 5. 🎯 Automated alerts for unsynced records
871
+
872
+ ---
873
+
874
+ ## Future Enhancements
875
+
876
+ ### Short Term (Next Sprint)
877
+
878
+ **1. Inventory Integration**
879
+ - Add inventory event handlers
880
+ - Test inventory tracking
881
+ - Verify on-hand calculations
882
+
883
+ **2. Absence Tracking**
884
+ - Background job to create empty timesheets
885
+ - Mark absent days
886
+ - Support leave management
887
+
888
+ **3. Performance Dashboard**
889
+ - Real-time agent activity view
890
+ - Daily/weekly performance charts
891
+ - Comparison across agents
892
+
893
+ ### Long Term (Future Quarters)
894
+
895
+ **1. Predictive Analytics**
896
+ - Forecast agent productivity
897
+ - Identify performance trends
898
+ - Anomaly detection
899
+
900
+ **2. Automated Reconciliation**
901
+ - Background job to fix unsynced records
902
+ - Smart retry logic
903
+ - Self-healing system
904
+
905
+ **3. Advanced Reporting**
906
+ - Custom report builder
907
+ - Export to Excel/PDF
908
+ - Scheduled report delivery
909
+
910
+ ---
911
+
912
+ ## Appendix
913
+
914
+ ### Related Documentation
915
+
916
+ - Timesheet Model: `src/app/models/timesheet.py`
917
+ - Payroll Service: `src/app/services/payroll_service.py`
918
+ - Ticket Assignments: `src/app/api/v1/ticket_assignments.py`
919
+ - Ticket Expenses: `src/app/api/v1/ticket_expenses.py`
920
+ - Current Reconciliation (to be removed): `src/app/services/reconciliation/`
921
+
922
+ ### Database Tables
923
+
924
+ **Primary:**
925
+ - `timesheets` - Daily activity snapshots
926
+ - `ticket_assignments` - Assignment lifecycle
927
+ - `ticket_expenses` - Expense claims
928
+ - `tickets` - Work orders
929
+ - `inventory_assignments` - Equipment tracking
930
+
931
+ **Supporting:**
932
+ - `user_payroll` - Weekly compensation
933
+ - `project_team` - Agent-project assignments
934
+ - `projects` - Project definitions
935
+
936
+ ### Key Constraints
937
+
938
+ **Timesheets:**
939
+ - Unique: (user_id, project_id, work_date)
940
+ - Check: hours_worked >= 0 AND hours_worked <= 24
941
+ - Check: check_out_time >= check_in_time
942
+
943
+ **Source Tables:**
944
+ - All have `deleted_at` for soft deletes
945
+ - All have indexes on foreign keys
946
+ - All have `timesheet_synced` and `timesheet_synced_at` (after migration)
947
+ - Indexes on `timesheet_synced` for fast unsynced record queries
948
+
949
+ ---
950
+
951
+ **Document Version:** 1.0
952
+ **Last Updated:** 2025-12-11
953
+ **Author:** System Architect
954
+ **Status:** Ready for Implementation
docs/features/reconciliation-removal-plan.md ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Reconciliation System Removal & Real-Time Timesheet Updates Plan
2
+
3
+ **Status:** Planning Phase
4
+ **Created:** 2025-12-11
5
+ **Priority:** High
6
+ **Estimated Effort:** 4-6 hours
7
+
8
+ ---
9
+
10
+ ## Executive Summary
11
+
12
+ The current reconciliation system has two components:
13
+ 1. **Real-time updates** - Event-driven timesheet updates (✅ Working)
14
+ 2. **Scheduled validation** - Nightly batch job at 10 PM (❌ Broken, unnecessary)
15
+
16
+ **Problem:** The scheduled job fails with PostgreSQL adapter errors and adds complexity without value since real-time updates already work.
17
+
18
+ **Solution:** Remove the broken scheduled reconciliation system, keep and enhance real-time timesheet updates for accurate daily performance tracking.
19
+
20
+ ---
21
+
22
+ ## Current System Analysis
23
+
24
+ ### What Works ✅
25
+ - Real-time timesheet updates triggered by events:
26
+ - Assignment created/completed
27
+ - Ticket completed
28
+ - Expense created/approved/rejected
29
+ - Timesheets are updated immediately when events occur
30
+ - Expense tracking columns on timesheets
31
+
32
+ ### What's Broken ❌
33
+ - Scheduled reconciliation job (10 PM daily)
34
+ - Error: `can't adapt type 'dict'` when saving to JSONB columns
35
+ - Reconciliation service's `_complete_run()` method
36
+ - Anomaly detection (unused)
37
+ - Validation logic (redundant if real-time works)
38
+
39
+ ### What's Unnecessary 🗑️
40
+ - `reconciliation_runs` table - audit trail we don't use
41
+ - `timesheet_updates` audit table - over-engineered
42
+ - Scheduled validation - redundant if real-time is accurate
43
+ - Anomaly detection - not being monitored
44
+ - Discrepancy tracking - adds complexity
45
+
46
+ ---
47
+
48
+ ## Goals
49
+
50
+ ### Primary Goals
51
+ 1. **Remove broken scheduled reconciliation** - Stop nightly job errors
52
+ 2. **Keep real-time updates working** - Maintain event-driven timesheet updates
53
+ 3. **Ensure accurate daily metrics** - Timesheets reflect agent activity in real-time
54
+ 4. **Simplify codebase** - Remove 1000+ lines of unused/broken code
55
+
56
+ ### Secondary Goals
57
+ 1. **Improve performance** - Remove unnecessary database writes
58
+ 2. **Better maintainability** - Simpler system = easier to debug
59
+ 3. **Preserve historical data** - Keep existing timesheet data intact
60
+
61
+ ---
62
+
63
+ ## Architecture Changes
64
+
65
+ ### Before (Current - Broken)
66
+ ```
67
+ Event (assignment/expense)
68
+ → Real-time update (✅ works)
69
+ → Timesheet updated immediately
70
+
71
+ Scheduled Job (10 PM)
72
+ → Batch reconciliation (❌ fails)
73
+ → Validation & anomaly detection
74
+ → Update reconciliation_runs table
75
+ → Fail with dict adaptation error
76
+ ```
77
+
78
+ ### After (Proposed - Simplified)
79
+ ```
80
+ Event (assignment/expense)
81
+ → Real-time update (✅ enhanced)
82
+ → Timesheet updated immediately
83
+ → Done! No batch job needed.
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Detailed Removal Plan
89
+
90
+ ### Phase 1: Code Removal (Safe, No Database Changes)
91
+
92
+ #### 1.1 Remove Scheduled Job
93
+ **File:** `src/app/tasks/scheduler.py`
94
+
95
+ **Remove:**
96
+ - `run_daily_validation()` function
97
+ - `validate_all_projects()` function
98
+ - Scheduler job registration for reconciliation
99
+ - Keep: Scheduler infrastructure (may be used for other jobs)
100
+
101
+ **Impact:** Stops nightly errors, no more failed reconciliation runs
102
+
103
+ ---
104
+
105
+ #### 1.2 Remove Reconciliation Service Calls
106
+ **Files to modify:**
107
+ - `src/app/api/v1/ticket_assignments.py` (3 locations)
108
+ - `src/app/api/v1/ticket_completion.py` (1 location)
109
+ - `src/app/api/v1/ticket_expenses.py` (3 locations)
110
+
111
+ **Strategy:**
112
+ - **Option A (Recommended):** Keep the calls but make them simpler
113
+ - Replace complex `ReconciliationService.update_user_timesheet_realtime()`
114
+ - With direct timesheet update logic (see Phase 3)
115
+
116
+ - **Option B:** Remove calls entirely
117
+ - Only if we decide timesheets aren't needed
118
+ - Not recommended - timesheets are useful for reporting
119
+
120
+ **Decision:** Go with Option A - keep real-time updates, simplify implementation
121
+
122
+ ---
123
+
124
+ #### 1.3 Delete Reconciliation Service Files
125
+ **Files to delete:**
126
+ ```
127
+ src/app/services/reconciliation/
128
+ ├── __init__.py
129
+ ├── reconciliation_service.py (500+ lines)
130
+ ├── anomaly_detector.py (150+ lines)
131
+ └── models.py (80+ lines)
132
+
133
+ src/app/api/endpoints/reconciliation.py (300+ lines)
134
+ ```
135
+
136
+ **Impact:** Removes ~1000 lines of broken/unused code
137
+
138
+ ---
139
+
140
+ #### 1.4 Remove API Router Registration
141
+ **File:** `src/app/api/v1/router.py`
142
+
143
+ **Remove:**
144
+ ```python
145
+ from app.api.endpoints import reconciliation
146
+ api_router.include_router(reconciliation.router, prefix="/reconciliation", tags=["Reconciliation"])
147
+ ```
148
+
149
+ **Impact:** `/reconciliation` endpoints will 404 (check if frontend uses them)
150
+
151
+ ---
152
+
153
+ ### Phase 2: Database Cleanup (Requires Migration)
154
+
155
+ #### 2.1 Tables to Drop
156
+ ```sql
157
+ -- Audit tables (not used for reporting)
158
+ DROP TABLE IF EXISTS timesheet_updates CASCADE;
159
+ DROP TABLE IF EXISTS reconciliation_runs CASCADE;
160
+ ```
161
+
162
+ **Data Loss:** Yes, but these tables aren't used for business logic
163
+ **Backup Strategy:** Export to CSV before dropping (optional)
164
+
165
+ ---
166
+
167
+ #### 2.2 Columns to Remove from Timesheets
168
+ ```sql
169
+ ALTER TABLE timesheets
170
+ DROP COLUMN IF EXISTS reconciliation_run_id,
171
+ DROP COLUMN IF EXISTS last_reconciled_at,
172
+ DROP COLUMN IF EXISTS update_source,
173
+ DROP COLUMN IF EXISTS last_realtime_update_at,
174
+ DROP COLUMN IF EXISTS last_validated_at,
175
+ DROP COLUMN IF EXISTS needs_review,
176
+ DROP COLUMN IF EXISTS discrepancy_notes,
177
+ DROP COLUMN IF EXISTS version;
178
+ ```
179
+
180
+ **Keep these columns (useful for reporting):**
181
+ - `total_expenses`
182
+ - `approved_expenses`
183
+ - `pending_expenses`
184
+ - `rejected_expenses`
185
+ - `expense_claims_count`
186
+ - `tickets_assigned`
187
+ - `tickets_completed`
188
+ - `tickets_rejected`
189
+ - `tickets_cancelled`
190
+ - `tickets_rescheduled`
191
+
192
+ ---
193
+
194
+ #### 2.3 Functions to Drop
195
+ ```sql
196
+ DROP FUNCTION IF EXISTS log_timesheet_update CASCADE;
197
+ DROP FUNCTION IF EXISTS increment_timesheet_version CASCADE;
198
+ DROP FUNCTION IF EXISTS update_reconciliation_runs_updated_at CASCADE;
199
+ ```
200
+
201
+ ---
202
+
203
+ #### 2.4 Indexes to Drop
204
+ ```sql
205
+ -- Reconciliation-specific indexes
206
+ DROP INDEX IF EXISTS idx_reconciliation_runs_project_date;
207
+ DROP INDEX IF EXISTS idx_reconciliation_runs_status;
208
+ DROP INDEX IF EXISTS idx_timesheets_reconciliation_run;
209
+ DROP INDEX IF EXISTS idx_timesheets_realtime_updates;
210
+ DROP INDEX IF EXISTS idx_timesheets_needs_review;
211
+ -- ... (see migration file for complete list)
212
+ ```
213
+
214
+ ---
215
+
216
+ ### Phase 3: Implement Simplified Real-Time Updates
217
+
218
+ #### 3.1 Create New Timesheet Update Service
219
+ **File:** `src/app/services/timesheet_update_service.py` (NEW)
220
+
221
+ **Purpose:** Simple, focused service for real-time timesheet updates
222
+
223
+ **Core Function:**
224
+ ```python
225
+ def update_timesheet_for_event(
226
+ db: Session,
227
+ user_id: UUID,
228
+ project_id: UUID,
229
+ work_date: date,
230
+ event_type: str # 'assignment_created', 'ticket_completed', 'expense_approved', etc.
231
+ ) -> Optional[UUID]:
232
+ """
233
+ Update timesheet in real-time when an event occurs.
234
+
235
+ Strategy:
236
+ 1. Query current counts from source tables (assignments, expenses)
237
+ 2. Upsert timesheet with fresh counts
238
+ 3. Return timesheet ID
239
+
240
+ No audit trail, no validation, no anomaly detection.
241
+ Just accurate, real-time counts.
242
+ """
243
+ ```
244
+
245
+ **Key Features:**
246
+ - ✅ Simple aggregation queries (no complex joins)
247
+ - ✅ Idempotent (safe to call multiple times)
248
+ - ✅ Fast (single query per update)
249
+ - ✅ No external dependencies
250
+ - ❌ No audit trail (not needed)
251
+ - ❌ No anomaly detection (not monitored)
252
+ - ❌ No validation (trust the data)
253
+
254
+ ---
255
+
256
+ #### 3.2 Aggregation Queries
257
+
258
+ **For Ticket Metrics:**
259
+ ```sql
260
+ -- Count assignments for user on specific date
261
+ SELECT
262
+ COUNT(*) FILTER (WHERE action = 'assigned') as tickets_assigned,
263
+ COUNT(*) FILTER (WHERE action = 'accepted') as tickets_accepted,
264
+ COUNT(DISTINCT ticket_id) FILTER (WHERE ticket.status = 'completed'
265
+ AND DATE(ticket.completed_at) = :work_date) as tickets_completed,
266
+ COUNT(*) FILTER (WHERE action = 'rejected') as tickets_rejected,
267
+ COUNT(*) FILTER (WHERE action = 'dropped') as tickets_cancelled
268
+ FROM ticket_assignments ta
269
+ JOIN tickets t ON ta.ticket_id = t.id
270
+ WHERE ta.user_id = :user_id
271
+ AND t.project_id = :project_id
272
+ AND DATE(ta.assigned_at) = :work_date
273
+ AND ta.deleted_at IS NULL
274
+ ```
275
+
276
+ **For Expense Metrics:**
277
+ ```sql
278
+ -- Sum expenses for user on specific date
279
+ SELECT
280
+ COALESCE(SUM(total_cost), 0) as total_expenses,
281
+ COALESCE(SUM(total_cost) FILTER (WHERE is_approved = TRUE), 0) as approved_expenses,
282
+ COALESCE(SUM(total_cost) FILTER (WHERE is_approved IS NULL OR is_approved = FALSE
283
+ AND rejection_reason IS NULL), 0) as pending_expenses,
284
+ COALESCE(SUM(total_cost) FILTER (WHERE is_approved = FALSE
285
+ AND rejection_reason IS NOT NULL), 0) as rejected_expenses,
286
+ COUNT(*) as expense_claims_count
287
+ FROM ticket_expenses te
288
+ JOIN ticket_assignments ta ON te.ticket_assignment_id = ta.id
289
+ JOIN tickets t ON ta.ticket_id = t.id
290
+ WHERE te.incurred_by_user_id = :user_id
291
+ AND t.project_id = :project_id
292
+ AND te.expense_date = :work_date
293
+ AND te.deleted_at IS NULL
294
+ ```
295
+
296
+ **Upsert Timesheet:**
297
+ ```sql
298
+ INSERT INTO timesheets (
299
+ user_id, project_id, work_date,
300
+ tickets_assigned, tickets_completed, tickets_rejected, tickets_cancelled,
301
+ total_expenses, approved_expenses, pending_expenses, rejected_expenses,
302
+ expense_claims_count,
303
+ status, created_at, updated_at
304
+ ) VALUES (
305
+ :user_id, :project_id, :work_date,
306
+ :tickets_assigned, :tickets_completed, :tickets_rejected, :tickets_cancelled,
307
+ :total_expenses, :approved_expenses, :pending_expenses, :rejected_expenses,
308
+ :expense_claims_count,
309
+ 'present', NOW(), NOW()
310
+ )
311
+ ON CONFLICT (user_id, work_date) WHERE deleted_at IS NULL
312
+ DO UPDATE SET
313
+ tickets_assigned = EXCLUDED.tickets_assigned,
314
+ tickets_completed = EXCLUDED.tickets_completed,
315
+ tickets_rejected = EXCLUDED.tickets_rejected,
316
+ tickets_cancelled = EXCLUDED.tickets_cancelled,
317
+ total_expenses = EXCLUDED.total_expenses,
318
+ approved_expenses = EXCLUDED.approved_expenses,
319
+ pending_expenses = EXCLUDED.pending_expenses,
320
+ rejected_expenses = EXCLUDED.rejected_expenses,
321
+ expense_claims_count = EXCLUDED.expense_claims_count,
322
+ updated_at = NOW()
323
+ RETURNING id;
324
+ ```
325
+
326
+ ---
327
+
328
+ #### 3.3 Integration Points
329
+
330
+ **Replace existing calls:**
331
+
332
+ **Before:**
333
+ ```python
334
+ from app.services.reconciliation.reconciliation_service import ReconciliationService
335
+
336
+ reconciliation_service = ReconciliationService(db)
337
+ reconciliation_service.update_user_timesheet_realtime(
338
+ user_id=user_id,
339
+ project_id=project_id,
340
+ work_date=date.today(),
341
+ trigger_type='assignment_created',
342
+ trigger_entity_type='ticket_assignment',
343
+ trigger_entity_id=assignment.id
344
+ )
345
+ ```
346
+
347
+ **After:**
348
+ ```python
349
+ from app.services.timesheet_update_service import update_timesheet_for_event
350
+
351
+ update_timesheet_for_event(
352
+ db=db,
353
+ user_id=user_id,
354
+ project_id=project_id,
355
+ work_date=date.today(),
356
+ event_type='assignment_created'
357
+ )
358
+ ```
359
+
360
+ **Simpler, cleaner, no unnecessary parameters.**
361
+
362
+ ---
363
+
364
+ ### Phase 4: Testing & Validation
365
+
366
+ #### 4.1 Unit Tests
367
+ **File:** `tests/unit/test_timesheet_update_service.py` (NEW)
368
+
369
+ **Test Cases:**
370
+ 1. ✅ Create new timesheet when none exists
371
+ 2. ✅ Update existing timesheet with new counts
372
+ 3. ✅ Handle multiple events on same day (idempotent)
373
+ 4. ✅ Correctly aggregate ticket counts
374
+ 5. ✅ Correctly aggregate expense amounts
375
+ 6. ✅ Handle edge cases (no assignments, no expenses)
376
+
377
+ ---
378
+
379
+ #### 4.2 Integration Tests
380
+ **File:** `tests/integration/test_timesheet_realtime_updates.py` (NEW)
381
+
382
+ **Test Scenarios:**
383
+ 1. ✅ Create assignment → Timesheet updated
384
+ 2. ✅ Complete ticket → Timesheet updated
385
+ 3. ✅ Create expense → Timesheet updated
386
+ 4. ✅ Approve expense → Timesheet updated
387
+ 5. ✅ Multiple events same day → Counts accurate
388
+ 6. ✅ Events on different days → Separate timesheets
389
+
390
+ ---
391
+
392
+ #### 4.3 Manual Testing Checklist
393
+
394
+ **Pre-Deployment:**
395
+ - [ ] Create test project with test agents
396
+ - [ ] Assign tickets to agents
397
+ - [ ] Complete tickets
398
+ - [ ] Create expenses
399
+ - [ ] Approve/reject expenses
400
+ - [ ] Verify timesheet counts match actual activity
401
+ - [ ] Check timesheet API endpoints return correct data
402
+ - [ ] Verify payroll calculations use timesheet data correctly
403
+
404
+ **Post-Deployment:**
405
+ - [ ] Monitor error logs for 24 hours
406
+ - [ ] Verify no reconciliation errors at 10 PM
407
+ - [ ] Spot-check timesheet accuracy for 5-10 agents
408
+ - [ ] Verify reports still work (if they use timesheet data)
409
+
410
+ ---
411
+
412
+ ## Risk Assessment
413
+
414
+ ### High Risk ⚠️
415
+ **Risk:** Timesheet counts become inaccurate
416
+ **Mitigation:**
417
+ - Thorough testing before deployment
418
+ - Keep aggregation queries simple and correct
419
+ - Add logging to track update failures
420
+
421
+ **Risk:** Reports break if they depend on reconciliation tables
422
+ **Mitigation:**
423
+ - Search codebase for references to `reconciliation_runs`
424
+ - Check frontend for `/reconciliation` API calls
425
+ - Update or remove dependent features
426
+
427
+ ---
428
+
429
+ ### Medium Risk ⚠️
430
+ **Risk:** Database migration fails
431
+ **Mitigation:**
432
+ - Test migration on staging first
433
+ - Backup database before migration
434
+ - Have rollback plan ready
435
+
436
+ **Risk:** Performance issues with real-time updates
437
+ **Mitigation:**
438
+ - Aggregation queries are simple and indexed
439
+ - Updates happen async (don't block main request)
440
+ - Monitor query performance
441
+
442
+ ---
443
+
444
+ ### Low Risk ✅
445
+ **Risk:** Losing historical reconciliation data
446
+ **Impact:** Low - data wasn't being used for business logic
447
+ **Mitigation:** Export to CSV before dropping tables (optional)
448
+
449
+ ---
450
+
451
+ ## Implementation Checklist
452
+
453
+ ### Pre-Implementation
454
+ - [ ] Review this plan with team
455
+ - [ ] Identify any reports/features that use reconciliation data
456
+ - [ ] Check frontend for `/reconciliation` API usage
457
+ - [ ] Backup production database
458
+ - [ ] Test migration on staging environment
459
+
460
+ ---
461
+
462
+ ### Phase 1: Code Removal (Day 1)
463
+ - [ ] Remove scheduled job from `scheduler.py`
464
+ - [ ] Delete `src/app/services/reconciliation/` directory
465
+ - [ ] Delete `src/app/api/endpoints/reconciliation.py`
466
+ - [ ] Remove reconciliation router from `router.py`
467
+ - [ ] Commit: "Remove broken reconciliation service"
468
+
469
+ ---
470
+
471
+ ### Phase 2: Create New Service (Day 1-2)
472
+ - [ ] Create `src/app/services/timesheet_update_service.py`
473
+ - [ ] Implement `update_timesheet_for_event()` function
474
+ - [ ] Write unit tests
475
+ - [ ] Test aggregation queries manually
476
+ - [ ] Commit: "Add simplified timesheet update service"
477
+
478
+ ---
479
+
480
+ ### Phase 3: Replace Integration Points (Day 2)
481
+ - [ ] Update `ticket_assignments.py` (3 locations)
482
+ - [ ] Update `ticket_completion.py` (1 location)
483
+ - [ ] Update `ticket_expenses.py` (3 locations)
484
+ - [ ] Add error handling and logging
485
+ - [ ] Write integration tests
486
+ - [ ] Commit: "Replace reconciliation calls with direct updates"
487
+
488
+ ---
489
+
490
+ ### Phase 4: Database Migration (Day 2-3)
491
+ - [ ] Create Alembic migration
492
+ - [ ] Test migration on local database
493
+ - [ ] Test migration on staging database
494
+ - [ ] Verify timesheets still work after migration
495
+ - [ ] Commit: "Remove reconciliation database tables"
496
+
497
+ ---
498
+
499
+ ### Phase 5: Testing & Deployment (Day 3)
500
+ - [ ] Run full test suite
501
+ - [ ] Manual testing on staging
502
+ - [ ] Code review
503
+ - [ ] Deploy to production
504
+ - [ ] Monitor for 24 hours
505
+ - [ ] Verify no errors at 10 PM (old job time)
506
+
507
+ ---
508
+
509
+ ## Rollback Plan
510
+
511
+ ### If Issues Arise Post-Deployment
512
+
513
+ **Scenario 1: Timesheet updates fail**
514
+ - Revert code changes (git revert)
515
+ - Redeploy previous version
516
+ - Investigate and fix issues
517
+
518
+ **Scenario 2: Reports break**
519
+ - Identify which reports are affected
520
+ - Update reports to use timesheet data directly
521
+ - Or temporarily restore reconciliation tables (from backup)
522
+
523
+ **Scenario 3: Database migration issues**
524
+ - Restore database from backup
525
+ - Fix migration script
526
+ - Re-test on staging
527
+
528
+ ---
529
+
530
+ ## Success Criteria
531
+
532
+ ### Must Have ✅
533
+ 1. No more reconciliation errors in logs
534
+ 2. Timesheets update in real-time when events occur
535
+ 3. Timesheet counts are accurate (match actual activity)
536
+ 4. All existing reports still work
537
+ 5. No performance degradation
538
+
539
+ ### Nice to Have 🎯
540
+ 1. Faster response times (less database overhead)
541
+ 2. Simpler codebase (easier to maintain)
542
+ 3. Better error messages if updates fail
543
+ 4. Monitoring dashboard for timesheet accuracy
544
+
545
+ ---
546
+
547
+ ## Timeline
548
+
549
+ **Total Estimated Time:** 4-6 hours (1 developer)
550
+
551
+ | Phase | Duration | Dependencies |
552
+ |-------|----------|--------------|
553
+ | Planning & Review | 1 hour | This document |
554
+ | Code Removal | 1 hour | None |
555
+ | New Service Implementation | 2 hours | Code removal |
556
+ | Integration & Testing | 1-2 hours | New service |
557
+ | Database Migration | 30 min | Testing complete |
558
+ | Deployment & Monitoring | 30 min | All above |
559
+
560
+ **Recommended Schedule:**
561
+ - **Day 1 Morning:** Planning, code removal, start new service
562
+ - **Day 1 Afternoon:** Finish new service, write tests
563
+ - **Day 2 Morning:** Integration, testing, migration
564
+ - **Day 2 Afternoon:** Deploy to staging, final testing
565
+ - **Day 3 Morning:** Deploy to production, monitor
566
+
567
+ ---
568
+
569
+ ## Questions to Answer Before Starting
570
+
571
+ 1. **Are there any reports that query `reconciliation_runs` table?**
572
+ - Search codebase for table references
573
+ - Check analytics/dashboard queries
574
+
575
+ 2. **Does the frontend use `/reconciliation` API endpoints?**
576
+ - Search frontend code for `/reconciliation`
577
+ - Check network tab in browser dev tools
578
+
579
+ 3. **Do we need to preserve reconciliation historical data?**
580
+ - Export to CSV before dropping tables?
581
+ - Or just drop and move on?
582
+
583
+ 4. **Are timesheets used for payroll calculations?**
584
+ - If yes, ensure accuracy is critical
585
+ - Add extra validation/testing
586
+
587
+ 5. **Do we want to add any new features while refactoring?**
588
+ - Better error handling?
589
+ - Monitoring/alerting?
590
+ - Performance metrics?
591
+
592
+ ---
593
+
594
+ ## Next Steps
595
+
596
+ 1. **Review this plan** - Team discussion, get buy-in
597
+ 2. **Answer questions above** - Clarify requirements
598
+ 3. **Set timeline** - When to start, who's doing it
599
+ 4. **Create tasks** - Break into smaller tickets if needed
600
+ 5. **Start implementation** - Follow checklist above
601
+
602
+ ---
603
+
604
+ ## Notes & Considerations
605
+
606
+ ### Why This Approach?
607
+ - **Pragmatic:** Fix what's broken, keep what works
608
+ - **Low risk:** Incremental changes, easy to rollback
609
+ - **Maintainable:** Simpler code = fewer bugs
610
+ - **Accurate:** Real-time updates are more accurate than batch
611
+
612
+ ### Alternative Approaches Considered
613
+
614
+ **Option 1: Fix the reconciliation service**
615
+ - ❌ More complex
616
+ - ❌ Still have redundant scheduled job
617
+ - ❌ Doesn't solve root issue (unnecessary complexity)
618
+
619
+ **Option 2: Remove all timesheet updates**
620
+ - ❌ Lose valuable reporting data
621
+ - ❌ Payroll calculations may break
622
+ - ❌ Can't track agent performance
623
+
624
+ **Option 3: Keep both real-time and scheduled**
625
+ - ❌ Redundant
626
+ - ❌ More maintenance burden
627
+ - ❌ Scheduled job still broken
628
+
629
+ **Selected: Remove scheduled, enhance real-time** ✅
630
+ - ✅ Simplest solution
631
+ - ✅ Keeps what works
632
+ - ✅ Removes what's broken
633
+ - ✅ Maintains functionality
634
+
635
+ ---
636
+
637
+ ## References
638
+
639
+ **Related Files:**
640
+ - Current implementation: `src/app/services/reconciliation/reconciliation_service.py`
641
+ - Scheduler: `src/app/tasks/scheduler.py`
642
+ - Migrations: `supabase/migrations/20241209_add_reconciliation_system.sql`
643
+ - Error log: `docs/devlogs/server/runtimeerror.txt`
644
+
645
+ **Related Documentation:**
646
+ - Timesheet model: `src/app/models/timesheet.py`
647
+ - Ticket assignments: `src/app/api/v1/ticket_assignments.py`
648
+ - Ticket expenses: `src/app/api/v1/ticket_expenses.py`
649
+
650
+ ---
651
+
652
+ **Document Version:** 1.0
653
+ **Last Updated:** 2025-12-11
654
+ **Author:** System Architect
655
+ **Status:** Ready for Review
docs/features/reconciliation-system-analysis.md ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Reconciliation System Analysis
2
+
3
+ **Status:** For Future Reference
4
+ **Created:** 2025-12-12
5
+ **Purpose:** Document current reconciliation system before removal for MVP
6
+
7
+ ---
8
+
9
+ ## Executive Summary
10
+
11
+ The reconciliation system has **two components**:
12
+ 1. **Real-time updates** (✅ Working) - Updates timesheets immediately when events occur
13
+ 2. **Scheduled validation** (❌ Broken) - Nightly batch job at 10 PM that fails with PostgreSQL errors
14
+
15
+ **Decision:** Remove scheduled job for MVP, keep real-time updates. May reintroduce background jobs post-MVP.
16
+
17
+ ---
18
+
19
+ ## System Architecture
20
+
21
+ ### Components
22
+
23
+ **1. Reconciliation Service** (`src/app/services/reconciliation/`)
24
+ - `reconciliation_service.py` (~500 lines) - Main service with batch + real-time logic
25
+ - `anomaly_detector.py` (~150 lines) - Detects suspicious patterns
26
+ - `models.py` (~65 lines) - Pydantic models
27
+ - `__init__.py` - Package exports
28
+
29
+ **2. Scheduled Job** (`src/app/tasks/scheduler.py`)
30
+ - Runs daily at 10 PM (Africa/Nairobi timezone)
31
+ - Calls `ReconciliationService.reconcile_project_day()` for all active projects
32
+ - Initialized in `main.py` startup event
33
+
34
+ **3. API Endpoints** (`src/app/api/endpoints/reconciliation.py`)
35
+ - `POST /reconciliation/run` - Manual trigger
36
+ - `GET /reconciliation/status/{run_id}` - Check run status
37
+ - `GET /reconciliation/report/{project_id}` - Daily report
38
+ - Registered in `src/app/api/v1/router.py`
39
+
40
+ **4. Database Tables**
41
+ - `reconciliation_runs` - Audit trail of batch jobs (JSONB columns cause errors)
42
+ - `timesheet_updates` - Audit trail of real-time updates
43
+ - `timesheets` - Daily agent activity snapshots (has reconciliation columns)
44
+
45
+ **5. Integration Points** (Real-time updates)
46
+ - `src/app/api/v1/ticket_assignments.py` - 3 locations (assign, self-assign, complete)
47
+ - `src/app/api/v1/ticket_completion.py` - 1 location (ticket completed)
48
+ - `src/app/api/v1/ticket_expenses.py` - 3 locations (create, approve, bulk approve)
49
+
50
+ ---
51
+
52
+ ## What Works ✅
53
+
54
+ ### Real-Time Timesheet Updates
55
+ - **Trigger:** Events fire immediately (assignment created, ticket completed, expense approved)
56
+ - **Method:** `ReconciliationService.update_user_timesheet_realtime()`
57
+ - **Behavior:**
58
+ - Aggregates current counts from source tables
59
+ - Upserts timesheet with fresh data
60
+ - Wrapped in try/except - failures don't block main operations
61
+ - **Performance:** Fast, happens inline with user actions
62
+ - **Reliability:** Works well, no reported issues
63
+
64
+ ### Data Aggregation Logic
65
+ - Single efficient SQL query aggregates all agent activity
66
+ - Handles multiple assignments per ticket correctly
67
+ - Properly attributes work to correct dates
68
+ - Uses proper indexes for performance
69
+
70
+ ---
71
+
72
+ ## What's Broken ❌
73
+
74
+ ### Scheduled Validation Job (10 PM Daily)
75
+ **Error:** `psycopg2.ProgrammingError: can't adapt type 'dict'`
76
+
77
+ **Root Cause:**
78
+ - Line 482 in `reconciliation_service.py` - `_complete_run()` method
79
+ - Tries to save Python dict to JSONB column without proper serialization
80
+ - Parameters `summary_stats` and `anomalies` are dicts, not JSON strings
81
+
82
+ **Impact:**
83
+ - Job fails every night at 10 PM
84
+ - Creates failed `reconciliation_runs` records
85
+ - Logs filled with error traces
86
+ - No actual functionality loss (real-time updates still work)
87
+
88
+ **Why It Exists:**
89
+ - Safety net to validate real-time updates
90
+ - Find orphaned records (activity without timesheet)
91
+ - Detect discrepancies
92
+ - Anomaly detection
93
+
94
+ **Why It's Unnecessary:**
95
+ - Real-time updates already work (99% coverage)
96
+ - No monitoring of anomaly detection results
97
+ - Audit tables (`reconciliation_runs`, `timesheet_updates`) not used for business logic
98
+ - Adds complexity without value
99
+
100
+ ---
101
+
102
+ ## Good Design Decisions 👍
103
+
104
+ 1. **Separation of Concerns**
105
+ - Real-time updates separate from batch validation
106
+ - Service layer properly encapsulates business logic
107
+ - Clear integration points in API endpoints
108
+
109
+ 2. **Fail-Safe Real-Time Updates**
110
+ - Wrapped in try/except blocks
111
+ - Don't block user operations if timesheet update fails
112
+ - Logged for debugging
113
+
114
+ 3. **Idempotent Batch Processing**
115
+ - Safe to re-run multiple times
116
+ - Uses upsert (INSERT ... ON CONFLICT)
117
+ - Prevents concurrent runs with unique index
118
+
119
+ 4. **Comprehensive Aggregation**
120
+ - Single query for all metrics (efficient)
121
+ - Handles edge cases (multiple assignments, cross-day activities)
122
+ - Proper date attribution
123
+
124
+ 5. **Audit Trail**
125
+ - Tracks all reconciliation runs
126
+ - Records what changed and why
127
+ - Useful for debugging (if anyone looked at it)
128
+
129
+ ---
130
+
131
+ ## Bad Design Decisions 👎
132
+
133
+ 1. **Over-Engineering**
134
+ - 1000+ lines of code for a "safety net" that's not monitored
135
+ - Anomaly detection that nobody reviews
136
+ - Audit tables that nobody queries
137
+ - Scheduled job that duplicates real-time work
138
+
139
+ 2. **JSONB Serialization Bug**
140
+ - Passing Python dicts directly to SQL without json.dumps()
141
+ - Should have been caught in testing
142
+ - Indicates lack of integration tests for scheduled job
143
+
144
+ 3. **Tight Coupling**
145
+ - ReconciliationService handles both real-time AND batch
146
+ - Should be two separate services
147
+ - Hard to remove one without affecting the other
148
+
149
+ 4. **No Monitoring**
150
+ - Anomaly detection runs but results ignored
151
+ - Failed reconciliation runs logged but not alerted
152
+ - No dashboard showing reconciliation health
153
+
154
+ 5. **Redundant Validation**
155
+ - Scheduled job re-validates what real-time already did
156
+ - If real-time is accurate, scheduled job is waste
157
+ - If real-time is broken, scheduled job won't fix it
158
+
159
+ ---
160
+
161
+ ## Integration Points Summary
162
+
163
+ ### Files That Import ReconciliationService
164
+
165
+ 1. **src/app/api/v1/ticket_assignments.py**
166
+ - Line 23: Import
167
+ - Line 105: Assignment created
168
+ - Line 293: Self-assignment
169
+ - Line 598: Assignment completed
170
+
171
+ 2. **src/app/api/v1/ticket_expenses.py**
172
+ - Line 29: Import
173
+ - Line 197: Expense created
174
+ - Line 320: Expense approved/rejected
175
+ - Line 914: Bulk expense approval
176
+
177
+ 3. **src/app/api/v1/ticket_completion.py**
178
+ - Line 282: Import
179
+ - Line 286: Ticket completed
180
+
181
+ 4. **src/app/tasks/scheduler.py**
182
+ - Line 101: Import
183
+ - Line 128: Scheduled validation
184
+
185
+ 5. **src/app/api/endpoints/reconciliation.py**
186
+ - Line 16: Import
187
+ - Line 56: Manual reconciliation trigger
188
+
189
+ ### Database Dependencies
190
+
191
+ **Tables:**
192
+ - `reconciliation_runs` - Can be dropped
193
+ - `timesheet_updates` - Can be dropped
194
+ - `timesheets` - Keep (core data), remove reconciliation columns
195
+
196
+ **Columns to Remove from Timesheets:**
197
+ - `reconciliation_run_id`
198
+ - `last_reconciled_at`
199
+ - `update_source`
200
+ - `last_realtime_update_at`
201
+ - `last_validated_at`
202
+ - `needs_review`
203
+ - `discrepancy_notes`
204
+ - `version`
205
+
206
+ **Columns to Keep in Timesheets:**
207
+ - All ticket metrics (tickets_assigned, tickets_completed, etc.)
208
+ - All expense metrics (total_expenses, approved_expenses, etc.)
209
+ - Time tracking (check_in_time, check_out_time, hours_worked)
210
+ - Payroll linkage (is_payroll_generated, payroll_id)
211
+
212
+ ---
213
+
214
+ ## Lessons for Future Background Jobs
215
+
216
+ ### Do's ✅
217
+ - Keep background jobs simple and focused
218
+ - Monitor job execution and alert on failures
219
+ - Make jobs idempotent (safe to retry)
220
+ - Use proper serialization for JSONB columns (json.dumps())
221
+ - Write integration tests for scheduled jobs
222
+ - Separate real-time and batch logic into different services
223
+
224
+ ### Don'ts ❌
225
+ - Don't create audit trails nobody uses
226
+ - Don't add "safety nets" without monitoring them
227
+ - Don't duplicate work already done in real-time
228
+ - Don't tightly couple real-time and batch processing
229
+ - Don't add complexity without clear business value
230
+ - Don't skip testing scheduled jobs
231
+
232
+ ---
233
+
234
+ ## Removal Impact Assessment
235
+
236
+ ### Low Risk ✅
237
+ - Removing scheduled job (it's broken anyway)
238
+ - Dropping audit tables (not used for business logic)
239
+ - Removing reconciliation API endpoints (likely unused by frontend)
240
+
241
+ ### Medium Risk ⚠️
242
+ - Removing real-time update calls (need to replace with simpler logic)
243
+ - Database migration to drop tables/columns
244
+
245
+ ### High Risk 🚨
246
+ - None - real-time updates can be replaced with simpler direct timesheet updates
247
+
248
+ ---
249
+
250
+ ## Recommendation
251
+
252
+ **For MVP:** Remove entire reconciliation system, replace with simple direct timesheet updates.
253
+
254
+ **Post-MVP:** If background jobs needed, design them properly:
255
+ - Separate service for batch processing
256
+ - Proper monitoring and alerting
257
+ - Clear business value (not just "safety net")
258
+ - Integration tests before deployment
259
+ - Simple, focused functionality
260
+
261
+ ---
262
+
263
+ **Document Version:** 1.0
264
+ **Last Updated:** 2025-12-12
265
+ **Status:** Reference for future development
266
+
docs/features/{bulk-invitations.md → users/bulk-invitations.md} RENAMED
File without changes
src/app/api/endpoints/reconciliation.py DELETED
@@ -1,320 +0,0 @@
1
- """
2
- Reconciliation API Endpoints
3
-
4
- Provides manual control over reconciliation process.
5
- """
6
-
7
- from fastapi import APIRouter, Depends, HTTPException, Query, status
8
- from sqlalchemy.orm import Session
9
- from sqlalchemy import text
10
- from datetime import date, timedelta
11
- from typing import Optional, List
12
- from uuid import UUID
13
-
14
- from app.api.deps import get_db, get_current_active_user
15
- from app.models.user import User
16
- from app.services.reconciliation import (
17
- ReconciliationService,
18
- ReconciliationError,
19
- ConcurrentRunError
20
- )
21
- from app.services.reconciliation.models import (
22
- ReconciliationRunCreate,
23
- ReconciliationRunResponse,
24
- DailyReportResponse
25
- )
26
-
27
- router = APIRouter(prefix="/reconciliation", tags=["reconciliation"])
28
-
29
-
30
- @router.post("/run", status_code=status.HTTP_200_OK)
31
- async def trigger_reconciliation(
32
- project_id: UUID,
33
- target_date: Optional[date] = None,
34
- user_ids: Optional[List[UUID]] = None,
35
- db: Session = Depends(get_db),
36
- current_user: User = Depends(get_current_active_user)
37
- ):
38
- """
39
- Manually trigger reconciliation for a project/date.
40
-
41
- - **project_id**: Project to reconcile
42
- - **target_date**: Date to reconcile (defaults to yesterday)
43
- - **user_ids**: Optional list of specific users (for partial reconciliation)
44
-
45
- Returns reconciliation run ID.
46
- """
47
- # Default to yesterday if not specified
48
- if target_date is None:
49
- target_date = date.today() - timedelta(days=1)
50
-
51
- # Validate permissions (user must be PM, dispatcher, or admin)
52
- if current_user.role not in ["project_manager", "dispatcher", "platform_admin"]:
53
- raise HTTPException(status_code=403, detail="Insufficient permissions")
54
-
55
- try:
56
- service = ReconciliationService(db)
57
- run_id = service.reconcile_project_day(
58
- project_id=project_id,
59
- target_date=target_date,
60
- user_ids=user_ids,
61
- triggered_by=current_user.id,
62
- run_type="manual" if not user_ids else "partial"
63
- )
64
-
65
- return {
66
- "status": "success",
67
- "run_id": str(run_id),
68
- "message": f"Reconciliation started for {target_date}",
69
- "project_id": str(project_id),
70
- "target_date": str(target_date)
71
- }
72
-
73
- except ConcurrentRunError as e:
74
- raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
75
- except ReconciliationError as e:
76
- raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
77
-
78
-
79
- @router.get("/status/{run_id}", response_model=ReconciliationRunResponse)
80
- async def get_reconciliation_status(
81
- run_id: UUID,
82
- db: Session = Depends(get_db),
83
- current_user: User = Depends(get_current_active_user)
84
- ):
85
- """
86
- Get status of a reconciliation run.
87
-
88
- Returns run details including progress, results, and any errors.
89
- """
90
-
91
- query = text("""
92
- SELECT
93
- id, project_id, reconciliation_date, run_type,
94
- started_at, completed_at, status,
95
- agents_processed, timesheets_created, timesheets_updated,
96
- execution_time_ms, summary_stats, anomalies_detected,
97
- error_message
98
- FROM reconciliation_runs
99
- WHERE id = :run_id
100
- AND deleted_at IS NULL
101
- """)
102
-
103
- result = db.execute(query, {"run_id": str(run_id)})
104
- run = result.fetchone()
105
-
106
- if not run:
107
- raise HTTPException(
108
- status_code=status.HTTP_404_NOT_FOUND,
109
- detail="Reconciliation run not found"
110
- )
111
-
112
- return ReconciliationRunResponse(
113
- id=run.id,
114
- project_id=run.project_id,
115
- reconciliation_date=run.reconciliation_date,
116
- run_type=run.run_type,
117
- status=run.status,
118
- started_at=run.started_at,
119
- completed_at=run.completed_at,
120
- agents_processed=run.agents_processed,
121
- timesheets_created=run.timesheets_created,
122
- timesheets_updated=run.timesheets_updated,
123
- execution_time_ms=run.execution_time_ms,
124
- summary_stats=run.summary_stats,
125
- anomalies_detected=run.anomalies_detected,
126
- error_message=run.error_message
127
- )
128
-
129
-
130
- @router.get("/report/{project_id}")
131
- async def get_daily_report(
132
- project_id: UUID,
133
- target_date: Optional[date] = Query(default=None),
134
- db: Session = Depends(get_db),
135
- current_user: User = Depends(get_current_active_user)
136
- ):
137
- """
138
- Get daily reconciliation report for a project.
139
-
140
- Returns summary statistics and agent-level details.
141
- """
142
- if target_date is None:
143
- target_date = date.today() - timedelta(days=1)
144
-
145
- # Get latest reconciliation run for this project/date
146
- run_query = text("""
147
- SELECT
148
- id, status, completed_at, summary_stats, anomalies_detected
149
- FROM reconciliation_runs
150
- WHERE project_id = :project_id
151
- AND reconciliation_date = :target_date
152
- AND deleted_at IS NULL
153
- ORDER BY started_at DESC
154
- LIMIT 1
155
- """)
156
-
157
- result = db.execute(run_query, {
158
- "project_id": str(project_id),
159
- "target_date": target_date
160
- })
161
- run = result.fetchone()
162
-
163
- if not run:
164
- raise HTTPException(
165
- status_code=status.HTTP_404_NOT_FOUND,
166
- detail=f"No reconciliation found for {target_date}"
167
- )
168
-
169
- # Get agent-level details from timesheets
170
- timesheets_query = text("""
171
- SELECT
172
- u.id as user_id,
173
- u.name as user_name,
174
- u.role as user_role,
175
- t.tickets_assigned,
176
- t.tickets_completed,
177
- t.tickets_rejected,
178
- t.tickets_cancelled,
179
- t.total_expenses,
180
- t.approved_expenses,
181
- t.pending_expenses
182
- FROM timesheets t
183
- JOIN users u ON t.user_id = u.id
184
- WHERE t.project_id = :project_id
185
- AND t.work_date = :target_date
186
- AND t.reconciliation_run_id = :run_id
187
- AND t.deleted_at IS NULL
188
- ORDER BY t.tickets_completed DESC
189
- """)
190
-
191
- result = db.execute(timesheets_query, {
192
- "project_id": str(project_id),
193
- "target_date": target_date,
194
- "run_id": str(run.id)
195
- })
196
- agents = [dict(row._mapping) for row in result.fetchall()]
197
-
198
- return {
199
- "project_id": str(project_id),
200
- "date": str(target_date),
201
- "reconciliation_status": run.status,
202
- "reconciled_at": run.completed_at,
203
- "summary": run.summary_stats or {},
204
- "anomalies": run.anomalies_detected or [],
205
- "agents": agents
206
- }
207
-
208
-
209
- @router.get("/history/{project_id}")
210
- async def get_reconciliation_history(
211
- project_id: UUID,
212
- limit: int = Query(default=30, le=90),
213
- db: Session = Depends(get_db),
214
- current_user: User = Depends(get_current_active_user)
215
- ):
216
- """
217
- Get reconciliation history for a project.
218
-
219
- Returns list of recent reconciliation runs.
220
- """
221
-
222
- query = text("""
223
- SELECT
224
- id, reconciliation_date, run_type, status,
225
- started_at, completed_at, execution_time_ms,
226
- agents_processed, timesheets_created, timesheets_updated
227
- FROM reconciliation_runs
228
- WHERE project_id = :project_id
229
- AND deleted_at IS NULL
230
- ORDER BY reconciliation_date DESC, started_at DESC
231
- LIMIT :limit
232
- """)
233
-
234
- result = db.execute(query, {
235
- "project_id": str(project_id),
236
- "limit": limit
237
- })
238
-
239
- runs = [dict(row._mapping) for row in result.fetchall()]
240
-
241
- return {
242
- "project_id": str(project_id),
243
- "total_runs": len(runs),
244
- "runs": runs
245
- }
246
-
247
-
248
- @router.get("/anomalies/{project_id}")
249
- async def get_project_anomalies(
250
- project_id: UUID,
251
- start_date: Optional[date] = Query(default=None),
252
- end_date: Optional[date] = Query(default=None),
253
- severity: Optional[str] = Query(default=None, regex="^(info|low|medium|high|critical)$"),
254
- db: Session = Depends(get_db),
255
- current_user: User = Depends(get_current_active_user)
256
- ):
257
- """
258
- Get anomalies detected for a project within a date range.
259
-
260
- Useful for identifying patterns and trends in agent behavior.
261
- """
262
- if start_date is None:
263
- start_date = date.today() - timedelta(days=30)
264
- if end_date is None:
265
- end_date = date.today()
266
-
267
- query = text("""
268
- SELECT
269
- reconciliation_date,
270
- anomalies_detected,
271
- summary_stats
272
- FROM reconciliation_runs
273
- WHERE project_id = :project_id
274
- AND reconciliation_date BETWEEN :start_date AND :end_date
275
- AND status = 'completed'
276
- AND deleted_at IS NULL
277
- ORDER BY reconciliation_date DESC
278
- """)
279
-
280
- result = db.execute(query, {
281
- "project_id": str(project_id),
282
- "start_date": start_date,
283
- "end_date": end_date
284
- })
285
-
286
- runs = result.fetchall()
287
-
288
- # Aggregate anomalies
289
- all_anomalies = []
290
- for run in runs:
291
- if run.anomalies_detected:
292
- for anomaly in run.anomalies_detected:
293
- anomaly["date"] = str(run.reconciliation_date)
294
- if severity is None or anomaly.get("severity") == severity:
295
- all_anomalies.append(anomaly)
296
-
297
- # Group by type
298
- anomaly_summary = {}
299
- for anomaly in all_anomalies:
300
- anom_type = anomaly.get("type", "unknown")
301
- if anom_type not in anomaly_summary:
302
- anomaly_summary[anom_type] = {
303
- "count": 0,
304
- "severity_distribution": {}
305
- }
306
- anomaly_summary[anom_type]["count"] += 1
307
- sev = anomaly.get("severity", "unknown")
308
- anomaly_summary[anom_type]["severity_distribution"][sev] = \
309
- anomaly_summary[anom_type]["severity_distribution"].get(sev, 0) + 1
310
-
311
- return {
312
- "project_id": str(project_id),
313
- "date_range": {
314
- "start": str(start_date),
315
- "end": str(end_date)
316
- },
317
- "total_anomalies": len(all_anomalies),
318
- "anomaly_summary": anomaly_summary,
319
- "anomalies": all_anomalies[:100] # Limit to 100 for performance
320
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/api/v1/router.py CHANGED
@@ -9,7 +9,6 @@ from app.api.v1 import (
9
  ticket_assignments, ticket_completion, ticket_comments, ticket_expenses, incidents, contractor_invoices, notifications, map, export, public_tracking, public_hub_tracking,
10
  audit_logs, analytics, progress_reports, incident_reports, locations, invoice_generation, invoice_viewing
11
  )
12
- from app.api.endpoints import reconciliation
13
 
14
  api_router = APIRouter()
15
 
@@ -125,6 +124,3 @@ api_router.include_router(analytics.router, prefix="/analytics", tags=["Analytic
125
 
126
  # Locations (Geographic Reference Data - Public, No Auth Required)
127
  api_router.include_router(locations.router)
128
-
129
- # Reconciliation (Daily Agent Activity Aggregation - PM/Dispatcher/Admin Only)
130
- api_router.include_router(reconciliation.router, prefix="/reconciliation", tags=["Reconciliation"])
 
9
  ticket_assignments, ticket_completion, ticket_comments, ticket_expenses, incidents, contractor_invoices, notifications, map, export, public_tracking, public_hub_tracking,
10
  audit_logs, analytics, progress_reports, incident_reports, locations, invoice_generation, invoice_viewing
11
  )
 
12
 
13
  api_router = APIRouter()
14
 
 
124
 
125
  # Locations (Geographic Reference Data - Public, No Auth Required)
126
  api_router.include_router(locations.router)
 
 
 
src/app/api/v1/ticket_assignments.py CHANGED
@@ -20,7 +20,6 @@ from app.api.deps import get_db, get_current_user
20
  from app.models.user import User
21
  from app.models.enums import AppRole
22
  from app.services.ticket_assignment_service import TicketAssignmentService
23
- from app.services.reconciliation.reconciliation_service import ReconciliationService
24
  from app.schemas.ticket_assignment import (
25
  TicketAssignCreate,
26
  TicketAssignTeamCreate,
@@ -98,23 +97,7 @@ def assign_ticket(
98
  require_dispatcher_or_pm(current_user)
99
  service = TicketAssignmentService(db)
100
  assignment = service.assign_ticket(ticket_id, data.user_id, current_user.id, data)
101
-
102
- # Real-time reconciliation update
103
- try:
104
- from datetime import date
105
- reconciliation_service = ReconciliationService(db)
106
- reconciliation_service.update_user_timesheet_realtime(
107
- user_id=data.user_id,
108
- project_id=assignment.ticket.project_id,
109
- work_date=date.today(),
110
- trigger_type='assignment_created',
111
- trigger_entity_type='ticket_assignment',
112
- trigger_entity_id=assignment.id
113
- )
114
- except Exception as e:
115
- # Don't fail the request if reconciliation fails
116
- logger.warning(f"Real-time reconciliation failed: {str(e)}")
117
-
118
  return assignment
119
 
120
 
@@ -286,22 +269,7 @@ def self_assign_ticket(
286
  ):
287
  service = TicketAssignmentService(db)
288
  assignment = service.self_assign_ticket(ticket_id, current_user.id, data)
289
-
290
- # Real-time reconciliation update
291
- try:
292
- from datetime import date
293
- reconciliation_service = ReconciliationService(db)
294
- reconciliation_service.update_user_timesheet_realtime(
295
- user_id=current_user.id,
296
- project_id=assignment.ticket.project_id,
297
- work_date=date.today(),
298
- trigger_type='self_assigned',
299
- trigger_entity_type='ticket_assignment',
300
- trigger_entity_id=assignment.id
301
- )
302
- except Exception as e:
303
- logger.warning(f"Real-time reconciliation failed: {str(e)}")
304
-
305
  return assignment
306
 
307
 
@@ -591,22 +559,7 @@ def complete_assignment(
591
  ):
592
  service = TicketAssignmentService(db)
593
  assignment = service.complete_assignment(assignment_id, current_user.id, data)
594
-
595
- # Real-time reconciliation update
596
- try:
597
- from datetime import date
598
- reconciliation_service = ReconciliationService(db)
599
- reconciliation_service.update_user_timesheet_realtime(
600
- user_id=current_user.id,
601
- project_id=assignment.ticket.project_id,
602
- work_date=date.today(),
603
- trigger_type='assignment_completed',
604
- trigger_entity_type='ticket_assignment',
605
- trigger_entity_id=assignment.id
606
- )
607
- except Exception as e:
608
- logger.warning(f"Real-time reconciliation failed: {str(e)}")
609
-
610
  return assignment
611
 
612
 
 
20
  from app.models.user import User
21
  from app.models.enums import AppRole
22
  from app.services.ticket_assignment_service import TicketAssignmentService
 
23
  from app.schemas.ticket_assignment import (
24
  TicketAssignCreate,
25
  TicketAssignTeamCreate,
 
97
  require_dispatcher_or_pm(current_user)
98
  service = TicketAssignmentService(db)
99
  assignment = service.assign_ticket(ticket_id, data.user_id, current_user.id, data)
100
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  return assignment
102
 
103
 
 
269
  ):
270
  service = TicketAssignmentService(db)
271
  assignment = service.self_assign_ticket(ticket_id, current_user.id, data)
272
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  return assignment
274
 
275
 
 
559
  ):
560
  service = TicketAssignmentService(db)
561
  assignment = service.complete_assignment(assignment_id, current_user.id, data)
562
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  return assignment
564
 
565
 
src/app/api/v1/ticket_completion.py CHANGED
@@ -275,24 +275,5 @@ def complete_ticket(
275
  location_accuracy=request.location_accuracy,
276
  db=db
277
  )
278
-
279
- # Real-time reconciliation update
280
- try:
281
- from datetime import date
282
- from app.services.reconciliation.reconciliation_service import ReconciliationService
283
- import logging
284
-
285
- logger = logging.getLogger(__name__)
286
- reconciliation_service = ReconciliationService(db)
287
- reconciliation_service.update_user_timesheet_realtime(
288
- user_id=current_user.id,
289
- project_id=ticket.project_id,
290
- work_date=date.today(),
291
- trigger_type='ticket_completed',
292
- trigger_entity_type='ticket',
293
- trigger_entity_id=ticket.id
294
- )
295
- except Exception as e:
296
- logger.warning(f"Real-time reconciliation failed: {str(e)}")
297
-
298
  return result
 
275
  location_accuracy=request.location_accuracy,
276
  db=db
277
  )
278
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  return result
src/app/api/v1/ticket_expenses.py CHANGED
@@ -26,7 +26,6 @@ from app.api.deps import get_db, get_current_user
26
  from app.models.user import User
27
  from app.models.enums import AppRole
28
  from app.services.ticket_expense_service import TicketExpenseService
29
- from app.services.reconciliation.reconciliation_service import ReconciliationService
30
  from app.models.ticket_expense import TicketExpense
31
  from app.models.ticket import Ticket
32
  from app.schemas.ticket_expense import (
@@ -191,21 +190,7 @@ def create_expense(
191
  data=data,
192
  current_user=current_user
193
  )
194
-
195
- # Real-time reconciliation update
196
- try:
197
- reconciliation_service = ReconciliationService(db)
198
- reconciliation_service.update_user_timesheet_realtime(
199
- user_id=expense.incurred_by_user_id,
200
- project_id=expense.ticket.project_id,
201
- work_date=expense.expense_date,
202
- trigger_type='expense_created',
203
- trigger_entity_type='ticket_expense',
204
- trigger_entity_id=expense.id
205
- )
206
- except Exception as e:
207
- logger.warning(f"Real-time reconciliation failed: {str(e)}")
208
-
209
  # Build response with user names
210
  response = TicketExpenseResponse.model_validate(expense)
211
  if expense.incurred_by_user:
@@ -314,22 +299,7 @@ def approve_expense(
314
  current_user=current_user,
315
  background_tasks=background_tasks
316
  )
317
-
318
- # Real-time reconciliation update
319
- try:
320
- reconciliation_service = ReconciliationService(db)
321
- trigger_type = 'expense_approved' if data.is_approved else 'expense_rejected'
322
- reconciliation_service.update_user_timesheet_realtime(
323
- user_id=expense.incurred_by_user_id,
324
- project_id=expense.ticket.project_id,
325
- work_date=expense.expense_date,
326
- trigger_type=trigger_type,
327
- trigger_entity_type='ticket_expense',
328
- trigger_entity_id=expense.id
329
- )
330
- except Exception as e:
331
- logger.warning(f"Real-time reconciliation failed: {str(e)}")
332
-
333
  response = TicketExpenseResponse.model_validate(expense)
334
  if expense.incurred_by_user:
335
  response.incurred_by_user_name = expense.incurred_by_user.name
@@ -909,23 +879,7 @@ def bulk_approve_expenses(
909
  current_user=current_user,
910
  background_tasks=background_tasks
911
  )
912
-
913
- # Real-time reconciliation update for each expense
914
- reconciliation_service = ReconciliationService(db)
915
- for expense in updated_expenses:
916
- try:
917
- trigger_type = 'expense_approved' if is_approved else 'expense_rejected'
918
- reconciliation_service.update_user_timesheet_realtime(
919
- user_id=expense.incurred_by_user_id,
920
- project_id=expense.ticket.project_id,
921
- work_date=expense.expense_date,
922
- trigger_type=trigger_type,
923
- trigger_entity_type='ticket_expense',
924
- trigger_entity_id=expense.id
925
- )
926
- except Exception as e:
927
- logger.error(f"Real-time reconciliation failed for expense {expense.id}: {str(e)}")
928
-
929
  # Build responses
930
  expense_responses = []
931
  for expense in updated_expenses:
 
26
  from app.models.user import User
27
  from app.models.enums import AppRole
28
  from app.services.ticket_expense_service import TicketExpenseService
 
29
  from app.models.ticket_expense import TicketExpense
30
  from app.models.ticket import Ticket
31
  from app.schemas.ticket_expense import (
 
190
  data=data,
191
  current_user=current_user
192
  )
193
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  # Build response with user names
195
  response = TicketExpenseResponse.model_validate(expense)
196
  if expense.incurred_by_user:
 
299
  current_user=current_user,
300
  background_tasks=background_tasks
301
  )
302
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  response = TicketExpenseResponse.model_validate(expense)
304
  if expense.incurred_by_user:
305
  response.incurred_by_user_name = expense.incurred_by_user.name
 
879
  current_user=current_user,
880
  background_tasks=background_tasks
881
  )
882
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
883
  # Build responses
884
  expense_responses = []
885
  for expense in updated_expenses:
src/app/api/v1/timesheets.py CHANGED
@@ -139,7 +139,7 @@ async def apply_for_leave(
139
  2. Timesheet created with status=on_leave/sick_leave
140
  3. leave_approved_by_user_id is NULL (pending approval)
141
  4. Manager approves via /approve-leave endpoint
142
- 5. Reconciliation job respects approved leave (won't mark as absent)
143
 
144
  **Response:**
145
  - Timesheet with requires_approval=True
 
139
  2. Timesheet created with status=on_leave/sick_leave
140
  3. leave_approved_by_user_id is NULL (pending approval)
141
  4. Manager approves via /approve-leave endpoint
142
+ 5. Approved leave is respected in attendance tracking
143
 
144
  **Response:**
145
  - Timesheet with requires_approval=True
src/app/main.py CHANGED
@@ -40,8 +40,7 @@ async def startup_event():
40
  """Initialize the application on startup"""
41
  from app.core.database import engine, SessionLocal
42
  from sqlalchemy import text
43
- from app.tasks import start_scheduler
44
-
45
  logger.info("=" * 60)
46
  logger.info(f"🚀 {settings.APP_NAME} v1.0.0 | {settings.ENVIRONMENT.upper()}")
47
  logger.info("📊 Dashboard: Enabled")
@@ -119,15 +118,7 @@ async def startup_event():
119
 
120
  except Exception as e:
121
  logger.warning(f" ⚠ External services check failed")
122
-
123
- # Start Reconciliation Scheduler
124
- logger.info("⏰ Scheduler:")
125
- try:
126
- start_scheduler()
127
- logger.info(" ✓ Daily reconciliation scheduler started (runs at midnight)")
128
- except Exception as e:
129
- logger.error(f" ✗ Scheduler failed to start: {str(e)}")
130
-
131
  logger.info("=" * 60)
132
  logger.info("✅ Startup complete | Ready to serve requests")
133
  logger.info("=" * 60)
@@ -136,15 +127,7 @@ async def startup_event():
136
  @app.on_event("shutdown")
137
  async def shutdown_event():
138
  """Cleanup on application shutdown"""
139
- from app.tasks import shutdown_scheduler
140
-
141
  logger.info("Shutting down application...")
142
- try:
143
- shutdown_scheduler()
144
- logger.info("✓ Scheduler stopped")
145
- except Exception as e:
146
- logger.error(f"Error stopping scheduler: {str(e)}")
147
-
148
  logger.info("Shutdown complete")
149
 
150
  # Mount static files
 
40
  """Initialize the application on startup"""
41
  from app.core.database import engine, SessionLocal
42
  from sqlalchemy import text
43
+
 
44
  logger.info("=" * 60)
45
  logger.info(f"🚀 {settings.APP_NAME} v1.0.0 | {settings.ENVIRONMENT.upper()}")
46
  logger.info("📊 Dashboard: Enabled")
 
118
 
119
  except Exception as e:
120
  logger.warning(f" ⚠ External services check failed")
121
+
 
 
 
 
 
 
 
 
122
  logger.info("=" * 60)
123
  logger.info("✅ Startup complete | Ready to serve requests")
124
  logger.info("=" * 60)
 
127
  @app.on_event("shutdown")
128
  async def shutdown_event():
129
  """Cleanup on application shutdown"""
 
 
130
  logger.info("Shutting down application...")
 
 
 
 
 
 
131
  logger.info("Shutdown complete")
132
 
133
  # Mount static files
src/app/services/reconciliation/__init__.py DELETED
@@ -1,15 +0,0 @@
1
- """
2
- Daily Reconciliation Service Package
3
-
4
- Handles aggregation of field agent activity into timesheet records.
5
- """
6
-
7
- from .reconciliation_service import ReconciliationService, ReconciliationError, ConcurrentRunError
8
- from .anomaly_detector import AnomalyDetector
9
-
10
- __all__ = [
11
- "ReconciliationService",
12
- "ReconciliationError",
13
- "ConcurrentRunError",
14
- "AnomalyDetector"
15
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/services/reconciliation/anomaly_detector.py DELETED
@@ -1,140 +0,0 @@
1
- """
2
- Anomaly Detection for Reconciliation
3
-
4
- Detects suspicious patterns in field agent activity.
5
- """
6
-
7
- from typing import List, Dict, Any
8
- from statistics import mean, stdev
9
- import logging
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- class AnomalyDetector:
15
- """Detects anomalies in agent activity data."""
16
-
17
- def detect(self, agent_stats: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
18
- """
19
- Detect anomalies across all agents.
20
-
21
- Args:
22
- agent_stats: List of agent statistics dictionaries
23
-
24
- Returns:
25
- List of anomaly records with:
26
- - type: Anomaly category
27
- - user_id: Affected agent
28
- - severity: 'low', 'medium', 'high', 'critical'
29
- - details: Human-readable description
30
- """
31
- if not agent_stats:
32
- return []
33
-
34
- anomalies = []
35
-
36
- try:
37
- # Calculate baseline statistics
38
- completed_counts = [s["tickets_completed"] for s in agent_stats]
39
- expense_totals = [float(s["total_expenses"]) for s in agent_stats]
40
-
41
- avg_completed = mean(completed_counts) if completed_counts else 0
42
- std_completed = stdev(completed_counts) if len(completed_counts) > 1 else 0
43
- avg_expenses = mean(expense_totals) if expense_totals else 0
44
- std_expenses = stdev(expense_totals) if len(expense_totals) > 1 else 0
45
-
46
- for stats in agent_stats:
47
- user_id = str(stats["user_id"])
48
-
49
- # Anomaly 1: Expenses without completed tickets
50
- if stats["total_expenses"] > 0 and stats["tickets_completed"] == 0:
51
- anomalies.append({
52
- "type": "expenses_without_completion",
53
- "user_id": user_id,
54
- "severity": "medium",
55
- "details": (
56
- f"Agent has {stats['expense_claims_count']} expense claims "
57
- f"totaling {stats['total_expenses']:.2f} but completed 0 tickets"
58
- )
59
- })
60
-
61
- # Anomaly 2: Unusually high productivity (>2 std deviations)
62
- if std_completed > 0 and avg_completed > 0:
63
- z_score = (stats["tickets_completed"] - avg_completed) / std_completed
64
- if z_score > 2:
65
- anomalies.append({
66
- "type": "high_productivity",
67
- "user_id": user_id,
68
- "severity": "info",
69
- "details": (
70
- f"Agent completed {stats['tickets_completed']} tickets "
71
- f"(avg: {avg_completed:.1f}, z-score: {z_score:.2f})"
72
- )
73
- })
74
-
75
- # Anomaly 3: High rejection rate (>50%)
76
- if stats["tickets_assigned"] > 0:
77
- rejection_rate = stats["tickets_rejected"] / stats["tickets_assigned"]
78
- if rejection_rate > 0.5:
79
- anomalies.append({
80
- "type": "high_rejection_rate",
81
- "user_id": user_id,
82
- "severity": "high",
83
- "details": (
84
- f"Agent rejected {stats['tickets_rejected']} of "
85
- f"{stats['tickets_assigned']} assigned tickets "
86
- f"({rejection_rate*100:.0f}%)"
87
- )
88
- })
89
-
90
- # Anomaly 4: Unusually high expenses (>3 std deviations)
91
- if std_expenses > 0 and avg_expenses > 0:
92
- z_score = (float(stats["total_expenses"]) - avg_expenses) / std_expenses
93
- if z_score > 3:
94
- anomalies.append({
95
- "type": "high_expenses",
96
- "user_id": user_id,
97
- "severity": "medium",
98
- "details": (
99
- f"Agent expenses {stats['total_expenses']:.2f} "
100
- f"(avg: {avg_expenses:.2f}, z-score: {z_score:.2f})"
101
- )
102
- })
103
-
104
- # Anomaly 5: Zero action taken (assigned but did nothing)
105
- if (stats["tickets_assigned"] > 0 and
106
- stats["tickets_completed"] == 0 and
107
- stats["tickets_rejected"] == 0 and
108
- stats["tickets_cancelled"] == 0):
109
- anomalies.append({
110
- "type": "no_action_taken",
111
- "user_id": user_id,
112
- "severity": "medium",
113
- "details": (
114
- f"Agent was assigned {stats['tickets_assigned']} tickets "
115
- f"but took no action (no completion, rejection, or cancellation)"
116
- )
117
- })
118
-
119
- # Anomaly 6: High cancellation rate (>30%)
120
- if stats["tickets_assigned"] > 0:
121
- cancellation_rate = stats["tickets_cancelled"] / stats["tickets_assigned"]
122
- if cancellation_rate > 0.3:
123
- anomalies.append({
124
- "type": "high_cancellation_rate",
125
- "user_id": user_id,
126
- "severity": "medium",
127
- "details": (
128
- f"Agent cancelled {stats['tickets_cancelled']} of "
129
- f"{stats['tickets_assigned']} assigned tickets "
130
- f"({cancellation_rate*100:.0f}%)"
131
- )
132
- })
133
-
134
- logger.info(f"Detected {len(anomalies)} anomalies across {len(agent_stats)} agents")
135
-
136
- except Exception as e:
137
- logger.error(f"Error detecting anomalies: {str(e)}", exc_info=True)
138
- # Don't fail reconciliation if anomaly detection fails
139
-
140
- return anomalies
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/services/reconciliation/models.py DELETED
@@ -1,64 +0,0 @@
1
- """
2
- Pydantic models for reconciliation service
3
- """
4
-
5
- from pydantic import BaseModel, Field
6
- from datetime import date, datetime
7
- from typing import Optional, List, Dict, Any
8
- from uuid import UUID
9
-
10
-
11
- class ReconciliationRunCreate(BaseModel):
12
- """Request model for creating a reconciliation run"""
13
- project_id: UUID
14
- target_date: date
15
- user_ids: Optional[List[UUID]] = None
16
- run_type: str = Field(default="manual", pattern="^(scheduled|manual|partial|historical)$")
17
-
18
-
19
- class ReconciliationRunResponse(BaseModel):
20
- """Response model for reconciliation run"""
21
- id: UUID
22
- project_id: UUID
23
- reconciliation_date: date
24
- run_type: str
25
- status: str
26
- started_at: datetime
27
- completed_at: Optional[datetime] = None
28
- agents_processed: int
29
- timesheets_created: int
30
- timesheets_updated: int
31
- execution_time_ms: Optional[int] = None
32
- summary_stats: Optional[Dict[str, Any]] = None
33
- anomalies_detected: Optional[List[Dict[str, Any]]] = None
34
- error_message: Optional[str] = None
35
-
36
- class Config:
37
- from_attributes = True
38
-
39
-
40
- class DailyReportResponse(BaseModel):
41
- """Response model for daily reconciliation report"""
42
- project_id: UUID
43
- date: date
44
- reconciliation_status: str
45
- reconciled_at: Optional[datetime] = None
46
- summary: Dict[str, Any]
47
- anomalies: List[Dict[str, Any]]
48
- agents: List[Dict[str, Any]]
49
-
50
-
51
- class AgentDayStats(BaseModel):
52
- """Statistics for a single agent for a single day"""
53
- user_id: UUID
54
- tickets_assigned: int = 0
55
- tickets_accepted: int = 0
56
- tickets_completed: int = 0
57
- tickets_rejected: int = 0
58
- tickets_cancelled: int = 0
59
- tickets_rescheduled: int = 0
60
- total_expenses: float = 0.0
61
- approved_expenses: float = 0.0
62
- pending_expenses: float = 0.0
63
- rejected_expenses: float = 0.0
64
- expense_claims_count: int = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/services/reconciliation/reconciliation_service.py DELETED
@@ -1,668 +0,0 @@
1
- """
2
- Daily Reconciliation Service
3
-
4
- Aggregates field agent activity into timesheet records.
5
- Designed for high performance with 500+ agents per project.
6
- """
7
-
8
- from datetime import date, datetime, timedelta
9
- from typing import Optional, List, Dict, Any
10
- from uuid import UUID
11
- import logging
12
- import json
13
- from sqlalchemy import text
14
- from sqlalchemy.orm import Session
15
-
16
- from .anomaly_detector import AnomalyDetector
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- class ReconciliationError(Exception):
22
- """Raised when reconciliation fails."""
23
- pass
24
-
25
-
26
- class ConcurrentRunError(Exception):
27
- """Raised when another reconciliation is already running."""
28
- pass
29
-
30
-
31
- class ReconciliationService:
32
- """
33
- Handles daily reconciliation of field agent activity.
34
-
35
- Key Features:
36
- - Idempotent: Safe to re-run multiple times
37
- - Transactional: All-or-nothing with automatic rollback
38
- - Efficient: One aggregation query per project
39
- - Observable: Complete audit trail and metrics
40
- """
41
-
42
- def __init__(self, db: Session):
43
- self.db = db
44
- self.anomaly_detector = AnomalyDetector()
45
-
46
- def reconcile_project_day(
47
- self,
48
- project_id: UUID,
49
- target_date: date,
50
- user_ids: Optional[List[UUID]] = None,
51
- triggered_by: Optional[UUID] = None,
52
- run_type: str = "scheduled"
53
- ) -> UUID:
54
- """
55
- Main entry point: Reconcile all field agent activity for a project/date.
56
-
57
- Args:
58
- project_id: Project to reconcile
59
- target_date: Date to reconcile (usually yesterday)
60
- user_ids: Optional list of specific users (for partial reconciliation)
61
- triggered_by: User who triggered (None for scheduled)
62
- run_type: 'scheduled', 'manual', 'partial', 'historical'
63
-
64
- Returns:
65
- UUID of reconciliation_run record
66
-
67
- Raises:
68
- ReconciliationError: If reconciliation fails
69
- ConcurrentRunError: If another run is active for same project/date
70
- """
71
- start_time = datetime.utcnow()
72
-
73
- logger.info(
74
- f"Starting reconciliation: project={project_id}, "
75
- f"date={target_date}, type={run_type}"
76
- )
77
-
78
- # Step 1: Create run record (also checks for concurrent runs)
79
- run_id = self._create_run(
80
- project_id=project_id,
81
- target_date=target_date,
82
- user_ids=user_ids,
83
- triggered_by=triggered_by,
84
- run_type=run_type
85
- )
86
-
87
- try:
88
- # Step 2: Execute reconciliation in transaction
89
-
90
- # 2a. Aggregate all agent activity (ONE QUERY)
91
- query_start = datetime.utcnow()
92
- agent_stats = self._aggregate_agent_activity(
93
- project_id=project_id,
94
- target_date=target_date,
95
- user_ids=user_ids
96
- )
97
- query_time = (datetime.utcnow() - query_start).total_seconds() * 1000
98
-
99
- logger.info(f"Aggregated {len(agent_stats)} agents in {query_time:.0f}ms")
100
-
101
- # 2b. Bulk upsert timesheets
102
- created, updated = self._bulk_upsert_timesheets(
103
- agent_stats=agent_stats,
104
- run_id=run_id,
105
- target_date=target_date,
106
- project_id=project_id
107
- )
108
-
109
- # 2c. Detect anomalies
110
- anomalies = self.anomaly_detector.detect(agent_stats)
111
-
112
- # 2d. Calculate summary stats
113
- summary = self._calculate_summary(agent_stats)
114
-
115
- # 2e. Mark run as complete
116
- execution_time = (datetime.utcnow() - start_time).total_seconds() * 1000
117
- self._complete_run(
118
- run_id=run_id,
119
- agents_processed=len(agent_stats),
120
- timesheets_created=created,
121
- timesheets_updated=updated,
122
- assignments_processed=sum(s["tickets_assigned"] for s in agent_stats),
123
- expenses_processed=sum(s["expense_claims_count"] for s in agent_stats),
124
- summary_stats=summary,
125
- anomalies=anomalies,
126
- execution_time_ms=int(execution_time),
127
- query_time_ms=int(query_time)
128
- )
129
-
130
- # Commit transaction
131
- self.db.commit()
132
-
133
- logger.info(
134
- f"Reconciliation completed: run_id={run_id}, "
135
- f"agents={len(agent_stats)}, time={execution_time:.0f}ms"
136
- )
137
-
138
- # Step 3: Send notifications (outside transaction)
139
- # TODO: Implement notification service integration
140
- # await self._send_reconciliation_report(...)
141
-
142
- return run_id
143
-
144
- except Exception as e:
145
- # Rollback happens automatically
146
- self.db.rollback()
147
- logger.error(f"Reconciliation failed: {str(e)}", exc_info=True)
148
- self._fail_run(run_id, str(e))
149
- raise ReconciliationError(f"Reconciliation failed: {str(e)}") from e
150
-
151
- def _create_run(
152
- self,
153
- project_id: UUID,
154
- target_date: date,
155
- user_ids: Optional[List[UUID]],
156
- triggered_by: Optional[UUID],
157
- run_type: str
158
- ) -> UUID:
159
- """Create reconciliation run record."""
160
-
161
- query = text("""
162
- INSERT INTO reconciliation_runs (
163
- project_id, reconciliation_date, run_type,
164
- user_ids, triggered_by_user_id, status
165
- ) VALUES (
166
- :project_id, :target_date, :run_type,
167
- :user_ids, :triggered_by, 'running'
168
- )
169
- RETURNING id
170
- """)
171
-
172
- try:
173
- result = self.db.execute(query, {
174
- "project_id": str(project_id),
175
- "target_date": target_date,
176
- "run_type": run_type,
177
- "user_ids": [str(uid) for uid in user_ids] if user_ids else None,
178
- "triggered_by": str(triggered_by) if triggered_by else None
179
- })
180
- run_id = result.scalar_one()
181
- self.db.commit()
182
- return run_id
183
-
184
- except Exception as e:
185
- if "unique_active_run" in str(e):
186
- raise ConcurrentRunError(
187
- f"Another reconciliation is already running for "
188
- f"project {project_id} on {target_date}"
189
- )
190
- raise
191
-
192
- def _aggregate_agent_activity(
193
- self,
194
- project_id: UUID,
195
- target_date: date,
196
- user_ids: Optional[List[UUID]] = None
197
- ) -> List[Dict[str, Any]]:
198
- """
199
- THE CRITICAL QUERY: Aggregate all agent activity in one efficient query.
200
-
201
- This query:
202
- - Counts assignments by action type (accepted, rejected, dropped, etc.)
203
- - Sums expenses by approval status (approved, pending, rejected)
204
- - Handles multiple assignments per ticket correctly
205
- - Properly attributes work to the correct date
206
- - Uses FULL OUTER JOIN to catch agents with only expenses or only assignments
207
-
208
- Performance: ~100-200ms for 500 agents with proper indexes
209
- """
210
-
211
- user_filter = ""
212
- expense_user_filter = ""
213
- if user_ids:
214
- # Build IN clause with individual parameters instead of array
215
- user_placeholders = ", ".join([f":user_id_{i}" for i in range(len(user_ids))])
216
- user_filter = f"AND ta.user_id IN ({user_placeholders})"
217
- expense_user_filter = f"AND te.incurred_by_user_id IN ({user_placeholders})"
218
-
219
- query = text(f"""
220
- WITH daily_assignments AS (
221
- -- All assignments for this project/date
222
- -- Include assignments that were assigned OR completed on this date
223
- SELECT
224
- ta.id as assignment_id,
225
- ta.user_id,
226
- ta.ticket_id,
227
- ta.action,
228
- ta.assigned_at,
229
- ta.ended_at,
230
- t.status as ticket_status,
231
- t.completed_at as ticket_completed_at
232
- FROM ticket_assignments ta
233
- JOIN tickets t ON ta.ticket_id = t.id
234
- WHERE t.project_id = :project_id
235
- AND t.deleted_at IS NULL
236
- AND ta.deleted_at IS NULL
237
- AND (
238
- DATE(ta.assigned_at) = :target_date
239
- OR DATE(ta.ended_at) = :target_date
240
- OR DATE(t.completed_at) = :target_date
241
- )
242
- {user_filter}
243
- ),
244
- daily_expenses AS (
245
- -- All expenses for this project/date (by expense_date, not created_at)
246
- SELECT
247
- te.ticket_assignment_id,
248
- te.incurred_by_user_id as user_id,
249
- te.total_cost,
250
- te.is_approved,
251
- te.rejection_reason,
252
- te.approved_at,
253
- CASE
254
- WHEN te.is_approved = TRUE THEN 'approved'
255
- WHEN te.is_approved = FALSE AND te.rejection_reason IS NOT NULL THEN 'rejected'
256
- WHEN te.is_approved = FALSE OR te.is_approved IS NULL THEN 'pending'
257
- ELSE 'pending'
258
- END as approval_status
259
- FROM ticket_expenses te
260
- JOIN ticket_assignments ta ON te.ticket_assignment_id = ta.id
261
- JOIN tickets t ON ta.ticket_id = t.id
262
- WHERE t.project_id = :project_id
263
- AND te.deleted_at IS NULL
264
- AND te.expense_date = :target_date
265
- {expense_user_filter}
266
- )
267
- SELECT
268
- COALESCE(da.user_id, de.user_id) as user_id,
269
-
270
- -- Check in/out times (first and last activity ON THIS DATE ONLY)
271
- MIN(da.assigned_at) FILTER (
272
- WHERE DATE(da.assigned_at) = :target_date
273
- ) as check_in_time,
274
- MAX(COALESCE(da.ended_at, da.assigned_at)) FILTER (
275
- WHERE DATE(COALESCE(da.ended_at, da.assigned_at)) = :target_date
276
- ) as check_out_time,
277
-
278
- -- Assignment counts (only assignments CREATED on this date)
279
- COUNT(DISTINCT da.assignment_id) FILTER (
280
- WHERE DATE(da.assigned_at) = :target_date
281
- ) as tickets_assigned,
282
-
283
- COUNT(DISTINCT da.assignment_id) FILTER (
284
- WHERE da.action = 'accepted'
285
- AND DATE(da.assigned_at) = :target_date
286
- ) as tickets_accepted,
287
-
288
- COUNT(DISTINCT da.ticket_id) FILTER (
289
- WHERE da.ticket_status = 'completed'
290
- AND DATE(da.ticket_completed_at) = :target_date
291
- ) as tickets_completed,
292
-
293
- COUNT(DISTINCT da.assignment_id) FILTER (
294
- WHERE da.action = 'rejected'
295
- AND DATE(da.assigned_at) = :target_date
296
- ) as tickets_rejected,
297
-
298
- COUNT(DISTINCT da.assignment_id) FILTER (
299
- WHERE da.action = 'dropped'
300
- AND DATE(da.ended_at) = :target_date
301
- ) as tickets_cancelled,
302
-
303
- COUNT(DISTINCT da.assignment_id) FILTER (
304
- WHERE da.action = 'reassigned'
305
- AND DATE(da.ended_at) = :target_date
306
- ) as tickets_rescheduled,
307
-
308
- -- Expense aggregations by approval status
309
- COALESCE(SUM(de.total_cost), 0) as total_expenses,
310
- COALESCE(SUM(de.total_cost) FILTER (
311
- WHERE de.approval_status = 'approved'
312
- ), 0) as approved_expenses,
313
- COALESCE(SUM(de.total_cost) FILTER (
314
- WHERE de.approval_status = 'pending'
315
- ), 0) as pending_expenses,
316
- COALESCE(SUM(de.total_cost) FILTER (
317
- WHERE de.approval_status = 'rejected'
318
- ), 0) as rejected_expenses,
319
- COUNT(DISTINCT de.ticket_assignment_id) as expense_claims_count
320
-
321
- FROM daily_assignments da
322
- FULL OUTER JOIN daily_expenses de ON da.user_id = de.user_id
323
- GROUP BY COALESCE(da.user_id, de.user_id)
324
- HAVING COUNT(DISTINCT da.assignment_id) > 0
325
- OR COUNT(DISTINCT de.ticket_assignment_id) > 0
326
- ORDER BY tickets_completed DESC
327
- """)
328
-
329
- params = {
330
- "project_id": str(project_id),
331
- "target_date": target_date
332
- }
333
- if user_ids:
334
- # Add individual user_id parameters
335
- for i, uid in enumerate(user_ids):
336
- params[f"user_id_{i}"] = str(uid)
337
-
338
- result = self.db.execute(query, params)
339
- rows = result.fetchall()
340
-
341
- return [dict(row._mapping) for row in rows]
342
-
343
- def _bulk_upsert_timesheets(
344
- self,
345
- agent_stats: List[Dict[str, Any]],
346
- run_id: UUID,
347
- target_date: date,
348
- project_id: UUID
349
- ) -> tuple[int, int]:
350
- """
351
- Bulk upsert timesheets using PostgreSQL's ON CONFLICT.
352
-
353
- Returns:
354
- (created_count, updated_count)
355
- """
356
-
357
- if not agent_stats:
358
- return (0, 0)
359
-
360
- query = text("""
361
- INSERT INTO timesheets (
362
- user_id, project_id, work_date,
363
- check_in_time, check_out_time,
364
- tickets_assigned, tickets_completed, tickets_rejected,
365
- tickets_cancelled, tickets_rescheduled,
366
- total_expenses, approved_expenses, pending_expenses, rejected_expenses,
367
- expense_claims_count,
368
- reconciliation_run_id, last_reconciled_at,
369
- status, created_at, updated_at
370
- ) VALUES (
371
- :user_id, :project_id, :work_date,
372
- :check_in_time, :check_out_time,
373
- :tickets_assigned, :tickets_completed, :tickets_rejected,
374
- :tickets_cancelled, :tickets_rescheduled,
375
- :total_expenses, :approved_expenses, :pending_expenses, :rejected_expenses,
376
- :expense_claims_count,
377
- :reconciliation_run_id, NOW(),
378
- 'present', NOW(), NOW()
379
- )
380
- ON CONFLICT (user_id, work_date) WHERE deleted_at IS NULL
381
- DO UPDATE SET
382
- check_in_time = EXCLUDED.check_in_time,
383
- check_out_time = EXCLUDED.check_out_time,
384
- tickets_assigned = EXCLUDED.tickets_assigned,
385
- tickets_completed = EXCLUDED.tickets_completed,
386
- tickets_rejected = EXCLUDED.tickets_rejected,
387
- tickets_cancelled = EXCLUDED.tickets_cancelled,
388
- tickets_rescheduled = EXCLUDED.tickets_rescheduled,
389
- total_expenses = EXCLUDED.total_expenses,
390
- approved_expenses = EXCLUDED.approved_expenses,
391
- pending_expenses = EXCLUDED.pending_expenses,
392
- rejected_expenses = EXCLUDED.rejected_expenses,
393
- expense_claims_count = EXCLUDED.expense_claims_count,
394
- reconciliation_run_id = EXCLUDED.reconciliation_run_id,
395
- last_reconciled_at = NOW(),
396
- updated_at = NOW()
397
- RETURNING (xmax = 0) AS inserted
398
- """)
399
-
400
- # Execute bulk upsert
401
- results = []
402
- for stats in agent_stats:
403
- result = self.db.execute(query, {
404
- "user_id": str(stats["user_id"]),
405
- "project_id": str(project_id),
406
- "work_date": target_date,
407
- "check_in_time": stats.get("check_in_time"),
408
- "check_out_time": stats.get("check_out_time"),
409
- "tickets_assigned": stats["tickets_assigned"],
410
- "tickets_completed": stats["tickets_completed"],
411
- "tickets_rejected": stats["tickets_rejected"],
412
- "tickets_cancelled": stats["tickets_cancelled"],
413
- "tickets_rescheduled": stats["tickets_rescheduled"],
414
- "total_expenses": float(stats["total_expenses"]),
415
- "approved_expenses": float(stats["approved_expenses"]),
416
- "pending_expenses": float(stats["pending_expenses"]),
417
- "rejected_expenses": float(stats["rejected_expenses"]),
418
- "expense_claims_count": stats["expense_claims_count"],
419
- "reconciliation_run_id": str(run_id)
420
- })
421
- row = result.fetchone()
422
- if row:
423
- results.append(row[0])
424
-
425
- created = sum(1 for r in results if r) # inserted = True
426
- updated = len(results) - created
427
-
428
- return (created, updated)
429
-
430
- def _calculate_summary(self, agent_stats: List[Dict[str, Any]]) -> Dict[str, Any]:
431
- """Calculate project-wide summary statistics."""
432
- if not agent_stats:
433
- return {}
434
-
435
- return {
436
- "total_agents": len(agent_stats),
437
- "total_tickets_assigned": sum(s["tickets_assigned"] for s in agent_stats),
438
- "total_tickets_completed": sum(s["tickets_completed"] for s in agent_stats),
439
- "total_tickets_rejected": sum(s["tickets_rejected"] for s in agent_stats),
440
- "total_tickets_cancelled": sum(s["tickets_cancelled"] for s in agent_stats),
441
- "total_expenses": float(sum(s["total_expenses"] for s in agent_stats)),
442
- "total_approved_expenses": float(sum(s["approved_expenses"] for s in agent_stats)),
443
- "total_pending_expenses": float(sum(s["pending_expenses"] for s in agent_stats)),
444
- "total_expense_claims": sum(s["expense_claims_count"] for s in agent_stats),
445
- "avg_tickets_per_agent": sum(s["tickets_completed"] for s in agent_stats) / len(agent_stats) if agent_stats else 0,
446
- "avg_expenses_per_agent": sum(s["total_expenses"] for s in agent_stats) / len(agent_stats) if agent_stats else 0
447
- }
448
-
449
- def _complete_run(
450
- self,
451
- run_id: UUID,
452
- agents_processed: int,
453
- timesheets_created: int,
454
- timesheets_updated: int,
455
- assignments_processed: int,
456
- expenses_processed: int,
457
- summary_stats: Dict[str, Any],
458
- anomalies: List[Dict[str, Any]],
459
- execution_time_ms: int,
460
- query_time_ms: int
461
- ):
462
- """Mark reconciliation run as complete."""
463
-
464
- query = text("""
465
- UPDATE reconciliation_runs
466
- SET
467
- status = 'completed',
468
- completed_at = NOW(),
469
- agents_processed = :agents_processed,
470
- timesheets_created = :timesheets_created,
471
- timesheets_updated = :timesheets_updated,
472
- assignments_processed = :assignments_processed,
473
- expenses_processed = :expenses_processed,
474
- summary_stats = :summary_stats,
475
- anomalies_detected = :anomalies,
476
- execution_time_ms = :execution_time_ms,
477
- query_time_ms = :query_time_ms,
478
- updated_at = NOW()
479
- WHERE id = :run_id
480
- """)
481
-
482
- self.db.execute(query, {
483
- "run_id": str(run_id),
484
- "agents_processed": agents_processed,
485
- "timesheets_created": timesheets_created,
486
- "timesheets_updated": timesheets_updated,
487
- "assignments_processed": assignments_processed,
488
- "expenses_processed": expenses_processed,
489
- "summary_stats": summary_stats,
490
- "anomalies": anomalies,
491
- "execution_time_ms": execution_time_ms,
492
- "query_time_ms": query_time_ms
493
- })
494
-
495
- def _fail_run(self, run_id: UUID, error_message: str):
496
- """Mark reconciliation run as failed."""
497
-
498
- query = text("""
499
- UPDATE reconciliation_runs
500
- SET
501
- status = 'failed',
502
- completed_at = NOW(),
503
- error_message = :error_message,
504
- updated_at = NOW()
505
- WHERE id = :run_id
506
- """)
507
-
508
- try:
509
- self.db.execute(query, {
510
- "run_id": str(run_id),
511
- "error_message": error_message
512
- })
513
- self.db.commit()
514
- except Exception as e:
515
- logger.error(f"Failed to update run status: {str(e)}")
516
-
517
- def update_user_timesheet_realtime(
518
- self,
519
- user_id: UUID,
520
- project_id: UUID,
521
- work_date: date,
522
- trigger_type: str,
523
- trigger_entity_type: str,
524
- trigger_entity_id: UUID
525
- ) -> Optional[UUID]:
526
- """
527
- Real-time timesheet update triggered by events.
528
-
529
- This is called immediately when:
530
- - Assignment created/updated
531
- - Ticket completed
532
- - Expense created/approved/rejected
533
- - Inventory issued/installed/returned/lost
534
-
535
- Args:
536
- user_id: Field agent whose timesheet to update
537
- project_id: Project context
538
- work_date: Date of the activity
539
- trigger_type: Event type (e.g., 'assignment_created', 'expense_approved')
540
- trigger_entity_type: Entity type (e.g., 'ticket_assignment', 'ticket_expense')
541
- trigger_entity_id: ID of the entity that triggered the update
542
-
543
- Returns:
544
- UUID of timesheet record (or None if update failed)
545
- """
546
- try:
547
- # Step 1: Aggregate current data for this user/date
548
- agent_stats = self._aggregate_agent_activity(
549
- project_id=project_id,
550
- target_date=work_date,
551
- user_ids=[user_id]
552
- )
553
-
554
- if not agent_stats:
555
- logger.warning(
556
- f"No activity found for real-time update: "
557
- f"user={user_id}, date={work_date}, trigger={trigger_type}"
558
- )
559
- return None
560
-
561
- stats = agent_stats[0]
562
-
563
- # Step 2: Upsert timesheet with all columns (migrations confirmed run)
564
- query = text("""
565
- INSERT INTO timesheets (
566
- user_id, project_id, work_date,
567
- check_in_time, check_out_time,
568
- tickets_assigned, tickets_completed, tickets_rejected,
569
- tickets_cancelled, tickets_rescheduled,
570
- total_expenses, approved_expenses, pending_expenses, rejected_expenses,
571
- expense_claims_count,
572
- update_source, last_realtime_update_at,
573
- status, created_at, updated_at
574
- ) VALUES (
575
- :user_id, :project_id, :work_date,
576
- :check_in_time, :check_out_time,
577
- :tickets_assigned, :tickets_completed, :tickets_rejected,
578
- :tickets_cancelled, :tickets_rescheduled,
579
- :total_expenses, :approved_expenses, :pending_expenses, :rejected_expenses,
580
- :expense_claims_count,
581
- 'realtime', NOW(),
582
- 'present', NOW(), NOW()
583
- )
584
- ON CONFLICT (user_id, work_date) WHERE deleted_at IS NULL
585
- DO UPDATE SET
586
- check_in_time = EXCLUDED.check_in_time,
587
- check_out_time = EXCLUDED.check_out_time,
588
- tickets_assigned = EXCLUDED.tickets_assigned,
589
- tickets_completed = EXCLUDED.tickets_completed,
590
- tickets_rejected = EXCLUDED.tickets_rejected,
591
- tickets_cancelled = EXCLUDED.tickets_cancelled,
592
- tickets_rescheduled = EXCLUDED.tickets_rescheduled,
593
- total_expenses = EXCLUDED.total_expenses,
594
- approved_expenses = EXCLUDED.approved_expenses,
595
- pending_expenses = EXCLUDED.pending_expenses,
596
- rejected_expenses = EXCLUDED.rejected_expenses,
597
- expense_claims_count = EXCLUDED.expense_claims_count,
598
- update_source = 'realtime',
599
- last_realtime_update_at = NOW(),
600
- updated_at = NOW()
601
- RETURNING id
602
- """)
603
-
604
- result = self.db.execute(query, {
605
- "user_id": str(user_id),
606
- "project_id": str(project_id),
607
- "work_date": work_date,
608
- "check_in_time": stats.get("check_in_time"),
609
- "check_out_time": stats.get("check_out_time"),
610
- "tickets_assigned": stats["tickets_assigned"],
611
- "tickets_completed": stats["tickets_completed"],
612
- "tickets_rejected": stats["tickets_rejected"],
613
- "tickets_cancelled": stats["tickets_cancelled"],
614
- "tickets_rescheduled": stats["tickets_rescheduled"],
615
- "total_expenses": float(stats["total_expenses"]),
616
- "approved_expenses": float(stats["approved_expenses"]),
617
- "pending_expenses": float(stats["pending_expenses"]),
618
- "rejected_expenses": float(stats["rejected_expenses"]),
619
- "expense_claims_count": stats["expense_claims_count"]
620
- })
621
-
622
- timesheet_id = result.scalar_one()
623
-
624
- # Step 3: Log the update in audit table
625
- log_query = text("""
626
- SELECT log_timesheet_update(
627
- :timesheet_id,
628
- :trigger_type,
629
- :trigger_entity_type,
630
- :trigger_entity_id,
631
- :fields_changed
632
- )
633
- """)
634
-
635
- # Build fields_changed JSON
636
- fields_changed = {
637
- "tickets_assigned": stats["tickets_assigned"],
638
- "tickets_completed": stats["tickets_completed"],
639
- "total_expenses": float(stats["total_expenses"]),
640
- "approved_expenses": float(stats["approved_expenses"])
641
- }
642
-
643
- self.db.execute(log_query, {
644
- "timesheet_id": str(timesheet_id),
645
- "trigger_type": trigger_type,
646
- "trigger_entity_type": trigger_entity_type,
647
- "trigger_entity_id": str(trigger_entity_id),
648
- "fields_changed": json.dumps(fields_changed)
649
- })
650
-
651
- # Commit the transaction
652
- self.db.commit()
653
-
654
- logger.info(
655
- f"Real-time timesheet update: user={user_id}, date={work_date}, "
656
- f"trigger={trigger_type}, timesheet={timesheet_id}"
657
- )
658
-
659
- return timesheet_id
660
-
661
- except Exception as e:
662
- self.db.rollback()
663
- logger.error(
664
- f"Real-time timesheet update failed: user={user_id}, "
665
- f"date={work_date}, trigger={trigger_type}, error={str(e)}",
666
- exc_info=True
667
- )
668
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/tasks/__init__.py CHANGED
@@ -2,6 +2,4 @@
2
  Background tasks and scheduled jobs
3
  """
4
 
5
- from .scheduler import start_scheduler, shutdown_scheduler
6
-
7
- __all__ = ["start_scheduler", "shutdown_scheduler"]
 
2
  Background tasks and scheduled jobs
3
  """
4
 
5
+ __all__ = []
 
 
src/app/tasks/scheduler.py DELETED
@@ -1,181 +0,0 @@
1
- """
2
- APScheduler Configuration for Daily Reconciliation
3
-
4
- Runs at 10 PM (Africa/Nairobi timezone) to validate today's real-time updates.
5
- Since we have real-time reconciliation (99% of updates), this job acts as a safety net:
6
- - Validates real-time updates are accurate
7
- - Finds orphaned records (assignments/expenses without timesheet updates)
8
- - Detects discrepancies between real-time and actual data
9
- - Marks timesheets needing review
10
- """
11
-
12
- from apscheduler.schedulers.background import BackgroundScheduler
13
- from apscheduler.triggers.cron import CronTrigger
14
- from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
15
- from datetime import date, timedelta
16
- import logging
17
- from sqlalchemy import text
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
- # Global scheduler instance
22
- scheduler = BackgroundScheduler(timezone='Africa/Nairobi')
23
-
24
-
25
- def start_scheduler():
26
- """
27
- Initialize and start the scheduler.
28
- Called on application startup.
29
- """
30
- # Add daily validation job (runs at 10 PM)
31
- scheduler.add_job(
32
- func=run_daily_validation,
33
- trigger=CronTrigger(hour=22, minute=0), # 10 PM Nairobi time
34
- id='daily_validation',
35
- name='Daily Field Agent Reconciliation',
36
- replace_existing=True,
37
- max_instances=1, # Prevent overlapping runs
38
- misfire_grace_time=3600 # Allow 1 hour grace if server was down
39
- )
40
-
41
- # Add event listeners for monitoring
42
- scheduler.add_listener(
43
- job_executed_listener,
44
- EVENT_JOB_EXECUTED | EVENT_JOB_ERROR
45
- )
46
-
47
- scheduler.start()
48
- logger.info("Reconciliation scheduler started (runs at 10 PM Africa/Nairobi)")
49
-
50
-
51
- def shutdown_scheduler():
52
- """
53
- Gracefully shutdown the scheduler.
54
- Called on application shutdown.
55
- """
56
- scheduler.shutdown(wait=True)
57
- logger.info("Reconciliation scheduler stopped")
58
-
59
-
60
- def run_daily_validation():
61
- """
62
- Scheduled job: Validate today's real-time updates for all active projects.
63
-
64
- Runs at 10 PM to catch any issues before end of day.
65
- This is a safety net - 99% of updates happen in real-time.
66
-
67
- What it does:
68
- 1. Re-runs aggregation for today
69
- 2. Compares with existing timesheet data
70
- 3. Finds orphaned records (activity without timesheet)
71
- 4. Marks discrepancies for review
72
- 5. Updates timesheets if needed
73
- """
74
- today = date.today()
75
-
76
- logger.info(f"Starting scheduled validation for {today}")
77
-
78
- try:
79
- validate_all_projects(today)
80
- logger.info(f"Scheduled validation completed for {today}")
81
-
82
- except Exception as e:
83
- logger.error(f"Scheduled validation failed: {str(e)}", exc_info=True)
84
- # Don't raise - let scheduler continue
85
-
86
-
87
- def validate_all_projects(target_date: date):
88
- """
89
- Validate real-time updates for all active projects.
90
-
91
- This is a safety net that runs at 10 PM to catch any issues.
92
- Since 99% of updates happen in real-time, this mainly:
93
- - Validates existing data is correct
94
- - Finds orphaned records
95
- - Detects discrepancies
96
-
97
- Processes projects sequentially to avoid overwhelming the database.
98
- For 10 projects with 500 agents each, total time ~5 minutes.
99
- """
100
- from app.core.database import SessionLocal
101
- from app.services.reconciliation import ReconciliationService
102
-
103
- db = SessionLocal()
104
- try:
105
- # Get all active projects
106
- query = text("""
107
- SELECT id, title
108
- FROM projects
109
- WHERE deleted_at IS NULL
110
- AND is_closed = FALSE
111
- AND status IN ('active', 'planning')
112
- ORDER BY created_at DESC
113
- """)
114
-
115
- result = db.execute(query)
116
- projects = result.fetchall()
117
-
118
- logger.info(f"Validating {len(projects)} projects for {target_date}")
119
-
120
- # Validate each project
121
- results = []
122
-
123
- for project in projects:
124
- try:
125
- # Create new session for each project to avoid transaction issues
126
- project_db = SessionLocal()
127
- try:
128
- service = ReconciliationService(project_db)
129
-
130
- # Run full reconciliation (will update existing timesheets)
131
- # This validates real-time updates and catches orphans
132
- run_id = service.reconcile_project_day(
133
- project_id=project.id,
134
- target_date=target_date,
135
- run_type="scheduled"
136
- )
137
-
138
- results.append({
139
- "project_id": str(project.id),
140
- "project_title": project.title,
141
- "run_id": str(run_id),
142
- "status": "success"
143
- })
144
- finally:
145
- project_db.close()
146
-
147
- except Exception as e:
148
- logger.error(
149
- f"Failed to validate project {project.id} ({project.title}): {str(e)}",
150
- exc_info=True
151
- )
152
- results.append({
153
- "project_id": str(project.id),
154
- "project_title": project.title,
155
- "error": str(e),
156
- "status": "failed"
157
- })
158
-
159
- # Log summary
160
- success_count = sum(1 for r in results if r["status"] == "success")
161
- logger.info(
162
- f"Validation summary: {success_count}/{len(projects)} projects succeeded"
163
- )
164
-
165
- # Log failed projects
166
- failed = [r for r in results if r["status"] == "failed"]
167
- if failed:
168
- logger.warning(f"Failed projects: {[r['project_title'] for r in failed]}")
169
-
170
- except Exception as e:
171
- logger.error(f"Error in validate_all_projects: {str(e)}", exc_info=True)
172
- finally:
173
- db.close()
174
-
175
-
176
- def job_executed_listener(event):
177
- """Log job execution events for monitoring."""
178
- if event.exception:
179
- logger.error(f"Job {event.job_id} failed: {event.exception}")
180
- else:
181
- logger.info(f"Job {event.job_id} completed successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
supabase/migrations/20241212_remove_reconciliation_system.sql ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- =====================================================
2
+ -- REMOVE RECONCILIATION SYSTEM
3
+ -- =====================================================
4
+ -- Created: 2025-12-12
5
+ -- Purpose: Completely remove reconciliation system for MVP
6
+ --
7
+ -- This migration:
8
+ -- 1. Drops reconciliation_runs table
9
+ -- 2. Drops timesheet_updates table
10
+ -- 3. Removes reconciliation columns from timesheets
11
+ -- 4. Drops related indexes and functions
12
+ -- 5. Cleans up all reconciliation infrastructure
13
+ --
14
+ -- IMPORTANT: Run this in Supabase SQL Editor
15
+ -- =====================================================
16
+
17
+ BEGIN;
18
+
19
+ -- =====================================================
20
+ -- 1. DROP AUDIT TABLES
21
+ -- =====================================================
22
+
23
+ -- Drop timesheet_updates table (audit trail)
24
+ DROP TABLE IF EXISTS timesheet_updates CASCADE;
25
+
26
+ -- Drop reconciliation_runs table (job tracking)
27
+ DROP TABLE IF EXISTS reconciliation_runs CASCADE;
28
+
29
+ -- =====================================================
30
+ -- 2. REMOVE RECONCILIATION COLUMNS FROM TIMESHEETS
31
+ -- =====================================================
32
+
33
+ -- Remove reconciliation tracking columns
34
+ ALTER TABLE timesheets
35
+ DROP COLUMN IF EXISTS reconciliation_run_id,
36
+ DROP COLUMN IF EXISTS last_reconciled_at,
37
+ DROP COLUMN IF EXISTS update_source,
38
+ DROP COLUMN IF EXISTS last_realtime_update_at,
39
+ DROP COLUMN IF EXISTS last_validated_at,
40
+ DROP COLUMN IF EXISTS needs_review,
41
+ DROP COLUMN IF EXISTS discrepancy_notes,
42
+ DROP COLUMN IF EXISTS version;
43
+
44
+ -- =====================================================
45
+ -- 3. DROP RECONCILIATION-SPECIFIC INDEXES
46
+ -- =====================================================
47
+
48
+ -- Reconciliation runs indexes
49
+ DROP INDEX IF EXISTS idx_reconciliation_runs_project_date;
50
+ DROP INDEX IF EXISTS idx_reconciliation_runs_status;
51
+ DROP INDEX IF EXISTS idx_reconciliation_runs_unique_active_run;
52
+ DROP INDEX IF EXISTS idx_reconciliation_runs_anomalies_gin;
53
+ DROP INDEX IF EXISTS idx_reconciliation_runs_discrepancies;
54
+
55
+ -- Timesheet updates indexes
56
+ DROP INDEX IF EXISTS idx_timesheet_updates_timesheet;
57
+ DROP INDEX IF EXISTS idx_timesheet_updates_trigger;
58
+
59
+ -- Timesheets reconciliation indexes
60
+ DROP INDEX IF EXISTS idx_timesheets_reconciliation_run;
61
+ DROP INDEX IF EXISTS idx_timesheets_realtime_updates;
62
+ DROP INDEX IF EXISTS idx_timesheets_needs_review;
63
+
64
+ -- Note: Keep idx_timesheets_user_date_unique (needed for core functionality)
65
+ -- Note: Keep idx_timesheets_project_date (needed for queries)
66
+
67
+ -- =====================================================
68
+ -- 4. DROP RECONCILIATION HELPER FUNCTIONS
69
+ -- =====================================================
70
+
71
+ -- Drop timesheet update logging function
72
+ DROP FUNCTION IF EXISTS log_timesheet_update CASCADE;
73
+
74
+ -- Drop version increment trigger function
75
+ DROP FUNCTION IF EXISTS increment_timesheet_version CASCADE;
76
+
77
+ -- Drop reconciliation runs updated_at trigger function
78
+ DROP FUNCTION IF EXISTS update_reconciliation_runs_updated_at CASCADE;
79
+
80
+ -- =====================================================
81
+ -- 5. VERIFY CLEANUP
82
+ -- =====================================================
83
+
84
+ -- Verify tables are dropped
85
+ DO $$
86
+ BEGIN
87
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'reconciliation_runs') THEN
88
+ RAISE EXCEPTION 'reconciliation_runs table still exists';
89
+ END IF;
90
+
91
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'timesheet_updates') THEN
92
+ RAISE EXCEPTION 'timesheet_updates table still exists';
93
+ END IF;
94
+
95
+ RAISE NOTICE 'Reconciliation system successfully removed';
96
+ END $$;
97
+
98
+ COMMIT;
99
+
100
+ -- =====================================================
101
+ -- ROLLBACK INSTRUCTIONS (if needed)
102
+ -- =====================================================
103
+ --
104
+ -- If you need to rollback this migration:
105
+ -- 1. Restore database from backup taken before migration
106
+ -- 2. Or re-run the original migrations:
107
+ -- - 20241209_add_reconciliation_system.sql
108
+ -- - 20241210_add_realtime_reconciliation.sql
109
+ --
110
+ -- =====================================================
111
+
112
+ -- =====================================================
113
+ -- POST-MIGRATION VERIFICATION QUERIES
114
+ -- =====================================================
115
+ --
116
+ -- Run these after migration to verify cleanup:
117
+ --
118
+ -- 1. Check for any remaining reconciliation columns in timesheets:
119
+ -- SELECT column_name
120
+ -- FROM information_schema.columns
121
+ -- WHERE table_name = 'timesheets'
122
+ -- AND column_name LIKE '%reconcil%';
123
+ --
124
+ -- 2. Check for any remaining reconciliation indexes:
125
+ -- SELECT indexname
126
+ -- FROM pg_indexes
127
+ -- WHERE indexname LIKE '%reconcil%';
128
+ --
129
+ -- 3. Check for any remaining reconciliation functions:
130
+ -- SELECT routine_name
131
+ -- FROM information_schema.routines
132
+ -- WHERE routine_name LIKE '%reconcil%';
133
+ --
134
+ -- All queries should return 0 rows.
135
+ --
136
+ -- =====================================================
137
+