diff --git a/src/huckleberry_api/api.py b/src/huckleberry_api/api.py index bf0b6d2..2de4333 100644 --- a/src/huckleberry_api/api.py +++ b/src/huckleberry_api/api.py @@ -47,6 +47,8 @@ FirebaseHealthDocumentData, FirebaseHealthMultiContainer, FirebaseLastActivityData, + FirebaseMedicationData, + MedicationUnits, FirebaseLastBottleData, FirebaseLastDiaperData, FirebaseLastNursingData, @@ -1782,6 +1784,82 @@ async def log_growth( _LOGGER.info("Growth data logged successfully (updated_last=%s)", should_update_last_growth) + async def log_medication( + self, + child_uid: str, + *, + start_time: datetime, + name: str, + amount: float | None = None, + units: MedicationUnits | None = None, + notes: str | None = None, + ) -> None: + """Log a medication dose. + + Args: + child_uid: Child unique identifier + start_time: Event time + name: Medication name (e.g., "Tylenol") + amount: Dose amount + units: Dose units — "ml", "oz", "tsp", or "drops" + notes: Optional notes + """ + _LOGGER.info("Logging medication for child %s: %s", child_uid, name) + + client = await self._get_firestore_client() + health_ref = client.collection("health").document(child_uid) + + start_timestamp = start_time.timestamp() + current_time = time.time() + current_offset = await self._get_timezone_offset_minutes() + + health_doc = await health_ref.get() + health_model = FirebaseHealthDocumentData.model_validate(health_doc.to_dict() or {}) + existing_last_med = health_model.prefs.lastMedication if health_model.prefs else None + existing_last_med_start = existing_last_med.start if existing_last_med else None + should_update_last_med = existing_last_med_start is None or start_timestamp >= float( + existing_last_med_start + ) + + interval_timestamp_ms = int(current_time * 1000) + interval_id = f"{interval_timestamp_ms}-{uuid.uuid4().hex[:20]}" + + # Health tracker uses "data" subcollection (not "intervals") + med_entry = FirebaseMedicationData( + type="health", + mode="medication", + start=start_timestamp, + lastUpdated=current_time, + offset=current_offset, + medication_name=name, + amount=float(amount) if amount is not None else None, + units=units, + notes=notes, + ) + + health_data_ref = health_ref.collection("data").document(interval_id) + try: + await health_data_ref.set(to_firebase_dict(med_entry)) + _LOGGER.info("Created medication entry: %s", interval_id) + except GoogleAPICallError as err: + _LOGGER.error("Failed to create medication entry: %s", err) + raise + + if should_update_last_med: + try: + await health_ref.update( + { + "prefs.lastMedication": to_firebase_dict(med_entry), + "prefs.timestamp": {"seconds": current_time}, + "prefs.local_timestamp": current_time, + } + ) + except GoogleAPICallError as err: + _LOGGER.error("Failed to update lastMedication prefs: %s", err) + raise + + _LOGGER.info("Medication logged successfully (updated_last=%s)", should_update_last_med) + async def log_pump( self, child_uid: str, diff --git a/tests/test_medication.py b/tests/test_medication.py new file mode 100644 index 0000000..fb1bd43 --- /dev/null +++ b/tests/test_medication.py @@ -0,0 +1,99 @@ +"""Medication logging tests for Huckleberry API.""" + +import asyncio +from datetime import datetime, timedelta, timezone + +from google.cloud import firestore + +from huckleberry_api import HuckleberryAPI + + +class TestMedicationLogging: + """Test medication logging functionality.""" + + async def test_log_medication_basic(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test logging a medication dose with amount and units.""" + await api.log_medication( + child_uid, + start_time=datetime.now(timezone.utc).replace(microsecond=0), + name="Tylenol", + amount=5.0, + units="ml", + ) + await asyncio.sleep(1) + + db = await api._get_firestore_client() + health_doc = await db.collection("health").document(child_uid).get() + data = health_doc.to_dict() + assert data is not None + assert "lastMedication" in data.get("prefs", {}) + last = data["prefs"]["lastMedication"] + assert last["medication_name"] == "Tylenol" + assert last["amount"] == 5.0 + assert last["units"] == "ml" + assert last["mode"] == "medication" + + async def test_log_medication_no_amount(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test logging a medication dose without amount or units.""" + await api.log_medication( + child_uid, + start_time=datetime.now(timezone.utc).replace(microsecond=0), + name="Vitamin D", + ) + await asyncio.sleep(1) + + db = await api._get_firestore_client() + health_doc = await db.collection("health").document(child_uid).get() + data = health_doc.to_dict() + assert data is not None + assert "lastMedication" in data.get("prefs", {}) + last = data["prefs"]["lastMedication"] + assert last["medication_name"] == "Vitamin D" + assert "amount" not in last + assert "units" not in last + + async def test_log_medication_with_explicit_start_time(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test logging a medication with an explicit past timestamp.""" + start_time = datetime.now(timezone.utc).replace(microsecond=0) - timedelta(hours=2) + + await api.log_medication( + child_uid, + start_time=start_time, + name="Ibuprofen", + amount=2.5, + units="ml", + ) + await asyncio.sleep(1) + + db = await api._get_firestore_client() + data_ref = db.collection("health").document(child_uid).collection("data") + matching = list( + await data_ref.where(filter=firestore.FieldFilter("start", "==", start_time.timestamp())).get() + ) + + assert matching + entry = matching[0].to_dict() + assert entry is not None + assert entry["start"] == start_time.timestamp() + assert entry["medication_name"] == "Ibuprofen" + assert entry["amount"] == 2.5 + assert entry["mode"] == "medication" + + async def test_log_medication_with_notes(self, api: HuckleberryAPI, child_uid: str) -> None: + """Test logging a medication with notes.""" + await api.log_medication( + child_uid, + start_time=datetime.now(timezone.utc).replace(microsecond=0), + name="Amoxicillin", + amount=5.0, + units="ml", + notes="Given with food", + ) + await asyncio.sleep(1) + + db = await api._get_firestore_client() + health_doc = await db.collection("health").document(child_uid).get() + data = health_doc.to_dict() + assert data is not None + last = data.get("prefs", {}).get("lastMedication", {}) + assert last.get("notes") == "Given with food"