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 {
|
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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue