feat: check for Customization constraint violation #43

Merged
realaravinth merged 7 commits from customize-products into master 2024-07-16 11:42:37 +05:30
12 changed files with 444 additions and 12 deletions

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_inventory_product_customizations_query\n WHERE\n customization_id = $1\n );",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "3292856681e8d41391ba1d213b118a1f879677b14a2e6aad113b513d93b7c9a3"
}

View file

@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_inventory_product_customizations_query\n WHERE\n name = $1\n AND\n product_id = $2\n AND\n deleted = false\n );",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "78c1fa1b50afb781ed7c28c5454f43c61894b22bcde01097ca41ab691d985aea"
}

View file

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::InventoryDBPostgresAdapter;
use crate::inventory::application::port::output::db::{customization_id_exists::*, errors::*};
#[async_trait::async_trait]
impl CustomizationIDExistsDBPort for InventoryDBPostgresAdapter {
async fn customization_id_exists(&self, customization_id: &Uuid) -> InventoryDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_inventory_product_customizations_query
WHERE
customization_id = $1
);",
customization_id
)
.fetch_one(&self.pool)
.await?;
if let Some(x) = res.exists {
Ok(x)
} else {
Ok(false)
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::inventory::domain::add_product_command::tests::get_customizations;
use crate::inventory::domain::product_aggregate::*;
use crate::utils::uuid::tests::UUID;
#[actix_rt::test]
async fn test_postgres_customization_exists() {
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::InventoryDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let customization = {
let c = get_customizations().first().unwrap().clone();
CustomizationBuilder::default()
.name(c.name().into())
.customization_id(UUID)
.deleted(false)
.build()
.unwrap()
};
// state doesn't exist
assert!(!db
.customization_id_exists(customization.customization_id())
.await
.unwrap());
create_dummy_customization_record(&customization, &db).await;
// state exists
assert!(db
.customization_id_exists(customization.customization_id())
.await
.unwrap());
settings.drop_db().await;
}
pub async fn create_dummy_customization_record(
c: &Customization,
db: &InventoryDBPostgresAdapter,
) {
sqlx::query!(
"INSERT INTO cqrs_inventory_product_customizations_query (
version,
name,
customization_id,
product_id,
deleted
) VALUES (
$1, $2, $3, $4, $5
);",
1,
c.name(),
c.customization_id(),
UUID,
c.deleted().clone(),
)
.execute(&db.pool)
.await
.unwrap();
}
}

View file

@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::InventoryDBPostgresAdapter;
use crate::inventory::application::port::output::db::{
customization_name_exists_for_product::*, errors::*,
};
use crate::inventory::domain::product_aggregate::*;
#[async_trait::async_trait]
impl CustomizationNameExistsForProductDBPort for InventoryDBPostgresAdapter {
async fn customization_name_exists_for_product(
&self,
c: &Customization,
product_id: &Uuid,
) -> InventoryDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_inventory_product_customizations_query
WHERE
name = $1
AND
product_id = $2
AND
deleted = false
);",
c.name(),
product_id,
)
.fetch_one(&self.pool)
.await?;
if let Some(x) = res.exists {
Ok(x)
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inventory::adapters::output::db::postgres::customization_id_exists::tests::create_dummy_customization_record;
use crate::utils::uuid::tests::UUID;
#[actix_rt::test]
async fn test_postgres_customization_exists() {
let customization_name = "foo_customization";
let product_id = UUID;
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::InventoryDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let customization = {
CustomizationBuilder::default()
.name(customization_name.into())
.customization_id(UUID)
.deleted(false)
.build()
.unwrap()
};
// state doesn't exist
assert!(!db
.customization_name_exists_for_product(&customization, &product_id)
.await
.unwrap());
create_dummy_customization_record(&customization, &db).await;
// state exists
assert!(db
.customization_name_exists_for_product(&customization, &product_id)
.await
.unwrap());
// Set customization.deleted = true; now db.customization_name_exists_for_product must return false
sqlx::query!(
"UPDATE
cqrs_inventory_product_customizations_query
SET
deleted = true
WHERE
customization_id = $1
AND
product_id = $2
AND
name = $3;",
customization.customization_id(),
&product_id,
customization.name()
)
.execute(&db.pool)
.await
.unwrap();
assert!(!db
.customization_name_exists_for_product(&customization, &product_id)
.await
.unwrap());
settings.drop_db().await;
}
}

View file

@ -19,6 +19,10 @@ impl From<SqlxError> for InventoryDBError {
return Self::DuplicateStoreID;
} else if msg.contains("cqrs_inventory_store_query_product_id_key") {
return Self::DuplicateProductID;
} else if msg
.contains("cqrs_inventory_product_customizations_query_customization_id_key")
{
return Self::DuplicateCustomizationID;
} else if msg.contains("cqrs_inventory_store_query_category_id_key") {
return Self::DuplicateCategoryID;
} else if msg.contains("cqrs_inventory_product_query_name_key") {
@ -27,6 +31,8 @@ impl From<SqlxError> for InventoryDBError {
return Self::DuplicateProductName;
} else if msg.contains("cqrs_inventory_store_query_name_key") {
return Self::DuplicateStoreName;
} else if msg.contains("cqrs_inventory_product_customizations_query_name_key") {
return Self::DuplicateCustomizationName;
} else {
println!("{msg}");
}

View file

@ -11,6 +11,8 @@ use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres};
mod category_id_exists;
mod category_name_exists_for_store;
mod category_view;
mod customization_id_exists;
mod customization_name_exists_for_product;
mod errors;
mod product_id_exists;
mod product_name_exists_for_category;

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
use uuid::Uuid;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait CustomizationIDExistsDBPort: Send + Sync {
async fn customization_id_exists(&self, c: &Uuid) -> InventoryDBResult<bool>;
}
pub type CustomizationIDExistsDBPortObj = std::sync::Arc<dyn CustomizationIDExistsDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_customization_id_exists_db_port_false(
times: Option<usize>,
) -> CustomizationIDExistsDBPortObj {
let mut m = MockCustomizationIDExistsDBPort::new();
if let Some(times) = times {
m.expect_customization_id_exists()
.times(times)
.returning(|_| Ok(false));
} else {
m.expect_customization_id_exists().returning(|_| Ok(false));
}
Arc::new(m)
}
pub fn mock_customization_id_exists_db_port_true(
times: Option<usize>,
) -> CustomizationIDExistsDBPortObj {
let mut m = MockCustomizationIDExistsDBPort::new();
if let Some(times) = times {
m.expect_customization_id_exists()
.times(times)
.returning(|_| Ok(true));
} else {
m.expect_customization_id_exists().returning(|_| Ok(true));
}
Arc::new(m)
}
}

View file

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
use uuid::Uuid;
use crate::inventory::domain::product_aggregate::Customization;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait CustomizationNameExistsForProductDBPort: Send + Sync {
async fn customization_name_exists_for_product(
&self,
c: &Customization,
product_id: &Uuid,
) -> InventoryDBResult<bool>;
}
pub type CustomizationNameExistsForProductDBPortObj =
std::sync::Arc<dyn CustomizationNameExistsForProductDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_customization_name_exists_for_product_db_port_false(
times: Option<usize>,
) -> CustomizationNameExistsForProductDBPortObj {
let mut m = MockCustomizationNameExistsForProductDBPort::new();
if let Some(times) = times {
m.expect_customization_name_exists_for_product()
.times(times)
.returning(|_, _| Ok(false));
} else {
m.expect_customization_name_exists_for_product()
.returning(|_, _| Ok(false));
}
Arc::new(m)
}
pub fn mock_customization_name_exists_for_product_db_port_true(
times: Option<usize>,
) -> CustomizationNameExistsForProductDBPortObj {
let mut m = MockCustomizationNameExistsForProductDBPort::new();
if let Some(times) = times {
m.expect_customization_name_exists_for_product()
.times(times)
.returning(|_, _| Ok(true));
} else {
m.expect_customization_name_exists_for_product()
.returning(|_, _| Ok(true));
}
Arc::new(m)
}
}

View file

@ -15,5 +15,7 @@ pub enum InventoryDBError {
DuplicateStoreID,
DuplicateProductName,
DuplicateProductID,
DuplicateCustomizationID,
DuplicateCustomizationName,
InternalError,
}

View file

@ -4,6 +4,8 @@
pub mod category_id_exists;
pub mod category_name_exists_for_store;
pub mod customization_id_exists;
pub mod customization_name_exists_for_product;
pub mod errors;
pub mod product_id_exists;
pub mod product_name_exists_for_category;

View file

@ -10,7 +10,12 @@ use mockall::*;
use super::errors::*;
use crate::inventory::{
application::port::output::db::{product_id_exists::*, product_name_exists_for_category::*},
application::port::output::db::{
customization_id_exists::{self, *},
customization_name_exists_for_product::*,
product_id_exists::*,
product_name_exists_for_category::*,
},
domain::{
add_product_command::AddProductCommand,
product_added_event::{ProductAddedEvent, ProductAddedEventBuilder},
@ -31,6 +36,8 @@ pub type AddProductServiceObj = Arc<dyn AddProductUseCase>;
pub struct AddProductService {
db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj,
db_product_id_exists: ProductIDExistsDBPortObj,
db_customization_id_exists: CustomizationIDExistsDBPortObj,
db_customization_name_exists_for_product: CustomizationNameExistsForProductDBPortObj,
get_uuid: GetUUIDInterfaceObj,
}
@ -55,19 +62,35 @@ impl AddProductUseCase for AddProductService {
let mut customizations = Vec::with_capacity(cmd.customizations().len());
for c in cmd.customizations().iter() {
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;
}
}
// TODO:
// 1. check if customization.name exists for product
// 2. check customization.customization_id duplicate in query table
let customization = CustomizationBuilder::default()
.name(c.name().into())
.deleted(false)
.customization_id(customization_id)
.build()
.unwrap();
customizations.push(
CustomizationBuilder::default()
.name(c.name().into())
.deleted(false)
.customization_id(customization_id)
.build()
.unwrap(),
);
if self
.db_customization_name_exists_for_product
.customization_name_exists_for_product(&customization, &product_id)
.await?
{
return Err(InventoryError::DuplicateCustomizationName);
}
customizations.push(customization);
}
let product = ProductBuilder::default()
@ -168,7 +191,13 @@ pub mod tests {
.db_product_name_exists_for_category(
mock_product_name_exists_for_category_db_port_false(IS_CALLED_ONLY_ONCE),
)
.db_customization_id_exists(mock_customization_id_exists_db_port_false(
IS_CALLED_ONLY_THRICE,
))
.db_product_id_exists(mock_product_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_THRICE),
)
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_FOUR_TIMES))
.build()
.unwrap();
@ -195,6 +224,12 @@ pub mod tests {
)
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_FOUR_TIMES))
.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_false(
IS_CALLED_ONLY_THRICE,
))
.db_customization_name_exists_for_product(
mock_customization_name_exists_for_product_db_port_false(IS_CALLED_ONLY_THRICE),
)
.build()
.unwrap();

View file

@ -15,6 +15,7 @@ pub enum InventoryError {
DuplicateCategoryName,
DuplicateStoreName,
DuplicateProductName,
DuplicateCustomizationName,
InternalError,
}
@ -24,6 +25,7 @@ impl From<InventoryDBError> for InventoryError {
InventoryDBError::DuplicateCategoryName => Self::DuplicateCategoryName,
InventoryDBError::DuplicateStoreName => Self::DuplicateStoreName,
InventoryDBError::DuplicateProductName => Self::DuplicateProductName,
InventoryDBError::DuplicateCustomizationName => Self::DuplicateCustomizationName,
InventoryDBError::DuplicateStoreID => {
error!("DuplicateStoreID");
Self::InternalError
@ -36,6 +38,10 @@ impl From<InventoryDBError> for InventoryError {
error!("DuplicateCategoryID");
Self::InternalError
}
InventoryDBError::DuplicateCustomizationID => {
error!("DuplicateCustomizationID");
Self::InternalError
}
InventoryDBError::InternalError => Self::InternalError,
}
}