diff --git a/src/inventory/application/services/add_category_service.rs b/src/inventory/application/services/add_category_service.rs new file mode 100644 index 0000000..6592989 --- /dev/null +++ b/src/inventory/application/services/add_category_service.rs @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +use uuid::Uuid; + +use super::errors::*; +use crate::inventory::{ + application::port::output::db::{category_id_exists::*, category_name_exists_for_store::*}, + domain::{ + add_category_command::AddCategoryCommand, + category_added_event::{CategoryAddedEvent, CategoryAddedEventBuilder}, + category_aggregate::*, + }, +}; +use crate::utils::uuid::*; + +#[async_trait::async_trait] +pub trait AddCategoryUseCase: Send + Sync { + async fn add_category(&self, cmd: AddCategoryCommand) -> InventoryResult; +} + +pub type AddCategoryServiceObj = std::sync::Arc; + +#[derive(Clone, Builder)] +pub struct AddCategoryService { + db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj, + db_category_id_exists: CategoryIDExistsDBPortObj, + get_uuid: GetUUIDInterfaceObj, +} + +#[async_trait::async_trait] +impl AddCategoryUseCase for AddCategoryService { + async fn add_category(&self, cmd: AddCategoryCommand) -> InventoryResult { + let mut category_id = self.get_uuid.get_uuid(); + let mut category = CategoryBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .store_id(cmd.store_id().clone()) + .category_id(category_id) + .build() + .unwrap(); + + if self + .db_category_name_exists_for_store + .category_name_exists_for_store(&category) + .await? + { + return Err(InventoryError::DuplicateCategoryName); + } + + loop { + if self + .db_category_id_exists + .category_id_exists(&category) + .await? + { + category_id = self.get_uuid.get_uuid(); + category = CategoryBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .store_id(cmd.store_id().clone()) + .category_id(category_id) + .build() + .unwrap(); + + continue; + } else { + break; + } + } + + Ok(CategoryAddedEventBuilder::default() + .name(category.name().into()) + .description(category.description().as_ref().map(|s| s.to_string())) + .added_by_user(cmd.adding_by().into()) + .store_id(category.store_id().clone()) + .category_id(category.category_id().clone()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use uuid::Uuid; + + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + #[actix_rt::test] + async fn test_service_category_doesnt_exist() { + let name = "foo"; + let description = "bar"; + let username = "baz"; + let store_id = Uuid::new_v4(); + + // description = None + let cmd = AddCategoryCommand::new( + name.into(), + Some(description.into()), + store_id.clone(), + username.into(), + ) + .unwrap(); + + let s = AddCategoryServiceBuilder::default() + .db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false( + IS_CALLED_ONLY_ONCE, + )) + .db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.add_category(cmd.clone()).await.unwrap(); + assert_eq!(res.name(), cmd.name()); + assert_eq!(res.description(), cmd.description()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + assert_eq!(res.store_id(), cmd.store_id()); + assert_eq!(res.category_id(), &UUID); + } + + #[actix_rt::test] + async fn test_service_category_name_exists_for_store() { + let name = "foo"; + let description = "bar"; + let username = "baz"; + let store_id = Uuid::new_v4(); + + // description = None + let cmd = AddCategoryCommand::new( + name.into(), + Some(description.into()), + store_id.clone(), + username.into(), + ) + .unwrap(); + + let s = AddCategoryServiceBuilder::default() + .db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_true( + IS_CALLED_ONLY_ONCE, + )) + .db_category_id_exists(mock_category_id_exists_db_port_false(IS_NEVER_CALLED)) + .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) + .build() + .unwrap(); + + assert_eq!( + s.add_category(cmd.clone()).await, + Err(InventoryError::DuplicateCategoryName) + ) + } +} diff --git a/src/inventory/application/services/add_store_service.rs b/src/inventory/application/services/add_store_service.rs index bdb5c38..d782d07 100644 --- a/src/inventory/application/services/add_store_service.rs +++ b/src/inventory/application/services/add_store_service.rs @@ -14,6 +14,7 @@ use crate::inventory::{ store_aggregate::*, }, }; +use crate::utils::uuid::*; #[async_trait::async_trait] pub trait AddStoreUseCase: Send + Sync { @@ -25,12 +26,13 @@ pub type AddStoreServiceObj = std::sync::Arc; #[derive(Clone, Builder)] pub struct AddStoreService { db_store_id_exists: StoreIDExistsDBPortObj, + get_uuid: GetUUIDInterfaceObj, } #[async_trait::async_trait] impl AddStoreUseCase for AddStoreService { async fn add_store(&self, cmd: AddStoreCommand) -> InventoryResult { - let mut store_id = Uuid::new_v4(); + let mut store_id = self.get_uuid.get_uuid(); let mut store = StoreBuilder::default() .name(cmd.name().into()) .address(cmd.address().as_ref().map(|s| s.to_string())) @@ -40,7 +42,7 @@ impl AddStoreUseCase for AddStoreService { .unwrap(); loop { if self.db_store_id_exists.store_id_exists(&store).await? { - store_id = Uuid::new_v4(); + store_id = self.get_uuid.get_uuid(); store = StoreBuilder::default() .name(cmd.name().into()) .address(cmd.address().as_ref().map(|s| s.to_string())) @@ -68,6 +70,7 @@ mod tests { use super::*; use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; #[actix_rt::test] async fn test_service_store_id_doesnt_exist() { @@ -80,6 +83,7 @@ mod tests { let s = AddStoreServiceBuilder::default() .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); @@ -87,5 +91,6 @@ mod tests { assert_eq!(res.name(), cmd.name()); assert_eq!(res.address(), cmd.address()); assert_eq!(res.owner(), cmd.owner()); + assert_eq!(res.store_id(), &UUID); } } diff --git a/src/main.rs b/src/main.rs index 3275444..8976208 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,7 @@ async fn main() { ) // .configure(auth::adapter::load_adapters(db.pool.clone(), &settings)) .configure(utils::random_string::GenerateRandomString::inject()) + .configure(utils::uuid::GenerateUUID::inject()) }) .bind(&socket_addr) .unwrap() diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ff3d17c..a52985c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -3,3 +3,4 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod random_string; +pub mod uuid; diff --git a/src/utils/uuid.rs b/src/utils/uuid.rs new file mode 100644 index 0000000..477fddd --- /dev/null +++ b/src/utils/uuid.rs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::sync::Arc; + +use actix_web::web; +use mockall::predicate::*; +use mockall::*; +#[cfg(test)] +#[allow(unused_imports)] +use tests::*; +use uuid::Uuid; + +pub type GetUUIDInterfaceObj = Arc; +pub type WebGetUUIDInterfaceObj = web::Data; + +#[automock] +pub trait GetUUID: Send + Sync { + fn get_uuid(&self) -> Uuid; +} + +pub struct GenerateUUID; +impl GetUUID for GenerateUUID { + fn get_uuid(&self) -> Uuid { + Uuid::new_v4() + } +} + +impl GenerateUUID { + pub fn inject() -> impl FnOnce(&mut web::ServiceConfig) { + let g = WebGetUUIDInterfaceObj::new(Arc::new(GenerateUUID)); + let f = move |cfg: &mut web::ServiceConfig| { + cfg.app_data(g); + }; + + Box::new(f) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + use uuid::uuid; + pub const UUID: Uuid = uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"); + + pub fn mock_get_uuid(times: Option) -> Arc { + let mut m = MockGetUUID::new(); + + if let Some(times) = times { + m.expect_get_uuid().times(times).return_const(UUID.clone()); + } else { + m.expect_get_uuid().return_const(UUID.clone()); + } + + Arc::new(m) + } +}