Ali2206 commited on
Commit
48462d9
·
verified ·
1 Parent(s): f39f16a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +458 -88
app.py CHANGED
@@ -1,104 +1,474 @@
1
- from fastapi import FastAPI, Request
2
- from fastapi.middleware.cors import CORSMiddleware
3
- from fastapi.responses import RedirectResponse
4
- from api import api_router
5
- import gradio as gr
6
- import requests
 
 
 
 
 
 
 
 
 
 
 
7
  import logging
 
 
8
 
9
- logging.basicConfig(level=logging.DEBUG)
10
- logger = logging.getLogger(__name__)
11
- logger.debug("Initializing application")
12
-
13
- app = FastAPI()
14
-
15
- # CORS
16
- app.add_middleware(
17
- CORSMiddleware,
18
- allow_origins=["*"],
19
- allow_credentials=True,
20
- allow_methods=["*"],
21
- allow_headers=["*"],
22
  )
 
23
 
24
- app.include_router(api_router)
25
 
26
- @app.get("/")
27
- def root():
28
- logger.debug("Root endpoint accessed")
29
- return {"message": "🚀 FastAPI with MongoDB + JWT is running."}
30
 
31
- # Redirect /login to /auth/login
32
- @app.post("/login")
33
- async def redirect_login(request: Request):
34
- logger.info("Redirecting /login to /auth/login")
35
- return RedirectResponse(url="/auth/login", status_code=307)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- # Gradio doctor creation logic
38
- BACKEND_URL = "https://rocketfarmstudios-cps-api.hf.space"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- def create_doctor(full_name, email, matricule, password, specialty):
41
- payload = {
42
- "full_name": full_name,
43
- "email": email,
44
- "license_number": matricule,
45
- "password": password,
46
- "specialty": specialty,
47
- }
 
 
 
 
 
 
 
 
48
  try:
49
- res = requests.post(f"{BACKEND_URL}/auth/admin/doctors", json=payload)
50
- if res.status_code == 201:
51
- return "✅ Doctor created successfully!"
52
- return f" {res.json().get('detail', 'Error occurred')}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  except Exception as e:
54
- return f" Network Error: {str(e)}"
 
 
 
 
55
 
56
- # Define Gradio interface as a separate function
57
- def setup_gradio():
58
- logger.debug("Setting up Gradio interface")
59
- with gr.Blocks(css="""
60
- .gradio-container {
61
- background-color: #1A1B1F;
62
- color: #E2E8F0;
63
- font-family: 'Segoe UI', sans-serif;
64
- padding: 3rem;
65
- }
66
- .title-text { text-align: center; font-size: 2rem; font-weight: 700; color: #37B6E9; margin-bottom: 0.5rem; }
67
- .description-text { text-align: center; font-size: 1rem; color: #A0AEC0; margin-bottom: 2rem; }
68
- .gr-box, .gr-form, .gr-column, .gr-panel { background-color: #2D2F36 !important; border-radius: 16px !important; padding: 2rem !important; max-width: 600px; margin: auto; box-shadow: 0 0 0 1px #3B3E47; }
69
- label { font-weight: 600; color: #F7FAFC; margin-bottom: 6px; }
70
- input, select, textarea { background-color: #1A1B1F !important; color: #F7FAFC !important; border: 1px solid #4A5568 !important; font-size: 14px; padding: 10px; border-radius: 10px; }
71
- button { background-color: #37B6E9 !important; color: #1A1B1F !important; border-radius: 10px !important; font-weight: 600; padding: 12px; width: 100%; margin-top: 1.5rem; }
72
- .output-box textarea { background-color: transparent !important; border: none; color: #90CDF4; font-size: 14px; margin-top: 1rem; }
73
- """) as admin_ui:
74
- gr.Markdown("<div class='title-text'>👨‍⚕️ Doctor Account Creator</div>")
75
- gr.Markdown("<div class='description-text'>Admins can register new doctors using this secure panel. Generated at 03:43 PM CET on Saturday, May 17, 2025.</div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- with gr.Column():
78
- full_name = gr.Textbox(label="Full Name", placeholder="e.g. Dr. Sarah Hopkins")
79
- email = gr.Textbox(label="Email", placeholder="e.g. doctor@clinic.org")
80
- matricule = gr.Textbox(label="Matricule", placeholder="e.g. DOC-1234")
81
- specialty = gr.Dropdown(
82
- label="Specialty",
83
- choices=["Cardiology", "Neurology", "Pediatrics", "Oncology", "General Practice", "Psychiatry", "Dermatology", "Orthopedics"],
84
- value="Cardiology"
 
 
 
 
 
 
 
 
 
 
 
85
  )
86
- password = gr.Textbox(label="Password", type="password", placeholder="Secure password")
87
- submit_btn = gr.Button("Create Doctor Account")
88
- output = gr.Textbox(label="", show_label=False, elem_classes=["output-box"])
 
 
 
 
 
 
 
 
 
 
89
 
90
- submit_btn.click(
91
- fn=create_doctor,
92
- inputs=[full_name, email, matricule, specialty, password],
93
- outputs=output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  )
95
- return admin_ui
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- # Mount Gradio at /admin
98
- if __name__ == "__main__":
99
- logger.debug("Running main block")
100
- admin_ui = setup_gradio()
101
- app = gr.mount_gradio_app(app, admin_ui, path="/admin")
102
- logger.debug("Gradio mounted, starting app")
103
- import uvicorn
104
- uvicorn.run(app, host="0.0.0.0", port=7860) # Hugging Face Spaces default port
 
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)
385
+ async def get_patient(
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)},
442
+ {"fhir_id": patient_id}
443
+ ]},
444
+ {
445
+ "$push": {"notes": note_data},
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