From 0921d73a6d8e3d42ba9ec0ffaa25d83c280b8b2f Mon Sep 17 00:00:00 2001 From: goose Date: Sun, 28 Jun 2026 12:49:02 -0300 Subject: [PATCH] feat(medications): persist real active flag + filter The active flag was synthesized as always-true. Now it's a real top-level field on the Medication document (serde default=true so old rows stay active), wired through create (default true), update ($set the field, not the data blob), and list (the previously-ignored ?active= query param now filters at Mongo level via find_by_user_filtered). Fixed stale comments claiming updates.active was 'accepted but not persisted' (the field didn't even exist on UpdateMedicationRequest). Verified: cargo fmt/clippy 0 warnings, 23 tests pass; frontend build + 20 tests. --- backend/src/handlers/medications.rs | 9 +++-- backend/src/models/medication.rs | 57 +++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/backend/src/handlers/medications.rs b/backend/src/handlers/medications.rs index 1aed58b..3ee8021 100644 --- a/backend/src/handlers/medications.rs +++ b/backend/src/handlers/medications.rs @@ -72,6 +72,7 @@ pub async fn create_medication( .collect(), created_at: now.into(), updated_at: now.into(), + active: req.active, pill_identification: req.pill_identification, // Phase 2.8 }; @@ -89,9 +90,11 @@ pub async fn list_medications( let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); - let _limit = query.limit.unwrap_or(100); - - match repo.find_by_user(&claims.sub).await { + // Honor the active filter (and optional profile_id) when provided. + match repo + .find_by_user_filtered(&claims.sub, query.active, query.profile_id.as_deref()) + .await + { Ok(medications) => { let resp: Vec = medications.into_iter().map(Into::into).collect(); Ok(Json(resp)) diff --git a/backend/src/models/medication.rs b/backend/src/models/medication.rs index b39f642..7bbbadd 100644 --- a/backend/src/models/medication.rs +++ b/backend/src/models/medication.rs @@ -124,9 +124,7 @@ pub struct MedicationData { /// 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. +/// Timestamps are ISO 8601 strings; `active` reflects the persisted flag. #[derive(Debug, Serialize)] pub struct MedicationResponse { pub id: String, @@ -176,7 +174,7 @@ impl std::convert::From for MedicationResponse { end_date: data.end_date, notes: data.notes, tags: data.tags, - active: true, // TODO: persist a real active flag. + active: m.active, created_at: system_time_to_rfc3339(m.created_at.to_system_time()), updated_at: system_time_to_rfc3339(m.updated_at.to_system_time()), } @@ -212,12 +210,22 @@ pub struct Medication { pub created_at: DateTime, #[serde(rename = "updatedAt")] pub updated_at: DateTime, + /// Whether this medication is currently active. Defaults to true for old + /// documents that predate the field (see `default_true`). + #[serde(rename = "active", default = "default_true")] + pub active: bool, /// Physical pill identification (Phase 2.8 - optional) #[serde(skip_serializing_if = "Option::is_none")] pub pill_identification: Option, } +/// serde default helper — old medication documents predate the `active` field +/// and should deserialize as active. +fn default_true() -> bool { + true +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MedicationReminder { #[serde(rename = "reminderId")] @@ -265,6 +273,9 @@ pub struct CreateMedicationRequest { pub tags: Option>, pub reminder_times: Option>, pub profile_id: String, + /// Whether the medication is active on creation (defaults to true). + #[serde(default = "default_true")] + pub active: bool, /// Pill identification (Phase 2.8 - optional) #[serde(rename = "pill_identification")] @@ -287,6 +298,8 @@ pub struct UpdateMedicationRequest { pub notes: Option, pub tags: Option>, pub reminder_times: Option>, + /// Set the medication active/inactive. + pub active: Option, /// Pill identification (Phase 2.8 - optional) #[serde(rename = "pill_identification")] @@ -336,6 +349,29 @@ impl MedicationRepository { Ok(medications) } + /// Like find_by_user, but optionally filtered by `active` and/or `profile_id` + /// at the Mongo level (these are real top-level document fields). + pub async fn find_by_user_filtered( + &self, + user_id: &str, + active: Option, + profile_id: Option<&str>, + ) -> Result, Box> { + let mut filter = doc! { "userId": user_id }; + if let Some(active) = active { + filter.insert("active", active); + } + if let Some(pid) = profile_id { + filter.insert("profileId", pid); + } + let mut cursor = self.collection.find(filter, None).await?; + let mut medications = Vec::new(); + while let Some(medication) = cursor.next().await { + medications.push(medication?); + } + Ok(medications) + } + pub async fn find_by_user_and_profile( &self, user_id: &str, @@ -428,7 +464,7 @@ impl MedicationRepository { if let Some(v) = updates.tags { data.tags = v; } - // NOTE: updates.active is accepted by the API but not yet persisted. + // `active` lives on the document, not in the data blob. let data_json = serde_json::json!({ "name": data.name, @@ -451,6 +487,9 @@ impl MedicationRepository { "medicationData.data": data_json, "updatedAt": DateTime::now(), }; + if let Some(active) = updates.active { + update_doc.insert("active", active); + } 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); @@ -532,8 +571,7 @@ impl MedicationRepository { if let Some(v) = updates.tags { data.tags = v; } - // NOTE: updates.active is accepted by the API but not yet persisted — - // the storage model has no active column (TODO). + // `active` lives on the document, not in the data blob. // Re-serialize with the SAME camelCase keys the create handler uses, so // reads stay consistent. @@ -558,6 +596,9 @@ impl MedicationRepository { "medicationData.data": data_json, "updatedAt": DateTime::now(), }; + if let Some(active) = updates.active { + update_doc.insert("active", active); + } 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); @@ -646,6 +687,7 @@ mod tests { reminders: vec![], created_at: DateTime::now(), updated_at: DateTime::now(), + active: true, pill_identification: None, }; @@ -674,6 +716,7 @@ mod tests { reminders: vec![], created_at: DateTime::now(), updated_at: DateTime::now(), + active: true, pill_identification: None, }; // Should NOT panic — falls back to defaults.