Ali2206 commited on
Commit
ad4bc9c
·
verified ·
1 Parent(s): cacd13a

Update api/routes/patients.py

Browse files
Files changed (1) hide show
  1. api/routes/patients.py +375 -57
api/routes/patients.py CHANGED
@@ -1,88 +1,384 @@
1
- from fastapi import APIRouter, HTTPException, Depends, Query, status
2
  from db.mongo import patients_collection
3
  from core.security import get_current_user
4
- from utils.helpers import calculate_age
5
- from models.entities import PatientCreate, Note
 
6
  from datetime import datetime
7
  from bson import ObjectId
8
  from bson.errors import InvalidId
9
- from typing import Optional, List
 
 
 
 
 
10
  import uuid
 
11
  import logging
 
 
12
 
 
 
 
 
 
13
  logger = logging.getLogger(__name__)
 
14
  router = APIRouter()
15
 
 
 
 
 
 
16
  @router.post("/", status_code=status.HTTP_201_CREATED)
17
  async def create_patient(
18
  patient_data: PatientCreate,
19
  current_user: dict = Depends(get_current_user)
20
  ):
21
- """Create a new patient"""
 
 
22
  if current_user.get('role') not in ['admin', 'doctor']:
 
23
  raise HTTPException(
24
  status_code=status.HTTP_403_FORBIDDEN,
25
- detail="Only admins and doctors can create patients"
26
  )
27
-
28
  try:
29
- patient = patient_data.dict()
 
30
  now = datetime.utcnow().isoformat()
31
 
32
- patient.update({
 
33
  "fhir_id": str(uuid.uuid4()),
34
  "import_date": now,
35
  "last_updated": now,
36
  "source": "manual",
37
  "created_by": current_user.get('email')
38
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- result = await patients_collection.insert_one(patient)
41
- created = await patients_collection.find_one({"_id": result.inserted_id})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- return {
44
- "id": str(created["_id"]),
45
- **{k: v for k, v in created.items() if k != "_id"}
46
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  except Exception as e:
49
- logger.error(f"Create patient error: {str(e)}")
50
  raise HTTPException(
51
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
52
- detail="Failed to create patient"
53
  )
54
 
55
  @router.get("/", response_model=List[dict])
56
  async def list_patients(
57
- search: Optional[str] = None,
58
- skip: int = 0,
59
- limit: int = 100,
 
 
60
  current_user: dict = Depends(get_current_user)
61
  ):
62
- """List patients with pagination and search"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  try:
64
- query = {}
65
- if search:
66
- query["$or"] = [
67
- {"full_name": {"$regex": search, "$options": "i"}},
68
- {"fhir_id": search}
69
- ]
70
-
71
  cursor = patients_collection.find(query).skip(skip).limit(limit)
72
  patients = []
73
 
74
- async for doc in cursor:
75
- doc["id"] = str(doc["_id"])
76
- doc["age"] = calculate_age(doc.get("date_of_birth"))
77
- patients.append(doc)
78
 
 
79
  return patients
80
-
81
  except Exception as e:
82
- logger.error(f"List patients error: {str(e)}")
83
  raise HTTPException(
84
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
85
- detail="Failed to fetch patients"
86
  )
87
 
88
  @router.get("/{patient_id}", response_model=dict)
@@ -90,46 +386,56 @@ async def get_patient(
90
  patient_id: str,
91
  current_user: dict = Depends(get_current_user)
92
  ):
93
- """Get patient by ID or FHIR ID"""
94
  try:
95
- # Try as ObjectId first
96
  try:
97
  patient = await patients_collection.find_one({"_id": ObjectId(patient_id)})
98
  except (InvalidId, ValueError):
99
- # Fall back to FHIR ID
100
  patient = await patients_collection.find_one({"fhir_id": patient_id})
101
-
102
  if not patient:
103
- raise HTTPException(status_code=404, detail="Patient not found")
104
-
 
 
 
 
105
  patient["id"] = str(patient["_id"])
106
  patient["age"] = calculate_age(patient.get("date_of_birth"))
 
 
107
  return patient
108
-
109
  except Exception as e:
110
- logger.error(f"Get patient error: {str(e)}")
111
  raise HTTPException(
112
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
113
- detail="Failed to fetch patient"
114
  )
115
 
116
  @router.post("/{patient_id}/notes", status_code=status.HTTP_201_CREATED)
117
- async def add_patient_note(
118
  patient_id: str,
119
  note: Note,
120
  current_user: dict = Depends(get_current_user)
121
  ):
122
- """Add a note to patient record"""
123
  if current_user.get('role') not in ['doctor', 'admin']:
124
- raise HTTPException(status_code=403, detail="Only clinicians can add notes")
125
-
 
 
 
 
126
  try:
127
  note_data = note.dict()
128
  note_data.update({
129
- "author": current_user.get('email'),
130
  "timestamp": datetime.utcnow().isoformat()
131
  })
132
-
133
  result = await patients_collection.update_one(
134
  {"$or": [
135
  {"_id": ObjectId(patient_id)},
@@ -140,17 +446,29 @@ async def add_patient_note(
140
  "$set": {"last_updated": datetime.utcnow().isoformat()}
141
  }
142
  )
143
-
144
  if result.modified_count == 0:
145
- raise HTTPException(status_code=404, detail="Patient not found")
146
-
147
- return {"status": "success"}
148
-
 
 
 
 
 
 
 
 
 
 
 
149
  except Exception as e:
150
- logger.error(f"Add note error: {str(e)}")
151
  raise HTTPException(
152
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
153
- detail="Failed to add note"
154
  )
155
 
 
156
  patients = router
 
1
+ from fastapi import APIRouter, HTTPException, Depends, Query, status, Body
2
  from db.mongo import patients_collection
3
  from core.security import get_current_user
4
+ from utils.db import create_indexes
5
+ from utils.helpers import calculate_age, standardize_language
6
+ from models.entities import Note, PatientCreate
7
  from datetime import datetime
8
  from bson import ObjectId
9
  from bson.errors import InvalidId
10
+ from typing import Optional, List, Dict
11
+ from pymongo import UpdateOne
12
+ from pymongo.errors import BulkWriteError
13
+ import json
14
+ from pathlib import Path
15
+ import glob
16
  import uuid
17
+ import re
18
  import logging
19
+ import time
20
+ import os
21
 
22
+ # Configure logging
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
26
+ )
27
  logger = logging.getLogger(__name__)
28
+
29
  router = APIRouter()
30
 
31
+ # Configuration
32
+ BASE_DIR = Path(__file__).resolve().parent.parent.parent
33
+ SYNTHEA_DATA_DIR = BASE_DIR / "output" / "fhir"
34
+ os.makedirs(SYNTHEA_DATA_DIR, exist_ok=True)
35
+
36
  @router.post("/", status_code=status.HTTP_201_CREATED)
37
  async def create_patient(
38
  patient_data: PatientCreate,
39
  current_user: dict = Depends(get_current_user)
40
  ):
41
+ """Create a new patient in the database"""
42
+ logger.info(f"Creating new patient by user {current_user.get('email')}")
43
+
44
  if current_user.get('role') not in ['admin', 'doctor']:
45
+ logger.warning(f"Unauthorized create attempt by {current_user.get('email')}")
46
  raise HTTPException(
47
  status_code=status.HTTP_403_FORBIDDEN,
48
+ detail="Only administrators and doctors can create patients"
49
  )
50
+
51
  try:
52
+ # Prepare the patient document
53
+ patient_doc = patient_data.dict()
54
  now = datetime.utcnow().isoformat()
55
 
56
+ # Add system-generated fields
57
+ patient_doc.update({
58
  "fhir_id": str(uuid.uuid4()),
59
  "import_date": now,
60
  "last_updated": now,
61
  "source": "manual",
62
  "created_by": current_user.get('email')
63
  })
64
+
65
+ # Ensure arrays exist even if empty
66
+ for field in ['conditions', 'medications', 'encounters', 'notes']:
67
+ if field not in patient_doc:
68
+ patient_doc[field] = []
69
+
70
+ # Insert the patient document
71
+ result = await patients_collection.insert_one(patient_doc)
72
+
73
+ # Return the created patient with the generated ID
74
+ created_patient = await patients_collection.find_one(
75
+ {"_id": result.inserted_id}
76
+ )
77
+
78
+ if not created_patient:
79
+ logger.error("Failed to retrieve created patient")
80
+ raise HTTPException(
81
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
82
+ detail="Failed to retrieve created patient"
83
+ )
84
+
85
+ created_patient["id"] = str(created_patient["_id"])
86
+ del created_patient["_id"]
87
+
88
+ logger.info(f"Successfully created patient {created_patient['fhir_id']}")
89
+ return created_patient
90
+
91
+ except Exception as e:
92
+ logger.error(f"Failed to create patient: {str(e)}")
93
+ raise HTTPException(
94
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
95
+ detail=f"Failed to create patient: {str(e)}"
96
+ )
97
 
98
+ async def process_synthea_patient(bundle: dict, file_path: str) -> Optional[dict]:
99
+ logger.debug(f"Processing patient from file: {file_path}")
100
+ patient_data = {}
101
+ notes = []
102
+ conditions = []
103
+ medications = []
104
+ encounters = []
105
+
106
+ # Validate bundle structure
107
+ if not isinstance(bundle, dict) or 'entry' not in bundle:
108
+ logger.error(f"Invalid FHIR bundle structure in {file_path}")
109
+ return None
110
+
111
+ for entry in bundle.get('entry', []):
112
+ resource = entry.get('resource', {})
113
+ resource_type = resource.get('resourceType')
114
 
115
+ if not resource_type:
116
+ logger.warning(f"Skipping entry with missing resourceType in {file_path}")
117
+ continue
118
+
119
+ try:
120
+ if resource_type == 'Patient':
121
+ name = resource.get('name', [{}])[0]
122
+ address = resource.get('address', [{}])[0]
123
+
124
+ # Construct full name and remove numbers
125
+ raw_full_name = f"{' '.join(name.get('given', ['']))} {name.get('family', '')}".strip()
126
+ clean_full_name = re.sub(r'\d+', '', raw_full_name).strip()
127
+
128
+ patient_data = {
129
+ 'fhir_id': resource.get('id'),
130
+ 'full_name': clean_full_name,
131
+ 'gender': resource.get('gender', 'unknown'),
132
+ 'date_of_birth': resource.get('birthDate', ''),
133
+ 'address': ' '.join(address.get('line', [''])),
134
+ 'city': address.get('city', ''),
135
+ 'state': address.get('state', ''),
136
+ 'postal_code': address.get('postalCode', ''),
137
+ 'country': address.get('country', ''),
138
+ 'marital_status': resource.get('maritalStatus', {}).get('text', ''),
139
+ 'language': standardize_language(resource.get('communication', [{}])[0].get('language', {}).get('text', '')),
140
+ 'source': 'synthea',
141
+ 'last_updated': datetime.utcnow().isoformat()
142
+ }
143
+
144
+ elif resource_type == 'Encounter':
145
+ encounter = {
146
+ 'id': resource.get('id'),
147
+ 'type': resource.get('type', [{}])[0].get('text', ''),
148
+ 'status': resource.get('status'),
149
+ 'period': resource.get('period', {}),
150
+ 'service_provider': resource.get('serviceProvider', {}).get('display', '')
151
+ }
152
+ encounters.append(encounter)
153
+
154
+ for note in resource.get('note', []):
155
+ if note.get('text'):
156
+ notes.append({
157
+ 'date': resource.get('period', {}).get('start', datetime.utcnow().isoformat()),
158
+ 'type': resource.get('type', [{}])[0].get('text', 'Encounter Note'),
159
+ 'text': note.get('text'),
160
+ 'context': f"Encounter: {encounter.get('type')}",
161
+ 'author': 'System Generated'
162
+ })
163
+
164
+ elif resource_type == 'Condition':
165
+ conditions.append({
166
+ 'id': resource.get('id'),
167
+ 'code': resource.get('code', {}).get('text', ''),
168
+ 'status': resource.get('clinicalStatus', {}).get('text', ''),
169
+ 'onset_date': resource.get('onsetDateTime'),
170
+ 'recorded_date': resource.get('recordedDate'),
171
+ 'verification_status': resource.get('verificationStatus', {}).get('text', '')
172
+ })
173
+
174
+ elif resource_type == 'MedicationRequest':
175
+ medications.append({
176
+ 'id': resource.get('id'),
177
+ 'name': resource.get('medicationCodeableConcept', {}).get('text', ''),
178
+ 'status': resource.get('status'),
179
+ 'prescribed_date': resource.get('authoredOn'),
180
+ 'requester': resource.get('requester', {}).get('display', ''),
181
+ 'dosage': resource.get('dosageInstruction', [{}])[0].get('text', '')
182
+ })
183
+
184
+ except Exception as e:
185
+ logger.error(f"Error processing {resource_type} in {file_path}: {str(e)}")
186
+ continue
187
+
188
+ if patient_data:
189
+ patient_data.update({
190
+ 'notes': notes,
191
+ 'conditions': conditions,
192
+ 'medications': medications,
193
+ 'encounters': encounters,
194
+ 'import_date': datetime.utcnow().isoformat()
195
+ })
196
+ logger.info(f"Successfully processed patient {patient_data.get('fhir_id')} from {file_path}")
197
+ return patient_data
198
+ logger.warning(f"No valid patient data found in {file_path}")
199
+ return None
200
 
201
+ @router.post("/import", status_code=status.HTTP_201_CREATED)
202
+ async def import_patients(
203
+ limit: int = Query(100, ge=1, le=1000),
204
+ current_user: dict = Depends(get_current_user)
205
+ ):
206
+ request_id = str(uuid.uuid4())
207
+ logger.info(f"Starting import request {request_id} by user {current_user.get('email')}")
208
+ start_time = time.time()
209
+
210
+ if current_user.get('role') not in ['admin', 'doctor']:
211
+ logger.warning(f"Unauthorized import attempt by {current_user.get('email')}")
212
+ raise HTTPException(
213
+ status_code=status.HTTP_403_FORBIDDEN,
214
+ detail="Only administrators and doctors can import data"
215
+ )
216
+
217
+ try:
218
+ await create_indexes()
219
+
220
+ if not SYNTHEA_DATA_DIR.exists():
221
+ logger.error(f"Synthea data directory not found: {SYNTHEA_DATA_DIR}")
222
+ raise HTTPException(
223
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
224
+ detail="Data directory not found"
225
+ )
226
+
227
+ # Filter out non-patient files
228
+ files = [
229
+ f for f in glob.glob(str(SYNTHEA_DATA_DIR / "*.json"))
230
+ if not re.search(r'(hospitalInformation|practitionerInformation)\d+\.json$', f)
231
+ ]
232
+ if not files:
233
+ logger.warning("No valid patient JSON files found in synthea data directory")
234
+ return {
235
+ "status": "success",
236
+ "message": "No patient data files found",
237
+ "imported": 0,
238
+ "request_id": request_id
239
+ }
240
+
241
+ operations = []
242
+ imported = 0
243
+ errors = []
244
+
245
+ for file_path in files[:limit]:
246
+ try:
247
+ logger.debug(f"Processing file: {file_path}")
248
+
249
+ # Check file accessibility
250
+ if not os.path.exists(file_path):
251
+ logger.error(f"File not found: {file_path}")
252
+ errors.append(f"File not found: {file_path}")
253
+ continue
254
+
255
+ # Check file size
256
+ file_size = os.path.getsize(file_path)
257
+ if file_size == 0:
258
+ logger.warning(f"Empty file: {file_path}")
259
+ errors.append(f"Empty file: {file_path}")
260
+ continue
261
+
262
+ with open(file_path, 'r', encoding='utf-8') as f:
263
+ try:
264
+ bundle = json.load(f)
265
+ except json.JSONDecodeError as je:
266
+ logger.error(f"Invalid JSON in {file_path}: {str(je)}")
267
+ errors.append(f"Invalid JSON in {file_path}: {str(je)}")
268
+ continue
269
+
270
+ patient = await process_synthea_patient(bundle, file_path)
271
+ if patient:
272
+ if not patient.get('fhir_id'):
273
+ logger.warning(f"Missing FHIR ID in patient data from {file_path}")
274
+ errors.append(f"Missing FHIR ID in {file_path}")
275
+ continue
276
+
277
+ operations.append(UpdateOne(
278
+ {"fhir_id": patient['fhir_id']},
279
+ {"$setOnInsert": patient},
280
+ upsert=True
281
+ ))
282
+ imported += 1
283
+ else:
284
+ logger.warning(f"No valid patient data in {file_path}")
285
+ errors.append(f"No valid patient data in {file_path}")
286
+
287
+ except Exception as e:
288
+ logger.error(f"Error processing {file_path}: {str(e)}")
289
+ errors.append(f"Error in {file_path}: {str(e)}")
290
+ continue
291
+
292
+ response = {
293
+ "status": "success",
294
+ "imported": imported,
295
+ "errors": errors,
296
+ "request_id": request_id,
297
+ "duration_seconds": time.time() - start_time
298
+ }
299
+
300
+ if operations:
301
+ try:
302
+ result = await patients_collection.bulk_write(operations, ordered=False)
303
+ response.update({
304
+ "upserted": result.upserted_count,
305
+ "existing": len(operations) - result.upserted_count
306
+ })
307
+ logger.info(f"Import request {request_id} completed: {imported} patients processed, "
308
+ f"{result.upserted_count} upserted, {len(errors)} errors")
309
+ except BulkWriteError as bwe:
310
+ logger.error(f"Partial bulk write failure for request {request_id}: {str(bwe.details)}")
311
+ response.update({
312
+ "upserted": bwe.details.get('nUpserted', 0),
313
+ "existing": len(operations) - bwe.details.get('nUpserted', 0),
314
+ "write_errors": [
315
+ f"Index {err['index']}: {err['errmsg']}" for err in bwe.details.get('writeErrors', [])
316
+ ]
317
+ })
318
+ logger.info(f"Import request {request_id} partially completed: {imported} patients processed, "
319
+ f"{response['upserted']} upserted, {len(errors)} errors")
320
+ except Exception as e:
321
+ logger.error(f"Bulk write failed for request {request_id}: {str(e)}")
322
+ raise HTTPException(
323
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
324
+ detail=f"Database operation failed: {str(e)}"
325
+ )
326
+ else:
327
+ logger.info(f"Import request {request_id} completed: No new patients to import, {len(errors)} errors")
328
+ response["message"] = "No new patients found to import"
329
+
330
+ return response
331
+
332
+ except HTTPException:
333
+ raise
334
  except Exception as e:
335
+ logger.error(f"Import request {request_id} failed: {str(e)}", exc_info=True)
336
  raise HTTPException(
337
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
338
+ detail=f"Import failed: {str(e)}"
339
  )
340
 
341
  @router.get("/", response_model=List[dict])
342
  async def list_patients(
343
+ search: Optional[str] = Query(None),
344
+ min_notes: int = Query(0, ge=0),
345
+ min_conditions: int = Query(0, ge=0),
346
+ limit: int = Query(100, ge=1, le=500),
347
+ skip: int = Query(0, ge=0),
348
  current_user: dict = Depends(get_current_user)
349
  ):
350
+ logger.info(f"Listing patients with search: {search}, limit: {limit}, skip: {skip}")
351
+ query = {}
352
+
353
+ if search:
354
+ query["$or"] = [
355
+ {"full_name": {"$regex": search, "$options": "i"}},
356
+ {"fhir_id": search}
357
+ ]
358
+
359
+ if min_notes > 0:
360
+ query[f"notes.{min_notes-1}"] = {"$exists": True}
361
+
362
+ if min_conditions > 0:
363
+ query[f"conditions.{min_conditions-1}"] = {"$exists": True}
364
+
365
  try:
 
 
 
 
 
 
 
366
  cursor = patients_collection.find(query).skip(skip).limit(limit)
367
  patients = []
368
 
369
+ async for patient in cursor:
370
+ patient["id"] = str(patient["_id"])
371
+ patient["age"] = calculate_age(patient.get("date_of_birth"))
372
+ patients.append(patient)
373
 
374
+ logger.info(f"Retrieved {len(patients)} patients")
375
  return patients
376
+
377
  except Exception as e:
378
+ logger.error(f"Failed to list patients: {str(e)}")
379
  raise HTTPException(
380
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
381
+ detail=f"Failed to retrieve patients: {str(e)}"
382
  )
383
 
384
  @router.get("/{patient_id}", response_model=dict)
 
386
  patient_id: str,
387
  current_user: dict = Depends(get_current_user)
388
  ):
389
+ logger.info(f"Retrieving patient: {patient_id}")
390
  try:
391
+ # First try to find by ObjectId
392
  try:
393
  patient = await patients_collection.find_one({"_id": ObjectId(patient_id)})
394
  except (InvalidId, ValueError):
395
+ # If not valid ObjectId, try by fhir_id
396
  patient = await patients_collection.find_one({"fhir_id": patient_id})
397
+
398
  if not patient:
399
+ logger.warning(f"Patient not found: {patient_id}")
400
+ raise HTTPException(
401
+ status_code=status.HTTP_404_NOT_FOUND,
402
+ detail="Patient not found"
403
+ )
404
+
405
  patient["id"] = str(patient["_id"])
406
  patient["age"] = calculate_age(patient.get("date_of_birth"))
407
+
408
+ logger.info(f"Successfully retrieved patient: {patient_id}")
409
  return patient
410
+
411
  except Exception as e:
412
+ logger.error(f"Failed to retrieve patient {patient_id}: {str(e)}")
413
  raise HTTPException(
414
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
415
+ detail=f"Failed to retrieve patient: {str(e)}"
416
  )
417
 
418
  @router.post("/{patient_id}/notes", status_code=status.HTTP_201_CREATED)
419
+ async def add_note(
420
  patient_id: str,
421
  note: Note,
422
  current_user: dict = Depends(get_current_user)
423
  ):
424
+ logger.info(f"Adding note for patient {patient_id} by user {current_user.get('email')}")
425
  if current_user.get('role') not in ['doctor', 'admin']:
426
+ logger.warning(f"Unauthorized note addition attempt by {current_user.get('email')}")
427
+ raise HTTPException(
428
+ status_code=status.HTTP_403_FORBIDDEN,
429
+ detail="Only clinicians can add notes"
430
+ )
431
+
432
  try:
433
  note_data = note.dict()
434
  note_data.update({
435
+ "author": current_user.get('full_name', 'System'),
436
  "timestamp": datetime.utcnow().isoformat()
437
  })
438
+
439
  result = await patients_collection.update_one(
440
  {"$or": [
441
  {"_id": ObjectId(patient_id)},
 
446
  "$set": {"last_updated": datetime.utcnow().isoformat()}
447
  }
448
  )
449
+
450
  if result.modified_count == 0:
451
+ logger.warning(f"Patient not found for note addition: {patient_id}")
452
+ raise HTTPException(
453
+ status_code=status.HTTP_404_NOT_FOUND,
454
+ detail="Patient not found"
455
+ )
456
+
457
+ logger.info(f"Note added successfully for patient {patient_id}")
458
+ return {"status": "success", "message": "Note added"}
459
+
460
+ except ValueError as ve:
461
+ logger.error(f"Invalid patient ID format: {patient_id}, error: {str(ve)}")
462
+ raise HTTPException(
463
+ status_code=status.HTTP_400_BAD_REQUEST,
464
+ detail="Invalid patient ID format"
465
+ )
466
  except Exception as e:
467
+ logger.error(f"Failed to add note for patient {patient_id}: {str(e)}")
468
  raise HTTPException(
469
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
470
+ detail=f"Failed to add note: {str(e)}"
471
  )
472
 
473
+ # Export the router as 'patients' for api.__init__.py
474
  patients = router