From 0f29902b4b5c39609b5cdbeff7aa4b8fbc82d4b4 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 16 Jul 2024 15:41:16 +0530 Subject: [PATCH 1/3] 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) + ) + } +} From f189ecbf38acbb9178de2e06bb95e6d64d2e3862 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 16 Jul 2024 15:43:07 +0530 Subject: [PATCH 2/3] feat: define Customization aggregate, and impl Query and View --- ...6cfac98a2dee007a1388ffe95f7feecbc7442.json | 18 -- ...0e6d64dcded15e56e1a4064c66d9df57fcdda.json | 94 ------ ...5077814f6c8b64cabe1cf4687187c1a766ce.json} | 12 +- ...b42b86d2cffee477a4b9a69507615ef0b91c5.json | 18 ++ ...3e530373595ce83cd36612ee48d3c36c81f3.json} | 10 +- ...e4f0682f740778e483a2db91d6c73ac6bc6d.json} | 5 +- ...5862d4fca3f565bb394ef4115bb94913a43c3.json | 28 ++ ...86bbc866218e4146b6d608255c1b929073b4.json} | 5 +- .../output/db/postgres/customization_view.rs | 271 ++++++++++++++++++ .../adapters/output/db/postgres/mod.rs | 1 + .../domain/customization_aggregate.rs | 147 ++++++++++ 11 files changed, 477 insertions(+), 132 deletions(-) delete mode 100644 .sqlx/query-0994f26a6ec76a33d2839cc290e6cfac98a2dee007a1388ffe95f7feecbc7442.json delete mode 100644 .sqlx/query-27212976fb97f84722c5bf522580e6d64dcded15e56e1a4064c66d9df57fcdda.json rename .sqlx/{query-674258549f4d79aeaf3ecd7d242df79e5f2ba063409fd81cb283bb2b75f10c32.json => query-2e0cec451addfee3a8f0ac1603205077814f6c8b64cabe1cf4687187c1a766ce.json} (61%) create mode 100644 .sqlx/query-3c21629e4125c2a26ed535b981cb42b86d2cffee477a4b9a69507615ef0b91c5.json rename .sqlx/{query-10c89e8b7be6fa4a1bf39ae2fbb922fad9db5e9d094497b9cc401e9301b5631f.json => query-3f83167782a2de1be7d87d35f63a3e530373595ce83cd36612ee48d3c36c81f3.json} (81%) rename .sqlx/{query-1c048c8e1fc4739d20df983b5042e7ed9eadef4497648acff9d472a2d7fabf78.json => query-9390910c71001ef77313e594dea0e4f0682f740778e483a2db91d6c73ac6bc6d.json} (73%) create mode 100644 .sqlx/query-bdaf5b691f50281bb7108d8153c5862d4fca3f565bb394ef4115bb94913a43c3.json rename .sqlx/{query-c0850c387878c368a89aa0f1f505fc337a3ece328f5eba95f11f987af88f8b23.json => query-f55fe530202bb369fdea3baaf91f86bbc866218e4146b6d608255c1b929073b4.json} (86%) create mode 100644 src/inventory/adapters/output/db/postgres/customization_view.rs create mode 100644 src/inventory/domain/customization_aggregate.rs diff --git a/.sqlx/query-0994f26a6ec76a33d2839cc290e6cfac98a2dee007a1388ffe95f7feecbc7442.json b/.sqlx/query-0994f26a6ec76a33d2839cc290e6cfac98a2dee007a1388ffe95f7feecbc7442.json deleted file mode 100644 index 6ddb481..0000000 --- a/.sqlx/query-0994f26a6ec76a33d2839cc290e6cfac98a2dee007a1388ffe95f7feecbc7442.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO cqrs_inventory_product_customizations_query (\n version,\n name,\n customization_id,\n product_id,\n deleted\n ) VALUES (\n $1, $2, $3, $4, $5\n );", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text", - "Uuid", - "Uuid", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "0994f26a6ec76a33d2839cc290e6cfac98a2dee007a1388ffe95f7feecbc7442" -} diff --git a/.sqlx/query-27212976fb97f84722c5bf522580e6d64dcded15e56e1a4064c66d9df57fcdda.json b/.sqlx/query-27212976fb97f84722c5bf522580e6d64dcded15e56e1a4064c66d9df57fcdda.json deleted file mode 100644 index a89ff98..0000000 --- a/.sqlx/query-27212976fb97f84722c5bf522580e6d64dcded15e56e1a4064c66d9df57fcdda.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT \n name,\n description,\n image,\n product_id,\n category_id,\n price_major,\n price_minor,\n price_currency,\n sku_able,\n quantity_unit,\n quantity_number,\n deleted,\n customizations_available\n FROM\n cqrs_inventory_product_query\n WHERE\n product_id = $1;", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "image", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "product_id", - "type_info": "Uuid" - }, - { - "ordinal": 4, - "name": "category_id", - "type_info": "Uuid" - }, - { - "ordinal": 5, - "name": "price_major", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "price_minor", - "type_info": "Int4" - }, - { - "ordinal": 7, - "name": "price_currency", - "type_info": "Text" - }, - { - "ordinal": 8, - "name": "sku_able", - "type_info": "Bool" - }, - { - "ordinal": 9, - "name": "quantity_unit", - "type_info": "Text" - }, - { - "ordinal": 10, - "name": "quantity_number", - "type_info": "Int4" - }, - { - "ordinal": 11, - "name": "deleted", - "type_info": "Bool" - }, - { - "ordinal": 12, - "name": "customizations_available", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - true, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false - ] - }, - "hash": "27212976fb97f84722c5bf522580e6d64dcded15e56e1a4064c66d9df57fcdda" -} diff --git a/.sqlx/query-674258549f4d79aeaf3ecd7d242df79e5f2ba063409fd81cb283bb2b75f10c32.json b/.sqlx/query-2e0cec451addfee3a8f0ac1603205077814f6c8b64cabe1cf4687187c1a766ce.json similarity index 61% rename from .sqlx/query-674258549f4d79aeaf3ecd7d242df79e5f2ba063409fd81cb283bb2b75f10c32.json rename to .sqlx/query-2e0cec451addfee3a8f0ac1603205077814f6c8b64cabe1cf4687187c1a766ce.json index e858e54..1451af4 100644 --- a/.sqlx/query-674258549f4d79aeaf3ecd7d242df79e5f2ba063409fd81cb283bb2b75f10c32.json +++ b/.sqlx/query-2e0cec451addfee3a8f0ac1603205077814f6c8b64cabe1cf4687187c1a766ce.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n name,\n customization_id,\n deleted,\n product_id\n FROM\n cqrs_inventory_product_customizations_query\n WHERE\n product_id = $1;", + "query": "SELECT \n name,\n customization_id,\n product_id,\n deleted\n FROM\n cqrs_inventory_product_customizations_query\n WHERE\n customization_id = $1;", "describe": { "columns": [ { @@ -15,13 +15,13 @@ }, { "ordinal": 2, - "name": "deleted", - "type_info": "Bool" + "name": "product_id", + "type_info": "Uuid" }, { "ordinal": 3, - "name": "product_id", - "type_info": "Uuid" + "name": "deleted", + "type_info": "Bool" } ], "parameters": { @@ -36,5 +36,5 @@ false ] }, - "hash": "674258549f4d79aeaf3ecd7d242df79e5f2ba063409fd81cb283bb2b75f10c32" + "hash": "2e0cec451addfee3a8f0ac1603205077814f6c8b64cabe1cf4687187c1a766ce" } diff --git a/.sqlx/query-3c21629e4125c2a26ed535b981cb42b86d2cffee477a4b9a69507615ef0b91c5.json b/.sqlx/query-3c21629e4125c2a26ed535b981cb42b86d2cffee477a4b9a69507615ef0b91c5.json new file mode 100644 index 0000000..e3966cf --- /dev/null +++ b/.sqlx/query-3c21629e4125c2a26ed535b981cb42b86d2cffee477a4b9a69507615ef0b91c5.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO cqrs_inventory_product_customizations_query (\n version,\n name,\n customization_id,\n product_id,\n deleted\n ) VALUES (\n $1, $2, $3, $4, $5\n );", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Uuid", + "Uuid", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "3c21629e4125c2a26ed535b981cb42b86d2cffee477a4b9a69507615ef0b91c5" +} diff --git a/.sqlx/query-10c89e8b7be6fa4a1bf39ae2fbb922fad9db5e9d094497b9cc401e9301b5631f.json b/.sqlx/query-3f83167782a2de1be7d87d35f63a3e530373595ce83cd36612ee48d3c36c81f3.json similarity index 81% rename from .sqlx/query-10c89e8b7be6fa4a1bf39ae2fbb922fad9db5e9d094497b9cc401e9301b5631f.json rename to .sqlx/query-3f83167782a2de1be7d87d35f63a3e530373595ce83cd36612ee48d3c36c81f3.json index 07a4bdf..bfc6121 100644 --- a/.sqlx/query-10c89e8b7be6fa4a1bf39ae2fbb922fad9db5e9d094497b9cc401e9301b5631f.json +++ b/.sqlx/query-3f83167782a2de1be7d87d35f63a3e530373595ce83cd36612ee48d3c36c81f3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \n name,\n description,\n image,\n product_id,\n category_id,\n price_major,\n price_minor,\n price_currency,\n sku_able,\n quantity_unit,\n quantity_number,\n customizations_available,\n deleted\n FROM\n cqrs_inventory_product_query\n WHERE\n product_id = $1;", + "query": "SELECT \n name,\n description,\n image,\n product_id,\n category_id,\n price_major,\n price_minor,\n price_currency,\n sku_able,\n quantity_unit,\n quantity_number,\n deleted\n FROM\n cqrs_inventory_product_query\n WHERE\n product_id = $1;", "describe": { "columns": [ { @@ -60,11 +60,6 @@ }, { "ordinal": 11, - "name": "customizations_available", - "type_info": "Bool" - }, - { - "ordinal": 12, "name": "deleted", "type_info": "Bool" } @@ -86,9 +81,8 @@ false, false, false, - false, false ] }, - "hash": "10c89e8b7be6fa4a1bf39ae2fbb922fad9db5e9d094497b9cc401e9301b5631f" + "hash": "3f83167782a2de1be7d87d35f63a3e530373595ce83cd36612ee48d3c36c81f3" } diff --git a/.sqlx/query-1c048c8e1fc4739d20df983b5042e7ed9eadef4497648acff9d472a2d7fabf78.json b/.sqlx/query-9390910c71001ef77313e594dea0e4f0682f740778e483a2db91d6c73ac6bc6d.json similarity index 73% rename from .sqlx/query-1c048c8e1fc4739d20df983b5042e7ed9eadef4497648acff9d472a2d7fabf78.json rename to .sqlx/query-9390910c71001ef77313e594dea0e4f0682f740778e483a2db91d6c73ac6bc6d.json index 00da8fb..d70d210 100644 --- a/.sqlx/query-1c048c8e1fc4739d20df983b5042e7ed9eadef4497648acff9d472a2d7fabf78.json +++ b/.sqlx/query-9390910c71001ef77313e594dea0e4f0682f740778e483a2db91d6c73ac6bc6d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO cqrs_inventory_product_query (\n version,\n name,\n description,\n image,\n product_id,\n category_id,\n price_major,\n price_minor,\n price_currency,\n sku_able,\n quantity_unit,\n quantity_number,\n deleted,\n customizations_available\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14\n );", + "query": "INSERT INTO cqrs_inventory_product_query (\n version,\n name,\n description,\n image,\n product_id,\n category_id,\n price_major,\n price_minor,\n price_currency,\n sku_able,\n quantity_unit,\n quantity_number,\n deleted\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13\n );", "describe": { "columns": [], "parameters": { @@ -17,11 +17,10 @@ "Bool", "Text", "Int4", - "Bool", "Bool" ] }, "nullable": [] }, - "hash": "1c048c8e1fc4739d20df983b5042e7ed9eadef4497648acff9d472a2d7fabf78" + "hash": "9390910c71001ef77313e594dea0e4f0682f740778e483a2db91d6c73ac6bc6d" } diff --git a/.sqlx/query-bdaf5b691f50281bb7108d8153c5862d4fca3f565bb394ef4115bb94913a43c3.json b/.sqlx/query-bdaf5b691f50281bb7108d8153c5862d4fca3f565bb394ef4115bb94913a43c3.json new file mode 100644 index 0000000..25ac26c --- /dev/null +++ b/.sqlx/query-bdaf5b691f50281bb7108d8153c5862d4fca3f565bb394ef4115bb94913a43c3.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n customization_id, version\n FROM\n cqrs_inventory_product_customizations_query\n WHERE\n customization_id = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "customization_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "version", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "bdaf5b691f50281bb7108d8153c5862d4fca3f565bb394ef4115bb94913a43c3" +} diff --git a/.sqlx/query-c0850c387878c368a89aa0f1f505fc337a3ece328f5eba95f11f987af88f8b23.json b/.sqlx/query-f55fe530202bb369fdea3baaf91f86bbc866218e4146b6d608255c1b929073b4.json similarity index 86% rename from .sqlx/query-c0850c387878c368a89aa0f1f505fc337a3ece328f5eba95f11f987af88f8b23.json rename to .sqlx/query-f55fe530202bb369fdea3baaf91f86bbc866218e4146b6d608255c1b929073b4.json index 7e5db91..b95a37b 100644 --- a/.sqlx/query-c0850c387878c368a89aa0f1f505fc337a3ece328f5eba95f11f987af88f8b23.json +++ b/.sqlx/query-f55fe530202bb369fdea3baaf91f86bbc866218e4146b6d608255c1b929073b4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_inventory_product_query\n SET\n version = $1,\n name = $2,\n description = $3,\n image = $4,\n product_id = $5,\n category_id = $6,\n price_major = $7,\n price_minor = $8,\n price_currency = $9,\n sku_able = $10,\n quantity_unit = $11,\n quantity_number = $12,\n deleted = $13,\n customizations_available = $14;", + "query": "UPDATE\n cqrs_inventory_product_query\n SET\n version = $1,\n name = $2,\n description = $3,\n image = $4,\n product_id = $5,\n category_id = $6,\n price_major = $7,\n price_minor = $8,\n price_currency = $9,\n sku_able = $10,\n quantity_unit = $11,\n quantity_number = $12,\n deleted = $13;", "describe": { "columns": [], "parameters": { @@ -17,11 +17,10 @@ "Bool", "Text", "Int4", - "Bool", "Bool" ] }, "nullable": [] }, - "hash": "c0850c387878c368a89aa0f1f505fc337a3ece328f5eba95f11f987af88f8b23" + "hash": "f55fe530202bb369fdea3baaf91f86bbc866218e4146b6d608255c1b929073b4" } diff --git a/src/inventory/adapters/output/db/postgres/customization_view.rs b/src/inventory/adapters/output/db/postgres/customization_view.rs new file mode 100644 index 0000000..f4d0e79 --- /dev/null +++ b/src/inventory/adapters/output/db/postgres/customization_view.rs @@ -0,0 +1,271 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::str::FromStr; + +use async_trait::async_trait; +use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; +use cqrs_es::{EventEnvelope, Query, View}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::errors::*; +use super::InventoryDBPostgresAdapter; +use crate::inventory::domain::{customization_aggregate::*, events::InventoryEvent}; +use crate::utils::parse_aggregate_id::parse_aggregate_id; + +pub const NEW_PRODUCT_NON_UUID: &str = "new_product_non_uuid-asdfa"; + +//#[derive(Debug, Default, Serialize, Deserialize)] +//struct Customizations { +// customizations: Vec, +//} + +#[derive(Debug, Default, Serialize, Deserialize)] +struct CustomizationView { + name: String, + product_id: Uuid, + customization_id: Uuid, + deleted: bool, +} + +impl From for Customization { + fn from(v: CustomizationView) -> Self { + CustomizationBuilder::default() + .name(v.name) + .customization_id(v.customization_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. +impl View for CustomizationView { + fn update(&mut self, event: &EventEnvelope) { + match &event.payload { + InventoryEvent::CustomizationAdded(val) => { + self.name = val.customization().name().into(); + self.product_id = val.product_id().clone(); + self.customization_id = val.customization().customization_id().clone(); + + self.deleted = false; + } + _ => (), + } + } +} + +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 InnerCustomizationView { +// 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, +// customization_id +// FROM +// cqrs_inventory_product_customizations_query +// WHERE +// customization_id = $1;", +// self.customization_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()); +// } +// customizations +// } else { +// Vec::default() +// }; +// Ok(customizations) +// } +//} + +#[async_trait] +impl ViewRepository for InventoryDBPostgresAdapter { + async fn load( + &self, + customization_id: &str, + ) -> Result, PersistenceError> { + let customization_id = match parse_aggregate_id(customization_id, NEW_PRODUCT_NON_UUID)? { + Some((val, _)) => return Ok(Some(val)), + None => Uuid::parse_str(customization_id).unwrap(), + }; + + let res = sqlx::query_as!( + CustomizationView, + "SELECT + name, + customization_id, + product_id, + deleted + FROM + cqrs_inventory_product_customizations_query + WHERE + customization_id = $1;", + customization_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + // let customizations = res.get_customizations(&self).await?; + Ok(Some(res)) + } + + async fn load_with_context( + &self, + customization_id: &str, + ) -> Result, PersistenceError> { + let customization_id = match parse_aggregate_id(customization_id, NEW_PRODUCT_NON_UUID)? { + Some(val) => return Ok(Some(val)), + None => Uuid::parse_str(customization_id).unwrap(), + }; + + let res = sqlx::query_as!( + CustomizationView, + "SELECT + name, + customization_id, + product_id, + deleted + FROM + cqrs_inventory_product_customizations_query + WHERE + customization_id = $1;", + customization_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + // let customizations = res.get_customizations(&self).await?; + + struct Context { + version: i64, + customization_id: Uuid, + } + + let ctx = sqlx::query_as!( + Context, + "SELECT + customization_id, version + FROM + cqrs_inventory_product_customizations_query + WHERE + customization_id = $1;", + customization_id + ) + .fetch_one(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + + let view_context = ViewContext::new(ctx.customization_id.to_string(), ctx.version); + Ok(Some((res, view_context))) + } + + async fn update_view( + &self, + view: CustomizationView, + context: ViewContext, + ) -> Result<(), PersistenceError> { + match context.version { + 0 => { + let version = context.version + 1; + sqlx::query!( + "INSERT INTO cqrs_inventory_product_customizations_query ( + version, + name, + customization_id, + product_id, + deleted + ) VALUES ( + $1, $2, $3, $4, $5 + );", + version, + view.name, + view.customization_id, + view.product_id, + view.deleted, + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + _ => { + let version = context.version + 1; + sqlx::query!( + "UPDATE + cqrs_inventory_product_customizations_query + SET + version = $1, + name = $2, + customization_id = $3, + product_id = $4, + deleted = $5;", + version, + view.name, + view.customization_id, + view.product_id, + view.deleted, + ) + .execute(&self.pool) + .await + .map_err(PostgresAggregateError::from)?; + } + } + + Ok(()) + } +} + +#[async_trait] +impl Query for InventoryDBPostgresAdapter { + async fn dispatch(&self, customization_id: &str, events: &[EventEnvelope]) { + let res = self + .load_with_context(&customization_id) + .await + .unwrap_or_else(|_| { + Some(( + CustomizationView::default(), + ViewContext::new(customization_id.into(), 0), + )) + }); + let (mut view, view_context): (CustomizationView, ViewContext) = res.unwrap(); + for event in events { + view.update(event); + } + self.update_view(view, view_context).await.unwrap(); + } +} diff --git a/src/inventory/adapters/output/db/postgres/mod.rs b/src/inventory/adapters/output/db/postgres/mod.rs index eac7d1e..550d113 100644 --- a/src/inventory/adapters/output/db/postgres/mod.rs +++ b/src/inventory/adapters/output/db/postgres/mod.rs @@ -13,6 +13,7 @@ mod category_name_exists_for_store; mod category_view; mod customization_id_exists; mod customization_name_exists_for_product; +mod customization_view; mod errors; mod product_id_exists; mod product_name_exists_for_category; diff --git a/src/inventory/domain/customization_aggregate.rs b/src/inventory/domain/customization_aggregate.rs new file mode 100644 index 0000000..597c2ec --- /dev/null +++ b/src/inventory/domain/customization_aggregate.rs @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::str::FromStr; + +use async_trait::async_trait; +use config::builder; +use cqrs_es::Aggregate; +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{commands::InventoryCommand, events::InventoryEvent}; +use crate::inventory::application::services::errors::*; +use crate::inventory::application::services::InventoryServicesInterface; + +#[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, +} + +#[async_trait] +impl Aggregate for Customization { + type Command = InventoryCommand; + type Event = InventoryEvent; + type Error = InventoryError; + type Services = std::sync::Arc; + + // This identifier should be unique to the system. + fn aggregate_type() -> String { + "inventory.product".to_string() + } + + // The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system + // so expect to use helper functions elsewhere to keep the code clean. + async fn handle( + &self, + command: Self::Command, + services: &Self::Services, + ) -> Result, Self::Error> { + match command { + InventoryCommand::AddCustomization(cmd) => { + let res = services.add_customization().add_customization(cmd).await?; + Ok(vec![InventoryEvent::CustomizationAdded(res)]) + } + _ => Ok(Vec::default()), + } + } + + fn apply(&mut self, event: Self::Event) { + match event { + InventoryEvent::CustomizationAdded(e) => { + *self = e.customization().clone(); + } + _ => (), + } + } +} + +//#[cfg(test)] +//mod aggregate_tests { +// use std::sync::Arc; +// +// use cqrs_es::test::TestFramework; +// +// use super::*; +// use crate::inventory::{ +// application::services::{add_product_service::tests::*, *}, +// domain::{ +// add_product_command::tests::get_command, commands::InventoryCommand, +// events::InventoryEvent, product_added_event::tests::get_event_from_command, +// }, +// }; +// use crate::tests::bdd::*; +// +// type ProductTestFramework = TestFramework; +// +// #[test] +// fn test_create_product() { +// let cmd = get_command(); +// let expected = get_event_from_command(&cmd); +// let expected = InventoryEvent::ProductAdded(expected); +// +// let mut services = MockInventoryServicesInterface::new(); +// services +// .expect_add_product() +// .times(IS_CALLED_ONLY_ONCE.unwrap()) +// .return_const(mock_add_product_service(IS_CALLED_ONLY_ONCE, cmd.clone())); +// +// ProductTestFramework::with(Arc::new(services)) +// .given_no_previous_events() +// .when(InventoryCommand::AddProduct(cmd)) +// .then_expect_events(vec![expected]); +// } +// +// fn test_helper(t: T, str_value: &str) -> bool +// where +// T: ToString + FromStr + std::fmt::Debug + PartialEq, +// ::Err: std::fmt::Debug, +// { +// println!("Testing type: {:?} against value {str_value}", t); +// assert_eq!(t.to_string(), str_value.to_string()); +// +// assert_eq!(T::from_str(str_value).unwrap(), t); +// +// assert_eq!(T::from_str(t.to_string().as_str()).unwrap(), t,); +// +// true +// } +// +// #[test] +// fn currency_to_string_from_str() { +// assert!(test_helper(Currency::INR, INR)); +// } +// +// #[test] +// fn quantity_unit_kilogram() { +// assert!(test_helper(QuantityUnit::Kilogram, KILO_GRAM)); +// } +// +// #[test] +// fn quantity_unit_gram() { +// assert!(test_helper(QuantityUnit::Gram, GRAM)); +// } +// +// #[test] +// fn quantity_unit_discrete_number() { +// assert!(test_helper(QuantityUnit::DiscreteNumber, DISCRETE_NUMBER)); +// } +// +// #[test] +// fn quantity_unit_milli_liter() { +// assert!(test_helper(QuantityUnit::MilliLiter, MILLI_LITER)); +// } +// +// #[test] +// fn quantity_unit_liter() { +// assert!(test_helper(QuantityUnit::Liter, LITER)); +// } +//} From d43d8683e96f12f24f97f209883846a276723145 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 16 Jul 2024 15:44:36 +0530 Subject: [PATCH 3/3] feat: define and impl add customization user case --- .../db/postgres/customization_id_exists.rs | 15 +- .../customization_name_exists_for_product.rs | 2 +- .../output/db/postgres/product_id_exists.rs | 4 +- .../adapters/output/db/postgres/store_view.rs | 10 +- .../services/add_category_service.rs | 1 + .../services/add_customization_service.rs | 203 ++++++++++++++++++ src/inventory/application/services/errors.rs | 1 + src/inventory/application/services/mod.rs | 7 + .../domain/add_customization_command.rs | 91 ++++++++ src/inventory/domain/commands.rs | 5 +- .../domain/customization_added_event.rs | 18 ++ src/inventory/domain/events.rs | 6 +- src/inventory/domain/mod.rs | 5 + 13 files changed, 350 insertions(+), 18 deletions(-) create mode 100644 src/inventory/application/services/add_customization_service.rs create mode 100644 src/inventory/domain/add_customization_command.rs create mode 100644 src/inventory/domain/customization_added_event.rs 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 0bc7b25..71b4355 100644 --- a/src/inventory/adapters/output/db/postgres/customization_id_exists.rs +++ b/src/inventory/adapters/output/db/postgres/customization_id_exists.rs @@ -32,6 +32,7 @@ impl CustomizationIDExistsDBPort for InventoryDBPostgresAdapter { pub mod tests { use super::*; + use crate::inventory::domain::customization_aggregate::*; use crate::utils::uuid::tests::UUID; @@ -45,14 +46,12 @@ pub mod tests { .unwrap(), ); - let customization = - CustomizationBuilder::default() - .name("cname".into()) - .customization_id(UUID) - .deleted(false) - .build() - .unwrap() - ; + let customization = CustomizationBuilder::default() + .name("customization_name".into()) + .customization_id(UUID) + .deleted(false) + .build() + .unwrap(); // state doesn't exist assert!(!db diff --git a/src/inventory/adapters/output/db/postgres/customization_name_exists_for_product.rs b/src/inventory/adapters/output/db/postgres/customization_name_exists_for_product.rs index ed7b710..76f52e5 100644 --- a/src/inventory/adapters/output/db/postgres/customization_name_exists_for_product.rs +++ b/src/inventory/adapters/output/db/postgres/customization_name_exists_for_product.rs @@ -7,7 +7,7 @@ use super::InventoryDBPostgresAdapter; use crate::inventory::application::port::output::db::{ customization_name_exists_for_product::*, errors::*, }; -use crate::inventory::domain::product_aggregate::*; +use crate::inventory::domain::customization_aggregate::*; #[async_trait::async_trait] impl CustomizationNameExistsForProductDBPort for InventoryDBPostgresAdapter { 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 9d98a1d..2738e55 100644 --- a/src/inventory/adapters/output/db/postgres/product_id_exists.rs +++ b/src/inventory/adapters/output/db/postgres/product_id_exists.rs @@ -33,9 +33,7 @@ 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_command, product_aggregate::*}; use crate::utils::uuid::tests::UUID; #[actix_rt::test] diff --git a/src/inventory/adapters/output/db/postgres/store_view.rs b/src/inventory/adapters/output/db/postgres/store_view.rs index ac8d6e1..8a118b7 100644 --- a/src/inventory/adapters/output/db/postgres/store_view.rs +++ b/src/inventory/adapters/output/db/postgres/store_view.rs @@ -212,12 +212,14 @@ mod tests { inventory::{ application::services::{ add_category_service::tests::mock_add_category_service, + add_customization_service::tests::mock_add_customization_service, add_product_service::tests::mock_add_product_service, add_store_service::AddStoreServiceBuilder, InventoryServicesBuilder, }, domain::{ - add_category_command::AddCategoryCommand, add_product_command::tests::get_command, - add_store_command::AddStoreCommand, commands::InventoryCommand, + add_category_command::AddCategoryCommand, add_customization_command, + add_product_command::tests::get_command, add_store_command::AddStoreCommand, + commands::InventoryCommand, }, }, tests::bdd::IS_NEVER_CALLED, @@ -254,6 +256,10 @@ mod tests { 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(), + )) .build() .unwrap(); diff --git a/src/inventory/application/services/add_category_service.rs b/src/inventory/application/services/add_category_service.rs index 5b0e9ef..5574614 100644 --- a/src/inventory/application/services/add_category_service.rs +++ b/src/inventory/application/services/add_category_service.rs @@ -29,6 +29,7 @@ pub type AddCategoryServiceObj = Arc; #[derive(Clone, Builder)] pub struct AddCategoryService { + // TODO: check if store ID exists db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj, db_category_id_exists: CategoryIDExistsDBPortObj, get_uuid: GetUUIDInterfaceObj, diff --git a/src/inventory/application/services/add_customization_service.rs b/src/inventory/application/services/add_customization_service.rs new file mode 100644 index 0000000..fcdaaee --- /dev/null +++ b/src/inventory/application/services/add_customization_service.rs @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::sync::Arc; + +use derive_builder::Builder; +use mockall::predicate::*; +use mockall::*; + +use super::errors::*; +use crate::inventory::{ + application::port::output::db::{ + customization_id_exists::{self, *}, + customization_name_exists_for_product::*, + product_id_exists::{self, *}, + product_name_exists_for_category::*, + }, + domain::{ + add_customization_command::AddCustomizationCommand, + customization_added_event::{self, *}, + customization_aggregate::*, + product_added_event::{self, ProductAddedEvent, ProductAddedEventBuilder}, + product_aggregate::*, + }, +}; +use crate::utils::uuid::*; + +#[automock] +#[async_trait::async_trait] +pub trait AddCustomizationUseCase: Send + Sync { + async fn add_customization( + &self, + cmd: AddCustomizationCommand, + ) -> InventoryResult; +} + +pub type AddCustomizationServiceObj = Arc; + +#[derive(Clone, Builder)] +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] +impl AddCustomizationUseCase for AddCustomizationService { + async fn add_customization( + &self, + cmd: AddCustomizationCommand, + ) -> InventoryResult { + if !self + .db_product_id_exists + .product_id_exists(cmd.product_id()) + .await? + { + return Err(InventoryError::ProductIDNotFound); + } + + let mut customization_id = self.get_uuid.get_uuid(); + loop { + if self + .db_customization_id_exists + .customization_id_exists(&customization_id) + .await? + { + customization_id = self.get_uuid.get_uuid(); + continue; + } else { + break; + } + } + + let customization = CustomizationBuilder::default() + .name(cmd.name().into()) + .deleted(false) + .customization_id(customization_id) + .build() + .unwrap(); + + if self + .db_customization_name_exists_for_product + .customization_name_exists_for_product(&customization, cmd.product_id()) + .await? + { + return Err(InventoryError::DuplicateCustomizationName); + } + + Ok(CustomizationAddedEventBuilder::default() + .customization(customization) + .product_id(cmd.product_id().clone()) + .build() + .unwrap()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use uuid::Uuid; + + use crate::inventory::domain::add_customization_command::tests::get_command; + use crate::utils::uuid::tests::UUID; + use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + + pub fn mock_add_customization_service( + times: Option, + cmd: AddCustomizationCommand, + ) -> AddCustomizationServiceObj { + let mut m = MockAddCustomizationUseCase::new(); + + let customization = CustomizationBuilder::default() + .name(cmd.name().into()) + .deleted(false) + .customization_id(UUID.clone()) + .build() + .unwrap(); + + let res = CustomizationAddedEventBuilder::default() + .customization(customization) + .product_id(cmd.product_id().clone()) + .build() + .unwrap(); + + if let Some(times) = times { + m.expect_add_customization() + .times(times) + .returning(move |_| Ok(res.clone())); + } else { + m.expect_add_customization() + .returning(move |_| Ok(res.clone())); + } + + Arc::new(m) + } + + #[actix_rt::test] + async fn test_service_product_doesnt_exist() { + let cmd = get_command(); + + let s = AddCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_customization_id_exists(mock_customization_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_ONCE), + ) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); + + let res = s.add_customization(cmd.clone()).await.unwrap(); + assert_eq!(res.customization().name(), cmd.name()); + assert_eq!(res.product_id(), cmd.product_id()); + // assert_eq!(customization_added_events.len(), cmd.customizations().len()); + } + + #[actix_rt::test] + async fn test_service_product_name_exists_for_store() { + let cmd = get_command(); + + let s = AddCustomizationServiceBuilder::default() + .db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) + .db_customization_id_exists(mock_customization_id_exists_db_port_false( + IS_CALLED_ONLY_ONCE, + )) + .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(); + + assert_eq!( + s.add_customization(cmd.clone()).await, + Err(InventoryError::DuplicateCustomizationName) + ) + } + + #[actix_rt::test] + async fn test_service_product_id_not_found() { + let cmd = get_command(); + + let s = AddCustomizationServiceBuilder::default() + .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_true(IS_NEVER_CALLED)) + .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(); + + assert_eq!( + s.add_customization(cmd.clone()).await, + Err(InventoryError::ProductIDNotFound) + ) + } +} diff --git a/src/inventory/application/services/errors.rs b/src/inventory/application/services/errors.rs index 06663f4..bc5b1d0 100644 --- a/src/inventory/application/services/errors.rs +++ b/src/inventory/application/services/errors.rs @@ -16,6 +16,7 @@ pub enum InventoryError { DuplicateStoreName, DuplicateProductName, DuplicateCustomizationName, + ProductIDNotFound, InternalError, } diff --git a/src/inventory/application/services/mod.rs b/src/inventory/application/services/mod.rs index 62788bf..704c116 100644 --- a/src/inventory/application/services/mod.rs +++ b/src/inventory/application/services/mod.rs @@ -10,6 +10,7 @@ pub mod errors; // services pub mod add_category_service; +pub mod add_customization_service; pub mod add_product_service; pub mod add_store_service; @@ -18,6 +19,7 @@ pub trait InventoryServicesInterface: Send + Sync { fn add_store(&self) -> add_store_service::AddStoreServiceObj; fn add_category(&self) -> add_category_service::AddCategoryServiceObj; fn add_product(&self) -> add_product_service::AddProductServiceObj; + fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj; } #[derive(Clone, Builder)] @@ -25,6 +27,7 @@ pub struct InventoryServices { add_store: add_store_service::AddStoreServiceObj, add_category: add_category_service::AddCategoryServiceObj, add_product: add_product_service::AddProductServiceObj, + add_customization: add_customization_service::AddCustomizationServiceObj, } impl InventoryServicesInterface for InventoryServices { @@ -37,4 +40,8 @@ impl InventoryServicesInterface for InventoryServices { fn add_product(&self) -> add_product_service::AddProductServiceObj { self.add_product.clone() } + + fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj { + self.add_customization.clone() + } } diff --git a/src/inventory/domain/add_customization_command.rs b/src/inventory/domain/add_customization_command.rs new file mode 100644 index 0000000..c79a64e --- /dev/null +++ b/src/inventory/domain/add_customization_command.rs @@ -0,0 +1,91 @@ +// 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}; +use uuid::Uuid; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddCustomizationCommandError { + NameIsEmpty, +} + +#[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)] +pub struct AddCustomizationCommand { + name: String, + product_id: Uuid, +} + +impl UnvalidatedAddCustomizationCommand { + pub fn validate(self) -> Result { + let name = self.name.trim().to_owned(); + if name.is_empty() { + return Err(AddCustomizationCommandError::NameIsEmpty); + } + + Ok(AddCustomizationCommand { + name, + product_id: self.product_id, + }) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use crate::utils::uuid::tests::UUID; + + pub fn get_command() -> AddCustomizationCommand { + UnvalidatedAddCustomizationCommandBuilder::default() + .name("foo".into()) + .product_id(UUID.clone()) + .build() + .unwrap() + .validate() + .unwrap() + } + + #[test] + fn test_cmd() { + let name = "foo"; + let product_id = UUID; + + let cmd = UnvalidatedAddCustomizationCommandBuilder::default() + .name(name.into()) + .product_id(product_id.clone()) + .build() + .unwrap() + .validate() + .unwrap(); + + assert_eq!(cmd.name(), name); + assert_eq!(cmd.product_id(), &product_id); + } + + #[test] + 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) + ); + } +} diff --git a/src/inventory/domain/commands.rs b/src/inventory/domain/commands.rs index 63ce9c5..d3f36ea 100644 --- a/src/inventory/domain/commands.rs +++ b/src/inventory/domain/commands.rs @@ -6,8 +6,8 @@ use mockall::predicate::*; use serde::{Deserialize, Serialize}; use super::{ - add_category_command::AddCategoryCommand, add_product_command::AddProductCommand, - add_store_command::AddStoreCommand, + add_category_command::AddCategoryCommand, add_customization_command::AddCustomizationCommand, + add_product_command::AddProductCommand, add_store_command::AddStoreCommand, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -15,4 +15,5 @@ pub enum InventoryCommand { AddCategory(AddCategoryCommand), AddStore(AddStoreCommand), AddProduct(AddProductCommand), + AddCustomization(AddCustomizationCommand), } diff --git a/src/inventory/domain/customization_added_event.rs b/src/inventory/domain/customization_added_event.rs new file mode 100644 index 0000000..d0913ff --- /dev/null +++ b/src/inventory/domain/customization_added_event.rs @@ -0,0 +1,18 @@ +// 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::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct CustomizationAddedEvent { + customization: Customization, + product_id: Uuid, +} diff --git a/src/inventory/domain/events.rs b/src/inventory/domain/events.rs index 03c64eb..e40bde1 100644 --- a/src/inventory/domain/events.rs +++ b/src/inventory/domain/events.rs @@ -6,8 +6,8 @@ use cqrs_es::DomainEvent; use serde::{Deserialize, Serialize}; use super::{ - category_added_event::*, product_added_event::ProductAddedEvent, - store_added_event::StoreAddedEvent, + category_added_event::*, customization_added_event::CustomizationAddedEvent, + product_added_event::ProductAddedEvent, store_added_event::StoreAddedEvent, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -15,6 +15,7 @@ pub enum InventoryEvent { CategoryAdded(CategoryAddedEvent), StoreAdded(StoreAddedEvent), ProductAdded(ProductAddedEvent), + CustomizationAdded(CustomizationAddedEvent), } impl DomainEvent for InventoryEvent { @@ -27,6 +28,7 @@ impl DomainEvent for InventoryEvent { InventoryEvent::CategoryAdded { .. } => "InventoryCategoryAdded", InventoryEvent::StoreAdded { .. } => "InventoryStoreAdded", InventoryEvent::ProductAdded { .. } => "InventoryProductAdded", + InventoryEvent::CustomizationAdded { .. } => "InventoryCustomizationAdded", }; e.to_string() diff --git a/src/inventory/domain/mod.rs b/src/inventory/domain/mod.rs index cb92726..7417b87 100644 --- a/src/inventory/domain/mod.rs +++ b/src/inventory/domain/mod.rs @@ -4,18 +4,23 @@ // aggregates pub mod category_aggregate; +pub mod customization_aggregate; pub mod product_aggregate; pub mod stock_aggregate; pub mod store_aggregate; // commands pub mod add_category_command; +pub mod add_customization_command; pub mod add_product_command; pub mod add_store_command; pub mod commands; +pub mod update_product_command; // events pub mod category_added_event; +pub mod customization_added_event; pub mod events; pub mod product_added_event; +pub mod product_updated_event; pub mod store_added_event;