From 0f29902b4b5c39609b5cdbeff7aa4b8fbc82d4b4 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 16 Jul 2024 15:41:16 +0530 Subject: [PATCH] fix: rm customization from product aggregate --- ...715113708_cqrs_inventory_product_query.sql | 1 - .../db/postgres/customization_id_exists.rs | 10 +- .../output/db/postgres/product_id_exists.rs | 26 +- .../product_name_exists_for_category.rs | 1 - .../output/db/postgres/product_view.rs | 199 ++------- .../customization_name_exists_for_product.rs | 2 +- .../services/add_product_service.rs | 100 +---- src/inventory/domain/add_product_command.rs | 128 +----- src/inventory/domain/product_added_event.rs | 20 +- src/inventory/domain/product_aggregate.rs | 41 +- src/inventory/domain/product_updated_event.rs | 69 +++ .../domain/update_product_command.rs | 400 ++++++++++++++++++ 12 files changed, 545 insertions(+), 452 deletions(-) create mode 100644 src/inventory/domain/product_updated_event.rs create mode 100644 src/inventory/domain/update_product_command.rs diff --git a/migrations/20240715113708_cqrs_inventory_product_query.sql b/migrations/20240715113708_cqrs_inventory_product_query.sql index 18cb0d4..8fdc3fd 100644 --- a/migrations/20240715113708_cqrs_inventory_product_query.sql +++ b/migrations/20240715113708_cqrs_inventory_product_query.sql @@ -23,7 +23,6 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_product_query category_id UUID NOT NULL, deleted BOOLEAN NOT NULL DEFAULT FALSE, - customizations_available BOOLEAN NOT NULL DEFAULT FALSE, UNIQUE(category_id, name), PRIMARY KEY (product_id) diff --git a/src/inventory/adapters/output/db/postgres/customization_id_exists.rs b/src/inventory/adapters/output/db/postgres/customization_id_exists.rs index f919727..0bc7b25 100644 --- a/src/inventory/adapters/output/db/postgres/customization_id_exists.rs +++ b/src/inventory/adapters/output/db/postgres/customization_id_exists.rs @@ -32,8 +32,7 @@ impl CustomizationIDExistsDBPort for InventoryDBPostgresAdapter { pub mod tests { use super::*; - use crate::inventory::domain::add_product_command::tests::get_customizations; - use crate::inventory::domain::product_aggregate::*; + use crate::inventory::domain::customization_aggregate::*; use crate::utils::uuid::tests::UUID; #[actix_rt::test] @@ -46,15 +45,14 @@ pub mod tests { .unwrap(), ); - let customization = { - let c = get_customizations().first().unwrap().clone(); + let customization = CustomizationBuilder::default() - .name(c.name().into()) + .name("cname".into()) .customization_id(UUID) .deleted(false) .build() .unwrap() - }; + ; // state doesn't exist assert!(!db diff --git a/src/inventory/adapters/output/db/postgres/product_id_exists.rs b/src/inventory/adapters/output/db/postgres/product_id_exists.rs index f21e8d5..9d98a1d 100644 --- a/src/inventory/adapters/output/db/postgres/product_id_exists.rs +++ b/src/inventory/adapters/output/db/postgres/product_id_exists.rs @@ -32,8 +32,10 @@ impl ProductIDExistsDBPort for InventoryDBPostgresAdapter { pub mod tests { use super::*; - use crate::inventory::domain::add_product_command::tests::get_customizations; - use crate::inventory::domain::{add_product_command::tests::get_command, product_aggregate::*}; + // use crate::inventory::domain::add_product_command::tests::get_customizations; + use crate::inventory::domain::{ + add_product_command::tests::get_command, product_aggregate::*, + }; use crate::utils::uuid::tests::UUID; #[actix_rt::test] @@ -48,17 +50,6 @@ pub mod tests { let cmd = get_command(); - let mut customizations = Vec::default(); - for c in get_customizations().iter() { - customizations.push( - CustomizationBuilder::default() - .name(c.name().into()) - .deleted(false) - .customization_id(Uuid::new_v4()) - .build() - .unwrap(), - ) - } let product = ProductBuilder::default() .name(cmd.name().into()) .description(cmd.description().as_ref().map(|s| s.to_string())) @@ -66,8 +57,7 @@ pub mod tests { .sku_able(*cmd.sku_able()) .category_id(*cmd.category_id()) .quantity(cmd.quantity().clone()) - .product_id(UUID) - .customizations(customizations) + .product_id(UUID.clone()) .price(cmd.price().clone()) .build() .unwrap(); @@ -98,10 +88,9 @@ pub mod tests { sku_able, quantity_unit, quantity_number, - deleted, - customizations_available + deleted ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 );", 1, p.name(), @@ -116,7 +105,6 @@ pub mod tests { p.quantity().unit().to_string(), p.quantity().number().clone() as i32, p.deleted().clone(), - false, ) .execute(&db.pool) .await diff --git a/src/inventory/adapters/output/db/postgres/product_name_exists_for_category.rs b/src/inventory/adapters/output/db/postgres/product_name_exists_for_category.rs index dd233e1..1217336 100644 --- a/src/inventory/adapters/output/db/postgres/product_name_exists_for_category.rs +++ b/src/inventory/adapters/output/db/postgres/product_name_exists_for_category.rs @@ -62,7 +62,6 @@ mod tests { .image(cmd.image().as_ref().map(|s| s.to_string())) .sku_able(*cmd.sku_able()) .category_id(*cmd.category_id()) - .customizations(Vec::default()) .product_id(UUID) .price(cmd.price().clone()) .quantity(cmd.quantity().clone()) diff --git a/src/inventory/adapters/output/db/postgres/product_view.rs b/src/inventory/adapters/output/db/postgres/product_view.rs index 958fb4e..38f7e5a 100644 --- a/src/inventory/adapters/output/db/postgres/product_view.rs +++ b/src/inventory/adapters/output/db/postgres/product_view.rs @@ -14,8 +14,7 @@ use super::errors::*; use super::InventoryDBPostgresAdapter; use crate::inventory::domain::events::InventoryEvent; use crate::inventory::domain::product_aggregate::{ - Currency, Customization, CustomizationBuilder, PriceBuilder, Product, ProductBuilder, - QuantityBuilder, QuantityUnit, + Currency, PriceBuilder, Product, ProductBuilder, QuantityBuilder, QuantityUnit, }; use crate::utils::parse_aggregate_id::parse_aggregate_id; @@ -40,53 +39,9 @@ pub struct ProductView { category_id: Uuid, - customizations: Vec, - deleted: bool, } -#[derive(Debug, Default, Serialize, Deserialize)] -struct InnerProductView { - name: String, - description: Option, - image: Option, // string = filename - product_id: Uuid, - sku_able: bool, - - price_minor: i32, - price_major: i32, - price_currency: String, - - quantity_unit: String, - quantity_number: i32, - - category_id: Uuid, - - customizations_available: bool, - - deleted: bool, -} - -impl InnerProductView { - fn to_product_view(self, customizations: Vec) -> ProductView { - ProductView { - name: self.name, - description: self.description, - image: self.image, - product_id: self.product_id, - category_id: self.category_id, - price_minor: self.price_minor, - price_major: self.price_major, - price_currency: self.price_currency, - sku_able: self.sku_able, - quantity_unit: self.quantity_unit, - quantity_number: self.quantity_number, - deleted: self.deleted, - customizations, - } - } -} - impl From for Product { fn from(v: ProductView) -> Self { let price = PriceBuilder::default() @@ -122,78 +77,27 @@ impl From for Product { // design the events to carry the balance information instead. impl View for ProductView { fn update(&mut self, event: &EventEnvelope) { - if let InventoryEvent::ProductAdded(val) = &event.payload { - self.name = val.name().into(); - self.description = val.description().clone(); - self.image = val.image().clone(); - self.product_id = *val.product_id(); - self.category_id = *val.category_id(); + match &event.payload { + InventoryEvent::ProductAdded(val) => { + self.name = val.name().into(); + self.description = val.description().clone(); + self.image = val.image().clone(); + self.product_id = *val.product_id(); + self.category_id = *val.category_id(); - self.sku_able = *val.sku_able(); + self.sku_able = *val.sku_able(); - self.price_minor = *val.price().minor() as i32; - self.price_major = *val.price().major() as i32; - self.price_currency = val.price().currency().to_string(); + self.price_minor = *val.price().minor() as i32; + self.price_major = *val.price().major() as i32; + self.price_currency = val.price().currency().to_string(); - self.quantity_number = *val.quantity().number() as i32; - self.quantity_unit = val.quantity().unit().to_string(); + self.quantity_number = *val.quantity().number() as i32; + self.quantity_unit = val.quantity().unit().to_string(); - self.deleted = false; - self.customizations = val.customizations().clone(); - } - } -} - -pub struct InnerCustomization { - name: String, - customization_id: Uuid, - product_id: Uuid, - deleted: bool, -} - -impl From for Customization { - fn from(value: InnerCustomization) -> Self { - CustomizationBuilder::default() - .name(value.name) - .customization_id(value.customization_id) - .deleted(value.deleted) - .build() - .unwrap() - } -} - -impl InnerProductView { - async fn get_customizations( - &self, - db: &InventoryDBPostgresAdapter, - ) -> Result, PersistenceError> { - let customizations = if self.customizations_available { - let mut inner_customizations = sqlx::query_as!( - InnerCustomization, - "SELECT - name, - customization_id, - deleted, - product_id - FROM - cqrs_inventory_product_customizations_query - WHERE - product_id = $1;", - self.product_id - ) - .fetch_all(&db.pool) - .await - .map_err(PostgresAggregateError::from)?; - - let mut customizations = Vec::with_capacity(inner_customizations.len()); - for c in inner_customizations.drain(0..) { - customizations.push(c.into()); + self.deleted = false; } - customizations - } else { - Vec::default() - }; - Ok(customizations) + _ => (), + } } } @@ -206,7 +110,7 @@ impl ViewRepository for InventoryDBPostgresAdapter { }; let res = sqlx::query_as!( - InnerProductView, + ProductView, "SELECT name, description, @@ -219,8 +123,7 @@ impl ViewRepository for InventoryDBPostgresAdapter { sku_able, quantity_unit, quantity_number, - deleted, - customizations_available + deleted FROM cqrs_inventory_product_query WHERE @@ -231,8 +134,7 @@ impl ViewRepository for InventoryDBPostgresAdapter { .await .map_err(PostgresAggregateError::from)?; - let customizations = res.get_customizations(self).await?; - Ok(Some(res.to_product_view(customizations))) + Ok(Some(res)) } async fn load_with_context( @@ -245,7 +147,7 @@ impl ViewRepository for InventoryDBPostgresAdapter { }; let res = sqlx::query_as!( - InnerProductView, + ProductView, "SELECT name, description, @@ -258,7 +160,6 @@ impl ViewRepository for InventoryDBPostgresAdapter { sku_able, quantity_unit, quantity_number, - customizations_available, deleted FROM cqrs_inventory_product_query @@ -270,8 +171,6 @@ impl ViewRepository for InventoryDBPostgresAdapter { .await .map_err(PostgresAggregateError::from)?; - let customizations = res.get_customizations(self).await?; - struct Context { version: i64, product_id: Uuid, @@ -292,7 +191,7 @@ impl ViewRepository for InventoryDBPostgresAdapter { .map_err(PostgresAggregateError::from)?; let view_context = ViewContext::new(ctx.product_id.to_string(), ctx.version); - Ok(Some((res.to_product_view(customizations), view_context))) + Ok(Some((res, view_context))) } async fn update_view( @@ -317,10 +216,9 @@ impl ViewRepository for InventoryDBPostgresAdapter { sku_able, quantity_unit, quantity_number, - deleted, - customizations_available + deleted ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 );", version, view.name, @@ -335,34 +233,12 @@ impl ViewRepository for InventoryDBPostgresAdapter { view.quantity_unit, view.quantity_number, view.deleted, - !view.customizations.is_empty() ) .execute(&self.pool) .await .map_err(PostgresAggregateError::from)?; - - for c in view.customizations.iter() { - sqlx::query!( - "INSERT INTO cqrs_inventory_product_customizations_query ( - version, - name, - customization_id, - product_id, - deleted - ) VALUES ( - $1, $2, $3, $4, $5 - );", - version, - c.name(), - c.customization_id(), - view.product_id, - c.deleted(), - ) - .execute(&self.pool) - .await - .map_err(PostgresAggregateError::from)?; - } } + _ => { let version = context.version + 1; sqlx::query!( @@ -381,8 +257,7 @@ impl ViewRepository for InventoryDBPostgresAdapter { sku_able = $10, quantity_unit = $11, quantity_number = $12, - deleted = $13, - customizations_available = $14;", + deleted = $13;", version, view.name, view.description, @@ -396,32 +271,10 @@ impl ViewRepository for InventoryDBPostgresAdapter { view.quantity_unit, view.quantity_number, view.deleted, - !view.customizations.is_empty() ) .execute(&self.pool) .await .map_err(PostgresAggregateError::from)?; - - for c in view.customizations.iter() { - sqlx::query!( - "UPDATE - cqrs_inventory_product_customizations_query - SET - version = $1, - name = $2, - customization_id = $3, - product_id = $4, - deleted = $5;", - version, - c.name(), - c.customization_id(), - view.product_id, - c.deleted(), - ) - .execute(&self.pool) - .await - .map_err(PostgresAggregateError::from)?; - } } } diff --git a/src/inventory/application/port/output/db/customization_name_exists_for_product.rs b/src/inventory/application/port/output/db/customization_name_exists_for_product.rs index 948d0db..632260b 100644 --- a/src/inventory/application/port/output/db/customization_name_exists_for_product.rs +++ b/src/inventory/application/port/output/db/customization_name_exists_for_product.rs @@ -6,7 +6,7 @@ use mockall::predicate::*; use mockall::*; use uuid::Uuid; -use crate::inventory::domain::product_aggregate::Customization; +use crate::inventory::domain::customization_aggregate::Customization; use super::errors::*; #[cfg(test)] diff --git a/src/inventory/application/services/add_product_service.rs b/src/inventory/application/services/add_product_service.rs index da23430..2030090 100644 --- a/src/inventory/application/services/add_product_service.rs +++ b/src/inventory/application/services/add_product_service.rs @@ -10,10 +10,7 @@ use mockall::*; use super::errors::*; use crate::inventory::{ - application::port::output::db::{ - customization_id_exists::*, customization_name_exists_for_product::*, product_id_exists::*, - product_name_exists_for_category::*, - }, + application::port::output::db::{product_id_exists::*, product_name_exists_for_category::*}, domain::{ add_product_command::AddProductCommand, product_added_event::{ProductAddedEvent, ProductAddedEventBuilder}, @@ -32,10 +29,9 @@ pub type AddProductServiceObj = Arc; #[derive(Clone, Builder)] pub struct AddProductService { + // TODO: check if category ID exists db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj, db_product_id_exists: ProductIDExistsDBPortObj, - db_customization_id_exists: CustomizationIDExistsDBPortObj, - db_customization_name_exists_for_product: CustomizationNameExistsForProductDBPortObj, get_uuid: GetUUIDInterfaceObj, } @@ -57,40 +53,6 @@ impl AddProductUseCase for AddProductService { } } - let mut customizations = Vec::with_capacity(cmd.customizations().len()); - for c in cmd.customizations().iter() { - let mut customization_id = self.get_uuid.get_uuid(); - loop { - if self - .db_customization_id_exists - .customization_id_exists(&customization_id) - .await? - { - customization_id = self.get_uuid.get_uuid(); - continue; - } else { - break; - } - } - - let customization = CustomizationBuilder::default() - .name(c.name().into()) - .deleted(false) - .customization_id(customization_id) - .build() - .unwrap(); - - if self - .db_customization_name_exists_for_product - .customization_name_exists_for_product(&customization, &product_id) - .await? - { - return Err(InventoryError::DuplicateCustomizationName); - } - - customizations.push(customization); - } - let product = ProductBuilder::default() .name(cmd.name().into()) .description(cmd.description().as_ref().map(|s| s.to_string())) @@ -99,7 +61,6 @@ impl AddProductUseCase for AddProductService { .price(cmd.price().clone()) .category_id(*cmd.category_id()) .quantity(cmd.quantity().clone()) - .customizations(customizations) .product_id(product_id) .build() .unwrap(); @@ -121,7 +82,8 @@ impl AddProductUseCase for AddProductService { .price(product.price().clone()) .category_id(*product.category_id()) .product_id(*product.product_id()) - .customizations(product.customizations().clone()) + .category_id(product.category_id().clone()) + .product_id(product.product_id().clone()) .quantity(product.quantity().clone()) .build() .unwrap()) @@ -142,31 +104,19 @@ pub mod tests { ) -> AddProductServiceObj { let mut m = MockAddProductUseCase::new(); - let mut customizations = Vec::with_capacity(cmd.customizations().len()); - for c in cmd.customizations().iter() { - customizations.push( - CustomizationBuilder::default() - .name(c.name().into()) - .deleted(false) - .customization_id(UUID) - .build() - .unwrap(), - ); - } - - let res = ProductAddedEventBuilder::default() - .name(cmd.name().into()) - .description(cmd.description().as_ref().map(|s| s.to_string())) - .image(cmd.image().as_ref().map(|s| s.to_string())) - .sku_able(*cmd.sku_able()) - .category_id(*cmd.category_id()) - .product_id(UUID) - .price(cmd.price().clone()) - .quantity(cmd.quantity().clone()) - .customizations(customizations) - .added_by_user(*cmd.adding_by()) - .build() - .unwrap(); + let res = //( + ProductAddedEventBuilder::default() + .name(cmd.name().into()) + .description(cmd.description().as_ref().map(|s| s.to_string())) + .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()) + .price(cmd.price().clone()) + .quantity(cmd.quantity().clone()) + .added_by_user(cmd.adding_by().clone()) + .build() + .unwrap(); if let Some(times) = times { m.expect_add_product() @@ -187,14 +137,8 @@ pub mod tests { .db_product_name_exists_for_category( mock_product_name_exists_for_category_db_port_false(IS_CALLED_ONLY_ONCE), ) - .db_customization_id_exists(mock_customization_id_exists_db_port_false( - IS_CALLED_ONLY_THRICE, - )) .db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) - .db_customization_name_exists_for_product( - mock_customization_name_exists_for_product_db_port_false(IS_CALLED_ONLY_THRICE), - ) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_FOUR_TIMES)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); @@ -218,14 +162,8 @@ pub mod tests { .db_product_name_exists_for_category( mock_product_name_exists_for_category_db_port_true(IS_CALLED_ONLY_ONCE), ) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_FOUR_TIMES)) + .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_customization_id_exists(mock_customization_id_exists_db_port_false( - IS_CALLED_ONLY_THRICE, - )) - .db_customization_name_exists_for_product( - mock_customization_name_exists_for_product_db_port_false(IS_CALLED_ONLY_THRICE), - ) .build() .unwrap(); diff --git a/src/inventory/domain/add_product_command.rs b/src/inventory/domain/add_product_command.rs index 5b0d22c..f5ef74a 100644 --- a/src/inventory/domain/add_product_command.rs +++ b/src/inventory/domain/add_product_command.rs @@ -2,8 +2,6 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -use std::collections::HashSet; - use derive_builder::Builder; use derive_getters::Getters; use derive_more::{Display, Error}; @@ -15,30 +13,6 @@ use super::product_aggregate::{Price, Quantity}; #[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum AddProductCommandError { NameIsEmpty, - CustomizationNameIsEmpty, - DuplicateCustomizationName, -} -#[derive( - Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, -)] -pub struct UnvalidatedAddCustomization { - name: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] -pub struct AddCustomization { - name: String, -} - -impl UnvalidatedAddCustomization { - pub fn validate(self) -> Result { - let name = self.name.trim().to_owned(); - if name.is_empty() { - return Err(AddProductCommandError::CustomizationNameIsEmpty); - } - - Ok(AddCustomization { name }) - } } #[derive( @@ -53,7 +27,6 @@ pub struct UnvalidatedAddProductCommand { quantity: Quantity, price: Price, adding_by: Uuid, - customizations: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] @@ -66,7 +39,6 @@ pub struct AddProductCommand { price: Price, quantity: Quantity, adding_by: Uuid, - customizations: Vec, } impl UnvalidatedAddProductCommand { @@ -98,15 +70,6 @@ impl UnvalidatedAddProductCommand { return Err(AddProductCommandError::NameIsEmpty); } - let mut unique_customization_names = HashSet::new(); - if !self - .customizations - .iter() - .all(|c| unique_customization_names.insert(c.name.clone())) - { - return Err(AddProductCommandError::DuplicateCustomizationName); - } - Ok(AddProductCommand { name, description, @@ -116,7 +79,6 @@ impl UnvalidatedAddProductCommand { price: self.price, quantity: self.quantity, adding_by: self.adding_by, - customizations: self.customizations, }) } } @@ -132,28 +94,6 @@ pub mod tests { utils::uuid::tests::UUID, }; - pub fn get_customizations() -> Vec { - vec![ - UnvalidatedAddCustomizationBuilder::default() - .name("foo".into()) - .build() - .unwrap() - .validate() - .unwrap(), - UnvalidatedAddCustomizationBuilder::default() - .name("bar".into()) - .build() - .unwrap() - .validate() - .unwrap(), - UnvalidatedAddCustomizationBuilder::default() - .name("baz".into()) - .build() - .unwrap() - .validate() - .unwrap(), - ] - } pub fn get_command() -> AddProductCommand { let name = "foo"; let adding_by = UUID; @@ -162,8 +102,6 @@ pub mod tests { let image = Some("image".to_string()); let description = Some("description".to_string()); - let customizations = get_customizations(); - let price = PriceBuilder::default() .minor(0) .major(100) @@ -186,7 +124,6 @@ pub mod tests { .quantity(quantity) .sku_able(sku_able) .price(price.clone()) - .customizations(customizations) .build() .unwrap(); @@ -200,7 +137,6 @@ pub mod tests { let category_id = Uuid::new_v4(); let sku_able = false; - let customizations = get_customizations(); let price = PriceBuilder::default() .minor(0) .major(100) @@ -223,7 +159,6 @@ pub mod tests { .quantity(quantity.clone()) .sku_able(sku_able) .price(price.clone()) - .customizations(customizations.clone()) .build() .unwrap(); @@ -237,7 +172,6 @@ pub mod tests { assert_eq!(cmd.sku_able(), &sku_able); assert_eq!(cmd.price(), &price); assert_eq!(cmd.quantity(), &quantity); - assert_eq!(cmd.customizations(), &customizations); } #[test] fn test_description_some() { @@ -248,7 +182,6 @@ pub mod tests { let image = Some("image".to_string()); let description = Some("description".to_string()); - let customizations = get_customizations(); let price = PriceBuilder::default() .minor(0) .major(100) @@ -270,7 +203,7 @@ pub mod tests { .adding_by(adding_by) .sku_able(sku_able) .price(price.clone()) - .customizations(customizations.clone()) + // .customizations(customizations.clone()) .build() .unwrap(); @@ -284,7 +217,6 @@ pub mod tests { assert_eq!(cmd.sku_able(), &sku_able); assert_eq!(cmd.price(), &price); assert_eq!(cmd.quantity(), &quantity); - assert_eq!(cmd.customizations(), &customizations); } #[test] @@ -295,7 +227,6 @@ pub mod tests { let image = Some("image".to_string()); let description = Some("description".to_string()); - let customizations = get_customizations(); let price = PriceBuilder::default() .minor(0) .major(100) @@ -316,7 +247,6 @@ pub mod tests { .adding_by(adding_by) .quantity(quantity) .sku_able(sku_able) - .customizations(customizations) .price(price.clone()) .build() .unwrap(); @@ -324,60 +254,4 @@ pub mod tests { // AddProductCommandError::NameIsEmpty assert_eq!(cmd.validate(), Err(AddProductCommandError::NameIsEmpty)) } - - #[test] - fn test_customization_name_is_empty() { - assert_eq!( - UnvalidatedAddCustomizationBuilder::default() - .name("".into()) - .build() - .unwrap() - .validate(), - Err(AddProductCommandError::CustomizationNameIsEmpty) - ) - } - - #[test] - fn test_duplicate_customization() { - let name = "foo"; - let adding_by = UUID; - let category_id = Uuid::new_v4(); - let sku_able = false; - let image = Some("image".to_string()); - let description = Some("description".to_string()); - - let mut customizations = get_customizations(); - customizations.push(customizations.first().unwrap().to_owned()); - - let price = PriceBuilder::default() - .minor(0) - .major(100) - .currency(Currency::INR) - .build() - .unwrap(); - let quantity = QuantityBuilder::default() - .number(1) - .unit(QuantityUnit::DiscreteNumber) - .build() - .unwrap(); - - let cmd = UnvalidatedAddProductCommandBuilder::default() - .name(name.into()) - .description(description.clone()) - .image(image.clone()) - .category_id(category_id) - .adding_by(adding_by) - .quantity(quantity) - .sku_able(sku_able) - .customizations(customizations) - .price(price.clone()) - .build() - .unwrap(); - - // AddProductCommandError::DuplicateCustomizationName - assert_eq!( - cmd.validate(), - Err(AddProductCommandError::DuplicateCustomizationName) - ) - } } diff --git a/src/inventory/domain/product_added_event.rs b/src/inventory/domain/product_added_event.rs index 53138a7..6c74e41 100644 --- a/src/inventory/domain/product_added_event.rs +++ b/src/inventory/domain/product_added_event.rs @@ -7,7 +7,7 @@ use derive_getters::Getters; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::product_aggregate::{Customization, Price, Quantity}; +use super::product_aggregate::{Price, Quantity}; #[derive( Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, @@ -23,32 +23,17 @@ pub struct ProductAddedEvent { category_id: Uuid, sku_able: bool, product_id: Uuid, - customizations: Vec, } #[cfg(test)] pub mod tests { - use crate::inventory::domain::{ - add_product_command::AddProductCommand, product_aggregate::CustomizationBuilder, - }; + use crate::inventory::domain::add_product_command::AddProductCommand; use super::*; use crate::utils::uuid::tests::UUID; pub fn get_event_from_command(cmd: &AddProductCommand) -> ProductAddedEvent { - let mut customizations = Vec::with_capacity(cmd.customizations().len()); - for c in cmd.customizations().iter() { - customizations.push( - CustomizationBuilder::default() - .name(c.name().into()) - .deleted(false) - .customization_id(UUID) - .build() - .unwrap(), - ); - } - ProductAddedEventBuilder::default() .name(cmd.name().into()) .description(cmd.description().as_ref().map(|s| s.to_string())) @@ -57,7 +42,6 @@ pub mod tests { .category_id(*cmd.category_id()) .product_id(UUID) .price(cmd.price().clone()) - .customizations(customizations) .quantity(cmd.quantity().clone()) .added_by_user(*cmd.adding_by()) .build() diff --git a/src/inventory/domain/product_aggregate.rs b/src/inventory/domain/product_aggregate.rs index edf01bd..5c04c73 100644 --- a/src/inventory/domain/product_aggregate.rs +++ b/src/inventory/domain/product_aggregate.rs @@ -98,16 +98,6 @@ pub struct Price { currency: Currency, } -#[derive( - Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, -)] -pub struct Customization { - name: String, - customization_id: Uuid, - #[builder(default = "false")] - deleted: bool, -} - #[derive( Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, )] @@ -122,7 +112,6 @@ pub struct Product { category_id: Uuid, sku_able: bool, product_id: Uuid, - customizations: Vec, #[builder(default = "false")] deleted: bool, } @@ -156,20 +145,22 @@ impl Aggregate for Product { } fn apply(&mut self, event: Self::Event) { - if let InventoryEvent::ProductAdded(e) = event { - *self = ProductBuilder::default() - .name(e.name().into()) - .description(e.description().clone()) - .image(e.image().clone()) - .price(e.price().clone()) - .category_id(*e.category_id()) - .sku_able(*e.sku_able()) - .product_id(*e.product_id()) - .quantity(e.quantity().clone()) - .customizations(e.customizations().clone()) - .deleted(false) - .build() - .unwrap(); + match event { + InventoryEvent::ProductAdded(e) => { + *self = ProductBuilder::default() + .name(e.name().into()) + .description(e.description().clone()) + .image(e.image().clone()) + .price(e.price().clone()) + .category_id(e.category_id().clone()) + .sku_able(e.sku_able().clone()) + .product_id(e.product_id().clone()) + .quantity(e.quantity().clone()) + .deleted(false) + .build() + .unwrap(); + } + _ => (), } } } diff --git a/src/inventory/domain/product_updated_event.rs b/src/inventory/domain/product_updated_event.rs new file mode 100644 index 0000000..91070cf --- /dev/null +++ b/src/inventory/domain/product_updated_event.rs @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::customization_aggregate::*; +use super::product_aggregate::{Price, Product, Quantity}; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct ProductUpdateedEvent { + added_by_user: Uuid, + + name: String, + description: Option, + image: Option, // string = file_name + price: Price, + quantity: Quantity, + category_id: Uuid, + sku_able: bool, + product_id: Uuid, + customizations: Vec, + + old_product: Product, +} + +//#[cfg(test)] +//pub mod tests { +// use crate::inventory::domain::{ +// add_product_command::UpdateProductCommand, product_aggregate::CustomizationBuilder, +// }; +// +// use super::*; +// +// use crate::utils::uuid::tests::UUID; +// +// pub fn get_event_from_command(cmd: &UpdateProductCommand) -> ProductUpdateedEvent { +// let mut customizations = Vec::with_capacity(cmd.customizations().len()); +// for c in cmd.customizations().iter() { +// customizations.push( +// CustomizationBuilder::default() +// .name(c.name().into()) +// .deleted(false) +// .customization_id(UUID) +// .build() +// .unwrap(), +// ); +// } +// +// ProductUpdateedEventBuilder::default() +// .name(cmd.name().into()) +// .description(cmd.description().as_ref().map(|s| s.to_string())) +// .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()) +// .price(cmd.price().clone()) +// .customizations(customizations) +// .quantity(cmd.quantity().clone()) +// .added_by_user(cmd.adding_by().clone()) +// .build() +// .unwrap() +// } +//} diff --git a/src/inventory/domain/update_product_command.rs b/src/inventory/domain/update_product_command.rs new file mode 100644 index 0000000..81db35f --- /dev/null +++ b/src/inventory/domain/update_product_command.rs @@ -0,0 +1,400 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::collections::HashSet; + +use derive_builder::Builder; +use derive_getters::Getters; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::product_aggregate::{Price, Product, Quantity}; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum UpdateProductCommandError { + NameIsEmpty, + CustomizationNameIsEmpty, + DuplicateCustomizationName, +} +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct UnvalidatedUpdateCustomization { + name: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct UpdateCustomization { + name: String, +} + +impl UnvalidatedUpdateCustomization { + pub fn validate(self) -> Result { + let name = self.name.trim().to_owned(); + if name.is_empty() { + return Err(UpdateProductCommandError::CustomizationNameIsEmpty); + } + + Ok(UpdateCustomization { name }) + } +} + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct UnvalidatedUpdateProductCommand { + name: String, + description: Option, + image: Option, + category_id: Uuid, + sku_able: bool, + quantity: Quantity, + price: Price, + adding_by: Uuid, + customizations: Vec, + old_product: Product, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct UpdateProductCommand { + name: String, + description: Option, + image: Option, + category_id: Uuid, + sku_able: bool, + price: Price, + quantity: Quantity, + adding_by: Uuid, + customizations: Vec, + old_product: Product, +} + +impl UnvalidatedUpdateProductCommand { + pub fn validate(self) -> Result { + let description: Option = if let Some(description) = self.description { + let description = description.trim(); + if description.is_empty() { + None + } else { + Some(description.to_owned()) + } + } else { + None + }; + + let image: Option = if let Some(image) = self.image { + let image = image.trim(); + if image.is_empty() { + None + } else { + Some(image.to_owned()) + } + } else { + None + }; + + let name = self.name.trim().to_owned(); + if name.is_empty() { + return Err(UpdateProductCommandError::NameIsEmpty); + } + + let mut unique_customization_names = HashSet::new(); + if !self + .customizations + .iter() + .all(|c| unique_customization_names.insert(c.name.clone())) + { + return Err(UpdateProductCommandError::DuplicateCustomizationName); + } + + // &name == self.old_product.name() && + // &description == self.old_product().description() && + // &image == self.old_product().image() && + // self.sku_able == *self.old_product().sku_able() && + // &self.price == self.old_product().price() && + // &self.quantity == self.old_product.quantity(); + + Ok(UpdateProductCommand { + name, + description, + image, + category_id: self.category_id, + sku_able: self.sku_able, + price: self.price, + quantity: self.quantity, + adding_by: self.adding_by, + customizations: self.customizations, + old_product: self.old_product, + }) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::{ + inventory::domain::product_aggregate::{ + Currency, PriceBuilder, QuantityBuilder, QuantityUnit, + }, + utils::uuid::tests::UUID, + }; + + pub fn get_customizations() -> Vec { + vec![ + UnvalidatedUpdateCustomizationBuilder::default() + .name("foo".into()) + .build() + .unwrap() + .validate() + .unwrap(), + UnvalidatedUpdateCustomizationBuilder::default() + .name("bar".into()) + .build() + .unwrap() + .validate() + .unwrap(), + UnvalidatedUpdateCustomizationBuilder::default() + .name("baz".into()) + .build() + .unwrap() + .validate() + .unwrap(), + ] + } + pub fn get_command() -> UpdateProductCommand { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let customizations = get_customizations(); + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + + let quantity = QuantityBuilder::default() + .number(1) + .unit(QuantityUnit::DiscreteNumber) + .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()) + .customizations(customizations) + .old_product(Product::default()) + .build() + .unwrap(); + + cmd.validate().unwrap() + } + + #[test] + fn test_description_and_image_none() { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + + let customizations = get_customizations(); + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + let quantity = QuantityBuilder::default() + .number(1) + .unit(QuantityUnit::DiscreteNumber) + .build() + .unwrap(); + + // description = None + let cmd = UnvalidatedUpdateProductCommandBuilder::default() + .name(name.into()) + .description(None) + .image(None) + .category_id(category_id.clone()) + .adding_by(adding_by.clone()) + .quantity(quantity.clone()) + .sku_able(sku_able) + .price(price.clone()) + .old_product(Product::default()) + .customizations(customizations.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); + assert_eq!(cmd.category_id(), &category_id); + assert_eq!(cmd.image(), &None); + assert_eq!(cmd.sku_able(), &sku_able); + assert_eq!(cmd.old_product(), &Product::default()); + assert_eq!(cmd.price(), &price); + assert_eq!(cmd.quantity(), &quantity); + assert_eq!(cmd.customizations(), &customizations); + } + #[test] + fn test_description_some() { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let customizations = get_customizations(); + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + let quantity = QuantityBuilder::default() + .number(1) + .unit(QuantityUnit::DiscreteNumber) + .build() + .unwrap(); + + let cmd = UnvalidatedUpdateProductCommandBuilder::default() + .name(name.into()) + .description(description.clone()) + .image(image.clone()) + .category_id(category_id.clone()) + .quantity(quantity.clone()) + .adding_by(adding_by.clone()) + .sku_able(sku_able) + .price(price.clone()) + .customizations(customizations.clone()) + .old_product(Product::default()) + .build() + .unwrap(); + + let cmd = cmd.validate().unwrap(); + + assert_eq!(cmd.name(), name); + assert_eq!(cmd.description(), &description); + assert_eq!(cmd.adding_by(), &adding_by); + assert_eq!(cmd.category_id(), &category_id); + assert_eq!(cmd.image(), &image); + assert_eq!(cmd.sku_able(), &sku_able); + assert_eq!(cmd.price(), &price); + assert_eq!(cmd.quantity(), &quantity); + assert_eq!(cmd.customizations(), &customizations); + assert_eq!(cmd.old_product(), &Product::default()); + } + + #[test] + fn test_name_is_empty() { + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let customizations = get_customizations(); + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + let quantity = QuantityBuilder::default() + .number(1) + .unit(QuantityUnit::DiscreteNumber) + .build() + .unwrap(); + + let cmd = UnvalidatedUpdateProductCommandBuilder::default() + .name("".into()) + .description(description.clone()) + .image(image.clone()) + .category_id(category_id.clone()) + .adding_by(adding_by.clone()) + .quantity(quantity) + .old_product(Product::default()) + .sku_able(sku_able) + .customizations(customizations) + .price(price.clone()) + .build() + .unwrap(); + + // UpdateProductCommandError::NameIsEmpty + assert_eq!(cmd.validate(), Err(UpdateProductCommandError::NameIsEmpty)) + } + + #[test] + fn test_customization_name_is_empty() { + assert_eq!( + UnvalidatedUpdateCustomizationBuilder::default() + .name("".into()) + .build() + .unwrap() + .validate(), + Err(UpdateProductCommandError::CustomizationNameIsEmpty) + ) + } + + #[test] + fn test_duplicate_customization() { + let name = "foo"; + let adding_by = UUID; + let category_id = Uuid::new_v4(); + let sku_able = false; + let image = Some("image".to_string()); + let description = Some("description".to_string()); + + let mut customizations = get_customizations(); + customizations.push(customizations.first().unwrap().to_owned()); + + let price = PriceBuilder::default() + .minor(0) + .major(100) + .currency(Currency::INR) + .build() + .unwrap(); + let quantity = QuantityBuilder::default() + .number(1) + .unit(QuantityUnit::DiscreteNumber) + .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) + .old_product(Product::default()) + .sku_able(sku_able) + .customizations(customizations) + .price(price.clone()) + .build() + .unwrap(); + + // UpdateProductCommandError::DuplicateCustomizationName + assert_eq!( + cmd.validate(), + Err(UpdateProductCommandError::DuplicateCustomizationName) + ) + } +}