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