diff --git a/src/inventory/adapters/output/db/postgres/customization_id_exists.rs b/src/inventory/adapters/output/db/postgres/customization_id_exists.rs index 0bc7b25..71b4355 100644 --- a/src/inventory/adapters/output/db/postgres/customization_id_exists.rs +++ b/src/inventory/adapters/output/db/postgres/customization_id_exists.rs @@ -32,6 +32,7 @@ impl CustomizationIDExistsDBPort for InventoryDBPostgresAdapter { pub mod tests { use super::*; + use crate::inventory::domain::customization_aggregate::*; use crate::utils::uuid::tests::UUID; @@ -45,14 +46,12 @@ pub mod tests { .unwrap(), ); - let customization = - CustomizationBuilder::default() - .name("cname".into()) - .customization_id(UUID) - .deleted(false) - .build() - .unwrap() - ; + let customization = CustomizationBuilder::default() + .name("customization_name".into()) + .customization_id(UUID) + .deleted(false) + .build() + .unwrap(); // state doesn't exist assert!(!db diff --git a/src/inventory/adapters/output/db/postgres/customization_name_exists_for_product.rs b/src/inventory/adapters/output/db/postgres/customization_name_exists_for_product.rs index ed7b710..76f52e5 100644 --- a/src/inventory/adapters/output/db/postgres/customization_name_exists_for_product.rs +++ b/src/inventory/adapters/output/db/postgres/customization_name_exists_for_product.rs @@ -7,7 +7,7 @@ use super::InventoryDBPostgresAdapter; use crate::inventory::application::port::output::db::{ customization_name_exists_for_product::*, errors::*, }; -use crate::inventory::domain::product_aggregate::*; +use crate::inventory::domain::customization_aggregate::*; #[async_trait::async_trait] impl CustomizationNameExistsForProductDBPort for InventoryDBPostgresAdapter { diff --git a/src/inventory/adapters/output/db/postgres/product_id_exists.rs b/src/inventory/adapters/output/db/postgres/product_id_exists.rs index 9d98a1d..2738e55 100644 --- a/src/inventory/adapters/output/db/postgres/product_id_exists.rs +++ b/src/inventory/adapters/output/db/postgres/product_id_exists.rs @@ -33,9 +33,7 @@ pub mod tests { use super::*; // use crate::inventory::domain::add_product_command::tests::get_customizations; - use crate::inventory::domain::{ - add_product_command::tests::get_command, product_aggregate::*, - }; + use crate::inventory::domain::{add_product_command::tests::get_command, product_aggregate::*}; use crate::utils::uuid::tests::UUID; #[actix_rt::test] diff --git a/src/inventory/adapters/output/db/postgres/store_view.rs b/src/inventory/adapters/output/db/postgres/store_view.rs index ac8d6e1..8a118b7 100644 --- a/src/inventory/adapters/output/db/postgres/store_view.rs +++ b/src/inventory/adapters/output/db/postgres/store_view.rs @@ -212,12 +212,14 @@ mod tests { inventory::{ application::services::{ add_category_service::tests::mock_add_category_service, + add_customization_service::tests::mock_add_customization_service, add_product_service::tests::mock_add_product_service, add_store_service::AddStoreServiceBuilder, InventoryServicesBuilder, }, domain::{ - add_category_command::AddCategoryCommand, add_product_command::tests::get_command, - add_store_command::AddStoreCommand, commands::InventoryCommand, + add_category_command::AddCategoryCommand, add_customization_command, + add_product_command::tests::get_command, add_store_command::AddStoreCommand, + commands::InventoryCommand, }, }, tests::bdd::IS_NEVER_CALLED, @@ -254,6 +256,10 @@ mod tests { AddCategoryCommand::new("foo".into(), None, UUID, UUID).unwrap(), )) .add_product(mock_add_product_service(IS_NEVER_CALLED, get_command())) + .add_customization(mock_add_customization_service( + IS_NEVER_CALLED, + add_customization_command::tests::get_command(), + )) .build() .unwrap(); diff --git a/src/inventory/application/services/add_category_service.rs b/src/inventory/application/services/add_category_service.rs index 5b0e9ef..5574614 100644 --- a/src/inventory/application/services/add_category_service.rs +++ b/src/inventory/application/services/add_category_service.rs @@ -29,6 +29,7 @@ pub type AddCategoryServiceObj = Arc; #[derive(Clone, Builder)] pub struct AddCategoryService { + // TODO: check if store ID exists db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj, db_category_id_exists: CategoryIDExistsDBPortObj, get_uuid: GetUUIDInterfaceObj, diff --git a/src/inventory/application/services/add_customization_service.rs b/src/inventory/application/services/add_customization_service.rs new file mode 100644 index 0000000..fcdaaee --- /dev/null +++ b/src/inventory/application/services/add_customization_service.rs @@ -0,0 +1,203 @@ +// 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::{ + customization_id_exists::{self, *}, + customization_name_exists_for_product::*, + product_id_exists::{self, *}, + product_name_exists_for_category::*, + }, + domain::{ + add_customization_command::AddCustomizationCommand, + customization_added_event::{self, *}, + customization_aggregate::*, + product_added_event::{self, ProductAddedEvent, ProductAddedEventBuilder}, + product_aggregate::*, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait AddCustomizationUseCase: Send + Sync { + async fn add_customization( + &self, + cmd: AddCustomizationCommand, + ) -> InventoryResult; +} + +pub type AddCustomizationServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct AddCustomizationService { + db_product_id_exists: ProductIDExistsDBPortObj, + db_customization_id_exists: CustomizationIDExistsDBPortObj, + db_customization_name_exists_for_product: CustomizationNameExistsForProductDBPortObj, + get_uuid: GetUUIDInterfaceObj, +} + +#[async_trait::async_trait] +impl AddCustomizationUseCase for AddCustomizationService { + async fn add_customization( + &self, + cmd: AddCustomizationCommand, + ) -> InventoryResult { + if !self + .db_product_id_exists + .product_id_exists(cmd.product_id()) + .await? + { + return Err(InventoryError::ProductIDNotFound); + } + + let mut customization_id = self.get_uuid.get_uuid(); + loop { + if self + .db_customization_id_exists + .customization_id_exists(&customization_id) + .await? + { + customization_id = self.get_uuid.get_uuid(); + continue; + } else { + break; + } + } + + let customization = CustomizationBuilder::default() + .name(cmd.name().into()) + .deleted(false) + .customization_id(customization_id) + .build() + .unwrap(); + + if self + .db_customization_name_exists_for_product + .customization_name_exists_for_product(&customization, cmd.product_id()) + .await? + { + return Err(InventoryError::DuplicateCustomizationName); + } + + Ok(CustomizationAddedEventBuilder::default() + .customization(customization) + .product_id(cmd.product_id().clone()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use uuid::Uuid; + + use crate::inventory::domain::add_customization_command::tests::get_command; + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + pub fn mock_add_customization_service( + times: Option, + cmd: AddCustomizationCommand, + ) -> AddCustomizationServiceObj { + let mut m = MockAddCustomizationUseCase::new(); + + let customization = CustomizationBuilder::default() + .name(cmd.name().into()) + .deleted(false) + .customization_id(UUID.clone()) + .build() + .unwrap(); + + let res = CustomizationAddedEventBuilder::default() + .customization(customization) + .product_id(cmd.product_id().clone()) + .build() + .unwrap(); + + if let Some(times) = times { + m.expect_add_customization() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_add_customization() + .returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service_product_doesnt_exist() { + let cmd = get_command(); + + let s = AddCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_customization_id_exists(mock_customization_id_exists_db_port_false( + IS_CALLED_ONLY_ONCE, + )) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_false(IS_CALLED_ONLY_ONCE), + ) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.add_customization(cmd.clone()).await.unwrap(); + assert_eq!(res.customization().name(), cmd.name()); + assert_eq!(res.product_id(), cmd.product_id()); + // assert_eq!(customization_added_events.len(), cmd.customizations().len()); + } + + #[actix_rt::test] + async fn test_service_product_name_exists_for_store() { + let cmd = get_command(); + + let s = AddCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_customization_id_exists(mock_customization_id_exists_db_port_false( + IS_CALLED_ONLY_ONCE, + )) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_true(IS_CALLED_ONLY_ONCE), + ) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.add_customization(cmd.clone()).await, + Err(InventoryError::DuplicateCustomizationName) + ) + } + + #[actix_rt::test] + async fn test_service_product_id_not_found() { + let cmd = get_command(); + + let s = AddCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_customization_id_exists(mock_customization_id_exists_db_port_true(IS_NEVER_CALLED)) + .db_customization_name_exists_for_product( + mock_customization_name_exists_for_product_db_port_false(IS_NEVER_CALLED), + ) + .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) + .build() + .unwrap(); + + assert_eq!( + s.add_customization(cmd.clone()).await, + Err(InventoryError::ProductIDNotFound) + ) + } +} diff --git a/src/inventory/application/services/errors.rs b/src/inventory/application/services/errors.rs index 06663f4..bc5b1d0 100644 --- a/src/inventory/application/services/errors.rs +++ b/src/inventory/application/services/errors.rs @@ -16,6 +16,7 @@ pub enum InventoryError { DuplicateStoreName, DuplicateProductName, DuplicateCustomizationName, + ProductIDNotFound, InternalError, } diff --git a/src/inventory/application/services/mod.rs b/src/inventory/application/services/mod.rs index 62788bf..704c116 100644 --- a/src/inventory/application/services/mod.rs +++ b/src/inventory/application/services/mod.rs @@ -10,6 +10,7 @@ pub mod errors; // services pub mod add_category_service; +pub mod add_customization_service; pub mod add_product_service; pub mod add_store_service; @@ -18,6 +19,7 @@ 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; + fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj; } #[derive(Clone, Builder)] @@ -25,6 +27,7 @@ pub struct InventoryServices { add_store: add_store_service::AddStoreServiceObj, add_category: add_category_service::AddCategoryServiceObj, add_product: add_product_service::AddProductServiceObj, + add_customization: add_customization_service::AddCustomizationServiceObj, } impl InventoryServicesInterface for InventoryServices { @@ -37,4 +40,8 @@ impl InventoryServicesInterface for InventoryServices { fn add_product(&self) -> add_product_service::AddProductServiceObj { self.add_product.clone() } + + fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj { + self.add_customization.clone() + } } diff --git a/src/inventory/domain/add_customization_command.rs b/src/inventory/domain/add_customization_command.rs new file mode 100644 index 0000000..c79a64e --- /dev/null +++ b/src/inventory/domain/add_customization_command.rs @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +use derive_getters::Getters; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddCustomizationCommandError { + NameIsEmpty, +} + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct UnvalidatedAddCustomizationCommand { + name: String, + product_id: Uuid, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct AddCustomizationCommand { + name: String, + product_id: Uuid, +} + +impl UnvalidatedAddCustomizationCommand { + pub fn validate(self) -> Result { + let name = self.name.trim().to_owned(); + if name.is_empty() { + return Err(AddCustomizationCommandError::NameIsEmpty); + } + + Ok(AddCustomizationCommand { + name, + product_id: self.product_id, + }) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::utils::uuid::tests::UUID; + + pub fn get_command() -> AddCustomizationCommand { + UnvalidatedAddCustomizationCommandBuilder::default() + .name("foo".into()) + .product_id(UUID.clone()) + .build() + .unwrap() + .validate() + .unwrap() + } + + #[test] + fn test_cmd() { + let name = "foo"; + let product_id = UUID; + + let cmd = UnvalidatedAddCustomizationCommandBuilder::default() + .name(name.into()) + .product_id(product_id.clone()) + .build() + .unwrap() + .validate() + .unwrap(); + + assert_eq!(cmd.name(), name); + assert_eq!(cmd.product_id(), &product_id); + } + + #[test] + fn test_cmd_name_is_empty() { + let product_id = UUID; + + assert_eq!( + UnvalidatedAddCustomizationCommandBuilder::default() + .name("".into()) + .product_id(product_id.clone()) + .build() + .unwrap() + .validate(), + Err(AddCustomizationCommandError::NameIsEmpty) + ); + } +} diff --git a/src/inventory/domain/commands.rs b/src/inventory/domain/commands.rs index 63ce9c5..d3f36ea 100644 --- a/src/inventory/domain/commands.rs +++ b/src/inventory/domain/commands.rs @@ -6,8 +6,8 @@ use mockall::predicate::*; use serde::{Deserialize, Serialize}; use super::{ - add_category_command::AddCategoryCommand, add_product_command::AddProductCommand, - add_store_command::AddStoreCommand, + add_category_command::AddCategoryCommand, add_customization_command::AddCustomizationCommand, + add_product_command::AddProductCommand, add_store_command::AddStoreCommand, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -15,4 +15,5 @@ pub enum InventoryCommand { AddCategory(AddCategoryCommand), AddStore(AddStoreCommand), AddProduct(AddProductCommand), + AddCustomization(AddCustomizationCommand), } diff --git a/src/inventory/domain/customization_added_event.rs b/src/inventory/domain/customization_added_event.rs new file mode 100644 index 0000000..d0913ff --- /dev/null +++ b/src/inventory/domain/customization_added_event.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::customization_aggregate::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct CustomizationAddedEvent { + customization: Customization, + product_id: Uuid, +} diff --git a/src/inventory/domain/events.rs b/src/inventory/domain/events.rs index 03c64eb..e40bde1 100644 --- a/src/inventory/domain/events.rs +++ b/src/inventory/domain/events.rs @@ -6,8 +6,8 @@ use cqrs_es::DomainEvent; use serde::{Deserialize, Serialize}; use super::{ - category_added_event::*, product_added_event::ProductAddedEvent, - store_added_event::StoreAddedEvent, + category_added_event::*, customization_added_event::CustomizationAddedEvent, + product_added_event::ProductAddedEvent, store_added_event::StoreAddedEvent, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -15,6 +15,7 @@ pub enum InventoryEvent { CategoryAdded(CategoryAddedEvent), StoreAdded(StoreAddedEvent), ProductAdded(ProductAddedEvent), + CustomizationAdded(CustomizationAddedEvent), } impl DomainEvent for InventoryEvent { @@ -27,6 +28,7 @@ impl DomainEvent for InventoryEvent { InventoryEvent::CategoryAdded { .. } => "InventoryCategoryAdded", InventoryEvent::StoreAdded { .. } => "InventoryStoreAdded", InventoryEvent::ProductAdded { .. } => "InventoryProductAdded", + InventoryEvent::CustomizationAdded { .. } => "InventoryCustomizationAdded", }; e.to_string() diff --git a/src/inventory/domain/mod.rs b/src/inventory/domain/mod.rs index cb92726..7417b87 100644 --- a/src/inventory/domain/mod.rs +++ b/src/inventory/domain/mod.rs @@ -4,18 +4,23 @@ // aggregates pub mod category_aggregate; +pub mod customization_aggregate; pub mod product_aggregate; pub mod stock_aggregate; pub mod store_aggregate; // commands pub mod add_category_command; +pub mod add_customization_command; pub mod add_product_command; pub mod add_store_command; pub mod commands; +pub mod update_product_command; // events pub mod category_added_event; +pub mod customization_added_event; pub mod events; pub mod product_added_event; +pub mod product_updated_event; pub mod store_added_event;