Compare commits
No commits in common. "8bcb03cd0945259efb95c9f825a1c979d34b29ec" and "68a50b045758294d0f4190581c7155e906715bba" have entirely different histories.
8bcb03cd09
...
68a50b0457
3 changed files with 65 additions and 414 deletions
|
|
@ -2,6 +2,7 @@ use axum::{
|
||||||
extract::{Extension, Json, Path, Query, State},
|
extract::{Extension, Json, Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
};
|
};
|
||||||
|
use mongodb::bson::oid::ObjectId;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -9,7 +10,7 @@ use crate::{
|
||||||
config::AppState,
|
config::AppState,
|
||||||
models::medication::{
|
models::medication::{
|
||||||
CreateMedicationRequest, LogDoseRequest, Medication, MedicationDose, MedicationRepository,
|
CreateMedicationRequest, LogDoseRequest, Medication, MedicationDose, MedicationRepository,
|
||||||
MedicationResponse, UpdateMedicationRequest,
|
UpdateMedicationRequest,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -24,7 +25,7 @@ pub async fn create_medication(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Extension(claims): Extension<Claims>,
|
Extension(claims): Extension<Claims>,
|
||||||
Json(req): Json<CreateMedicationRequest>,
|
Json(req): Json<CreateMedicationRequest>,
|
||||||
) -> Result<Json<MedicationResponse>, StatusCode> {
|
) -> Result<Json<Medication>, StatusCode> {
|
||||||
let database = state.db.get_database();
|
let database = state.db.get_database();
|
||||||
let repo = MedicationRepository::new(database.collection("medications"));
|
let repo = MedicationRepository::new(database.collection("medications"));
|
||||||
|
|
||||||
|
|
@ -76,7 +77,7 @@ pub async fn create_medication(
|
||||||
};
|
};
|
||||||
|
|
||||||
match repo.create(medication).await {
|
match repo.create(medication).await {
|
||||||
Ok(med) => Ok(Json(med.into())),
|
Ok(med) => Ok(Json(med)),
|
||||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,17 +86,14 @@ pub async fn list_medications(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Extension(claims): Extension<Claims>,
|
Extension(claims): Extension<Claims>,
|
||||||
Query(query): Query<ListMedicationsQuery>,
|
Query(query): Query<ListMedicationsQuery>,
|
||||||
) -> Result<Json<Vec<MedicationResponse>>, StatusCode> {
|
) -> Result<Json<Vec<Medication>>, StatusCode> {
|
||||||
let database = state.db.get_database();
|
let database = state.db.get_database();
|
||||||
let repo = MedicationRepository::new(database.collection("medications"));
|
let repo = MedicationRepository::new(database.collection("medications"));
|
||||||
|
|
||||||
let _limit = query.limit.unwrap_or(100);
|
let _limit = query.limit.unwrap_or(100);
|
||||||
|
|
||||||
match repo.find_by_user(&claims.sub).await {
|
match repo.find_by_user(&claims.sub).await {
|
||||||
Ok(medications) => {
|
Ok(medications) => Ok(Json(medications)),
|
||||||
let resp: Vec<MedicationResponse> = medications.into_iter().map(Into::into).collect();
|
|
||||||
Ok(Json(resp))
|
|
||||||
}
|
|
||||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,16 +102,17 @@ pub async fn get_medication(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Extension(_claims): Extension<Claims>,
|
Extension(_claims): Extension<Claims>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Result<Json<MedicationResponse>, StatusCode> {
|
) -> Result<Json<Medication>, StatusCode> {
|
||||||
let database = state.db.get_database();
|
let database = state.db.get_database();
|
||||||
let repo = MedicationRepository::new(database.collection("medications"));
|
let repo = MedicationRepository::new(database.collection("medications"));
|
||||||
|
|
||||||
// The path param is the application-level medication_id (a UUID), not the
|
match ObjectId::parse_str(&id) {
|
||||||
// Mongo _id, so look it up directly instead of parsing as ObjectId.
|
Ok(oid) => match repo.find_by_id(&oid).await {
|
||||||
match repo.find_by_medication_id(&id).await {
|
Ok(Some(medication)) => Ok(Json(medication)),
|
||||||
Ok(Some(medication)) => Ok(Json(medication.into())),
|
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
},
|
||||||
|
Err(_) => Err(StatusCode::BAD_REQUEST),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,15 +121,17 @@ pub async fn update_medication(
|
||||||
Extension(_claims): Extension<Claims>,
|
Extension(_claims): Extension<Claims>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(req): Json<UpdateMedicationRequest>,
|
Json(req): Json<UpdateMedicationRequest>,
|
||||||
) -> Result<Json<MedicationResponse>, StatusCode> {
|
) -> Result<Json<Medication>, StatusCode> {
|
||||||
let database = state.db.get_database();
|
let database = state.db.get_database();
|
||||||
let repo = MedicationRepository::new(database.collection("medications"));
|
let repo = MedicationRepository::new(database.collection("medications"));
|
||||||
|
|
||||||
// Look up by medication_id (UUID) — the path param is not a Mongo ObjectId.
|
match ObjectId::parse_str(&id) {
|
||||||
match repo.update_by_medication_id(&id, req).await {
|
Ok(oid) => match repo.update(&oid, req).await {
|
||||||
Ok(Some(medication)) => Ok(Json(medication.into())),
|
Ok(Some(medication)) => Ok(Json(medication)),
|
||||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
},
|
||||||
|
Err(_) => Err(StatusCode::BAD_REQUEST),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,11 +143,13 @@ pub async fn delete_medication(
|
||||||
let database = state.db.get_database();
|
let database = state.db.get_database();
|
||||||
let repo = MedicationRepository::new(database.collection("medications"));
|
let repo = MedicationRepository::new(database.collection("medications"));
|
||||||
|
|
||||||
// Look up by medication_id (UUID), not Mongo _id.
|
match ObjectId::parse_str(&id) {
|
||||||
match repo.delete_by_medication_id(&id).await {
|
Ok(oid) => match repo.delete(&oid).await {
|
||||||
Ok(true) => Ok(StatusCode::NO_CONTENT),
|
Ok(true) => Ok(StatusCode::NO_CONTENT),
|
||||||
Ok(false) => Err(StatusCode::NOT_FOUND),
|
Ok(false) => Err(StatusCode::NOT_FOUND),
|
||||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
},
|
||||||
|
Err(_) => Err(StatusCode::BAD_REQUEST),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,104 +96,6 @@ pub struct AdherenceStats {
|
||||||
// MEDICATION MODEL (Existing + Phase 2.8 updates)
|
// MEDICATION MODEL (Existing + Phase 2.8 updates)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// The structured content stored (as a JSON string) inside `medication_data.data`.
|
|
||||||
/// Deserializes from the camelCase keys the create handler packs in. This is the
|
|
||||||
/// intermediate type used to flatten a `Medication` into a `MedicationResponse`.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Default)]
|
|
||||||
pub struct MedicationData {
|
|
||||||
pub name: String,
|
|
||||||
pub dosage: String,
|
|
||||||
pub frequency: String,
|
|
||||||
pub route: String,
|
|
||||||
pub reason: Option<String>,
|
|
||||||
pub instructions: Option<String>,
|
|
||||||
#[serde(rename = "sideEffects", default)]
|
|
||||||
pub side_effects: Vec<String>,
|
|
||||||
#[serde(rename = "prescribedBy")]
|
|
||||||
pub prescribed_by: Option<String>,
|
|
||||||
#[serde(rename = "prescribedDate")]
|
|
||||||
pub prescribed_date: Option<String>,
|
|
||||||
#[serde(rename = "startDate")]
|
|
||||||
pub start_date: Option<String>,
|
|
||||||
#[serde(rename = "endDate")]
|
|
||||||
pub end_date: Option<String>,
|
|
||||||
pub notes: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub tags: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The flat, snake_case medication representation returned by the API. Built from
|
|
||||||
/// a stored `Medication` by deserializing its `medication_data.data` blob.
|
|
||||||
///
|
|
||||||
/// NOTE: `active` is synthesized as `true` for now — the storage model does not
|
|
||||||
/// track active/inactive (TODO). Timestamps are ISO 8601 strings.
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct MedicationResponse {
|
|
||||||
pub id: String,
|
|
||||||
pub medication_id: String,
|
|
||||||
pub user_id: String,
|
|
||||||
pub profile_id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub dosage: String,
|
|
||||||
pub frequency: String,
|
|
||||||
pub route: String,
|
|
||||||
pub reason: Option<String>,
|
|
||||||
pub instructions: Option<String>,
|
|
||||||
pub side_effects: Vec<String>,
|
|
||||||
pub prescribed_by: Option<String>,
|
|
||||||
pub prescribed_date: Option<String>,
|
|
||||||
pub start_date: Option<String>,
|
|
||||||
pub end_date: Option<String>,
|
|
||||||
pub notes: Option<String>,
|
|
||||||
pub tags: Vec<String>,
|
|
||||||
pub active: bool,
|
|
||||||
pub created_at: String,
|
|
||||||
pub updated_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::convert::From<Medication> for MedicationResponse {
|
|
||||||
fn from(m: Medication) -> Self {
|
|
||||||
// The stored data is a JSON string; tolerate old/malformed rows by
|
|
||||||
// falling back to empty defaults rather than failing the whole response.
|
|
||||||
let data: MedicationData =
|
|
||||||
serde_json::from_str(&m.medication_data.data).unwrap_or_default();
|
|
||||||
|
|
||||||
MedicationResponse {
|
|
||||||
id: m.id.map(|o| o.to_hex()).unwrap_or_default(),
|
|
||||||
medication_id: m.medication_id,
|
|
||||||
user_id: m.user_id,
|
|
||||||
profile_id: m.profile_id,
|
|
||||||
name: data.name,
|
|
||||||
dosage: data.dosage,
|
|
||||||
frequency: data.frequency,
|
|
||||||
route: data.route,
|
|
||||||
reason: data.reason,
|
|
||||||
instructions: data.instructions,
|
|
||||||
side_effects: data.side_effects,
|
|
||||||
prescribed_by: data.prescribed_by,
|
|
||||||
prescribed_date: data.prescribed_date,
|
|
||||||
start_date: data.start_date,
|
|
||||||
end_date: data.end_date,
|
|
||||||
notes: data.notes,
|
|
||||||
tags: data.tags,
|
|
||||||
active: true, // TODO: persist a real active flag.
|
|
||||||
created_at: system_time_to_rfc3339(m.created_at.to_system_time()),
|
|
||||||
updated_at: system_time_to_rfc3339(m.updated_at.to_system_time()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format a SystemTime as an ISO 8601 (RFC 3339) string.
|
|
||||||
fn system_time_to_rfc3339(t: std::time::SystemTime) -> String {
|
|
||||||
let secs = t
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.map(|d| d.as_secs() as i64)
|
|
||||||
.unwrap_or(0);
|
|
||||||
chrono::DateTime::from_timestamp(secs, 0)
|
|
||||||
.map(|dt| dt.to_rfc3339())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Medication {
|
pub struct Medication {
|
||||||
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -362,208 +264,64 @@ impl MedicationRepository {
|
||||||
Ok(medication)
|
Ok(medication)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up by the application-level medication_id (a UUID string), which is
|
|
||||||
/// what the API exposes in URLs. (find_by_id filters on the Mongo _id.)
|
|
||||||
pub async fn find_by_medication_id(
|
|
||||||
&self,
|
|
||||||
medication_id: &str,
|
|
||||||
) -> Result<Option<Medication>, Box<dyn std::error::Error>> {
|
|
||||||
let filter = doc! { "medicationId": medication_id };
|
|
||||||
let medication = self.collection.find_one(filter, None).await?;
|
|
||||||
Ok(medication)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update by the application-level medication_id (UUID). The API exposes
|
|
||||||
/// medication_id in URLs, not the Mongo _id.
|
|
||||||
pub async fn update_by_medication_id(
|
|
||||||
&self,
|
|
||||||
medication_id: &str,
|
|
||||||
updates: UpdateMedicationRequest,
|
|
||||||
) -> Result<Option<Medication>, Box<dyn std::error::Error>> {
|
|
||||||
let filter = doc! { "medicationId": medication_id };
|
|
||||||
let existing = self.collection.find_one(filter.clone(), None).await?;
|
|
||||||
let Some(existing) = existing else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut data: MedicationData =
|
|
||||||
serde_json::from_str(&existing.medication_data.data).unwrap_or_default();
|
|
||||||
|
|
||||||
if let Some(v) = updates.name {
|
|
||||||
data.name = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.dosage {
|
|
||||||
data.dosage = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.frequency {
|
|
||||||
data.frequency = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.route {
|
|
||||||
data.route = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.reason {
|
|
||||||
data.reason = Some(v);
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.instructions {
|
|
||||||
data.instructions = Some(v);
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.side_effects {
|
|
||||||
data.side_effects = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.prescribed_by {
|
|
||||||
data.prescribed_by = Some(v);
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.prescribed_date {
|
|
||||||
data.prescribed_date = Some(v);
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.start_date {
|
|
||||||
data.start_date = Some(v);
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.end_date {
|
|
||||||
data.end_date = Some(v);
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.notes {
|
|
||||||
data.notes = Some(v);
|
|
||||||
}
|
|
||||||
if let Some(v) = updates.tags {
|
|
||||||
data.tags = v;
|
|
||||||
}
|
|
||||||
// NOTE: updates.active is accepted by the API but not yet persisted.
|
|
||||||
|
|
||||||
let data_json = serde_json::json!({
|
|
||||||
"name": data.name,
|
|
||||||
"dosage": data.dosage,
|
|
||||||
"frequency": data.frequency,
|
|
||||||
"route": data.route,
|
|
||||||
"reason": data.reason,
|
|
||||||
"instructions": data.instructions,
|
|
||||||
"sideEffects": data.side_effects,
|
|
||||||
"prescribedBy": data.prescribed_by,
|
|
||||||
"prescribedDate": data.prescribed_date,
|
|
||||||
"startDate": data.start_date,
|
|
||||||
"endDate": data.end_date,
|
|
||||||
"notes": data.notes,
|
|
||||||
"tags": data.tags,
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let mut update_doc = doc! {
|
|
||||||
"medicationData.data": data_json,
|
|
||||||
"updatedAt": DateTime::now(),
|
|
||||||
};
|
|
||||||
if let Some(pill_identification) = updates.pill_identification {
|
|
||||||
if let Ok(pill_doc) = mongodb::bson::to_document(&pill_identification) {
|
|
||||||
update_doc.insert("pill_identification", pill_doc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let medication = self
|
|
||||||
.collection
|
|
||||||
.find_one_and_update(filter, doc! { "$set": update_doc }, None)
|
|
||||||
.await?;
|
|
||||||
Ok(medication)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete by the application-level medication_id (UUID).
|
|
||||||
pub async fn delete_by_medication_id(
|
|
||||||
&self,
|
|
||||||
medication_id: &str,
|
|
||||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
||||||
let filter = doc! { "medicationId": medication_id };
|
|
||||||
let result = self.collection.delete_one(filter, None).await?;
|
|
||||||
Ok(result.deleted_count > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update(
|
pub async fn update(
|
||||||
&self,
|
&self,
|
||||||
id: &ObjectId,
|
id: &ObjectId,
|
||||||
updates: UpdateMedicationRequest,
|
updates: UpdateMedicationRequest,
|
||||||
) -> Result<Option<Medication>, Box<dyn std::error::Error>> {
|
) -> Result<Option<Medication>, Box<dyn std::error::Error>> {
|
||||||
// The stored shape is `medicationData: { data: "<JSON string>", ... }`.
|
let mut update_doc = doc! {};
|
||||||
// The previous implementation used dot-notation ("medicationData.name")
|
|
||||||
// which wrote into phantom paths. Instead: load the existing document,
|
|
||||||
// deserialize its data blob, apply the overrides, re-serialize, and
|
|
||||||
// $set the whole `medicationData.data` string.
|
|
||||||
let filter = doc! { "_id": id };
|
|
||||||
let existing = self.collection.find_one(filter.clone(), None).await?;
|
|
||||||
let Some(existing) = existing else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut data: MedicationData =
|
if let Some(name) = updates.name {
|
||||||
serde_json::from_str(&existing.medication_data.data).unwrap_or_default();
|
update_doc.insert("medicationData.name", name);
|
||||||
|
|
||||||
if let Some(v) = updates.name {
|
|
||||||
data.name = v;
|
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.dosage {
|
if let Some(dosage) = updates.dosage {
|
||||||
data.dosage = v;
|
update_doc.insert("medicationData.dosage", dosage);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.frequency {
|
if let Some(frequency) = updates.frequency {
|
||||||
data.frequency = v;
|
update_doc.insert("medicationData.frequency", frequency);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.route {
|
if let Some(route) = updates.route {
|
||||||
data.route = v;
|
update_doc.insert("medicationData.route", route);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.reason {
|
if let Some(reason) = updates.reason {
|
||||||
data.reason = Some(v);
|
update_doc.insert("medicationData.reason", reason);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.instructions {
|
if let Some(instructions) = updates.instructions {
|
||||||
data.instructions = Some(v);
|
update_doc.insert("medicationData.instructions", instructions);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.side_effects {
|
if let Some(side_effects) = updates.side_effects {
|
||||||
data.side_effects = v;
|
update_doc.insert("medicationData.sideEffects", side_effects);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.prescribed_by {
|
if let Some(prescribed_by) = updates.prescribed_by {
|
||||||
data.prescribed_by = Some(v);
|
update_doc.insert("medicationData.prescribedBy", prescribed_by);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.prescribed_date {
|
if let Some(prescribed_date) = updates.prescribed_date {
|
||||||
data.prescribed_date = Some(v);
|
update_doc.insert("medicationData.prescribedDate", prescribed_date);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.start_date {
|
if let Some(start_date) = updates.start_date {
|
||||||
data.start_date = Some(v);
|
update_doc.insert("medicationData.startDate", start_date);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.end_date {
|
if let Some(end_date) = updates.end_date {
|
||||||
data.end_date = Some(v);
|
update_doc.insert("medicationData.endDate", end_date);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.notes {
|
if let Some(notes) = updates.notes {
|
||||||
data.notes = Some(v);
|
update_doc.insert("medicationData.notes", notes);
|
||||||
}
|
}
|
||||||
if let Some(v) = updates.tags {
|
if let Some(tags) = updates.tags {
|
||||||
data.tags = v;
|
update_doc.insert("medicationData.tags", tags);
|
||||||
|
}
|
||||||
|
if let Some(reminder_times) = updates.reminder_times {
|
||||||
|
update_doc.insert("reminderTimes", reminder_times);
|
||||||
}
|
}
|
||||||
// NOTE: updates.active is accepted by the API but not yet persisted —
|
|
||||||
// the storage model has no active column (TODO).
|
|
||||||
|
|
||||||
// Re-serialize with the SAME camelCase keys the create handler uses, so
|
|
||||||
// reads stay consistent.
|
|
||||||
let data_json = serde_json::json!({
|
|
||||||
"name": data.name,
|
|
||||||
"dosage": data.dosage,
|
|
||||||
"frequency": data.frequency,
|
|
||||||
"route": data.route,
|
|
||||||
"reason": data.reason,
|
|
||||||
"instructions": data.instructions,
|
|
||||||
"sideEffects": data.side_effects,
|
|
||||||
"prescribedBy": data.prescribed_by,
|
|
||||||
"prescribedDate": data.prescribed_date,
|
|
||||||
"startDate": data.start_date,
|
|
||||||
"endDate": data.end_date,
|
|
||||||
"notes": data.notes,
|
|
||||||
"tags": data.tags,
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let mut update_doc = doc! {
|
|
||||||
"medicationData.data": data_json,
|
|
||||||
"updatedAt": DateTime::now(),
|
|
||||||
};
|
|
||||||
if let Some(pill_identification) = updates.pill_identification {
|
if let Some(pill_identification) = updates.pill_identification {
|
||||||
if let Ok(pill_doc) = mongodb::bson::to_document(&pill_identification) {
|
if let Ok(pill_doc) = mongodb::bson::to_document(&pill_identification) {
|
||||||
update_doc.insert("pill_identification", pill_doc);
|
update_doc.insert("pillIdentification", pill_doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_doc.insert("updatedAt", mongodb::bson::DateTime::now());
|
||||||
|
|
||||||
|
let filter = doc! { "_id": id };
|
||||||
let medication = self
|
let medication = self
|
||||||
.collection
|
.collection
|
||||||
.find_one_and_update(filter, doc! { "$set": update_doc }, None)
|
.find_one_and_update(filter, doc! { "$set": update_doc }, None)
|
||||||
|
|
@ -581,104 +339,3 @@ impl MedicationRepository {
|
||||||
// querying the medication_doses collection directly (it needs a different
|
// querying the medication_doses collection directly (it needs a different
|
||||||
// collection than the medications this repository wraps).
|
// collection than the medications this repository wraps).
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::models::health_data::EncryptedField;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn medication_data_deserializes_camel_case_blob() {
|
|
||||||
// This is the exact shape the create handler packs into the data string.
|
|
||||||
let json = r#"{
|
|
||||||
"name": "Ibuprofen",
|
|
||||||
"dosage": "200mg",
|
|
||||||
"frequency": "as needed",
|
|
||||||
"route": "oral",
|
|
||||||
"reason": null,
|
|
||||||
"instructions": "with food",
|
|
||||||
"sideEffects": ["nausea"],
|
|
||||||
"prescribedBy": "Dr. House",
|
|
||||||
"prescribedDate": "2026-01-01",
|
|
||||||
"startDate": "2026-01-02",
|
|
||||||
"endDate": null,
|
|
||||||
"notes": "take care",
|
|
||||||
"tags": ["pain"]
|
|
||||||
}"#;
|
|
||||||
let data: MedicationData = serde_json::from_str(json).unwrap();
|
|
||||||
assert_eq!(data.name, "Ibuprofen");
|
|
||||||
assert_eq!(data.side_effects, vec!["nausea".to_string()]);
|
|
||||||
assert_eq!(data.prescribed_by.as_deref(), Some("Dr. House"));
|
|
||||||
assert_eq!(data.tags, vec!["pain".to_string()]);
|
|
||||||
assert_eq!(data.route, "oral");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn medication_data_defaults_empty_arrays_for_missing_keys() {
|
|
||||||
// Old rows may lack sideEffects/tags; the #[default] should yield empty vecs.
|
|
||||||
let json = r#"{"name":"X","dosage":"1","frequency":"daily","route":"oral"}"#;
|
|
||||||
let data: MedicationData = serde_json::from_str(json).unwrap();
|
|
||||||
assert!(data.side_effects.is_empty());
|
|
||||||
assert!(data.tags.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn medication_response_flattens_and_uses_snake_case_fields() {
|
|
||||||
let blob = serde_json::json!({
|
|
||||||
"name": "Aspirin", "dosage": "100mg", "frequency": "daily", "route": "oral",
|
|
||||||
"reason": null, "instructions": null,
|
|
||||||
"sideEffects": ["bleeding"], "prescribedBy": null, "prescribedDate": null,
|
|
||||||
"startDate": null, "endDate": null, "notes": null, "tags": []
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let med = Medication {
|
|
||||||
id: Some(ObjectId::new()),
|
|
||||||
medication_id: "med-123".to_string(),
|
|
||||||
user_id: "user-1".to_string(),
|
|
||||||
profile_id: "profile_user-1".to_string(),
|
|
||||||
medication_data: EncryptedField {
|
|
||||||
data: blob,
|
|
||||||
encrypted: false,
|
|
||||||
iv: String::new(),
|
|
||||||
auth_tag: String::new(),
|
|
||||||
},
|
|
||||||
reminders: vec![],
|
|
||||||
created_at: DateTime::now(),
|
|
||||||
updated_at: DateTime::now(),
|
|
||||||
pill_identification: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let resp: MedicationResponse = med.into();
|
|
||||||
assert_eq!(resp.medication_id, "med-123");
|
|
||||||
assert_eq!(resp.name, "Aspirin");
|
|
||||||
assert_eq!(resp.side_effects, vec!["bleeding".to_string()]);
|
|
||||||
assert!(resp.active);
|
|
||||||
assert!(!resp.id.is_empty());
|
|
||||||
assert!(!resp.created_at.is_empty()); // ISO string
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn medication_response_tolerates_malformed_data_blob() {
|
|
||||||
let med = Medication {
|
|
||||||
id: None,
|
|
||||||
medication_id: "m".to_string(),
|
|
||||||
user_id: "u".to_string(),
|
|
||||||
profile_id: "p".to_string(),
|
|
||||||
medication_data: EncryptedField {
|
|
||||||
data: "not valid json".to_string(),
|
|
||||||
encrypted: false,
|
|
||||||
iv: String::new(),
|
|
||||||
auth_tag: String::new(),
|
|
||||||
},
|
|
||||||
reminders: vec![],
|
|
||||||
created_at: DateTime::now(),
|
|
||||||
updated_at: DateTime::now(),
|
|
||||||
pill_identification: None,
|
|
||||||
};
|
|
||||||
// Should NOT panic — falls back to defaults.
|
|
||||||
let resp: MedicationResponse = med.into();
|
|
||||||
assert_eq!(resp.name, "");
|
|
||||||
assert!(resp.active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -111,23 +111,14 @@ export interface PillIdentification {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Medication {
|
export interface Medication {
|
||||||
id?: string;
|
|
||||||
medication_id?: string;
|
medication_id?: string;
|
||||||
user_id?: string;
|
user_id: string;
|
||||||
profile_id?: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
dosage: string;
|
dosage: string;
|
||||||
frequency: string;
|
frequency: string;
|
||||||
route?: string;
|
|
||||||
reason?: string;
|
|
||||||
instructions?: string;
|
|
||||||
side_effects?: string[];
|
|
||||||
prescribed_by?: string;
|
|
||||||
prescribed_date?: string;
|
|
||||||
start_date?: string;
|
start_date?: string;
|
||||||
end_date?: string;
|
end_date?: string;
|
||||||
notes?: string;
|
instructions?: string;
|
||||||
tags?: string[];
|
|
||||||
active: boolean;
|
active: boolean;
|
||||||
pill_identification?: PillIdentification;
|
pill_identification?: PillIdentification;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue