Ali2206 commited on
Commit
49f3013
·
verified ·
1 Parent(s): 7863b18

Update api/routes.py

Browse files
Files changed (1) hide show
  1. api/routes.py +298 -126
api/routes.py CHANGED
@@ -5,7 +5,7 @@ from db.mongo import users_collection, patients_collection, appointments_collect
5
  from core.security import hash_password, verify_password, create_access_token, get_current_user
6
  from datetime import datetime, timedelta
7
  from bson import ObjectId
8
- from typing import Optional, List
9
  from pydantic import BaseModel, Field
10
  from pymongo import UpdateOne, InsertOne, IndexModel
11
  import httpx
@@ -14,11 +14,21 @@ import json
14
  from pathlib import Path
15
  import glob
16
  from motor.motor_asyncio import AsyncIOMotorClient
 
 
 
 
 
 
 
 
 
 
17
 
18
  router = APIRouter()
19
 
20
  # Configuration
21
- BASE_DIR = Path(__file__).resolve().parent.parent # goes up from /api to project root
22
  SYNTHEA_DATA_DIR = BASE_DIR / "output" / "fhir"
23
  os.makedirs(SYNTHEA_DATA_DIR, exist_ok=True)
24
 
@@ -46,12 +56,17 @@ class PatientUpdate(BaseModel):
46
 
47
  # Indexes
48
  async def create_indexes():
49
- await patients_collection.create_indexes([
50
- IndexModel([("fhir_id", 1)], unique=True),
51
- IndexModel([("full_name", "text")]),
52
- IndexModel([("date_of_birth", 1)]),
53
- IndexModel([("notes.date", -1)])
54
- ])
 
 
 
 
 
55
 
56
  # Helper Functions
57
  def calculate_age(birth_date: str) -> Optional[int]:
@@ -62,79 +77,95 @@ def calculate_age(birth_date: str) -> Optional[int]:
62
  today = datetime.now()
63
  return today.year - birth_date.year - (
64
  (today.month, today.day) < (birth_date.month, birth_date.day))
65
- except ValueError:
 
66
  return None
67
 
68
- async def process_synthea_patient(bundle: dict) -> Optional[dict]:
 
69
  patient_data = {}
70
  notes = []
71
  conditions = []
72
  medications = []
73
  encounters = []
74
 
 
 
 
 
 
75
  for entry in bundle.get('entry', []):
76
  resource = entry.get('resource', {})
77
  resource_type = resource.get('resourceType')
78
 
79
- if resource_type == 'Patient':
80
- name = resource.get('name', [{}])[0]
81
- address = resource.get('address', [{}])[0]
82
 
83
- patient_data = {
84
- 'fhir_id': resource.get('id'),
85
- 'full_name': f"{' '.join(name.get('given', ['']))} {name.get('family', '')}".strip(),
86
- 'gender': resource.get('gender', 'unknown'),
87
- 'date_of_birth': resource.get('birthDate', ''),
88
- 'address': ' '.join(address.get('line', [''])),
89
- 'city': address.get('city', ''),
90
- 'state': address.get('state', ''),
91
- 'postal_code': address.get('postalCode', ''),
92
- 'country': address.get('country', ''),
93
- 'marital_status': resource.get('maritalStatus', {}).get('text', ''),
94
- 'language': resource.get('communication', [{}])[0].get('language', {}).get('text', ''),
95
- 'source': 'synthea',
96
- 'last_updated': datetime.utcnow()
97
- }
98
-
99
- elif resource_type == 'Encounter':
100
- encounter = {
101
- 'id': resource.get('id'),
102
- 'type': resource.get('type', [{}])[0].get('text', ''),
103
- 'status': resource.get('status'),
104
- 'period': resource.get('period', {}),
105
- 'service_provider': resource.get('serviceProvider', {}).get('display', '')
106
- }
107
- encounters.append(encounter)
108
-
109
- for note in resource.get('note', []):
110
- if note.get('text'):
111
- notes.append({
112
- 'date': resource.get('period', {}).get('start', datetime.utcnow().isoformat()),
113
- 'type': resource.get('type', [{}])[0].get('text', 'Encounter Note'),
114
- 'text': note.get('text'),
115
- 'context': f"Encounter: {encounter.get('type')}",
116
- 'author': 'System Generated'
117
- })
118
-
119
- elif resource_type == 'Condition':
120
- conditions.append({
121
- 'id': resource.get('id'),
122
- 'code': resource.get('code', {}).get('text', ''),
123
- 'status': resource.get('clinicalStatus', {}).get('text', ''),
124
- 'onset_date': resource.get('onsetDateTime'),
125
- 'recorded_date': resource.get('recordedDate'),
126
- 'verification_status': resource.get('verificationStatus', {}).get('text', '')
127
- })
128
-
129
- elif resource_type == 'MedicationRequest':
130
- medications.append({
131
- 'id': resource.get('id'),
132
- 'name': resource.get('medicationCodeableConcept', {}).get('text', ''),
133
- 'status': resource.get('status'),
134
- 'prescribed_date': resource.get('authoredOn'),
135
- 'requester': resource.get('requester', {}).get('display', ''),
136
- 'dosage': resource.get('dosageInstruction', [{}])[0].get('text', '')
137
- })
 
 
 
 
 
 
 
 
 
138
 
139
  if patient_data:
140
  patient_data.update({
@@ -144,7 +175,9 @@ async def process_synthea_patient(bundle: dict) -> Optional[dict]:
144
  'encounters': encounters,
145
  'import_date': datetime.utcnow()
146
  })
 
147
  return patient_data
 
148
  return None
149
 
150
  # Routes
@@ -153,7 +186,12 @@ async def import_patients(
153
  limit: int = Query(100, ge=1, le=1000),
154
  current_user: dict = Depends(get_current_user)
155
  ):
 
 
 
 
156
  if current_user.get('role') not in ['admin', 'doctor']:
 
157
  raise HTTPException(
158
  status_code=status.HTTP_403_FORBIDDEN,
159
  detail="Only administrators and doctors can import data"
@@ -161,36 +199,108 @@ async def import_patients(
161
 
162
  try:
163
  await create_indexes()
 
 
 
 
 
 
 
 
164
  files = glob.glob(str(SYNTHEA_DATA_DIR / "*.json"))
 
 
 
 
 
 
 
 
 
165
  operations = []
166
  imported = 0
 
167
 
168
  for file_path in files[:limit]:
169
  try:
170
- with open(file_path, 'r') as f:
171
- bundle = json.load(f)
172
- patient = await process_synthea_patient(bundle)
173
- if patient:
174
- operations.append(UpdateOne(
175
- {"fhir_id": patient['fhir_id']},
176
- {"$setOnInsert": patient},
177
- upsert=True
178
- ))
179
- imported += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  except Exception as e:
 
 
181
  continue
182
 
 
 
 
 
 
 
 
 
183
  if operations:
184
- result = await patients_collection.bulk_write(operations)
185
- return {
186
- "status": "success",
187
- "imported": imported,
188
- "upserted": result.upserted_count,
189
- "existing": len(operations) - result.upserted_count
190
- }
191
- return {"status": "success", "message": "No new patients found to import"}
192
-
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  except Exception as e:
 
194
  raise HTTPException(
195
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
196
  detail=f"Import failed: {str(e)}"
@@ -204,6 +314,7 @@ async def list_patients(
204
  limit: int = Query(100, ge=1, le=500),
205
  skip: int = Query(0, ge=0)
206
  ):
 
207
  query = {"source": "synthea"}
208
 
209
  if search:
@@ -228,27 +339,37 @@ async def list_patients(
228
  "medications": {"$slice": 1}
229
  }
230
 
231
- cursor = patients_collection.find(query, projection).skip(skip).limit(limit)
232
- patients = []
233
-
234
- async for patient in cursor:
235
- patients.append({
236
- "id": str(patient["_id"]),
237
- "fhir_id": patient.get("fhir_id"),
238
- "full_name": patient.get("full_name"),
239
- "gender": patient.get("gender"),
240
- "age": calculate_age(patient.get("date_of_birth")),
241
- "stats": {
242
- "notes": len(patient.get("notes", [])),
243
- "conditions": len(patient.get("conditions", [])),
244
- "medications": len(patient.get("medications", []))
245
- }
246
- })
247
-
248
- return patients
 
 
 
 
 
 
 
 
 
249
 
250
  @router.get("/ehr/patients/{patient_id}", response_model=dict)
251
  async def get_patient(patient_id: str):
 
252
  try:
253
  patient = await patients_collection.find_one({
254
  "$or": [
@@ -258,12 +379,13 @@ async def get_patient(patient_id: str):
258
  })
259
 
260
  if not patient:
 
261
  raise HTTPException(
262
  status_code=status.HTTP_404_NOT_FOUND,
263
  detail="Patient not found"
264
  )
265
 
266
- return {
267
  "demographics": {
268
  "id": str(patient["_id"]),
269
  "fhir_id": patient.get("fhir_id"),
@@ -293,11 +415,21 @@ async def get_patient(patient_id: str):
293
  "last_updated": patient.get("last_updated")
294
  }
295
  }
296
-
 
 
 
 
 
 
 
 
 
297
  except Exception as e:
 
298
  raise HTTPException(
299
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
300
- detail=str(e)
301
  )
302
 
303
  @router.post("/ehr/patients/{patient_id}/notes", status_code=status.HTTP_201_CREATED)
@@ -306,7 +438,9 @@ async def add_note(
306
  note: Note,
307
  current_user: dict = Depends(get_current_user)
308
  ):
 
309
  if current_user.get('role') not in ['doctor', 'admin']:
 
310
  raise HTTPException(
311
  status_code=status.HTTP_403_FORBIDDEN,
312
  detail="Only clinicians can add notes"
@@ -331,25 +465,36 @@ async def add_note(
331
  )
332
 
333
  if result.modified_count == 0:
 
334
  raise HTTPException(
335
  status_code=status.HTTP_404_NOT_FOUND,
336
  detail="Patient not found"
337
  )
338
 
 
339
  return {"status": "success", "message": "Note added"}
340
-
 
 
 
 
 
 
341
  except Exception as e:
 
342
  raise HTTPException(
343
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
344
- detail=str(e)
345
  )
346
 
347
  # Original Auth Routes (fully implemented)
348
  @router.post("/signup", status_code=status.HTTP_201_CREATED)
349
  async def signup(data: SignupForm):
 
350
  email = data.email.lower().strip()
351
  existing = await users_collection.find_one({"email": email})
352
  if existing:
 
353
  raise HTTPException(
354
  status_code=status.HTTP_409_CONFLICT,
355
  detail="Email already exists"
@@ -365,26 +510,38 @@ async def signup(data: SignupForm):
365
  "updated_at": datetime.utcnow()
366
  }
367
 
368
- result = await users_collection.insert_one(user_doc)
369
- return {
370
- "status": "success",
371
- "id": str(result.inserted_id),
372
- "email": email
373
- }
 
 
 
 
 
 
 
 
374
 
375
  @router.post("/admin/doctors", status_code=status.HTTP_201_CREATED)
376
  async def create_doctor(
377
  data: DoctorCreate,
378
  current_user: dict = Depends(get_current_user)
379
  ):
 
380
  if current_user.get('role') != 'admin':
 
381
  raise HTTPException(
382
  status_code=status.HTTP_403_FORBIDDEN,
383
  detail="Only admins can create doctor accounts"
384
  )
385
 
386
- existing = await users_collection.find_one({"email": data.email.lower()})
 
387
  if existing:
 
388
  raise HTTPException(
389
  status_code=status.HTTP_409_CONFLICT,
390
  detail="Email already exists"
@@ -392,7 +549,7 @@ async def create_doctor(
392
 
393
  hashed_pw = hash_password(data.password)
394
  doctor_doc = {
395
- "email": data.email.lower().strip(),
396
  "full_name": data.full_name.strip(),
397
  "password": hashed_pw,
398
  "role": "doctor",
@@ -402,17 +559,27 @@ async def create_doctor(
402
  "updated_at": datetime.utcnow()
403
  }
404
 
405
- result = await users_collection.insert_one(doctor_doc)
406
- return {
407
- "status": "success",
408
- "id": str(result.inserted_id),
409
- "email": data.email
410
- }
 
 
 
 
 
 
 
 
411
 
412
  @router.post("/login", response_model=TokenResponse)
413
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
 
414
  user = await users_collection.find_one({"email": form_data.username.lower()})
415
  if not user or not verify_password(form_data.password, user["password"]):
 
416
  raise HTTPException(
417
  status_code=status.HTTP_401_UNAUTHORIZED,
418
  detail="Invalid credentials",
@@ -420,6 +587,7 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
420
  )
421
 
422
  access_token = create_access_token(data={"sub": user["email"]})
 
423
  return {
424
  "access_token": access_token,
425
  "token_type": "bearer",
@@ -428,14 +596,16 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
428
 
429
  @router.get("/me")
430
  async def get_me(current_user: dict = Depends(get_current_user)):
 
431
  user = await users_collection.find_one({"email": current_user["email"]})
432
  if not user:
 
433
  raise HTTPException(
434
  status_code=status.HTTP_404_NOT_FOUND,
435
  detail="User not found"
436
  )
437
 
438
- return {
439
  "id": str(user["_id"]),
440
  "email": user["email"],
441
  "full_name": user.get("full_name", ""),
@@ -443,4 +613,6 @@ async def get_me(current_user: dict = Depends(get_current_user)):
443
  "specialty": user.get("specialty"),
444
  "created_at": user.get("created_at"),
445
  "updated_at": user.get("updated_at")
446
- }
 
 
 
5
  from core.security import hash_password, verify_password, create_access_token, get_current_user
6
  from datetime import datetime, timedelta
7
  from bson import ObjectId
8
+ from typing import Optional, List, Dict
9
  from pydantic import BaseModel, Field
10
  from pymongo import UpdateOne, InsertOne, IndexModel
11
  import httpx
 
14
  from pathlib import Path
15
  import glob
16
  from motor.motor_asyncio import AsyncIOMotorClient
17
+ import logging
18
+ import time
19
+ import uuid
20
+
21
+ # Configure logging
22
+ logging.basicConfig(
23
+ level=logging.INFO,
24
+ format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
25
+ )
26
+ logger = logging.getLogger(__name__)
27
 
28
  router = APIRouter()
29
 
30
  # Configuration
31
+ BASE_DIR = Path(__file__).resolve().parent.parent
32
  SYNTHEA_DATA_DIR = BASE_DIR / "output" / "fhir"
33
  os.makedirs(SYNTHEA_DATA_DIR, exist_ok=True)
34
 
 
56
 
57
  # Indexes
58
  async def create_indexes():
59
+ try:
60
+ await patients_collection.create_indexes([
61
+ IndexModel([("fhir_id", 1)], unique=True),
62
+ IndexModel([("full_name", "text")]),
63
+ IndexModel([("date_of_birth", 1)]),
64
+ IndexModel([("notes.date", -1)])
65
+ ])
66
+ logger.info("Database indexes created successfully")
67
+ except Exception as e:
68
+ logger.error(f"Failed to create indexes: {str(e)}")
69
+ raise
70
 
71
  # Helper Functions
72
  def calculate_age(birth_date: str) -> Optional[int]:
 
77
  today = datetime.now()
78
  return today.year - birth_date.year - (
79
  (today.month, today.day) < (birth_date.month, birth_date.day))
80
+ except ValueError as e:
81
+ logger.warning(f"Invalid birth date format: {birth_date}, error: {str(e)}")
82
  return None
83
 
84
+ async def process_synthea_patient(bundle: dict, file_path: str) -> Optional[dict]:
85
+ logger.debug(f"Processing patient from file: {file_path}")
86
  patient_data = {}
87
  notes = []
88
  conditions = []
89
  medications = []
90
  encounters = []
91
 
92
+ # Validate bundle structure
93
+ if not isinstance(bundle, dict) or 'entry' not in bundle:
94
+ logger.error(f"Invalid FHIR bundle structure in {file_path}")
95
+ return None
96
+
97
  for entry in bundle.get('entry', []):
98
  resource = entry.get('resource', {})
99
  resource_type = resource.get('resourceType')
100
 
101
+ if not resource_type:
102
+ logger.warning(f"Skipping entry with missing resourceType in {file_path}")
103
+ continue
104
 
105
+ try:
106
+ if resource_type == 'Patient':
107
+ name = resource.get('name', [{}])[0]
108
+ address = resource.get('address', [{}])[0]
109
+
110
+ patient_data = {
111
+ 'fhir_id': resource.get('id'),
112
+ 'full_name': f"{' '.join(name.get('given', ['']))} {name.get('family', '')}".strip(),
113
+ 'gender': resource.get('gender', 'unknown'),
114
+ 'date_of_birth': resource.get('birthDate', ''),
115
+ 'address': ' '.join(address.get('line', [''])),
116
+ 'city': address.get('city', ''),
117
+ 'state': address.get('state', ''),
118
+ 'postal_code': address.get('postalCode', ''),
119
+ 'country': address.get('country', ''),
120
+ 'marital_status': resource.get('maritalStatus', {}).get('text', ''),
121
+ 'language': resource.get('communication', [{}])[0].get('language', {}).get('text', ''),
122
+ 'source': 'synthea',
123
+ 'last_updated': datetime.utcnow()
124
+ }
125
+
126
+ elif resource_type == 'Encounter':
127
+ encounter = {
128
+ 'id': resource.get('id'),
129
+ 'type': resource.get('type', [{}])[0].get('text', ''),
130
+ 'status': resource.get('status'),
131
+ 'period': resource.get('period', {}),
132
+ 'service_provider': resource.get('serviceProvider', {}).get('display', '')
133
+ }
134
+ encounters.append(encounter)
135
+
136
+ for note in resource.get('note', []):
137
+ if note.get('text'):
138
+ notes.append({
139
+ 'date': resource.get('period', {}).get('start', datetime.utcnow().isoformat()),
140
+ 'type': resource.get('type', [{}])[0].get('text', 'Encounter Note'),
141
+ 'text': note.get('text'),
142
+ 'context': f"Encounter: {encounter.get('type')}",
143
+ 'author': 'System Generated'
144
+ })
145
+
146
+ elif resource_type == 'Condition':
147
+ conditions.append({
148
+ 'id': resource.get('id'),
149
+ 'code': resource.get('code', {}).get('text', ''),
150
+ 'status': resource.get('clinicalStatus', {}).get('text', ''),
151
+ 'onset_date': resource.get('onsetDateTime'),
152
+ 'recorded_date': resource.get('recordedDate'),
153
+ 'verification_status': resource.get('verificationStatus', {}).get('text', '')
154
+ })
155
+
156
+ elif resource_type == 'MedicationRequest':
157
+ medications.append({
158
+ 'id': resource.get('id'),
159
+ 'name': resource.get('medicationCodeableConcept', {}).get('text', ''),
160
+ 'status': resource.get('status'),
161
+ 'prescribed_date': resource.get('authoredOn'),
162
+ 'requester': resource.get('requester', {}).get('display', ''),
163
+ 'dosage': resource.get('dosageInstruction', [{}])[0].get('text', '')
164
+ })
165
+
166
+ except Exception as e:
167
+ logger.error(f"Error processing {resource_type} in {file_path}: {str(e)}")
168
+ continue
169
 
170
  if patient_data:
171
  patient_data.update({
 
175
  'encounters': encounters,
176
  'import_date': datetime.utcnow()
177
  })
178
+ logger.info(f"Successfully processed patient {patient_data.get('fhir_id')} from {file_path}")
179
  return patient_data
180
+ logger.warning(f"No valid patient data found in {file_path}")
181
  return None
182
 
183
  # Routes
 
186
  limit: int = Query(100, ge=1, le=1000),
187
  current_user: dict = Depends(get_current_user)
188
  ):
189
+ request_id = str(uuid.uuid4())
190
+ logger.info(f"Starting import request {request_id} by user {current_user.get('email')}")
191
+ start_time = time.time()
192
+
193
  if current_user.get('role') not in ['admin', 'doctor']:
194
+ logger.warning(f"Unauthorized import attempt by {current_user.get('email')}")
195
  raise HTTPException(
196
  status_code=status.HTTP_403_FORBIDDEN,
197
  detail="Only administrators and doctors can import data"
 
199
 
200
  try:
201
  await create_indexes()
202
+
203
+ if not SYNTHEA_DATA_DIR.exists():
204
+ logger.error(f"Synthea data directory not found: {SYNTHEA_DATA_DIR}")
205
+ raise HTTPException(
206
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
207
+ detail="Data directory not found"
208
+ )
209
+
210
  files = glob.glob(str(SYNTHEA_DATA_DIR / "*.json"))
211
+ if not files:
212
+ logger.warning("No JSON files found in synthea data directory")
213
+ return {
214
+ "status": "success",
215
+ "message": "No data files found",
216
+ "imported": 0,
217
+ "request_id": request_id
218
+ }
219
+
220
  operations = []
221
  imported = 0
222
+ errors = []
223
 
224
  for file_path in files[:limit]:
225
  try:
226
+ logger.debug(f"Processing file: {file_path}")
227
+
228
+ # Check file accessibility
229
+ if not os.path.exists(file_path):
230
+ logger.error(f"File not found: {file_path}")
231
+ errors.append(f"File not found: {file_path}")
232
+ continue
233
+
234
+ # Check file size
235
+ file_size = os.path.getsize(file_path)
236
+ if file_size == 0:
237
+ logger.warning(f"Empty file: {file_path}")
238
+ errors.append(f"Empty file: {file_path}")
239
+ continue
240
+
241
+ with open(file_path, 'r', encoding='utf-8') as f:
242
+ try:
243
+ bundle = json.load(f)
244
+ except json.JSONDecodeError as je:
245
+ logger.error(f"Invalid JSON in {file_path}: {str(je)}")
246
+ errors.append(f"Invalid JSON in {file_path}: {str(je)}")
247
+ continue
248
+
249
+ patient = await process_synthea_patient(bundle, file_path)
250
+ if patient:
251
+ if not patient.get('fhir_id'):
252
+ logger.warning(f"Missing FHIR ID in patient data from {file_path}")
253
+ errors.append(f"Missing FHIR ID in {file_path}")
254
+ continue
255
+
256
+ operations.append(UpdateOne(
257
+ {"fhir_id": patient['fhir_id']},
258
+ {"$setOnInsert": patient},
259
+ upsert=True
260
+ ))
261
+ imported += 1
262
+ else:
263
+ logger.warning(f"No valid patient data in {file_path}")
264
+ errors.append(f"No valid patient data in {file_path}")
265
+
266
  except Exception as e:
267
+ logger.error(f"Error processing {file_path}: {str(e)}")
268
+ errors.append(f"Error in {file_path}: {str(e)}")
269
  continue
270
 
271
+ response = {
272
+ "status": "success",
273
+ "imported": imported,
274
+ "errors": errors,
275
+ "request_id": request_id,
276
+ "duration_seconds": time.time() - start_time
277
+ }
278
+
279
  if operations:
280
+ try:
281
+ result = await patients_collection.bulk_write(operations)
282
+ response.update({
283
+ "upserted": result.upserted_count,
284
+ "existing": len(operations) - result.upserted_count
285
+ })
286
+ logger.info(f"Import request {request_id} completed: {imported} patients processed, "
287
+ f"{result.upserted_count} upserted, {len(errors)} errors")
288
+ except Exception as e:
289
+ logger.error(f"Bulk write failed for request {request_id}: {str(e)}")
290
+ raise HTTPException(
291
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
292
+ detail=f"Database operation failed: {str(e)}"
293
+ )
294
+ else:
295
+ logger.info(f"Import request {request_id} completed: No new patients to import, {len(errors)} errors")
296
+ response["message"] = "No new patients found to import"
297
+
298
+ return response
299
+
300
+ except HTTPException:
301
+ raise
302
  except Exception as e:
303
+ logger.error(f"Import request {request_id} failed: {str(e)}", exc_info=True)
304
  raise HTTPException(
305
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
306
  detail=f"Import failed: {str(e)}"
 
314
  limit: int = Query(100, ge=1, le=500),
315
  skip: int = Query(0, ge=0)
316
  ):
317
+ logger.info(f"Listing patients with search: {search}, limit: {limit}, skip: {skip}")
318
  query = {"source": "synthea"}
319
 
320
  if search:
 
339
  "medications": {"$slice": 1}
340
  }
341
 
342
+ try:
343
+ cursor = patients_collection.find(query, projection).skip(skip).limit(limit)
344
+ patients = []
345
+
346
+ async for patient in cursor:
347
+ patients.append({
348
+ "id": str(patient["_id"]),
349
+ "fhir_id": patient.get("fhir_id"),
350
+ "full_name": patient.get("full_name"),
351
+ "gender": patient.get("gender"),
352
+ "age": calculate_age(patient.get("date_of_birth")),
353
+ "stats": {
354
+ "notes": len(patient.get("notes", [])),
355
+ "conditions": len(patient.get("conditions", [])),
356
+ "medications": len(patient.get("medications", []))
357
+ }
358
+ })
359
+
360
+ logger.info(f"Retrieved {len(patients)} patients")
361
+ return patients
362
+
363
+ except Exception as e:
364
+ logger.error(f"Failed to list patients: {str(e)}")
365
+ raise HTTPException(
366
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
367
+ detail=f"Failed to retrieve patients: {str(e)}"
368
+ )
369
 
370
  @router.get("/ehr/patients/{patient_id}", response_model=dict)
371
  async def get_patient(patient_id: str):
372
+ logger.info(f"Retrieving patient: {patient_id}")
373
  try:
374
  patient = await patients_collection.find_one({
375
  "$or": [
 
379
  })
380
 
381
  if not patient:
382
+ logger.warning(f"Patient not found: {patient_id}")
383
  raise HTTPException(
384
  status_code=status.HTTP_404_NOT_FOUND,
385
  detail="Patient not found"
386
  )
387
 
388
+ response = {
389
  "demographics": {
390
  "id": str(patient["_id"]),
391
  "fhir_id": patient.get("fhir_id"),
 
415
  "last_updated": patient.get("last_updated")
416
  }
417
  }
418
+
419
+ logger.info(f"Successfully retrieved patient: {patient_id}")
420
+ return response
421
+
422
+ except ValueError as ve:
423
+ logger.error(f"Invalid patient ID format: {patient_id}, error: {str(ve)}")
424
+ raise HTTPException(
425
+ status_code=status.HTTP_400_BAD_REQUEST,
426
+ detail="Invalid patient ID format"
427
+ )
428
  except Exception as e:
429
+ logger.error(f"Failed to retrieve patient {patient_id}: {str(e)}")
430
  raise HTTPException(
431
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
432
+ detail=f"Failed to retrieve patient: {str(e)}"
433
  )
434
 
435
  @router.post("/ehr/patients/{patient_id}/notes", status_code=status.HTTP_201_CREATED)
 
438
  note: Note,
439
  current_user: dict = Depends(get_current_user)
440
  ):
441
+ logger.info(f"Adding note for patient {patient_id} by user {current_user.get('email')}")
442
  if current_user.get('role') not in ['doctor', 'admin']:
443
+ logger.warning(f"Unauthorized note addition attempt by {current_user.get('email')}")
444
  raise HTTPException(
445
  status_code=status.HTTP_403_FORBIDDEN,
446
  detail="Only clinicians can add notes"
 
465
  )
466
 
467
  if result.modified_count == 0:
468
+ logger.warning(f"Patient not found for note addition: {patient_id}")
469
  raise HTTPException(
470
  status_code=status.HTTP_404_NOT_FOUND,
471
  detail="Patient not found"
472
  )
473
 
474
+ logger.info(f"Note added successfully for patient {patient_id}")
475
  return {"status": "success", "message": "Note added"}
476
+
477
+ except ValueError as ve:
478
+ logger.error(f"Invalid patient ID format: {patient_id}, error: {str(ve)}")
479
+ raise HTTPException(
480
+ status_code=status.HTTP_400_BAD_REQUEST,
481
+ detail="Invalid patient ID format"
482
+ )
483
  except Exception as e:
484
+ logger.error(f"Failed to add note for patient {patient_id}: {str(e)}")
485
  raise HTTPException(
486
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
487
+ detail=f"Failed to add note: {str(e)}"
488
  )
489
 
490
  # Original Auth Routes (fully implemented)
491
  @router.post("/signup", status_code=status.HTTP_201_CREATED)
492
  async def signup(data: SignupForm):
493
+ logger.info(f"Signup attempt for email: {data.email}")
494
  email = data.email.lower().strip()
495
  existing = await users_collection.find_one({"email": email})
496
  if existing:
497
+ logger.warning(f"Signup failed: Email already exists: {email}")
498
  raise HTTPException(
499
  status_code=status.HTTP_409_CONFLICT,
500
  detail="Email already exists"
 
510
  "updated_at": datetime.utcnow()
511
  }
512
 
513
+ try:
514
+ result = await users_collection.insert_one(user_doc)
515
+ logger.info(f"User created successfully: {email}")
516
+ return {
517
+ "status": "success",
518
+ "id": str(result.inserted_id),
519
+ "email": email
520
+ }
521
+ except Exception as e:
522
+ logger.error(f"Failed to create user {email}: {str(e)}")
523
+ raise HTTPException(
524
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
525
+ detail=f"Failed to create user: {str(e)}"
526
+ )
527
 
528
  @router.post("/admin/doctors", status_code=status.HTTP_201_CREATED)
529
  async def create_doctor(
530
  data: DoctorCreate,
531
  current_user: dict = Depends(get_current_user)
532
  ):
533
+ logger.info(f"Doctor creation attempt by {current_user.get('email')}")
534
  if current_user.get('role') != 'admin':
535
+ logger.warning(f"Unauthorized doctor creation attempt by {current_user.get('email')}")
536
  raise HTTPException(
537
  status_code=status.HTTP_403_FORBIDDEN,
538
  detail="Only admins can create doctor accounts"
539
  )
540
 
541
+ email = data.email.lower().strip()
542
+ existing = await users_collection.find_one({"email": email})
543
  if existing:
544
+ logger.warning(f"Doctor creation failed: Email already exists: {email}")
545
  raise HTTPException(
546
  status_code=status.HTTP_409_CONFLICT,
547
  detail="Email already exists"
 
549
 
550
  hashed_pw = hash_password(data.password)
551
  doctor_doc = {
552
+ "email": email,
553
  "full_name": data.full_name.strip(),
554
  "password": hashed_pw,
555
  "role": "doctor",
 
559
  "updated_at": datetime.utcnow()
560
  }
561
 
562
+ try:
563
+ result = await users_collection.insert_one(doctor_doc)
564
+ logger.info(f"Doctor created successfully: {email}")
565
+ return {
566
+ "status": "success",
567
+ "id": str(result.inserted_id),
568
+ "email": email
569
+ }
570
+ except Exception as e:
571
+ logger.error(f"Failed to create doctor {email}: {str(e)}")
572
+ raise HTTPException(
573
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
574
+ detail=f"Failed to create doctor: {str(e)}"
575
+ )
576
 
577
  @router.post("/login", response_model=TokenResponse)
578
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
579
+ logger.info(f"Login attempt for email: {form_data.username}")
580
  user = await users_collection.find_one({"email": form_data.username.lower()})
581
  if not user or not verify_password(form_data.password, user["password"]):
582
+ logger.warning(f"Login failed for {form_data.username}: Invalid credentials")
583
  raise HTTPException(
584
  status_code=status.HTTP_401_UNAUTHORIZED,
585
  detail="Invalid credentials",
 
587
  )
588
 
589
  access_token = create_access_token(data={"sub": user["email"]})
590
+ logger.info(f"Successful login for {form_data.username}")
591
  return {
592
  "access_token": access_token,
593
  "token_type": "bearer",
 
596
 
597
  @router.get("/me")
598
  async def get_me(current_user: dict = Depends(get_current_user)):
599
+ logger.info(f"Fetching user profile for {current_user['email']}")
600
  user = await users_collection.find_one({"email": current_user["email"]})
601
  if not user:
602
+ logger.warning(f"User not found: {current_user['email']}")
603
  raise HTTPException(
604
  status_code=status.HTTP_404_NOT_FOUND,
605
  detail="User not found"
606
  )
607
 
608
+ response = {
609
  "id": str(user["_id"]),
610
  "email": user["email"],
611
  "full_name": user.get("full_name", ""),
 
613
  "specialty": user.get("specialty"),
614
  "created_at": user.get("created_at"),
615
  "updated_at": user.get("updated_at")
616
+ }
617
+ logger.info(f"User profile retrieved for {current_user['email']}")
618
+ return response