teoat commited on
Commit
a1df533
·
verified ·
1 Parent(s): 87a43ef

Upload app/routers/cases.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app/routers/cases.py +3 -564
app/routers/cases.py CHANGED
@@ -1,565 +1,4 @@
1
- import logging
2
- import uuid
3
- from datetime import UTC, datetime
4
- from typing import Any
5
 
6
- from fastapi import APIRouter, Body, Depends, HTTPException, Query
7
- from pydantic import BaseModel, Field
8
- from sqlalchemy.orm import Session
9
-
10
- from app.dependencies import get_current_project_id
11
- from app.services.infrastructure.auth_service import auth_service
12
- from app.services.infrastructure.storage.database_service import db_service
13
- from core.database import get_db
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
- # router defined below near line 113
18
-
19
- # Clean dependency injection - services are imported at module level
20
- # No test placeholders needed with proper service architecture
21
-
22
- # ===== REQUEST/RESPONSE MODELS =====
23
-
24
-
25
- # ===== REQUEST/RESPONSE MODELS =====
26
-
27
-
28
- class CaseCreate(BaseModel):
29
- title: str = Field( min_length=1, max_length=200)
30
- description: str | None = Field(None, max_length=2000)
31
- priority: str | None = Field("Medium", pattern=r"^(Low|Medium|High|Critical)$")
32
- assignee_id: str | None = None
33
- tags: list[str] | None = Field(default_factory=list, max_items=20)
34
-
35
-
36
- class CaseUpdate(BaseModel):
37
- title: str | None = Field(None, min_length=1, max_length=200)
38
- description: str | None = Field(None, max_length=2000)
39
- priority: str | None = Field(None, pattern=r"^(Low|Medium|High|Critical)$")
40
- assignee_id: str | None = None
41
- tags: list[str] | None = Field(None, max_items=20)
42
-
43
-
44
- class CaseResponse(BaseModel):
45
- id: str
46
- case_id: str
47
- title: str
48
- description: str | None = None
49
- status: str
50
- priority: str
51
- assignee_id: str | None = None
52
- risk_score: float | None = 0.0
53
- risk_level: str | None = "low"
54
- fraud_amount: float | None = 0.0
55
- customer_name: str | None = "Unknown"
56
- created_at: datetime
57
- updated_at: datetime | None = None
58
- due_date: datetime | None = None
59
- tags: list[str] = Field(default_factory=list)
60
-
61
-
62
- class CaseCreateResponse(BaseModel):
63
- id: str
64
- case_id: str
65
- message: str
66
- case: dict[str, Any]
67
-
68
-
69
- class CaseListResponse(BaseModel):
70
- cases: list[CaseResponse]
71
- page: int
72
- per_page: int
73
- total: int
74
- total_pages: int
75
-
76
-
77
- class CaseNoteCreate(BaseModel):
78
- content: str = Field( min_length=1, max_length=2000)
79
- is_internal: bool = False
80
- category: str | None = Field(None, pattern=r"^(Investigation|Evidence|Analysis|Communication)$")
81
-
82
-
83
- class CaseNoteResponse(BaseModel):
84
- id: str
85
- content: str
86
- author_id: str
87
- author_name: str
88
- is_internal: bool
89
- category: str | None
90
- created_at: datetime
91
-
92
-
93
- class BulkDeleteRequest(BaseModel):
94
- case_ids: list[str] = Field( min_items=1, max_items=100)
95
-
96
-
97
- class BulkDeleteResponse(BaseModel):
98
- deleted_count: int
99
- failed_ids: list[str]
100
- message: str
101
- selected_country: str | None = None
102
- selected_documents: list[str] | None = []
103
- reconciliation_type: str | None = "general"
104
- selected_calendar_format: str | None = "gregory"
105
- selected_currency_format: str | None = "USD"
106
- selected_decimal_format: str | None = "standard"
107
- milestones: list[str] | None = []
108
- proposed_features: list[str] | None = []
109
- status: str | None = "OPEN"
110
- fraud_amount: float | None = 0.0
111
- customer_name: str | None = "Unknown"
112
-
113
-
114
- router = APIRouter()
115
-
116
- # ===== CASE MANAGEMENT ENDPOINTS =====
117
-
118
-
119
- @router.post("", response_model=CaseCreateResponse, status_code=201)
120
- async def create_case(
121
- case_data: CaseCreate,
122
- current_user: dict = Depends(auth_service.get_current_user),
123
- db: Session = Depends(get_db),
124
- project_id: str = Depends(get_current_project_id),
125
- ):
126
- """Create a new case"""
127
- try:
128
- # Prepare metadata for fields not in standard columns
129
- case_metadata = {
130
- "selected_country": getattr(case_data, "selected_country", None),
131
- "selected_documents": getattr(case_data, "selected_documents", []),
132
- "reconciliation_type": getattr(case_data, "reconciliation_type", "general"),
133
- "selected_calendar_format": getattr(case_data, "selected_calendar_format", "gregory"),
134
- "selected_currency_format": getattr(case_data, "selected_currency_format", "USD"),
135
- "selected_decimal_format": getattr(case_data, "selected_decimal_format", "standard"),
136
- "milestones": getattr(case_data, "milestones", []),
137
- "proposed_features": getattr(case_data, "proposed_features", []),
138
- }
139
-
140
- # Creates persistence call
141
- new_case = db_service.create_case(
142
- db,
143
- id=str(uuid.uuid4()),
144
- title=case_data.title,
145
- description=case_data.description,
146
- priority=case_data.priority.lower() if case_data.priority else "medium",
147
- status="open",
148
- fraud_amount=0.0,
149
- tags=case_data.tags or [],
150
- case_metadata=case_metadata,
151
- project_id=project_id,
152
- )
153
-
154
- return {
155
- "id": new_case.id,
156
- "case_id": new_case.id,
157
- "message": "Case created successfully",
158
- "case": {
159
- "id": new_case.id,
160
- "title": new_case.title,
161
- "status": new_case.status,
162
- "priority": new_case.priority,
163
- "fraud_amount": getattr(new_case, "fraud_amount", 0.0),
164
- "customer_name": getattr(new_case, "customer_name", "Unknown"),
165
- "created_at": new_case.created_at.isoformat() if new_case.created_at else None,
166
- },
167
- }
168
- except Exception as e:
169
- logger.error(f"Error creating case: {e}")
170
- raise HTTPException(status_code=500, detail=str(e))
171
-
172
-
173
- # Backwards-compatible root endpoints (tests may call the router root `/` under
174
- # the `/api/v1/cases` prefix). Register the same handlers at `/` so both
175
- # `/api/v1/cases/` and `/api/v1/cases/cases` work.
176
- router.post("/", status_code=201)
177
-
178
-
179
- async def create_case_root(case: CaseCreate):
180
- return await create_case(case)
181
-
182
-
183
- @router.get("", response_model=CaseListResponse)
184
- async def get_cases(
185
- page: int = Query(1, ge=1, le=1000),
186
- per_page: int = Query(20, ge=1, le=100),
187
- search: str | None = Query(None, min_length=1, max_length=100),
188
- status: str | None = Query(None, pattern=r"^(OPEN|INVESTIGATING|PENDING_REVIEW|ESCALATED|CLOSED|ARCHIVED)$"),
189
- assignee_id: str | None = Query(None),
190
- priority: str | None = Query(None, pattern=r"^(Low|Medium|High|Critical)$"),
191
- risk_level: str | None = Query(None, pattern=r"^(Low|Medium|High|Critical)$"),
192
- current_user: dict = Depends(auth_service.get_current_user),
193
- db: Session = Depends(get_db),
194
- project_id: str = Depends(get_current_project_id),
195
- ):
196
- """
197
- Get a paginated list of cases with optional filtering.
198
- """
199
- try:
200
- # Normalize filters
201
- if status:
202
- status = status.lower()
203
- if priority:
204
- priority = priority.lower()
205
-
206
- filters = {
207
- "status": status,
208
- "priority": priority,
209
- "search": search,
210
- "project_id": project_id,
211
- }
212
- result = db_service.get_cases_paginated(db, page, per_page, filters)
213
-
214
- # Convert rows to dicts with camelCase keys for frontend compatibility
215
- cases_data = []
216
- for row in result["cases"]:
217
- cases_data.append(
218
- {
219
- "id": row.id,
220
- "title": row.title,
221
- "description": row.description,
222
- "status": row.status,
223
- "type": row.case_type,
224
- "assignee_id": row.assignee_id,
225
- "risk_score": row.risk_score or 0,
226
- "risk_level": getattr(row, "risk_level", "low"),
227
- "fraud_amount": getattr(row, "fraud_amount", 0.0),
228
- "customer_name": getattr(row, "customer_name", "Unknown"),
229
- "created_at": row.created_at.isoformat() if row.created_at else None,
230
- "updated_at": row.updated_at.isoformat() if row.updated_at else None,
231
- "due_date": getattr(row, "due_date", None).isoformat() if getattr(row, "due_date", None) else None,
232
- "tags": row.tags if hasattr(row, "tags") else [],
233
- }
234
- )
235
-
236
- return {
237
- "cases": cases_data,
238
- "items": cases_data,
239
- "page": page,
240
- "per_page": per_page,
241
- "total": result["total"],
242
- "total_count": result["total"],
243
- "total_pages": result["total_pages"],
244
- }
245
- except Exception as e:
246
- logger.error(f"Error listing cases: {e}")
247
- raise HTTPException(status_code=500, detail=str(e))
248
-
249
-
250
- @router.get("/search")
251
- async def search_cases(
252
- q: str,
253
- status: str | None = None,
254
- priority: str | None = None,
255
- db: Session = Depends(get_db),
256
- current_user: dict = Depends(auth_service.get_current_user),
257
- project_id: str = Depends(get_current_project_id),
258
- ):
259
- """Specific search endpoint for cases"""
260
- return await get_cases(
261
- search=q,
262
- status=status,
263
- priority=priority,
264
- db=db,
265
- current_user=current_user,
266
- project_id=project_id,
267
- page=1,
268
- per_page=20,
269
- )
270
-
271
-
272
- @router.get("/{case_id}", response_model=CaseResponse)
273
- async def get_case_detail(
274
- case_id: str,
275
- db: Session = Depends(get_db),
276
- current_user: dict = Depends(auth_service.get_current_user),
277
- ):
278
- """Get detailed information for a specific case"""
279
- try:
280
- from app.services.infrastructure.storage.database_service import db_service
281
-
282
- case = db_service.get_case(db, case_id)
283
- if not case:
284
- raise HTTPException(status_code=404, detail="Case not found")
285
-
286
- return {
287
- "id": case.id,
288
- "case_id": case.id,
289
- "title": case.title,
290
- "description": case.description,
291
- "status": case.status,
292
- "priority": case.priority,
293
- "assignee_id": case.assignee_id,
294
- "risk_score": getattr(case, "risk_score", 0),
295
- "risk_level": getattr(case, "risk_level", "low"),
296
- "fraud_amount": getattr(case, "fraud_amount", 0.0),
297
- "customer_name": getattr(case, "customer_name", "Unknown"),
298
- "created_at": case.created_at,
299
- "updated_at": case.updated_at,
300
- "due_date": getattr(case, "due_date", None),
301
- "tags": case.tags or [],
302
- }
303
- except HTTPException:
304
- raise
305
- except Exception as e:
306
- logger.error(f"Error getting case details: {e}")
307
- raise HTTPException(status_code=500, detail=str(e))
308
-
309
-
310
- @router.patch("/{case_id}")
311
- async def update_case_partial(
312
- case_id: str,
313
- updates: dict[str, Any] = Body(),
314
- db: Session = Depends(get_db),
315
- current_user: dict = Depends(auth_service.get_current_user),
316
- ):
317
- """Update general case details"""
318
- from app.services.infrastructure.storage.database_service import db_service
319
-
320
- case = db_service.update_case(db, case_id, **updates)
321
- if not case:
322
- raise HTTPException(status_code=404, detail="Case not found")
323
- return case
324
-
325
-
326
- @router.put("/{case_id}/status")
327
- async def update_case_status(
328
- case_id: str,
329
- status_data: dict[str, str] = Body(),
330
- db: Session = Depends(get_db),
331
- current_user: dict = Depends(auth_service.get_current_user),
332
- ):
333
- """Update case status specifically"""
334
- status = status_data.get("status")
335
- if not status:
336
- raise HTTPException(status_code=400, detail="Status is required")
337
-
338
- from app.services.infrastructure.storage.database_service import db_service
339
-
340
- case = db_service.update_case(db, case_id, status=status.lower())
341
- if not case:
342
- raise HTTPException(status_code=404, detail="Case not found")
343
- return case
344
-
345
-
346
- @router.get("/{case_id}/notes", response_model=list[CaseNoteResponse])
347
- async def get_case_notes(
348
- case_id: str,
349
- include_internal: bool = True,
350
- current_user: dict = Depends(auth_service.get_current_user),
351
- db: Session = Depends(get_db),
352
- ):
353
- """Get notes for a specific case"""
354
- from app.services.infrastructure.storage.database_service import db_service
355
-
356
- notes = db_service.get_case_notes(db, case_id, include_internal)
357
-
358
- # Map to response model
359
- response_notes = []
360
- for note in notes:
361
- # Get author name - simplistic for now, ideally join user table
362
- author_name = "Unknown"
363
- if note.created_by:
364
- user = db_service.get_user(db, note.created_by)
365
- if user:
366
- author_name = user.username
367
-
368
- response_notes.append({
369
- "id": note.id,
370
- "content": note.content,
371
- "author_id": note.created_by,
372
- "author_name": author_name,
373
- "is_internal": note.is_internal,
374
- "category": note.category,
375
- "created_at": note.created_at,
376
- })
377
- return response_notes
378
-
379
-
380
- @router.post("/{case_id}/notes", response_model=CaseNoteResponse, status_code=201)
381
- async def add_case_note(
382
- case_id: str,
383
- note_data: CaseNoteCreate,
384
- current_user: dict = Depends(auth_service.get_current_user),
385
- db: Session = Depends(get_db),
386
- ):
387
- """Add a note to a case"""
388
- from app.services.infrastructure.storage.database_service import db_service
389
-
390
- case = db_service.get_case(db, case_id)
391
- if not case:
392
- raise HTTPException(status_code=404, detail="Case not found")
393
-
394
- user_id = getattr(current_user, "id", None) or "system" # Fallback for safety
395
-
396
- note = db_service.add_case_note(
397
- db,
398
- {
399
- "case_id": case_id,
400
- "content": note_data.content,
401
- "created_by": user_id,
402
- "is_internal": note_data.is_internal,
403
- "category": note_data.category,
404
- "created_at": datetime.now(UTC),
405
- }
406
- )
407
-
408
- return {
409
- "id": note.id,
410
- "content": note.content,
411
- "author_id": note.created_by,
412
- "author_name": getattr(current_user, "username", "System"),
413
- "is_internal": note.is_internal,
414
- "category": note.category,
415
- "created_at": note.created_at,
416
- }
417
-
418
-
419
- @router.delete("/{case_id}/notes/{note_id}")
420
- async def delete_case_note(
421
- case_id: str,
422
- note_id: str,
423
- current_user: dict = Depends(auth_service.get_current_user),
424
- db: Session = Depends(get_db),
425
- ):
426
- """Delete a case note"""
427
- from app.services.infrastructure.storage.database_service import db_service
428
-
429
- # Verify case exists
430
- case = db_service.get_case(db, case_id)
431
- if not case:
432
- raise HTTPException(status_code=404, detail="Case not found")
433
-
434
- success = db_service.delete_case_note(db, note_id)
435
- if not success:
436
- raise HTTPException(status_code=404, detail="Note not found")
437
-
438
- return {"status": "success", "message": "Note deleted"}
439
-
440
-
441
- @router.post("/{case_id}/close")
442
- async def close_case(
443
- case_id: str,
444
- close_data: dict[str, Any] = Body(),
445
- db: Session = Depends(get_db),
446
- current_user: dict = Depends(auth_service.get_current_user),
447
- ):
448
- """Close a case"""
449
- from app.services.infrastructure.storage.database_service import db_service
450
-
451
- case = db_service.update_case(db, case_id, status="closed", closed_at=datetime.now(UTC))
452
- if not case:
453
- raise HTTPException(status_code=404, detail="Case not found")
454
- return {
455
- "status": "CLOSED",
456
- "case_id": case_id,
457
- "resolution": close_data.get("resolution"),
458
- }
459
-
460
-
461
- @router.put("/{case_id}", response_model=CaseResponse)
462
- async def update_case(
463
- case_id: str,
464
- update_data: dict[str, Any] = Body(),
465
- db: Session = Depends(get_db),
466
- current_user: dict = Depends(auth_service.get_current_user),
467
- ):
468
- """Update a case"""
469
- try:
470
- from app.services.infrastructure.storage.database_service import db_service
471
-
472
- # Map camelCase inputs to snake_case for DB
473
- mapped_data = {}
474
- for k, v in update_data.items():
475
- if k == "type":
476
- mapped_data["case_type"] = v
477
- elif k == "assignee_id":
478
- mapped_data["assignee_id"] = v
479
- elif k == "risk_score":
480
- mapped_data["risk_score"] = v
481
- elif k == "fraud_amount":
482
- mapped_data["fraud_amount"] = v
483
- elif k == "customer_name":
484
- mapped_data["customer_name"] = v
485
- elif k == "due_date":
486
- mapped_data["due_date"] = v
487
- else:
488
- # Keep other keys as is or map if needed
489
- mapped_data[k] = v
490
-
491
- case = db_service.update_case(db, case_id, **mapped_data)
492
- if not case:
493
- raise HTTPException(status_code=404, detail="Case not found")
494
-
495
- return {
496
- "id": case.id,
497
- "title": case.title,
498
- "status": case.status,
499
- "priority": case.priority,
500
- "fraud_amount": getattr(case, "fraud_amount", 0.0),
501
- "customer_name": getattr(case, "customer_name", "Unknown"),
502
- "created_at": case.created_at,
503
- "updated_at": case.updated_at,
504
- "tags": case.tags or [],
505
- }
506
- except Exception as e:
507
- logger.error(f"Error updating case: {e}")
508
- raise HTTPException(status_code=500, detail=str(e))
509
-
510
-
511
- @router.delete("/{case_id}")
512
- async def delete_case(
513
- case_id: str,
514
- db: Session = Depends(get_db),
515
- current_user: dict = Depends(auth_service.get_current_user),
516
- ):
517
- """Delete a case"""
518
- try:
519
- from app.services.infrastructure.storage.database_service import db_service
520
-
521
- success = db_service.delete_case(db, case_id)
522
- if not success:
523
- raise HTTPException(status_code=404, detail="Case not found")
524
-
525
- return {"message": "Case deleted successfully"}
526
- except Exception as e:
527
- logger.error(f"Error deleting case: {e}")
528
- raise HTTPException(status_code=500, detail=str(e))
529
-
530
-
531
- @router.post("/bulk-delete", response_model=BulkDeleteResponse)
532
- async def bulk_delete_cases(
533
- request: BulkDeleteRequest,
534
- db: Session = Depends(get_db),
535
- current_user: dict = Depends(auth_service.get_current_user),
536
- ):
537
- """Bulk delete cases"""
538
- try:
539
- case_ids = request.case_ids
540
- if not case_ids:
541
- return BulkDeleteResponse(deleted_count=0, failed_ids=[], message="No cases specified for deletion")
542
-
543
- deleted_count = 0
544
- failed_ids = []
545
-
546
- from app.services.infrastructure.storage.database_service import db_service
547
-
548
- for case_id in case_ids:
549
- try:
550
- if db_service.delete_case(db, case_id):
551
- deleted_count += 1
552
- else:
553
- failed_ids.append(case_id)
554
- except Exception as e:
555
- logger.warning(f"Failed to delete case {case_id}: {e}")
556
- failed_ids.append(case_id)
557
-
558
- message = f"Successfully deleted {deleted_count} cases"
559
- if failed_ids:
560
- message += f", {len(failed_ids)} failed"
561
-
562
- return BulkDeleteResponse(deleted_count=deleted_count, failed_ids=failed_ids, message=message)
563
- except Exception as e:
564
- logger.error(f"Bulk delete cases failed: {e}")
565
- raise HTTPException(status_code=500, detail=str(e))
 
1
+ # SHIM: Redirects to new module location
2
+ # This file is maintained for backward compatibility.
3
+ # Please import from app.modules.cases.router instead.
 
4