From d265412d06a8f731d47551021bacf96bc3448f47 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 24 Sep 2024 12:20:06 +0530 Subject: [PATCH 1/8] feat: ordering: store ID is provided by caller & store view tests --- ...bab76f5962f37e58c7dee4e5f9341dca8c0e.json} | 5 +- src/billing/domain/add_store_command.rs | 83 ------ src/ordering/adapters/output/db/store_view.rs | 260 ++++++++++-------- .../application/services/add_store_service.rs | 42 +-- src/ordering/application/services/errors.rs | 45 +-- src/ordering/domain/add_store_command.rs | 79 ++++-- src/ordering/domain/store_aggregate.rs | 8 +- 7 files changed, 249 insertions(+), 273 deletions(-) rename .sqlx/{query-d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613.json => query-8a7958c4f8419e1fd95b2d0c75a3bab76f5962f37e58c7dee4e5f9341dca8c0e.json} (64%) diff --git a/.sqlx/query-d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613.json b/.sqlx/query-8a7958c4f8419e1fd95b2d0c75a3bab76f5962f37e58c7dee4e5f9341dca8c0e.json similarity index 64% rename from .sqlx/query-d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613.json rename to .sqlx/query-8a7958c4f8419e1fd95b2d0c75a3bab76f5962f37e58c7dee4e5f9341dca8c0e.json index 7d2cb4d..d4e65bd 100644 --- a/.sqlx/query-d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613.json +++ b/.sqlx/query-8a7958c4f8419e1fd95b2d0c75a3bab76f5962f37e58c7dee4e5f9341dca8c0e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_ordering_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_ordering_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": "d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613" + "hash": "8a7958c4f8419e1fd95b2d0c75a3bab76f5962f37e58c7dee4e5f9341dca8c0e" } diff --git a/src/billing/domain/add_store_command.rs b/src/billing/domain/add_store_command.rs index 44c745c..68cb21e 100644 --- a/src/billing/domain/add_store_command.rs +++ b/src/billing/domain/add_store_command.rs @@ -1,86 +1,3 @@ -//// 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; -// -//#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -//pub enum AddStoreCommandError { -// NameIsEmpty, -//} -// -//#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] -//pub struct AddStoreCommand { -// name: String, -// address: Option, -// owner: Uuid, -//} -// -//impl AddStoreCommand { -// pub fn new( -// name: String, -// address: Option, -// owner: 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(AddStoreCommandError::NameIsEmpty); -// } -// -// Ok(Self { -// name, -// address, -// owner, -// }) -// } -//} -// -//#[cfg(test)] -//mod tests { -// use crate::utils::uuid::tests::UUID; -// -// use super::*; -// -// #[test] -// fn test_cmd() { -// let name = "foo"; -// let address = "bar"; -// let owner = UUID; -// -// // address = None -// let cmd = AddStoreCommand::new(name.into(), None, owner).unwrap(); -// assert_eq!(cmd.name(), name); -// assert_eq!(cmd.address(), &None); -// assert_eq!(cmd.owner(), &owner); -// -// // address = Some -// let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap(); -// assert_eq!(cmd.name(), name); -// assert_eq!(cmd.address(), &Some(address.to_owned())); -// assert_eq!(cmd.owner(), &owner); -// -// // AddStoreCommandError::NameIsEmpty -// assert_eq!( -// AddStoreCommand::new("".into(), Some(address.into()), owner), -// Err(AddStoreCommandError::NameIsEmpty) -// ) -// } -//} - // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/src/ordering/adapters/output/db/store_view.rs b/src/ordering/adapters/output/db/store_view.rs index ec62fc1..8680b1b 100644 --- a/src/ordering/adapters/output/db/store_view.rs +++ b/src/ordering/adapters/output/db/store_view.rs @@ -11,7 +11,7 @@ use uuid::Uuid; use super::errors::*; use super::OrderingDBPostgresAdapter; use crate::ordering::domain::events::OrderingEvent; -use crate::ordering::domain::store_aggregate::Store; +use crate::ordering::domain::store_aggregate::*; use crate::utils::parse_aggregate_id::parse_aggregate_id; pub const NEW_STORE_NON_UUID: &str = "ordering_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 OrderingDBPostgresAdapter { 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, ) @@ -205,108 +216,139 @@ impl Query for OrderingDBPostgresAdapter { } } -// 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 { -// use super::*; -// -// use postgres_es::PostgresCqrs; -// -// use crate::{ -// db::migrate::*, -// ordering::{ -// application::services::{ -// add_category_service::tests::mock_add_category_service, add_customization_service::tests::mock_add_customization_service, add_line_item_service::tests::mock_add_line_item_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, OrderingServicesBuilder -// }, -// domain::{ -// add_category_command::AddCategoryCommand, add_customization_command, -// add_product_command::tests::get_command, add_store_command::AddStoreCommand, -// commands::OrderingCommand, -// 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, -// }, -// }, -// tests::bdd::IS_NEVER_CALLED, -// utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, -// }; -// use std::sync::Arc; -// -// #[actix_rt::test] -// async fn pg_query() { -// 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 = OrderingDBPostgresAdapter::new(db.pool.clone()); -// -// let simple_query = SimpleLoggingQuery {}; -// -// let queries: Vec>> = -// vec![Box::new(simple_query), Box::new(db.clone())]; -// -// let services = OrderingServicesBuilder::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 (cqrs, _store_query): ( -// Arc>, -// Arc>, -// ) = ( -// Arc::new(postgres_es::postgres_cqrs( -// db.pool.clone(), -// queries, -// Arc::new(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("", OrderingCommand::AddStore(cmd.clone())) -// .await -// .unwrap(); -// -// settings.drop_db().await; -// } -//} +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + ordering::{ + application::services::{ + add_store_service::AddStoreServiceBuilder, update_store_service::*, + MockOrderingServicesInterface, + }, + domain::add_store_command::*, + domain::commands::OrderingCommand, + domain::update_store_command::*, + }, + db::migrate::*, + tests::bdd::*, + utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_ordering_store_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 = OrderingDBPostgresAdapter::new(db.pool.clone()); + + let simple_query = SimpleLoggingQuery {}; + + let queries: Vec>> = + vec![Box::new(simple_query), Box::new(db.clone())]; + + let mut mock_services = MockOrderingServicesInterface::new(); + + 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(mock_services), + )), + Arc::new(db.clone()), + ); + + let rand = crate::utils::random_string::GenerateRandomString {}; + 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(), + OrderingCommand::AddStore(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(), 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(), + OrderingCommand::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; + } +} diff --git a/src/ordering/application/services/add_store_service.rs b/src/ordering/application/services/add_store_service.rs index 041ead3..aa88411 100644 --- a/src/ordering/application/services/add_store_service.rs +++ b/src/ordering/application/services/add_store_service.rs @@ -12,12 +12,11 @@ use super::errors::*; use crate::ordering::{ 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,21 @@ 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) -> OrderingResult { - 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(OrderingError::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 +56,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()) } @@ -87,7 +79,7 @@ pub mod tests { .name(cmd.name().into()) .address(cmd.address().as_ref().map(|s| s.to_string())) .owner(*cmd.owner()) - .store_id(UUID) + .store_id(*cmd.store_id()) .build() .unwrap(); @@ -108,13 +100,17 @@ pub mod tests { let address = "bar"; 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_false(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); @@ -122,7 +118,7 @@ pub mod tests { assert_eq!(res.name(), cmd.name()); assert_eq!(res.address(), cmd.address()); assert_eq!(res.owner(), cmd.owner()); - assert_eq!(res.store_id(), &UUID); + assert_eq!(res.store_id(), cmd.store_id()); } #[actix_rt::test] @@ -131,13 +127,17 @@ pub mod tests { let address = "bar"; 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/ordering/application/services/errors.rs b/src/ordering/application/services/errors.rs index b8e598f..1cacb54 100644 --- a/src/ordering/application/services/errors.rs +++ b/src/ordering/application/services/errors.rs @@ -26,52 +26,37 @@ pub enum OrderingError { ProductIDNotFound, DuplicateCustomizationName, CustomizationIDNotFound, + + DuplicateKotID, + DuplicateOrderID, + DuplicateStoreID, + DuplicateCustomizationID, + DuplicateProductID, + DuplicateCategoryID, + DuplicateLineItemID, } // impl From for OrderingError { fn from(value: OrderingDBError) -> Self { match value { - OrderingDBError::DuplicateLineItemID => { - error!("DuplicateLineItemID"); - Self::InternalError - } + OrderingDBError::DuplicateLineItemID => Self::DuplicateLineItemID, OrderingDBError::LineItemIDNotFound => OrderingError::LineItemIDNotFound, - OrderingDBError::DuplicateOrderID => { - error!("DuplicateOrderID"); - Self::InternalError - } + OrderingDBError::DuplicateOrderID => Self::DuplicateOrderID, OrderingDBError::DuplicateStoreName => Self::DuplicateStoreName, - OrderingDBError::DuplicateStoreID => { - error!("DuplicateStoreID"); - Self::InternalError - } + OrderingDBError::DuplicateStoreID => Self::DuplicateStoreID, OrderingDBError::StoreIDNotFound => OrderingError::StoreIDNotFound, - OrderingDBError::OrderIDNotFound => OrderingError::OrderIDNotFound, - - OrderingDBError::DuplicateKotID => { - error!("DuplicateKotID"); - Self::InternalError - } + OrderingDBError::DuplicateKotID => Self::DuplicateKotID, OrderingDBError::KotIDNotFound => OrderingError::KotIDNotFound, OrderingDBError::InternalError => Self::InternalError, OrderingDBError::DuplicateCategoryName => Self::DuplicateCategoryName, - OrderingDBError::DuplicateCategoryID => { - error!("DuplicateCategoryID"); - Self::InternalError - } + OrderingDBError::DuplicateCategoryID => Self::DuplicateCategoryID, OrderingDBError::CategoryIDNotFound => OrderingError::CategoryIDNotFound, OrderingDBError::DuplicateProductName => Self::DuplicateProductName, - OrderingDBError::DuplicateProductID => { - error!("DuplicateProductID"); - Self::InternalError - } + OrderingDBError::DuplicateProductID => Self::DuplicateProductID, OrderingDBError::ProductIDNotFound => OrderingError::ProductIDNotFound, OrderingDBError::DuplicateCustomizationName => Self::DuplicateCustomizationName, - OrderingDBError::DuplicateCustomizationID => { - error!("DuplicateCustomizationID"); - Self::InternalError - } + OrderingDBError::DuplicateCustomizationID => Self::DuplicateCustomizationID, OrderingDBError::CustomizationIDNotFound => OrderingError::CustomizationIDNotFound, } } diff --git a/src/ordering/domain/add_store_command.rs b/src/ordering/domain/add_store_command.rs index 3f11af8..b7d3567 100644 --- a/src/ordering/domain/add_store_command.rs +++ b/src/ordering/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,52 @@ 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 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 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 +69,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(); + 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(); + 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/ordering/domain/store_aggregate.rs b/src/ordering/domain/store_aggregate.rs index 9ac4c6e..e005aa1 100644 --- a/src/ordering/domain/store_aggregate.rs +++ b/src/ordering/domain/store_aggregate.rs @@ -113,7 +113,13 @@ mod tests { .unwrap(); let expected = OrderingEvent::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 = MockOrderingServicesInterface::new(); services From 5a050fde0e39e096fa8d2e71518ced5856902337 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 24 Sep 2024 12:31:39 +0530 Subject: [PATCH 2/8] feat: ordering: customization ID is provided by caller & customization view tests --- ...cfab30cc8b01fed9b73b9cc0813750230314.json} | 5 +- .../adapters/output/db/customization_view.rs | 154 +++++++++++++++++- .../adapters/output/db/product_id_exists.rs | 4 +- .../services/add_customization_service.rs | 41 ++--- .../domain/add_customization_command.rs | 56 +++---- 5 files changed, 187 insertions(+), 73 deletions(-) rename .sqlx/{query-d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e.json => query-a5a58d14ddbfa78cca3729392faecfab30cc8b01fed9b73b9cc0813750230314.json} (64%) diff --git a/.sqlx/query-d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e.json b/.sqlx/query-a5a58d14ddbfa78cca3729392faecfab30cc8b01fed9b73b9cc0813750230314.json similarity index 64% rename from .sqlx/query-d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e.json rename to .sqlx/query-a5a58d14ddbfa78cca3729392faecfab30cc8b01fed9b73b9cc0813750230314.json index 1cff257..6fe3c5f 100644 --- a/.sqlx/query-d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e.json +++ b/.sqlx/query-a5a58d14ddbfa78cca3729392faecfab30cc8b01fed9b73b9cc0813750230314.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_ordering_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_ordering_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": "d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e" + "hash": "a5a58d14ddbfa78cca3729392faecfab30cc8b01fed9b73b9cc0813750230314" } diff --git a/src/ordering/adapters/output/db/customization_view.rs b/src/ordering/adapters/output/db/customization_view.rs index d43a4d3..87a5c36 100644 --- a/src/ordering/adapters/output/db/customization_view.rs +++ b/src/ordering/adapters/output/db/customization_view.rs @@ -17,11 +17,6 @@ use crate::utils::parse_aggregate_id::parse_aggregate_id; pub const NEW_CUSTOMIZATION_NON_UUID: &str = "ordering_new_customization_non_uuid-asdfa"; -//#[derive(Debug, Default, Serialize, Deserialize)] -//struct Customizations { -// customizations: Vec, -//} - #[derive(Debug, Default, Serialize, Deserialize)] struct CustomizationView { name: String, @@ -187,12 +182,10 @@ impl ViewRepository for OrderingDBPostgresAdap 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 +218,146 @@ impl Query for OrderingDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + db::migrate::*, + ordering::{ + adapters::output::db::product_id_exists::tests::create_dummy_product_record, + application::services::{ + add_customization_service::*, update_customization_service::*, + MockOrderingServicesInterface, + }, + domain::{ + add_customization_command::*, + commands::OrderingCommand, + 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_ordering_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 = OrderingDBPostgresAdapter::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 = MockOrderingServicesInterface::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(), + OrderingCommand::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(), + OrderingCommand::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/ordering/adapters/output/db/product_id_exists.rs b/src/ordering/adapters/output/db/product_id_exists.rs index 7904a9a..40d1f6b 100644 --- a/src/ordering/adapters/output/db/product_id_exists.rs +++ b/src/ordering/adapters/output/db/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/ordering/application/services/add_customization_service.rs b/src/ordering/application/services/add_customization_service.rs index 7e5146c..0b52d68 100644 --- a/src/ordering/application/services/add_customization_service.rs +++ b/src/ordering/application/services/add_customization_service.rs @@ -11,20 +11,16 @@ use mockall::*; use super::errors::*; use crate::ordering::{ application::port::output::db::{ - customization_id_exists::{self, *}, + customization_id_exists::*, customization_name_exists_for_product::*, - product_id_exists::{self, *}, - product_name_exists_for_category::*, + product_id_exists::*, }, domain::{ - add_customization_command::AddCustomizationCommand, - customization_added_event::{self, *}, + add_customization_command::*, + customization_added_event::*, customization_aggregate::*, - product_added_event::{self, ProductAddedEvent, ProductAddedEventBuilder}, - product_aggregate::*, }, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -42,7 +38,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 +54,19 @@ impl AddCustomizationUseCase for AddCustomizationService { return Err(OrderingError::ProductIDNotFound); } - let mut customization_id = self.get_uuid.get_uuid(); - loop { if self .db_customization_id_exists - .customization_id_exists(&customization_id) + .customization_id_exists(cmd.customization_id()) .await? { - customization_id = self.get_uuid.get_uuid(); - continue; - } else { - break; + return Err(OrderingError::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(); @@ -100,12 +89,11 @@ impl AddCustomizationUseCase for AddCustomizationService { pub mod tests { use super::*; - use customization_added_event::tests::get_customization_added_event_from_cmd; - use uuid::Uuid; - - use crate::ordering::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::ordering::domain::{ + add_customization_command::tests::*, + customization_added_event::tests::*, + }; + use crate::tests::bdd::*; pub fn mock_add_customization_service( times: Option, @@ -138,13 +126,12 @@ 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!(customization_added_events.len(), cmd.customizations().len()); + assert_eq!(res.customization().customization_id(), cmd.customization_id()); } #[actix_rt::test] @@ -159,7 +146,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 +165,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/ordering/domain/add_customization_command.rs b/src/ordering/domain/add_customization_command.rs index c79a64e..a912070 100644 --- a/src/ordering/domain/add_customization_command.rs +++ b/src/ordering/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,12 @@ 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(),); } } + From df64f434bee19fcdfeefedeaa533e5ab00d3a600 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 24 Sep 2024 12:39:58 +0530 Subject: [PATCH 3/8] feat: ordering: slineitem ID is provided by caller & lineitemview tests --- ...866b288fec93971bf9ab41faec21680e50f71.json | 24 -- ...a94d89c30f8f5d633004ba16a6e971e041a14.json | 23 ++ .../adapters/output/db/customization_view.rs | 1 - .../adapters/output/db/kot_id_exists.rs | 2 +- .../adapters/output/db/line_item_view.rs | 229 +++++++++++++++++- src/ordering/adapters/output/db/store_view.rs | 3 +- .../services/add_customization_service.rs | 30 ++- .../services/add_line_item_service.rs | 20 +- .../application/services/add_store_service.rs | 11 +- src/ordering/application/services/errors.rs | 2 +- .../domain/add_customization_command.rs | 1 - src/ordering/domain/add_line_item_command.rs | 112 ++++----- 12 files changed, 318 insertions(+), 140 deletions(-) delete mode 100644 .sqlx/query-c348d55dd91acb0d4697c433f61866b288fec93971bf9ab41faec21680e50f71.json create mode 100644 .sqlx/query-e61db066fe7e66879f8ce8ccd3da94d89c30f8f5d633004ba16a6e971e041a14.json diff --git a/.sqlx/query-c348d55dd91acb0d4697c433f61866b288fec93971bf9ab41faec21680e50f71.json b/.sqlx/query-c348d55dd91acb0d4697c433f61866b288fec93971bf9ab41faec21680e50f71.json deleted file mode 100644 index f2ce603..0000000 --- a/.sqlx/query-c348d55dd91acb0d4697c433f61866b288fec93971bf9ab41faec21680e50f71.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_ordering_line_item_query\n SET\n version = $1,\n product_name = $2,\n product_id = $3,\n line_item_id = $4,\n quantity_minor_unit = $5,\n quantity_minor_number = $6,\n quantity_major_unit = $7,\n quantity_major_number = $8,\n created_time = $9,\n kot_id = $10,\n deleted = $11;", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text", - "Uuid", - "Uuid", - "Text", - "Int4", - "Text", - "Int4", - "Timestamptz", - "Uuid", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "c348d55dd91acb0d4697c433f61866b288fec93971bf9ab41faec21680e50f71" -} diff --git a/.sqlx/query-e61db066fe7e66879f8ce8ccd3da94d89c30f8f5d633004ba16a6e971e041a14.json b/.sqlx/query-e61db066fe7e66879f8ce8ccd3da94d89c30f8f5d633004ba16a6e971e041a14.json new file mode 100644 index 0000000..e48e3e8 --- /dev/null +++ b/.sqlx/query-e61db066fe7e66879f8ce8ccd3da94d89c30f8f5d633004ba16a6e971e041a14.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE\n cqrs_ordering_line_item_query\n SET\n version = $1,\n product_name = $2,\n product_id = $3,\n quantity_minor_unit = $4,\n quantity_minor_number = $5,\n quantity_major_unit = $6,\n quantity_major_number = $7,\n created_time = $8,\n kot_id = $9,\n deleted = $10;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Uuid", + "Text", + "Int4", + "Text", + "Int4", + "Timestamptz", + "Uuid", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "e61db066fe7e66879f8ce8ccd3da94d89c30f8f5d633004ba16a6e971e041a14" +} diff --git a/src/ordering/adapters/output/db/customization_view.rs b/src/ordering/adapters/output/db/customization_view.rs index 87a5c36..c31a26b 100644 --- a/src/ordering/adapters/output/db/customization_view.rs +++ b/src/ordering/adapters/output/db/customization_view.rs @@ -219,7 +219,6 @@ impl Query for OrderingDBPostgresAdapter { } } - #[cfg(test)] mod tests { use super::*; diff --git a/src/ordering/adapters/output/db/kot_id_exists.rs b/src/ordering/adapters/output/db/kot_id_exists.rs index 5d3486f..48d2879 100644 --- a/src/ordering/adapters/output/db/kot_id_exists.rs +++ b/src/ordering/adapters/output/db/kot_id_exists.rs @@ -36,7 +36,7 @@ pub mod tests { // use crate::ordering::domain::add_product_command::tests::get_customizations; use crate::ordering::domain::kot_aggregate::*; - async fn create_dummy_kot(kot: &Kot, db: &OrderingDBPostgresAdapter) { + pub async fn create_dummy_kot(kot: &Kot, db: &OrderingDBPostgresAdapter) { sqlx::query!( "INSERT INTO cqrs_ordering_kot_query ( version, diff --git a/src/ordering/adapters/output/db/line_item_view.rs b/src/ordering/adapters/output/db/line_item_view.rs index 341cc7f..5fe5c2a 100644 --- a/src/ordering/adapters/output/db/line_item_view.rs +++ b/src/ordering/adapters/output/db/line_item_view.rs @@ -271,18 +271,16 @@ impl ViewRepository for OrderingDBPostgresAdapter { version = $1, product_name = $2, product_id = $3, - line_item_id = $4, - quantity_minor_unit = $5, - quantity_minor_number = $6, - quantity_major_unit = $7, - quantity_major_number = $8, - created_time = $9, - kot_id = $10, - deleted = $11;", + quantity_minor_unit = $4, + quantity_minor_number = $5, + quantity_major_unit = $6, + quantity_major_number = $7, + created_time = $8, + kot_id = $9, + deleted = $10;", version, view.product_name, view.product_id, - view.line_item_id, view.quantity_minor_unit, view.quantity_minor_number, view.quantity_major_unit, @@ -320,3 +318,216 @@ impl Query for OrderingDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + ordering::{ + application::services::{ + add_line_item_service::AddLineItemServiceBuilder, delete_line_item_service::*, + update_line_item_service::*, MockOrderingServicesInterface, + }, + domain::{ + add_line_item_command::*, kot_aggregate::Kot, commands::OrderingCommand, + delete_line_item_command::DeleteLineItemCommandBuilder, + update_line_item_command::*, + }, + }, + db::migrate::*, + tests::bdd::*, + types::quantity::*, + utils::{ + random_string::GenerateRandomStringInterface, + uuid::{tests::UUID, *}, + }, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_ordering_line_item_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 = OrderingDBPostgresAdapter::new(db.pool.clone()); + + let queries: Vec>> = vec![Box::new(db.clone())]; + + let mut mock_services = MockOrderingServicesInterface::new(); + + let kot = Kot::default(); + crate::ordering::adapters::output::db::kot_id_exists::tests::create_dummy_kot( + &kot, &db, + ) + .await; + + let db2 = db.clone(); + mock_services + .expect_add_line_item() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddLineItemServiceBuilder::default() + .db_line_item_id_exists(Arc::new(db2.clone())) + .db_kot_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_update_line_item() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateLineItemServiceBuilder::default() + .db_line_item_id_exists(Arc::new(db2.clone())) + .db_kot_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_delete_line_item() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + DeleteLineItemServiceBuilder::default() + .db_line_item_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let (cqrs, line_item_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 uuid = GenerateUUID {}; + let line_item_id = uuid.get_uuid(); + + let cmd = AddLineItemCommandBuilder::default() + .product_name(rand.get_random(10)) + .adding_by(UUID) + + .quantity(Quantity::get_quantity()) + .product_id(UUID) + .kot_id(*kot.kot_id()) + .line_item_id(line_item_id) + .build() + .unwrap(); + + cqrs.execute( + &cmd.line_item_id().to_string(), + OrderingCommand::AddLineItem(cmd.clone()), + ) + .await + .unwrap(); + + let line_item = line_item_query + .load(&(*cmd.line_item_id()).to_string()) + .await + .unwrap() + .unwrap(); + let line_item: LineItem = line_item.into(); + assert_eq!(line_item.line_item_id(), cmd.line_item_id()); + assert_eq!(line_item.product_name(), cmd.product_name()); + assert_eq!(line_item.product_id(), cmd.product_id()); + assert_eq!(line_item.quantity(), cmd.quantity()); + assert!(!line_item.deleted()); + + let update_line_item_cmd = UnvalidatedUpdateLineItemCommandBuilder::default() + .product_name(rand.get_random(10)) + .adding_by(UUID) + .quantity(Quantity::get_quantity()) + .product_id(UUID) + .kot_id(*kot.kot_id()) + .old_line_item(line_item.clone()) + + .build() + .unwrap() + .validate() + .unwrap(); + + cqrs.execute( + &cmd.line_item_id().to_string(), + OrderingCommand::UpdateLineItem(update_line_item_cmd.clone()), + ) + .await + .unwrap(); + let line_item = line_item_query + .load(&(*cmd.line_item_id()).to_string()) + .await + .unwrap() + .unwrap(); + let line_item: LineItem = line_item.into(); + assert_eq!( + line_item.line_item_id(), + update_line_item_cmd.old_line_item().line_item_id() + ); + assert_eq!( + line_item.product_name(), + update_line_item_cmd.product_name() + ); + assert_eq!(line_item.product_id(), update_line_item_cmd.product_id()); + assert_eq!(line_item.quantity(), update_line_item_cmd.quantity()); + assert!(!line_item.deleted()); + + // delete + let delete_line_item_cmd = DeleteLineItemCommandBuilder::default() + .line_item(line_item.clone()) + .adding_by(UUID) + .build() + .unwrap(); + cqrs.execute( + &cmd.line_item_id().to_string(), + OrderingCommand::DeleteLineItem(delete_line_item_cmd.clone()), + ) + .await + .unwrap(); + let deleted_line_item = line_item_query + .load(&(*cmd.line_item_id()).to_string()) + .await + .unwrap() + .unwrap(); + let deleted_line_item: LineItem = deleted_line_item.into(); + assert_eq!( + deleted_line_item.line_item_id(), + delete_line_item_cmd.line_item().line_item_id() + ); + assert_eq!( + deleted_line_item.product_name(), + delete_line_item_cmd.line_item().product_name() + ); + assert_eq!( + deleted_line_item.product_id(), + delete_line_item_cmd.line_item().product_id() + ); + assert_eq!( + deleted_line_item.quantity(), + delete_line_item_cmd.line_item().quantity() + ); + assert!(deleted_line_item.deleted()); + + settings.drop_db().await; + } +} diff --git a/src/ordering/adapters/output/db/store_view.rs b/src/ordering/adapters/output/db/store_view.rs index 8680b1b..7c3a29c 100644 --- a/src/ordering/adapters/output/db/store_view.rs +++ b/src/ordering/adapters/output/db/store_view.rs @@ -216,7 +216,6 @@ impl Query for OrderingDBPostgresAdapter { } } - #[cfg(test)] mod tests { use super::*; @@ -224,6 +223,7 @@ mod tests { use postgres_es::PostgresCqrs; use crate::{ + db::migrate::*, ordering::{ application::services::{ add_store_service::AddStoreServiceBuilder, update_store_service::*, @@ -233,7 +233,6 @@ mod tests { domain::commands::OrderingCommand, domain::update_store_command::*, }, - db::migrate::*, tests::bdd::*, utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, }; diff --git a/src/ordering/application/services/add_customization_service.rs b/src/ordering/application/services/add_customization_service.rs index 0b52d68..f47656f 100644 --- a/src/ordering/application/services/add_customization_service.rs +++ b/src/ordering/application/services/add_customization_service.rs @@ -11,14 +11,10 @@ use mockall::*; use super::errors::*; use crate::ordering::{ application::port::output::db::{ - customization_id_exists::*, - customization_name_exists_for_product::*, - product_id_exists::*, + customization_id_exists::*, customization_name_exists_for_product::*, product_id_exists::*, }, domain::{ - add_customization_command::*, - customization_added_event::*, - customization_aggregate::*, + add_customization_command::*, customization_added_event::*, customization_aggregate::*, }, }; @@ -54,13 +50,13 @@ impl AddCustomizationUseCase for AddCustomizationService { return Err(OrderingError::ProductIDNotFound); } - if self - .db_customization_id_exists - .customization_id_exists(cmd.customization_id()) - .await? - { - return Err(OrderingError::DuplicateCustomizationID); - } + if self + .db_customization_id_exists + .customization_id_exists(cmd.customization_id()) + .await? + { + return Err(OrderingError::DuplicateCustomizationID); + } let customization = CustomizationBuilder::default() .name(cmd.name().into()) @@ -90,8 +86,7 @@ pub mod tests { use super::*; use crate::ordering::domain::{ - add_customization_command::tests::*, - customization_added_event::tests::*, + add_customization_command::tests::*, customization_added_event::tests::*, }; use crate::tests::bdd::*; @@ -131,7 +126,10 @@ pub mod tests { let res = s.add_customization(cmd.clone()).await.unwrap(); assert_eq!(res.customization().name(), cmd.name()); - assert_eq!(res.customization().customization_id(), cmd.customization_id()); + assert_eq!( + res.customization().customization_id(), + cmd.customization_id() + ); } #[actix_rt::test] diff --git a/src/ordering/application/services/add_line_item_service.rs b/src/ordering/application/services/add_line_item_service.rs index 4214c3b..1e5d573 100644 --- a/src/ordering/application/services/add_line_item_service.rs +++ b/src/ordering/application/services/add_line_item_service.rs @@ -15,7 +15,6 @@ use crate::ordering::{ application::port::output::db::line_item_id_exists::*, domain::{add_line_item_command::*, line_item_added_event::*, line_item_aggregate::*}, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -29,7 +28,6 @@ pub type AddLineItemServiceObj = Arc; pub struct AddLineItemService { db_line_item_id_exists: LineItemIDExistsDBPortObj, db_kot_id_exists: KotIDExistsDBPortObj, - get_uuid: GetUUIDInterfaceObj, } #[async_trait::async_trait] @@ -39,27 +37,20 @@ impl AddLineItemUseCase for AddLineItemService { return Err(OrderingError::KotIDNotFound); } - let mut line_item_id = self.get_uuid.get_uuid(); - - loop { if self .db_line_item_id_exists - .line_item_id_exists(&line_item_id) + .line_item_id_exists(cmd.line_item_id()) .await? { - line_item_id = self.get_uuid.get_uuid(); - continue; - } else { - break; + return Err(OrderingError::DuplicateLineItemID); } - } let line_item = LineItemBuilder::default() .created_time(cmd.created_time().clone()) .product_name(cmd.product_name().into()) .product_id(*cmd.product_id()) .kot_id(*cmd.kot_id()) - .line_item_id(line_item_id) + .line_item_id(*cmd.line_item_id()) .quantity(cmd.quantity().clone()) .deleted(false) .build() @@ -79,7 +70,7 @@ pub mod tests { use crate::ordering::domain::line_item_added_event::tests::get_added_line_item_event_from_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_line_item_service( times: Option, @@ -106,7 +97,6 @@ pub mod tests { let s = AddLineItemServiceBuilder::default() .db_line_item_id_exists(mock_line_item_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) .db_kot_id_exists(mock_kot_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); @@ -117,6 +107,7 @@ pub mod tests { assert_eq!(res.line_item().quantity(), cmd.quantity()); assert_eq!(res.line_item().quantity(), cmd.quantity()); assert_eq!(res.line_item().created_time(), cmd.created_time()); + assert_eq!(res.line_item().line_item_id(), cmd.line_item_id()); assert!(!res.line_item().deleted()); assert_eq!(res.added_by_user(), cmd.adding_by()); } @@ -128,7 +119,6 @@ pub mod tests { let s = AddLineItemServiceBuilder::default() .db_line_item_id_exists(mock_line_item_id_exists_db_port_false(IS_NEVER_CALLED)) .db_kot_id_exists(mock_kot_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) .build() .unwrap(); diff --git a/src/ordering/application/services/add_store_service.rs b/src/ordering/application/services/add_store_service.rs index aa88411..9b268a6 100644 --- a/src/ordering/application/services/add_store_service.rs +++ b/src/ordering/application/services/add_store_service.rs @@ -35,10 +35,13 @@ pub struct AddStoreService { #[async_trait::async_trait] impl AddStoreUseCase for AddStoreService { async fn add_store(&self, cmd: AddStoreCommand) -> OrderingResult { - - if self.db_store_id_exists.store_id_exists(cmd.store_id()).await? { - return Err(OrderingError::DuplicateStoreID); - } + if self + .db_store_id_exists + .store_id_exists(cmd.store_id()) + .await? + { + return Err(OrderingError::DuplicateStoreID); + } let store = StoreBuilder::default() .name(cmd.name().into()) diff --git a/src/ordering/application/services/errors.rs b/src/ordering/application/services/errors.rs index 1cacb54..83303f6 100644 --- a/src/ordering/application/services/errors.rs +++ b/src/ordering/application/services/errors.rs @@ -46,7 +46,7 @@ impl From for OrderingError { OrderingDBError::DuplicateStoreID => Self::DuplicateStoreID, OrderingDBError::StoreIDNotFound => OrderingError::StoreIDNotFound, OrderingDBError::OrderIDNotFound => OrderingError::OrderIDNotFound, - OrderingDBError::DuplicateKotID => Self::DuplicateKotID, + OrderingDBError::DuplicateKotID => Self::DuplicateKotID, OrderingDBError::KotIDNotFound => OrderingError::KotIDNotFound, OrderingDBError::InternalError => Self::InternalError, OrderingDBError::DuplicateCategoryName => Self::DuplicateCategoryName, diff --git a/src/ordering/domain/add_customization_command.rs b/src/ordering/domain/add_customization_command.rs index a912070..507412c 100644 --- a/src/ordering/domain/add_customization_command.rs +++ b/src/ordering/domain/add_customization_command.rs @@ -82,4 +82,3 @@ pub mod tests { .is_err(),); } } - diff --git a/src/ordering/domain/add_line_item_command.rs b/src/ordering/domain/add_line_item_command.rs index d1f68cb..4a70415 100644 --- a/src/ordering/domain/add_line_item_command.rs +++ b/src/ordering/domain/add_line_item_command.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; -use crate::types::quantity::*; +use crate::types::{currency::*, quantity::*}; use crate::utils::string::empty_string_err; #[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] @@ -19,52 +19,41 @@ pub enum AddLineItemCommandError { } #[derive( - Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, + Clone, Debug, Builder, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, )] -pub struct UnvalidatedAddLineItemCommand { - adding_by: Uuid, - +#[builder(build_fn(validate = "Self::validate"))] +pub struct AddLineItemCommand { #[builder(default = "OffsetDateTime::now_utc()")] created_time: OffsetDateTime, + #[builder(setter(custom))] product_name: String, product_id: Uuid, kot_id: Uuid, - quantity: Quantity, -} - -impl UnvalidatedAddLineItemCommand { - pub fn validate(self) -> Result { - let product_name = empty_string_err( - self.product_name, - AddLineItemCommandError::ProductNameIsEmpty, - )?; - - if self.quantity.is_empty() { - return Err(AddLineItemCommandError::QuantityIsEmpty); - } - - Ok(AddLineItemCommand { - created_time: self.created_time, - product_name, - product_id: self.product_id, - kot_id: self.kot_id, - quantity: self.quantity, - adding_by: self.adding_by, - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] -pub struct AddLineItemCommand { - created_time: OffsetDateTime, - product_name: String, - product_id: Uuid, - kot_id: Uuid, + line_item_id: Uuid, quantity: Quantity, adding_by: Uuid, } +impl AddLineItemCommandBuilder { + pub fn product_name(&mut self, product_name: String) -> &mut Self { + self.product_name = Some(product_name.trim().to_owned()); + self + } + + fn validate(&self) -> Result<(), String> { + let product_name = self.product_name.as_ref().unwrap().trim().to_owned(); + if product_name.is_empty() { + return Err(AddLineItemCommandError::ProductNameIsEmpty.to_string()); + } + + if self.quantity.as_ref().unwrap().is_empty() { + return Err(AddLineItemCommandError::QuantityIsEmpty.to_string()); + } + Ok(()) + } +} + #[cfg(test)] mod tests { use time::macros::datetime; @@ -81,17 +70,16 @@ mod tests { let adding_by = UUID; let quantity = Quantity::get_quantity(); - UnvalidatedAddLineItemCommandBuilder::default() + AddLineItemCommandBuilder::default() .product_name(product_name.into()) .adding_by(adding_by) .created_time(datetime!(1970-01-01 0:00 UTC)) .quantity(quantity.clone()) .product_id(product_id) .kot_id(kot_id) + .line_item_id(UUID) .build() .unwrap() - .validate() - .unwrap() } } @@ -103,15 +91,14 @@ mod tests { let adding_by = UUID; let quantity = Quantity::get_quantity(); - let cmd = UnvalidatedAddLineItemCommandBuilder::default() + let cmd = AddLineItemCommandBuilder::default() .product_name(product_name.into()) .adding_by(adding_by) .quantity(quantity.clone()) .product_id(product_id) .kot_id(kot_id) + .line_item_id(UUID) .build() - .unwrap() - .validate() .unwrap(); assert_eq!(cmd.quantity(), &quantity); @@ -128,18 +115,15 @@ mod tests { let adding_by = UUID; let quantity = Quantity::get_quantity(); - assert_eq!( - UnvalidatedAddLineItemCommandBuilder::default() - .product_name(product_name.into()) - .adding_by(adding_by) - .quantity(quantity.clone()) - .product_id(product_id) - .kot_id(kot_id) - .build() - .unwrap() - .validate(), - Err(AddLineItemCommandError::ProductNameIsEmpty) - ); + assert!(AddLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .product_id(product_id) + .line_item_id(UUID) + .kot_id(kot_id) + .build() + .is_err()); } #[test] @@ -151,17 +135,13 @@ mod tests { // minor = 0; major = 0; let quantity = Quantity::default(); - assert_eq!( - UnvalidatedAddLineItemCommandBuilder::default() - .product_name(product_name.into()) - .adding_by(adding_by) - .quantity(quantity.clone()) - .product_id(product_id) - .kot_id(kot_id) - .build() - .unwrap() - .validate(), - Err(AddLineItemCommandError::QuantityIsEmpty) - ); + assert!(AddLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .product_id(product_id) + .kot_id(kot_id) + .build() + .is_err()); } } From c5d5dcb323b08b4809bddb5fe4cbfbed9db8d096 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 24 Sep 2024 15:44:48 +0530 Subject: [PATCH 4/8] feat: ordering: Kot ID is provided by caller & Kot view tests --- ...e5fc2837b3943b8bbcbade97920a99019c86.json} | 5 +- src/ordering/adapters/output/db/kot_view.rs | 185 +++++++++++++++++- .../adapters/output/db/line_item_view.rs | 15 +- .../adapters/output/db/order_id_exists.rs | 2 +- .../application/services/add_kot_service.rs | 20 +- .../services/add_line_item_service.rs | 16 +- src/ordering/domain/add_kot_command.rs | 4 + src/ordering/domain/kot_added_event.rs | 10 +- 8 files changed, 213 insertions(+), 44 deletions(-) rename .sqlx/{query-4b7100e5c7442066dbb4ea7c9733e038af635766f0be61a86ca7084b34a2109d.json => query-97c136fd927b9a153a6093da6594e5fc2837b3943b8bbcbade97920a99019c86.json} (67%) diff --git a/.sqlx/query-4b7100e5c7442066dbb4ea7c9733e038af635766f0be61a86ca7084b34a2109d.json b/.sqlx/query-97c136fd927b9a153a6093da6594e5fc2837b3943b8bbcbade97920a99019c86.json similarity index 67% rename from .sqlx/query-4b7100e5c7442066dbb4ea7c9733e038af635766f0be61a86ca7084b34a2109d.json rename to .sqlx/query-97c136fd927b9a153a6093da6594e5fc2837b3943b8bbcbade97920a99019c86.json index 2aeaf77..319c68c 100644 --- a/.sqlx/query-4b7100e5c7442066dbb4ea7c9733e038af635766f0be61a86ca7084b34a2109d.json +++ b/.sqlx/query-97c136fd927b9a153a6093da6594e5fc2837b3943b8bbcbade97920a99019c86.json @@ -1,18 +1,17 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_ordering_kot_query\n SET\n version = $1,\n order_id = $2,\n kot_id = $3,\n created_time = $4,\n deleted = $5;", + "query": "UPDATE\n cqrs_ordering_kot_query\n SET\n version = $1,\n order_id = $2,\n created_time = $3,\n deleted = $4;", "describe": { "columns": [], "parameters": { "Left": [ "Int8", "Uuid", - "Uuid", "Timestamptz", "Bool" ] }, "nullable": [] }, - "hash": "4b7100e5c7442066dbb4ea7c9733e038af635766f0be61a86ca7084b34a2109d" + "hash": "97c136fd927b9a153a6093da6594e5fc2837b3943b8bbcbade97920a99019c86" } diff --git a/src/ordering/adapters/output/db/kot_view.rs b/src/ordering/adapters/output/db/kot_view.rs index 047da2a..2adccd8 100644 --- a/src/ordering/adapters/output/db/kot_view.rs +++ b/src/ordering/adapters/output/db/kot_view.rs @@ -197,12 +197,10 @@ impl ViewRepository for OrderingDBPostgresAdapter { SET version = $1, order_id = $2, - kot_id = $3, - created_time = $4, - deleted = $5;", + created_time = $3, + deleted = $4;", version, view.order_id, - view.kot_id, view.created_time, view.deleted, ) @@ -230,3 +228,182 @@ impl Query for OrderingDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + use time::macros::datetime; + + use crate::{ + db::migrate::*, + ordering::{ + application::services::{ + add_kot_service::AddKotServiceBuilder, delete_kot_service::DeleteKotServiceBuilder, + update_kot_service::*, MockOrderingServicesInterface, + }, + domain::{ + add_kot_command::*, commands::OrderingCommand, + delete_kot_command::DeleteKotCommandBuilder, order_aggregate::Order, + update_kot_command::*, + }, + }, + tests::bdd::*, + utils::{ + random_string::GenerateRandomStringInterface, + uuid::{tests::UUID, *}, + }, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_ordering_kot_view() { + let settings = crate::settings::tests::get_settings().await; + //let settings = crate::settings::Settings::new().unwrap(); + settings.create_db().await; + + let uuid = GenerateUUID {}; + + let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await; + db.migrate().await; + let db = OrderingDBPostgresAdapter::new(db.pool.clone()); + + let order = Order::default(); + crate::ordering::adapters::output::db::order_id_exists::tests::create_dummy_order( + &order, &db, + ) + .await; + + let queries: Vec>> = vec![Box::new(db.clone())]; + + let mut mock_services = MockOrderingServicesInterface::new(); + + let db2 = db.clone(); + mock_services + .expect_add_kot() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddKotServiceBuilder::default() + .db_kot_id_exists(Arc::new(db2.clone())) + .db_order_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_update_kot() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateKotServiceBuilder::default() + .db_kot_id_exists(Arc::new(db2.clone())) + .db_order_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_delete_kot() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + DeleteKotServiceBuilder::default() + .db_kot_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let (cqrs, kot_query): ( + Arc>, + Arc>, + ) = ( + Arc::new(postgres_es::postgres_cqrs( + db.pool.clone(), + queries, + Arc::new(mock_services), + )), + Arc::new(db.clone()), + ); + + let cmd = AddKotCommandBuilder::default() + .adding_by(UUID) + .created_time(datetime!(1970-01-01 0:00 UTC)) + .order_id(*order.order_id()) + .kot_id(uuid.get_uuid()) + .build() + .unwrap(); + + cqrs.execute( + &cmd.kot_id().to_string(), + OrderingCommand::AddKot(cmd.clone()), + ) + .await + .unwrap(); + + let kot = kot_query + .load(&(*cmd.kot_id()).to_string()) + .await + .unwrap() + .unwrap(); + let kot: Kot = kot.into(); + assert_eq!(kot.order_id(), cmd.order_id()); + assert_eq!(kot.kot_id(), cmd.kot_id()); + assert!(!kot.deleted()); + + // update + let update_kot_cmd = UpdateKotCommandBuilder::default() + .adding_by(UUID) + .created_time(datetime!(1970-01-01 1:00 UTC)) + .order_id(*order.order_id()) + .old_kot(kot.clone()) + .build() + .unwrap(); + cqrs.execute( + &cmd.kot_id().to_string(), + OrderingCommand::UpdateKot(update_kot_cmd.clone()), + ) + .await + .unwrap(); + let kot = kot_query + .load(&(*cmd.kot_id()).to_string()) + .await + .unwrap() + .unwrap(); + let kot: Kot = kot.into(); + assert_eq!(kot.order_id(), update_kot_cmd.order_id()); + assert_eq!(kot.kot_id(), update_kot_cmd.old_kot().kot_id()); + assert!(!kot.deleted()); + + // delete + let delete_kot_command = DeleteKotCommandBuilder::default() + .kot(kot.clone()) + .adding_by(UUID) + .build() + .unwrap(); + + cqrs.execute( + &cmd.kot_id().to_string(), + OrderingCommand::DeleteKot(delete_kot_command.clone()), + ) + .await + .unwrap(); + let kot = kot_query + .load(&(*cmd.kot_id()).to_string()) + .await + .unwrap() + .unwrap(); + let kot: Kot = kot.into(); + assert_eq!(kot.order_id(), delete_kot_command.kot().order_id()); + assert_eq!(kot.kot_id(), delete_kot_command.kot().kot_id()); + assert!(kot.deleted()); + + settings.drop_db().await; + } +} diff --git a/src/ordering/adapters/output/db/line_item_view.rs b/src/ordering/adapters/output/db/line_item_view.rs index 5fe5c2a..3e3fa33 100644 --- a/src/ordering/adapters/output/db/line_item_view.rs +++ b/src/ordering/adapters/output/db/line_item_view.rs @@ -319,7 +319,6 @@ impl Query for OrderingDBPostgresAdapter { } } - #[cfg(test)] mod tests { use super::*; @@ -327,18 +326,18 @@ mod tests { use postgres_es::PostgresCqrs; use crate::{ + db::migrate::*, ordering::{ application::services::{ add_line_item_service::AddLineItemServiceBuilder, delete_line_item_service::*, update_line_item_service::*, MockOrderingServicesInterface, }, domain::{ - add_line_item_command::*, kot_aggregate::Kot, commands::OrderingCommand, - delete_line_item_command::DeleteLineItemCommandBuilder, + add_line_item_command::*, commands::OrderingCommand, + delete_line_item_command::DeleteLineItemCommandBuilder, kot_aggregate::Kot, update_line_item_command::*, }, }, - db::migrate::*, tests::bdd::*, types::quantity::*, utils::{ @@ -363,10 +362,8 @@ mod tests { let mut mock_services = MockOrderingServicesInterface::new(); let kot = Kot::default(); - crate::ordering::adapters::output::db::kot_id_exists::tests::create_dummy_kot( - &kot, &db, - ) - .await; + crate::ordering::adapters::output::db::kot_id_exists::tests::create_dummy_kot(&kot, &db) + .await; let db2 = db.clone(); mock_services @@ -428,7 +425,6 @@ mod tests { let cmd = AddLineItemCommandBuilder::default() .product_name(rand.get_random(10)) .adding_by(UUID) - .quantity(Quantity::get_quantity()) .product_id(UUID) .kot_id(*kot.kot_id()) @@ -462,7 +458,6 @@ mod tests { .product_id(UUID) .kot_id(*kot.kot_id()) .old_line_item(line_item.clone()) - .build() .unwrap() .validate() diff --git a/src/ordering/adapters/output/db/order_id_exists.rs b/src/ordering/adapters/output/db/order_id_exists.rs index 5b5a99c..5f0f634 100644 --- a/src/ordering/adapters/output/db/order_id_exists.rs +++ b/src/ordering/adapters/output/db/order_id_exists.rs @@ -36,7 +36,7 @@ pub mod tests { // use crate::ordering::domain::add_order_command::tests::get_customizations; use crate::ordering::domain::order_aggregate::*; - async fn create_dummy_order(order: &Order, db: &OrderingDBPostgresAdapter) { + pub async fn create_dummy_order(order: &Order, db: &OrderingDBPostgresAdapter) { sqlx::query!( "INSERT INTO cqrs_ordering_order_query ( version, diff --git a/src/ordering/application/services/add_kot_service.rs b/src/ordering/application/services/add_kot_service.rs index 09edd5d..c04d874 100644 --- a/src/ordering/application/services/add_kot_service.rs +++ b/src/ordering/application/services/add_kot_service.rs @@ -7,7 +7,6 @@ use std::sync::Arc; use derive_builder::Builder; use mockall::predicate::*; use mockall::*; -use time::OffsetDateTime; use super::errors::*; use crate::ordering::{ @@ -15,7 +14,6 @@ use crate::ordering::{ application::port::output::db::order_id_exists::*, domain::{add_kot_command::*, kot_added_event::*, kot_aggregate::*}, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -29,7 +27,6 @@ pub type AddKotServiceObj = Arc; pub struct AddKotService { db_kot_id_exists: KotIDExistsDBPortObj, db_order_id_exists: OrderIDExistsDBPortObj, - get_uuid: GetUUIDInterfaceObj, } #[async_trait::async_trait] @@ -43,21 +40,14 @@ impl AddKotUseCase for AddKotService { return Err(OrderingError::OrderIDNotFound); } - let mut kot_id = self.get_uuid.get_uuid(); - - loop { - if self.db_kot_id_exists.kot_id_exists(&kot_id).await? { - kot_id = self.get_uuid.get_uuid(); - continue; - } else { - break; - } + if self.db_kot_id_exists.kot_id_exists(cmd.kot_id()).await? { + return Err(OrderingError::KotIDNotFound); } let kot = KotBuilder::default() .created_time(cmd.created_time().clone()) .order_id(*cmd.order_id()) - .kot_id(kot_id) + .kot_id(*cmd.kot_id()) .deleted(false) .build() .unwrap(); @@ -75,8 +65,8 @@ pub mod tests { use super::*; use crate::ordering::domain::kot_added_event::tests::get_added_kot_event_from_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_kot_service(times: Option, cmd: AddKotCommand) -> AddKotServiceObj { let mut m = MockAddKotUseCase::new(); @@ -100,7 +90,6 @@ pub mod tests { let s = AddKotServiceBuilder::default() .db_kot_id_exists(mock_kot_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) .db_order_id_exists(mock_order_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); @@ -118,7 +107,6 @@ pub mod tests { let s = AddKotServiceBuilder::default() .db_kot_id_exists(mock_kot_id_exists_db_port_false(IS_NEVER_CALLED)) .db_order_id_exists(mock_order_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) .build() .unwrap(); diff --git a/src/ordering/application/services/add_line_item_service.rs b/src/ordering/application/services/add_line_item_service.rs index 1e5d573..ca329be 100644 --- a/src/ordering/application/services/add_line_item_service.rs +++ b/src/ordering/application/services/add_line_item_service.rs @@ -37,13 +37,13 @@ impl AddLineItemUseCase for AddLineItemService { return Err(OrderingError::KotIDNotFound); } - if self - .db_line_item_id_exists - .line_item_id_exists(cmd.line_item_id()) - .await? - { - return Err(OrderingError::DuplicateLineItemID); - } + if self + .db_line_item_id_exists + .line_item_id_exists(cmd.line_item_id()) + .await? + { + return Err(OrderingError::DuplicateLineItemID); + } let line_item = LineItemBuilder::default() .created_time(cmd.created_time().clone()) @@ -69,8 +69,8 @@ pub mod tests { use super::*; use crate::ordering::domain::line_item_added_event::tests::get_added_line_item_event_from_command; - use crate::utils::uuid::tests::UUID; use crate::tests::bdd::*; + use crate::utils::uuid::tests::UUID; pub fn mock_add_line_item_service( times: Option, diff --git a/src/ordering/domain/add_kot_command.rs b/src/ordering/domain/add_kot_command.rs index 597330a..6fdd91e 100644 --- a/src/ordering/domain/add_kot_command.rs +++ b/src/ordering/domain/add_kot_command.rs @@ -17,6 +17,7 @@ pub struct AddKotCommand { #[builder(default = "OffsetDateTime::now_utc()")] created_time: OffsetDateTime, order_id: Uuid, + kot_id: Uuid, } #[cfg(test)] @@ -36,6 +37,7 @@ mod tests { .adding_by(adding_by) .created_time(datetime!(1970-01-01 0:00 UTC)) .order_id(order_id) + .kot_id(UUID) .build() .unwrap() } @@ -49,10 +51,12 @@ mod tests { let cmd = AddKotCommandBuilder::default() .adding_by(adding_by) .order_id(order_id) + .kot_id(UUID) .build() .unwrap(); assert_eq!(*cmd.order_id(), order_id); + assert_eq!(*cmd.kot_id(), UUID); assert_eq!(*cmd.adding_by(), adding_by); } } diff --git a/src/ordering/domain/kot_added_event.rs b/src/ordering/domain/kot_added_event.rs index 26fd5f6..eb33ff5 100644 --- a/src/ordering/domain/kot_added_event.rs +++ b/src/ordering/domain/kot_added_event.rs @@ -20,12 +20,18 @@ pub struct KotAddedEvent { #[cfg(test)] pub mod tests { - use crate::ordering::domain::add_kot_command::AddKotCommand; + use crate::ordering::domain::{add_kot_command::AddKotCommand, kot_aggregate::*}; use super::*; pub fn get_added_kot_event_from_command(cmd: &AddKotCommand) -> KotAddedEvent { - let kot = Kot::get_kot(); + let kot = KotBuilder::default() + .created_time(cmd.created_time().clone()) + .order_id(*cmd.order_id()) + .kot_id(*cmd.kot_id()) + .deleted(false) + .build() + .unwrap(); KotAddedEventBuilder::default() .added_by_user(cmd.adding_by().clone()) From 5f466fce80a0d4b3e3f217ab3495e3539664e83b Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 24 Sep 2024 15:59:50 +0530 Subject: [PATCH 5/8] feat: ordering: Category ID is provided by caller & Category view tests --- ...5979786aac15b0c78359b3f634b74c4686d4.json} | 5 +- .../adapters/output/db/category_view.rs | 164 +++++++++++++++- .../adapters/output/db/store_id_exists.rs | 2 +- .../services/add_category_service.rs | 63 ++---- src/ordering/domain/add_category_command.rs | 182 +++++++++++++++--- src/ordering/domain/category_aggregate.rs | 14 +- 6 files changed, 335 insertions(+), 95 deletions(-) rename .sqlx/{query-d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb.json => query-289e6d9fe105ae9e91d947c22d0a5979786aac15b0c78359b3f634b74c4686d4.json} (63%) diff --git a/.sqlx/query-d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb.json b/.sqlx/query-289e6d9fe105ae9e91d947c22d0a5979786aac15b0c78359b3f634b74c4686d4.json similarity index 63% rename from .sqlx/query-d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb.json rename to .sqlx/query-289e6d9fe105ae9e91d947c22d0a5979786aac15b0c78359b3f634b74c4686d4.json index db85039..b3a9c5a 100644 --- a/.sqlx/query-d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb.json +++ b/.sqlx/query-289e6d9fe105ae9e91d947c22d0a5979786aac15b0c78359b3f634b74c4686d4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_ordering_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_ordering_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": "d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb" + "hash": "289e6d9fe105ae9e91d947c22d0a5979786aac15b0c78359b3f634b74c4686d4" } diff --git a/src/ordering/adapters/output/db/category_view.rs b/src/ordering/adapters/output/db/category_view.rs index e817a24..247799d 100644 --- a/src/ordering/adapters/output/db/category_view.rs +++ b/src/ordering/adapters/output/db/category_view.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use super::errors::*; use super::OrderingDBPostgresAdapter; -use crate::ordering::domain::category_aggregate::Category; +use crate::ordering::domain::category_aggregate::*; use crate::ordering::domain::events::OrderingEvent; 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. @@ -157,13 +170,11 @@ impl ViewRepository for OrderingDBPostgresAdapter { 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 ) @@ -210,3 +221,146 @@ impl Query for OrderingDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + db::migrate::*, + ordering::{ + application::services::{ + add_category_service::*, update_category_service::*, MockOrderingServicesInterface, + }, + 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_ordering_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 = OrderingDBPostgresAdapter::new(db.pool.clone()); + + let store = Store::default(); + crate::ordering::adapters::output::db::store_id_exists::tests::create_dummy_store_record( + &store, &db, + ) + .await; + + let queries: Vec>> = vec![Box::new(db.clone())]; + + let mut mock_services = MockOrderingServicesInterface::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(), + OrderingCommand::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(), + OrderingCommand::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/ordering/adapters/output/db/store_id_exists.rs b/src/ordering/adapters/output/db/store_id_exists.rs index c67fbe0..1254cb1 100644 --- a/src/ordering/adapters/output/db/store_id_exists.rs +++ b/src/ordering/adapters/output/db/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 diff --git a/src/ordering/application/services/add_category_service.rs b/src/ordering/application/services/add_category_service.rs index 61e04ba..64aa569 100644 --- a/src/ordering/application/services/add_category_service.rs +++ b/src/ordering/application/services/add_category_service.rs @@ -19,7 +19,6 @@ use crate::ordering::{ 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(OrderingError::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(OrderingError::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,9 @@ pub mod tests { use uuid::Uuid; - use crate::utils::uuid::tests::UUID; - use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + use crate::tests::bdd::*; + + use crate::ordering::domain::add_category_command::tests::get_add_category_cmd; pub fn mock_add_category_service( times: Option, @@ -111,7 +102,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(); @@ -128,14 +119,7 @@ pub mod tests { #[actix_rt::test] async fn test_service_category_doesnt_exist() { - let name = "foo"; - let description = "bar"; - 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) - .unwrap(); + let cmd = get_add_category_cmd(); let s = AddCategoryServiceBuilder::default() .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) @@ -143,7 +127,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(); @@ -152,19 +135,12 @@ pub mod tests { assert_eq!(res.description(), cmd.description()); assert_eq!(res.added_by_user(), cmd.adding_by()); assert_eq!(res.store_id(), cmd.store_id()); - assert_eq!(res.category_id(), &UUID); + assert_eq!(res.category_id(), cmd.category_id()); } #[actix_rt::test] async fn test_service_category_name_exists_for_store() { - let name = "foo"; - let description = "bar"; - 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) - .unwrap(); + let cmd = get_add_category_cmd(); let s = AddCategoryServiceBuilder::default() .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) @@ -172,7 +148,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(); @@ -184,14 +159,7 @@ pub mod tests { #[actix_rt::test] async fn test_service_store_doesnt_exist() { - let name = "foo"; - let description = "bar"; - 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) - .unwrap(); + let cmd = get_add_category_cmd(); let s = AddCategoryServiceBuilder::default() .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) @@ -199,7 +167,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/ordering/domain/add_category_command.rs b/src/ordering/domain/add_category_command.rs index 9c8d0eb..ec116de 100644 --- a/src/ordering/domain/add_category_command.rs +++ b/src/ordering/domain/add_category_command.rs @@ -1,7 +1,100 @@ +//// 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; +// +//#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +//pub enum AddCategoryCommandError { +// NameIsEmpty, +//} +// +//#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +//pub struct AddCategoryCommand { +// name: String, +// description: Option, +// store_id: Uuid, +// adding_by: Uuid, +//} +// +//impl AddCategoryCommand { +// pub fn new( +// name: String, +// description: Option, +// store_id: Uuid, +// adding_by: Uuid, +// ) -> Result { +// let description: Option = if let Some(description) = description { +// let description = description.trim(); +// if description.is_empty() { +// None +// } else { +// Some(description.to_owned()) +// } +// } else { +// None +// }; +// +// let name = name.trim().to_owned(); +// if name.is_empty() { +// return Err(AddCategoryCommandError::NameIsEmpty); +// } +// +// Ok(Self { +// name, +// store_id, +// description, +// adding_by, +// }) +// } +//} +// +//#[cfg(test)] +//mod tests { +// use super::*; +// +// use crate::utils::uuid::tests::UUID; +// +// #[test] +// fn test_cmd() { +// let name = "foo"; +// let description = "bar"; +// let adding_by = UUID; +// let store_id = Uuid::new_v4(); +// +// // description = None +// let cmd = AddCategoryCommand::new(name.into(), None, store_id, adding_by).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(); +// 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) +// ) +// } +//} +// +// // 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}; @@ -12,21 +105,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,27 +131,46 @@ 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(()) } } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use crate::utils::uuid::tests::UUID; + pub fn get_add_category_cmd() -> AddCategoryCommand { + let name = "foo"; + let adding_by = UUID; + let store_id = Uuid::new_v4(); + + AddCategoryCommandBuilder::default() + .name(name.into()) + .description(None) + .store_id(store_id) + .adding_by(adding_by) + .category_id(UUID) + .build() + .unwrap() + } + #[test] fn test_cmd() { let name = "foo"; @@ -65,26 +178,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/ordering/domain/category_aggregate.rs b/src/ordering/domain/category_aggregate.rs index c14130f..e207da2 100644 --- a/src/ordering/domain/category_aggregate.rs +++ b/src/ordering/domain/category_aggregate.rs @@ -81,8 +81,8 @@ mod aggregate_tests { use std::sync::Arc; use cqrs_es::test::TestFramework; + use tests::get_add_category_cmd; use update_category_service::tests::mock_update_category_service; - use uuid::Uuid; use super::*; use crate::ordering::{ @@ -95,27 +95,19 @@ mod aggregate_tests { }, }; use crate::tests::bdd::*; - use crate::utils::uuid::tests::*; type CategoryTestFramework = TestFramework; #[test] fn test_create_category() { - let name = "category_name"; - let description = Some("category_description".to_string()); - let adding_by = UUID; - 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 = get_add_category_cmd(); let expected = CategoryAddedEventBuilder::default() .name(cmd.name().into()) .description(cmd.description().as_ref().map(|s| s.to_string())) .added_by_user(*cmd.adding_by()) .store_id(*cmd.store_id()) - .category_id(category_id) + .category_id(*cmd.category_id()) .build() .unwrap(); let expected = OrderingEvent::CategoryAdded(expected); From 7c3676e84dc570742aa58fda1e287561e370715b Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 24 Sep 2024 16:07:21 +0530 Subject: [PATCH 6/8] feat: ordering: Product ID is provided by caller & Product view tests --- ...144190120c13752364bc3b78a92a08bd9157.json} | 5 +- .../db/category_name_exists_for_store.rs | 2 +- .../adapters/output/db/product_view.rs | 179 ++++++++++++++++-- .../services/add_product_service.rs | 33 +--- src/ordering/domain/add_category_command.rs | 92 --------- src/ordering/domain/add_product_command.rs | 88 ++++----- src/ordering/domain/update_product_command.rs | 49 +++++ 7 files changed, 269 insertions(+), 179 deletions(-) rename .sqlx/{query-c3a3348990d0fea3225fd2be2ef883ca1649e21fd28c1a35a0ffffce6035fd75.json => query-a3fa1c6271b85d23d70116363f19144190120c13752364bc3b78a92a08bd9157.json} (50%) diff --git a/.sqlx/query-c3a3348990d0fea3225fd2be2ef883ca1649e21fd28c1a35a0ffffce6035fd75.json b/.sqlx/query-a3fa1c6271b85d23d70116363f19144190120c13752364bc3b78a92a08bd9157.json similarity index 50% rename from .sqlx/query-c3a3348990d0fea3225fd2be2ef883ca1649e21fd28c1a35a0ffffce6035fd75.json rename to .sqlx/query-a3fa1c6271b85d23d70116363f19144190120c13752364bc3b78a92a08bd9157.json index 7a97455..f83d560 100644 --- a/.sqlx/query-c3a3348990d0fea3225fd2be2ef883ca1649e21fd28c1a35a0ffffce6035fd75.json +++ b/.sqlx/query-a3fa1c6271b85d23d70116363f19144190120c13752364bc3b78a92a08bd9157.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_ordering_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_ordering_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": "c3a3348990d0fea3225fd2be2ef883ca1649e21fd28c1a35a0ffffce6035fd75" + "hash": "a3fa1c6271b85d23d70116363f19144190120c13752364bc3b78a92a08bd9157" } diff --git a/src/ordering/adapters/output/db/category_name_exists_for_store.rs b/src/ordering/adapters/output/db/category_name_exists_for_store.rs index e6e17f2..7ef5283 100644 --- a/src/ordering/adapters/output/db/category_name_exists_for_store.rs +++ b/src/ordering/adapters/output/db/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/ordering/adapters/output/db/product_view.rs b/src/ordering/adapters/output/db/product_view.rs index 84aa7e1..6a990c6 100644 --- a/src/ordering/adapters/output/db/product_view.rs +++ b/src/ordering/adapters/output/db/product_view.rs @@ -293,22 +293,20 @@ impl ViewRepository for OrderingDBPostgresAdapter { 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,160 @@ impl Query for OrderingDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + db::migrate::*, + ordering::{ + application::{ + port::output::full_text_search::add_product_to_store::*, + services::{ + add_product_service::*, update_product_service::*, + MockOrderingServicesInterface, + }, + }, + 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_ordering_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 = OrderingDBPostgresAdapter::new(db.pool.clone()); + + let store = Store::default(); + crate::ordering::adapters::output::db::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::ordering::adapters::output::db::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 = MockOrderingServicesInterface::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(), + OrderingCommand::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(), + OrderingCommand::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; + } +} diff --git a/src/ordering/application/services/add_product_service.rs b/src/ordering/application/services/add_product_service.rs index 3eb4e7e..e9ec342 100644 --- a/src/ordering/application/services/add_product_service.rs +++ b/src/ordering/application/services/add_product_service.rs @@ -23,7 +23,6 @@ use crate::ordering::{ 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(OrderingError::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(OrderingError::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,7 @@ pub mod tests { use super::*; use crate::ordering::domain::add_product_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_product_service( times: Option, @@ -135,7 +125,7 @@ pub mod tests { .image(cmd.image().as_ref().map(|s| s.to_string())) .sku_able(cmd.sku_able().clone()) .category_id(cmd.category_id().clone()) - .product_id(UUID.clone()) + .product_id(*cmd.product_id()) .price(cmd.price().clone()) .quantity(cmd.quantity().clone()) .added_by_user(cmd.adding_by().clone()) @@ -165,7 +155,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(); @@ -177,7 +166,7 @@ pub mod tests { assert_eq!(res.price(), cmd.price()); assert_eq!(res.added_by_user(), cmd.adding_by()); assert_eq!(res.category_id(), cmd.category_id()); - assert_eq!(res.product_id(), &UUID); + assert_eq!(res.product_id(), cmd.product_id()); assert_eq!(res.quantity(), cmd.quantity()); } @@ -190,7 +179,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 +203,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/ordering/domain/add_category_command.rs b/src/ordering/domain/add_category_command.rs index ec116de..f64d222 100644 --- a/src/ordering/domain/add_category_command.rs +++ b/src/ordering/domain/add_category_command.rs @@ -1,95 +1,3 @@ -//// 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; -// -//#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -//pub enum AddCategoryCommandError { -// NameIsEmpty, -//} -// -//#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] -//pub struct AddCategoryCommand { -// name: String, -// description: Option, -// store_id: Uuid, -// adding_by: Uuid, -//} -// -//impl AddCategoryCommand { -// pub fn new( -// name: String, -// description: Option, -// store_id: Uuid, -// adding_by: Uuid, -// ) -> Result { -// let description: Option = if let Some(description) = description { -// let description = description.trim(); -// if description.is_empty() { -// None -// } else { -// Some(description.to_owned()) -// } -// } else { -// None -// }; -// -// let name = name.trim().to_owned(); -// if name.is_empty() { -// return Err(AddCategoryCommandError::NameIsEmpty); -// } -// -// Ok(Self { -// name, -// store_id, -// description, -// adding_by, -// }) -// } -//} -// -//#[cfg(test)] -//mod tests { -// use super::*; -// -// use crate::utils::uuid::tests::UUID; -// -// #[test] -// fn test_cmd() { -// let name = "foo"; -// let description = "bar"; -// let adding_by = UUID; -// let store_id = Uuid::new_v4(); -// -// // description = None -// let cmd = AddCategoryCommand::new(name.into(), None, store_id, adding_by).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(); -// 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) -// ) -// } -//} -// -// // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/src/ordering/domain/add_product_command.rs b/src/ordering/domain/add_product_command.rs index f347838..5484ebd 100644 --- a/src/ordering/domain/add_product_command.rs +++ b/src/ordering/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/ordering/domain/update_product_command.rs b/src/ordering/domain/update_product_command.rs index f158f07..ee399b9 100644 --- a/src/ordering/domain/update_product_command.rs +++ b/src/ordering/domain/update_product_command.rs @@ -95,6 +95,55 @@ pub mod tests { use crate::types::quantity::*; use crate::utils::uuid::tests::UUID; + pub fn get_command_with_product(product: Product) -> 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) + .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 = "foo"; let adding_by = UUID; From eba2f7204c6badd8b145a951a5bb58c45069503f Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 24 Sep 2024 17:26:28 +0530 Subject: [PATCH 7/8] feat: ordering: Order ID is provided by caller --- ...8be7320f99002b9a9279981053a5062b0ed8.json} | 10 ++- ...58cca61e4853bee64e5afc44679e685cd744.json} | 5 +- ...c785e41473116ceda238a8089bcb33588e3d.json} | 6 +- ...240723132531_cqrs_ordering_order_query.sql | 3 +- .../adapters/output/db/order_id_exists.rs | 6 +- src/ordering/adapters/output/db/order_view.rs | 23 ++++-- .../application/services/add_order_service.rs | 54 +++++++++---- .../services/delete_order_service.rs | 3 +- .../services/update_order_service.rs | 7 +- src/ordering/domain/add_order_command.rs | 79 ++++++++----------- src/ordering/domain/order_aggregate.rs | 12 +-- src/ordering/domain/order_deleted_event.rs | 3 +- src/ordering/domain/order_updated_event.rs | 3 +- src/ordering/domain/update_order_command.rs | 73 +++++++---------- 14 files changed, 155 insertions(+), 132 deletions(-) rename .sqlx/{query-dbe1e41f04a81b2a504b9179911201ec52340d09d45041addd54eb349af82488.json => query-4f8a2294b40e4285fa6d4c7bcf648be7320f99002b9a9279981053a5062b0ed8.json} (62%) rename .sqlx/{query-a342003149f2991ef0280d008f18e664c96299edfd9a11d08487e4db10a10e8b.json => query-7e2e91418e136fc5091b27c5f61158cca61e4853bee64e5afc44679e685cd744.json} (60%) rename .sqlx/{query-bc7d17aab113d0519c53e5f612116a6e72bb0007a298cdba17f45f4b8bed5f56.json => query-876ca2c177175439a1604a2d5aeec785e41473116ceda238a8089bcb33588e3d.json} (68%) diff --git a/.sqlx/query-dbe1e41f04a81b2a504b9179911201ec52340d09d45041addd54eb349af82488.json b/.sqlx/query-4f8a2294b40e4285fa6d4c7bcf648be7320f99002b9a9279981053a5062b0ed8.json similarity index 62% rename from .sqlx/query-dbe1e41f04a81b2a504b9179911201ec52340d09d45041addd54eb349af82488.json rename to .sqlx/query-4f8a2294b40e4285fa6d4c7bcf648be7320f99002b9a9279981053a5062b0ed8.json index 804ef95..56a2135 100644 --- a/.sqlx/query-dbe1e41f04a81b2a504b9179911201ec52340d09d45041addd54eb349af82488.json +++ b/.sqlx/query-4f8a2294b40e4285fa6d4c7bcf648be7320f99002b9a9279981053a5062b0ed8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \n customer_name,\n order_id,\n created_time,\n deleted\n FROM\n cqrs_ordering_order_query\n WHERE\n order_id = $1;", + "query": "SELECT \n customer_name,\n order_id,\n created_time,\n store_id,\n deleted\n FROM\n cqrs_ordering_order_query\n WHERE\n order_id = $1;", "describe": { "columns": [ { @@ -20,6 +20,11 @@ }, { "ordinal": 3, + "name": "store_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, "name": "deleted", "type_info": "Bool" } @@ -30,11 +35,12 @@ ] }, "nullable": [ + true, false, false, false, false ] }, - "hash": "dbe1e41f04a81b2a504b9179911201ec52340d09d45041addd54eb349af82488" + "hash": "4f8a2294b40e4285fa6d4c7bcf648be7320f99002b9a9279981053a5062b0ed8" } diff --git a/.sqlx/query-a342003149f2991ef0280d008f18e664c96299edfd9a11d08487e4db10a10e8b.json b/.sqlx/query-7e2e91418e136fc5091b27c5f61158cca61e4853bee64e5afc44679e685cd744.json similarity index 60% rename from .sqlx/query-a342003149f2991ef0280d008f18e664c96299edfd9a11d08487e4db10a10e8b.json rename to .sqlx/query-7e2e91418e136fc5091b27c5f61158cca61e4853bee64e5afc44679e685cd744.json index 8fed07b..d61cb85 100644 --- a/.sqlx/query-a342003149f2991ef0280d008f18e664c96299edfd9a11d08487e4db10a10e8b.json +++ b/.sqlx/query-7e2e91418e136fc5091b27c5f61158cca61e4853bee64e5afc44679e685cd744.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO cqrs_ordering_order_query (\n version,\n customer_name,\n order_id,\n created_time,\n deleted\n\n ) VALUES (\n $1, $2, $3, $4, $5\n );", + "query": "INSERT INTO cqrs_ordering_order_query (\n version,\n customer_name,\n order_id,\n created_time,\n store_id,\n deleted\n\n ) VALUES (\n $1, $2, $3, $4, $5, $6\n );", "describe": { "columns": [], "parameters": { @@ -9,10 +9,11 @@ "Text", "Uuid", "Timestamptz", + "Uuid", "Bool" ] }, "nullable": [] }, - "hash": "a342003149f2991ef0280d008f18e664c96299edfd9a11d08487e4db10a10e8b" + "hash": "7e2e91418e136fc5091b27c5f61158cca61e4853bee64e5afc44679e685cd744" } diff --git a/.sqlx/query-bc7d17aab113d0519c53e5f612116a6e72bb0007a298cdba17f45f4b8bed5f56.json b/.sqlx/query-876ca2c177175439a1604a2d5aeec785e41473116ceda238a8089bcb33588e3d.json similarity index 68% rename from .sqlx/query-bc7d17aab113d0519c53e5f612116a6e72bb0007a298cdba17f45f4b8bed5f56.json rename to .sqlx/query-876ca2c177175439a1604a2d5aeec785e41473116ceda238a8089bcb33588e3d.json index bcbb2e8..fff3263 100644 --- a/.sqlx/query-bc7d17aab113d0519c53e5f612116a6e72bb0007a298cdba17f45f4b8bed5f56.json +++ b/.sqlx/query-876ca2c177175439a1604a2d5aeec785e41473116ceda238a8089bcb33588e3d.json @@ -1,18 +1,18 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_ordering_order_query\n SET\n version = $1,\n customer_name = $2,\n order_id = $3,\n created_time = $4,\n deleted = $5;", + "query": "UPDATE\n cqrs_ordering_order_query\n SET\n version = $1,\n customer_name = $2,\n created_time = $3,\n store_id= $4,\n deleted = $5;", "describe": { "columns": [], "parameters": { "Left": [ "Int8", "Text", - "Uuid", "Timestamptz", + "Uuid", "Bool" ] }, "nullable": [] }, - "hash": "bc7d17aab113d0519c53e5f612116a6e72bb0007a298cdba17f45f4b8bed5f56" + "hash": "876ca2c177175439a1604a2d5aeec785e41473116ceda238a8089bcb33588e3d" } diff --git a/migrations/20240723132531_cqrs_ordering_order_query.sql b/migrations/20240723132531_cqrs_ordering_order_query.sql index 3b4b094..7ab2796 100644 --- a/migrations/20240723132531_cqrs_ordering_order_query.sql +++ b/migrations/20240723132531_cqrs_ordering_order_query.sql @@ -8,8 +8,9 @@ CREATE TABLE IF NOT EXISTS cqrs_ordering_order_query created_time timestamp with time zone DEFAULT (CURRENT_TIMESTAMP) NOT NULL, order_id UUID NOT NULL UNIQUE, + store_id UUID NOT NULL, - customer_name TEXT NOT NULL, + customer_name TEXT, deleted BOOLEAN NOT NULL DEFAULT FALSE, diff --git a/src/ordering/adapters/output/db/order_id_exists.rs b/src/ordering/adapters/output/db/order_id_exists.rs index 5f0f634..5f3bbd6 100644 --- a/src/ordering/adapters/output/db/order_id_exists.rs +++ b/src/ordering/adapters/output/db/order_id_exists.rs @@ -41,15 +41,17 @@ pub mod tests { "INSERT INTO cqrs_ordering_order_query ( version, order_id, + store_id, customer_name, created_time, deleted ) VALUES ( - $1, $2, $3, $4, $5 + $1, $2, $3, $4, $5, $6 );", 1, order.order_id(), - order.customer_name(), + order.store_id(), + order.customer_name().as_ref().map(|s| s.as_str()), order.created_time(), order.deleted().clone(), ) diff --git a/src/ordering/adapters/output/db/order_view.rs b/src/ordering/adapters/output/db/order_view.rs index 6714241..b19c6b0 100644 --- a/src/ordering/adapters/output/db/order_view.rs +++ b/src/ordering/adapters/output/db/order_view.rs @@ -23,8 +23,9 @@ pub const NEW_ORDER_NON_UUID: &str = "new_order_non_uuid-asdfa"; // be designed to reflect the response dto that will be returned to a user. #[derive(Debug, Serialize, Deserialize)] pub struct OrderView { - customer_name: String, + customer_name: Option, order_id: Uuid, + store_id: Uuid, created_time: OffsetDateTime, deleted: bool, @@ -36,6 +37,7 @@ impl Default for OrderView { customer_name: Default::default(), order_id: Default::default(), created_time: OffsetDateTime::now_utc(), + store_id: Default::default(), deleted: false, } } @@ -46,6 +48,7 @@ impl From for Order { OrderBuilder::default() .customer_name(v.customer_name) .order_id(v.order_id) + .store_id(v.store_id) .created_time(v.created_time) .deleted(v.deleted) .build() @@ -60,16 +63,18 @@ impl View for OrderView { fn update(&mut self, event: &EventEnvelope) { match &event.payload { OrderingEvent::OrderAdded(val) => { - self.customer_name = val.order().customer_name().into(); + self.customer_name = val.order().customer_name().clone(); self.order_id = *val.order().order_id(); self.created_time = val.order().created_time().clone(); + self.store_id = *val.order().store_id(); self.deleted = false; } OrderingEvent::OrderUpdated(e) => { let new = e.new_order(); - self.customer_name = new.customer_name().into(); + self.customer_name = new.customer_name().clone(); self.order_id = *new.order_id(); + self.store_id = *new.store_id(); self.created_time = new.created_time().clone(); } OrderingEvent::OrderDeleted(_) => self.deleted = true, @@ -93,6 +98,7 @@ impl ViewRepository for OrderingDBPostgresAdapter { customer_name, order_id, created_time, + store_id, deleted FROM cqrs_ordering_order_query @@ -122,6 +128,7 @@ impl ViewRepository for OrderingDBPostgresAdapter { customer_name, order_id, created_time, + store_id, deleted FROM cqrs_ordering_order_query @@ -170,15 +177,17 @@ impl ViewRepository for OrderingDBPostgresAdapter { customer_name, order_id, created_time, + store_id, deleted ) VALUES ( - $1, $2, $3, $4, $5 + $1, $2, $3, $4, $5, $6 );", version, view.customer_name, view.order_id, view.created_time, + view.store_id, view.deleted, ) .execute(&self.pool) @@ -194,13 +203,13 @@ impl ViewRepository for OrderingDBPostgresAdapter { SET version = $1, customer_name = $2, - order_id = $3, - created_time = $4, + created_time = $3, + store_id= $4, deleted = $5;", version, view.customer_name, - view.order_id, view.created_time, + view.store_id, view.deleted, ) .execute(&self.pool) diff --git a/src/ordering/application/services/add_order_service.rs b/src/ordering/application/services/add_order_service.rs index e9893ee..04379e9 100644 --- a/src/ordering/application/services/add_order_service.rs +++ b/src/ordering/application/services/add_order_service.rs @@ -11,10 +11,9 @@ use time::OffsetDateTime; use super::errors::*; use crate::ordering::{ - application::port::output::db::order_id_exists::*, + application::port::output::db::{order_id_exists::*, store_id_exists::*}, domain::{add_order_command::*, order_added_event::*, order_aggregate::*}, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -27,27 +26,33 @@ pub type AddOrderServiceObj = Arc; #[derive(Clone, Builder)] pub struct AddOrderService { db_order_id_exists: OrderIDExistsDBPortObj, - get_uuid: GetUUIDInterfaceObj, + db_store_id_exists: StoreIDExistsDBPortObj, } #[async_trait::async_trait] impl AddOrderUseCase for AddOrderService { async fn add_order(&self, cmd: AddOrderCommand) -> OrderingResult { - let mut order_id = self.get_uuid.get_uuid(); + if !self + .db_store_id_exists + .store_id_exists(cmd.store_id()) + .await? + { + return Err(OrderingError::StoreIDNotFound); + } - loop { - if self.db_order_id_exists.order_id_exists(&order_id).await? { - order_id = self.get_uuid.get_uuid(); - continue; - } else { - break; - } + if self + .db_order_id_exists + .order_id_exists(cmd.order_id()) + .await? + { + return Err(OrderingError::DuplicateOrderID); } let order = OrderBuilder::default() .created_time(OffsetDateTime::now_utc()) - .customer_name(cmd.customer_name().into()) - .order_id(order_id) + .customer_name(cmd.customer_name().clone()) + .order_id(*cmd.order_id()) + .store_id(*cmd.store_id()) .deleted(false) .build() .unwrap(); @@ -65,8 +70,7 @@ pub mod tests { use super::*; use crate::ordering::domain::order_added_event::tests::get_added_order_event_from_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_order_service( times: Option, @@ -92,7 +96,7 @@ pub mod tests { let s = AddOrderServiceBuilder::default() .db_order_id_exists(mock_order_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); @@ -100,5 +104,23 @@ pub mod tests { assert_eq!(res.order().customer_name(), cmd.customer_name()); assert!(!res.order().deleted()); assert_eq!(res.added_by_user(), cmd.adding_by()); + assert_eq!(res.order().store_id(), cmd.store_id()); + assert_eq!(res.order().order_id(), cmd.order_id()); + } + + #[actix_rt::test] + async fn test_service_store_id_doesnt_exist() { + let cmd = AddOrderCommand::get_cmd(); + + let s = AddOrderServiceBuilder::default() + .db_order_id_exists(mock_order_id_exists_db_port_false(IS_NEVER_CALLED)) + .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + assert_eq!( + s.add_order(cmd.clone()).await, + Err(OrderingError::StoreIDNotFound) + ); } } diff --git a/src/ordering/application/services/delete_order_service.rs b/src/ordering/application/services/delete_order_service.rs index 8536f93..2e26cc5 100644 --- a/src/ordering/application/services/delete_order_service.rs +++ b/src/ordering/application/services/delete_order_service.rs @@ -42,8 +42,9 @@ impl DeleteOrderUseCase for DeleteOrderService { let deleted_order = OrderBuilder::default() .created_time(cmd.order().created_time().clone()) - .customer_name(cmd.order().customer_name().into()) + .customer_name(cmd.order().customer_name().clone()) .order_id(*cmd.order().order_id()) + .store_id(*cmd.order().store_id()) .deleted(true) .build() .unwrap(); diff --git a/src/ordering/application/services/update_order_service.rs b/src/ordering/application/services/update_order_service.rs index 667628e..35259bb 100644 --- a/src/ordering/application/services/update_order_service.rs +++ b/src/ordering/application/services/update_order_service.rs @@ -7,14 +7,12 @@ use std::sync::Arc; use derive_builder::Builder; use mockall::predicate::*; use mockall::*; -use time::OffsetDateTime; use super::errors::*; use crate::ordering::{ application::port::output::db::order_id_exists::*, domain::{order_aggregate::*, order_updated_event::*, update_order_command::*}, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -42,7 +40,8 @@ impl UpdateOrderUseCase for UpdateOrderService { let new_order = OrderBuilder::default() .created_time(cmd.created_time().clone()) - .customer_name(cmd.customer_name().into()) + .customer_name(cmd.customer_name().clone()) + .store_id(*cmd.old_order().store_id()) .order_id(*cmd.old_order().order_id()) .deleted(false) .build() @@ -62,8 +61,8 @@ pub mod tests { use super::*; use crate::ordering::domain::order_updated_event::tests::get_updated_order_event_from_command; + use crate::tests::bdd::*; use crate::utils::uuid::tests::UUID; - use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; pub fn mock_update_order_service( times: Option, diff --git a/src/ordering/domain/add_order_command.rs b/src/ordering/domain/add_order_command.rs index 72a3526..dda5df9 100644 --- a/src/ordering/domain/add_order_command.rs +++ b/src/ordering/domain/add_order_command.rs @@ -9,8 +9,6 @@ use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; -use crate::utils::string::empty_string_err; - #[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum AddOrderCommandError { CustomerNameIsEmpty, @@ -19,36 +17,33 @@ pub enum AddOrderCommandError { #[derive( Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, )] -pub struct UnvalidatedAddOrderCommand { +pub struct AddOrderCommand { adding_by: Uuid, #[builder(default = "OffsetDateTime::now_utc()")] created_time: OffsetDateTime, - customer_name: String, + #[builder(setter(custom))] + customer_name: Option, + store_id: Uuid, + order_id: Uuid, } -impl UnvalidatedAddOrderCommand { - pub fn validate(self) -> Result { - let customer_name = empty_string_err( - self.customer_name, - AddOrderCommandError::CustomerNameIsEmpty, - )?; - - Ok(AddOrderCommand { - created_time: self.created_time, - customer_name, - adding_by: self.adding_by, - }) +impl AddOrderCommandBuilder { + pub fn customer_name(&mut self, customer_name: Option) -> &mut Self { + self.customer_name = if let Some(customer_name) = customer_name { + let customer_name = customer_name.trim(); + if customer_name.is_empty() { + Some(None) + } else { + Some(Some(customer_name.to_string())) + } + } else { + Some(None) + }; + self } } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] -pub struct AddOrderCommand { - created_time: OffsetDateTime, - customer_name: String, - adding_by: Uuid, -} - #[cfg(test)] mod tests { use time::macros::datetime; @@ -62,14 +57,14 @@ mod tests { let customer_name = "foo"; let adding_by = UUID; - UnvalidatedAddOrderCommandBuilder::default() - .customer_name(customer_name.into()) + AddOrderCommandBuilder::default() + .customer_name(Some(customer_name.into())) .adding_by(adding_by) .created_time(datetime!(1970-01-01 0:00 UTC)) + .store_id(UUID) + .order_id(UUID) .build() .unwrap() - .validate() - .unwrap() } } @@ -78,31 +73,27 @@ mod tests { let customer_name = "foo"; let adding_by = UUID; - let cmd = UnvalidatedAddOrderCommandBuilder::default() - .customer_name(customer_name.into()) + let _cmd = AddOrderCommandBuilder::default() + .customer_name(Some(customer_name.into())) .adding_by(adding_by) + .store_id(UUID) + .order_id(UUID) .build() - .unwrap() - .validate() .unwrap(); - - assert_eq!(*cmd.adding_by(), adding_by); - assert_eq!(cmd.customer_name(), customer_name); } #[test] fn test_cmd_customer_name_empty() { let customer_name = ""; - let adding_by = UUID; - assert_eq!( - UnvalidatedAddOrderCommandBuilder::default() - .customer_name(customer_name.into()) - .adding_by(adding_by) - .build() - .unwrap() - .validate(), - Err(AddOrderCommandError::CustomerNameIsEmpty) - ); + let cmd = AddOrderCommandBuilder::default() + .customer_name(Some(customer_name.into())) + .adding_by(UUID) + .store_id(UUID) + .order_id(UUID) + .build() + .unwrap(); + + assert!(cmd.customer_name().is_none()); } } diff --git a/src/ordering/domain/order_aggregate.rs b/src/ordering/domain/order_aggregate.rs index e7c0f29..c81cbc4 100644 --- a/src/ordering/domain/order_aggregate.rs +++ b/src/ordering/domain/order_aggregate.rs @@ -21,11 +21,11 @@ use crate::ordering::{ pub struct Order { #[builder(default = "OffsetDateTime::now_utc()")] created_time: OffsetDateTime, - // kot_ids: Vec, + store_id: Uuid, order_id: Uuid, #[builder(default = "false")] deleted: bool, - customer_name: String, + customer_name: Option, } impl Default for Order { @@ -33,8 +33,9 @@ impl Default for Order { Self { created_time: OffsetDateTime::now_utc(), order_id: Default::default(), + store_id: Default::default(), deleted: false, - customer_name: Default::default(), + customer_name: None, } } } @@ -51,8 +52,9 @@ mod tests { OrderBuilder::default() .created_time(cmd.created_time().clone()) - .customer_name("test_product".into()) - .order_id(UUID) + .customer_name(Some("test_product".into())) + .order_id(*cmd.order_id()) + .store_id(*cmd.store_id()) .build() .unwrap() } diff --git a/src/ordering/domain/order_deleted_event.rs b/src/ordering/domain/order_deleted_event.rs index 981b204..1c760d7 100644 --- a/src/ordering/domain/order_deleted_event.rs +++ b/src/ordering/domain/order_deleted_event.rs @@ -27,8 +27,9 @@ pub mod tests { pub fn get_deleted_order_event_from_command(cmd: &DeleteOrderCommand) -> OrderDeletedEvent { let deleted_order = OrderBuilder::default() .created_time(cmd.order().created_time().clone()) - .customer_name(cmd.order().customer_name().into()) + .customer_name(cmd.order().customer_name().clone()) .order_id(*cmd.order().order_id()) + .store_id(*cmd.order().store_id()) .deleted(true) .build() .unwrap(); diff --git a/src/ordering/domain/order_updated_event.rs b/src/ordering/domain/order_updated_event.rs index 594a5ba..930529f 100644 --- a/src/ordering/domain/order_updated_event.rs +++ b/src/ordering/domain/order_updated_event.rs @@ -28,8 +28,9 @@ pub mod tests { pub fn get_updated_order_event_from_command(cmd: &UpdateOrderCommand) -> OrderUpdatedEvent { let new_order = OrderBuilder::default() .created_time(cmd.created_time().clone()) - .customer_name(cmd.customer_name().into()) + .customer_name(cmd.customer_name().clone()) .order_id(*cmd.old_order().order_id()) + .store_id(*cmd.old_order().store_id()) .build() .unwrap(); diff --git a/src/ordering/domain/update_order_command.rs b/src/ordering/domain/update_order_command.rs index 3029043..4a035c5 100644 --- a/src/ordering/domain/update_order_command.rs +++ b/src/ordering/domain/update_order_command.rs @@ -10,7 +10,6 @@ use time::OffsetDateTime; use uuid::Uuid; use super::order_aggregate::*; -use crate::utils::string::empty_string_err; #[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum UpdateOrderCommandError { @@ -20,39 +19,33 @@ pub enum UpdateOrderCommandError { #[derive( Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, )] -pub struct UnvalidatedUpdateOrderCommand { +pub struct UpdateOrderCommand { adding_by: Uuid, #[builder(default = "OffsetDateTime::now_utc()")] created_time: OffsetDateTime, - customer_name: String, + #[builder(setter(custom))] + customer_name: Option, + old_order: Order, } -impl UnvalidatedUpdateOrderCommand { - pub fn validate(self) -> Result { - let customer_name = empty_string_err( - self.customer_name, - UpdateOrderCommandError::CustomerNameIsEmpty, - )?; - - Ok(UpdateOrderCommand { - created_time: self.created_time, - customer_name, - adding_by: self.adding_by, - old_order: self.old_order, - }) +impl UpdateOrderCommandBuilder { + pub fn customer_name(&mut self, customer_name: Option) -> &mut Self { + self.customer_name = if let Some(customer_name) = customer_name { + let customer_name = customer_name.trim(); + if customer_name.is_empty() { + Some(None) + } else { + Some(Some(customer_name.to_string())) + } + } else { + Some(None) + }; + self } } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] -pub struct UpdateOrderCommand { - created_time: OffsetDateTime, - customer_name: String, - adding_by: Uuid, - old_order: Order, -} - #[cfg(test)] mod tests { use time::macros::datetime; @@ -66,15 +59,13 @@ mod tests { let customer_name = "foo-new-name"; let adding_by = UUID; - UnvalidatedUpdateOrderCommandBuilder::default() - .customer_name(customer_name.into()) + UpdateOrderCommandBuilder::default() + .customer_name(Some(customer_name.into())) .adding_by(adding_by) .created_time(datetime!(1970-01-01 0:00 UTC)) .old_order(Order::default()) .build() .unwrap() - .validate() - .unwrap() } } @@ -84,17 +75,15 @@ mod tests { let adding_by = UUID; let old_order = Order::default(); - let cmd = UnvalidatedUpdateOrderCommandBuilder::default() - .customer_name(customer_name.into()) + let cmd = UpdateOrderCommandBuilder::default() + .customer_name(Some(customer_name.into())) .adding_by(adding_by) .old_order(old_order.clone()) .build() - .unwrap() - .validate() .unwrap(); assert_eq!(*cmd.adding_by(), adding_by); - assert_eq!(cmd.customer_name(), customer_name); + assert_eq!(cmd.customer_name().as_ref().unwrap(), customer_name); assert_eq!(cmd.old_order(), &old_order); } @@ -103,15 +92,13 @@ mod tests { let customer_name = ""; let adding_by = UUID; - assert_eq!( - UnvalidatedUpdateOrderCommandBuilder::default() - .customer_name(customer_name.into()) - .adding_by(adding_by) - .old_order(Order::default()) - .build() - .unwrap() - .validate(), - Err(UpdateOrderCommandError::CustomerNameIsEmpty) - ); + let cmd = UpdateOrderCommandBuilder::default() + .customer_name(Some(customer_name.into())) + .adding_by(adding_by) + .old_order(Order::default()) + .build() + .unwrap(); + + assert!(cmd.customer_name().is_none()); } } From 88f4dd6825ba02298d6c16f4d5c547af6a6fcdc0 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 24 Sep 2024 17:37:03 +0530 Subject: [PATCH 8/8] feat: ordering: Order view tests --- src/ordering/adapters/output/db/order_view.rs | 184 ++++++++++++++++++ .../adapters/output/db/product_view.rs | 18 +- 2 files changed, 190 insertions(+), 12 deletions(-) diff --git a/src/ordering/adapters/output/db/order_view.rs b/src/ordering/adapters/output/db/order_view.rs index b19c6b0..b075b57 100644 --- a/src/ordering/adapters/output/db/order_view.rs +++ b/src/ordering/adapters/output/db/order_view.rs @@ -236,3 +236,187 @@ impl Query for OrderingDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + use time::macros::datetime; + + use crate::{ + db::migrate::*, + ordering::{ + application::services::{ + add_order_service::*, delete_order_service::DeleteOrderServiceBuilder, + update_order_service::*, MockOrderingServicesInterface, + }, + domain::{ + add_order_command::*, commands::*, delete_order_command::DeleteOrderCommandBuilder, + events::*, order_aggregate::*, store_aggregate::*, update_order_command::*, + }, + }, + tests::bdd::*, + utils::{ + random_string::GenerateRandomStringInterface, + uuid::{tests::UUID, *}, + }, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_ordering_order_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 = OrderingDBPostgresAdapter::new(db.pool.clone()); + + let store = Store::default(); + crate::ordering::adapters::output::db::store_id_exists::tests::create_dummy_store_record( + &store, &db, + ) + .await; + + let queries: Vec>> = vec![Box::new(db.clone())]; + + let mut mock_services = MockOrderingServicesInterface::new(); + + let db2 = Arc::new(db.clone()); + mock_services + .expect_add_order() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddOrderServiceBuilder::default() + .db_order_id_exists(db2.clone()) + .db_store_id_exists(db2.clone()) + .build() + .unwrap(), + ) + }); + + let db2 = Arc::new(db.clone()); + mock_services + .expect_update_order() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateOrderServiceBuilder::default() + .db_order_id_exists(db2.clone()) + .build() + .unwrap(), + ) + }); + + let db2 = Arc::new(db.clone()); + mock_services + .expect_delete_order() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + DeleteOrderServiceBuilder::default() + .db_order_id_exists(db2.clone()) + .build() + .unwrap(), + ) + }); + + let (cqrs, order_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 uuid = GenerateUUID {}; + + let cmd = AddOrderCommandBuilder::default() + .customer_name(None) + .adding_by(UUID) + .created_time(datetime!(1970-01-01 0:00 UTC)) + .store_id(*store.store_id()) + .order_id(uuid.get_uuid()) + .build() + .unwrap(); + + cqrs.execute( + &cmd.order_id().to_string(), + OrderingCommand::AddOrder(cmd.clone()), + ) + .await + .unwrap(); + + let order = order_query + .load(&(*cmd.order_id()).to_string()) + .await + .unwrap() + .unwrap(); + let order: Order = order.into(); + assert_eq!(order.customer_name(), cmd.customer_name()); + assert_eq!(order.store_id(), cmd.store_id()); + assert_eq!(order.order_id(), cmd.order_id()); + assert!(!order.deleted()); + + // update + let update_order_cmd = UpdateOrderCommandBuilder::default() + .customer_name(Some(rand.get_random(10))) + .adding_by(UUID) + .created_time(datetime!(1970-01-01 0:00 UTC)) + .old_order(order.clone()) + .build() + .unwrap(); + + cqrs.execute( + &cmd.order_id().to_string(), + OrderingCommand::UpdateOrder(update_order_cmd.clone()), + ) + .await + .unwrap(); + let order = order_query + .load(&(*cmd.order_id()).to_string()) + .await + .unwrap() + .unwrap(); + let order: Order = order.into(); + assert_eq!(order.customer_name(), update_order_cmd.customer_name()); + assert_eq!(order.store_id(), update_order_cmd.old_order().store_id()); + assert_eq!(order.order_id(), update_order_cmd.old_order().order_id()); + assert!(!order.deleted()); + + let delete_order_cmd = DeleteOrderCommandBuilder::default() + .adding_by(UUID) + .order(order.clone()) + .build() + .unwrap(); + cqrs.execute( + &cmd.order_id().to_string(), + OrderingCommand::DeleteOrder(delete_order_cmd.clone()), + ) + .await + .unwrap(); + let order = order_query + .load(&(*cmd.order_id()).to_string()) + .await + .unwrap() + .unwrap(); + let order: Order = order.into(); + assert_eq!( + order.customer_name(), + delete_order_cmd.order().customer_name() + ); + assert_eq!(order.store_id(), delete_order_cmd.order().store_id()); + assert_eq!(order.order_id(), delete_order_cmd.order().order_id()); + assert!(order.deleted()); + + settings.drop_db().await; + } +} diff --git a/src/ordering/adapters/output/db/product_view.rs b/src/ordering/adapters/output/db/product_view.rs index 6a990c6..c192122 100644 --- a/src/ordering/adapters/output/db/product_view.rs +++ b/src/ordering/adapters/output/db/product_view.rs @@ -365,17 +365,13 @@ mod tests { }, }, 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, *}, + add_product_command::tests::get_command, category_aggregate::*, commands::*, + product_aggregate::*, store_aggregate::*, + update_product_command::tests::get_command_with_product, }, }, tests::bdd::*, - utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, + utils::uuid::tests::UUID, }; use std::sync::Arc; @@ -451,8 +447,6 @@ mod tests { Arc::new(db.clone()), ); - let rand = crate::utils::random_string::GenerateRandomString {}; - let cmd = get_command(); cqrs.execute( &cmd.product_id().to_string(), @@ -474,7 +468,7 @@ mod tests { assert_eq!(product.quantity(), cmd.quantity()); assert_eq!(product.sku_able(), cmd.sku_able()); assert_eq!(product.price(), cmd.price()); - assert!(!store.deleted()); + assert!(!product.deleted()); let update_product_cmd = get_command_with_product(product.clone()); cqrs.execute( @@ -499,7 +493,7 @@ mod tests { 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()); + assert!(!product.deleted()); settings.drop_db().await; }