Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
78 changes: 78 additions & 0 deletions src/huckleberry_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
FirebaseHealthDocumentData,
FirebaseHealthMultiContainer,
FirebaseLastActivityData,
FirebaseMedicationData,
MedicationUnits,
FirebaseLastBottleData,
FirebaseLastDiaperData,
FirebaseLastNursingData,
Expand Down Expand Up @@ -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,
Expand Down
99 changes: 99 additions & 0 deletions tests/test_medication.py
Original file line number Diff line number Diff line change
@@ -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"