From e14c42023abd69e683e737125c5503fa83c3f457 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 20 Sep 2024 17:20:54 +0530 Subject: [PATCH 1/9] debug: print db url in tests --- src/settings/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 8945e65..fb2dee8 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -158,6 +158,7 @@ pub mod tests { let mut db_url = Url::parse(&settings.database.url).unwrap(); db_url.set_path(&GenerateRandomString.get_random(12)); settings.database.url = db_url.to_string(); + println!("Test database URL: {}", settings.database.url); settings.database.pool = 1; settings From 4580aea18b6255c8748ea91d67771ab7931211dd Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 20 Sep 2024 17:22:11 +0530 Subject: [PATCH 2/9] feat&fix: caller provides aggregate ID --- .../application/services/add_store_service.rs | 41 +++++---- src/inventory/application/services/errors.rs | 6 +- src/inventory/domain/add_store_command.rs | 83 +++++++++++++------ src/inventory/domain/store_aggregate.rs | 8 +- 4 files changed, 89 insertions(+), 49 deletions(-) diff --git a/src/inventory/application/services/add_store_service.rs b/src/inventory/application/services/add_store_service.rs index 30d6912..9f718f5 100644 --- a/src/inventory/application/services/add_store_service.rs +++ b/src/inventory/application/services/add_store_service.rs @@ -12,12 +12,11 @@ use super::errors::*; use crate::inventory::{ application::port::output::db::{store_id_exists::*, store_name_exists::*}, domain::{ - add_store_command::AddStoreCommand, + add_store_command::*, store_added_event::{StoreAddedEvent, StoreAddedEventBuilder}, store_aggregate::*, }, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -31,28 +30,24 @@ pub type AddStoreServiceObj = Arc; pub struct AddStoreService { db_store_id_exists: StoreIDExistsDBPortObj, db_store_name_exists: StoreNameExistsDBPortObj, - get_uuid: GetUUIDInterfaceObj, } #[async_trait::async_trait] impl AddStoreUseCase for AddStoreService { async fn add_store(&self, cmd: AddStoreCommand) -> InventoryResult { - let mut store_id = self.get_uuid.get_uuid(); - - loop { - if self.db_store_id_exists.store_id_exists(&store_id).await? { - store_id = self.get_uuid.get_uuid(); - continue; - } else { - break; - } + if self + .db_store_id_exists + .store_id_exists(cmd.store_id()) + .await? + { + return Err(InventoryError::DuplicateStoreID); } let store = StoreBuilder::default() .name(cmd.name().into()) .address(cmd.address().as_ref().map(|s| s.to_string())) .owner(*cmd.owner()) - .store_id(store_id) + .store_id(*cmd.store_id()) .build() .unwrap(); @@ -64,7 +59,7 @@ impl AddStoreUseCase for AddStoreService { .name(store.name().into()) .address(store.address().as_ref().map(|s| s.to_string())) .owner(*cmd.owner()) - .store_id(store_id) + .store_id(*cmd.store_id()) .build() .unwrap()) } @@ -109,12 +104,17 @@ pub mod tests { let owner = UUID; // address = None - let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(None) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); let s = AddStoreServiceBuilder::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_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); @@ -132,12 +132,17 @@ pub mod tests { let owner = UUID; // address = None - let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); let s = AddStoreServiceBuilder::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_true(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); diff --git a/src/inventory/application/services/errors.rs b/src/inventory/application/services/errors.rs index 703a9e2..c913f34 100644 --- a/src/inventory/application/services/errors.rs +++ b/src/inventory/application/services/errors.rs @@ -18,6 +18,7 @@ pub enum InventoryError { DuplicateStoreName, DuplicateProductName, DuplicateCustomizationName, + DuplicateStoreID, ProductIDNotFound, CategoryIDNotFound, CustomizationIDNotFound, @@ -32,10 +33,7 @@ impl From for InventoryError { InventoryDBError::DuplicateStoreName => Self::DuplicateStoreName, InventoryDBError::DuplicateProductName => Self::DuplicateProductName, InventoryDBError::DuplicateCustomizationName => Self::DuplicateCustomizationName, - InventoryDBError::DuplicateStoreID => { - error!("DuplicateStoreID"); - Self::InternalError - } + InventoryDBError::DuplicateStoreID => Self::DuplicateStoreID, InventoryDBError::DuplicateProductID => { error!("DuplicateProductID"); Self::InternalError diff --git a/src/inventory/domain/add_store_command.rs b/src/inventory/domain/add_store_command.rs index 3f11af8..0ad67f8 100644 --- a/src/inventory/domain/add_store_command.rs +++ b/src/inventory/domain/add_store_command.rs @@ -2,6 +2,7 @@ // // 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}; @@ -12,46 +13,56 @@ pub enum AddStoreCommandError { NameIsEmpty, } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +#[derive( + Clone, Builder, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, +)] +#[builder(build_fn(validate = "Self::validate"))] pub struct AddStoreCommand { + #[builder(setter(custom))] name: String, + #[builder(setter(custom))] address: Option, + store_id: Uuid, owner: Uuid, } -impl AddStoreCommand { - pub fn new( - name: String, - address: Option, - owner: Uuid, - ) -> Result { - let address: Option = if let Some(address) = address { +impl AddStoreCommandBuilder { + // pub fn custom_address(&mut self, address: Option) -> &mut Self { + // fn address(&mut self, address: Option) { + pub fn address(&mut self, address: Option) -> &mut Self { + self.address = if let Some(address) = address { let address = address.trim(); if address.is_empty() { - None + Some(None) } else { - Some(address.to_owned()) + Some(Some(address.to_owned())) } } else { - None + Some(None) }; + self + } - let name = name.trim().to_owned(); + //pub fn custom_name(&mut self, name: String) -> &mut Self { + //fn name(&mut self, name: String) { + pub fn name(&mut self, name: String) -> &mut Self { + self.name = Some(name.trim().to_owned()); + self + } + + fn validate(&self) -> Result<(), String> { + let name = self.name.as_ref().unwrap().trim().to_owned(); if name.is_empty() { - return Err(AddStoreCommandError::NameIsEmpty); + return Err(AddStoreCommandError::NameIsEmpty.to_string()); } - - Ok(Self { - name, - address, - owner, - }) + Ok(()) } } #[cfg(test)] mod tests { - use crate::utils::uuid::tests::UUID; + use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; use super::*; @@ -62,21 +73,41 @@ mod tests { let owner = UUID; // address = None - let cmd = AddStoreCommand::new(name.into(), None, owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(None) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); + // let cmd = AddStoreCommand::new(name.into(), None, owner, UUID).unwrap(); assert_eq!(cmd.name(), name); assert_eq!(cmd.address(), &None); assert_eq!(cmd.owner(), &owner); + assert_eq!(*cmd.store_id(), UUID); // address = Some - let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); + // let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner, UUID).unwrap(); assert_eq!(cmd.name(), name); assert_eq!(cmd.address(), &Some(address.to_owned())); assert_eq!(cmd.owner(), &owner); + assert_eq!(*cmd.store_id(), UUID); // AddStoreCommandError::NameIsEmpty - assert_eq!( - AddStoreCommand::new("".into(), Some(address.into()), owner), - Err(AddStoreCommandError::NameIsEmpty) - ) + + assert!(AddStoreCommandBuilder::default() + .name("".into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .is_err()) } } diff --git a/src/inventory/domain/store_aggregate.rs b/src/inventory/domain/store_aggregate.rs index d2e1365..397fae1 100644 --- a/src/inventory/domain/store_aggregate.rs +++ b/src/inventory/domain/store_aggregate.rs @@ -113,7 +113,13 @@ mod tests { .unwrap(); let expected = InventoryEvent::StoreAdded(expected); - let cmd = AddStoreCommand::new(name.into(), address.clone(), owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(address.clone()) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); let mut services = MockInventoryServicesInterface::new(); services From 2853bc5a7589aeb5d1a7de65d8a58d5b4e50b6bf Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 20 Sep 2024 17:23:14 +0530 Subject: [PATCH 3/9] feat: store view tests --- ...c9603f05782f55f72d21fc29954a65622b5d.json} | 5 +- .../adapters/output/db/postgres/store_view.rs | 190 +++++++++++------- 2 files changed, 119 insertions(+), 76 deletions(-) rename .sqlx/{query-0ccd86643c13d9d7c793f362aedc2d720d9dda982a0416849bf97291fc645ea5.json => query-8d62d7b612fd7f323aee586047dec9603f05782f55f72d21fc29954a65622b5d.json} (64%) diff --git a/.sqlx/query-0ccd86643c13d9d7c793f362aedc2d720d9dda982a0416849bf97291fc645ea5.json b/.sqlx/query-8d62d7b612fd7f323aee586047dec9603f05782f55f72d21fc29954a65622b5d.json similarity index 64% rename from .sqlx/query-0ccd86643c13d9d7c793f362aedc2d720d9dda982a0416849bf97291fc645ea5.json rename to .sqlx/query-8d62d7b612fd7f323aee586047dec9603f05782f55f72d21fc29954a65622b5d.json index f0b010a..6d8947b 100644 --- a/.sqlx/query-0ccd86643c13d9d7c793f362aedc2d720d9dda982a0416849bf97291fc645ea5.json +++ b/.sqlx/query-8d62d7b612fd7f323aee586047dec9603f05782f55f72d21fc29954a65622b5d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_inventory_store_query\n SET\n version = $1,\n name = $2,\n address = $3,\n store_id = $4,\n owner = $5,\n deleted = $6;", + "query": "UPDATE\n cqrs_inventory_store_query\n SET\n version = $1,\n name = $2,\n address = $3,\n owner = $4,\n deleted = $5;", "describe": { "columns": [], "parameters": { @@ -9,11 +9,10 @@ "Text", "Text", "Uuid", - "Uuid", "Bool" ] }, "nullable": [] }, - "hash": "0ccd86643c13d9d7c793f362aedc2d720d9dda982a0416849bf97291fc645ea5" + "hash": "8d62d7b612fd7f323aee586047dec9603f05782f55f72d21fc29954a65622b5d" } diff --git a/src/inventory/adapters/output/db/postgres/store_view.rs b/src/inventory/adapters/output/db/postgres/store_view.rs index 6c114c2..ced26ec 100644 --- a/src/inventory/adapters/output/db/postgres/store_view.rs +++ b/src/inventory/adapters/output/db/postgres/store_view.rs @@ -11,7 +11,7 @@ use uuid::Uuid; use super::errors::*; use super::InventoryDBPostgresAdapter; use crate::inventory::domain::events::InventoryEvent; -use crate::inventory::domain::store_aggregate::Store; +use crate::inventory::domain::store_aggregate::{Store, StoreBuilder}; use crate::utils::parse_aggregate_id::parse_aggregate_id; pub const NEW_STORE_NON_UUID: &str = "new_store_non_uuid-asdfa"; @@ -27,6 +27,19 @@ pub struct StoreView { deleted: bool, } +impl From for Store { + fn from(value: StoreView) -> Self { + StoreBuilder::default() + .name(value.name) + .address(value.address) + .store_id(value.store_id) + .owner(value.owner) + .deleted(value.deleted) + .build() + .unwrap() + } +} + // This updates the view with events as they are committed. // The logic should be minimal here, e.g., don't calculate the account balance, // design the events to carry the balance information instead. @@ -156,13 +169,11 @@ impl ViewRepository for InventoryDBPostgresAdapter { version = $1, name = $2, address = $3, - store_id = $4, - owner = $5, - deleted = $6;", + owner = $4, + deleted = $5;", version, view.name, view.address, - view.store_id, view.owner, view.deleted, ) @@ -192,12 +203,21 @@ impl Query for SimpleLoggingQuery { #[async_trait] impl Query for InventoryDBPostgresAdapter { - async fn dispatch(&self, store_id: &str, events: &[EventEnvelope]) { + async fn dispatch(&self, aggregate_id: &str, events: &[EventEnvelope]) { + println!("Got store ID: {}", aggregate_id); + let res = self - .load_with_context(store_id) + .load_with_context(&aggregate_id) .await - .unwrap_or_else(|_| Some((StoreView::default(), ViewContext::new(store_id.into(), 0)))); + .unwrap_or_else(|_| { + println!("generating default"); + Some(( + StoreView::default(), + ViewContext::new(aggregate_id.into(), 0), + )) + }); let (mut view, view_context): (StoreView, ViewContext) = res.unwrap(); + for event in events { view.update(event); } @@ -205,11 +225,6 @@ impl Query for InventoryDBPostgresAdapter { } } -// Our second query, this one will be handled with Postgres `GenericQuery` -// which will serialize and persist our view after it is updated. It also -// provides a `load` method to deserialize the view on request. -//pub type StoreQuery = GenericQuery; -//pub type StoreQuery = Query; #[cfg(test)] mod tests { @@ -221,31 +236,20 @@ mod tests { db::migrate::*, 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, - update_category_service::tests::mock_update_category_service, - update_customization_service::tests::mock_update_customization_service, - update_product_service::tests::mock_update_product_service, - update_store_service::tests::mock_update_store_service, InventoryServicesBuilder, - }, - domain::{ - add_category_command::AddCategoryCommand, add_customization_command, - add_product_command::tests::get_command, add_store_command::AddStoreCommand, - commands::InventoryCommand, - update_category_command::tests::get_update_category_command, - update_customization_command::tests::get_update_customization_command, - update_product_command, update_store_command::tests::get_update_store_cmd, + add_store_service::AddStoreServiceBuilder, update_store_service::*, + MockInventoryServicesInterface, }, + domain::add_store_command::*, + domain::commands::InventoryCommand, + domain::update_store_command::*, }, - tests::bdd::IS_NEVER_CALLED, + tests::bdd::*, utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, }; use std::sync::Arc; #[actix_rt::test] - async fn pg_query() { + async fn pg_query_inventory_store_view() { let settings = crate::settings::tests::get_settings().await; //let settings = crate::settings::Settings::new().unwrap(); settings.create_db().await; @@ -259,61 +263,101 @@ mod tests { let queries: Vec>> = vec![Box::new(simple_query), Box::new(db.clone())]; - let services = InventoryServicesBuilder::default() - .add_store(Arc::new( - AddStoreServiceBuilder::default() - .db_store_id_exists(Arc::new(db.clone())) - .db_store_name_exists(Arc::new(db.clone())) - .get_uuid(Arc::new(crate::utils::uuid::GenerateUUID {})) - .build() - .unwrap(), - )) - .add_category(mock_add_category_service( - IS_NEVER_CALLED, - 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(), - )) - .update_product(mock_update_product_service( - IS_NEVER_CALLED, - update_product_command::tests::get_command(), - )) - .update_customization(mock_update_customization_service( - IS_NEVER_CALLED, - get_update_customization_command(), - )) - .update_category(mock_update_category_service( - IS_NEVER_CALLED, - get_update_category_command(), - )) - .update_store(mock_update_store_service( - IS_NEVER_CALLED, - get_update_store_cmd(), - )) - .build() - .unwrap(); + let mut mock_services = MockInventoryServicesInterface::new(); - let (cqrs, _store_query): ( + let db2 = db.clone(); + mock_services + .expect_add_store() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddStoreServiceBuilder::default() + .db_store_id_exists(Arc::new(db2.clone())) + .db_store_name_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_update_store() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateStoreServiceBuilder::default() + .db_store_id_exists(Arc::new(db2.clone())) + .db_store_name_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let (cqrs, store_query): ( Arc>, Arc>, ) = ( Arc::new(postgres_es::postgres_cqrs( db.pool.clone(), queries, - Arc::new(services), + Arc::new(mock_services), )), Arc::new(db.clone()), ); let rand = crate::utils::random_string::GenerateRandomString {}; - let cmd = AddStoreCommand::new(rand.get_random(10), None, UUID).unwrap(); - cqrs.execute("", InventoryCommand::AddStore(cmd.clone())) - .await + let cmd = AddStoreCommandBuilder::default() + .name(rand.get_random(10)) + .address(None) + .owner(UUID) + .store_id(UUID) + .build() .unwrap(); + cqrs.execute( + &cmd.store_id().to_string(), + InventoryCommand::AddStore(cmd.clone()), + ) + .await + .unwrap(); - settings.drop_db().await; + let store = store_query + .load(&(*cmd.store_id()).to_string()) + .await + .unwrap() + .unwrap(); + let store: Store = store.into(); + assert_eq!(store.name(), cmd.name()); + assert_eq!(store.address(), cmd.address()); + assert_eq!(store.owner(), cmd.owner()); + assert_eq!(store.store_id(), cmd.store_id()); + assert!(!store.deleted()); + + let update_store_cmd = UpdateStoreCommand::new( + rand.get_random(10), + Some(rand.get_random(10)), + UUID, + store, + UUID, + ) + .unwrap(); + cqrs.execute( + &cmd.store_id().to_string(), + InventoryCommand::UpdateStore(update_store_cmd.clone()), + ) + .await + .unwrap(); + let store = store_query + .load(&(*cmd.store_id()).to_string()) + .await + .unwrap() + .unwrap(); + let store: Store = store.into(); + assert_eq!(store.name(), update_store_cmd.name()); + assert_eq!(store.address(), update_store_cmd.address()); + assert_eq!(store.owner(), update_store_cmd.owner()); + assert_eq!(store.store_id(), update_store_cmd.old_store().store_id()); + assert!(!store.deleted()); + + settings.drop_db().await; } } From ac6da029a58df821601149234cfad30225e3c400 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 20 Sep 2024 18:09:08 +0530 Subject: [PATCH 4/9] feat: inventory: customization ID is provided by caller --- .../services/add_customization_service.rs | 34 +++++------- src/inventory/application/services/errors.rs | 6 +- .../domain/add_customization_command.rs | 55 ++++++++----------- src/inventory/domain/add_store_command.rs | 4 -- 4 files changed, 39 insertions(+), 60 deletions(-) diff --git a/src/inventory/application/services/add_customization_service.rs b/src/inventory/application/services/add_customization_service.rs index 9dcedcf..acf4905 100644 --- a/src/inventory/application/services/add_customization_service.rs +++ b/src/inventory/application/services/add_customization_service.rs @@ -24,7 +24,6 @@ use crate::inventory::{ product_aggregate::*, }, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -42,7 +41,6 @@ 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] @@ -59,25 +57,19 @@ impl AddCustomizationUseCase for AddCustomizationService { 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; - } + if self + .db_customization_id_exists + .customization_id_exists(cmd.customization_id()) + .await? + { + return Err(InventoryError::DuplicateCustomizationID); } let customization = CustomizationBuilder::default() .name(cmd.name().into()) .deleted(false) .product_id(*cmd.product_id()) - .customization_id(customization_id) + .customization_id(*cmd.customization_id()) .build() .unwrap(); @@ -101,11 +93,9 @@ pub mod tests { use super::*; use customization_added_event::tests::get_customization_added_event_from_cmd; - 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}; + use crate::tests::bdd::*; pub fn mock_add_customization_service( times: Option, @@ -138,12 +128,16 @@ pub mod tests { .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.customization().product_id(), cmd.product_id()); + assert_eq!( + res.customization().customization_id(), + cmd.customization_id() + ); // assert_eq!(customization_added_events.len(), cmd.customizations().len()); } @@ -159,7 +153,6 @@ pub mod tests { .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(); @@ -179,7 +172,6 @@ pub mod tests { .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(); diff --git a/src/inventory/application/services/errors.rs b/src/inventory/application/services/errors.rs index c913f34..8d256f5 100644 --- a/src/inventory/application/services/errors.rs +++ b/src/inventory/application/services/errors.rs @@ -18,6 +18,7 @@ pub enum InventoryError { DuplicateStoreName, DuplicateProductName, DuplicateCustomizationName, + DuplicateCustomizationID, DuplicateStoreID, ProductIDNotFound, CategoryIDNotFound, @@ -42,10 +43,7 @@ impl From for InventoryError { error!("DuplicateCategoryID"); Self::InternalError } - InventoryDBError::DuplicateCustomizationID => { - error!("DuplicateCustomizationID"); - Self::InternalError - } + InventoryDBError::DuplicateCustomizationID => Self::DuplicateCustomizationID, InventoryDBError::InternalError => Self::InternalError, InventoryDBError::ProductIDNotFound => InventoryError::ProductIDNotFound, InventoryDBError::CategoryIDNotFound => InventoryError::CategoryIDNotFound, diff --git a/src/inventory/domain/add_customization_command.rs b/src/inventory/domain/add_customization_command.rs index c79a64e..507412c 100644 --- a/src/inventory/domain/add_customization_command.rs +++ b/src/inventory/domain/add_customization_command.rs @@ -16,28 +16,26 @@ pub enum AddCustomizationCommandError { #[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)] +#[builder(build_fn(validate = "Self::validate"))] pub struct AddCustomizationCommand { + #[builder(setter(custom))] name: String, product_id: Uuid, + customization_id: Uuid, } -impl UnvalidatedAddCustomizationCommand { - pub fn validate(self) -> Result { - let name = self.name.trim().to_owned(); - if name.is_empty() { - return Err(AddCustomizationCommandError::NameIsEmpty); - } +impl AddCustomizationCommandBuilder { + pub fn name(&mut self, name: String) -> &mut Self { + self.name = Some(name.trim().to_owned()); + self + } - Ok(AddCustomizationCommand { - name, - product_id: self.product_id, - }) + fn validate(&self) -> Result<(), String> { + let name = self.name.as_ref().unwrap().trim().to_owned(); + if name.is_empty() { + return Err(AddCustomizationCommandError::NameIsEmpty.to_string()); + } + Ok(()) } } @@ -48,13 +46,12 @@ pub mod tests { use crate::utils::uuid::tests::UUID; pub fn get_command() -> AddCustomizationCommand { - UnvalidatedAddCustomizationCommandBuilder::default() + AddCustomizationCommandBuilder::default() .name("foo".into()) .product_id(UUID.clone()) + .customization_id(UUID.clone()) .build() .unwrap() - .validate() - .unwrap() } #[test] @@ -62,12 +59,11 @@ pub mod tests { let name = "foo"; let product_id = UUID; - let cmd = UnvalidatedAddCustomizationCommandBuilder::default() + let cmd = AddCustomizationCommandBuilder::default() .name(name.into()) .product_id(product_id.clone()) + .customization_id(UUID.clone()) .build() - .unwrap() - .validate() .unwrap(); assert_eq!(cmd.name(), name); @@ -78,14 +74,11 @@ pub mod tests { 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) - ); + assert!(AddCustomizationCommandBuilder::default() + .name("".into()) + .product_id(product_id.clone()) + .customization_id(UUID.clone()) + .build() + .is_err(),); } } diff --git a/src/inventory/domain/add_store_command.rs b/src/inventory/domain/add_store_command.rs index 0ad67f8..68cb21e 100644 --- a/src/inventory/domain/add_store_command.rs +++ b/src/inventory/domain/add_store_command.rs @@ -27,8 +27,6 @@ pub struct AddStoreCommand { } impl AddStoreCommandBuilder { - // pub fn custom_address(&mut self, address: Option) -> &mut Self { - // fn address(&mut self, address: Option) { pub fn address(&mut self, address: Option) -> &mut Self { self.address = if let Some(address) = address { let address = address.trim(); @@ -43,8 +41,6 @@ impl AddStoreCommandBuilder { self } - //pub fn custom_name(&mut self, name: String) -> &mut Self { - //fn name(&mut self, name: String) { pub fn name(&mut self, name: String) -> &mut Self { self.name = Some(name.trim().to_owned()); self From 5245cf02e0c0142b2cea05572919c32c435919f6 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 20 Sep 2024 18:09:20 +0530 Subject: [PATCH 5/9] feat: inventory: customization view tests --- .../output/db/postgres/customization_view.rs | 148 +++++++++++++++++- .../output/db/postgres/product_id_exists.rs | 4 +- .../adapters/output/db/postgres/store_view.rs | 3 +- 3 files changed, 147 insertions(+), 8 deletions(-) diff --git a/src/inventory/adapters/output/db/postgres/customization_view.rs b/src/inventory/adapters/output/db/postgres/customization_view.rs index e97a15f..8ed83f2 100644 --- a/src/inventory/adapters/output/db/postgres/customization_view.rs +++ b/src/inventory/adapters/output/db/postgres/customization_view.rs @@ -187,12 +187,10 @@ impl ViewRepository for InventoryDBPostgresAda SET version = $1, name = $2, - customization_id = $3, - product_id = $4, - deleted = $5;", + product_id = $3, + deleted = $4;", version, view.name, - view.customization_id, view.product_id, view.deleted, ) @@ -225,3 +223,145 @@ impl Query for InventoryDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + db::migrate::*, + inventory::{ + adapters::output::db::postgres::product_id_exists::tests::create_dummy_product_record, + application::services::{ + add_customization_service::*, update_customization_service::*, + MockInventoryServicesInterface, + }, + domain::{ + add_customization_command::*, + commands::InventoryCommand, + product_aggregate::Product, + update_customization_command::{tests::get_update_customization_command, *}, + }, + }, + tests::bdd::*, + utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_inventory_customization_view() { + let settings = crate::settings::tests::get_settings().await; + //let settings = crate::settings::Settings::new().unwrap(); + settings.create_db().await; + + let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; + db.migrate().await; + let db = InventoryDBPostgresAdapter::new(db.pool.clone()); + let product = Product::default(); + create_dummy_product_record(&product, &db).await; + + // let simple_query = super::store_view::SimpleLoggingQuery {}; + + let queries: Vec>> = vec![Box::new(db.clone())]; + + let mut mock_services = MockInventoryServicesInterface::new(); + + let db2 = db.clone(); + mock_services + .expect_add_customization() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddCustomizationServiceBuilder::default() + .db_product_id_exists(Arc::new(db2.clone())) + .db_customization_id_exists(Arc::new(db2.clone())) + .db_customization_name_exists_for_product(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = Arc::new(db.clone()); + mock_services + .expect_update_customization() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateCustomizationServiceBuilder::default() + .db_product_id_exists(db2.clone()) + .db_customization_name_exists_for_product(db2.clone()) + .db_customization_id_exists(db2.clone()) + .build() + .unwrap(), + ) + }); + + let (cqrs, custmoization_query): ( + Arc>, + Arc>, + ) = ( + Arc::new(postgres_es::postgres_cqrs( + db.pool.clone(), + queries, + Arc::new(mock_services), + )), + Arc::new(db.clone()), + ); + + let rand = crate::utils::random_string::GenerateRandomString {}; + + let cmd = AddCustomizationCommandBuilder::default() + .name(rand.get_random(10)) + .product_id(product.product_id().clone()) + .customization_id(UUID.clone()) + .build() + .unwrap(); + + cqrs.execute( + &cmd.customization_id().to_string(), + InventoryCommand::AddCustomization(cmd.clone()), + ) + .await + .unwrap(); + let customization = custmoization_query + .load(&(*cmd.customization_id()).to_string()) + .await + .unwrap() + .unwrap(); + let customization: Customization = customization.into(); + assert_eq!(customization.name(), cmd.name()); + assert_eq!(customization.customization_id(), cmd.customization_id()); + assert_eq!(customization.product_id(), cmd.product_id()); + assert!(!customization.deleted()); + + let update_customization_command = UnvalidatedUpdateCustomizationCommandBuilder::default() + .name(rand.get_random(10)) + .old_customization(customization.clone()) + .adding_by(UUID.clone()) + .build() + .unwrap() + .validate() + .unwrap(); + cqrs.execute( + &cmd.customization_id().to_string(), + InventoryCommand::UpdateCustomization(update_customization_command.clone()), + ) + .await + .unwrap(); + let c = custmoization_query + .load(&(*cmd.customization_id()).to_string()) + .await + .unwrap() + .unwrap(); + let c: Customization = c.into(); + assert_eq!(c.name(), update_customization_command.name()); + assert_eq!( + update_customization_command.old_customization(), + &customization + ); + assert!(!c.deleted()); + settings.drop_db().await; + } +} 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 63b1c35..aed744f 100644 --- a/src/inventory/adapters/output/db/postgres/product_id_exists.rs +++ b/src/inventory/adapters/output/db/postgres/product_id_exists.rs @@ -95,8 +95,8 @@ pub mod tests { );", 1, p.name(), - p.description().as_ref().unwrap(), - p.image().as_ref().unwrap(), + p.description().as_ref().map(|s| s.as_str()), + p.image().as_ref().map(|s| s.as_str()), p.product_id(), p.category_id(), p.price().major().clone() as i32, diff --git a/src/inventory/adapters/output/db/postgres/store_view.rs b/src/inventory/adapters/output/db/postgres/store_view.rs index ced26ec..bf35792 100644 --- a/src/inventory/adapters/output/db/postgres/store_view.rs +++ b/src/inventory/adapters/output/db/postgres/store_view.rs @@ -225,7 +225,6 @@ impl Query for InventoryDBPostgresAdapter { } } - #[cfg(test)] mod tests { use super::*; @@ -358,6 +357,6 @@ mod tests { assert_eq!(store.store_id(), update_store_cmd.old_store().store_id()); assert!(!store.deleted()); - settings.drop_db().await; + settings.drop_db().await; } } From d727d0b5b0b5afd039380b8de0ca356c83f18d31 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sat, 21 Sep 2024 15:34:14 +0530 Subject: [PATCH 6/9] feat: inventory: category ID is provided by caller --- .../services/add_category_service.rs | 60 +++++++-------- src/inventory/application/services/errors.rs | 6 +- .../services/update_category_service.rs | 1 - src/inventory/domain/add_category_command.rs | 73 ++++++++++++------- src/inventory/domain/category_aggregate.rs | 10 ++- 5 files changed, 88 insertions(+), 62 deletions(-) diff --git a/src/inventory/application/services/add_category_service.rs b/src/inventory/application/services/add_category_service.rs index 8e16448..8958a59 100644 --- a/src/inventory/application/services/add_category_service.rs +++ b/src/inventory/application/services/add_category_service.rs @@ -14,12 +14,11 @@ use crate::inventory::{ category_id_exists::*, category_name_exists_for_store::*, store_id_exists::*, }, domain::{ - add_category_command::AddCategoryCommand, + add_category_command::*, category_added_event::{CategoryAddedEvent, CategoryAddedEventBuilder}, category_aggregate::*, }, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -34,7 +33,6 @@ pub struct AddCategoryService { db_store_id_exists: StoreIDExistsDBPortObj, db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj, db_category_id_exists: CategoryIDExistsDBPortObj, - get_uuid: GetUUIDInterfaceObj, } #[async_trait::async_trait] @@ -48,27 +46,19 @@ impl AddCategoryUseCase for AddCategoryService { return Err(InventoryError::StoreIDNotFound); } - let mut category_id = self.get_uuid.get_uuid(); - - loop { - if self - .db_category_id_exists - .category_id_exists(&category_id) - .await? - { - category_id = self.get_uuid.get_uuid(); - - continue; - } else { - break; - } + if self + .db_category_id_exists + .category_id_exists(cmd.category_id()) + .await? + { + return Err(InventoryError::DuplicateCategoryID); } let category = CategoryBuilder::default() .name(cmd.name().into()) .description(cmd.description().as_ref().map(|s| s.to_string())) .store_id(*cmd.store_id()) - .category_id(category_id) + .category_id(*cmd.category_id()) .build() .unwrap(); @@ -97,8 +87,8 @@ pub mod tests { use uuid::Uuid; + use crate::tests::bdd::*; use crate::utils::uuid::tests::UUID; - use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; pub fn mock_add_category_service( times: Option, @@ -111,7 +101,7 @@ pub mod tests { .description(cmd.description().as_ref().map(|s| s.to_string())) .added_by_user(*cmd.adding_by()) .store_id(*cmd.store_id()) - .category_id(UUID) + .category_id(*cmd.category_id()) .build() .unwrap(); @@ -133,8 +123,13 @@ pub mod tests { let user_id = UUID; let store_id = Uuid::new_v4(); - // description = None - let cmd = AddCategoryCommand::new(name.into(), Some(description.into()), store_id, user_id) + let cmd = AddCategoryCommandBuilder::default() + .name(name.into()) + .description(Some(description.into())) + .store_id(store_id) + .adding_by(user_id) + .category_id(UUID) + .build() .unwrap(); let s = AddCategoryServiceBuilder::default() @@ -143,7 +138,6 @@ pub mod tests { 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(); @@ -162,8 +156,13 @@ pub mod tests { let user_id = UUID; let store_id = Uuid::new_v4(); - // description = None - let cmd = AddCategoryCommand::new(name.into(), Some(description.into()), store_id, user_id) + let cmd = AddCategoryCommandBuilder::default() + .name(name.into()) + .description(Some(description.into())) + .store_id(store_id) + .adding_by(user_id) + .category_id(UUID) + .build() .unwrap(); let s = AddCategoryServiceBuilder::default() @@ -172,7 +171,6 @@ pub mod tests { 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(); @@ -189,8 +187,13 @@ pub mod tests { let user_id = UUID; let store_id = Uuid::new_v4(); - // description = None - let cmd = AddCategoryCommand::new(name.into(), Some(description.into()), store_id, user_id) + let cmd = AddCategoryCommandBuilder::default() + .name(name.into()) + .description(Some(description.into())) + .store_id(store_id) + .adding_by(user_id) + .category_id(UUID) + .build() .unwrap(); let s = AddCategoryServiceBuilder::default() @@ -199,7 +202,6 @@ pub mod tests { IS_NEVER_CALLED, )) .db_category_id_exists(mock_category_id_exists_db_port_false(IS_NEVER_CALLED)) - .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) .build() .unwrap(); diff --git a/src/inventory/application/services/errors.rs b/src/inventory/application/services/errors.rs index 8d256f5..5373fed 100644 --- a/src/inventory/application/services/errors.rs +++ b/src/inventory/application/services/errors.rs @@ -20,6 +20,7 @@ pub enum InventoryError { DuplicateCustomizationName, DuplicateCustomizationID, DuplicateStoreID, + DuplicateCategoryID, ProductIDNotFound, CategoryIDNotFound, CustomizationIDNotFound, @@ -39,10 +40,7 @@ impl From for InventoryError { error!("DuplicateProductID"); Self::InternalError } - InventoryDBError::DuplicateCategoryID => { - error!("DuplicateCategoryID"); - Self::InternalError - } + InventoryDBError::DuplicateCategoryID => Self::DuplicateCategoryID, InventoryDBError::DuplicateCustomizationID => Self::DuplicateCustomizationID, InventoryDBError::InternalError => Self::InternalError, InventoryDBError::ProductIDNotFound => InventoryError::ProductIDNotFound, diff --git a/src/inventory/application/services/update_category_service.rs b/src/inventory/application/services/update_category_service.rs index be6f876..b2a9c61 100644 --- a/src/inventory/application/services/update_category_service.rs +++ b/src/inventory/application/services/update_category_service.rs @@ -15,7 +15,6 @@ use crate::inventory::{ }, domain::{category_aggregate::*, category_updated_event::*, update_category_command::*}, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] diff --git a/src/inventory/domain/add_category_command.rs b/src/inventory/domain/add_category_command.rs index 9c8d0eb..406ab70 100644 --- a/src/inventory/domain/add_category_command.rs +++ b/src/inventory/domain/add_category_command.rs @@ -2,6 +2,7 @@ // // 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}; @@ -12,21 +13,22 @@ pub enum AddCategoryCommandError { NameIsEmpty, } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, +)] +#[builder(build_fn(validate = "Self::validate"))] pub struct AddCategoryCommand { + #[builder(setter(custom))] name: String, + #[builder(setter(custom))] description: Option, store_id: Uuid, + category_id: Uuid, adding_by: Uuid, } -impl AddCategoryCommand { - pub fn new( - name: String, - description: Option, - store_id: Uuid, - adding_by: Uuid, - ) -> Result { +impl AddCategoryCommandBuilder { + pub fn description(&mut self, description: Option) -> &mut Self { let description: Option = if let Some(description) = description { let description = description.trim(); if description.is_empty() { @@ -37,18 +39,22 @@ impl AddCategoryCommand { } else { None }; + self.description = Some(description); + self + } + pub fn name(&mut self, name: String) -> &mut Self { let name = name.trim().to_owned(); - if name.is_empty() { - return Err(AddCategoryCommandError::NameIsEmpty); + self.name = Some(name); + self + } + + pub fn validate(&self) -> Result<(), String> { + if self.name.as_ref().unwrap().is_empty() { + return Err(AddCategoryCommandError::NameIsEmpty.to_string()); } - Ok(Self { - name, - store_id, - description, - adding_by, - }) + Ok(()) } } @@ -65,26 +71,41 @@ mod tests { let adding_by = UUID; let store_id = Uuid::new_v4(); - // description = None - let cmd = AddCategoryCommand::new(name.into(), None, store_id, adding_by).unwrap(); + let cmd = AddCategoryCommandBuilder::default() + .name(name.into()) + .description(None) + .store_id(store_id) + .adding_by(adding_by) + .category_id(UUID) + .build() + .unwrap(); assert_eq!(cmd.name(), name); assert_eq!(cmd.description(), &None); assert_eq!(cmd.adding_by(), &adding_by); assert_eq!(cmd.store_id(), &store_id); - // description = Some - let cmd = - AddCategoryCommand::new(name.into(), Some(description.into()), store_id, adding_by) - .unwrap(); + let cmd = AddCategoryCommandBuilder::default() + .name(name.into()) + .description(Some(description.into())) + .store_id(store_id) + .adding_by(adding_by) + .category_id(UUID) + .build() + .unwrap(); + assert_eq!(cmd.name(), name); assert_eq!(cmd.description(), &Some(description.to_owned())); assert_eq!(cmd.adding_by(), &adding_by); assert_eq!(cmd.store_id(), &store_id); // AddCategoryCommandError::NameIsEmpty - assert_eq!( - AddCategoryCommand::new("".into(), Some(description.into()), store_id, adding_by,), - Err(AddCategoryCommandError::NameIsEmpty) - ) + assert!(AddCategoryCommandBuilder::default() + .name("".into()) + .description(Some(description.into())) + .store_id(store_id) + .adding_by(adding_by) + .category_id(UUID) + .build() + .is_err()) } } diff --git a/src/inventory/domain/category_aggregate.rs b/src/inventory/domain/category_aggregate.rs index faeabb7..09bf84e 100644 --- a/src/inventory/domain/category_aggregate.rs +++ b/src/inventory/domain/category_aggregate.rs @@ -107,8 +107,14 @@ mod aggregate_tests { let store_id = Uuid::new_v4(); let category_id = UUID; - let cmd = - AddCategoryCommand::new(name.into(), description.clone(), store_id, adding_by).unwrap(); + let cmd = AddCategoryCommandBuilder::default() + .name(name.into()) + .description(description.clone()) + .store_id(store_id) + .adding_by(adding_by) + .category_id(category_id) + .build() + .unwrap(); let expected = CategoryAddedEventBuilder::default() .name(cmd.name().into()) From 73d829f3f8d2fff261ca7cdc47b8cecd419ba275 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sat, 21 Sep 2024 15:34:26 +0530 Subject: [PATCH 7/9] feat: inventory: category view tests --- ...a97b964024252bed2d0caeebee175acd55d9.json} | 5 +- ...67b2cade64d9ebd47f3fc6143b057671cc1b.json} | 5 +- .../output/db/postgres/category_view.rs | 161 +++++++++++++++++- .../output/db/postgres/store_id_exists.rs | 2 +- 4 files changed, 161 insertions(+), 12 deletions(-) rename .sqlx/{query-14d111d7453b89f2346966a9fdd725d269ef36288ed5e7fe1f7ad452deaab0e6.json => query-c9bee14e15dae80b7af9b0fdca43a97b964024252bed2d0caeebee175acd55d9.json} (62%) rename .sqlx/{query-e88a5dae732c3f8180664f306b4bb1d21f97a2f1391860eb8714a52ef4439d81.json => query-d46bb69f4e2afbae01ab08beb48a67b2cade64d9ebd47f3fc6143b057671cc1b.json} (64%) diff --git a/.sqlx/query-14d111d7453b89f2346966a9fdd725d269ef36288ed5e7fe1f7ad452deaab0e6.json b/.sqlx/query-c9bee14e15dae80b7af9b0fdca43a97b964024252bed2d0caeebee175acd55d9.json similarity index 62% rename from .sqlx/query-14d111d7453b89f2346966a9fdd725d269ef36288ed5e7fe1f7ad452deaab0e6.json rename to .sqlx/query-c9bee14e15dae80b7af9b0fdca43a97b964024252bed2d0caeebee175acd55d9.json index eb6f222..10d0d3b 100644 --- a/.sqlx/query-14d111d7453b89f2346966a9fdd725d269ef36288ed5e7fe1f7ad452deaab0e6.json +++ b/.sqlx/query-c9bee14e15dae80b7af9b0fdca43a97b964024252bed2d0caeebee175acd55d9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_inventory_category_query\n SET\n version = $1,\n name = $2,\n description = $3,\n category_id = $4,\n store_id = $5,\n deleted = $6;", + "query": "UPDATE\n cqrs_inventory_category_query\n SET\n version = $1,\n name = $2,\n description = $3,\n store_id = $4,\n deleted = $5;", "describe": { "columns": [], "parameters": { @@ -9,11 +9,10 @@ "Text", "Text", "Uuid", - "Uuid", "Bool" ] }, "nullable": [] }, - "hash": "14d111d7453b89f2346966a9fdd725d269ef36288ed5e7fe1f7ad452deaab0e6" + "hash": "c9bee14e15dae80b7af9b0fdca43a97b964024252bed2d0caeebee175acd55d9" } diff --git a/.sqlx/query-e88a5dae732c3f8180664f306b4bb1d21f97a2f1391860eb8714a52ef4439d81.json b/.sqlx/query-d46bb69f4e2afbae01ab08beb48a67b2cade64d9ebd47f3fc6143b057671cc1b.json similarity index 64% rename from .sqlx/query-e88a5dae732c3f8180664f306b4bb1d21f97a2f1391860eb8714a52ef4439d81.json rename to .sqlx/query-d46bb69f4e2afbae01ab08beb48a67b2cade64d9ebd47f3fc6143b057671cc1b.json index 64c9c18..510d21f 100644 --- a/.sqlx/query-e88a5dae732c3f8180664f306b4bb1d21f97a2f1391860eb8714a52ef4439d81.json +++ b/.sqlx/query-d46bb69f4e2afbae01ab08beb48a67b2cade64d9ebd47f3fc6143b057671cc1b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_inventory_product_customizations_query\n SET\n version = $1,\n name = $2,\n customization_id = $3,\n product_id = $4,\n deleted = $5;", + "query": "UPDATE\n cqrs_inventory_product_customizations_query\n SET\n version = $1,\n name = $2,\n product_id = $3,\n deleted = $4;", "describe": { "columns": [], "parameters": { @@ -8,11 +8,10 @@ "Int8", "Text", "Uuid", - "Uuid", "Bool" ] }, "nullable": [] }, - "hash": "e88a5dae732c3f8180664f306b4bb1d21f97a2f1391860eb8714a52ef4439d81" + "hash": "d46bb69f4e2afbae01ab08beb48a67b2cade64d9ebd47f3fc6143b057671cc1b" } diff --git a/src/inventory/adapters/output/db/postgres/category_view.rs b/src/inventory/adapters/output/db/postgres/category_view.rs index 34beb61..ca05a38 100644 --- a/src/inventory/adapters/output/db/postgres/category_view.rs +++ b/src/inventory/adapters/output/db/postgres/category_view.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use super::errors::*; use super::InventoryDBPostgresAdapter; -use crate::inventory::domain::category_aggregate::Category; +use crate::inventory::domain::category_aggregate::{Category, CategoryBuilder}; use crate::inventory::domain::events::InventoryEvent; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -27,6 +27,19 @@ pub struct CategoryView { deleted: bool, } +impl From for Category { + fn from(v: CategoryView) -> Self { + CategoryBuilder::default() + .name(v.name) + .description(v.description) + .category_id(v.category_id) + .store_id(v.store_id) + .deleted(v.deleted) + .build() + .unwrap() + } +} + // This updates the view with events as they are committed. // The logic should be minimal here, e.g., don't calculate the account balance, // design the events to carry the balance information instead. @@ -164,13 +177,11 @@ impl ViewRepository for InventoryDBPostgresAdapter { version = $1, name = $2, description = $3, - category_id = $4, - store_id = $5, - deleted = $6;", + store_id = $4, + deleted = $5;", version, view.name, view.description, - view.category_id, view.store_id, view.deleted ) @@ -217,3 +228,143 @@ impl Query for InventoryDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + db::migrate::*, + inventory::{ + application::services::{ + add_category_service::*, update_category_service::*, MockInventoryServicesInterface, + }, + domain::{ + add_category_command::*, category_aggregate::*, commands::*, events::*, + store_aggregate::*, update_category_command::*, + }, + }, + tests::bdd::*, + utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_inventory_category_view() { + let settings = crate::settings::tests::get_settings().await; + //let settings = crate::settings::Settings::new().unwrap(); + settings.create_db().await; + + let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; + db.migrate().await; + let db = InventoryDBPostgresAdapter::new(db.pool.clone()); + + let store = Store::default(); + crate::inventory::adapters::output::db::postgres::store_id_exists::tests::create_dummy_store_record(&store, &db).await; + + let queries: Vec>> = vec![Box::new(db.clone())]; + + let mut mock_services = MockInventoryServicesInterface::new(); + + let db2 = Arc::new(db.clone()); + mock_services + .expect_add_category() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddCategoryServiceBuilder::default() + .db_store_id_exists(db2.clone()) + .db_category_name_exists_for_store(db2.clone()) + .db_category_id_exists(db2.clone()) + .build() + .unwrap(), + ) + }); + + let db2 = Arc::new(db.clone()); + mock_services + .expect_update_category() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateCategoryServiceBuilder::default() + .db_store_id_exists(db2.clone()) + .db_category_name_exists_for_store(db2.clone()) + .db_category_id_exists(db2.clone()) + .build() + .unwrap(), + ) + }); + + let (cqrs, category_query): ( + Arc>, + Arc>, + ) = ( + Arc::new(postgres_es::postgres_cqrs( + db.pool.clone(), + queries, + Arc::new(mock_services), + )), + Arc::new(db.clone()), + ); + + let rand = crate::utils::random_string::GenerateRandomString {}; + + let cmd = AddCategoryCommandBuilder::default() + .name(rand.get_random(10)) + .description(None) + .store_id(*store.store_id()) + .adding_by(UUID) + .category_id(UUID) + .build() + .unwrap(); + + cqrs.execute( + &cmd.category_id().to_string(), + InventoryCommand::AddCategory(cmd.clone()), + ) + .await + .unwrap(); + + let category = category_query + .load(&(*cmd.category_id()).to_string()) + .await + .unwrap() + .unwrap(); + let category: Category = category.into(); + assert_eq!(category.name(), cmd.name()); + assert_eq!(category.description(), cmd.description()); + assert_eq!(category.category_id(), cmd.category_id()); + assert_eq!(category.store_id(), cmd.store_id()); + assert!(!store.deleted()); + + let update_category_cmd = UpdateCategoryCommand::new( + rand.get_random(10), + Some(rand.get_random(10)), + category, + UUID, + ) + .unwrap(); + cqrs.execute( + &cmd.category_id().to_string(), + InventoryCommand::UpdateCategory(update_category_cmd.clone()), + ) + .await + .unwrap(); + let category = category_query + .load(&(*cmd.category_id()).to_string()) + .await + .unwrap() + .unwrap(); + let category: Category = category.into(); + assert_eq!(category.name(), update_category_cmd.name()); + assert_eq!(category.description(), update_category_cmd.description()); + assert_eq!(category.category_id(), cmd.category_id()); + assert_eq!(category.store_id(), cmd.store_id()); + assert!(!category.deleted()); + + settings.drop_db().await; + } +} diff --git a/src/inventory/adapters/output/db/postgres/store_id_exists.rs b/src/inventory/adapters/output/db/postgres/store_id_exists.rs index c333ceb..4702e8a 100644 --- a/src/inventory/adapters/output/db/postgres/store_id_exists.rs +++ b/src/inventory/adapters/output/db/postgres/store_id_exists.rs @@ -45,7 +45,7 @@ pub mod tests { VALUES ($1, $2, $3, $4, $5 ,$6);", 1, s.name(), - s.address().as_ref().unwrap(), + s.address().as_ref().map(|s| s.as_str()), s.store_id(), s.owner(), false From 88446b94c4589943422c3cba9230d758e1f51259 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sat, 21 Sep 2024 16:21:19 +0530 Subject: [PATCH 8/9] feat: inventory: product ID is provided by caller --- .../services/add_product_service.rs | 28 ++---- src/inventory/application/services/errors.rs | 6 +- src/inventory/domain/add_product_command.rs | 88 +++++++++---------- .../domain/update_product_command.rs | 59 +++++++++++-- 4 files changed, 104 insertions(+), 77 deletions(-) diff --git a/src/inventory/application/services/add_product_service.rs b/src/inventory/application/services/add_product_service.rs index e446886..c3f1501 100644 --- a/src/inventory/application/services/add_product_service.rs +++ b/src/inventory/application/services/add_product_service.rs @@ -23,7 +23,6 @@ use crate::inventory::{ product_aggregate::*, }, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -40,7 +39,6 @@ pub struct AddProductService { db_product_id_exists: ProductIDExistsDBPortObj, db_get_category: GetCategoryDBPortObj, fts_add_product: AddProductToStoreFTSPortObj, - get_uuid: GetUUIDInterfaceObj, } #[async_trait::async_trait] @@ -54,19 +52,12 @@ impl AddProductUseCase for AddProductService { return Err(InventoryError::CategoryIDNotFound); } - 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; - } + if self + .db_product_id_exists + .product_id_exists(cmd.product_id()) + .await? + { + return Err(InventoryError::DuplicateProductID); } let product = ProductBuilder::default() @@ -77,7 +68,7 @@ impl AddProductUseCase for AddProductService { .price(cmd.price().clone()) .category_id(*cmd.category_id()) .quantity(cmd.quantity().clone()) - .product_id(product_id) + .product_id(*cmd.product_id()) .build() .unwrap(); @@ -119,8 +110,8 @@ pub mod tests { use super::*; use crate::inventory::domain::add_product_command::tests::get_command; + use crate::tests::bdd::*; use crate::utils::uuid::tests::UUID; - use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; pub fn mock_add_product_service( times: Option, @@ -165,7 +156,6 @@ pub mod tests { .db_get_category(mock_get_category_db_port(IS_CALLED_ONLY_ONCE)) .db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) .fts_add_product(mock_add_product_to_store_fts_port(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); @@ -190,7 +180,6 @@ pub mod tests { mock_product_name_exists_for_category_db_port_true(IS_CALLED_ONLY_ONCE), ) .db_category_id_exists(mock_category_id_exists_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)) .db_get_category(mock_get_category_db_port(IS_NEVER_CALLED)) .fts_add_product(mock_add_product_to_store_fts_port(IS_NEVER_CALLED)) @@ -215,7 +204,6 @@ pub mod tests { .db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) .db_get_category(mock_get_category_db_port(IS_NEVER_CALLED)) .fts_add_product(mock_add_product_to_store_fts_port(IS_NEVER_CALLED)) - .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) .build() .unwrap(); diff --git a/src/inventory/application/services/errors.rs b/src/inventory/application/services/errors.rs index 5373fed..5c1782c 100644 --- a/src/inventory/application/services/errors.rs +++ b/src/inventory/application/services/errors.rs @@ -21,6 +21,7 @@ pub enum InventoryError { DuplicateCustomizationID, DuplicateStoreID, DuplicateCategoryID, + DuplicateProductID, ProductIDNotFound, CategoryIDNotFound, CustomizationIDNotFound, @@ -36,10 +37,7 @@ impl From for InventoryError { InventoryDBError::DuplicateProductName => Self::DuplicateProductName, InventoryDBError::DuplicateCustomizationName => Self::DuplicateCustomizationName, InventoryDBError::DuplicateStoreID => Self::DuplicateStoreID, - InventoryDBError::DuplicateProductID => { - error!("DuplicateProductID"); - Self::InternalError - } + InventoryDBError::DuplicateProductID => Self::DuplicateProductID, InventoryDBError::DuplicateCategoryID => Self::DuplicateCategoryID, InventoryDBError::DuplicateCustomizationID => Self::DuplicateCustomizationID, InventoryDBError::InternalError => Self::InternalError, diff --git a/src/inventory/domain/add_product_command.rs b/src/inventory/domain/add_product_command.rs index f347838..5484ebd 100644 --- a/src/inventory/domain/add_product_command.rs +++ b/src/inventory/domain/add_product_command.rs @@ -17,24 +17,17 @@ pub enum AddProductCommandError { } #[derive( - Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, + Clone, Builder, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, )] -pub struct UnvalidatedAddProductCommand { - name: String, - description: Option, - image: Option, - category_id: Uuid, - sku_able: bool, - quantity: Quantity, - price: Price, - adding_by: Uuid, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +#[builder(build_fn(validate = "Self::validate"))] pub struct AddProductCommand { + #[builder(setter(custom))] name: String, + #[builder(setter(custom))] description: Option, + #[builder(setter(custom))] image: Option, + product_id: Uuid, category_id: Uuid, sku_able: bool, price: Price, @@ -42,9 +35,9 @@ pub struct AddProductCommand { adding_by: Uuid, } -impl UnvalidatedAddProductCommand { - pub fn validate(self) -> Result { - let description: Option = if let Some(description) = self.description { +impl AddProductCommandBuilder { + pub fn description(&mut self, description: Option) -> &mut Self { + let description: Option = if let Some(description) = description { let description = description.trim(); if description.is_empty() { None @@ -54,8 +47,12 @@ impl UnvalidatedAddProductCommand { } else { None }; + self.description = Some(description); + self + } - let image: Option = if let Some(image) = self.image { + pub fn image(&mut self, image: Option) -> &mut Self { + let image: Option = if let Some(image) = image { let image = image.trim(); if image.is_empty() { None @@ -65,22 +62,22 @@ impl UnvalidatedAddProductCommand { } else { None }; + self.image = Some(image); + self + } - let name = self.name.trim().to_owned(); - if name.is_empty() { - return Err(AddProductCommandError::NameIsEmpty); + pub fn name(&mut self, name: String) -> &mut Self { + let name = name.trim().to_owned(); + self.name = Some(name); + self + } + + pub fn validate(&self) -> Result<(), String> { + if self.name.as_ref().unwrap().is_empty() { + return Err(AddProductCommandError::NameIsEmpty.to_string()); } - Ok(AddProductCommand { - name, - description, - image, - category_id: self.category_id, - sku_able: self.sku_able, - price: self.price, - quantity: self.quantity, - adding_by: self.adding_by, - }) + Ok(()) } } @@ -93,7 +90,7 @@ pub mod tests { pub fn get_command() -> AddProductCommand { let name = "foo"; let adding_by = UUID; - let category_id = Uuid::new_v4(); + let category_id = UUID; let sku_able = false; let image = Some("image".to_string()); let description = Some("description".to_string()); @@ -123,7 +120,7 @@ pub mod tests { .build() .unwrap(); - let cmd = UnvalidatedAddProductCommandBuilder::default() + AddProductCommandBuilder::default() .name(name.into()) .description(description.clone()) .image(image.clone()) @@ -131,11 +128,10 @@ pub mod tests { .adding_by(adding_by) .quantity(quantity) .sku_able(sku_able) + .product_id(UUID) .price(price.clone()) .build() - .unwrap(); - - cmd.validate().unwrap() + .unwrap() } #[test] @@ -155,7 +151,7 @@ pub mod tests { let quantity = Quantity::default(); // description = None - let cmd = UnvalidatedAddProductCommandBuilder::default() + let cmd = AddProductCommandBuilder::default() .name(name.into()) .description(None) .image(None) @@ -163,12 +159,11 @@ pub mod tests { .adding_by(adding_by) .quantity(quantity.clone()) .sku_able(sku_able) + .product_id(UUID) .price(price.clone()) .build() .unwrap(); - let cmd = cmd.validate().unwrap(); - assert_eq!(cmd.name(), name); assert_eq!(cmd.description(), &None); assert_eq!(cmd.adding_by(), &adding_by); @@ -196,21 +191,19 @@ pub mod tests { let quantity = Quantity::default(); - let cmd = UnvalidatedAddProductCommandBuilder::default() + let cmd = AddProductCommandBuilder::default() .name(name.into()) .description(description.clone()) .image(image.clone()) .category_id(category_id) - .quantity(quantity.clone()) .adding_by(adding_by) + .quantity(quantity.clone()) .sku_able(sku_able) + .product_id(UUID) .price(price.clone()) - // .customizations(customizations.clone()) .build() .unwrap(); - let cmd = cmd.validate().unwrap(); - assert_eq!(cmd.name(), name); assert_eq!(cmd.description(), &description); assert_eq!(cmd.adding_by(), &adding_by); @@ -238,7 +231,8 @@ pub mod tests { let quantity = Quantity::default(); - let cmd = UnvalidatedAddProductCommandBuilder::default() + // AddProductCommandError::NameIsEmpty + assert!(AddProductCommandBuilder::default() .name("".into()) .description(description.clone()) .image(image.clone()) @@ -246,11 +240,9 @@ pub mod tests { .adding_by(adding_by) .quantity(quantity) .sku_able(sku_able) + .product_id(UUID) .price(price.clone()) .build() - .unwrap(); - - // AddProductCommandError::NameIsEmpty - assert_eq!(cmd.validate(), Err(AddProductCommandError::NameIsEmpty)) + .is_err()); } } diff --git a/src/inventory/domain/update_product_command.rs b/src/inventory/domain/update_product_command.rs index f158f07..b99a897 100644 --- a/src/inventory/domain/update_product_command.rs +++ b/src/inventory/domain/update_product_command.rs @@ -95,13 +95,62 @@ pub mod tests { use crate::types::quantity::*; use crate::utils::uuid::tests::UUID; - pub fn get_command() -> UpdateProductCommand { - let name = "foo"; + pub fn get_command_with_product(product: Product) -> UpdateProductCommand { + let name = "foobaaar"; let adding_by = UUID; - let category_id = Uuid::new_v4(); + let category_id = UUID; let sku_able = false; - let image = Some("image".to_string()); - let description = Some("description".to_string()); + let image = Some("imageeee".to_string()); + let description = Some("descriptionnnn".to_string()); + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = QuantityBuilder::default() + .minor( + QuantityPartBuilder::default() + .number(0) + .unit(QuantityUnit::DiscreteNumber) + .build() + .unwrap(), + ) + .major( + QuantityPartBuilder::default() + .number(1) + .unit(QuantityUnit::DiscreteNumber) + .build() + .unwrap(), + ) + .build() + .unwrap(); + + let cmd = UnvalidatedUpdateProductCommandBuilder::default() + .name(name.into()) + .description(description.clone()) + .image(image.clone()) + .category_id(category_id.clone()) + .adding_by(adding_by.clone()) + .quantity(quantity) + .sku_able(sku_able) + .price(price.clone()) + .old_product(product) + .build() + .unwrap(); + + cmd.validate().unwrap() + } + + pub fn get_command() -> UpdateProductCommand { + let name = "foobaaar"; + let adding_by = UUID; + let category_id = UUID; + let sku_able = false; + let image = Some("imageeee".to_string()); + let description = Some("descriptionnnn".to_string()); let price = PriceBuilder::default() .minor(0) From ca7defe7248526e93763046b1de6890de58f3ed3 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sat, 21 Sep 2024 16:21:28 +0530 Subject: [PATCH 9/9] feat: inventory: product view tests --- ...767f7209a2443fd944420baf1e1cf5c51ccb.json} | 5 +- .../category_name_exists_for_store.rs | 2 +- .../output/db/postgres/product_view.rs | 176 ++++++++++++++++-- 3 files changed, 167 insertions(+), 16 deletions(-) rename .sqlx/{query-e2f9f291a20aac77851774ba8cd37325143a4d98e0980632f097c5885cc71094.json => query-c358d3b79d35668b3475f29f5bf6767f7209a2443fd944420baf1e1cf5c51ccb.json} (50%) diff --git a/.sqlx/query-e2f9f291a20aac77851774ba8cd37325143a4d98e0980632f097c5885cc71094.json b/.sqlx/query-c358d3b79d35668b3475f29f5bf6767f7209a2443fd944420baf1e1cf5c51ccb.json similarity index 50% rename from .sqlx/query-e2f9f291a20aac77851774ba8cd37325143a4d98e0980632f097c5885cc71094.json rename to .sqlx/query-c358d3b79d35668b3475f29f5bf6767f7209a2443fd944420baf1e1cf5c51ccb.json index d8268b4..779fd97 100644 --- a/.sqlx/query-e2f9f291a20aac77851774ba8cd37325143a4d98e0980632f097c5885cc71094.json +++ b/.sqlx/query-c358d3b79d35668b3475f29f5bf6767f7209a2443fd944420baf1e1cf5c51ccb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_inventory_product_query\n SET\n version = $1,\n name = $2,\n description = $3,\n image = $4,\n product_id = $5,\n category_id = $6,\n price_major = $7,\n price_minor = $8,\n price_currency = $9,\n sku_able = $10,\n quantity_minor_unit = $11,\n quantity_minor_number = $12,\n quantity_major_unit = $13,\n quantity_major_number = $14,\n deleted = $15;", + "query": "UPDATE\n cqrs_inventory_product_query\n SET\n version = $1,\n name = $2,\n description = $3,\n image = $4,\n category_id = $5,\n price_major = $6,\n price_minor = $7,\n price_currency = $8,\n sku_able = $9,\n quantity_minor_unit = $10,\n quantity_minor_number = $11,\n quantity_major_unit = $12,\n quantity_major_number = $13,\n deleted = $14;", "describe": { "columns": [], "parameters": { @@ -10,7 +10,6 @@ "Text", "Text", "Uuid", - "Uuid", "Int4", "Int4", "Text", @@ -24,5 +23,5 @@ }, "nullable": [] }, - "hash": "e2f9f291a20aac77851774ba8cd37325143a4d98e0980632f097c5885cc71094" + "hash": "c358d3b79d35668b3475f29f5bf6767f7209a2443fd944420baf1e1cf5c51ccb" } diff --git a/src/inventory/adapters/output/db/postgres/category_name_exists_for_store.rs b/src/inventory/adapters/output/db/postgres/category_name_exists_for_store.rs index cc30580..bd032d8 100644 --- a/src/inventory/adapters/output/db/postgres/category_name_exists_for_store.rs +++ b/src/inventory/adapters/output/db/postgres/category_name_exists_for_store.rs @@ -49,7 +49,7 @@ pub mod tests { VALUES ($1, $2, $3, $4, $5, $6);", 1, c.name(), - c.description().as_ref().unwrap(), + c.description().as_ref().map(|s| s.as_str()), c.category_id(), c.store_id(), c.deleted().clone(), diff --git a/src/inventory/adapters/output/db/postgres/product_view.rs b/src/inventory/adapters/output/db/postgres/product_view.rs index 066df00..d3e3836 100644 --- a/src/inventory/adapters/output/db/postgres/product_view.rs +++ b/src/inventory/adapters/output/db/postgres/product_view.rs @@ -293,22 +293,20 @@ impl ViewRepository for InventoryDBPostgresAdapter { name = $2, description = $3, image = $4, - product_id = $5, - category_id = $6, - price_major = $7, - price_minor = $8, - price_currency = $9, - sku_able = $10, - quantity_minor_unit = $11, - quantity_minor_number = $12, - quantity_major_unit = $13, - quantity_major_number = $14, - deleted = $15;", + category_id = $5, + price_major = $6, + price_minor = $7, + price_currency = $8, + sku_able = $9, + quantity_minor_unit = $10, + quantity_minor_number = $11, + quantity_major_unit = $12, + quantity_major_number = $13, + deleted = $14;", version, view.name, view.description, view.image, - view.product_id, view.category_id, view.price_major, view.price_minor, @@ -349,3 +347,157 @@ impl Query for InventoryDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + db::migrate::*, + inventory::{ + application::{ + port::output::full_text_search::add_product_to_store::*, + services::{ + add_product_service::*, update_product_service::*, + MockInventoryServicesInterface, + }, + }, + domain::{ + add_product_command::{tests::get_command, *}, + category_aggregate::{Category, CategoryBuilder}, + commands::*, + events::*, + product_aggregate::*, + store_aggregate::*, + update_product_command::{tests::get_command_with_product, *}, + }, + }, + tests::bdd::*, + utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_inventory_product_view() { + let settings = crate::settings::tests::get_settings().await; + //let settings = crate::settings::Settings::new().unwrap(); + settings.create_db().await; + + let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; + db.migrate().await; + let db = InventoryDBPostgresAdapter::new(db.pool.clone()); + + let store = Store::default(); + crate::inventory::adapters::output::db::postgres::store_id_exists::tests::create_dummy_store_record(&store, &db).await; + let category = CategoryBuilder::default() + .name("fooooo_cat".into()) + .description(None) + .store_id(*store.store_id()) + .category_id(UUID) + .build() + .unwrap(); + crate::inventory::adapters::output::db::postgres::category_name_exists_for_store::tests::create_dummy_category_record(&category, &db).await; + + let queries: Vec>> = vec![Box::new(db.clone())]; + + let mut mock_services = MockInventoryServicesInterface::new(); + + let db2 = Arc::new(db.clone()); + mock_services + .expect_add_product() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddProductServiceBuilder::default() + .db_product_name_exists_for_category(db2.clone()) + .db_product_id_exists(db2.clone()) + .db_get_category(db2.clone()) + .db_category_id_exists(db2.clone()) + .fts_add_product(mock_add_product_to_store_fts_port(IGNORE_CALL_COUNT)) + .build() + .unwrap(), + ) + }); + + let db2 = Arc::new(db.clone()); + mock_services + .expect_update_product() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateProductServiceBuilder::default() + .db_category_id_exists(db2.clone()) + .db_product_name_exists_for_category(db2.clone()) + .db_product_id_exists(db2.clone()) + .build() + .unwrap(), + ) + }); + + let (cqrs, product_query): ( + Arc>, + Arc>, + ) = ( + Arc::new(postgres_es::postgres_cqrs( + db.pool.clone(), + queries, + Arc::new(mock_services), + )), + Arc::new(db.clone()), + ); + + let rand = crate::utils::random_string::GenerateRandomString {}; + + let cmd = get_command(); + cqrs.execute( + &cmd.product_id().to_string(), + InventoryCommand::AddProduct(cmd.clone()), + ) + .await + .unwrap(); + + let product = product_query + .load(&(*cmd.product_id()).to_string()) + .await + .unwrap() + .unwrap(); + let product: Product = product.into(); + assert_eq!(product.name(), cmd.name()); + assert_eq!(product.description(), cmd.description()); + assert_eq!(product.image(), cmd.image()); + assert_eq!(product.product_id(), cmd.product_id()); + assert_eq!(product.quantity(), cmd.quantity()); + assert_eq!(product.sku_able(), cmd.sku_able()); + assert_eq!(product.price(), cmd.price()); + assert!(!store.deleted()); + + let update_product_cmd = get_command_with_product(product.clone()); + cqrs.execute( + &cmd.product_id().to_string(), + InventoryCommand::UpdateProduct(update_product_cmd.clone()), + ) + .await + .unwrap(); + let product = product_query + .load(&(*cmd.product_id()).to_string()) + .await + .unwrap() + .unwrap(); + let product: Product = product.into(); + assert_eq!(product.name(), update_product_cmd.name()); + assert_eq!(product.description(), update_product_cmd.description()); + assert_eq!(product.image(), update_product_cmd.image()); + assert_eq!( + product.product_id(), + update_product_cmd.old_product().product_id() + ); + assert_eq!(product.quantity(), update_product_cmd.quantity()); + assert_eq!(product.sku_able(), update_product_cmd.sku_able()); + assert_eq!(product.price(), update_product_cmd.price()); + assert!(!store.deleted()); + + settings.drop_db().await; + } +}