feat: add product against categories #34
3 changed files with 185 additions and 0 deletions
169
src/inventory/application/services/add_product_service.rs
Normal file
169
src/inventory/application/services/add_product_service.rs
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ pub type InventoryResult<V> = Result<V, InventoryError>;
|
|||
pub enum InventoryError {
|
||||
DuplicateCategoryName,
|
||||
DuplicateStoreName,
|
||||
DuplicateProductName,
|
||||
InternalError,
|
||||
}
|
||||
|
||||
|
@ -22,9 +23,18 @@ impl From<InventoryDBError> for InventoryError {
|
|||
match value {
|
||||
InventoryDBError::DuplicateCategoryName => Self::DuplicateCategoryName,
|
||||
InventoryDBError::DuplicateStoreName => Self::DuplicateStoreName,
|
||||
InventoryDBError::DuplicateProductName => Self::DuplicateProductName,
|
||||
InventoryDBError::DuplicateStoreID => {
|
||||
error!("DuplicateStoreID");
|
||||
Self::InternalError
|
||||
},
|
||||
InventoryDBError::DuplicateProductID => {
|
||||
error!("DuplicateProductID");
|
||||
Self::InternalError
|
||||
},
|
||||
InventoryDBError::DuplicateCategoryID => {
|
||||
error!("DuplicateCategoryID");
|
||||
Self::InternalError
|
||||
}
|
||||
InventoryDBError::InternalError => Self::InternalError,
|
||||
}
|
||||
|
|
|
@ -10,18 +10,21 @@ pub mod errors;
|
|||
|
||||
// services
|
||||
pub mod add_category_service;
|
||||
pub mod add_product_service;
|
||||
pub mod add_store_service;
|
||||
|
||||
#[automock]
|
||||
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;
|
||||
}
|
||||
|
||||
#[derive(Clone, Builder)]
|
||||
pub struct InventoryServices {
|
||||
add_store: add_store_service::AddStoreServiceObj,
|
||||
add_category: add_category_service::AddCategoryServiceObj,
|
||||
add_product: add_product_service::AddProductServiceObj,
|
||||
}
|
||||
|
||||
impl InventoryServicesInterface for InventoryServices {
|
||||
|
@ -31,4 +34,7 @@ impl InventoryServicesInterface for InventoryServices {
|
|||
fn add_category(&self) -> add_category_service::AddCategoryServiceObj {
|
||||
self.add_category.clone()
|
||||
}
|
||||
fn add_product(&self) -> add_product_service::AddProductServiceObj {
|
||||
self.add_product.clone()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue