feat: define and impl add customization user case
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful

This commit is contained in:
Aravinth Manivannan 2024-07-16 15:44:36 +05:30
parent f189ecbf38
commit d43d8683e9
Signed by: realaravinth
GPG key ID: F8F50389936984FF
13 changed files with 350 additions and 18 deletions

View file

@ -32,6 +32,7 @@ impl CustomizationIDExistsDBPort for InventoryDBPostgresAdapter {
pub mod tests { pub mod tests {
use super::*; use super::*;
use crate::inventory::domain::customization_aggregate::*; use crate::inventory::domain::customization_aggregate::*;
use crate::utils::uuid::tests::UUID; use crate::utils::uuid::tests::UUID;
@ -45,14 +46,12 @@ pub mod tests {
.unwrap(), .unwrap(),
); );
let customization = let customization = CustomizationBuilder::default()
CustomizationBuilder::default() .name("customization_name".into())
.name("cname".into()) .customization_id(UUID)
.customization_id(UUID) .deleted(false)
.deleted(false) .build()
.build() .unwrap();
.unwrap()
;
// state doesn't exist // state doesn't exist
assert!(!db assert!(!db

View file

@ -7,7 +7,7 @@ use super::InventoryDBPostgresAdapter;
use crate::inventory::application::port::output::db::{ use crate::inventory::application::port::output::db::{
customization_name_exists_for_product::*, errors::*, customization_name_exists_for_product::*, errors::*,
}; };
use crate::inventory::domain::product_aggregate::*; use crate::inventory::domain::customization_aggregate::*;
#[async_trait::async_trait] #[async_trait::async_trait]
impl CustomizationNameExistsForProductDBPort for InventoryDBPostgresAdapter { impl CustomizationNameExistsForProductDBPort for InventoryDBPostgresAdapter {

View file

@ -33,9 +33,7 @@ pub mod tests {
use super::*; use super::*;
// use crate::inventory::domain::add_product_command::tests::get_customizations; // use crate::inventory::domain::add_product_command::tests::get_customizations;
use crate::inventory::domain::{ use crate::inventory::domain::{add_product_command::tests::get_command, product_aggregate::*};
add_product_command::tests::get_command, product_aggregate::*,
};
use crate::utils::uuid::tests::UUID; use crate::utils::uuid::tests::UUID;
#[actix_rt::test] #[actix_rt::test]

View file

@ -212,12 +212,14 @@ mod tests {
inventory::{ inventory::{
application::services::{ application::services::{
add_category_service::tests::mock_add_category_service, 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_product_service::tests::mock_add_product_service,
add_store_service::AddStoreServiceBuilder, InventoryServicesBuilder, add_store_service::AddStoreServiceBuilder, InventoryServicesBuilder,
}, },
domain::{ domain::{
add_category_command::AddCategoryCommand, add_product_command::tests::get_command, add_category_command::AddCategoryCommand, add_customization_command,
add_store_command::AddStoreCommand, commands::InventoryCommand, add_product_command::tests::get_command, add_store_command::AddStoreCommand,
commands::InventoryCommand,
}, },
}, },
tests::bdd::IS_NEVER_CALLED, tests::bdd::IS_NEVER_CALLED,
@ -254,6 +256,10 @@ mod tests {
AddCategoryCommand::new("foo".into(), None, UUID, UUID).unwrap(), AddCategoryCommand::new("foo".into(), None, UUID, UUID).unwrap(),
)) ))
.add_product(mock_add_product_service(IS_NEVER_CALLED, get_command())) .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() .build()
.unwrap(); .unwrap();

View file

@ -29,6 +29,7 @@ pub type AddCategoryServiceObj = Arc<dyn AddCategoryUseCase>;
#[derive(Clone, Builder)] #[derive(Clone, Builder)]
pub struct AddCategoryService { pub struct AddCategoryService {
// TODO: check if store ID exists
db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj, db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj,
db_category_id_exists: CategoryIDExistsDBPortObj, db_category_id_exists: CategoryIDExistsDBPortObj,
get_uuid: GetUUIDInterfaceObj, get_uuid: GetUUIDInterfaceObj,

View file

@ -0,0 +1,203 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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<CustomizationAddedEvent>;
}
pub type AddCustomizationServiceObj = Arc<dyn AddCustomizationUseCase>;
#[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<CustomizationAddedEvent> {
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<usize>,
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)
)
}
}

View file

@ -16,6 +16,7 @@ pub enum InventoryError {
DuplicateStoreName, DuplicateStoreName,
DuplicateProductName, DuplicateProductName,
DuplicateCustomizationName, DuplicateCustomizationName,
ProductIDNotFound,
InternalError, InternalError,
} }

View file

@ -10,6 +10,7 @@ pub mod errors;
// services // services
pub mod add_category_service; pub mod add_category_service;
pub mod add_customization_service;
pub mod add_product_service; pub mod add_product_service;
pub mod add_store_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_store(&self) -> add_store_service::AddStoreServiceObj;
fn add_category(&self) -> add_category_service::AddCategoryServiceObj; fn add_category(&self) -> add_category_service::AddCategoryServiceObj;
fn add_product(&self) -> add_product_service::AddProductServiceObj; fn add_product(&self) -> add_product_service::AddProductServiceObj;
fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj;
} }
#[derive(Clone, Builder)] #[derive(Clone, Builder)]
@ -25,6 +27,7 @@ pub struct InventoryServices {
add_store: add_store_service::AddStoreServiceObj, add_store: add_store_service::AddStoreServiceObj,
add_category: add_category_service::AddCategoryServiceObj, add_category: add_category_service::AddCategoryServiceObj,
add_product: add_product_service::AddProductServiceObj, add_product: add_product_service::AddProductServiceObj,
add_customization: add_customization_service::AddCustomizationServiceObj,
} }
impl InventoryServicesInterface for InventoryServices { impl InventoryServicesInterface for InventoryServices {
@ -37,4 +40,8 @@ impl InventoryServicesInterface for InventoryServices {
fn add_product(&self) -> add_product_service::AddProductServiceObj { fn add_product(&self) -> add_product_service::AddProductServiceObj {
self.add_product.clone() self.add_product.clone()
} }
fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj {
self.add_customization.clone()
}
} }

View file

@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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<AddCustomizationCommand, AddCustomizationCommandError> {
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)
);
}
}

View file

@ -6,8 +6,8 @@ use mockall::predicate::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{ use super::{
add_category_command::AddCategoryCommand, add_product_command::AddProductCommand, add_category_command::AddCategoryCommand, add_customization_command::AddCustomizationCommand,
add_store_command::AddStoreCommand, add_product_command::AddProductCommand, add_store_command::AddStoreCommand,
}; };
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
@ -15,4 +15,5 @@ pub enum InventoryCommand {
AddCategory(AddCategoryCommand), AddCategory(AddCategoryCommand),
AddStore(AddStoreCommand), AddStore(AddStoreCommand),
AddProduct(AddProductCommand), AddProduct(AddProductCommand),
AddCustomization(AddCustomizationCommand),
} }

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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,
}

View file

@ -6,8 +6,8 @@ use cqrs_es::DomainEvent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{ use super::{
category_added_event::*, product_added_event::ProductAddedEvent, category_added_event::*, customization_added_event::CustomizationAddedEvent,
store_added_event::StoreAddedEvent, product_added_event::ProductAddedEvent, store_added_event::StoreAddedEvent,
}; };
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
@ -15,6 +15,7 @@ pub enum InventoryEvent {
CategoryAdded(CategoryAddedEvent), CategoryAdded(CategoryAddedEvent),
StoreAdded(StoreAddedEvent), StoreAdded(StoreAddedEvent),
ProductAdded(ProductAddedEvent), ProductAdded(ProductAddedEvent),
CustomizationAdded(CustomizationAddedEvent),
} }
impl DomainEvent for InventoryEvent { impl DomainEvent for InventoryEvent {
@ -27,6 +28,7 @@ impl DomainEvent for InventoryEvent {
InventoryEvent::CategoryAdded { .. } => "InventoryCategoryAdded", InventoryEvent::CategoryAdded { .. } => "InventoryCategoryAdded",
InventoryEvent::StoreAdded { .. } => "InventoryStoreAdded", InventoryEvent::StoreAdded { .. } => "InventoryStoreAdded",
InventoryEvent::ProductAdded { .. } => "InventoryProductAdded", InventoryEvent::ProductAdded { .. } => "InventoryProductAdded",
InventoryEvent::CustomizationAdded { .. } => "InventoryCustomizationAdded",
}; };
e.to_string() e.to_string()

View file

@ -4,18 +4,23 @@
// aggregates // aggregates
pub mod category_aggregate; pub mod category_aggregate;
pub mod customization_aggregate;
pub mod product_aggregate; pub mod product_aggregate;
pub mod stock_aggregate; pub mod stock_aggregate;
pub mod store_aggregate; pub mod store_aggregate;
// commands // commands
pub mod add_category_command; pub mod add_category_command;
pub mod add_customization_command;
pub mod add_product_command; pub mod add_product_command;
pub mod add_store_command; pub mod add_store_command;
pub mod commands; pub mod commands;
pub mod update_product_command;
// events // events
pub mod category_added_event; pub mod category_added_event;
pub mod customization_added_event;
pub mod events; pub mod events;
pub mod product_added_event; pub mod product_added_event;
pub mod product_updated_event;
pub mod store_added_event; pub mod store_added_event;