diff --git a/src/inventory/adapters/output/db/postgres/store_view.rs b/src/inventory/adapters/output/db/postgres/store_view.rs index 4198bee..cf6bfb1 100644 --- a/src/inventory/adapters/output/db/postgres/store_view.rs +++ b/src/inventory/adapters/output/db/postgres/store_view.rs @@ -218,7 +218,7 @@ mod tests { update_category_service::tests::mock_update_category_service, update_customization_service::tests::mock_update_customization_service, update_product_service::tests::mock_update_product_service, - InventoryServicesBuilder, + update_store_service::tests::mock_update_store_service, InventoryServicesBuilder, }, domain::{ add_category_command::AddCategoryCommand, add_customization_command, @@ -226,7 +226,7 @@ mod tests { commands::InventoryCommand, update_category_command::tests::get_update_category_command, update_customization_command::tests::get_update_customization_command, - update_product_command, + update_product_command, update_store_command::tests::get_update_store_cmd, }, }, tests::bdd::IS_NEVER_CALLED, @@ -279,6 +279,10 @@ mod tests { IS_NEVER_CALLED, get_update_category_command(), )) + .update_store(mock_update_store_service( + IS_NEVER_CALLED, + get_update_store_cmd(), + )) .build() .unwrap(); diff --git a/src/inventory/application/services/mod.rs b/src/inventory/application/services/mod.rs index b2b5f50..59a9f4b 100644 --- a/src/inventory/application/services/mod.rs +++ b/src/inventory/application/services/mod.rs @@ -16,6 +16,7 @@ pub mod add_store_service; pub mod update_category_service; pub mod update_customization_service; pub mod update_product_service; +pub mod update_store_service; #[automock] pub trait InventoryServicesInterface: Send + Sync { @@ -26,6 +27,7 @@ pub trait InventoryServicesInterface: Send + Sync { fn update_product(&self) -> update_product_service::UpdateProductServiceObj; fn update_customization(&self) -> update_customization_service::UpdateCustomizationServiceObj; fn update_category(&self) -> update_category_service::UpdateCategoryServiceObj; + fn update_store(&self) -> update_store_service::UpdateStoreServiceObj; } #[derive(Clone, Builder)] @@ -37,6 +39,7 @@ pub struct InventoryServices { update_product: update_product_service::UpdateProductServiceObj, update_customization: update_customization_service::UpdateCustomizationServiceObj, update_category: update_category_service::UpdateCategoryServiceObj, + update_store: update_store_service::UpdateStoreServiceObj, } impl InventoryServicesInterface for InventoryServices { @@ -65,4 +68,8 @@ impl InventoryServicesInterface for InventoryServices { fn update_category(&self) -> update_category_service::UpdateCategoryServiceObj { self.update_category.clone() } + + fn update_store(&self) -> update_store_service::UpdateStoreServiceObj { + self.update_store.clone() + } } diff --git a/src/inventory/application/services/update_store_service.rs b/src/inventory/application/services/update_store_service.rs new file mode 100644 index 0000000..08ebeb9 --- /dev/null +++ b/src/inventory/application/services/update_store_service.rs @@ -0,0 +1,146 @@ +// 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::{store_id_exists::*, store_name_exists::*}, + domain::{ + store_aggregate::*, store_updated_event::*, update_store_command::UpdateStoreCommand, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait UpdateStoreUseCase: Send + Sync { + async fn update_store(&self, cmd: UpdateStoreCommand) -> InventoryResult; +} + +pub type UpdateStoreServiceObj = Arc; + +#[derive(Clone, Builder)] +pub struct UpdateStoreService { + db_store_id_exists: StoreIDExistsDBPortObj, + db_store_name_exists: StoreNameExistsDBPortObj, +} + +#[async_trait::async_trait] +impl UpdateStoreUseCase for UpdateStoreService { + async fn update_store(&self, cmd: UpdateStoreCommand) -> InventoryResult { + if !self + .db_store_id_exists + .store_id_exists(cmd.old_store().store_id()) + .await? + { + return Err(InventoryError::StoreIDNotFound); + } + + let store = StoreBuilder::default() + .name(cmd.name().into()) + .address(cmd.address().as_ref().map(|s| s.to_string())) + .owner(*cmd.owner()) + .store_id(*cmd.old_store().store_id()) + .build() + .unwrap(); + + if cmd.name() != cmd.old_store().name() { + if self.db_store_name_exists.store_name_exists(&store).await? { + return Err(InventoryError::DuplicateStoreName); + } + } + + Ok(StoreUpdatedEventBuilder::default() + .added_by_user(*cmd.adding_by()) + .new_store(store) + .old_store(cmd.old_store().clone()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::inventory::domain::store_updated_event::tests::get_store_updated_event_from_command; + use crate::inventory::domain::update_store_command::tests::get_update_store_cmd; + use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; + + pub fn mock_update_store_service( + times: Option, + cmd: UpdateStoreCommand, + ) -> UpdateStoreServiceObj { + let mut m = MockUpdateStoreUseCase::new(); + + let res = get_store_updated_event_from_command(&cmd); + + if let Some(times) = times { + m.expect_update_store() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_update_store().returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service() { + let cmd = get_update_store_cmd(); + + let s = UpdateStoreServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_store_name_exists(mock_store_name_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.update_store(cmd.clone()).await.unwrap(); + assert_eq!(res.new_store().name(), cmd.name()); + assert_eq!(res.new_store().address(), cmd.address()); + assert_eq!(res.new_store().owner(), cmd.owner()); + assert_eq!(res.new_store().store_id(), cmd.old_store().store_id()); + assert_eq!(res.old_store(), cmd.old_store()); + assert_eq!(res.added_by_user(), cmd.adding_by()); + } + + #[actix_rt::test] + async fn test_service_store_name_exists() { + let cmd = get_update_store_cmd(); + + let s = UpdateStoreServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_store_name_exists(mock_store_name_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.update_store(cmd.clone()).await, + Err(InventoryError::DuplicateStoreName) + ); + } + + #[actix_rt::test] + async fn test_service_store_id_doesnt_exist() { + let cmd = get_update_store_cmd(); + + let s = UpdateStoreServiceBuilder::default() + .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .db_store_name_exists(mock_store_name_exists_db_port_false(IS_NEVER_CALLED)) + .build() + .unwrap(); + + assert_eq!( + s.update_store(cmd.clone()).await, + Err(InventoryError::StoreIDNotFound) + ); + } +} diff --git a/src/inventory/domain/commands.rs b/src/inventory/domain/commands.rs index 8779580..9eda0b6 100644 --- a/src/inventory/domain/commands.rs +++ b/src/inventory/domain/commands.rs @@ -10,7 +10,7 @@ use super::{ add_product_command::AddProductCommand, add_store_command::AddStoreCommand, update_category_command::UpdateCategoryCommand, update_customization_command::UpdateCustomizationCommand, - update_product_command::UpdateProductCommand, + update_product_command::UpdateProductCommand, update_store_command::UpdateStoreCommand, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -22,4 +22,5 @@ pub enum InventoryCommand { UpdateProduct(UpdateProductCommand), UpdateCustomization(UpdateCustomizationCommand), UpdateCategory(UpdateCategoryCommand), + UpdateStore(UpdateStoreCommand), } diff --git a/src/inventory/domain/events.rs b/src/inventory/domain/events.rs index cb5906c..cf4d863 100644 --- a/src/inventory/domain/events.rs +++ b/src/inventory/domain/events.rs @@ -10,6 +10,7 @@ use super::{ customization_added_event::CustomizationAddedEvent, customization_updated_event::CustomizationUpdatedEvent, product_added_event::ProductAddedEvent, product_updated_event::ProductUpdatedEvent, store_added_event::StoreAddedEvent, + store_updated_event::StoreUpdatedEvent, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -21,6 +22,7 @@ pub enum InventoryEvent { ProductUpdated(ProductUpdatedEvent), CustomizationUpdated(CustomizationUpdatedEvent), CategoryUpdated(CategoryUpdatedEvent), + StoreUpdated(StoreUpdatedEvent), } impl DomainEvent for InventoryEvent { @@ -37,6 +39,7 @@ impl DomainEvent for InventoryEvent { InventoryEvent::ProductUpdated { .. } => "InventoryProductUpdated", InventoryEvent::CustomizationUpdated { .. } => "InventoryCustomizationUpdated", InventoryEvent::CategoryUpdated { .. } => "InventoryCategoryUpdated", + InventoryEvent::StoreUpdated { .. } => "InventoryStoreUpdated", }; e.to_string() diff --git a/src/inventory/domain/mod.rs b/src/inventory/domain/mod.rs index 3f9de8c..248002f 100644 --- a/src/inventory/domain/mod.rs +++ b/src/inventory/domain/mod.rs @@ -18,6 +18,7 @@ pub mod commands; pub mod update_category_command; pub mod update_customization_command; pub mod update_product_command; +pub mod update_store_command; // events pub mod category_added_event; @@ -28,3 +29,4 @@ pub mod events; pub mod product_added_event; pub mod product_updated_event; pub mod store_added_event; +pub mod store_updated_event; diff --git a/src/inventory/domain/store_aggregate.rs b/src/inventory/domain/store_aggregate.rs index aaffbdd..d2e1365 100644 --- a/src/inventory/domain/store_aggregate.rs +++ b/src/inventory/domain/store_aggregate.rs @@ -50,17 +50,26 @@ impl Aggregate for Store { let res = services.add_store().add_store(cmd).await?; Ok(vec![InventoryEvent::StoreAdded(res)]) } + InventoryCommand::UpdateStore(cmd) => { + let res = services.update_store().update_store(cmd).await?; + Ok(vec![InventoryEvent::StoreUpdated(res)]) + } + _ => Ok(Vec::default()), } } fn apply(&mut self, event: Self::Event) { - if let InventoryEvent::StoreAdded(e) = event { - self.name = e.name().into(); - self.address = e.address().as_ref().map(|s| s.to_string()); - self.owner = *e.owner(); - self.store_id = *e.store_id(); - self.deleted = false; + match event { + InventoryEvent::StoreAdded(e) => { + self.name = e.name().into(); + self.address = e.address().as_ref().map(|s| s.to_string()); + self.owner = *e.owner(); + self.store_id = *e.store_id(); + self.deleted = false; + } + InventoryEvent::StoreUpdated(e) => *self = e.new_store().clone(), + _ => (), } } } @@ -70,13 +79,15 @@ mod tests { use std::sync::Arc; use cqrs_es::test::TestFramework; + use update_store_service::tests::mock_update_store_service; use super::*; use crate::inventory::{ application::services::{add_store_service::tests::*, *}, domain::{ add_store_command::*, commands::InventoryCommand, events::InventoryEvent, - store_added_event::*, + store_added_event::*, store_updated_event::tests::get_store_updated_event_from_command, + update_store_command::tests::get_update_store_cmd, }, }; use crate::tests::bdd::*; @@ -115,4 +126,21 @@ mod tests { .when(InventoryCommand::AddStore(cmd)) .then_expect_events(vec![expected]); } + + #[test] + fn test_update_store() { + let cmd = get_update_store_cmd(); + let expected = InventoryEvent::StoreUpdated(get_store_updated_event_from_command(&cmd)); + + let mut services = MockInventoryServicesInterface::new(); + services + .expect_update_store() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .return_const(mock_update_store_service(IS_CALLED_ONLY_ONCE, cmd.clone())); + + StoreTestFramework::with(Arc::new(services)) + .given_no_previous_events() + .when(InventoryCommand::UpdateStore(cmd)) + .then_expect_events(vec![expected]); + } } diff --git a/src/inventory/domain/store_updated_event.rs b/src/inventory/domain/store_updated_event.rs new file mode 100644 index 0000000..473ae88 --- /dev/null +++ b/src/inventory/domain/store_updated_event.rs @@ -0,0 +1,43 @@ +// 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::store_aggregate::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct StoreUpdatedEvent { + added_by_user: Uuid, + old_store: Store, + new_store: Store, +} + +#[cfg(test)] +pub mod tests { + use crate::inventory::domain::update_store_command::UpdateStoreCommand; + + use super::*; + + pub fn get_store_updated_event_from_command(cmd: &UpdateStoreCommand) -> StoreUpdatedEvent { + let new_store = StoreBuilder::default() + .name(cmd.name().into()) + .address(cmd.address().as_ref().map(|s| s.to_string())) + .owner(*cmd.owner()) + .store_id(*cmd.old_store().store_id()) + .build() + .unwrap(); + + StoreUpdatedEventBuilder::default() + .new_store(new_store) + .old_store(cmd.old_store().clone()) + .added_by_user(*cmd.adding_by()) + .build() + .unwrap() + } +} diff --git a/src/inventory/domain/update_store_command.rs b/src/inventory/domain/update_store_command.rs new file mode 100644 index 0000000..53c2e2c --- /dev/null +++ b/src/inventory/domain/update_store_command.rs @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_getters::Getters; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::store_aggregate::*; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum UpdateStoreCommandError { + NameIsEmpty, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct UpdateStoreCommand { + name: String, + address: Option, + owner: Uuid, + old_store: Store, + adding_by: Uuid, +} + +impl UpdateStoreCommand { + pub fn new( + name: String, + address: Option, + owner: Uuid, + old_store: Store, + adding_by: Uuid, + ) -> Result { + let address: Option = if let Some(address) = address { + let address = address.trim(); + if address.is_empty() { + None + } else { + Some(address.to_owned()) + } + } else { + None + }; + + let name = name.trim().to_owned(); + if name.is_empty() { + return Err(UpdateStoreCommandError::NameIsEmpty); + } + + Ok(Self { + name, + address, + owner, + old_store, + adding_by, + }) + } +} + +#[cfg(test)] +pub mod tests { + use crate::utils::uuid::tests::UUID; + + use super::*; + + pub fn get_update_store_cmd() -> UpdateStoreCommand { + let name = "foo"; + let address = "bar"; + let owner = UUID; + let adding_by = UUID; + let old_store = Store::default(); + + UpdateStoreCommand::new( + name.into(), + Some(address.into()), + owner, + old_store.clone(), + adding_by, + ) + .unwrap() + } + + #[test] + fn test_cmd() { + let name = "foo"; + let address = "bar"; + let owner = UUID; + let old_store = Store::default(); + let adding_by = Uuid::new_v4(); + + // address = None + let cmd = UpdateStoreCommand::new(name.into(), None, owner, old_store.clone(), adding_by) + .unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.address(), &None); + assert_eq!(cmd.owner(), &owner); + assert_eq!(cmd.old_store(), &old_store); + assert_eq!(cmd.adding_by(), &adding_by); + + // address = Some + let cmd = UpdateStoreCommand::new( + name.into(), + Some(address.into()), + owner, + old_store.clone(), + adding_by, + ) + .unwrap(); + assert_eq!(cmd.name(), name); + assert_eq!(cmd.address(), &Some(address.to_owned())); + assert_eq!(cmd.owner(), &owner); + assert_eq!(cmd.old_store(), &old_store); + assert_eq!(cmd.adding_by(), &adding_by); + + // UpdateStoreCommandError::NameIsEmpty + assert_eq!( + UpdateStoreCommand::new( + "".into(), + Some(address.into()), + owner, + old_store.clone(), + adding_by + ), + Err(UpdateStoreCommandError::NameIsEmpty) + ) + } +}