kamau1 commited on
Commit
0f127a7
Β·
1 Parent(s): a71d9fe

Fix circular import by registering TicketStatusHistory before Ticket in models init to resolve SQLAlchemy mapper errors.

Browse files
docs/ORGANIZATION_CODES.md DELETED
@@ -1,468 +0,0 @@
1
- # SwiftOps Organization Codes Implementation
2
-
3
- ## Overview
4
-
5
- SwiftOps codes are unique, memorable identifiers for organizations (clients and contractors) that enable easy search and discovery in the platform.
6
-
7
- ### Format
8
-
9
- **Pattern:** `[Initials(1-3)][Year(2)][Sequence(3)]`
10
-
11
- **Examples:**
12
- - `FWL25001` - FiberWorks Ltd (2025, sequence 1)
13
- - `TIL25002` - TechInstall Ltd (2025, sequence 2)
14
- - `ABC25001` - ABC Corporation (2025, sequence 1)
15
- - `M25001` - M Company (single letter name)
16
-
17
- **Length:** 6-8 characters (depending on name length)
18
-
19
- ## Key Features
20
-
21
- 1. **Uniqueness:** Codes are unique across ALL organizations (both clients and contractors)
22
- 2. **Memorability:** Based on organization name initials for easy recall
23
- 3. **Searchability:** Can search organizations by typing initials (e.g., "FWL" finds FiberWorks)
24
- 4. **Year-aware:** Includes year suffix to organize chronologically
25
- 5. **Sequential:** Auto-increments within same prefix+year combination
26
-
27
- ## Code Generation Logic
28
-
29
- ### Initials Extraction
30
-
31
- ```python
32
- # Multi-word names: First letter of each word (max 3)
33
- "FiberWorks Ltd" β†’ "FWL"
34
- "ABC Corp" β†’ "ABC"
35
-
36
- # Single-word names: First N characters (max 3)
37
- "TechInstall" β†’ "TEC"
38
- "M" β†’ "M"
39
- ```
40
-
41
- ### Year Suffix
42
-
43
- - Takes last 2 digits of current year
44
- - Examples: 2025 β†’ "25", 2026 β†’ "26"
45
-
46
- ### Sequence Number
47
-
48
- - 3-digit zero-padded number (001-999)
49
- - Finds next available sequence for given prefix+year
50
- - Checks both clients and contractors tables
51
- - Supports up to 999 organizations with same prefix per year
52
-
53
- ## Implementation
54
-
55
- ### Database Schema
56
-
57
- ```sql
58
- -- Added to both clients and contractors tables
59
- ALTER TABLE public.clients
60
- ADD COLUMN swiftops_code VARCHAR(10) NULL;
61
-
62
- ALTER TABLE public.contractors
63
- ADD COLUMN swiftops_code VARCHAR(10) NULL;
64
-
65
- -- Unique indexes
66
- CREATE UNIQUE INDEX idx_clients_swiftops_code
67
- ON public.clients(swiftops_code) WHERE swiftops_code IS NOT NULL;
68
-
69
- CREATE UNIQUE INDEX idx_contractors_swiftops_code
70
- ON public.contractors(swiftops_code) WHERE swiftops_code IS NOT NULL;
71
- ```
72
-
73
- ### SQLAlchemy Models
74
-
75
- Both `Client` and `Contractor` models have:
76
-
77
- ```python
78
- swiftops_code = Column(String(10), unique=True, nullable=True, index=True)
79
- ```
80
-
81
- ### API Changes
82
-
83
- #### Response Schemas
84
-
85
- All organization responses now include `swiftops_code`:
86
-
87
- - `ClientResponse`
88
- - `ContractorResponse`
89
- - `OrganizationPartner`
90
-
91
- #### Search Enhancement
92
-
93
- The `/api/v1/organizations/partners` endpoint now searches by both name AND code:
94
-
95
- ```python
96
- # Users can search by typing organization name or code
97
- GET /api/v1/organizations/partners?search=FWL
98
- # Returns: FiberWorks Ltd (FWL25001)
99
-
100
- GET /api/v1/organizations/partners?search=FiberWorks
101
- # Same result
102
- ```
103
-
104
- #### Auto-Generation on Create
105
-
106
- When creating new clients or contractors, codes are automatically generated if not provided:
107
-
108
- ```python
109
- POST /api/v1/clients
110
- {
111
- "name": "FiberWorks Ltd",
112
- "industry": "Telecommunications"
113
- }
114
-
115
- # Response includes auto-generated code:
116
- {
117
- "id": "...",
118
- "name": "FiberWorks Ltd",
119
- "swiftops_code": "FWL25001", // ← Auto-generated
120
- ...
121
- }
122
- ```
123
-
124
- ## Migration Process
125
-
126
- ### Step 1: Run Database Migration
127
-
128
- ```bash
129
- # Apply the migration (adds nullable columns with indexes)
130
- # Run this in Supabase SQL Editor:
131
- supabase/migrations/20250128000000_add_swiftops_code.sql
132
- ```
133
-
134
- ### Step 2: Populate Existing Organizations
135
-
136
- ```bash
137
- # Generate codes for all existing organizations
138
- python scripts/populate_org_codes.py
139
- ```
140
-
141
- Output:
142
- ```
143
- Starting organization code population...
144
- ------------------------------------------------------------
145
-
146
- Found 15 clients without codes
147
- βœ“ Client: FiberWorks Ltd -> FWL25001
148
- βœ“ Client: TechInstall Corporation -> TIC25002
149
- ...
150
-
151
- Found 22 contractors without codes
152
- βœ“ Contractor: FieldOps Kenya -> FOK25001
153
- βœ“ Contractor: InstallPro Services -> IPS25002
154
- ...
155
-
156
- βœ… Code population completed successfully!
157
- ```
158
-
159
- ### Step 3: Verify Population
160
-
161
- ```sql
162
- -- Check for any organizations without codes
163
- SELECT name, swiftops_code FROM clients WHERE swiftops_code IS NULL;
164
- SELECT name, swiftops_code FROM contractors WHERE swiftops_code IS NULL;
165
-
166
- -- Should return 0 rows
167
- ```
168
-
169
- ### Step 4: Make Field Required (Optional)
170
-
171
- Once all organizations have codes, you can make the field NOT NULL:
172
-
173
- ```sql
174
- ALTER TABLE public.clients ALTER COLUMN swiftops_code SET NOT NULL;
175
- ALTER TABLE public.contractors ALTER COLUMN swiftops_code SET NOT NULL;
176
- ```
177
-
178
- ## Utility Functions
179
-
180
- ### `generate_org_code(org_name: str, db: Session) -> str`
181
-
182
- Generates a unique code for an organization.
183
-
184
- ```python
185
- from app.utils.org_code_generator import generate_org_code
186
-
187
- code = generate_org_code("FiberWorks Ltd", db)
188
- # Returns: "FWL25001"
189
- ```
190
-
191
- ### `is_code_available(code: str, db: Session) -> bool`
192
-
193
- Checks if a code is available (not in use).
194
-
195
- ```python
196
- from app.utils.org_code_generator import is_code_available
197
-
198
- available = is_code_available("FWL25001", db)
199
- # Returns: False (already in use)
200
- ```
201
-
202
- ### `validate_org_code_format(code: str) -> bool`
203
-
204
- Validates code format.
205
-
206
- ```python
207
- from app.utils.org_code_generator import validate_org_code_format
208
-
209
- valid = validate_org_code_format("FWL25001") # True
210
- valid = validate_org_code_format("fwl25001") # False (must be uppercase)
211
- valid = validate_org_code_format("FWL-25001") # False (no special chars)
212
- ```
213
-
214
- ## Code Collision Handling
215
-
216
- ### What Happens if Initials Match?
217
-
218
- Organizations with same initials get sequential codes:
219
-
220
- ```
221
- FiberWorks Ltd β†’ FWL25001
222
- Fiber World LLC β†’ FWL25002
223
- FW Logistics Inc β†’ FWL25003
224
- ```
225
-
226
- ### What if All 999 Slots Are Taken?
227
-
228
- The system raises a `ValueError` indicating exhaustion. This is unlikely in practice (would require 999 organizations with same initials in same year).
229
-
230
- ## Use Cases
231
-
232
- ### 1. Partner Discovery
233
-
234
- Frontend can show codes in organization lists:
235
-
236
- ```
237
- Available Contractors:
238
- - FiberWorks Ltd (FWL25001)
239
- - TechInstall Corp (TIC25001)
240
- - FieldOps Kenya (FOK25001)
241
- ```
242
-
243
- ### 2. Quick Search
244
-
245
- Users can type initials to quickly find organizations:
246
-
247
- ```
248
- Search: "FWL" β†’ FiberWorks Ltd
249
- Search: "TIC" β†’ TechInstall Corp
250
- Search: "FOK" β†’ FieldOps Kenya
251
- ```
252
-
253
- ### 3. Project Creation
254
-
255
- When creating projects, display partner codes:
256
-
257
- ```
258
- Select Client:
259
- - FiberWorks Ltd (FWL25001) - 5 active projects
260
-
261
- Select Contractor:
262
- - TechInstall Corp (TIC25001) - 12 active projects
263
- ```
264
-
265
- ### 4. External Communication
266
-
267
- Include codes in:
268
- - Invoices: "Invoice for project with FWL25001"
269
- - Reports: "Contractor performance: TIC25001"
270
- - Support tickets: "Issue with organization FWL25001"
271
-
272
- ## Frontend Integration
273
-
274
- ### Display Format
275
-
276
- Recommended display: `Name (CODE)`
277
-
278
- ```jsx
279
- <div>
280
- <span>{org.name}</span>
281
- <span className="text-gray-500">({org.swiftops_code})</span>
282
- </div>
283
- ```
284
-
285
- ### Search Implementation
286
-
287
- ```jsx
288
- // Search by name or code
289
- <SearchBar
290
- placeholder="Search by name or SwiftOps code (e.g., FWL)"
291
- onChange={(value) => fetchOrganizations({ search: value })}
292
- />
293
- ```
294
-
295
- ### Partner Selection
296
-
297
- ```jsx
298
- // Show code in dropdown
299
- <Select>
300
- {partners.map(p => (
301
- <option value={p.id}>
302
- {p.name} ({p.swiftops_code})
303
- </option>
304
- ))}
305
- </Select>
306
- ```
307
-
308
- ## Testing
309
-
310
- ### Unit Tests
311
-
312
- Test code generation:
313
-
314
- ```python
315
- def test_generate_org_code():
316
- code = generate_org_code("FiberWorks Ltd", db)
317
- assert code.startswith("FWL")
318
- assert len(code) == 8
319
- assert code[-3:].isdigit()
320
-
321
- def test_code_uniqueness():
322
- code1 = generate_org_code("FiberWorks Ltd", db)
323
- code2 = generate_org_code("FiberWorks Ltd", db)
324
- assert code1 != code2 # Different sequences
325
- ```
326
-
327
- ### Integration Tests
328
-
329
- Test endpoint:
330
-
331
- ```python
332
- def test_create_client_generates_code():
333
- response = client.post("/api/v1/clients", json={
334
- "name": "Test Company",
335
- "industry": "Tech"
336
- })
337
- assert response.status_code == 201
338
- assert "swiftops_code" in response.json()
339
- assert len(response.json()["swiftops_code"]) >= 6
340
- ```
341
-
342
- ## Monitoring
343
-
344
- ### Metrics to Track
345
-
346
- 1. **Code Generation Success Rate**
347
- - Track failures to generate codes
348
- - Alert if collision rate increases
349
-
350
- 2. **Search Usage**
351
- - Track how often users search by code vs name
352
- - Optimize search if code search is popular
353
-
354
- 3. **Code Uniqueness**
355
- - Monitor prefix collision rates
356
- - Plan for alternative formats if needed
357
-
358
- ## Future Enhancements
359
-
360
- ### 1. Custom Codes (Optional)
361
-
362
- Allow admins to customize their organization code:
363
-
364
- ```python
365
- # Validation would ensure uniqueness
366
- PUT /api/v1/clients/{id}
367
- {
368
- "swiftops_code": "FIBER01" // Custom code
369
- }
370
- ```
371
-
372
- ### 2. Code Reservation
373
-
374
- Pre-reserve codes for known organizations:
375
-
376
- ```sql
377
- INSERT INTO reserved_codes (code, org_name)
378
- VALUES ('FWL25001', 'FiberWorks Ltd');
379
- ```
380
-
381
- ### 3. QR Code Integration
382
-
383
- Generate QR codes containing SwiftOps code for:
384
- - Asset tagging
385
- - Job site check-ins
386
- - Invoice scanning
387
-
388
- ### 4. Code Analytics
389
-
390
- Dashboard showing:
391
- - Most searched codes
392
- - Code collision patterns
393
- - Yearly code generation trends
394
-
395
- ## Troubleshooting
396
-
397
- ### Problem: Code not generated on create
398
-
399
- **Solution:** Ensure code generation is called before `db.add()`:
400
-
401
- ```python
402
- new_client = Client(**data)
403
- new_client.swiftops_code = generate_org_code(new_client.name, db)
404
- db.add(new_client)
405
- ```
406
-
407
- ### Problem: Duplicate codes
408
-
409
- **Solution:** Check unique index exists:
410
-
411
- ```sql
412
- SELECT * FROM pg_indexes
413
- WHERE tablename IN ('clients', 'contractors')
414
- AND indexname LIKE '%swiftops_code%';
415
- ```
416
-
417
- ### Problem: Population script fails
418
-
419
- **Solution:** Run with transaction rollback on error:
420
-
421
- ```python
422
- try:
423
- # Generate codes
424
- db.commit()
425
- except Exception as e:
426
- db.rollback()
427
- print(f"Error: {e}")
428
- ```
429
-
430
- ## Files Modified
431
-
432
- ### Models
433
- - `src/app/models/client.py` - Added swiftops_code field
434
- - `src/app/models/contractor.py` - Added swiftops_code field
435
-
436
- ### Schemas
437
- - `src/app/schemas/client.py` - Added swiftops_code to ClientBase, ClientUpdate
438
- - `src/app/schemas/contractor.py` - Added swiftops_code to ContractorBase, ContractorUpdate
439
-
440
- ### API Endpoints
441
- - `src/app/api/v1/clients.py` - Auto-generate code on create
442
- - `src/app/api/v1/contractors.py` - Auto-generate code on create
443
- - `src/app/api/v1/organizations.py` - Added code to responses, search by code
444
-
445
- ### Utilities
446
- - `src/app/utils/org_code_generator.py` - Code generation logic
447
-
448
- ### Database
449
- - `supabase/migrations/20250128000000_add_swiftops_code.sql` - Migration
450
-
451
- ### Scripts
452
- - `scripts/populate_org_codes.py` - Population script
453
-
454
- ### Documentation
455
- - `docs/ORGANIZATION_CODES.md` - This file
456
-
457
- ## Summary
458
-
459
- SwiftOps codes provide a memorable, searchable way to identify organizations in the system. The implementation:
460
-
461
- βœ… Automatically generates codes on organization creation
462
- βœ… Supports searching by both name and code
463
- βœ… Ensures uniqueness across all organizations
464
- βœ… Handles existing organizations via migration script
465
- βœ… Uses memorable format based on organization names
466
- βœ… Scales to 999 organizations per prefix per year
467
-
468
- The system is production-ready and requires only running the migration and population script to activate for existing organizations.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/ORGANIZATION_CODES_QUICK_REFERENCE.md DELETED
@@ -1,162 +0,0 @@
1
- # SwiftOps Codes - Quick Reference
2
-
3
- ## Code Format
4
-
5
- ```
6
- [Initials(1-3)][Year(2)][Sequence(3)]
7
-
8
- Examples:
9
- FWL25001 - FiberWorks Ltd
10
- TIC25002 - TechInstall Corp
11
- ABC25001 - ABC Corporation
12
- ```
13
-
14
- ## Deployment Steps
15
-
16
- ### 1. Apply Migration
17
-
18
- ```sql
19
- -- Run in Supabase SQL Editor
20
- \i supabase/migrations/20250128000000_add_swiftops_code.sql
21
- ```
22
-
23
- ### 2. Populate Existing Organizations
24
-
25
- ```bash
26
- # Set DATABASE_URL environment variable
27
- export DATABASE_URL="your-database-url"
28
-
29
- # Run population script
30
- python scripts/populate_org_codes.py
31
- ```
32
-
33
- ### 3. Verify (Optional)
34
-
35
- ```sql
36
- -- Check all have codes
37
- SELECT COUNT(*) FROM clients WHERE swiftops_code IS NULL; -- Should be 0
38
- SELECT COUNT(*) FROM contractors WHERE swiftops_code IS NULL; -- Should be 0
39
- ```
40
-
41
- ### 4. Make Required (Optional)
42
-
43
- ```sql
44
- -- Only after verification
45
- ALTER TABLE clients ALTER COLUMN swiftops_code SET NOT NULL;
46
- ALTER TABLE contractors ALTER COLUMN swiftops_code SET NOT NULL;
47
- ```
48
-
49
- ## API Usage
50
-
51
- ### Search by Code
52
-
53
- ```bash
54
- # Search organizations by name or code
55
- GET /api/v1/organizations/partners?search=FWL
56
-
57
- # Response includes swiftops_code
58
- {
59
- "partners": [
60
- {
61
- "id": "...",
62
- "name": "FiberWorks Ltd",
63
- "swiftops_code": "FWL25001",
64
- ...
65
- }
66
- ]
67
- }
68
- ```
69
-
70
- ### Create with Auto-Generated Code
71
-
72
- ```bash
73
- # POST to create client or contractor
74
- POST /api/v1/clients
75
- {
76
- "name": "New Company Ltd",
77
- "industry": "Telecommunications"
78
- }
79
-
80
- # Response includes auto-generated code
81
- {
82
- "id": "...",
83
- "name": "New Company Ltd",
84
- "swiftops_code": "NCL25001", # Auto-generated
85
- ...
86
- }
87
- ```
88
-
89
- ## Python Utility Functions
90
-
91
- ```python
92
- from app.utils.org_code_generator import generate_org_code, is_code_available
93
-
94
- # Generate code
95
- code = generate_org_code("FiberWorks Ltd", db) # "FWL25001"
96
-
97
- # Check availability
98
- available = is_code_available("FWL25002", db) # True/False
99
- ```
100
-
101
- ## Frontend Display
102
-
103
- ```jsx
104
- // Recommended format: Name (CODE)
105
- <div>
106
- <span>{org.name}</span>
107
- <span className="text-gray-500">({org.swiftops_code})</span>
108
- </div>
109
-
110
- // Example: FiberWorks Ltd (FWL25001)
111
- ```
112
-
113
- ## Troubleshooting
114
-
115
- | Issue | Solution |
116
- |-------|----------|
117
- | Code not generated on create | Ensure `generate_org_code()` called before `db.add()` |
118
- | Population script fails | Check DATABASE_URL is set correctly |
119
- | Duplicate codes | Verify unique index exists on swiftops_code |
120
- | Search not finding codes | Ensure search filter includes `swiftops_code.ilike()` |
121
-
122
- ## Files Modified
123
-
124
- **Models:**
125
- - `src/app/models/client.py`
126
- - `src/app/models/contractor.py`
127
-
128
- **Schemas:**
129
- - `src/app/schemas/client.py`
130
- - `src/app/schemas/contractor.py`
131
-
132
- **Endpoints:**
133
- - `src/app/api/v1/clients.py`
134
- - `src/app/api/v1/contractors.py`
135
- - `src/app/api/v1/organizations.py`
136
-
137
- **Utilities:**
138
- - `src/app/utils/org_code_generator.py`
139
-
140
- **Database:**
141
- - `supabase/migrations/20250128000000_add_swiftops_code.sql`
142
- - `scripts/populate_org_codes.py`
143
-
144
- ## Key Features
145
-
146
- βœ… **Unique** - Across all organizations (clients + contractors)
147
- βœ… **Memorable** - Based on organization name initials
148
- βœ… **Searchable** - Type initials to find organizations
149
- βœ… **Auto-generated** - On organization creation
150
- βœ… **Year-aware** - Includes year suffix
151
- βœ… **Sequential** - Auto-increments within prefix
152
-
153
- ## Next Steps After Deployment
154
-
155
- 1. Update frontend to display codes in organization lists
156
- 2. Add code-based search to partner selection UI
157
- 3. Include codes in invoices and reports
158
- 4. Train users on searching by code (e.g., "FWL" for FiberWorks)
159
-
160
- ---
161
-
162
- **Full Documentation:** See `docs/ORGANIZATION_CODES.md`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/PROJECT_CREATION_FLOW.md DELETED
@@ -1,366 +0,0 @@
1
- # πŸš€ PROJECT CREATION FLOW - Complete Guide
2
-
3
- ## Current Implementation Status: βœ… FULLY WORKING
4
-
5
- Your intuition was **100% CORRECT**. The system already implements an **open marketplace model** without invitations!
6
-
7
- ---
8
-
9
- ## How Client Admin Creates a Project
10
-
11
- ### Step 1: Login as Client Admin
12
- ```
13
- User: client_admin@company.com
14
- Role: client_admin
15
- Organization: Acme Telecom (client_id: xxx-xxx-xxx)
16
- ```
17
-
18
- ### Step 2: Discover Available Partners
19
- **NEW ENDPOINT (Just implemented):**
20
- ```http
21
- GET /api/v1/organizations/partners
22
- ```
23
-
24
- **What it returns:**
25
- - **For Client Admins**: List of ALL contractors on SwiftOps
26
- - **For Contractor Admins**: List of ALL clients on SwiftOps
27
- - Each with stats: active_projects_count, total_projects_count, competencies
28
-
29
- **Response example:**
30
- ```json
31
- {
32
- "partners": [
33
- {
34
- "id": "550e8400-e29b-41d4-a716-446655440000",
35
- "name": "FiberWorks Ltd",
36
- "type": "contractor",
37
- "industry": "Telecommunications",
38
- "competencies": ["FTTH Installation", "Fiber Splicing", "Network Testing"],
39
- "is_active": true,
40
- "active_projects_count": 5,
41
- "total_projects_count": 23,
42
- "main_email": "contact@fiberworks.com",
43
- "main_phone": "+254712345678",
44
- "contact_person": "John Doe"
45
- },
46
- {
47
- "id": "660e8400-e29b-41d4-a716-446655440001",
48
- "name": "NetworkPro Solutions",
49
- "type": "contractor",
50
- "industry": "Telecommunications",
51
- "competencies": ["Fixed Wireless", "DSL", "Customer Support"],
52
- "is_active": true,
53
- "active_projects_count": 2,
54
- "total_projects_count": 15,
55
- "main_email": "info@networkpro.com",
56
- "main_phone": "+254723456789",
57
- "contact_person": "Jane Smith"
58
- }
59
- ],
60
- "total": 2,
61
- "user_role": "client_admin",
62
- "user_organization_type": "client"
63
- }
64
- ```
65
-
66
- **Filters available:**
67
- - `?is_active=true` - Only active organizations
68
- - `?industry=Telecommunications` - Filter by industry
69
- - `?search=Fiber` - Search by name
70
- - `?skip=0&limit=100` - Pagination
71
-
72
- ### Step 3: View Partner Details (Optional)
73
- ```http
74
- GET /api/v1/organizations/partners/{contractor_id}
75
- ```
76
-
77
- Returns full details about the contractor, including:
78
- - Contact information
79
- - Service capabilities
80
- - Project history
81
- - Performance stats
82
-
83
- ### Step 4: Create Project
84
- ```http
85
- POST /api/v1/projects
86
-
87
- {
88
- "title": "Nairobi West FTTH Rollout",
89
- "client_id": "xxx-xxx-xxx", // Your own client_id (auto-filled)
90
- "contractor_id": "550e8400-e29b-41d4-a716-446655440000", // Selected partner
91
- "description": "Install fiber optic infrastructure in Nairobi West area",
92
- "project_type": "installation",
93
- "service_type": "ftth",
94
- "budget": {
95
- "materials": { "amount": 500000, "currency": "KES" },
96
- "labor": { "amount": 300000, "currency": "KES" }
97
- },
98
- "start_date": "2025-02-01",
99
- "end_date": "2025-04-30"
100
- }
101
- ```
102
-
103
- **Backend validation:**
104
- - βœ… Client must be YOUR organization (enforced)
105
- - βœ… Contractor must exist and be active
106
- - βœ… No duplicate projects (same client+contractor+title)
107
- - βœ… Dates are valid (end >= start)
108
-
109
- ---
110
-
111
- ## How Contractor Admin Creates a Project
112
-
113
- Same flow, but reversed:
114
-
115
- ### Step 1: Login as Contractor Admin
116
- ```
117
- User: contractor@fiberworks.com
118
- Role: contractor_admin
119
- Organization: FiberWorks Ltd (contractor_id: xxx-xxx-xxx)
120
- ```
121
-
122
- ### Step 2: Discover Available Clients
123
- ```http
124
- GET /api/v1/organizations/partners
125
- ```
126
-
127
- **Returns ALL clients on the platform** with their project history and contact info.
128
-
129
- ### Step 3: Create Project
130
- ```http
131
- POST /api/v1/projects
132
-
133
- {
134
- "title": "Safaricom FTTH Maintenance Contract",
135
- "client_id": "770e8400-e29b-41d4-a716-446655440002", // Selected client
136
- "contractor_id": "xxx-xxx-xxx", // Your own contractor_id (auto-filled)
137
- "description": "Ongoing maintenance and support for Safaricom FTTH network",
138
- "project_type": "maintenance",
139
- "service_type": "ftth"
140
- }
141
- ```
142
-
143
- ---
144
-
145
- ## Authorization Model
146
-
147
- ### Who Can Create Projects?
148
-
149
- | Role | Can Create? | Restrictions |
150
- |------|-------------|--------------|
151
- | **platform_admin** | βœ… Yes | Can create ANY project (any client + any contractor) |
152
- | **client_admin** | βœ… Yes | Can ONLY create projects for THEIR OWN client |
153
- | **contractor_admin** | βœ… Yes | Can ONLY create projects for THEIR OWN contractor |
154
- | field_agent | ❌ No | Cannot create projects |
155
- | project_manager | ❌ No | Cannot create projects (only assigned to existing ones) |
156
-
157
- ### Who Can See Which Organizations?
158
-
159
- | Role | Sees Clients? | Sees Contractors? |
160
- |------|---------------|-------------------|
161
- | **platform_admin** | ALL clients | ALL contractors |
162
- | **client_admin** | Only OWN client | ALL contractors βœ… |
163
- | **contractor_admin** | ALL clients βœ… | Only OWN contractor |
164
-
165
- ---
166
-
167
- ## Why This "Open Marketplace" Model Makes Sense
168
-
169
- ### βœ… Advantages:
170
-
171
- 1. **No Gatekeeping**
172
- - Client admin sees ALL available contractors
173
- - Can immediately create project with any contractor
174
- - No approval workflows or invitation delays
175
-
176
- 2. **Discovery**
177
- - Organizations can browse partners
178
- - See project history and capabilities
179
- - Make informed decisions
180
-
181
- 3. **Self-Service Onboarding**
182
- - If partner not on platform, invite them to subscribe
183
- - No platform admin bottleneck
184
- - Organic network growth
185
-
186
- 4. **B2B Transparency**
187
- - Organizations are businesses, not individuals
188
- - Transparency builds trust
189
- - Similar to Upwork, Procore, Buildertrend
190
-
191
- ### ⚠️ Potential Concerns (And Why They Don't Matter):
192
-
193
- **"What if competitors see each other?"**
194
- - In B2B, this is normal. Contractors know who their competition is.
195
- - Clients benefit from transparency when choosing partners.
196
-
197
- **"What about privacy?"**
198
- - Organization details are public-facing anyway (website, social media)
199
- - We only show business contact info, not sensitive data
200
- - Similar to Yellow Pages or LinkedIn Company Pages
201
-
202
- **"Should we require approval?"**
203
- - NO. Approval slows down business.
204
- - If organization is on SwiftOps, they're already vetted.
205
- - Projects can have their own approval workflows if needed.
206
-
207
- ---
208
-
209
- ## Alternative Models (Why They're NOT Better)
210
-
211
- ### ❌ Invitation-Only Model
212
- ```
213
- Client β†’ Send invitation β†’ Contractor β†’ Accept β†’ Create project
214
- ```
215
- **Problems:**
216
- - Adds friction (2-3 day delay)
217
- - Requires contractor to be online and check email
218
- - Platform admin becomes bottleneck
219
- - Hurts user experience
220
-
221
- ### ❌ Request-Approval Model
222
- ```
223
- Client β†’ Request project β†’ Platform admin reviews β†’ Approve β†’ Create
224
- ```
225
- **Problems:**
226
- - Platform admin becomes bottleneck
227
- - Doesn't scale beyond 10-20 projects/day
228
- - Kills the "self-service" value proposition
229
-
230
- ### βœ… Current Open Marketplace (BEST)
231
- ```
232
- Client β†’ Browse contractors β†’ Select β†’ Create project (instant)
233
- ```
234
- **Benefits:**
235
- - Instant project creation
236
- - Self-service
237
- - Scalable
238
- - Industry standard
239
-
240
- ---
241
-
242
- ## Implementation Complete βœ…
243
-
244
- ### What Was Already There:
245
- 1. βœ… Project creation endpoint with proper validation
246
- 2. βœ… Authorization checks (client_admin/contractor_admin)
247
- 3. βœ… Open visibility (clients see all contractors, vice versa)
248
- 4. βœ… Duplicate project prevention
249
- 5. βœ… Active organization validation
250
-
251
- ### What I Just Added:
252
- 1. βœ… `/api/v1/organizations/partners` - Unified partner discovery endpoint
253
- 2. βœ… `/api/v1/organizations/partners/{id}` - Partner detail view
254
- 3. βœ… Project statistics (active_projects_count, total_projects_count)
255
- 4. βœ… Search and filter capabilities
256
- 5. βœ… Proper response schemas with all needed info
257
-
258
- ---
259
-
260
- ## Frontend Implementation Guide
261
-
262
- ### Project Creation Form:
263
-
264
- ```typescript
265
- // Step 1: Load available partners when form opens
266
- const loadPartners = async () => {
267
- const response = await fetch('/api/v1/organizations/partners', {
268
- headers: { 'Authorization': `Bearer ${token}` }
269
- });
270
- const data = await response.json();
271
-
272
- // data.partners = array of organizations
273
- // data.user_organization_type = "client" or "contractor"
274
-
275
- return data;
276
- };
277
-
278
- // Step 2: Display partner selection dropdown
279
- <Select
280
- label={data.user_organization_type === 'client' ? 'Select Contractor' : 'Select Client'}
281
- options={data.partners.map(p => ({
282
- value: p.id,
283
- label: `${p.name} (${p.active_projects_count} active projects)`,
284
- subtitle: p.industry,
285
- badge: p.competencies?.join(', ')
286
- }))}
287
- />
288
-
289
- // Step 3: Create project
290
- const createProject = async (formData) => {
291
- const response = await fetch('/api/v1/projects', {
292
- method: 'POST',
293
- headers: {
294
- 'Authorization': `Bearer ${token}`,
295
- 'Content-Type': 'application/json'
296
- },
297
- body: JSON.stringify({
298
- title: formData.title,
299
- client_id: userRole === 'client_admin' ? userOrgId : formData.selectedPartnerId,
300
- contractor_id: userRole === 'contractor_admin' ? userOrgId : formData.selectedPartnerId,
301
- description: formData.description,
302
- // ... other fields
303
- })
304
- });
305
-
306
- if (response.ok) {
307
- // Project created! Redirect to project details
308
- }
309
- };
310
- ```
311
-
312
- ### Partner Discovery Page (Optional):
313
-
314
- ```typescript
315
- // Browse all available partners with search
316
- <PartnersList>
317
- <SearchBar placeholder="Search contractors by name or competency" />
318
- <FilterBar>
319
- <Filter type="industry" />
320
- <Filter type="active_only" />
321
- </FilterBar>
322
-
323
- {partners.map(partner => (
324
- <PartnerCard
325
- name={partner.name}
326
- industry={partner.industry}
327
- competencies={partner.competencies}
328
- stats={{
329
- activeProjects: partner.active_projects_count,
330
- totalProjects: partner.total_projects_count
331
- }}
332
- contact={{
333
- email: partner.main_email,
334
- phone: partner.main_phone,
335
- person: partner.contact_person
336
- }}
337
- onSelect={() => openProjectForm(partner.id)}
338
- onViewDetails={() => navigate(`/partners/${partner.id}`)}
339
- />
340
- ))}
341
- </PartnersList>
342
- ```
343
-
344
- ---
345
-
346
- ## Summary
347
-
348
- ### Your Intuition: βœ… CORRECT
349
-
350
- The "open marketplace without invitations" model is:
351
- - βœ… Already mostly implemented
352
- - οΏ½οΏ½ The right approach for B2B collaboration
353
- - βœ… Scalable and self-service
354
- - βœ… Industry standard
355
-
356
- ### What Changed:
357
- - Added unified `/organizations/partners` endpoint
358
- - Added project statistics to partner listings
359
- - Made partner discovery easier
360
-
361
- ### What Stayed the Same:
362
- - Authorization rules (can only create for own org)
363
- - Project validation
364
- - Open visibility model
365
-
366
- **You can now create robust projects with full partner discovery!** πŸŽ‰
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/TODO/NOTIFICATION_SYSTEM_REFACTOR.md ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Notification System Refactor - TODO
2
+
3
+ ## Current State (MVP)
4
+
5
+ **Problem:** Mixed sync/async code causing event loop errors. Services are sync but notification helpers are async.
6
+
7
+ **Temporary Solution:** Removed notification calls entirely, just logging "notification queued".
8
+
9
+ **Limitations:**
10
+ - No actual notifications sent
11
+ - Lost on server restart
12
+ - No retries on failure
13
+ - Runs in web worker (blocks requests)
14
+ - Tight coupling (endpoints know about notifications)
15
+
16
+ ## Proper Implementation Plan
17
+
18
+ ### Phase 1: Sync Notification Wrapper (Quick Fix)
19
+
20
+ **Goal:** Get notifications working without full async refactor.
21
+
22
+ **Implementation:**
23
+ ```python
24
+ # app/background/notifications.py
25
+ from fastapi import BackgroundTasks
26
+
27
+ def notify_ticket_assigned(db, ticket_id, agent_id, background_tasks: BackgroundTasks):
28
+ """Sync wrapper - creates notification and queues sending"""
29
+ # 1. Create notification record (sync DB insert)
30
+ notification = NotificationService().create_notification_sync(...)
31
+
32
+ # 2. Queue async sending
33
+ background_tasks.add_task(send_notification_async, notification.id)
34
+ ```
35
+
36
+ **Changes needed:**
37
+ - Create sync version of `create_notification()`
38
+ - Add `BackgroundTasks` parameter to all endpoints
39
+ - Update services to accept `background_tasks` parameter
40
+ - Call notification wrapper from services
41
+
42
+ **Pros:** Works now, minimal changes
43
+ **Cons:** Still not production-ready, lost on crash
44
+
45
+ ### Phase 2: Full Async/Await (Proper Fix)
46
+
47
+ **Goal:** Make everything async for proper async notification handling.
48
+
49
+ **Changes:**
50
+ ```python
51
+ # All services become async
52
+ class TicketAssignmentService:
53
+ async def self_assign_ticket(self, ...): # Add async
54
+ assignment = await create_assignment() # Add await
55
+ await NotificationHelper.notify_ticket_assigned(...) # Works now
56
+ return assignment
57
+
58
+ # All endpoints become async
59
+ @router.post("/tickets/{id}/self-assign")
60
+ async def self_assign(...): # Add async
61
+ assignment = await service.self_assign_ticket(...) # Add await
62
+ return assignment
63
+ ```
64
+
65
+ **Migration steps:**
66
+ 1. Convert database operations to async (use `asyncpg` or SQLAlchemy async)
67
+ 2. Convert all service methods to `async def`
68
+ 3. Convert all endpoints to `async def`
69
+ 4. Add `await` to all async calls
70
+ 5. Test thoroughly (async bugs are subtle)
71
+
72
+ **Pros:** Proper async, notifications work correctly
73
+ **Cons:** Large refactor, risky, time-consuming
74
+
75
+ ### Phase 3: Celery for Critical Tasks (Production)
76
+
77
+ **Goal:** Reliable, persistent, retriable background tasks.
78
+
79
+ **Setup:**
80
+ ```bash
81
+ pip install celery redis
82
+ ```
83
+
84
+ **Implementation:**
85
+ ```python
86
+ # celery_app/celery.py
87
+ from celery import Celery
88
+
89
+ celery_app = Celery('swiftops', broker='redis://localhost:6379/0')
90
+
91
+ # celery_app/tasks/notifications.py
92
+ @celery_app.task(bind=True, max_retries=3)
93
+ def send_notification(self, notification_id):
94
+ try:
95
+ # Send notification
96
+ pass
97
+ except Exception as e:
98
+ raise self.retry(exc=e, countdown=60)
99
+
100
+ # In service
101
+ from celery_app.tasks.notifications import send_notification
102
+
103
+ def self_assign_ticket(self, ...):
104
+ assignment = create_assignment()
105
+ notification = create_notification_record(...)
106
+ send_notification.delay(notification.id) # Queue in Celery
107
+ return assignment
108
+ ```
109
+
110
+ **Run:**
111
+ ```bash
112
+ # Terminal 1: Web server
113
+ uvicorn app.main:app
114
+
115
+ # Terminal 2: Celery worker
116
+ celery -A celery_app worker --loglevel=info
117
+ ```
118
+
119
+ **Pros:** Persistent, retriable, scalable, monitoring
120
+ **Cons:** Extra process to manage, more complexity
121
+
122
+ ### Phase 4: Event-Driven Architecture (Scale)
123
+
124
+ **Goal:** Decouple services from notifications completely.
125
+
126
+ **Pattern:**
127
+ ```python
128
+ # Services emit events
129
+ event_bus.emit(TicketAssignedEvent(ticket_id, agent_id))
130
+
131
+ # Handlers react
132
+ @event_bus.on(TicketAssignedEvent)
133
+ def on_ticket_assigned(event):
134
+ send_notification.delay(event.ticket_id, event.agent_id)
135
+ update_analytics(event)
136
+ log_audit_trail(event)
137
+ ```
138
+
139
+ **Benefits:** Clean separation, easy to add new reactions, testable
140
+
141
+ ## Recommendation
142
+
143
+ **Now:** Skip Phase 1, go straight to Phase 3 (Celery) when you implement notifications properly.
144
+
145
+ **Why skip Phase 1?** It's a half-measure that you'll throw away anyway. Better to do it right once.
146
+
147
+ **When to do it:** When you implement payroll (needs Celery anyway), add notifications at same time.
148
+
149
+ ## Files to Update (Phase 3)
150
+
151
+ 1. Create `celery_app/celery.py` - Celery config
152
+ 2. Create `celery_app/tasks/notifications.py` - Notification tasks
153
+ 3. Update `src/app/services/ticket_assignment_service.py` - Call Celery tasks
154
+ 4. Update `src/app/services/notification_service.py` - Add sync create method
155
+ 5. Update `requirements.txt` - Add celery, redis
156
+ 6. Update deployment - Run celery worker process
157
+
158
+ ## Testing Checklist
159
+
160
+ - [ ] Notifications sent on ticket assignment
161
+ - [ ] Notifications sent on inventory distribution
162
+ - [ ] Notifications sent on bulk sales order promotion
163
+ - [ ] Failed notifications retry automatically
164
+ - [ ] Server restart doesn't lose queued notifications
165
+ - [ ] Can monitor notification queue status
166
+ - [ ] Can manually retry failed notifications
docs/devlogs/browser/browserconsole.txt CHANGED
@@ -1,12 +1,95 @@
1
- index-CJi_XLkH.js:583 ℹ️ [05:58:31] [AUTH] Token expired, attempting refresh
2
- index-CJi_XLkH.js:583 ℹ️ [05:58:35] [AUTH] Token refreshed successfully
3
- index-CJi_XLkH.js:583 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/auth/me
4
- index-CJi_XLkH.js:583 GET https://kamau1-swiftops-backend.hf.space/api/v1/auth/me β†’ 200 (845ms)
5
- index-CJi_XLkH.js:583 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/tickets/1f807cf8-f139-421b-86e3-38c2f8bc7070/detail
6
- index-CJi_XLkH.js:583 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences
7
- index-CJi_XLkH.js:583 GET https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences β†’ 200 (931ms)
8
- kamau1-swiftops-backend.hf.space/api/v1/tickets/1f807cf8-f139-421b-86e3-38c2f8bc7070/detail:1 Failed to load resource: the server responded with a status of 500 ()
9
- index-CJi_XLkH.js:583 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets/1f807cf8-f139-421b-86e3-38c2f8bc7070/detail β†’ 500 (972ms)
10
- index-CJi_XLkH.js:583 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/tickets/1f807cf8-f139-421b-86e3-38c2f8bc7070/detail
11
- kamau1-swiftops-backend.hf.space/api/v1/tickets/1f807cf8-f139-421b-86e3-38c2f8bc7070/detail:1 Failed to load resource: the server responded with a status of 500 ()
12
- index-CJi_XLkH.js:583 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets/1f807cf8-f139-421b-86e3-38c2f8bc7070/detail β†’ 500 (1.11s)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chunk-276SZO74.js?v=c40a0948:21551 Download the React DevTools for a better development experience: https://reactjs.org/link/react-devtools
2
+ react-router-dom.js?v=c40a0948:4393 ⚠️ React Router Future Flag Warning: React Router will begin wrapping state updates in `React.startTransition` in v7. You can use the `v7_startTransition` future flag to opt-in early. For more information, see https://reactrouter.com/v6/upgrading/future#v7_starttransition.
3
+ warnOnce @ react-router-dom.js?v=c40a0948:4393
4
+ logDeprecation @ react-router-dom.js?v=c40a0948:4396
5
+ logV6DeprecationWarnings @ react-router-dom.js?v=c40a0948:4399
6
+ (anonymous) @ react-router-dom.js?v=c40a0948:5271
7
+ commitHookEffectListMount @ chunk-276SZO74.js?v=c40a0948:16915
8
+ commitPassiveMountOnFiber @ chunk-276SZO74.js?v=c40a0948:18156
9
+ commitPassiveMountEffects_complete @ chunk-276SZO74.js?v=c40a0948:18129
10
+ commitPassiveMountEffects_begin @ chunk-276SZO74.js?v=c40a0948:18119
11
+ commitPassiveMountEffects @ chunk-276SZO74.js?v=c40a0948:18109
12
+ flushPassiveEffectsImpl @ chunk-276SZO74.js?v=c40a0948:19490
13
+ flushPassiveEffects @ chunk-276SZO74.js?v=c40a0948:19447
14
+ (anonymous) @ chunk-276SZO74.js?v=c40a0948:19328
15
+ workLoop @ chunk-276SZO74.js?v=c40a0948:197
16
+ flushWork @ chunk-276SZO74.js?v=c40a0948:176
17
+ performWorkUntilDeadline @ chunk-276SZO74.js?v=c40a0948:384
18
+ react-router-dom.js?v=c40a0948:4393 ⚠️ React Router Future Flag Warning: Relative route resolution within Splat routes is changing in v7. You can use the `v7_relativeSplatPath` future flag to opt-in early. For more information, see https://reactrouter.com/v6/upgrading/future#v7_relativesplatpath.
19
+ warnOnce @ react-router-dom.js?v=c40a0948:4393
20
+ logDeprecation @ react-router-dom.js?v=c40a0948:4396
21
+ logV6DeprecationWarnings @ react-router-dom.js?v=c40a0948:4402
22
+ (anonymous) @ react-router-dom.js?v=c40a0948:5271
23
+ commitHookEffectListMount @ chunk-276SZO74.js?v=c40a0948:16915
24
+ commitPassiveMountOnFiber @ chunk-276SZO74.js?v=c40a0948:18156
25
+ commitPassiveMountEffects_complete @ chunk-276SZO74.js?v=c40a0948:18129
26
+ commitPassiveMountEffects_begin @ chunk-276SZO74.js?v=c40a0948:18119
27
+ commitPassiveMountEffects @ chunk-276SZO74.js?v=c40a0948:18109
28
+ flushPassiveEffectsImpl @ chunk-276SZO74.js?v=c40a0948:19490
29
+ flushPassiveEffects @ chunk-276SZO74.js?v=c40a0948:19447
30
+ (anonymous) @ chunk-276SZO74.js?v=c40a0948:19328
31
+ workLoop @ chunk-276SZO74.js?v=c40a0948:197
32
+ flushWork @ chunk-276SZO74.js?v=c40a0948:176
33
+ performWorkUntilDeadline @ chunk-276SZO74.js?v=c40a0948:384
34
+ core.ts:117 ℹ️ [09:56:24] [AUTH] Login attempt {email: 'viyisa8151@feralrex.com'}
35
+ core.ts:167 %cPOST%c https://kamau1-swiftops-backend.hf.space/api/v1/auth/login
36
+ auth.service.ts:152 POST https://kamau1-swiftops-backend.hf.space/api/v1/auth/login 401 (Unauthorized)
37
+ login @ auth.service.ts:152
38
+ handleSubmit @ LoginPage.tsx:47
39
+ callCallback2 @ chunk-276SZO74.js?v=c40a0948:3674
40
+ invokeGuardedCallbackDev @ chunk-276SZO74.js?v=c40a0948:3699
41
+ invokeGuardedCallback @ chunk-276SZO74.js?v=c40a0948:3733
42
+ invokeGuardedCallbackAndCatchFirstError @ chunk-276SZO74.js?v=c40a0948:3736
43
+ executeDispatch @ chunk-276SZO74.js?v=c40a0948:7014
44
+ processDispatchQueueItemsInOrder @ chunk-276SZO74.js?v=c40a0948:7034
45
+ processDispatchQueue @ chunk-276SZO74.js?v=c40a0948:7043
46
+ dispatchEventsForPlugins @ chunk-276SZO74.js?v=c40a0948:7051
47
+ (anonymous) @ chunk-276SZO74.js?v=c40a0948:7174
48
+ batchedUpdates$1 @ chunk-276SZO74.js?v=c40a0948:18913
49
+ batchedUpdates @ chunk-276SZO74.js?v=c40a0948:3579
50
+ dispatchEventForPluginEventSystem @ chunk-276SZO74.js?v=c40a0948:7173
51
+ dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-276SZO74.js?v=c40a0948:5478
52
+ dispatchEvent @ chunk-276SZO74.js?v=c40a0948:5472
53
+ dispatchDiscreteEvent @ chunk-276SZO74.js?v=c40a0948:5449
54
+ core.ts:167 POST https://kamau1-swiftops-backend.hf.space/api/v1/auth/login β†’ 401 (1.74s)
55
+ core.ts:117 ❌ [09:56:26] [AUTH] Login failed {email: 'viyisa8151@feralrex.com', error: {…}}
56
+ log @ core.ts:117
57
+ error @ core.ts:157
58
+ login @ auth.service.ts:164
59
+ await in login
60
+ handleSubmit @ LoginPage.tsx:47
61
+ callCallback2 @ chunk-276SZO74.js?v=c40a0948:3674
62
+ invokeGuardedCallbackDev @ chunk-276SZO74.js?v=c40a0948:3699
63
+ invokeGuardedCallback @ chunk-276SZO74.js?v=c40a0948:3733
64
+ invokeGuardedCallbackAndCatchFirstError @ chunk-276SZO74.js?v=c40a0948:3736
65
+ executeDispatch @ chunk-276SZO74.js?v=c40a0948:7014
66
+ processDispatchQueueItemsInOrder @ chunk-276SZO74.js?v=c40a0948:7034
67
+ processDispatchQueue @ chunk-276SZO74.js?v=c40a0948:7043
68
+ dispatchEventsForPlugins @ chunk-276SZO74.js?v=c40a0948:7051
69
+ (anonymous) @ chunk-276SZO74.js?v=c40a0948:7174
70
+ batchedUpdates$1 @ chunk-276SZO74.js?v=c40a0948:18913
71
+ batchedUpdates @ chunk-276SZO74.js?v=c40a0948:3579
72
+ dispatchEventForPluginEventSystem @ chunk-276SZO74.js?v=c40a0948:7173
73
+ dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-276SZO74.js?v=c40a0948:5478
74
+ dispatchEvent @ chunk-276SZO74.js?v=c40a0948:5472
75
+ dispatchDiscreteEvent @ chunk-276SZO74.js?v=c40a0948:5449
76
+ LoginPage.tsx:55 Login error: Error: Incorrect email or password
77
+ at AuthService.login (auth.service.ts:163:21)
78
+ at async handleSubmit (LoginPage.tsx:47:7)
79
+ handleSubmit @ LoginPage.tsx:55
80
+ await in handleSubmit
81
+ callCallback2 @ chunk-276SZO74.js?v=c40a0948:3674
82
+ invokeGuardedCallbackDev @ chunk-276SZO74.js?v=c40a0948:3699
83
+ invokeGuardedCallback @ chunk-276SZO74.js?v=c40a0948:3733
84
+ invokeGuardedCallbackAndCatchFirstError @ chunk-276SZO74.js?v=c40a0948:3736
85
+ executeDispatch @ chunk-276SZO74.js?v=c40a0948:7014
86
+ processDispatchQueueItemsInOrder @ chunk-276SZO74.js?v=c40a0948:7034
87
+ processDispatchQueue @ chunk-276SZO74.js?v=c40a0948:7043
88
+ dispatchEventsForPlugins @ chunk-276SZO74.js?v=c40a0948:7051
89
+ (anonymous) @ chunk-276SZO74.js?v=c40a0948:7174
90
+ batchedUpdates$1 @ chunk-276SZO74.js?v=c40a0948:18913
91
+ batchedUpdates @ chunk-276SZO74.js?v=c40a0948:3579
92
+ dispatchEventForPluginEventSystem @ chunk-276SZO74.js?v=c40a0948:7173
93
+ dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-276SZO74.js?v=c40a0948:5478
94
+ dispatchEvent @ chunk-276SZO74.js?v=c40a0948:5472
95
+ dispatchDiscreteEvent @ chunk-276SZO74.js?v=c40a0948:5449
docs/devlogs/server/runtimeerror.txt CHANGED
@@ -1,58 +1,65 @@
1
- ===== Application Startup at 2025-11-28 09:27:13 =====
2
 
3
- INFO: Started server process [7]
4
  INFO: Waiting for application startup.
5
- INFO: 2025-11-28T09:27:27 - app.main: ============================================================
6
- INFO: 2025-11-28T09:27:27 - app.main: πŸš€ SwiftOps API v1.0.0 | PRODUCTION
7
- INFO: 2025-11-28T09:27:27 - app.main: πŸ“Š Dashboard: Enabled
8
- INFO: 2025-11-28T09:27:27 - app.main: ============================================================
9
- INFO: 2025-11-28T09:27:27 - app.main: πŸ“¦ Database:
10
- INFO: 2025-11-28T09:27:28 - app.main: βœ“ Connected | 44 tables | 6 users
11
- INFO: 2025-11-28T09:27:28 - app.main: πŸ’Ύ Cache & Sessions:
12
- INFO: 2025-11-28T09:27:29 - app.services.otp_service: βœ… OTP Service initialized with Redis storage
13
- INFO: 2025-11-28T09:27:29 - app.main: βœ“ Redis: Connected
14
- INFO: 2025-11-28T09:27:29 - app.main: πŸ”Œ External Services:
15
- INFO: 2025-11-28T09:27:30 - app.main: βœ“ Cloudinary: Connected
16
- INFO: 2025-11-28T09:27:30 - app.main: βœ“ Resend: Configured
17
- INFO: 2025-11-28T09:27:30 - app.main: β—‹ WASender: Failed
18
- INFO: 2025-11-28T09:27:30 - app.main: βœ“ Supabase: Connected | 6 buckets
19
- INFO: 2025-11-28T09:27:30 - app.main: ============================================================
20
- INFO: 2025-11-28T09:27:30 - app.main: βœ… Startup complete | Ready to serve requests
21
- INFO: 2025-11-28T09:27:30 - 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.6.70:2924 - "GET /health HTTP/1.1" 200 OK
25
- INFO: 10.16.6.70:2924 - "GET /?logs=container HTTP/1.1" 200 OK
26
- INFO: 10.16.34.155:6801 - "GET /health HTTP/1.1" 200 OK
27
- INFO: 2025-11-28T09:27:38 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
28
- INFO: 2025-11-28T09:27:38 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
29
- INFO: 10.16.18.114:11421 - "GET /api/v1/auth/me HTTP/1.1" 200 OK
30
- INFO: 2025-11-28T09:27:38 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
31
- INFO: 2025-11-28T09:27:38 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
32
- INFO: 10.16.18.114:11421 - "GET /api/v1/auth/me/preferences HTTP/1.1" 200 OK
33
- INFO: 2025-11-28T09:27:38 - app.api.deps: Checking active user: 43b778b0-2062-4724-abbb-916a4835a9b0, is_active: True, type: <class 'bool'>
34
- INFO: 2025-11-28T09:27:38 - app.api.deps: User 43b778b0-2062-4724-abbb-916a4835a9b0 is active - proceeding
35
- INFO: 10.16.11.176:30663 - "GET /api/v1/auth/me/preferences/available-apps HTTP/1.1" 200 OK
36
- INFO: 10.16.11.176:49895 - "GET /api/v1/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/detail HTTP/1.1" 200 OK
37
- ERROR: 2025-11-28T09:27:42 - app.services.ticket_assignment_service: Failed to send self-assignment notification: no running event loop
38
- /app/src/app/services/ticket_assignment_service.py:437: RuntimeWarning: coroutine 'NotificationHelper.notify_ticket_status_changed' was never awaited
39
- logger.error(f"Failed to send self-assignment notification: {str(e)}")
40
- RuntimeWarning: Enable tracemalloc to get the object allocation traceback
41
- INFO: 10.16.6.70:17030 - "POST /api/v1/ticket-assignments/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/self-assign HTTP/1.1" 201 Created
42
- INFO: 10.16.6.70:17030 - "GET /api/v1/tickets/8f08ad14-df8b-4780-84e7-0d45e133f2a6/detail HTTP/1.1" 200 OK
43
- INFO: 10.16.11.176:23737 - "GET /health HTTP/1.1" 200 OK
44
- INFO: 10.16.11.176:45766 - "GET /health HTTP/1.1" 200 OK
45
- INFO: 10.16.34.155:20217 - "GET /health HTTP/1.1" 200 OK
46
- INFO: 10.16.6.70:34100 - "GET /health HTTP/1.1" 200 OK
47
- INFO: 10.16.34.155:41867 - "GET /health HTTP/1.1" 200 OK
48
- INFO: 10.16.6.70:1574 - "GET /health HTTP/1.1" 200 OK
49
- INFO: 10.16.11.176:16400 - "GET /health HTTP/1.1" 200 OK
50
- INFO: 10.16.11.176:63121 - "GET /health HTTP/1.1" 200 OK
51
- INFO: 10.16.34.155:62014 - "GET /health HTTP/1.1" 200 OK
52
- INFO: 10.16.25.209:22027 - "GET /health HTTP/1.1" 200 OK
53
- INFO: 10.16.18.114:26652 - "GET /health HTTP/1.1" 200 OK
54
- INFO: 10.16.18.114:29820 - "GET /health HTTP/1.1" 200 OK
55
- INFO: 10.16.25.209:44298 - "GET /health HTTP/1.1" 200 OK
56
- INFO: 10.16.18.114:22934 - "GET /health HTTP/1.1" 200 OK
57
- INFO: 10.16.6.70:44658 - "GET /health HTTP/1.1" 200 OK
58
- INFO: 10.16.18.114:28198 - "GET /health HTTP/1.1" 200 OK
 
 
 
 
 
 
 
 
1
+ ===== Application Startup at 2025-11-28 09:42:05 =====
2
 
3
+ INFO: Started server process [6]
4
  INFO: Waiting for application startup.
5
+ INFO: 2025-11-28T09:42:30 - app.main: ============================================================
6
+ INFO: 2025-11-28T09:42:30 - app.main: πŸš€ SwiftOps API v1.0.0 | PRODUCTION
7
+ INFO: 2025-11-28T09:42:30 - app.main: πŸ“Š Dashboard: Enabled
8
+ INFO: 2025-11-28T09:42:30 - app.main: ============================================================
9
+ INFO: 2025-11-28T09:42:30 - app.main: πŸ“¦ Database:
10
+ INFO: 2025-11-28T09:42:31 - app.main: βœ“ Connected | 44 tables | 6 users
11
+ INFO: 2025-11-28T09:42:31 - app.main: πŸ’Ύ Cache & Sessions:
12
+ INFO: 2025-11-28T09:42:32 - app.services.otp_service: βœ… OTP Service initialized with Redis storage
13
+ INFO: 2025-11-28T09:42:32 - app.main: βœ“ Redis: Connected
14
+ INFO: 2025-11-28T09:42:32 - app.main: πŸ”Œ External Services:
15
+ INFO: 2025-11-28T09:42:33 - app.main: βœ“ Cloudinary: Connected
16
+ INFO: 2025-11-28T09:42:33 - app.main: βœ“ Resend: Configured
17
+ INFO: 2025-11-28T09:42:33 - app.main: β—‹ WASender: Failed
18
+ INFO: 2025-11-28T09:42:33 - app.main: βœ“ Supabase: Connected | 6 buckets
19
+ INFO: 2025-11-28T09:42:33 - app.main: ============================================================
20
+ INFO: 2025-11-28T09:42:33 - app.main: βœ… Startup complete | Ready to serve requests
21
+ INFO: 2025-11-28T09:42:33 - 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.6.70:5079 - "GET /health HTTP/1.1" 200 OK
25
+ INFO: 10.16.6.70:60771 - "GET /health HTTP/1.1" 200 OK
26
+ INFO: 10.16.34.155:15030 - "GET /health HTTP/1.1" 200 OK
27
+ INFO: 10.16.11.176:52821 - "GET /health HTTP/1.1" 200 OK
28
+ INFO: 10.16.6.70:34619 - "GET /health HTTP/1.1" 200 OK
29
+ INFO: 10.16.6.70:34619 - "GET /health HTTP/1.1" 200 OK
30
+ INFO: 2025-11-28T09:45:12 - app.core.supabase_auth: Session refreshed successfully
31
+ ERROR: 2025-11-28T09:45:12 - app.api.v1.auth: ❌ Token refresh error: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
32
+ INFO: 10.16.11.176:26415 - "POST /api/v1/auth/refresh-token HTTP/1.1" 401 Unauthorized
33
+ INFO: 10.16.6.70:60338 - "GET /health HTTP/1.1" 200 OK
34
+ INFO: 10.16.34.155:30463 - "GET /health HTTP/1.1" 200 OK
35
+ INFO: 10.16.11.176:52829 - "GET /health HTTP/1.1" 200 OK
36
+ INFO: 10.16.34.155:60696 - "GET /health HTTP/1.1" 200 OK
37
+ INFO: 10.16.18.114:56609 - "GET /health HTTP/1.1" 200 OK
38
+ INFO: 10.16.11.176:4192 - "GET /health HTTP/1.1" 200 OK
39
+ INFO: 10.16.34.155:2023 - "GET /health HTTP/1.1" 200 OK
40
+ INFO: 10.16.11.176:27097 - "GET /health HTTP/1.1" 200 OK
41
+ INFO: 2025-11-28T09:48:41 - app.core.supabase_auth: Session refreshed successfully
42
+ ERROR: 2025-11-28T09:48:41 - app.api.v1.auth: ❌ Token refresh error: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper[Ticket(tickets)]'. Original exception was: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
43
+ INFO: 10.16.34.155:59759 - "POST /api/v1/auth/refresh-token HTTP/1.1" 401 Unauthorized
44
+ INFO: 10.16.11.176:9673 - "GET /health HTTP/1.1" 200 OK
45
+ INFO: 10.16.11.176:9673 - "GET /health HTTP/1.1" 200 OK
46
+ INFO: 10.16.18.114:33528 - "GET /health HTTP/1.1" 200 OK
47
+ INFO: 10.16.34.155:53346 - "GET /health HTTP/1.1" 200 OK
48
+ INFO: 10.16.6.70:52356 - "GET /health HTTP/1.1" 200 OK
49
+ INFO: 10.16.34.155:32440 - "GET /health HTTP/1.1" 200 OK
50
+ INFO: 10.16.6.70:25171 - "GET /health HTTP/1.1" 200 OK
51
+ INFO: 10.16.6.70:25171 - "GET /health HTTP/1.1" 200 OK
52
+ INFO: 10.16.34.155:47092 - "GET /health HTTP/1.1" 200 OK
53
+ INFO: 10.16.11.176:8190 - "GET /health HTTP/1.1" 200 OK
54
+ INFO: 10.16.0.39:65512 - "GET /health HTTP/1.1" 200 OK
55
+ INFO: 10.16.0.39:10423 - "GET /health HTTP/1.1" 200 OK
56
+ INFO: 10.16.18.114:32065 - "GET /health HTTP/1.1" 200 OK
57
+ INFO: 10.16.18.114:9598 - "GET /health HTTP/1.1" 200 OK
58
+ INFO: 10.16.11.176:17205 - "GET /health HTTP/1.1" 200 OK
59
+ INFO: 10.16.11.176:27035 - "GET /health HTTP/1.1" 200 OK
60
+ INFO: 10.16.6.70:62059 - "GET /health HTTP/1.1" 200 OK
61
+ INFO: 2025-11-28T09:56:23 - app.core.supabase_auth: User signed in successfully: viyisa8151@feralrex.com
62
+ ERROR: 2025-11-28T09:56:23 - app.api.v1.auth: Login error: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper[Ticket(tickets)]'. Original exception was: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
63
+ ERROR: 2025-11-28T09:56:23 - app.services.audit_service: Failed to create audit log: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper[Ticket(tickets)]'. Original exception was: When initializing mapper Mapper[Ticket(tickets)], expression 'TicketStatusHistory' failed to locate a name ('TicketStatusHistory'). If this is a class name, consider adding this relationship() to the <class 'app.models.ticket.Ticket'> class after both dependent classes have been defined.
64
+ INFO: 10.16.18.114:33294 - "POST /api/v1/auth/login HTTP/1.1" 401 Unauthorized
65
+
src/app/models/__init__.py CHANGED
@@ -44,6 +44,7 @@ from app.models.ticket_expense import TicketExpense
44
  from app.models.ticket_image import TicketImage
45
  from app.models.ticket_progress_report import TicketProgressReport
46
  from app.models.ticket_incident_report import TicketIncidentReport
 
47
  from app.models.ticket import Ticket
48
  from app.models.ticket_assignment import TicketAssignment
49
 
@@ -109,6 +110,7 @@ __all__ = [
109
  "TicketImage",
110
  "TicketProgressReport",
111
  "TicketIncidentReport",
 
112
 
113
  # Incidents
114
  "Incident",
 
44
  from app.models.ticket_image import TicketImage
45
  from app.models.ticket_progress_report import TicketProgressReport
46
  from app.models.ticket_incident_report import TicketIncidentReport
47
+ from app.models.ticket_status_history import TicketStatusHistory
48
  from app.models.ticket import Ticket
49
  from app.models.ticket_assignment import TicketAssignment
50
 
 
110
  "TicketImage",
111
  "TicketProgressReport",
112
  "TicketIncidentReport",
113
+ "TicketStatusHistory",
114
 
115
  # Incidents
116
  "Incident",