Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions backend/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ class Settings(BaseSettings):
PrescriptionCreate,
PrescriptionUpdate,
PrescriptionResponse,
DoseStatus,
MedicationDose,
MedicationDoseCreate,
MedicationDoseResponse,
)
from .risk_calculator import calculate_health_risk
from .health_summary import build_health_summary
Expand Down Expand Up @@ -183,6 +187,7 @@ async def lifespan(app: FastAPI):
Questionnaire,
HealthRiskAssessment,
Prescription,
MedicationDose,
],
)
print("Database connection established.")
Expand Down Expand Up @@ -811,3 +816,70 @@ async def archive_prescription(
return SuccessResponse(
data=PrescriptionResponse.model_validate(prescription)
)


@app.post(
"/api/v1/prescriptions/{prescription_id}/doses",
response_model=SuccessResponse[MedicationDoseResponse],
)
async def record_medication_dose(
prescription_id: uuid.UUID,
dose_data: MedicationDoseCreate,
current_user: Annotated[User, Depends(require_health_consent)],
):
prescription = await Prescription.find_one(
Prescription.id == prescription_id,
Prescription.user_id == current_user.id,
Prescription.archived == False,
)

if not prescription:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Prescription not found",
)

dose = MedicationDose(
user_id=current_user.id,
prescription_id=prescription.id,
status=dose_data.status,
)
await dose.insert()

return SuccessResponse(
data=MedicationDoseResponse.model_validate(dose)
)


@app.get(
"/api/v1/prescriptions/{prescription_id}/doses",
response_model=SuccessResponse[list[MedicationDoseResponse]],
)
async def get_medication_doses(
prescription_id: uuid.UUID,
current_user: Annotated[User, Depends(require_health_consent)],
):
prescription = await Prescription.find_one(
Prescription.id == prescription_id,
Prescription.user_id == current_user.id,
Prescription.archived == False,
)

if not prescription:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Prescription not found",
)

doses = await MedicationDose.find(
MedicationDose.user_id == current_user.id,
MedicationDose.prescription_id == prescription.id,
).sort(-MedicationDose.recorded_at).to_list()

return SuccessResponse(
data=[
MedicationDoseResponse.model_validate(dose)
for dose in doses
]
)

32 changes: 32 additions & 0 deletions backend/src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,3 +451,35 @@ class PrescriptionResponse(BaseModel):

class Config:
from_attributes = True


class DoseStatus(str, Enum):
taken = "taken"
skipped = "skipped"


class MedicationDose(Document):
id: uuid.UUID = Field(default_factory=uuid.uuid4)
user_id: uuid.UUID
prescription_id: uuid.UUID
status: DoseStatus
recorded_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc)
)

class Settings:
name = "medication_doses"


class MedicationDoseCreate(BaseModel):
status: DoseStatus


class MedicationDoseResponse(BaseModel):
id: uuid.UUID
prescription_id: uuid.UUID
status: DoseStatus
recorded_at: datetime

class Config:
from_attributes = True
142 changes: 141 additions & 1 deletion backend/tests/test_prescriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
GenderEnum,
Prescription,
User,
MedicationDose,
DoseStatus,
)


Expand All @@ -25,7 +27,7 @@ async def test_db():

await init_beanie(
database=database,
document_models=[User, Prescription],
document_models=[User, Prescription, MedicationDose],
)

yield database
Expand Down Expand Up @@ -429,3 +431,141 @@ async def test_archived_prescription_cannot_be_archived_again(
)

assert response.status_code == 404


@pytest.mark.anyio
async def test_record_taken_dose(client, test_user):
prescription = Prescription(
user_id=test_user.id,
medication_name="Test Medication",
dosage="10mg",
frequency="Once daily",
)
await prescription.insert()

response = await client.post(
f"/api/v1/prescriptions/{prescription.id}/doses",
json={"status": "taken"},
)

assert response.status_code == 200
data = response.json()["data"]
assert data["status"] == "taken"
assert data["prescription_id"] == str(prescription.id)
assert data["recorded_at"] is not None

doses = await MedicationDose.find(
MedicationDose.user_id == test_user.id,
MedicationDose.prescription_id == prescription.id,
).to_list()
assert len(doses) == 1
assert doses[0].status == DoseStatus.taken


@pytest.mark.anyio
async def test_record_skipped_dose(client, test_user):
prescription = Prescription(
user_id=test_user.id,
medication_name="Test Medication",
dosage="10mg",
frequency="Once daily",
)
await prescription.insert()

response = await client.post(
f"/api/v1/prescriptions/{prescription.id}/doses",
json={"status": "skipped"},
)

assert response.status_code == 200
assert response.json()["data"]["status"] == "skipped"


@pytest.mark.anyio
async def test_invalid_dose_status_is_rejected(client, test_user):
prescription = Prescription(
user_id=test_user.id,
medication_name="Test Medication",
dosage="10mg",
frequency="Once daily",
)
await prescription.insert()

response = await client.post(
f"/api/v1/prescriptions/{prescription.id}/doses",
json={"status": "maybe"},
)

assert response.status_code == 422


@pytest.mark.anyio
async def test_cannot_record_dose_for_another_users_prescription(
client, other_user
):
prescription = Prescription(
user_id=other_user.id,
medication_name="Other User Medication",
dosage="10mg",
frequency="Once daily",
)
await prescription.insert()

response = await client.post(
f"/api/v1/prescriptions/{prescription.id}/doses",
json={"status": "taken"},
)

assert response.status_code == 404

doses = await MedicationDose.find(
MedicationDose.prescription_id == prescription.id,
).to_list()
assert doses == []


@pytest.mark.anyio
async def test_get_dose_history(client, test_user):
prescription = Prescription(
user_id=test_user.id,
medication_name="Test Medication",
dosage="10mg",
frequency="Once daily",
)
await prescription.insert()

for dose_status in (DoseStatus.taken, DoseStatus.skipped):
await MedicationDose(
user_id=test_user.id,
prescription_id=prescription.id,
status=dose_status,
).insert()

response = await client.get(
f"/api/v1/prescriptions/{prescription.id}/doses"
)

assert response.status_code == 200
records = response.json()["data"]
assert len(records) == 2
assert {record["status"] for record in records} == {"taken", "skipped"}


@pytest.mark.anyio
async def test_cannot_get_dose_history_for_another_users_prescription(
client, other_user
):
prescription = Prescription(
user_id=other_user.id,
medication_name="Other User Medication",
dosage="10mg",
frequency="Once daily",
)
await prescription.insert()

response = await client.get(
f"/api/v1/prescriptions/{prescription.id}/doses"
)

assert response.status_code == 404

16 changes: 15 additions & 1 deletion frontend/public/locales/ar/modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,19 @@
"retry": "إعادة المحاولة",
"confirmArchive": "هل أنت متأكد من أنك تريد أرشفة هذه الوصفة الطبية؟",
"prescriptionRequiredFields": "يرجى ملء جميع الحقول المطلوبة.",
"prescriptionValidationError": "يرجى مراجعة المدخلات وتصحيح أي حقول غير صالحة."
"prescriptionValidationError": "يرجى مراجعة المدخلات وتصحيح أي حقول غير صالحة.",
"doseTracking": "الالتزام بتناول الدواء",
"recordTaken": "تحديد الجرعة على أنها تم تناولها",
"recordSkipped": "تحديد الجرعة على أنها فائتة",
"doseHistory": "سجل الجرعات",
"loadingDoseHistory": "جارٍ تحميل سجل الجرعات...",
"noDoseHistory": "لم يتم تسجيل أي جرعات بعد.",
"doseRecordedTaken": "تم تسجيل الجرعة على أنها تم تناولها.",
"doseRecordedSkipped": "تم تسجيل الجرعة على أنها فائتة.",
"doseRecordError": "تعذر تسجيل هذه الجرعة. يرجى المحاولة مرة أخرى.",
"doseHistoryError": "تعذر تحميل سجل الجرعات. يرجى المحاولة مرة أخرى.",
"recordingDose": "جارٍ التسجيل...",
"taken": "تم تناولها",
"skipped": "تم تخطيها",
"recordedAt": "وقت التسجيل"
}
16 changes: 15 additions & 1 deletion frontend/public/locales/en/modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,19 @@
"retry": "Retry",
"confirmArchive": "Are you sure you want to archive this prescription?",
"prescriptionRequiredFields": "Please fill in all required fields.",
"prescriptionValidationError": "Please check your entries and correct any invalid fields."
"prescriptionValidationError": "Please check your entries and correct any invalid fields.",
"doseTracking": "Medication adherence",
"recordTaken": "Mark as taken",
"recordSkipped": "Mark as skipped",
"doseHistory": "Dose history",
"loadingDoseHistory": "Loading dose history...",
"noDoseHistory": "No doses recorded yet.",
"doseRecordedTaken": "Dose recorded as taken.",
"doseRecordedSkipped": "Dose recorded as skipped.",
"doseRecordError": "Could not record this dose. Please try again.",
"doseHistoryError": "Could not load dose history. Please try again.",
"recordingDose": "Recording...",
"taken": "Taken",
"skipped": "Skipped",
"recordedAt": "Recorded"
}
16 changes: 15 additions & 1 deletion frontend/public/locales/es/modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,19 @@
"retry": "Reintentar",
"confirmArchive": "¿Está seguro de que desea archivar esta receta?",
"prescriptionRequiredFields": "Completa todos los campos obligatorios.",
"prescriptionValidationError": "Revisa tus datos y corrige los campos no válidos."
"prescriptionValidationError": "Revisa tus datos y corrige los campos no válidos.",
"doseTracking": "Adherencia a la medicación",
"recordTaken": "Marcar como tomada",
"recordSkipped": "Marcar como omitida",
"doseHistory": "Historial de dosis",
"loadingDoseHistory": "Cargando el historial de dosis...",
"noDoseHistory": "Todavía no hay dosis registradas.",
"doseRecordedTaken": "Dosis registrada como tomada.",
"doseRecordedSkipped": "Dosis registrada como omitida.",
"doseRecordError": "No se pudo registrar esta dosis. Inténtalo de nuevo.",
"doseHistoryError": "No se pudo cargar el historial de dosis. Inténtalo de nuevo.",
"recordingDose": "Registrando...",
"taken": "Tomada",
"skipped": "Omitida",
"recordedAt": "Registrada"
}
16 changes: 15 additions & 1 deletion frontend/public/locales/hi/modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,19 @@
"retry": "पुनः प्रयास करें",
"confirmArchive": "क्या आप वाकई इस नुस्खे को संग्रहित करना चाहते हैं?",
"prescriptionRequiredFields": "कृपया सभी आवश्यक फ़ील्ड भरें।",
"prescriptionValidationError": "कृपया अपनी प्रविष्टियाँ जाँचें और अमान्य फ़ील्ड ठीक करें।"
"prescriptionValidationError": "कृपया अपनी प्रविष्टियाँ जाँचें और अमान्य फ़ील्ड ठीक करें।",
"doseTracking": "दवा लेने का पालन",
"recordTaken": "ली गई के रूप में चिह्नित करें",
"recordSkipped": "छोड़ी गई के रूप में चिह्नित करें",
"doseHistory": "दवा की खुराक का इतिहास",
"loadingDoseHistory": "खुराक का इतिहास लोड हो रहा है...",
"noDoseHistory": "अभी तक कोई खुराक दर्ज नहीं की गई है।",
"doseRecordedTaken": "खुराक को ली गई के रूप में दर्ज किया गया।",
"doseRecordedSkipped": "खुराक को छोड़ी गई के रूप में दर्ज किया गया।",
"doseRecordError": "यह खुराक दर्ज नहीं हो सकी। कृपया पुनः प्रयास करें।",
"doseHistoryError": "खुराक का इतिहास लोड नहीं हो सका। कृपया पुनः प्रयास करें।",
"recordingDose": "दर्ज किया जा रहा है...",
"taken": "ली गई",
"skipped": "छोड़ी गई",
"recordedAt": "दर्ज किया गया"
}
16 changes: 15 additions & 1 deletion frontend/public/locales/zh/modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,19 @@
"retry": "重试",
"confirmArchive": "您确定要归档此处方吗?",
"prescriptionRequiredFields": "请填写所有必填字段。",
"prescriptionValidationError": "请检查您填写的内容并更正无效字段。"
"prescriptionValidationError": "请检查您填写的内容并更正无效字段。",
"doseTracking": "用药依从性",
"recordTaken": "标记为已服用",
"recordSkipped": "标记为跳过",
"doseHistory": "服药记录",
"loadingDoseHistory": "正在加载服药记录...",
"noDoseHistory": "尚无服药记录。",
"doseRecordedTaken": "已记录为服用。",
"doseRecordedSkipped": "已记录为跳过。",
"doseRecordError": "无法记录此次服药,请重试。",
"doseHistoryError": "无法加载服药记录,请重试。",
"recordingDose": "正在记录...",
"taken": "已服用",
"skipped": "已跳过",
"recordedAt": "记录时间"
}
Loading
Loading