feat: add product against categories #34

Merged
realaravinth merged 10 commits from add-product into master 2024-07-15 18:21:13 +05:30
3 changed files with 185 additions and 0 deletions
Showing only changes of commit 06066426d8 - Show all commits

View file

@ -0,0 +1,169 @@
// 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::{product_id_exists::*, product_name_exists_for_category::*},
domain::{
add_product_command::AddProductCommand,
product_added_event::{ProductAddedEvent, ProductAddedEventBuilder},
product_aggregate::*,
},
};
use crate::utils::uuid::*;
#[automock]
#[async_trait::async_trait]
pub trait AddProductUseCase: Send + Sync {
async fn add_product(&self, cmd: AddProductCommand) -> InventoryResult<ProductAddedEvent>;
}
pub type AddProductServiceObj = Arc<dyn AddProductUseCase>;
#[derive(Clone, Builder)]
pub struct AddProductService {
db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj,
db_product_id_exists: ProductIDExistsDBPortObj,
get_uuid: GetUUIDInterfaceObj,
}
#[async_trait::async_trait]
impl AddProductUseCase for AddProductService {
async fn add_product(&self, cmd: AddProductCommand) -> InventoryResult<ProductAddedEvent> {
let mut product_id = self.get_uuid.get_uuid();
loop {
if self
.db_product_id_exists
.product_id_exists(&product_id)
.await?
{
product_id = self.get_uuid.get_uuid();
continue;
} else {
break;
}
}
let product = ProductBuilder::default()
.name(cmd.name().into())
.description(cmd.description().as_ref().map(|s| s.to_string()))
.image(cmd.image().clone())
.sku_able(cmd.sku_able().clone())
.price(cmd.price().clone())
.category_id(cmd.category_id().clone())
.product_id(product_id)
.build()
.unwrap();
if self
.db_product_name_exists_for_category
.product_name_exists_for_category(&product)
.await?
{
return Err(InventoryError::DuplicateProductName);
}
Ok(ProductAddedEventBuilder::default()
.added_by_user(cmd.adding_by().clone())
.name(product.name().into())
.description(product.description().as_ref().map(|s| s.to_string()))
.image(product.image().clone())
.sku_able(product.sku_able().clone())
.price(product.price().clone())
.category_id(product.category_id().clone())
.product_id(product.product_id().clone())
.build()
.unwrap())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use uuid::Uuid;
use crate::inventory::domain::add_product_command::tests::get_command;
use crate::utils::uuid::tests::UUID;
use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid};
pub fn mock_add_product_service(
times: Option<usize>,
cmd: AddProductCommand,
) -> AddProductServiceObj {
let mut m = MockAddProductUseCase::new();
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())
.added_by_user(cmd.adding_by().clone())
.build()
.unwrap();
if let Some(times) = times {
m.expect_add_product()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_add_product().returning(move |_| Ok(res.clone()));
}
Arc::new(m)
}
#[actix_rt::test]
async fn test_service_product_doesnt_exist() {
let cmd = get_command();
let s = AddProductServiceBuilder::default()
.db_product_name_exists_for_category(
mock_product_name_exists_for_category_db_port_false(IS_CALLED_ONLY_ONCE),
)
.db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
let res = s.add_product(cmd.clone()).await.unwrap();
assert_eq!(res.name(), cmd.name());
assert_eq!(res.description(), cmd.description());
assert_eq!(res.image(), cmd.image());
assert_eq!(res.sku_able(), cmd.sku_able());
assert_eq!(res.price(), cmd.price());
assert_eq!(res.added_by_user(), cmd.adding_by());
assert_eq!(res.category_id(), cmd.category_id());
assert_eq!(res.product_id(), &UUID);
}
#[actix_rt::test]
async fn test_service_product_name_exists_for_store() {
let cmd = get_command();
let s = AddProductServiceBuilder::default()
.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_ONCE))
.db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.add_product(cmd.clone()).await,
Err(InventoryError::DuplicateProductName)
)
}
}

View file

@ -14,6 +14,7 @@ pub type InventoryResult<V> = Result<V, InventoryError>;
pub enum InventoryError { pub enum InventoryError {
DuplicateCategoryName, DuplicateCategoryName,
DuplicateStoreName, DuplicateStoreName,
DuplicateProductName,
InternalError, InternalError,
} }
@ -22,9 +23,18 @@ impl From<InventoryDBError> for InventoryError {
match value { match value {
InventoryDBError::DuplicateCategoryName => Self::DuplicateCategoryName, InventoryDBError::DuplicateCategoryName => Self::DuplicateCategoryName,
InventoryDBError::DuplicateStoreName => Self::DuplicateStoreName, InventoryDBError::DuplicateStoreName => Self::DuplicateStoreName,
InventoryDBError::DuplicateProductName => Self::DuplicateProductName,
InventoryDBError::DuplicateStoreID => { InventoryDBError::DuplicateStoreID => {
error!("DuplicateStoreID"); error!("DuplicateStoreID");
Self::InternalError Self::InternalError
},
InventoryDBError::DuplicateProductID => {
error!("DuplicateProductID");
Self::InternalError
},
InventoryDBError::DuplicateCategoryID => {
error!("DuplicateCategoryID");
Self::InternalError
} }
InventoryDBError::InternalError => Self::InternalError, InventoryDBError::InternalError => Self::InternalError,
} }

View file

@ -10,18 +10,21 @@ pub mod errors;
// services // services
pub mod add_category_service; pub mod add_category_service;
pub mod add_product_service;
pub mod add_store_service; pub mod add_store_service;
#[automock] #[automock]
pub trait InventoryServicesInterface: Send + Sync { 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;
} }
#[derive(Clone, Builder)] #[derive(Clone, Builder)]
pub struct InventoryServices { 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,
} }
impl InventoryServicesInterface for InventoryServices { impl InventoryServicesInterface for InventoryServices {
@ -31,4 +34,7 @@ impl InventoryServicesInterface for InventoryServices {
fn add_category(&self) -> add_category_service::AddCategoryServiceObj { fn add_category(&self) -> add_category_service::AddCategoryServiceObj {
self.add_category.clone() self.add_category.clone()
} }
fn add_product(&self) -> add_product_service::AddProductServiceObj {
self.add_product.clone()
}
} }