bitpill 0.3.3

A personal medication management TUI application built in Rust.
Documentation
use std::sync::Arc;
use uuid::Uuid;

use crate::application::{
    dtos::{requests::EditMedicationRequest, responses::EditMedicationResponse},
    errors::ApplicationError,
    ports::{
        inbound::edit_medication_port::EditMedicationPort,
        outbound::medication_repository_port::MedicationRepository,
    },
};
use crate::domain::entities::medication::Medication;
use crate::domain::value_objects::{
    dosage::Dosage, medication_frequency::DoseFrequency, medication_id::MedicationId,
    medication_name::MedicationName, scheduled_time::ScheduledTime,
};

pub struct EditMedicationService {
    repository: Arc<dyn MedicationRepository>,
}

impl EditMedicationService {
    pub fn new(repository: Arc<dyn MedicationRepository>) -> Self {
        Self { repository }
    }
}

impl EditMedicationPort for EditMedicationService {
    fn execute(
        &self,
        request: EditMedicationRequest,
    ) -> Result<EditMedicationResponse, ApplicationError> {
        let uuid = Uuid::parse_str(&request.id)
            .map_err(|_| ApplicationError::InvalidInput("invalid id".into()))?;
        let id = MedicationId::from(uuid);

        let name = MedicationName::new(request.name)?;
        let dosage = Dosage::new(request.amount_mg)?;
        let mut scheduled_times = Vec::new();
        for (h, m) in request.scheduled_time {
            scheduled_times.push(ScheduledTime::new(h, m)?);
        }

        let dose_frequency = match request.dose_frequency.as_str() {
            "TwiceDaily" => DoseFrequency::TwiceDaily,
            "ThriceDaily" => DoseFrequency::ThriceDaily,
            "Custom" => DoseFrequency::Custom(scheduled_times.clone()),
            _ => DoseFrequency::OnceDaily,
        };

        let medication = Medication::with_id(id, name, dosage, scheduled_times, dose_frequency);

        self.repository.save(&medication)?;

        Ok(EditMedicationResponse { id: request.id })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::application::ports::fakes::FakeMedicationRepository;

    fn make_service(repo: std::sync::Arc<FakeMedicationRepository>) -> EditMedicationService {
        EditMedicationService::new(repo)
    }

    #[test]
    fn execute_with_invalid_uuid_returns_invalid_input() {
        let repo = std::sync::Arc::new(FakeMedicationRepository::new());
        let service = make_service(repo);
        let req = EditMedicationRequest::new("not-a-uuid", "Name", 100, vec![(8, 0)], "OnceDaily");

        let res = service.execute(req);
        assert!(matches!(res, Err(ApplicationError::InvalidInput(_))));
    }

    #[test]
    fn execute_with_empty_name_returns_domain_error() {
        let repo = std::sync::Arc::new(FakeMedicationRepository::new());
        let service = make_service(repo);
        let id = uuid::Uuid::now_v7().to_string();
        let req = EditMedicationRequest::new(id, "", 100, vec![(8, 0)], "OnceDaily");

        let res = service.execute(req);
        assert!(matches!(res, Err(ApplicationError::Domain(_))));
    }

    #[test]
    fn execute_with_valid_request_saves() {
        let repo = std::sync::Arc::new(FakeMedicationRepository::new());
        let service = make_service(repo.clone());
        let id = uuid::Uuid::now_v7().to_string();
        let req = EditMedicationRequest::new(id.clone(), "Test", 100, vec![(8, 0)], "OnceDaily");

        let res = service.execute(req).unwrap();
        assert_eq!(res.id, id);
        assert_eq!(repo.saved_count(), 1);
    }
}