// 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, 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( Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, )] pub struct UnvalidatedAddProductCommand { name: String, description: Option, image: Option, category_id: Uuid, sku_able: bool, quantity: Quantity, price: Price, adding_by: Uuid, customizations: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] pub struct AddProductCommand { name: String, description: Option, image: Option, category_id: Uuid, sku_able: bool, price: Price, quantity: Quantity, adding_by: Uuid, customizations: Vec, } impl UnvalidatedAddProductCommand { 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(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, 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, }) } } #[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![ 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; 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 = UnvalidatedAddProductCommandBuilder::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) .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 = UnvalidatedAddProductCommandBuilder::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()) .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.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 = UnvalidatedAddProductCommandBuilder::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()) .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); } #[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 = UnvalidatedAddProductCommandBuilder::default() .name("".into()) .description(description.clone()) .image(image.clone()) .category_id(category_id.clone()) .adding_by(adding_by.clone()) .quantity(quantity) .sku_able(sku_able) .customizations(customizations) .price(price.clone()) .build() .unwrap(); // 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.clone()) .adding_by(adding_by.clone()) .quantity(quantity) .sku_able(sku_able) .customizations(customizations) .price(price.clone()) .build() .unwrap(); // AddProductCommandError::DuplicateCustomizationName assert_eq!( cmd.validate(), Err(AddProductCommandError::DuplicateCustomizationName) ) } }