Spaces:
Sleeping
Sleeping
refactor: remove reconciliation system and all related code, tasks, and docs
Browse files- docs/agent/implementation-notes/RECONCILIATION_REMOVAL_COMPLETE.md +226 -0
- docs/devlogs/browser/browserconsole.txt +0 -391
- docs/devlogs/server/builderrors.txt +228 -91
- docs/features/realtime-timesheet-updates.md +954 -0
- docs/features/reconciliation-removal-plan.md +655 -0
- docs/features/reconciliation-system-analysis.md +266 -0
- docs/features/{bulk-invitations.md → users/bulk-invitations.md} +0 -0
- src/app/api/endpoints/reconciliation.py +0 -320
- src/app/api/v1/router.py +0 -4
- src/app/api/v1/ticket_assignments.py +3 -50
- src/app/api/v1/ticket_completion.py +1 -20
- src/app/api/v1/ticket_expenses.py +3 -49
- src/app/api/v1/timesheets.py +1 -1
- src/app/main.py +2 -19
- src/app/services/reconciliation/__init__.py +0 -15
- src/app/services/reconciliation/anomaly_detector.py +0 -140
- src/app/services/reconciliation/models.py +0 -64
- src/app/services/reconciliation/reconciliation_service.py +0 -668
- src/app/tasks/__init__.py +1 -3
- src/app/tasks/scheduler.py +0 -181
- supabase/migrations/20241212_remove_reconciliation_system.sql +137 -0
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 |
-
|
| 2 |
-
|
| 3 |
-
INFO:
|
| 4 |
-
INFO:
|
| 5 |
-
INFO: 2025-12-
|
| 6 |
-
INFO: 2025-12-
|
| 7 |
-
INFO: 2025-12-
|
| 8 |
-
INFO: 2025-12-
|
| 9 |
-
INFO: 2025-12-
|
| 10 |
-
INFO: 2025-12-
|
| 11 |
-
INFO: 2025-12-
|
| 12 |
-
INFO: 2025-12-
|
| 13 |
-
INFO: 2025-12-
|
| 14 |
-
INFO: 2025-12-
|
| 15 |
-
INFO: 2025-12-
|
| 16 |
-
INFO: 2025-12-
|
| 17 |
-
INFO: 2025-12-
|
| 18 |
-
INFO: 2025-12-
|
| 19 |
-
INFO: 2025-12-
|
| 20 |
-
INFO: 2025-12-
|
| 21 |
-
INFO: 2025-12-
|
| 22 |
-
INFO: 2025-12-
|
| 23 |
-
INFO: 2025-12-
|
| 24 |
-
INFO: 2025-12-
|
| 25 |
-
INFO: 2025-12-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
INFO:
|
| 29 |
-
INFO:
|
| 30 |
-
INFO:
|
| 31 |
-
INFO: 2025-12-
|
| 32 |
-
INFO: 2025-12-
|
| 33 |
-
INFO: 2025-12-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
|
| 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 |
-
|
| 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 |
+
|