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.
This commit is contained in:
parent
8bcb03cd09
commit
0921d73a6d
2 changed files with 56 additions and 10 deletions
|
|
@ -72,6 +72,7 @@ pub async fn create_medication(
|
||||||
.collect(),
|
.collect(),
|
||||||
created_at: now.into(),
|
created_at: now.into(),
|
||||||
updated_at: now.into(),
|
updated_at: now.into(),
|
||||||
|
active: req.active,
|
||||||
pill_identification: req.pill_identification, // Phase 2.8
|
pill_identification: req.pill_identification, // Phase 2.8
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -89,9 +90,11 @@ pub async fn list_medications(
|
||||||
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);
|
// Honor the active filter (and optional profile_id) when provided.
|
||||||
|
match repo
|
||||||
match repo.find_by_user(&claims.sub).await {
|
.find_by_user_filtered(&claims.sub, query.active, query.profile_id.as_deref())
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(medications) => {
|
Ok(medications) => {
|
||||||
let resp: Vec<MedicationResponse> = medications.into_iter().map(Into::into).collect();
|
let resp: Vec<MedicationResponse> = medications.into_iter().map(Into::into).collect();
|
||||||
Ok(Json(resp))
|
Ok(Json(resp))
|
||||||
|
|
|
||||||
|
|
@ -124,9 +124,7 @@ pub struct MedicationData {
|
||||||
|
|
||||||
/// The flat, snake_case medication representation returned by the API. Built from
|
/// The flat, snake_case medication representation returned by the API. Built from
|
||||||
/// a stored `Medication` by deserializing its `medication_data.data` blob.
|
/// a stored `Medication` by deserializing its `medication_data.data` blob.
|
||||||
///
|
/// Timestamps are ISO 8601 strings; `active` reflects the persisted flag.
|
||||||
/// 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)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct MedicationResponse {
|
pub struct MedicationResponse {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -176,7 +174,7 @@ impl std::convert::From<Medication> for MedicationResponse {
|
||||||
end_date: data.end_date,
|
end_date: data.end_date,
|
||||||
notes: data.notes,
|
notes: data.notes,
|
||||||
tags: data.tags,
|
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()),
|
created_at: system_time_to_rfc3339(m.created_at.to_system_time()),
|
||||||
updated_at: system_time_to_rfc3339(m.updated_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,
|
pub created_at: DateTime,
|
||||||
#[serde(rename = "updatedAt")]
|
#[serde(rename = "updatedAt")]
|
||||||
pub updated_at: DateTime,
|
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)
|
/// Physical pill identification (Phase 2.8 - optional)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub pill_identification: Option<PillIdentification>,
|
pub pill_identification: Option<PillIdentification>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct MedicationReminder {
|
pub struct MedicationReminder {
|
||||||
#[serde(rename = "reminderId")]
|
#[serde(rename = "reminderId")]
|
||||||
|
|
@ -265,6 +273,9 @@ pub struct CreateMedicationRequest {
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
pub reminder_times: Option<Vec<String>>,
|
pub reminder_times: Option<Vec<String>>,
|
||||||
pub profile_id: String,
|
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)
|
/// Pill identification (Phase 2.8 - optional)
|
||||||
#[serde(rename = "pill_identification")]
|
#[serde(rename = "pill_identification")]
|
||||||
|
|
@ -287,6 +298,8 @@ pub struct UpdateMedicationRequest {
|
||||||
pub notes: Option<String>,
|
pub notes: Option<String>,
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
pub reminder_times: Option<Vec<String>>,
|
pub reminder_times: Option<Vec<String>>,
|
||||||
|
/// Set the medication active/inactive.
|
||||||
|
pub active: Option<bool>,
|
||||||
|
|
||||||
/// Pill identification (Phase 2.8 - optional)
|
/// Pill identification (Phase 2.8 - optional)
|
||||||
#[serde(rename = "pill_identification")]
|
#[serde(rename = "pill_identification")]
|
||||||
|
|
@ -336,6 +349,29 @@ impl MedicationRepository {
|
||||||
Ok(medications)
|
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<bool>,
|
||||||
|
profile_id: Option<&str>,
|
||||||
|
) -> Result<Vec<Medication>, Box<dyn std::error::Error>> {
|
||||||
|
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(
|
pub async fn find_by_user_and_profile(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
|
|
@ -428,7 +464,7 @@ impl MedicationRepository {
|
||||||
if let Some(v) = updates.tags {
|
if let Some(v) = updates.tags {
|
||||||
data.tags = v;
|
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!({
|
let data_json = serde_json::json!({
|
||||||
"name": data.name,
|
"name": data.name,
|
||||||
|
|
@ -451,6 +487,9 @@ impl MedicationRepository {
|
||||||
"medicationData.data": data_json,
|
"medicationData.data": data_json,
|
||||||
"updatedAt": DateTime::now(),
|
"updatedAt": DateTime::now(),
|
||||||
};
|
};
|
||||||
|
if let Some(active) = updates.active {
|
||||||
|
update_doc.insert("active", active);
|
||||||
|
}
|
||||||
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("pill_identification", pill_doc);
|
||||||
|
|
@ -532,8 +571,7 @@ impl MedicationRepository {
|
||||||
if let Some(v) = updates.tags {
|
if let Some(v) = updates.tags {
|
||||||
data.tags = v;
|
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.
|
||||||
// the storage model has no active column (TODO).
|
|
||||||
|
|
||||||
// Re-serialize with the SAME camelCase keys the create handler uses, so
|
// Re-serialize with the SAME camelCase keys the create handler uses, so
|
||||||
// reads stay consistent.
|
// reads stay consistent.
|
||||||
|
|
@ -558,6 +596,9 @@ impl MedicationRepository {
|
||||||
"medicationData.data": data_json,
|
"medicationData.data": data_json,
|
||||||
"updatedAt": DateTime::now(),
|
"updatedAt": DateTime::now(),
|
||||||
};
|
};
|
||||||
|
if let Some(active) = updates.active {
|
||||||
|
update_doc.insert("active", active);
|
||||||
|
}
|
||||||
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("pill_identification", pill_doc);
|
||||||
|
|
@ -646,6 +687,7 @@ mod tests {
|
||||||
reminders: vec![],
|
reminders: vec![],
|
||||||
created_at: DateTime::now(),
|
created_at: DateTime::now(),
|
||||||
updated_at: DateTime::now(),
|
updated_at: DateTime::now(),
|
||||||
|
active: true,
|
||||||
pill_identification: None,
|
pill_identification: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -674,6 +716,7 @@ mod tests {
|
||||||
reminders: vec![],
|
reminders: vec![],
|
||||||
created_at: DateTime::now(),
|
created_at: DateTime::now(),
|
||||||
updated_at: DateTime::now(),
|
updated_at: DateTime::now(),
|
||||||
|
active: true,
|
||||||
pill_identification: None,
|
pill_identification: None,
|
||||||
};
|
};
|
||||||
// Should NOT panic — falls back to defaults.
|
// Should NOT panic — falls back to defaults.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue