kamau1 commited on
Commit
807713e
·
1 Parent(s): 94e7f53

Add dedicated PUT /completion-data endpoint for partial updates to ticket completion data

Browse files
docs/devlogs/browser/browserconsole.txt CHANGED
The diff for this file is too large to render. See raw diff
 
docs/devlogs/browser/response.json CHANGED
@@ -1,43 +1,140 @@
1
  {
2
- "id": "a82a3824-f4f1-4283-a2e3-8c348dbb28ce",
3
  "ticket_id": "f59b29fc-d0b9-4618-b0d1-889e340da612",
4
- "user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
5
- "assigned_by_user_id": "43b778b0-2062-4724-abbb-916a4835a9b0",
6
- "action": "accepted",
7
- "is_self_assigned": true,
8
- "execution_order": null,
9
- "planned_start_time": null,
10
- "assigned_at": "2025-11-30T10:21:10.085152Z",
11
- "responded_at": "2025-11-30T10:21:10.085155Z",
12
- "journey_started_at": null,
13
- "arrived_at": null,
14
- "ended_at": null,
15
- "journey_start_latitude": null,
16
- "journey_start_longitude": null,
17
- "arrival_latitude": null,
18
- "arrival_longitude": null,
19
- "arrival_verified": false,
20
- "journey_location_history": [],
21
- "status": "ACCEPTED",
22
- "is_active": true,
23
- "travel_time_minutes": null,
24
- "work_time_minutes": null,
25
- "total_time_minutes": null,
26
- "journey_distance_km": null,
27
- "reason": null,
28
- "notes": null,
29
- "user": {
30
- "id": "43b778b0-2062-4724-abbb-916a4835a9b0",
31
- "full_name": "Viyisa Sasa",
32
- "email": "viyisa8151@feralrex.com",
33
- "phone": "+25470000001"
34
- },
35
- "assigned_by": {
36
- "id": "43b778b0-2062-4724-abbb-916a4835a9b0",
37
- "full_name": "Viyisa Sasa",
38
- "email": "viyisa8151@feralrex.com",
39
- "phone": "+25470000001"
40
- },
41
- "created_at": "2025-11-30T10:21:10.111327Z",
42
- "updated_at": "2025-11-30T10:21:10.111330Z"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
 
1
  {
 
2
  "ticket_id": "f59b29fc-d0b9-4618-b0d1-889e340da612",
3
+ "project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a",
4
+ "checklist_items": [
5
+ {
6
+ "id": "photo_Speedtest",
7
+ "type": "photo",
8
+ "photo_type": "Speedtest",
9
+ "label": "speed test collected ",
10
+ "required": true,
11
+ "min_photos": 1,
12
+ "max_photos": 1,
13
+ "uploaded_count": 0,
14
+ "uploaded_photos": [],
15
+ "status": "pending"
16
+ },
17
+ {
18
+ "id": "photo_Airtel network",
19
+ "type": "photo",
20
+ "photo_type": "Airtel network",
21
+ "label": "strength metrics",
22
+ "required": true,
23
+ "min_photos": 1,
24
+ "max_photos": 1,
25
+ "uploaded_count": 0,
26
+ "uploaded_photos": [],
27
+ "status": "pending"
28
+ },
29
+ {
30
+ "id": "photo_ODU outdoor image",
31
+ "type": "photo",
32
+ "photo_type": "ODU outdoor image",
33
+ "label": "take a photo of the odu antenna",
34
+ "required": true,
35
+ "min_photos": 1,
36
+ "max_photos": 1,
37
+ "uploaded_count": 0,
38
+ "uploaded_photos": [],
39
+ "status": "pending"
40
+ },
41
+ {
42
+ "id": "field_o",
43
+ "type": "field",
44
+ "field_name": "o",
45
+ "label": "ONT",
46
+ "data_type": "text",
47
+ "required": true,
48
+ "placeholder": null,
49
+ "validation_regex": null,
50
+ "options": null,
51
+ "value": null,
52
+ "status": "pending"
53
+ },
54
+ {
55
+ "id": "field_o",
56
+ "type": "field",
57
+ "field_name": "o",
58
+ "label": "ODU",
59
+ "data_type": "text",
60
+ "required": true,
61
+ "placeholder": null,
62
+ "validation_regex": null,
63
+ "options": null,
64
+ "value": null,
65
+ "status": "pending"
66
+ }
67
+ ],
68
+ "photo_items": [
69
+ {
70
+ "id": "photo_Speedtest",
71
+ "type": "photo",
72
+ "photo_type": "Speedtest",
73
+ "label": "speed test collected ",
74
+ "required": true,
75
+ "min_photos": 1,
76
+ "max_photos": 1,
77
+ "uploaded_count": 0,
78
+ "uploaded_photos": [],
79
+ "status": "pending"
80
+ },
81
+ {
82
+ "id": "photo_Airtel network",
83
+ "type": "photo",
84
+ "photo_type": "Airtel network",
85
+ "label": "strength metrics",
86
+ "required": true,
87
+ "min_photos": 1,
88
+ "max_photos": 1,
89
+ "uploaded_count": 0,
90
+ "uploaded_photos": [],
91
+ "status": "pending"
92
+ },
93
+ {
94
+ "id": "photo_ODU outdoor image",
95
+ "type": "photo",
96
+ "photo_type": "ODU outdoor image",
97
+ "label": "take a photo of the odu antenna",
98
+ "required": true,
99
+ "min_photos": 1,
100
+ "max_photos": 1,
101
+ "uploaded_count": 0,
102
+ "uploaded_photos": [],
103
+ "status": "pending"
104
+ }
105
+ ],
106
+ "field_items": [
107
+ {
108
+ "id": "field_o",
109
+ "type": "field",
110
+ "field_name": "o",
111
+ "label": "ONT",
112
+ "data_type": "text",
113
+ "required": true,
114
+ "placeholder": null,
115
+ "validation_regex": null,
116
+ "options": null,
117
+ "value": null,
118
+ "status": "pending"
119
+ },
120
+ {
121
+ "id": "field_o",
122
+ "type": "field",
123
+ "field_name": "o",
124
+ "label": "ODU",
125
+ "data_type": "text",
126
+ "required": true,
127
+ "placeholder": null,
128
+ "validation_regex": null,
129
+ "options": null,
130
+ "value": null,
131
+ "status": "pending"
132
+ }
133
+ ],
134
+ "is_photos_complete": false,
135
+ "is_activation_complete": false,
136
+ "is_complete": false,
137
+ "completion_percentage": 0.0,
138
+ "photos_verified_at": "2025-11-30T10:57:17.471435+00:00",
139
+ "activation_verified_at": null
140
  }
docs/devlogs/server/runtimeerror.txt CHANGED
@@ -1,107 +1,137 @@
1
- ===== Application Startup at 2025-11-30 10:20:04 =====
2
 
3
  INFO: Started server process [7]
4
  INFO: Waiting for application startup.
5
- INFO: 2025-11-30T10:20:14 - app.main: ============================================================
6
- INFO: 2025-11-30T10:20:14 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
- INFO: 2025-11-30T10:20:14 - app.main: 📊 Dashboard: Enabled
8
- INFO: 2025-11-30T10:20:14 - app.main: ============================================================
9
- INFO: 2025-11-30T10:20:14 - app.main: 📦 Database:
10
- INFO: 2025-11-30T10:20:14 - app.main: ✓ Connected | 44 tables | 6 users
11
- INFO: 2025-11-30T10:20:14 - app.main: 💾 Cache & Sessions:
12
- INFO: 2025-11-30T10:20:15 - app.services.otp_service: ✅ OTP Service initialized with Redis storage
13
- INFO: 2025-11-30T10:20:16 - app.main: ✓ Redis: Connected
14
- INFO: 2025-11-30T10:20:16 - app.main: 🔌 External Services:
15
- INFO: 2025-11-30T10:20:16 - app.main: ✓ Cloudinary: Connected
16
- INFO: 2025-11-30T10:20:16 - app.main: ✓ Resend: Configured
17
- INFO: 2025-11-30T10:20:16 - app.main: ○ WASender: Failed
18
- INFO: 2025-11-30T10:20:16 - app.main: ✓ Supabase: Connected | 6 buckets
19
- INFO: 2025-11-30T10:20:16 - app.main: ============================================================
20
- INFO: 2025-11-30T10:20:16 - app.main: ✅ Startup complete | Ready to serve requests
21
- INFO: 2025-11-30T10:20:16 - app.main: ============================================================
22
  INFO: Application startup complete.
23
  INFO: Uvicorn running on http://0.0.0.0:7860 (Press CTRL+C to quit)
24
- INFO: 10.16.11.176:37653 - "GET /health HTTP/1.1" 200 OK
25
- INFO: 10.16.11.176:37653 - "GET /health HTTP/1.1" 200 OK
26
- INFO: 10.16.11.176:51654 - "GET /health HTTP/1.1" 200 OK
27
- INFO: 10.16.6.70:45423 - "GET /health HTTP/1.1" 200 OK
28
- INFO: 10.16.34.155:12449 - "GET /health HTTP/1.1" 200 OK
29
- INFO: 2025-11-30T10:20:54 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
30
- INFO: 2025-11-30T10:20:54 - app.services.audit_service: Audit log created: login on auth by viyisa8151@feralrex.com
31
- INFO: 2025-11-30T10:20:54 - app.api.v1.auth: User logged in successfully: viyisa8151@feralrex.com
32
- INFO: 10.16.11.176:11276 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
33
- INFO: 2025-11-30T10:20:55 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
34
- INFO: 2025-11-30T10:20:55 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
35
- INFO: 10.16.11.176:11276 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
36
- INFO: 2025-11-30T10:20:56 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
37
- INFO: 2025-11-30T10:20:56 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
38
- INFO: 10.16.34.155:49018 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
39
- INFO: 2025-11-30T10:20:56 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
40
- INFO: 2025-11-30T10:20:56 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
41
- INFO: 10.16.34.155:45714 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
42
- INFO: 2025-11-30T10:20:56 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
43
- INFO: 2025-11-30T10:20:56 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
44
- INFO: 10.16.6.70:20389 - "GET /api/v1/analytics/user/overview?limit=50 HTTP/1.1" 200 OK
45
- INFO: 2025-11-30T10:20:57 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
46
- INFO: 2025-11-30T10:20:57 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
47
- INFO: 10.16.34.155:45714 - "GET /api/v1/projects?page=1&per_page=100&status=active HTTP/1.1" 200 OK
48
- INFO: 10.16.6.70:7828 - "GET /health HTTP/1.1" 200 OK
49
- INFO: 10.16.6.70:7828 - "GET /api/v1/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/detail HTTP/1.1" 200 OK
50
- /app/src/app/services/ticket_service.py:694: SAWarning: Coercing Subquery object into a select() for use in IN(); please pass a select() construct explicitly
51
- query = query.filter(Ticket.project_id.in_(team_projects))
52
- INFO: 10.16.18.114:24662 - "GET /api/v1/tickets?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&skip=0&limit=50 HTTP/1.1" 200 OK
53
- INFO: 10.16.11.176:10506 - "GET /api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a HTTP/1.1" 200 OK
54
- INFO: 2025-11-30T10:21:05 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
55
- INFO: 2025-11-30T10:21:05 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
56
- INFO: 10.16.11.176:10506 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions HTTP/1.1" 200 OK
57
- INFO: 10.16.34.155:55245 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/detail HTTP/1.1" 200 OK
58
- INFO: 2025-11-30T10:21:10 - app.services.ticket_assignment_service: Ticket f59b29fc-d0b9-4618-b0d1-889e340da612 self-assigned by Viyisa Sasa - notification queued
59
- INFO: 10.16.6.70:14190 - "POST /api/v1/ticket-assignments/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/self-assign HTTP/1.1" 201 Created
60
- INFO: 10.16.6.70:14190 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/detail HTTP/1.1" 200 OK
61
- INFO: 10.16.25.209:61895 - "GET /health HTTP/1.1" 200 OK
62
- INFO: 10.16.25.209:21306 - "GET /health HTTP/1.1" 200 OK
63
- INFO: 10.16.6.70:54905 - "GET /health HTTP/1.1" 200 OK
64
- ERROR: 2025-11-30T10:22:11 - app.core.supabase_auth: Session refresh error: Invalid Refresh Token: Already Used
65
- ERROR: 2025-11-30T10:22:11 - app.api.v1.auth: Token refresh error: Invalid Refresh Token: Already Used
66
- INFO: 10.16.6.70:54905 - "POST /api/v1/auth/refresh-token HTTP/1.1" 401 Unauthorized
67
- INFO: 10.16.25.209:11050 - "GET /health HTTP/1.1" 200 OK
68
- INFO: 2025-11-30T10:22:19 - app.core.supabase_auth: User signed in successfully: nadina73@nembors.com
69
- INFO: 2025-11-30T10:22:19 - app.services.audit_service: Audit log created: login on auth by nadina73@nembors.com
70
- INFO: 2025-11-30T10:22:19 - app.api.v1.auth: User logged in successfully: nadina73@nembors.com
71
- INFO: 10.16.34.155:36441 - "POST /api/v1/auth/login HTTP/1.1" 200 OK
72
- INFO: 2025-11-30T10:22:20 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
73
- INFO: 2025-11-30T10:22:20 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
74
- INFO: 10.16.25.209:52587 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
75
- INFO: 2025-11-30T10:22:21 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
76
- INFO: 2025-11-30T10:22:21 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
77
- INFO: 10.16.34.155:36441 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
78
- INFO: 2025-11-30T10:22:21 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
79
- INFO: 2025-11-30T10:22:21 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
80
- INFO: 2025-11-30T10:22:21 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
81
- INFO: 2025-11-30T10:22:21 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
82
- INFO: 10.16.18.114:63198 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
83
- INFO: 10.16.6.70:46136 - "GET /api/v1/analytics/user/overview HTTP/1.1" 200 OK
84
- INFO: 2025-11-30T10:22:22 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
85
- INFO: 2025-11-30T10:22:22 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
86
- INFO: 10.16.34.155:36441 - "GET /api/v1/projects?page=1&per_page=100 HTTP/1.1" 200 OK
87
- INFO: 2025-11-30T10:22:28 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
88
- INFO: 2025-11-30T10:22:28 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
89
- INFO: 2025-11-30T10:22:28 - app.services.audit_service: Audit log created: update on user_preferences by nadina73@nembors.com
90
- INFO: 2025-11-30T10:22:28 - app.api.v1.auth: Preferences updated for user: nadina73@nembors.com
91
- INFO: 10.16.6.70:40549 - "PUT /api/v1/auth/me/preferences HTTP/1.1" 200 OK
92
- INFO: 2025-11-30T10:22:28 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
93
- INFO: 2025-11-30T10:22:28 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
94
- INFO: 2025-11-30T10:22:29 - app.services.dashboard_service: Dashboard cache MISS for project 0ade6bd1-e492-4e25-b681-59f42058d29a, user c5cf92be-4172-4fe2-af5c-f05d83b3a938 - building fresh data
95
- INFO: 2025-11-30T10:22:29 - app.services.dashboard_service: Built and cached dashboard for project 0ade6bd1-e492-4e25-b681-59f42058d29a
96
- INFO: 10.16.25.209:42985 - "GET /api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/dashboard HTTP/1.1" 200 OK
97
- INFO: 2025-11-30T10:22:29 - app.api.deps: Checking active user: c5cf92be-4172-4fe2-af5c-f05d83b3a938, is_active: True, type: <class 'bool'>
98
- INFO: 2025-11-30T10:22:29 - app.api.deps: User c5cf92be-4172-4fe2-af5c-f05d83b3a938 is active - proceeding
99
- INFO: 10.16.18.114:64034 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
100
- INFO: 10.16.18.114:4512 - "GET /api/v1/notifications?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&page_size=50 HTTP/1.1" 200 OK
101
- INFO: 10.16.6.70:40549 - "GET /api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a HTTP/1.1" 200 OK
102
- INFO: 10.16.6.70:40549 - "GET /health HTTP/1.1" 200 OK
103
- INFO: 10.16.11.176:16113 - "GET /health HTTP/1.1" 200 OK
104
- INFO: 10.16.25.209:8875 - "GET /health HTTP/1.1" 200 OK
105
- INFO: 10.16.34.155:9722 - "GET /health HTTP/1.1" 200 OK
106
- INFO: 10.16.11.176:17094 - "GET /health HTTP/1.1" 200 OK
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
 
1
+ ===== Application Startup at 2025-11-30 10:44:27 =====
2
 
3
  INFO: Started server process [7]
4
  INFO: Waiting for application startup.
5
+ INFO: 2025-11-30T10:44:39 - app.main: ============================================================
6
+ INFO: 2025-11-30T10:44:39 - app.main: 🚀 SwiftOps API v1.0.0 | PRODUCTION
7
+ INFO: 2025-11-30T10:44:39 - app.main: 📊 Dashboard: Enabled
8
+ INFO: 2025-11-30T10:44:39 - app.main: ============================================================
9
+ INFO: 2025-11-30T10:44:39 - app.main: 📦 Database:
10
+ INFO: 2025-11-30T10:44:39 - app.main: ✓ Connected | 44 tables | 6 users
11
+ INFO: 2025-11-30T10:44:39 - app.main: 💾 Cache & Sessions:
12
+ INFO: 2025-11-30T10:44:40 - app.services.otp_service: ✅ OTP Service initialized with Redis storage
13
+ INFO: 2025-11-30T10:44:40 - app.main: ✓ Redis: Connected
14
+ INFO: 2025-11-30T10:44:40 - app.main: 🔌 External Services:
15
+ INFO: 2025-11-30T10:44:41 - app.main: ✓ Cloudinary: Connected
16
+ INFO: 2025-11-30T10:44:41 - app.main: ✓ Resend: Configured
17
+ INFO: 2025-11-30T10:44:41 - app.main: ○ WASender: Failed
18
+ INFO: 2025-11-30T10:44:41 - app.main: ✓ Supabase: Connected | 6 buckets
19
+ INFO: 2025-11-30T10:44:41 - app.main: ============================================================
20
+ INFO: 2025-11-30T10:44:41 - app.main: ✅ Startup complete | Ready to serve requests
21
+ INFO: 2025-11-30T10:44:41 - app.main: ============================================================
22
  INFO: Application startup complete.
23
  INFO: Uvicorn running on http://0.0.0.0:7860 (Press CTRL+C to quit)
24
+ INFO: 2025-11-30T10:44:46 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
25
+ INFO: 2025-11-30T10:44:46 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
26
+ INFO: 10.16.11.176:44468 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
27
+ INFO: 10.16.34.155:33289 - "GET /health HTTP/1.1" 200 OK
28
+ INFO: 2025-11-30T10:44:46 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
29
+ INFO: 2025-11-30T10:44:46 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
30
+ INFO: 10.16.11.176:44468 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
31
+ INFO: 2025-11-30T10:44:46 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
32
+ INFO: 2025-11-30T10:44:46 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
33
+ INFO: 10.16.25.209:26712 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
34
+ INFO: 10.16.25.209:48972 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/detail HTTP/1.1" 200 OK
35
+ INFO: 10.16.25.209:2106 - "GET /health HTTP/1.1" 200 OK
36
+ INFO: 10.16.6.70:56240 - "GET /health HTTP/1.1" 200 OK
37
+ INFO: 2025-11-30T10:45:35 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
38
+ INFO: 2025-11-30T10:45:35 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
39
+ INFO: 10.16.34.155:28174 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
40
+ INFO: 10.16.6.70:4951 - "GET /health HTTP/1.1" 200 OK
41
+ INFO: 2025-11-30T10:45:35 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
42
+ INFO: 2025-11-30T10:45:35 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
43
+ INFO: 10.16.34.155:28174 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
44
+ INFO: 2025-11-30T10:45:35 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
45
+ INFO: 2025-11-30T10:45:35 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
46
+ INFO: 10.16.25.209:63687 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
47
+ INFO: 10.16.11.176:12875 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/detail HTTP/1.1" 200 OK
48
+ INFO: 10.16.18.114:44621 - "GET /health HTTP/1.1" 200 OK
49
+ INFO: 10.16.34.155:18720 - "GET /health HTTP/1.1" 200 OK
50
+ INFO: 10.16.6.70:37291 - "GET /health HTTP/1.1" 200 OK
51
+ INFO: 10.16.6.70:37464 - "GET /health HTTP/1.1" 200 OK
52
+ INFO: 10.16.6.70:61632 - "GET /health HTTP/1.1" 200 OK
53
+ INFO: 10.16.6.70:49849 - "GET /health HTTP/1.1" 200 OK
54
+ INFO: 10.16.18.114:45207 - "GET /health HTTP/1.1" 200 OK
55
+ INFO: 10.16.11.176:1595 - "GET /health HTTP/1.1" 200 OK
56
+ INFO: 10.16.6.70:20115 - "GET /health HTTP/1.1" 200 OK
57
+ INFO: 10.16.18.114:13081 - "GET /health HTTP/1.1" 200 OK
58
+ INFO: 10.16.25.209:4876 - "GET /health HTTP/1.1" 200 OK
59
+ INFO: 10.16.6.70:4890 - "GET /health HTTP/1.1" 200 OK
60
+ INFO: 2025-11-30T10:49:04 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
61
+ INFO: 2025-11-30T10:49:04 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
62
+ INFO: 10.16.34.155:32608 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
63
+ INFO: 10.16.25.209:56255 - "GET /health HTTP/1.1" 200 OK
64
+ INFO: 2025-11-30T10:49:04 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
65
+ INFO: 2025-11-30T10:49:04 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
66
+ INFO: 10.16.11.176:20419 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
67
+ INFO: 10.16.34.155:17206 - "GET /health HTTP/1.1" 200 OK
68
+ INFO: 10.16.18.114:24637 - "GET /health HTTP/1.1" 200 OK
69
+ INFO: 10.16.34.155:43834 - "GET /health HTTP/1.1" 200 OK
70
+ INFO: 10.16.11.176:39270 - "GET / HTTP/1.1" 200 OK
71
+ INFO: 10.16.6.70:24221 - "GET /health HTTP/1.1" 200 OK
72
+ INFO: 10.16.25.209:58970 - "GET /health HTTP/1.1" 200 OK
73
+ INFO: 10.16.11.176:23159 - "GET /health HTTP/1.1" 200 OK
74
+ INFO: 10.16.11.176:39192 - "GET /health HTTP/1.1" 200 OK
75
+ INFO: 10.16.11.176:19174 - "GET /health HTTP/1.1" 200 OK
76
+ INFO: 10.16.11.176:19174 - "GET /health HTTP/1.1" 200 OK
77
+ INFO: 10.16.34.155:63357 - "GET /health HTTP/1.1" 200 OK
78
+ INFO: 10.16.11.176:31952 - "GET /health HTTP/1.1" 200 OK
79
+ INFO: 10.16.11.176:43886 - "GET /health HTTP/1.1" 200 OK
80
+ INFO: 10.16.34.155:45381 - "GET /health HTTP/1.1" 200 OK
81
+ INFO: 10.16.25.209:23771 - "GET /health HTTP/1.1" 200 OK
82
+ INFO: 10.16.25.209:63336 - "GET /health HTTP/1.1" 200 OK
83
+ INFO: 10.16.25.209:64141 - "GET /health HTTP/1.1" 200 OK
84
+ INFO: 10.16.6.70:48596 - "GET /health HTTP/1.1" 200 OK
85
+ INFO: 10.16.6.70:13091 - "GET /health HTTP/1.1" 200 OK
86
+ INFO: 10.16.18.114:2584 - "GET /health HTTP/1.1" 200 OK
87
+ INFO: 10.16.6.70:49060 - "GET /health HTTP/1.1" 200 OK
88
+ INFO: 10.16.11.176:45525 - "GET /health HTTP/1.1" 200 OK
89
+ INFO: 10.16.25.209:12389 - "GET /health HTTP/1.1" 200 OK
90
+ INFO: 10.16.6.70:8793 - "GET /health HTTP/1.1" 200 OK
91
+ INFO: 10.16.25.209:30861 - "GET /health HTTP/1.1" 200 OK
92
+ INFO: 10.16.25.209:30861 - "GET /health HTTP/1.1" 200 OK
93
+ INFO: 10.16.18.114:22088 - "GET /health HTTP/1.1" 200 OK
94
+ INFO: 2025-11-30T10:55:21 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
95
+ INFO: 2025-11-30T10:55:21 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
96
+ INFO: 10.16.34.155:29232 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
97
+ INFO: 2025-11-30T10:55:22 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
98
+ INFO: 2025-11-30T10:55:22 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
99
+ INFO: 10.16.34.155:29232 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
100
+ INFO: 2025-11-30T10:55:22 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
101
+ INFO: 2025-11-30T10:55:22 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
102
+ INFO: 10.16.34.155:37709 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
103
+ INFO: 10.16.18.114:22088 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/detail HTTP/1.1" 200 OK
104
+ INFO: 10.16.6.70:17441 - "GET /health HTTP/1.1" 200 OK
105
+ INFO: 10.16.18.114:26540 - "POST /api/v1/ticket-assignments/assignments/a82a3824-f4f1-4283-a2e3-8c348dbb28ce/start-journey HTTP/1.1" 200 OK
106
+ INFO: 10.16.6.70:14508 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/detail HTTP/1.1" 200 OK
107
+ INFO: 10.16.11.176:20636 - "POST /api/v1/ticket-assignments/assignments/a82a3824-f4f1-4283-a2e3-8c348dbb28ce/update-location HTTP/1.1" 200 OK
108
+ INFO: 10.16.11.176:17733 - "GET /health HTTP/1.1" 200 OK
109
+ INFO: 10.16.18.114:40864 - "POST /api/v1/ticket-assignments/assignments/a82a3824-f4f1-4283-a2e3-8c348dbb28ce/update-location HTTP/1.1" 200 OK
110
+ INFO: 10.16.34.155:18221 - "POST /api/v1/ticket-assignments/assignments/a82a3824-f4f1-4283-a2e3-8c348dbb28ce/arrived HTTP/1.1" 200 OK
111
+ INFO: 10.16.6.70:41146 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/detail HTTP/1.1" 200 OK
112
+ INFO: 10.16.11.176:54402 - "GET /health HTTP/1.1" 200 OK
113
+ INFO: 10.16.34.155:39104 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/completion-checklist HTTP/1.1" 200 OK
114
+ INFO: 10.16.25.209:18778 - "GET /health HTTP/1.1" 200 OK
115
+ INFO: 2025-11-30T10:57:16 - app.services.media_service: Using forced provider: cloudinary
116
+ INFO: 2025-11-30T10:57:16 - app.services.media_service: Uploading Gemini_Generated_Image_anof4fanof4fanof-removebg-preview.png (image/png) to cloudinary
117
+ INFO: 2025-11-30T10:57:17 - app.integrations.cloudinary: Uploaded file to Cloudinary: https://res.cloudinary.com/dnhajmziu/image/upload/v1764500236/file_kistjf.png
118
+ INFO: 2025-11-30T10:57:17 - app.services.media_service: Document created: e76d848f-640a-4998-9a2c-948d4bc94d77 (version 1)
119
+ INFO: 2025-11-30T10:57:17 - app.services.ticket_completion_service: Photos updated for ticket f59b29fc-d0b9-4618-b0d1-889e340da612: ['Airtel network']
120
+ INFO: 10.16.18.114:36731 - "POST /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/upload-photos HTTP/1.1" 200 OK
121
+ INFO: 2025-11-30T10:57:17 - app.services.media_service: Using forced provider: cloudinary
122
+ INFO: 2025-11-30T10:57:17 - app.services.media_service: Uploading passport 2025-11-27 101849.png (image/png) to cloudinary
123
+ INFO: 2025-11-30T10:57:18 - app.integrations.cloudinary: Uploaded file to Cloudinary: https://res.cloudinary.com/dnhajmziu/image/upload/v1764500237/file_a6bhg3.png
124
+ INFO: 2025-11-30T10:57:18 - app.services.media_service: Document created: 28e39514-4ffd-44f3-8a38-3972597264a3 (version 1)
125
+ INFO: 2025-11-30T10:57:18 - app.services.ticket_completion_service: Photos updated for ticket f59b29fc-d0b9-4618-b0d1-889e340da612: ['Speedtest']
126
+ INFO: 10.16.34.155:33974 - "POST /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/upload-photos HTTP/1.1" 200 OK
127
+ INFO: 2025-11-30T10:57:18 - app.services.media_service: Using forced provider: cloudinary
128
+ INFO: 2025-11-30T10:57:18 - app.services.media_service: Uploading Screenshot_20251111-113721.png (image/png) to cloudinary
129
+ INFO: 2025-11-30T10:57:18 - app.integrations.cloudinary: Uploaded file to Cloudinary: https://res.cloudinary.com/dnhajmziu/image/upload/v1764500238/file_ejhwnj.png
130
+ INFO: 2025-11-30T10:57:18 - app.services.media_service: Document created: 03b98bf0-00bf-489b-b82e-b5ac8ed93ff1 (version 1)
131
+ INFO: 2025-11-30T10:57:18 - app.services.ticket_completion_service: Photos updated for ticket f59b29fc-d0b9-4618-b0d1-889e340da612: ['ODU outdoor image']
132
+ INFO: 10.16.11.176:21259 - "POST /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/upload-photos HTTP/1.1" 200 OK
133
+ INFO: 10.16.11.176:21259 - "GET /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/completion-checklist HTTP/1.1" 200 OK
134
+ INFO: 10.16.6.70:11342 - "GET /health HTTP/1.1" 200 OK
135
+ INFO: 10.16.6.70:42258 - "POST /api/v1/tickets/f59b29fc-d0b9-4618-b0d1-889e340da612/activation-data HTTP/1.1" 404 Not Found
136
+ INFO: 10.16.11.176:27978 - "GET /health HTTP/1.1" 200 OK
137
 
src/app/api/v1/ticket_completion.py CHANGED
@@ -126,40 +126,97 @@ async def upload_photos(
126
 
127
 
128
  @router.post(
129
- "/{ticket_id}/update-activation",
130
  response_model=TicketCompletionResponse,
131
  responses={400: {"model": ValidationErrorResponse}},
132
- summary="Update Activation Data (Store in JSONB)",
133
  description="""
134
- Update activation/equipment data - Scoped update (data only).
 
 
135
 
136
  **Data Storage Flow:**
137
- 1. Validate against project.activation_requirements
138
- 2. Store in ticket.completion_data (JSONB column)
139
  3. Mark ticket.completion_data_verified = true
140
 
141
- Agent can fill activation form separately from photos.
142
- When ticket completes, this data routes to:
143
- - Installation tickets subscription.equipment_details + activation_details
144
- - Support tickets subscription.equipment_details (update)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  """
146
  )
147
- def update_activation_data(
148
  ticket_id: UUID,
149
  request: TicketActivationDataUpdate,
150
  db: Session = Depends(get_db),
151
  current_user: User = Depends(get_current_user)
152
  ):
153
- """Update activation/equipment data (stores in ticket.completion_data JSONB)"""
154
 
155
  ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
156
  if not ticket:
157
  raise HTTPException(status_code=404, detail="Ticket not found")
158
 
159
- # Validate and store
160
- result = TicketCompletionService.update_activation_data(
161
  ticket=ticket,
162
- activation_data=request.activation_data,
 
163
  db=db
164
  )
165
 
 
126
 
127
 
128
  @router.post(
129
+ "/{ticket_id}/completion-data",
130
  response_model=TicketCompletionResponse,
131
  responses={400: {"model": ValidationErrorResponse}},
132
+ summary="Set Completion Data (Replace All)",
133
  description="""
134
+ Set/replace ALL ticket completion data (activation, inventory, or any custom fields).
135
+
136
+ **Replaces entire completion_data object** - use this for initial submission or full replacement.
137
 
138
  **Data Storage Flow:**
139
+ 1. Validate against project requirements (activation_requirements + inventory_requirements)
140
+ 2. Replace ticket.completion_data (JSONB column) entirely
141
  3. Mark ticket.completion_data_verified = true
142
 
143
+ **Flexible for any project type:**
144
+ - Installation projects: ONT serial, ODU serial, router MAC, etc.
145
+ - Infrastructure projects: Equipment used, materials consumed, pole IDs, etc.
146
+ - Support projects: Replacement parts, diagnostic data, etc.
147
+
148
+ Project manager defines what fields are needed, this endpoint validates against those.
149
+ """
150
+ )
151
+ def set_completion_data(
152
+ ticket_id: UUID,
153
+ request: TicketActivationDataUpdate,
154
+ db: Session = Depends(get_db),
155
+ current_user: User = Depends(get_current_user)
156
+ ):
157
+ """Set/replace completion data (replaces entire object)"""
158
+
159
+ ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
160
+ if not ticket:
161
+ raise HTTPException(status_code=404, detail="Ticket not found")
162
+
163
+ # Validate and store (replaces entire object)
164
+ result = TicketCompletionService.set_completion_data(
165
+ ticket=ticket,
166
+ completion_data=request.activation_data,
167
+ replace=True,
168
+ db=db
169
+ )
170
+
171
+ return result
172
+
173
+
174
+ @router.put(
175
+ "/{ticket_id}/completion-data",
176
+ response_model=TicketCompletionResponse,
177
+ responses={400: {"model": ValidationErrorResponse}},
178
+ summary="Update Completion Data (Merge)",
179
+ description="""
180
+ Update/merge ticket completion data (partial update).
181
+
182
+ **Merges with existing data** - only updates provided fields, keeps others intact.
183
+
184
+ **Use cases:**
185
+ - Agent fills ONT serial first, then adds ODU serial later
186
+ - Update one field without resending all fields
187
+ - Incremental data entry
188
+
189
+ **Data Storage Flow:**
190
+ 1. Merge new data with existing ticket.completion_data
191
+ 2. Validate merged result against project requirements
192
+ 3. Store merged data back to ticket.completion_data
193
+ 4. Mark ticket.completion_data_verified = true if all required fields present
194
+
195
+ **Example:**
196
+ ```
197
+ Existing data: {"ont_serial": "ABC123"}
198
+ Update with: {"odu_serial": "XYZ789"}
199
+ Result: {"ont_serial": "ABC123", "odu_serial": "XYZ789"}
200
+ ```
201
  """
202
  )
203
+ def update_completion_data(
204
  ticket_id: UUID,
205
  request: TicketActivationDataUpdate,
206
  db: Session = Depends(get_db),
207
  current_user: User = Depends(get_current_user)
208
  ):
209
+ """Update completion data (merges with existing)"""
210
 
211
  ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
212
  if not ticket:
213
  raise HTTPException(status_code=404, detail="Ticket not found")
214
 
215
+ # Validate and merge with existing data
216
+ result = TicketCompletionService.set_completion_data(
217
  ticket=ticket,
218
+ completion_data=request.activation_data,
219
+ replace=False, # Merge mode
220
  db=db
221
  )
222
 
src/app/services/project_requirements_cache.py CHANGED
@@ -29,7 +29,7 @@ class ProjectRequirementsCache:
29
  Get cached project requirements
30
 
31
  Returns:
32
- Dict with photo_requirements and activation_requirements, or None if not cached
33
  """
34
  try:
35
  with requirements_cache_lock:
@@ -43,21 +43,21 @@ class ProjectRequirementsCache:
43
  return None
44
 
45
  @staticmethod
46
- def set(project_id: UUID, photo_requirements: list, activation_requirements: list):
47
  """
48
  Cache project requirements
49
 
50
  Args:
51
  project_id: Project ID
52
  photo_requirements: List of photo requirement dicts
53
- activation_requirements: List of activation requirement dicts
54
  """
55
  try:
56
  with requirements_cache_lock:
57
  key = f"requirements:{str(project_id)}"
58
  cache_data = {
59
  "photo_requirements": photo_requirements,
60
- "activation_requirements": activation_requirements,
61
  "cached_at": datetime.utcnow().isoformat()
62
  }
63
  requirements_cache[key] = cache_data
 
29
  Get cached project requirements
30
 
31
  Returns:
32
+ Dict with photo_requirements and field_requirements, or None if not cached
33
  """
34
  try:
35
  with requirements_cache_lock:
 
43
  return None
44
 
45
  @staticmethod
46
+ def set(project_id: UUID, photo_requirements: list, field_requirements: list):
47
  """
48
  Cache project requirements
49
 
50
  Args:
51
  project_id: Project ID
52
  photo_requirements: List of photo requirement dicts
53
+ field_requirements: Combined list of all field requirements (activation + inventory)
54
  """
55
  try:
56
  with requirements_cache_lock:
57
  key = f"requirements:{str(project_id)}"
58
  cache_data = {
59
  "photo_requirements": photo_requirements,
60
+ "field_requirements": field_requirements,
61
  "cached_at": datetime.utcnow().isoformat()
62
  }
63
  requirements_cache[key] = cache_data
src/app/services/ticket_completion_service.py CHANGED
@@ -2,15 +2,20 @@
2
  Ticket Completion Service - Runtime Checklist Generation & Validation
3
 
4
  NO DATABASE PERSISTENCE OF CHECKLISTS!
5
- Checklist is generated on-the-fly from project.activation_requirements and project.photo_requirements
 
 
 
 
 
6
 
7
  Flow:
8
- 1. Read project requirements (JSONB)
9
  2. Generate checklist in memory (ephemeral)
10
  3. Validate submitted data against requirements
11
  4. Route data to appropriate storage:
12
  - Photos → ticket_images + documents (via media_service.py)
13
- - Activation data → ticket.completion_data (JSONB)
14
  - Equipment details → subscription.equipment_details (JSONB)
15
  """
16
 
@@ -41,13 +46,14 @@ class TicketCompletionService:
41
  Get project requirements from cache or database
42
 
43
  Returns:
44
- Tuple of (photo_requirements, activation_requirements)
 
45
  """
46
  # Try cache first
47
  cached = ProjectRequirementsCache.get(project_id)
48
 
49
  if cached:
50
- return cached["photo_requirements"], cached["activation_requirements"]
51
 
52
  # Cache miss - query database
53
  project = db.query(Project).filter(Project.id == project_id).first()
@@ -55,12 +61,19 @@ class TicketCompletionService:
55
  raise HTTPException(status_code=404, detail="Project not found")
56
 
57
  photo_reqs = project.photo_requirements or []
 
 
 
58
  activation_reqs = project.activation_requirements or []
 
 
 
 
59
 
60
  # Cache for next time
61
- ProjectRequirementsCache.set(project_id, photo_reqs, activation_reqs)
62
 
63
- return photo_reqs, activation_reqs
64
 
65
  @staticmethod
66
  def generate_checklist(ticket: Ticket, db: Session) -> Dict[str, Any]:
@@ -77,7 +90,7 @@ class TicketCompletionService:
77
  Checklist dictionary with photo and field requirements
78
  """
79
  # Get requirements from cache or database
80
- photo_requirements, activation_requirements = TicketCompletionService._get_project_requirements(ticket.project_id, db)
81
 
82
  # Get current ticket images grouped by type
83
  ticket_images_by_type = {}
@@ -114,11 +127,11 @@ class TicketCompletionService:
114
  "status": "complete" if is_satisfied else "pending"
115
  })
116
 
117
- # Generate field checklist items from activation_requirements
118
  field_items = []
119
  completion_data = ticket.completion_data or {}
120
 
121
- for req in activation_requirements:
122
  field_name = req.get('field')
123
  current_value = completion_data.get(field_name)
124
  is_satisfied = current_value is not None and str(current_value).strip() != ""
@@ -259,46 +272,60 @@ class TicketCompletionService:
259
  }
260
 
261
  @staticmethod
262
- def update_activation_data(
263
  ticket: Ticket,
264
- activation_data: Dict[str, Any],
 
265
  db: Session
266
  ) -> Dict[str, Any]:
267
  """
268
- Update ticket activation data - Store in ticket.completion_data (JSONB)
 
 
 
269
 
270
  Args:
271
  ticket: Ticket to update
272
- activation_data: Activation/equipment data to store
 
273
  db: Database session
274
 
275
  Returns:
276
  Updated checklist with validation results
277
  """
278
  # Get requirements from cache or database
279
- _, activation_requirements = TicketCompletionService._get_project_requirements(ticket.project_id, db)
280
-
281
- # Validate activation data against project requirements
282
- validation_errors = TicketCompletionService._validate_activation_data(
283
- activation_data, activation_requirements
 
 
 
 
 
 
 
 
284
  )
285
 
286
  if validation_errors:
287
  raise HTTPException(
288
  status_code=400,
289
  detail={
290
- "message": "Activation data validation failed",
291
  "errors": validation_errors
292
  }
293
  )
294
 
295
  # Store in ticket.completion_data (JSONB)
296
- ticket.completion_data = activation_data
297
  ticket.completion_data_verified = True
298
  db.commit()
299
  db.refresh(ticket)
300
 
301
- logger.info(f"Activation data updated for ticket {ticket.id}")
 
302
 
303
  # Generate updated checklist
304
  checklist = TicketCompletionService.generate_checklist(ticket, db)
@@ -464,20 +491,20 @@ class TicketCompletionService:
464
  return errors
465
 
466
  @staticmethod
467
- def _validate_activation_data(
468
- activation_data: Dict[str, Any],
469
- activation_requirements: List[Dict[str, Any]]
470
  ) -> List[Dict[str, Any]]:
471
- """Validate activation data against project requirements"""
472
  errors = []
473
 
474
- for req in activation_requirements:
475
  field_name = req.get('field')
476
  required = req.get('required', True)
477
  data_type = req.get('type', 'text')
478
  validation_regex = req.get('validation_regex')
479
 
480
- value = activation_data.get(field_name)
481
 
482
  # Check required fields
483
  if required and (value is None or str(value).strip() == ""):
 
2
  Ticket Completion Service - Runtime Checklist Generation & Validation
3
 
4
  NO DATABASE PERSISTENCE OF CHECKLISTS!
5
+ Checklist is generated on-the-fly from ALL project requirements:
6
+ - project.photo_requirements (photos needed)
7
+ - project.activation_requirements (activation/equipment data)
8
+ - project.inventory_requirements (materials/equipment used)
9
+
10
+ Project manager populates relevant requirements, system reads and validates ALL of them.
11
 
12
  Flow:
13
+ 1. Read project requirements (JSONB) - combines activation + inventory
14
  2. Generate checklist in memory (ephemeral)
15
  3. Validate submitted data against requirements
16
  4. Route data to appropriate storage:
17
  - Photos → ticket_images + documents (via media_service.py)
18
+ - Completion data → ticket.completion_data (JSONB)
19
  - Equipment details → subscription.equipment_details (JSONB)
20
  """
21
 
 
46
  Get project requirements from cache or database
47
 
48
  Returns:
49
+ Tuple of (photo_requirements, field_requirements)
50
+ field_requirements combines both activation_requirements AND inventory_requirements
51
  """
52
  # Try cache first
53
  cached = ProjectRequirementsCache.get(project_id)
54
 
55
  if cached:
56
+ return cached["photo_requirements"], cached["field_requirements"]
57
 
58
  # Cache miss - query database
59
  project = db.query(Project).filter(Project.id == project_id).first()
 
61
  raise HTTPException(status_code=404, detail="Project not found")
62
 
63
  photo_reqs = project.photo_requirements or []
64
+
65
+ # Combine ALL field requirements (activation + inventory)
66
+ # Project manager decides what to populate - we just read and validate
67
  activation_reqs = project.activation_requirements or []
68
+ inventory_reqs = project.inventory_requirements or []
69
+
70
+ # Merge both lists - project can have both activation AND inventory requirements
71
+ field_reqs = activation_reqs + inventory_reqs
72
 
73
  # Cache for next time
74
+ ProjectRequirementsCache.set(project_id, photo_reqs, field_reqs)
75
 
76
+ return photo_reqs, field_reqs
77
 
78
  @staticmethod
79
  def generate_checklist(ticket: Ticket, db: Session) -> Dict[str, Any]:
 
90
  Checklist dictionary with photo and field requirements
91
  """
92
  # Get requirements from cache or database
93
+ photo_requirements, field_requirements = TicketCompletionService._get_project_requirements(ticket.project_id, db)
94
 
95
  # Get current ticket images grouped by type
96
  ticket_images_by_type = {}
 
127
  "status": "complete" if is_satisfied else "pending"
128
  })
129
 
130
+ # Generate field checklist items from field_requirements (activation or inventory)
131
  field_items = []
132
  completion_data = ticket.completion_data or {}
133
 
134
+ for req in field_requirements:
135
  field_name = req.get('field')
136
  current_value = completion_data.get(field_name)
137
  is_satisfied = current_value is not None and str(current_value).strip() != ""
 
272
  }
273
 
274
  @staticmethod
275
+ def set_completion_data(
276
  ticket: Ticket,
277
+ completion_data: Dict[str, Any],
278
+ replace: bool,
279
  db: Session
280
  ) -> Dict[str, Any]:
281
  """
282
+ Set or update ticket completion data - Store in ticket.completion_data (JSONB)
283
+
284
+ Validates against ALL project requirements (activation + inventory combined).
285
+ Project manager defines what fields are needed, we just validate and store.
286
 
287
  Args:
288
  ticket: Ticket to update
289
+ completion_data: Completion data to store (activation, inventory, or custom fields)
290
+ replace: If True, replace entire object. If False, merge with existing data.
291
  db: Database session
292
 
293
  Returns:
294
  Updated checklist with validation results
295
  """
296
  # Get requirements from cache or database
297
+ _, field_requirements = TicketCompletionService._get_project_requirements(ticket.project_id, db)
298
+
299
+ # Merge with existing data if not replacing
300
+ if replace:
301
+ final_data = completion_data
302
+ else:
303
+ # Merge mode - combine with existing data
304
+ existing_data = ticket.completion_data or {}
305
+ final_data = {**existing_data, **completion_data}
306
+
307
+ # Validate completion data against project requirements
308
+ validation_errors = TicketCompletionService._validate_completion_data(
309
+ final_data, field_requirements
310
  )
311
 
312
  if validation_errors:
313
  raise HTTPException(
314
  status_code=400,
315
  detail={
316
+ "message": "Completion data validation failed",
317
  "errors": validation_errors
318
  }
319
  )
320
 
321
  # Store in ticket.completion_data (JSONB)
322
+ ticket.completion_data = final_data
323
  ticket.completion_data_verified = True
324
  db.commit()
325
  db.refresh(ticket)
326
 
327
+ action = "replaced" if replace else "merged"
328
+ logger.info(f"Completion data {action} for ticket {ticket.id}")
329
 
330
  # Generate updated checklist
331
  checklist = TicketCompletionService.generate_checklist(ticket, db)
 
491
  return errors
492
 
493
  @staticmethod
494
+ def _validate_completion_data(
495
+ completion_data: Dict[str, Any],
496
+ field_requirements: List[Dict[str, Any]]
497
  ) -> List[Dict[str, Any]]:
498
+ """Validate completion data against project requirements (activation + inventory)"""
499
  errors = []
500
 
501
+ for req in field_requirements:
502
  field_name = req.get('field')
503
  required = req.get('required', True)
504
  data_type = req.get('type', 'text')
505
  validation_regex = req.get('validation_regex')
506
 
507
+ value = completion_data.get(field_name)
508
 
509
  # Check required fields
510
  if required and (value is None or str(value).strip() == ""):