From 7c3676e84dc570742aa58fda1e287561e370715b Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 24 Sep 2024 16:07:21 +0530 Subject: [PATCH] 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;