diff --git a/src/inventory/application/services/add_product_service.rs b/src/inventory/application/services/add_product_service.rs new file mode 100644 index 0000000..de5fe7f --- /dev/null +++ b/src/inventory/application/services/add_product_service.rs @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::sync::Arc; + +use derive_builder::Builder; +use mockall::predicate::*; +use mockall::*; + +use super::errors::*; +use crate::inventory::{ + application::port::output::db::{product_id_exists::*, product_name_exists_for_category::*}, + domain::{ + add_product_command::AddProductCommand, + product_added_event::{ProductAddedEvent, ProductAddedEventBuilder}, + product_aggregate::*, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait AddProductUseCase: Send + Sync { + async fn add_product(&self, cmd: AddProductCommand) -> InventoryResult; +} + +pub type AddProductServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct AddProductService { + db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj, + db_product_id_exists: ProductIDExistsDBPortObj, + get_uuid: GetUUIDInterfaceObj, +} + +#[async_trait::async_trait] +impl AddProductUseCase for AddProductService { + async fn add_product(&self, cmd: AddProductCommand) -> InventoryResult { + let mut product_id = self.get_uuid.get_uuid(); + + loop { + if self + .db_product_id_exists + .product_id_exists(&product_id) + .await? + { + product_id = self.get_uuid.get_uuid(); + continue; + } else { + break; + } + } + + let product = ProductBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .image(cmd.image().clone()) + .sku_able(cmd.sku_able().clone()) + .price(cmd.price().clone()) + .category_id(cmd.category_id().clone()) + .product_id(product_id) + .build() + .unwrap(); + + if self + .db_product_name_exists_for_category + .product_name_exists_for_category(&product) + .await? + { + return Err(InventoryError::DuplicateProductName); + } + + Ok(ProductAddedEventBuilder::default() + .added_by_user(cmd.adding_by().clone()) + .name(product.name().into()) + .description(product.description().as_ref().map(|s| s.to_string())) + .image(product.image().clone()) + .sku_able(product.sku_able().clone()) + .price(product.price().clone()) + .category_id(product.category_id().clone()) + .product_id(product.product_id().clone()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use uuid::Uuid; + + use crate::inventory::domain::add_product_command::tests::get_command; + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + pub fn mock_add_product_service( + times: Option, + cmd: AddProductCommand, + ) -> AddProductServiceObj { + let mut m = MockAddProductUseCase::new(); + + let res = ProductAddedEventBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .image(cmd.image().as_ref().map(|s| s.to_string())) + .sku_able(cmd.sku_able().clone()) + .category_id(cmd.category_id().clone()) + .product_id(UUID.clone()) + .price(cmd.price().clone()) + .added_by_user(cmd.adding_by().clone()) + .build() + .unwrap(); + + if let Some(times) = times { + m.expect_add_product() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_add_product().returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service_product_doesnt_exist() { + let cmd = get_command(); + + let s = AddProductServiceBuilder::default() + .db_product_name_exists_for_category( + mock_product_name_exists_for_category_db_port_false(IS_CALLED_ONLY_ONCE), + ) + .db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.add_product(cmd.clone()).await.unwrap(); + assert_eq!(res.name(), cmd.name()); + assert_eq!(res.description(), cmd.description()); + assert_eq!(res.image(), cmd.image()); + assert_eq!(res.sku_able(), cmd.sku_able()); + assert_eq!(res.price(), cmd.price()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + assert_eq!(res.category_id(), cmd.category_id()); + assert_eq!(res.product_id(), &UUID); + } + + #[actix_rt::test] + async fn test_service_product_name_exists_for_store() { + let cmd = get_command(); + + let s = AddProductServiceBuilder::default() + .db_product_name_exists_for_category( + mock_product_name_exists_for_category_db_port_true(IS_CALLED_ONLY_ONCE), + ) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.add_product(cmd.clone()).await, + Err(InventoryError::DuplicateProductName) + ) + } +} diff --git a/src/inventory/application/services/errors.rs b/src/inventory/application/services/errors.rs index 7bb9fb2..e1a5a70 100644 --- a/src/inventory/application/services/errors.rs +++ b/src/inventory/application/services/errors.rs @@ -14,6 +14,7 @@ pub type InventoryResult = Result; pub enum InventoryError { DuplicateCategoryName, DuplicateStoreName, + DuplicateProductName, InternalError, } @@ -22,9 +23,18 @@ impl From for InventoryError { match value { InventoryDBError::DuplicateCategoryName => Self::DuplicateCategoryName, InventoryDBError::DuplicateStoreName => Self::DuplicateStoreName, + InventoryDBError::DuplicateProductName => Self::DuplicateProductName, InventoryDBError::DuplicateStoreID => { error!("DuplicateStoreID"); Self::InternalError + }, + InventoryDBError::DuplicateProductID => { + error!("DuplicateProductID"); + Self::InternalError + }, + InventoryDBError::DuplicateCategoryID => { + error!("DuplicateCategoryID"); + Self::InternalError } InventoryDBError::InternalError => Self::InternalError, } diff --git a/src/inventory/application/services/mod.rs b/src/inventory/application/services/mod.rs index 4a95814..62788bf 100644 --- a/src/inventory/application/services/mod.rs +++ b/src/inventory/application/services/mod.rs @@ -10,18 +10,21 @@ pub mod errors; // services pub mod add_category_service; +pub mod add_product_service; pub mod add_store_service; #[automock] pub trait InventoryServicesInterface: Send + Sync { fn add_store(&self) -> add_store_service::AddStoreServiceObj; fn add_category(&self) -> add_category_service::AddCategoryServiceObj; + fn add_product(&self) -> add_product_service::AddProductServiceObj; } #[derive(Clone, Builder)] pub struct InventoryServices { add_store: add_store_service::AddStoreServiceObj, add_category: add_category_service::AddCategoryServiceObj, + add_product: add_product_service::AddProductServiceObj, } impl InventoryServicesInterface for InventoryServices { @@ -31,4 +34,7 @@ impl InventoryServicesInterface for InventoryServices { fn add_category(&self) -> add_category_service::AddCategoryServiceObj { self.add_category.clone() } + fn add_product(&self) -> add_product_service::AddProductServiceObj { + self.add_product.clone() + } }