feat: import inventory services&domain obj to implement pantry
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-09-16 14:54:23 +05:30
parent cb778aa89e
commit cddba71eb5
Signed by: realaravinth
GPG key ID: F8F50389936984FF
99 changed files with 7003 additions and 24 deletions

View file

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

View file

@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_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": "2f5ec6062904e7124f56a237e80922537b577f2e7aeafd0708578bf62f8423db"
}

View file

@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO cqrs_ordering_store_query (\n version, name, address, store_id, owner, deleted\n ) VALUES (\n $1, $2, $3, $4, $5, $6\n );",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Text",
"Uuid",
"Uuid",
"Bool"
]
},
"nullable": []
},
"hash": "37334f91e68e9d95bc9675d98be833c7763ff3e1eb368456fd41e1208863b9b2"
}

View file

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

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n store_id, version\n FROM\n cqrs_ordering_store_query\n WHERE\n store_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "store_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "version",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "5568e35d9a0b7acb1c0b1f65015e274c0b064a370dda1c436c0e598ffd6ba599"
}

View file

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

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n name, description, category_id, store_id, deleted\n FROM\n cqrs_ordering_category_query\n WHERE\n category_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "category_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "store_id",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "deleted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
false,
false,
false
]
},
"hash": "5d3972a89f5d64e0c9cbe3a086401889c7f2c4cc7eda9e6a7c0f501c103ab9ac"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n customization_id, version\n FROM\n cqrs_ordering_product_customizations_query\n WHERE\n customization_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "customization_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "version",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "6f06c0f4b71f0458229dad370d46da62e933651ffbe39ee27e5e3dd078d86e05"
}

View file

@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO cqrs_ordering_category_query (\n version, name, description, category_id, store_id, deleted\n ) VALUES (\n $1, $2, $3, $4, $5, $6\n );",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Text",
"Uuid",
"Uuid",
"Bool"
]
},
"nullable": []
},
"hash": "7cb52847f00e985c9475485de63c6671f35d27bde39be34677a6b74228bf3e97"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_store_query\n WHERE\n name = $1\n AND\n deleted = false\n );",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "81de3abeb5dcbe7a87e20bab82dc6a258b3017a710f8c3e249d73a5d980e2115"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_ordering_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": "89387ed4e97c8c957e576b2753533112bf6ea3eb662460776fcaa625046b7a0d"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n category_id, version\n FROM\n cqrs_ordering_category_query\n WHERE\n category_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "category_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "version",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "8f83767550a0efbb13020cff6b0b976d0756b470f448394d4832866b5a209ecd"
}

View file

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

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n name, address, store_id, owner, deleted\n FROM\n cqrs_ordering_store_query\n WHERE\n store_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "address",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "store_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "owner",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "deleted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
false,
false,
false
]
},
"hash": "cae7149b31d542cc01d263d682510e60f44de01ccb095e63542c11e5b2386ee5"
}

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n name, description, store_id, category_id, deleted\n FROM\n cqrs_ordering_category_query\n WHERE\n category_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "store_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "category_id",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "deleted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
false,
false,
false
]
},
"hash": "cfab77a90a7a7f3d74b739442d9a75a065ad6a9a7a74432bcc3b0d6802af1eb2"
}

View file

@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE\n cqrs_ordering_store_query\n SET\n version = $1,\n name = $2,\n address = $3,\n store_id = $4,\n owner = $5,\n deleted = $6;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Text",
"Uuid",
"Uuid",
"Bool"
]
},
"nullable": []
},
"hash": "d0580ff6dc77150cb00186302f20460dc3be59be1f8f5588bdc3d0f4489eb613"
}

View file

@ -0,0 +1,40 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n name,\n customization_id,\n product_id,\n deleted\n FROM\n cqrs_ordering_product_customizations_query\n WHERE\n customization_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "customization_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "product_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "deleted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "d5eeb278addc02f44a2a799a0ba03226d1f03935d73dc3bfc1af490df4726c78"
}

View file

@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO cqrs_ordering_product_customizations_query (\n version,\n name,\n customization_id,\n product_id,\n deleted\n ) VALUES (\n $1, $2, $3, $4, $5\n );",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Uuid",
"Uuid",
"Bool"
]
},
"nullable": []
},
"hash": "d873aaab136d804c0c0c1744d6914f616f0f8987c81a0d1d3c8d923b580b2ae1"
}

View file

@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE\n cqrs_ordering_category_query\n SET\n version = $1,\n name = $2,\n description = $3,\n category_id = $4,\n store_id = $5,\n deleted = $6;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Text",
"Uuid",
"Uuid",
"Bool"
]
},
"nullable": []
},
"hash": "d896f6ffb486efad5ed10a9c824656d863de0c9140054de66eef32491ace9ddb"
}

View file

@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE\n cqrs_ordering_product_customizations_query\n SET\n version = $1,\n name = $2,\n customization_id = $3,\n product_id = $4,\n deleted = $5;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Uuid",
"Uuid",
"Bool"
]
},
"nullable": []
},
"hash": "d9c625876e7d398cb48c6278e69b2eb6ad8515e68d5520013634415109309e6e"
}

View file

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

View file

@ -0,0 +1,16 @@
--- SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
CREATE TABLE IF NOT EXISTS cqrs_ordering_store_query
(
version bigint CHECK (version >= 0) NOT NULL,
name TEXT NOT NULL,
address TEXT,
owner UUID NOT NULL,
store_id UUID NOT NULL UNIQUE,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (store_id)
);

View file

@ -0,0 +1,17 @@
-- SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
CREATE TABLE IF NOT EXISTS cqrs_ordering_category_query
(
version bigint CHECK (version >= 0) NOT NULL,
name TEXT NOT NULL,
description TEXT,
store_id UUID NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
category_id UUID NOT NULL UNIQUE,
UNIQUE(store_id, name),
PRIMARY KEY (category_id)
);

View file

@ -0,0 +1,14 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS cqrs_ordering_product_customizations_query
(
version bigint CHECK (version >= 0) NOT NULL,
name TEXT NOT NULL,
customization_id UUID NOT NULL UNIQUE,
product_id UUID NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE(product_id, name),
PRIMARY KEY (customization_id)
);

View file

@ -0,0 +1,29 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS cqrs_ordering_product_query
(
version bigint CHECK (version >= 0) NOT NULL,
name TEXT NOT NULL,
description TEXT,
image TEXT,
sku_able BOOLEAN NOT NULL DEFAULT FALSE,
product_id UUID NOT NULL UNIQUE,
price_minor INTEGER NOT NULL,
price_major INTEGER NOT NULL,
price_currency TEXT NOT NULL,
quantity_major_number INTEGER NOT NULL,
quantity_minor_number INTEGER NOT NULL,
quantity_major_unit TEXT NOT NULL,
quantity_minor_unit TEXT NOT NULL,
category_id UUID NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE(category_id, name),
PRIMARY KEY (product_id)
)

View file

@ -15,7 +15,7 @@ use super::InventoryDBPostgresAdapter;
use crate::inventory::domain::{customization_aggregate::*, events::InventoryEvent};
use crate::utils::parse_aggregate_id::parse_aggregate_id;
pub const NEW_PRODUCT_NON_UUID: &str = "new_product_non_uuid-asdfa";
pub const NEW_CUSTOMIZATION_NON_UUID: &str = "new_customization_non_uuid-asdfa";
//#[derive(Debug, Default, Serialize, Deserialize)]
//struct Customizations {
@ -66,10 +66,11 @@ impl ViewRepository<CustomizationView, Customization> for InventoryDBPostgresAda
&self,
customization_id: &str,
) -> Result<Option<CustomizationView>, PersistenceError> {
let customization_id = match parse_aggregate_id(customization_id, NEW_PRODUCT_NON_UUID)? {
Some((val, _)) => return Ok(Some(val)),
None => Uuid::parse_str(customization_id).unwrap(),
};
let customization_id =
match parse_aggregate_id(customization_id, NEW_CUSTOMIZATION_NON_UUID)? {
Some((val, _)) => return Ok(Some(val)),
None => Uuid::parse_str(customization_id).unwrap(),
};
let res = sqlx::query_as!(
CustomizationView,
@ -96,10 +97,11 @@ impl ViewRepository<CustomizationView, Customization> for InventoryDBPostgresAda
&self,
customization_id: &str,
) -> Result<Option<(CustomizationView, ViewContext)>, PersistenceError> {
let customization_id = match parse_aggregate_id(customization_id, NEW_PRODUCT_NON_UUID)? {
Some(val) => return Ok(Some(val)),
None => Uuid::parse_str(customization_id).unwrap(),
};
let customization_id =
match parse_aggregate_id(customization_id, NEW_CUSTOMIZATION_NON_UUID)? {
Some(val) => return Ok(Some(val)),
None => Uuid::parse_str(customization_id).unwrap(),
};
let res = sqlx::query_as!(
CustomizationView,

View file

@ -14,8 +14,8 @@ use super::errors::*;
use super::InventoryDBPostgresAdapter;
use crate::inventory::domain::events::InventoryEvent;
use crate::inventory::domain::product_aggregate::{Product, ProductBuilder};
use crate::types::quantity::*;
use crate::types::currency::*;
use crate::types::quantity::*;
use crate::utils::parse_aggregate_id::parse_aggregate_id;
pub const NEW_PRODUCT_NON_UUID: &str = "new_product_non_uuid-asdfa";

View file

@ -8,8 +8,8 @@ use derive_more::{Display, Error};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::types::quantity::*;
use crate::types::currency::*;
use crate::types::quantity::*;
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum AddProductCommandError {

View file

@ -7,8 +7,8 @@ use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::types::quantity::Quantity;
use crate::types::currency::*;
use crate::types::quantity::Quantity;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,

View file

@ -12,8 +12,8 @@ use uuid::Uuid;
use super::{commands::InventoryCommand, events::InventoryEvent};
use crate::inventory::application::services::errors::*;
use crate::inventory::application::services::InventoryServicesInterface;
use crate::types::quantity::Quantity;
use crate::types::currency::*;
use crate::types::quantity::Quantity;
#[derive(
Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,

View file

@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::product_aggregate::Product;
use crate::types::quantity::Quantity;
use crate::types::currency::*;
use crate::types::quantity::Quantity;
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum UpdateProductCommandError {

View file

@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::InventoryDBPostgresAdapter;
use crate::inventory::application::port::output::db::{
errors::*, store_id_exists::*,
};
use crate::inventory::domain::store_aggregate::*;
#[async_trait::async_trait]
impl StoreIDExistsDBPort for InventoryDBPostgresAdapter {
async fn store_id_exists(&self, s: &Store) -> InventoryDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_inventory_store_query
WHERE
store_id = $1
);",
s.store_id(),
)
.fetch_one(&self.pool)
.await?;
if let Some(x) = res.exists {
Ok(x)
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use super::*;
#[actix_rt::test]
async fn test_postgres_store_exists() {
let store_id = Uuid::new_v4();
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 store = StoreBuilder::default().name("store_name".into()).owner("store_owner".into())
.address(Some("store_address".into()))
.store_id(store_id)
.build().unwrap();
// state doesn't exist
assert!(!db.store_id_exists(&store).await.unwrap());
sqlx::query!(
"INSERT INTO cqrs_inventory_store_query
(view_id, version, name, address, store_id, owner)
VALUES ($1, $2, $3, $4, $5, $6);",
"1",
1,
store.name(),
store.address().as_ref().unwrap(),
store.store_id(),
store.owner(),
)
.execute(&db.pool)
.await
.unwrap();
// state exists
assert!(db.store_id_exists(&store).await.unwrap());
settings.drop_db().await;
}
}

View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::OrderingDBPostgresAdapter;
use crate::ordering::application::port::output::db::{category_id_exists::*, errors::*};
use crate::ordering::domain::category_aggregate::*;
#[async_trait::async_trait]
impl CategoryIDExistsDBPort for OrderingDBPostgresAdapter {
async fn category_id_exists(&self, category_id: &Uuid) -> OrderingDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_ordering_category_query
WHERE
category_id = $1
);",
category_id,
)
.fetch_one(&self.pool)
.await?;
if let Some(x) = res.exists {
Ok(x)
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use crate::ordering::adapters::output::db::category_name_exists_for_store::tests::create_dummy_category_record;
use super::*;
#[actix_rt::test]
async fn test_postgres_category_exists() {
let category_id = Uuid::new_v4();
let store_id = Uuid::new_v4();
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::OrderingDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let category = CategoryBuilder::default()
.name("category_name".into())
.description(Some("category_description".into()))
.category_id(category_id)
.store_id(store_id)
.build()
.unwrap();
// state doesn't exist
assert!(!db.category_id_exists(category.category_id()).await.unwrap());
create_dummy_category_record(&category, &db).await;
// state exists
assert!(db.category_id_exists(category.category_id()).await.unwrap());
settings.drop_db().await;
}
}

View file

@ -0,0 +1,102 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::OrderingDBPostgresAdapter;
use crate::ordering::application::port::output::db::{
category_name_exists_for_store::*, errors::*,
};
use crate::ordering::domain::category_aggregate::*;
#[async_trait::async_trait]
impl CategoryNameExistsForStoreDBPort for OrderingDBPostgresAdapter {
async fn category_name_exists_for_store(&self, s: &Category) -> OrderingDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_ordering_category_query
WHERE
name = $1
AND
store_id = $2
AND
deleted = false
);",
s.name(),
s.store_id(),
)
.fetch_one(&self.pool)
.await?;
if let Some(x) = res.exists {
Ok(x)
} else {
Ok(false)
}
}
}
#[cfg(test)]
pub mod tests {
use uuid::Uuid;
use super::*;
pub async fn create_dummy_category_record(c: &Category, db: &OrderingDBPostgresAdapter) {
sqlx::query!(
"INSERT INTO cqrs_ordering_category_query
(version, name, description, category_id, store_id, deleted)
VALUES ($1, $2, $3, $4, $5, $6);",
1,
c.name(),
c.description().as_ref().unwrap(),
c.category_id(),
c.store_id(),
c.deleted().clone(),
)
.execute(&db.pool)
.await
.unwrap();
}
#[actix_rt::test]
async fn test_postgres_category_exists() {
let category_id = Uuid::new_v4();
let store_id = Uuid::new_v4();
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::OrderingDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let category = CategoryBuilder::default()
.name("category_name".into())
.description(Some("category_description".into()))
.category_id(category_id)
.store_id(store_id)
.build()
.unwrap();
// state doesn't exist
assert!(!db.category_name_exists_for_store(&category).await.unwrap());
create_dummy_category_record(&category, &db).await;
// state exists
assert!(db.category_name_exists_for_store(&category).await.unwrap());
// Set category.deleted = true; now db.category_name_exists_for_store must return false
sqlx::query!(
"UPDATE cqrs_ordering_category_query SET deleted = true WHERE category_id = $1;",
category.category_id(),
)
.execute(&db.pool)
.await
.unwrap();
assert!(!db.category_name_exists_for_store(&category).await.unwrap());
settings.drop_db().await;
}
}

View file

@ -0,0 +1,201 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use async_trait::async_trait;
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
use cqrs_es::{EventEnvelope, Query, View};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::errors::*;
use super::OrderingDBPostgresAdapter;
use crate::ordering::domain::category_aggregate::Category;
use crate::ordering::domain::events::OrderingEvent;
use crate::utils::parse_aggregate_id::parse_aggregate_id;
pub const NEW_CATEGORY_NON_UUID: &str = "ordering_new_category_non_uuid-asdfa";
// The view for a Category query, for a standard http application this should
// be designed to reflect the response dto that will be returned to a user.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct CategoryView {
name: String,
description: Option<String>,
category_id: Uuid,
store_id: Uuid,
deleted: bool,
}
// This updates the view with events as they are committed.
// The logic should be minimal here, e.g., don't calculate the account balance,
// design the events to carry the balance information instead.
impl View<Category> for CategoryView {
fn update(&mut self, event: &EventEnvelope<Category>) {
if let OrderingEvent::CategoryAdded(val) = &event.payload {
self.name = val.name().into();
self.description = val.description().clone();
self.category_id = *val.category_id();
self.store_id = *val.store_id();
self.deleted = false;
}
}
}
#[async_trait]
impl ViewRepository<CategoryView, Category> for OrderingDBPostgresAdapter {
async fn load(&self, category_id: &str) -> Result<Option<CategoryView>, PersistenceError> {
let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? {
Some((val, _)) => return Ok(Some(val)),
None => Uuid::parse_str(category_id).unwrap(),
};
let res = sqlx::query_as!(
CategoryView,
"SELECT
name, description, category_id, store_id, deleted
FROM
cqrs_ordering_category_query
WHERE
category_id = $1;",
category_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
Ok(Some(res))
}
async fn load_with_context(
&self,
category_id: &str,
) -> Result<Option<(CategoryView, ViewContext)>, PersistenceError> {
let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? {
Some(val) => return Ok(Some(val)),
None => Uuid::parse_str(category_id).unwrap(),
};
let res = sqlx::query_as!(
CategoryView,
"SELECT
name, description, category_id, store_id, deleted
FROM
cqrs_ordering_category_query
WHERE
category_id = $1;",
category_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
struct Context {
version: i64,
category_id: Uuid,
}
let ctx = sqlx::query_as!(
Context,
"SELECT
category_id, version
FROM
cqrs_ordering_category_query
WHERE
category_id = $1;",
category_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
let view_context = ViewContext::new(ctx.category_id.to_string(), ctx.version);
Ok(Some((res, view_context)))
}
async fn update_view(
&self,
view: CategoryView,
context: ViewContext,
) -> Result<(), PersistenceError> {
match context.version {
0 => {
let version = context.version + 1;
sqlx::query!(
"INSERT INTO cqrs_ordering_category_query (
version, name, description, category_id, store_id, deleted
) VALUES (
$1, $2, $3, $4, $5, $6
);",
version,
view.name,
view.description,
view.category_id,
view.store_id,
view.deleted
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
_ => {
let version = context.version + 1;
sqlx::query!(
"UPDATE
cqrs_ordering_category_query
SET
version = $1,
name = $2,
description = $3,
category_id = $4,
store_id = $5,
deleted = $6;",
version,
view.name,
view.description,
view.category_id,
view.store_id,
view.deleted
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
}
Ok(())
}
}
pub struct SimpleLoggingQuery {}
// Our simplest query, this is great for debugging but absolutely useless in production.
// This query just pretty prints the events as they are processed.
#[async_trait]
impl Query<Category> for SimpleLoggingQuery {
async fn dispatch(&self, aggregate_id: &str, events: &[EventEnvelope<Category>]) {
for event in events {
let payload = serde_json::to_string_pretty(&event.payload).unwrap();
println!("{}-{}\n{}", aggregate_id, event.sequence, payload);
}
}
}
#[async_trait]
impl Query<Category> for OrderingDBPostgresAdapter {
async fn dispatch(&self, category_id: &str, events: &[EventEnvelope<Category>]) {
let res = self
.load_with_context(category_id)
.await
.unwrap_or_else(|_| {
Some((
CategoryView::default(),
ViewContext::new(category_id.into(), 0),
))
});
let (mut view, view_context): (CategoryView, ViewContext) = res.unwrap();
for event in events {
view.update(event);
}
self.update_view(view, view_context).await.unwrap();
}
}

View file

@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::OrderingDBPostgresAdapter;
use crate::ordering::application::port::output::db::{customization_id_exists::*, errors::*};
#[async_trait::async_trait]
impl CustomizationIDExistsDBPort for OrderingDBPostgresAdapter {
async fn customization_id_exists(&self, customization_id: &Uuid) -> OrderingDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_ordering_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::ordering::domain::customization_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::OrderingDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let customization = CustomizationBuilder::default()
.name("customization_name".into())
.customization_id(UUID)
.product_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: &OrderingDBPostgresAdapter,
) {
sqlx::query!(
"INSERT INTO cqrs_ordering_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,112 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::OrderingDBPostgresAdapter;
use crate::ordering::application::port::output::db::{
customization_name_exists_for_product::*, errors::*,
};
use crate::ordering::domain::customization_aggregate::*;
#[async_trait::async_trait]
impl CustomizationNameExistsForProductDBPort for OrderingDBPostgresAdapter {
async fn customization_name_exists_for_product(
&self,
c: &Customization,
) -> OrderingDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_ordering_product_customizations_query
WHERE
name = $1
AND
product_id = $2
AND
deleted = false
);",
c.name(),
c.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::ordering::adapters::output::db::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::OrderingDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let customization = {
CustomizationBuilder::default()
.name(customization_name.into())
.product_id(UUID)
.customization_id(UUID)
.deleted(false)
.build()
.unwrap()
};
// state doesn't exist
assert!(!db
.customization_name_exists_for_product(&customization)
.await
.unwrap());
create_dummy_customization_record(&customization, &db).await;
// state exists
assert!(db
.customization_name_exists_for_product(&customization)
.await
.unwrap());
// Set customization.deleted = true; now db.customization_name_exists_for_product must return false
sqlx::query!(
"UPDATE
cqrs_ordering_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)
.await
.unwrap());
settings.drop_db().await;
}
}

View file

@ -0,0 +1,221 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::str::FromStr;
use async_trait::async_trait;
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
use cqrs_es::{EventEnvelope, Query, View};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::errors::*;
use super::OrderingDBPostgresAdapter;
use crate::ordering::domain::{customization_aggregate::*, events::OrderingEvent};
use crate::utils::parse_aggregate_id::parse_aggregate_id;
pub const NEW_CUSTOMIZATION_NON_UUID: &str = "ordering_new_customization_non_uuid-asdfa";
//#[derive(Debug, Default, Serialize, Deserialize)]
//struct Customizations {
// customizations: Vec<CustomizationView>,
//}
#[derive(Debug, Default, Serialize, Deserialize)]
struct CustomizationView {
name: String,
product_id: Uuid,
customization_id: Uuid,
deleted: bool,
}
impl From<CustomizationView> for Customization {
fn from(v: CustomizationView) -> Self {
CustomizationBuilder::default()
.name(v.name)
.customization_id(v.customization_id)
.product_id(v.product_id)
.deleted(v.deleted)
.build()
.unwrap()
}
}
// This updates the view with events as they are committed.
// The logic should be minimal here, e.g., don't calculate the account balance,
// design the events to carry the balance information instead.
impl View<Customization> for CustomizationView {
fn update(&mut self, event: &EventEnvelope<Customization>) {
match &event.payload {
OrderingEvent::CustomizationAdded(val) => {
self.name = val.customization().name().into();
self.product_id = *val.customization().product_id();
self.customization_id = *val.customization().customization_id();
self.deleted = false;
}
_ => (),
}
}
}
#[async_trait]
impl ViewRepository<CustomizationView, Customization> for OrderingDBPostgresAdapter {
async fn load(
&self,
customization_id: &str,
) -> Result<Option<CustomizationView>, PersistenceError> {
let customization_id =
match parse_aggregate_id(customization_id, NEW_CUSTOMIZATION_NON_UUID)? {
Some((val, _)) => return Ok(Some(val)),
None => Uuid::parse_str(customization_id).unwrap(),
};
let res = sqlx::query_as!(
CustomizationView,
"SELECT
name,
customization_id,
product_id,
deleted
FROM
cqrs_ordering_product_customizations_query
WHERE
customization_id = $1;",
customization_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
// let customizations = res.get_customizations(&self).await?;
Ok(Some(res))
}
async fn load_with_context(
&self,
customization_id: &str,
) -> Result<Option<(CustomizationView, ViewContext)>, PersistenceError> {
let customization_id =
match parse_aggregate_id(customization_id, NEW_CUSTOMIZATION_NON_UUID)? {
Some(val) => return Ok(Some(val)),
None => Uuid::parse_str(customization_id).unwrap(),
};
let res = sqlx::query_as!(
CustomizationView,
"SELECT
name,
customization_id,
product_id,
deleted
FROM
cqrs_ordering_product_customizations_query
WHERE
customization_id = $1;",
customization_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
// let customizations = res.get_customizations(&self).await?;
struct Context {
version: i64,
customization_id: Uuid,
}
let ctx = sqlx::query_as!(
Context,
"SELECT
customization_id, version
FROM
cqrs_ordering_product_customizations_query
WHERE
customization_id = $1;",
customization_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
let view_context = ViewContext::new(ctx.customization_id.to_string(), ctx.version);
Ok(Some((res, view_context)))
}
async fn update_view(
&self,
view: CustomizationView,
context: ViewContext,
) -> Result<(), PersistenceError> {
match context.version {
0 => {
let version = context.version + 1;
sqlx::query!(
"INSERT INTO cqrs_ordering_product_customizations_query (
version,
name,
customization_id,
product_id,
deleted
) VALUES (
$1, $2, $3, $4, $5
);",
version,
view.name,
view.customization_id,
view.product_id,
view.deleted,
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
_ => {
let version = context.version + 1;
sqlx::query!(
"UPDATE
cqrs_ordering_product_customizations_query
SET
version = $1,
name = $2,
customization_id = $3,
product_id = $4,
deleted = $5;",
version,
view.name,
view.customization_id,
view.product_id,
view.deleted,
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
}
Ok(())
}
}
#[async_trait]
impl Query<Customization> for OrderingDBPostgresAdapter {
async fn dispatch(&self, customization_id: &str, events: &[EventEnvelope<Customization>]) {
let res = self
.load_with_context(&customization_id)
.await
.unwrap_or_else(|_| {
Some((
CustomizationView::default(),
ViewContext::new(customization_id.into(), 0),
))
});
let (mut view, view_context): (CustomizationView, ViewContext) = res.unwrap();
for event in events {
view.update(event);
}
self.update_view(view, view_context).await.unwrap();
}
}

View file

@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::errors::*;
use super::OrderingDBPostgresAdapter;
use crate::ordering::application::port::output::db::{errors::*, get_category::*};
use crate::ordering::domain::category_aggregate::*;
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
pub struct InnerCategory {
name: String,
description: Option<String>,
store_id: Uuid,
category_id: Uuid,
deleted: bool,
}
impl From<InnerCategory> for Category {
fn from(v: InnerCategory) -> Self {
CategoryBuilder::default()
.name(v.name)
.description(v.description)
.store_id(v.store_id)
.category_id(v.category_id)
.deleted(v.deleted)
.build()
.unwrap()
}
}
#[async_trait::async_trait]
impl GetCategoryDBPort for OrderingDBPostgresAdapter {
async fn get_category(&self, category_id: &Uuid) -> OrderingDBResult<Category> {
let res = sqlx::query_as!(
InnerCategory,
"SELECT
name, description, store_id, category_id, deleted
FROM
cqrs_ordering_category_query
WHERE
category_id = $1;",
category_id,
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, OrderingDBError::CategoryIDNotFound))?;
Ok(res.into())
}
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use crate::ordering::adapters::output::db::category_name_exists_for_store::tests::create_dummy_category_record;
use super::*;
#[actix_rt::test]
async fn test_postgres() {
let category_id = Uuid::new_v4();
let store_id = Uuid::new_v4();
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::OrderingDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let category = CategoryBuilder::default()
.name("category_name".into())
.description(Some("category_description".into()))
.category_id(category_id)
.store_id(store_id)
.build()
.unwrap();
// state doesn't exist
assert_eq!(
db.get_category(category.category_id()).await,
Err(OrderingDBError::CategoryIDNotFound)
);
create_dummy_category_record(&category, &db).await;
// state exists
assert_eq!(
db.get_category(category.category_id()).await.unwrap(),
category
);
settings.drop_db().await;
}
}

View file

@ -8,13 +8,25 @@ use sqlx::postgres::PgPool;
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 customization_view;
mod errors;
mod get_category;
mod kot_id_exists;
mod kot_view;
mod line_item_id_exists;
mod line_item_view;
mod order_id_exists;
mod order_view;
mod product_id_exists;
mod product_name_exists_for_category;
mod store_id_exists;
mod store_name_exists;
mod store_view;
#[derive(Clone)]
pub struct OrderingDBPostgresAdapter {

View file

@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::OrderingDBPostgresAdapter;
use crate::ordering::application::port::output::db::{errors::*, product_id_exists::*};
#[async_trait::async_trait]
impl ProductIDExistsDBPort for OrderingDBPostgresAdapter {
async fn product_id_exists(&self, product_id: &Uuid) -> OrderingDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_ordering_product_query
WHERE
product_id = $1
);",
product_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::ordering::domain::add_product_command::tests::get_customizations;
use crate::ordering::domain::{add_product_command::tests::get_command, product_aggregate::*};
use crate::utils::uuid::tests::UUID;
#[actix_rt::test]
async fn test_postgres_product_exists() {
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::OrderingDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let cmd = get_command();
let product = ProductBuilder::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())
.category_id(*cmd.category_id())
.quantity(cmd.quantity().clone())
.product_id(UUID.clone())
.price(cmd.price().clone())
.build()
.unwrap();
// state doesn't exist
assert!(!db.product_id_exists(product.product_id()).await.unwrap());
create_dummy_product_record(&product, &db).await;
// state exists
assert!(db.product_id_exists(product.product_id()).await.unwrap());
settings.drop_db().await;
}
pub async fn create_dummy_product_record(p: &Product, db: &OrderingDBPostgresAdapter) {
sqlx::query!(
"INSERT INTO cqrs_ordering_product_query (
version,
name,
description,
image,
product_id,
category_id,
price_major,
price_minor,
price_currency,
sku_able,
quantity_minor_unit,
quantity_minor_number,
quantity_major_unit,
quantity_major_number,
deleted
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
);",
1,
p.name(),
p.description().as_ref().unwrap(),
p.image().as_ref().unwrap(),
p.product_id(),
p.category_id(),
p.price().major().clone() as i32,
p.price().minor().clone() as i32,
p.price().currency().to_string(),
p.sku_able().clone(),
p.quantity().major().unit().to_string(),
p.quantity().major().number().clone() as i32,
p.quantity().minor().unit().to_string(),
p.quantity().minor().number().clone() as i32,
p.deleted().clone(),
)
.execute(&db.pool)
.await
.unwrap();
}
}

View file

@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::OrderingDBPostgresAdapter;
use crate::ordering::application::port::output::db::{
errors::*, product_name_exists_for_category::*,
};
use crate::ordering::domain::product_aggregate::*;
#[async_trait::async_trait]
impl ProductNameExistsForCategoryDBPort for OrderingDBPostgresAdapter {
async fn product_name_exists_for_category(&self, s: &Product) -> OrderingDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_ordering_product_query
WHERE
name = $1
AND
category_id = $2
AND
deleted = false
);",
s.name(),
s.category_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::ordering::adapters::output::db::product_id_exists::tests::create_dummy_product_record;
use crate::ordering::domain::add_product_command::tests::get_command;
use crate::utils::uuid::tests::UUID;
#[actix_rt::test]
async fn test_postgres_product_exists() {
let product_name = "foo_product";
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::OrderingDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let cmd = get_command();
let product = ProductBuilder::default()
.name(product_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())
.category_id(*cmd.category_id())
.product_id(UUID)
.price(cmd.price().clone())
.quantity(cmd.quantity().clone())
.build()
.unwrap();
// state doesn't exist
assert!(!db.product_name_exists_for_category(&product).await.unwrap());
create_dummy_product_record(&product, &db).await;
// state exists
assert!(db.product_name_exists_for_category(&product).await.unwrap());
// Set product.deleted = true; now db.product_name_exists_for_category must return false
sqlx::query!(
"UPDATE cqrs_ordering_product_query SET deleted = true WHERE product_id = $1;",
product.product_id(),
)
.execute(&db.pool)
.await
.unwrap();
assert!(!db.product_name_exists_for_category(&product).await.unwrap());
settings.drop_db().await;
}
}

View file

@ -0,0 +1,332 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::str::FromStr;
use async_trait::async_trait;
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
use cqrs_es::{EventEnvelope, Query, View};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::errors::*;
use super::InventoryDBPostgresAdapter;
use crate::inventory::domain::events::InventoryEvent;
use crate::inventory::domain::product_aggregate::{Product, ProductBuilder};
use crate::types::currency::*;
use crate::types::quantity::*;
use crate::utils::parse_aggregate_id::parse_aggregate_id;
pub const NEW_PRODUCT_NON_UUID: &str = "new_product_non_uuid-asdfa";
// The view for a Product query, for a standard http application this should
// be designed to reflect the response dto that will be returned to a user.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ProductView {
name: String,
description: Option<String>,
image: Option<String>, // string = filename
product_id: Uuid,
sku_able: bool,
price_minor: i32,
price_major: i32,
price_currency: String,
quantity_major_number: i32,
quantity_minor_number: i32,
quantity_major_unit: String,
quantity_minor_unit: String,
category_id: Uuid,
deleted: bool,
}
impl From<ProductView> for Product {
fn from(v: ProductView) -> Self {
let price = PriceBuilder::default()
.minor(v.price_minor as usize)
.major(v.price_major as usize)
.currency(Currency::from_str(&v.price_currency).unwrap())
.build()
.unwrap();
let quantity = QuantityBuilder::default()
.minor(
QuantityPartBuilder::default()
.number(v.quantity_minor_number as usize)
.unit(QuantityUnit::from_str(&v.quantity_minor_unit).unwrap())
.build()
.unwrap(),
)
.major(
QuantityPartBuilder::default()
.number(v.quantity_major_number as usize)
.unit(QuantityUnit::from_str(&v.quantity_major_unit).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
ProductBuilder::default()
.name(v.name)
.description(v.description)
.image(v.image)
.sku_able(v.sku_able)
.price(price)
.category_id(v.category_id)
.quantity(quantity)
.product_id(v.product_id)
.deleted(v.deleted)
.build()
.unwrap()
}
}
// This updates the view with events as they are committed.
// The logic should be minimal here, e.g., don't calculate the account balance,
// design the events to carry the balance information instead.
impl View<Product> for ProductView {
fn update(&mut self, event: &EventEnvelope<Product>) {
match &event.payload {
InventoryEvent::ProductAdded(val) => {
self.name = val.name().into();
self.description = val.description().clone();
self.image = val.image().clone();
self.product_id = *val.product_id();
self.category_id = *val.category_id();
self.sku_able = *val.sku_able();
self.price_minor = *val.price().minor() as i32;
self.price_major = *val.price().major() as i32;
self.price_currency = val.price().currency().to_string();
self.quantity_major_number = *val.quantity().major().number() as i32;
self.quantity_minor_number = *val.quantity().minor().number() as i32;
self.quantity_major_unit = val.quantity().major().unit().to_string();
self.quantity_minor_unit = val.quantity().minor().unit().to_string();
self.deleted = false;
}
_ => (),
}
}
}
#[async_trait]
impl ViewRepository<ProductView, Product> for InventoryDBPostgresAdapter {
async fn load(&self, product_id: &str) -> Result<Option<ProductView>, PersistenceError> {
let product_id = match parse_aggregate_id(product_id, NEW_PRODUCT_NON_UUID)? {
Some((val, _)) => return Ok(Some(val)),
None => Uuid::parse_str(product_id).unwrap(),
};
let res = sqlx::query_as!(
ProductView,
"SELECT
name,
description,
image,
product_id,
category_id,
price_major,
price_minor,
price_currency,
sku_able,
quantity_minor_unit,
quantity_minor_number,
quantity_major_unit,
quantity_major_number,
deleted
FROM
cqrs_inventory_product_query
WHERE
product_id = $1;",
product_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
Ok(Some(res))
}
async fn load_with_context(
&self,
product_id: &str,
) -> Result<Option<(ProductView, ViewContext)>, PersistenceError> {
let product_id = match parse_aggregate_id(product_id, NEW_PRODUCT_NON_UUID)? {
Some(val) => return Ok(Some(val)),
None => Uuid::parse_str(product_id).unwrap(),
};
let res = sqlx::query_as!(
ProductView,
"SELECT
name,
description,
image,
product_id,
category_id,
price_major,
price_minor,
price_currency,
sku_able,
quantity_minor_unit,
quantity_minor_number,
quantity_major_unit,
quantity_major_number,
deleted
FROM
cqrs_inventory_product_query
WHERE
product_id = $1;",
product_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
struct Context {
version: i64,
product_id: Uuid,
}
let ctx = sqlx::query_as!(
Context,
"SELECT
product_id, version
FROM
cqrs_inventory_product_query
WHERE
product_id = $1;",
product_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
let view_context = ViewContext::new(ctx.product_id.to_string(), ctx.version);
Ok(Some((res, view_context)))
}
async fn update_view(
&self,
view: ProductView,
context: ViewContext,
) -> Result<(), PersistenceError> {
match context.version {
0 => {
let version = context.version + 1;
sqlx::query!(
"INSERT INTO cqrs_inventory_product_query (
version,
name,
description,
image,
product_id,
category_id,
price_major,
price_minor,
price_currency,
sku_able,
quantity_minor_unit,
quantity_minor_number,
quantity_major_unit,
quantity_major_number,
deleted
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
);",
version,
view.name,
view.description,
view.image,
view.product_id,
view.category_id,
view.price_major,
view.price_minor,
view.price_currency,
view.sku_able,
view.quantity_minor_unit,
view.quantity_minor_number,
view.quantity_major_unit,
view.quantity_major_number,
view.deleted,
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
_ => {
let version = context.version + 1;
sqlx::query!(
"UPDATE
cqrs_inventory_product_query
SET
version = $1,
name = $2,
description = $3,
image = $4,
product_id = $5,
category_id = $6,
price_major = $7,
price_minor = $8,
price_currency = $9,
sku_able = $10,
quantity_minor_unit = $11,
quantity_minor_number = $12,
quantity_major_unit = $13,
quantity_major_number = $14,
deleted = $15;",
version,
view.name,
view.description,
view.image,
view.product_id,
view.category_id,
view.price_major,
view.price_minor,
view.price_currency,
view.sku_able,
view.quantity_minor_unit,
view.quantity_minor_number,
view.quantity_major_unit,
view.quantity_major_number,
view.deleted,
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
}
Ok(())
}
}
#[async_trait]
impl Query<Product> for InventoryDBPostgresAdapter {
async fn dispatch(&self, product_id: &str, events: &[EventEnvelope<Product>]) {
let res = self
.load_with_context(product_id)
.await
.unwrap_or_else(|_| {
Some((
ProductView::default(),
ViewContext::new(product_id.into(), 0),
))
});
let (mut view, view_context): (ProductView, ViewContext) = res.unwrap();
for event in events {
view.update(event);
}
self.update_view(view, view_context).await.unwrap();
}
}

View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::OrderingDBPostgresAdapter;
use crate::ordering::application::port::output::db::{errors::*, store_id_exists::*};
use crate::ordering::domain::store_aggregate::*;
#[async_trait::async_trait]
impl StoreIDExistsDBPort for OrderingDBPostgresAdapter {
async fn store_id_exists(&self, store_id: &Uuid) -> OrderingDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_ordering_store_query
WHERE
store_id = $1
);",
store_id
)
.fetch_one(&self.pool)
.await?;
if let Some(x) = res.exists {
Ok(x)
} else {
Ok(false)
}
}
}
#[cfg(test)]
pub mod tests {
use uuid::Uuid;
use crate::utils::uuid::tests::UUID;
use super::*;
pub async fn create_dummy_store_record(s: &Store, db: &OrderingDBPostgresAdapter) {
sqlx::query!(
"INSERT INTO cqrs_ordering_store_query
(version, name, address, store_id, owner, deleted)
VALUES ($1, $2, $3, $4, $5 ,$6);",
1,
s.name(),
s.address().as_ref().unwrap(),
s.store_id(),
s.owner(),
false
)
.execute(&db.pool)
.await
.unwrap();
}
#[actix_rt::test]
async fn test_postgres_store_exists() {
let store_id = Uuid::new_v4();
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::OrderingDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let store = StoreBuilder::default()
.name("store_name".into())
.owner(UUID)
.address(Some("store_address".into()))
.store_id(store_id)
.build()
.unwrap();
// state doesn't exist
assert!(!db.store_id_exists(store.store_id()).await.unwrap());
create_dummy_store_record(&store, &db).await;
// state exists
assert!(db.store_id_exists(store.store_id()).await.unwrap());
settings.drop_db().await;
}
}

View file

@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::OrderingDBPostgresAdapter;
use crate::ordering::application::port::output::db::{errors::*, store_name_exists::*};
use crate::ordering::domain::store_aggregate::*;
#[async_trait::async_trait]
impl StoreNameExistsDBPort for OrderingDBPostgresAdapter {
async fn store_name_exists(&self, s: &Store) -> OrderingDBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_ordering_store_query
WHERE
name = $1
AND
deleted = false
);",
s.name(),
)
.fetch_one(&self.pool)
.await?;
if let Some(x) = res.exists {
Ok(x)
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use crate::utils::uuid::tests::UUID;
use super::*;
use crate::ordering::adapters::output::db::store_id_exists::tests::create_dummy_store_record;
#[actix_rt::test]
async fn test_postgres_store_exists() {
let store_id = Uuid::new_v4();
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::OrderingDBPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let store = StoreBuilder::default()
.name("store_name".into())
.owner(UUID)
.address(Some("store_address".into()))
.store_id(store_id)
.build()
.unwrap();
// state doesn't exist
assert!(!db.store_name_exists(&store).await.unwrap());
create_dummy_store_record(&store, &db).await;
// state exists
assert!(db.store_name_exists(&store).await.unwrap());
// Set store.deleted = true; now db.store_name_exists must return false
sqlx::query!(
"UPDATE cqrs_ordering_store_query SET deleted = true WHERE store_id = $1;",
store.store_id()
)
.execute(&db.pool)
.await
.unwrap();
assert!(!db.store_name_exists(&store).await.unwrap());
settings.drop_db().await;
}
}

View file

@ -0,0 +1,302 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use async_trait::async_trait;
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
use cqrs_es::{EventEnvelope, Query, View};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::errors::*;
use super::OrderingDBPostgresAdapter;
use crate::ordering::domain::events::OrderingEvent;
use crate::ordering::domain::store_aggregate::Store;
use crate::utils::parse_aggregate_id::parse_aggregate_id;
pub const NEW_STORE_NON_UUID: &str = "ordering_new_store_non_uuid-asdfa";
// The view for a Store query, for a standard http application this should
// be designed to reflect the response dto that will be returned to a user.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct StoreView {
name: String,
address: Option<String>,
store_id: Uuid,
owner: Uuid,
deleted: bool,
}
// This updates the view with events as they are committed.
// The logic should be minimal here, e.g., don't calculate the account balance,
// design the events to carry the balance information instead.
impl View<Store> for StoreView {
fn update(&mut self, event: &EventEnvelope<Store>) {
if let OrderingEvent::StoreAdded(val) = &event.payload {
self.name = val.name().into();
self.address = val.address().clone();
self.store_id = *val.store_id();
self.owner = *val.owner();
self.deleted = false;
}
}
}
#[async_trait]
impl ViewRepository<StoreView, Store> for OrderingDBPostgresAdapter {
async fn load(&self, store_id: &str) -> Result<Option<StoreView>, PersistenceError> {
let store_id = match parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? {
Some((val, _)) => return Ok(Some(val)),
None => Uuid::parse_str(store_id).unwrap(),
};
let res = sqlx::query_as!(
StoreView,
"SELECT
name, address, store_id, owner, deleted
FROM
cqrs_ordering_store_query
WHERE
store_id = $1;",
store_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
Ok(Some(res))
}
async fn load_with_context(
&self,
store_id: &str,
) -> Result<Option<(StoreView, ViewContext)>, PersistenceError> {
let store_id = match parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? {
Some(val) => return Ok(Some(val)),
None => Uuid::parse_str(store_id).unwrap(),
};
let res = sqlx::query_as!(
StoreView,
"SELECT
name, address, store_id, owner, deleted
FROM
cqrs_ordering_store_query
WHERE
store_id = $1;",
&store_id,
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
struct Context {
version: i64,
store_id: Uuid,
}
let ctx = sqlx::query_as!(
Context,
"SELECT
store_id, version
FROM
cqrs_ordering_store_query
WHERE
store_id = $1;",
store_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
let view_context = ViewContext::new(ctx.store_id.to_string(), ctx.version);
Ok(Some((res, view_context)))
}
async fn update_view(
&self,
view: StoreView,
context: ViewContext,
) -> Result<(), PersistenceError> {
match context.version {
0 => {
let version = context.version + 1;
sqlx::query!(
"INSERT INTO cqrs_ordering_store_query (
version, name, address, store_id, owner, deleted
) VALUES (
$1, $2, $3, $4, $5, $6
);",
version,
view.name,
view.address,
view.store_id,
view.owner,
view.deleted,
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
_ => {
let version = context.version + 1;
sqlx::query!(
"UPDATE
cqrs_ordering_store_query
SET
version = $1,
name = $2,
address = $3,
store_id = $4,
owner = $5,
deleted = $6;",
version,
view.name,
view.address,
view.store_id,
view.owner,
view.deleted,
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
}
Ok(())
}
}
pub struct SimpleLoggingQuery {}
// Our simplest query, this is great for debugging but absolutely useless in production.
// This query just pretty prints the events as they are processed.
#[async_trait]
impl Query<Store> for SimpleLoggingQuery {
async fn dispatch(&self, aggregate_id: &str, events: &[EventEnvelope<Store>]) {
for event in events {
let payload = serde_json::to_string_pretty(&event.payload).unwrap();
println!("{}-{}\n{}", aggregate_id, event.sequence, payload);
}
}
}
#[async_trait]
impl Query<Store> for OrderingDBPostgresAdapter {
async fn dispatch(&self, store_id: &str, events: &[EventEnvelope<Store>]) {
let res = self
.load_with_context(store_id)
.await
.unwrap_or_else(|_| Some((StoreView::default(), ViewContext::new(store_id.into(), 0))));
let (mut view, view_context): (StoreView, ViewContext) = res.unwrap();
for event in events {
view.update(event);
}
self.update_view(view, view_context).await.unwrap();
}
}
// Our second query, this one will be handled with Postgres `GenericQuery`
// which will serialize and persist our view after it is updated. It also
// provides a `load` method to deserialize the view on request.
//pub type StoreQuery = GenericQuery<OrderingDBPostgresAdapter, StoreView, Store>;
//pub type StoreQuery = Query<dyn OrderingDBPostgresAdapter, StoreView, Store>;
//#[cfg(test)]
//mod tests {
// use super::*;
//
// use postgres_es::PostgresCqrs;
//
// use crate::{
// db::migrate::*,
// ordering::{
// application::services::{
// add_category_service::tests::mock_add_category_service, add_customization_service::tests::mock_add_customization_service, add_line_item_service::tests::mock_add_line_item_service, add_product_service::tests::mock_add_product_service, add_store_service::AddStoreServiceBuilder, update_category_service::tests::mock_update_category_service, update_customization_service::tests::mock_update_customization_service, update_product_service::tests::mock_update_product_service, update_store_service::tests::mock_update_store_service, OrderingServicesBuilder
// },
// domain::{
// add_category_command::AddCategoryCommand, add_customization_command,
// add_product_command::tests::get_command, add_store_command::AddStoreCommand,
// commands::OrderingCommand,
// update_category_command::tests::get_update_category_command,
// update_customization_command::tests::get_update_customization_command,
// update_product_command, update_store_command::tests::get_update_store_cmd,
// },
// },
// tests::bdd::IS_NEVER_CALLED,
// utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID},
// };
// use std::sync::Arc;
//
// #[actix_rt::test]
// async fn pg_query() {
// let settings = crate::settings::tests::get_settings().await;
// //let settings = crate::settings::Settings::new().unwrap();
// settings.create_db().await;
//
// let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await;
// db.migrate().await;
// let db = OrderingDBPostgresAdapter::new(db.pool.clone());
//
// let simple_query = SimpleLoggingQuery {};
//
// let queries: Vec<Box<dyn Query<Store>>> =
// vec![Box::new(simple_query), Box::new(db.clone())];
//
// let services = OrderingServicesBuilder::default()
// .add_store(Arc::new(
// AddStoreServiceBuilder::default()
// .db_store_id_exists(Arc::new(db.clone()))
// .db_store_name_exists(Arc::new(db.clone()))
// .get_uuid(Arc::new(crate::utils::uuid::GenerateUUID {}))
// .build()
// .unwrap(),
// ))
// .add_category(mock_add_category_service(
// IS_NEVER_CALLED,
// AddCategoryCommand::new("foo".into(), None, UUID, UUID).unwrap(),
// ))
// .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(),
// ))
// .update_product(mock_update_product_service(
// IS_NEVER_CALLED,
// update_product_command::tests::get_command(),
// ))
// .update_customization(mock_update_customization_service(
// IS_NEVER_CALLED,
// get_update_customization_command(),
// ))
// .update_category(mock_update_category_service(
// IS_NEVER_CALLED,
// get_update_category_command(),
// ))
// .update_store(mock_update_store_service(
// IS_NEVER_CALLED,
// get_update_store_cmd(),
// ))
// .build()
// .unwrap();
//
// let (cqrs, _store_query): (
// Arc<PostgresCqrs<Store>>,
// Arc<dyn ViewRepository<StoreView, Store>>,
// ) = (
// Arc::new(postgres_es::postgres_cqrs(
// db.pool.clone(),
// queries,
// Arc::new(services),
// )),
// Arc::new(db.clone()),
// );
//
// let rand = crate::utils::random_string::GenerateRandomString {};
// let cmd = AddStoreCommand::new(rand.get_random(10), None, UUID).unwrap();
// cqrs.execute("", OrderingCommand::AddStore(cmd.clone()))
// .await
// .unwrap();
//
// settings.drop_db().await;
// }
//}

View file

@ -0,0 +1,55 @@
// 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 CategoryIDExistsDBPort: Send + Sync {
async fn category_id_exists(&self, category_id: &Uuid) -> OrderingDBResult<bool>;
}
pub type CategoryIDExistsDBPortObj = std::sync::Arc<dyn CategoryIDExistsDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_category_id_exists_db_port_false(
times: Option<usize>,
) -> CategoryIDExistsDBPortObj {
let mut m = MockCategoryIDExistsDBPort::new();
if let Some(times) = times {
m.expect_category_id_exists()
.times(times)
.returning(|_| Ok(false));
} else {
m.expect_category_id_exists().returning(|_| Ok(false));
}
Arc::new(m)
}
pub fn mock_category_id_exists_db_port_true(times: Option<usize>) -> CategoryIDExistsDBPortObj {
let mut m = MockCategoryIDExistsDBPort::new();
if let Some(times) = times {
m.expect_category_id_exists()
.times(times)
.returning(|_| Ok(true));
} else {
m.expect_category_id_exists().returning(|_| Ok(true));
}
Arc::new(m)
}
}

View file

@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
use crate::ordering::domain::category_aggregate::Category;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait CategoryNameExistsForStoreDBPort: Send + Sync {
async fn category_name_exists_for_store(&self, c: &Category) -> OrderingDBResult<bool>;
}
pub type CategoryNameExistsForStoreDBPortObj = std::sync::Arc<dyn CategoryNameExistsForStoreDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_category_name_exists_for_store_db_port_false(
times: Option<usize>,
) -> CategoryNameExistsForStoreDBPortObj {
let mut m = MockCategoryNameExistsForStoreDBPort::new();
if let Some(times) = times {
m.expect_category_name_exists_for_store()
.times(times)
.returning(|_| Ok(false));
} else {
m.expect_category_name_exists_for_store()
.returning(|_| Ok(false));
}
Arc::new(m)
}
pub fn mock_category_name_exists_for_store_db_port_true(
times: Option<usize>,
) -> CategoryNameExistsForStoreDBPortObj {
let mut m = MockCategoryNameExistsForStoreDBPort::new();
if let Some(times) = times {
m.expect_category_name_exists_for_store()
.times(times)
.returning(|_| Ok(true));
} else {
m.expect_category_name_exists_for_store()
.returning(|_| Ok(true));
}
Arc::new(m)
}
}

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) -> OrderingDBResult<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,65 @@
// 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::ordering::domain::customization_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,
) -> OrderingDBResult<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

@ -16,4 +16,16 @@ pub enum OrderingDBError {
OrderIDNotFound,
DuplicateKotID,
KotIDNotFound,
DuplicateStoreName,
DuplicateStoreID,
StoreIDNotFound,
DuplicateCategoryName,
DuplicateCategoryID,
CategoryIDNotFound,
DuplicateProductName,
DuplicateProductID,
ProductIDNotFound,
CustomizationIDNotFound,
DuplicateCustomizationID,
DuplicateCustomizationName,
}

View file

@ -0,0 +1,44 @@
// 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::ordering::domain::category_aggregate::Category;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait GetCategoryDBPort: Send + Sync {
async fn get_category(&self, category_id: &Uuid) -> OrderingDBResult<Category>;
}
pub type GetCategoryDBPortObj = std::sync::Arc<dyn GetCategoryDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_get_category_db_port(times: Option<usize>) -> GetCategoryDBPortObj {
let mut m = MockGetCategoryDBPort::new();
if let Some(times) = times {
m.expect_get_category()
.times(times)
.returning(|_| Ok(Category::default()));
} else {
m.expect_get_category()
.returning(|_| Ok(Category::default()));
}
Arc::new(m)
}
}

View file

@ -2,7 +2,16 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
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 get_category;
pub mod kot_id_exists;
pub mod line_item_id_exists;
pub mod order_id_exists;
pub mod product_id_exists;
pub mod product_name_exists_for_category;
pub mod store_id_exists;
pub mod store_name_exists;

View file

@ -0,0 +1,53 @@
// 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 ProductIDExistsDBPort: Send + Sync {
async fn product_id_exists(&self, c: &Uuid) -> OrderingDBResult<bool>;
}
pub type ProductIDExistsDBPortObj = std::sync::Arc<dyn ProductIDExistsDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_product_id_exists_db_port_false(times: Option<usize>) -> ProductIDExistsDBPortObj {
let mut m = MockProductIDExistsDBPort::new();
if let Some(times) = times {
m.expect_product_id_exists()
.times(times)
.returning(|_| Ok(false));
} else {
m.expect_product_id_exists().returning(|_| Ok(false));
}
Arc::new(m)
}
pub fn mock_product_id_exists_db_port_true(times: Option<usize>) -> ProductIDExistsDBPortObj {
let mut m = MockProductIDExistsDBPort::new();
if let Some(times) = times {
m.expect_product_id_exists()
.times(times)
.returning(|_| Ok(true));
} else {
m.expect_product_id_exists().returning(|_| Ok(true));
}
Arc::new(m)
}
}

View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
use crate::ordering::domain::product_aggregate::Product;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait ProductNameExistsForCategoryDBPort: Send + Sync {
async fn product_name_exists_for_category(&self, c: &Product) -> OrderingDBResult<bool>;
}
pub type ProductNameExistsForCategoryDBPortObj =
std::sync::Arc<dyn ProductNameExistsForCategoryDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_product_name_exists_for_category_db_port_false(
times: Option<usize>,
) -> ProductNameExistsForCategoryDBPortObj {
let mut m = MockProductNameExistsForCategoryDBPort::new();
if let Some(times) = times {
m.expect_product_name_exists_for_category()
.times(times)
.returning(|_| Ok(false));
} else {
m.expect_product_name_exists_for_category()
.returning(|_| Ok(false));
}
Arc::new(m)
}
pub fn mock_product_name_exists_for_category_db_port_true(
times: Option<usize>,
) -> ProductNameExistsForCategoryDBPortObj {
let mut m = MockProductNameExistsForCategoryDBPort::new();
if let Some(times) = times {
m.expect_product_name_exists_for_category()
.times(times)
.returning(|_| Ok(true));
} else {
m.expect_product_name_exists_for_category()
.returning(|_| Ok(true));
}
Arc::new(m)
}
}

View file

@ -0,0 +1,55 @@
// 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::ordering::domain::store_aggregate::Store;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait StoreIDExistsDBPort: Send + Sync {
async fn store_id_exists(&self, store_id: &Uuid) -> OrderingDBResult<bool>;
}
pub type StoreIDExistsDBPortObj = std::sync::Arc<dyn StoreIDExistsDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_store_id_exists_db_port_false(times: Option<usize>) -> StoreIDExistsDBPortObj {
let mut m = MockStoreIDExistsDBPort::new();
if let Some(times) = times {
m.expect_store_id_exists()
.times(times)
.returning(|_| Ok(false));
} else {
m.expect_store_id_exists().returning(|_| Ok(false));
}
Arc::new(m)
}
pub fn mock_store_id_exists_db_port_true(times: Option<usize>) -> StoreIDExistsDBPortObj {
let mut m = MockStoreIDExistsDBPort::new();
if let Some(times) = times {
m.expect_store_id_exists()
.times(times)
.returning(|_| Ok(true));
} else {
m.expect_store_id_exists().returning(|_| Ok(true));
}
Arc::new(m)
}
}

View file

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
use crate::ordering::domain::store_aggregate::Store;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait StoreNameExistsDBPort: Send + Sync {
async fn store_name_exists(&self, s: &Store) -> OrderingDBResult<bool>;
}
pub type StoreNameExistsDBPortObj = std::sync::Arc<dyn StoreNameExistsDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_store_name_exists_db_port_false(times: Option<usize>) -> StoreNameExistsDBPortObj {
let mut m = MockStoreNameExistsDBPort::new();
if let Some(times) = times {
m.expect_store_name_exists()
.times(times)
.returning(|_| Ok(false));
} else {
m.expect_store_name_exists().returning(|_| Ok(false));
}
Arc::new(m)
}
pub fn mock_store_name_exists_db_port_true(times: Option<usize>) -> StoreNameExistsDBPortObj {
let mut m = MockStoreNameExistsDBPort::new();
if let Some(times) = times {
m.expect_store_name_exists()
.times(times)
.returning(|_| Ok(true));
} else {
m.expect_store_name_exists().returning(|_| Ok(true));
}
Arc::new(m)
}
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
use crate::ordering::domain::{category_aggregate::*, product_aggregate::*};
#[automock]
#[async_trait::async_trait]
pub trait AddProductToStoreFTSPort: Send + Sync {
async fn add_product_to_store(
&self,
product: &Product,
cateogry: &Category,
) -> OrderingFTSResult<()>;
}
pub type AddProductToStoreFTSPortObj = std::sync::Arc<dyn AddProductToStoreFTSPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_add_product_to_store_fts_port(times: Option<usize>) -> AddProductToStoreFTSPortObj {
let mut m = MockAddProductToStoreFTSPort::new();
if let Some(times) = times {
m.expect_add_product_to_store()
.times(times)
.returning(|_, _| Ok(()));
} else {
m.expect_add_product_to_store().returning(|_, _| Ok(()));
}
Arc::new(m)
}
}

View file

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_more::Display;
use serde::{Deserialize, Serialize};
pub type OrderingFTSResult<V> = Result<V, OrderingFTSError>;
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum OrderingFTSError {}

View file

@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod add_product_to_store;
pub mod errors;
pub mod update_product;

View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

View file

@ -3,3 +3,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod db;
pub mod full_text_search;

View file

@ -0,0 +1,211 @@
// 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::ordering::{
application::port::output::db::{
category_id_exists::*, category_name_exists_for_store::*, store_id_exists::*,
},
domain::{
add_category_command::AddCategoryCommand,
category_added_event::{CategoryAddedEvent, CategoryAddedEventBuilder},
category_aggregate::*,
},
};
use crate::utils::uuid::*;
#[automock]
#[async_trait::async_trait]
pub trait AddCategoryUseCase: Send + Sync {
async fn add_category(&self, cmd: AddCategoryCommand) -> OrderingResult<CategoryAddedEvent>;
}
pub type AddCategoryServiceObj = Arc<dyn AddCategoryUseCase>;
#[derive(Clone, Builder)]
pub struct AddCategoryService {
db_store_id_exists: StoreIDExistsDBPortObj,
db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj,
db_category_id_exists: CategoryIDExistsDBPortObj,
get_uuid: GetUUIDInterfaceObj,
}
#[async_trait::async_trait]
impl AddCategoryUseCase for AddCategoryService {
async fn add_category(&self, cmd: AddCategoryCommand) -> OrderingResult<CategoryAddedEvent> {
if !self
.db_store_id_exists
.store_id_exists(cmd.store_id())
.await?
{
return Err(OrderingError::StoreIDNotFound);
}
let mut category_id = self.get_uuid.get_uuid();
loop {
if self
.db_category_id_exists
.category_id_exists(&category_id)
.await?
{
category_id = self.get_uuid.get_uuid();
continue;
} else {
break;
}
}
let category = CategoryBuilder::default()
.name(cmd.name().into())
.description(cmd.description().as_ref().map(|s| s.to_string()))
.store_id(*cmd.store_id())
.category_id(category_id)
.build()
.unwrap();
if self
.db_category_name_exists_for_store
.category_name_exists_for_store(&category)
.await?
{
return Err(OrderingError::DuplicateCategoryName);
}
Ok(CategoryAddedEventBuilder::default()
.name(category.name().into())
.description(category.description().as_ref().map(|s| s.to_string()))
.added_by_user(*cmd.adding_by())
.store_id(*category.store_id())
.category_id(*category.category_id())
.build()
.unwrap())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use uuid::Uuid;
use crate::utils::uuid::tests::UUID;
use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid};
pub fn mock_add_category_service(
times: Option<usize>,
cmd: AddCategoryCommand,
) -> AddCategoryServiceObj {
let mut m = MockAddCategoryUseCase::new();
let res = CategoryAddedEventBuilder::default()
.name(cmd.name().into())
.description(cmd.description().as_ref().map(|s| s.to_string()))
.added_by_user(*cmd.adding_by())
.store_id(*cmd.store_id())
.category_id(UUID)
.build()
.unwrap();
if let Some(times) = times {
m.expect_add_category()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_add_category().returning(move |_| Ok(res.clone()));
}
Arc::new(m)
}
#[actix_rt::test]
async fn test_service_category_doesnt_exist() {
let name = "foo";
let description = "bar";
let user_id = UUID;
let store_id = Uuid::new_v4();
// description = None
let cmd = AddCategoryCommand::new(name.into(), Some(description.into()), store_id, user_id)
.unwrap();
let s = AddCategoryServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false(
IS_CALLED_ONLY_ONCE,
))
.db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
let res = s.add_category(cmd.clone()).await.unwrap();
assert_eq!(res.name(), cmd.name());
assert_eq!(res.description(), cmd.description());
assert_eq!(res.added_by_user(), cmd.adding_by());
assert_eq!(res.store_id(), cmd.store_id());
assert_eq!(res.category_id(), &UUID);
}
#[actix_rt::test]
async fn test_service_category_name_exists_for_store() {
let name = "foo";
let description = "bar";
let user_id = UUID;
let store_id = Uuid::new_v4();
// description = None
let cmd = AddCategoryCommand::new(name.into(), Some(description.into()), store_id, user_id)
.unwrap();
let s = AddCategoryServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_true(
IS_CALLED_ONLY_ONCE,
))
.db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.add_category(cmd.clone()).await,
Err(OrderingError::DuplicateCategoryName)
)
}
#[actix_rt::test]
async fn test_service_store_doesnt_exist() {
let name = "foo";
let description = "bar";
let user_id = UUID;
let store_id = Uuid::new_v4();
// description = None
let cmd = AddCategoryCommand::new(name.into(), Some(description.into()), store_id, user_id)
.unwrap();
let s = AddCategoryServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false(
IS_NEVER_CALLED,
))
.db_category_id_exists(mock_category_id_exists_db_port_false(IS_NEVER_CALLED))
.get_uuid(mock_get_uuid(IS_NEVER_CALLED))
.build()
.unwrap();
assert_eq!(
s.add_category(cmd.clone()).await,
Err(OrderingError::StoreIDNotFound)
)
}
}

View file

@ -0,0 +1,191 @@
// 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::ordering::{
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,
) -> OrderingResult<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,
) -> OrderingResult<CustomizationAddedEvent> {
if !self
.db_product_id_exists
.product_id_exists(cmd.product_id())
.await?
{
return Err(OrderingError::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)
.product_id(*cmd.product_id())
.customization_id(customization_id)
.build()
.unwrap();
if self
.db_customization_name_exists_for_product
.customization_name_exists_for_product(&customization)
.await?
{
return Err(OrderingError::DuplicateCustomizationName);
}
Ok(CustomizationAddedEventBuilder::default()
.customization(customization)
.build()
.unwrap())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use customization_added_event::tests::get_customization_added_event_from_cmd;
use uuid::Uuid;
use crate::ordering::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 res = get_customization_added_event_from_cmd(&cmd);
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!(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(OrderingError::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(OrderingError::ProductIDNotFound)
)
}
}

View file

@ -0,0 +1,227 @@
// 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::ordering::{
application::port::output::{
db::{
category_id_exists::*, get_category::*, product_id_exists::*,
product_name_exists_for_category::*,
},
full_text_search::add_product_to_store::*,
},
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) -> OrderingResult<ProductAddedEvent>;
}
pub type AddProductServiceObj = Arc<dyn AddProductUseCase>;
#[derive(Clone, Builder)]
pub struct AddProductService {
db_category_id_exists: CategoryIDExistsDBPortObj,
db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj,
db_product_id_exists: ProductIDExistsDBPortObj,
db_get_category: GetCategoryDBPortObj,
fts_add_product: AddProductToStoreFTSPortObj,
get_uuid: GetUUIDInterfaceObj,
}
#[async_trait::async_trait]
impl AddProductUseCase for AddProductService {
async fn add_product(&self, cmd: AddProductCommand) -> OrderingResult<ProductAddedEvent> {
if !self
.db_category_id_exists
.category_id_exists(cmd.category_id())
.await?
{
return Err(OrderingError::CategoryIDNotFound);
}
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())
.price(cmd.price().clone())
.category_id(*cmd.category_id())
.quantity(cmd.quantity().clone())
.product_id(product_id)
.build()
.unwrap();
if self
.db_product_name_exists_for_category
.product_name_exists_for_category(&product)
.await?
{
return Err(OrderingError::DuplicateProductName);
}
let category = self
.db_get_category
.get_category(product.category_id())
.await?;
self.fts_add_product
.add_product_to_store(&product, &category)
.await?;
Ok(ProductAddedEventBuilder::default()
.added_by_user(*cmd.adding_by())
.name(product.name().into())
.description(product.description().as_ref().map(|s| s.to_string()))
.image(product.image().clone())
.sku_able(*product.sku_able())
.price(product.price().clone())
.category_id(*product.category_id())
.product_id(*product.product_id())
.category_id(product.category_id().clone())
.product_id(product.product_id().clone())
.quantity(product.quantity().clone())
.build()
.unwrap())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::ordering::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())
.quantity(cmd.quantity().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))
.db_get_category(mock_get_category_db_port(IS_CALLED_ONLY_ONCE))
.db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.fts_add_product(mock_add_product_to_store_fts_port(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);
assert_eq!(res.quantity(), cmd.quantity());
}
#[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),
)
.db_category_id_exists(mock_category_id_exists_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))
.db_get_category(mock_get_category_db_port(IS_NEVER_CALLED))
.fts_add_product(mock_add_product_to_store_fts_port(IS_NEVER_CALLED))
.build()
.unwrap();
assert_eq!(
s.add_product(cmd.clone()).await,
Err(OrderingError::DuplicateProductName)
)
}
#[actix_rt::test]
async fn test_service_category_id_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_NEVER_CALLED),
)
.db_product_id_exists(mock_product_id_exists_db_port_false(IS_NEVER_CALLED))
.db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_get_category(mock_get_category_db_port(IS_NEVER_CALLED))
.fts_add_product(mock_add_product_to_store_fts_port(IS_NEVER_CALLED))
.get_uuid(mock_get_uuid(IS_NEVER_CALLED))
.build()
.unwrap();
assert_eq!(
s.add_product(cmd.clone()).await,
Err(OrderingError::CategoryIDNotFound)
)
}
}

View file

@ -0,0 +1,149 @@
// 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::ordering::{
application::port::output::db::{store_id_exists::*, store_name_exists::*},
domain::{
add_store_command::AddStoreCommand,
store_added_event::{StoreAddedEvent, StoreAddedEventBuilder},
store_aggregate::*,
},
};
use crate::utils::uuid::*;
#[automock]
#[async_trait::async_trait]
pub trait AddStoreUseCase: Send + Sync {
async fn add_store(&self, cmd: AddStoreCommand) -> OrderingResult<StoreAddedEvent>;
}
pub type AddStoreServiceObj = Arc<dyn AddStoreUseCase>;
#[derive(Clone, Builder)]
pub struct AddStoreService {
db_store_id_exists: StoreIDExistsDBPortObj,
db_store_name_exists: StoreNameExistsDBPortObj,
get_uuid: GetUUIDInterfaceObj,
}
#[async_trait::async_trait]
impl AddStoreUseCase for AddStoreService {
async fn add_store(&self, cmd: AddStoreCommand) -> OrderingResult<StoreAddedEvent> {
let mut store_id = self.get_uuid.get_uuid();
loop {
if self.db_store_id_exists.store_id_exists(&store_id).await? {
store_id = self.get_uuid.get_uuid();
continue;
} else {
break;
}
}
let store = StoreBuilder::default()
.name(cmd.name().into())
.address(cmd.address().as_ref().map(|s| s.to_string()))
.owner(*cmd.owner())
.store_id(store_id)
.build()
.unwrap();
if self.db_store_name_exists.store_name_exists(&store).await? {
return Err(OrderingError::DuplicateStoreName);
}
Ok(StoreAddedEventBuilder::default()
.name(store.name().into())
.address(store.address().as_ref().map(|s| s.to_string()))
.owner(*cmd.owner())
.store_id(store_id)
.build()
.unwrap())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::tests::bdd::*;
use crate::utils::uuid::tests::*;
pub fn mock_add_store_service(
times: Option<usize>,
cmd: AddStoreCommand,
) -> AddStoreServiceObj {
let mut m = MockAddStoreUseCase::new();
let res = StoreAddedEventBuilder::default()
.name(cmd.name().into())
.address(cmd.address().as_ref().map(|s| s.to_string()))
.owner(*cmd.owner())
.store_id(UUID)
.build()
.unwrap();
if let Some(times) = times {
m.expect_add_store()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_add_store().returning(move |_| Ok(res.clone()));
}
Arc::new(m)
}
#[actix_rt::test]
async fn test_service_store_id_doesnt_exist() {
let name = "foo";
let address = "bar";
let owner = UUID;
// address = None
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap();
let s = AddStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_store_name_exists(mock_store_name_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
let res = s.add_store(cmd.clone()).await.unwrap();
assert_eq!(res.name(), cmd.name());
assert_eq!(res.address(), cmd.address());
assert_eq!(res.owner(), cmd.owner());
assert_eq!(res.store_id(), &UUID);
}
#[actix_rt::test]
async fn test_service_store_name_exists() {
let name = "foo";
let address = "bar";
let owner = UUID;
// address = None
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap();
let s = AddStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_store_name_exists(mock_store_name_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.add_store(cmd.clone()).await,
Err(OrderingError::DuplicateStoreName)
);
}
}

View file

@ -6,7 +6,9 @@ use derive_more::{Display, Error};
use log::error;
use serde::{Deserialize, Serialize};
use crate::ordering::application::port::output::db::errors::OrderingDBError;
use crate::ordering::application::port::output::{
db::errors::OrderingDBError, full_text_search::errors::OrderingFTSError,
};
pub type OrderingResult<V> = Result<V, OrderingError>;
@ -16,6 +18,14 @@ pub enum OrderingError {
InternalError,
OrderIDNotFound,
KotIDNotFound,
DuplicateStoreName,
StoreIDNotFound,
CategoryIDNotFound,
DuplicateCategoryName,
DuplicateProductName,
ProductIDNotFound,
DuplicateCustomizationName,
CustomizationIDNotFound,
}
//
impl From<OrderingDBError> for OrderingError {
@ -30,6 +40,13 @@ impl From<OrderingDBError> for OrderingError {
error!("DuplicateOrderID");
Self::InternalError
}
OrderingDBError::DuplicateStoreName => Self::DuplicateStoreName,
OrderingDBError::DuplicateStoreID => {
error!("DuplicateStoreID");
Self::InternalError
}
OrderingDBError::StoreIDNotFound => OrderingError::StoreIDNotFound,
OrderingDBError::OrderIDNotFound => OrderingError::OrderIDNotFound,
OrderingDBError::DuplicateKotID => {
@ -38,6 +55,31 @@ impl From<OrderingDBError> for OrderingError {
}
OrderingDBError::KotIDNotFound => OrderingError::KotIDNotFound,
OrderingDBError::InternalError => Self::InternalError,
OrderingDBError::DuplicateCategoryName => Self::DuplicateCategoryName,
OrderingDBError::DuplicateCategoryID => {
error!("DuplicateCategoryID");
Self::InternalError
}
OrderingDBError::CategoryIDNotFound => OrderingError::CategoryIDNotFound,
OrderingDBError::DuplicateProductName => Self::DuplicateProductName,
OrderingDBError::DuplicateProductID => {
error!("DuplicateProductID");
Self::InternalError
}
OrderingDBError::ProductIDNotFound => OrderingError::ProductIDNotFound,
OrderingDBError::DuplicateCustomizationName => Self::DuplicateCustomizationName,
OrderingDBError::DuplicateCustomizationID => {
error!("DuplicateCustomizationID");
Self::InternalError
}
OrderingDBError::CustomizationIDNotFound => OrderingError::CustomizationIDNotFound,
}
}
}
impl From<OrderingFTSError> for OrderingError {
fn from(value: OrderingFTSError) -> Self {
error!("{}", value);
OrderingError::InternalError
}
}

View file

@ -9,15 +9,23 @@ use mockall::*;
pub mod errors;
//services
pub mod add_category_service;
pub mod add_customization_service;
pub mod add_kot_service;
pub mod add_line_item_service;
pub mod add_order_service;
pub mod add_product_service;
pub mod add_store_service;
pub mod delete_kot_service;
pub mod delete_line_item_service;
pub mod delete_order_service;
pub mod update_category_service;
pub mod update_customization_service;
pub mod update_kot_service;
pub mod update_line_item_service;
pub mod update_order_service;
pub mod update_product_service;
pub mod update_store_service;
#[automock]
pub trait OrderingServicesInterface: Send + Sync {
@ -30,6 +38,14 @@ pub trait OrderingServicesInterface: Send + Sync {
fn add_kot(&self) -> add_kot_service::AddKotServiceObj;
fn update_kot(&self) -> update_kot_service::UpdateKotServiceObj;
fn delete_kot(&self) -> delete_kot_service::DeleteKotServiceObj;
fn add_store(&self) -> add_store_service::AddStoreServiceObj;
fn update_store(&self) -> update_store_service::UpdateStoreServiceObj;
fn add_category(&self) -> add_category_service::AddCategoryServiceObj;
fn update_category(&self) -> update_category_service::UpdateCategoryServiceObj;
fn add_product(&self) -> add_product_service::AddProductServiceObj;
fn update_product(&self) -> update_product_service::UpdateProductServiceObj;
fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj;
fn update_customization(&self) -> update_customization_service::UpdateCustomizationServiceObj;
}
#[derive(Clone, Builder)]
@ -43,6 +59,14 @@ pub struct OrderingServices {
add_kot: add_kot_service::AddKotServiceObj,
update_kot: update_kot_service::UpdateKotServiceObj,
delete_kot: delete_kot_service::DeleteKotServiceObj,
add_store: add_store_service::AddStoreServiceObj,
update_store: update_store_service::UpdateStoreServiceObj,
add_category: add_category_service::AddCategoryServiceObj,
update_category: update_category_service::UpdateCategoryServiceObj,
add_product: add_product_service::AddProductServiceObj,
update_product: update_product_service::UpdateProductServiceObj,
add_customization: add_customization_service::AddCustomizationServiceObj,
update_customization: update_customization_service::UpdateCustomizationServiceObj,
}
impl OrderingServicesInterface for OrderingServices {
@ -80,4 +104,28 @@ impl OrderingServicesInterface for OrderingServices {
fn delete_kot(&self) -> delete_kot_service::DeleteKotServiceObj {
self.delete_kot.clone()
}
fn add_store(&self) -> add_store_service::AddStoreServiceObj {
self.add_store.clone()
}
fn update_store(&self) -> update_store_service::UpdateStoreServiceObj {
self.update_store.clone()
}
fn add_category(&self) -> add_category_service::AddCategoryServiceObj {
self.add_category.clone()
}
fn update_category(&self) -> update_category_service::UpdateCategoryServiceObj {
self.update_category.clone()
}
fn add_product(&self) -> add_product_service::AddProductServiceObj {
self.add_product.clone()
}
fn update_product(&self) -> update_product_service::UpdateProductServiceObj {
self.update_product.clone()
}
fn add_customization(&self) -> add_customization_service::AddCustomizationServiceObj {
self.add_customization.clone()
}
fn update_customization(&self) -> update_customization_service::UpdateCustomizationServiceObj {
self.update_customization.clone()
}
}

View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

View file

@ -0,0 +1,198 @@
// 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::ordering::{
application::port::output::db::{
category_id_exists::*, category_name_exists_for_store::*, store_id_exists::*,
},
domain::{category_aggregate::*, category_updated_event::*, update_category_command::*},
};
use crate::utils::uuid::*;
#[automock]
#[async_trait::async_trait]
pub trait UpdateCategoryUseCase: Send + Sync {
async fn update_category(
&self,
cmd: UpdateCategoryCommand,
) -> OrderingResult<CategoryUpdatedEvent>;
}
pub type UpdateCategoryServiceObj = Arc<dyn UpdateCategoryUseCase>;
#[derive(Clone, Builder)]
pub struct UpdateCategoryService {
// TODO: check if store ID exists
db_category_name_exists_for_store: CategoryNameExistsForStoreDBPortObj,
db_category_id_exists: CategoryIDExistsDBPortObj,
db_store_id_exists: StoreIDExistsDBPortObj,
}
#[async_trait::async_trait]
impl UpdateCategoryUseCase for UpdateCategoryService {
async fn update_category(
&self,
cmd: UpdateCategoryCommand,
) -> OrderingResult<CategoryUpdatedEvent> {
if !self
.db_category_id_exists
.category_id_exists(cmd.old_category().category_id())
.await?
{
return Err(OrderingError::CategoryIDNotFound);
}
if !self
.db_store_id_exists
.store_id_exists(cmd.old_category().store_id())
.await?
{
return Err(OrderingError::StoreIDNotFound);
}
let updated_category = CategoryBuilder::default()
.name(cmd.name().into())
.description(cmd.description().as_ref().map(|s| s.to_string()))
.category_id(*cmd.old_category().category_id())
.store_id(*cmd.old_category().store_id())
.build()
.unwrap();
if updated_category.name() != cmd.old_category().name() {
if self
.db_category_name_exists_for_store
.category_name_exists_for_store(&updated_category)
.await?
{
return Err(OrderingError::DuplicateCategoryName);
}
}
Ok(CategoryUpdatedEventBuilder::default()
.added_by_user(*cmd.adding_by())
.old_category(cmd.old_category().clone())
.new_category(updated_category)
.build()
.unwrap())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::ordering::domain::category_updated_event;
use crate::ordering::domain::update_category_command::tests::get_update_category_command;
use crate::utils::uuid::tests::UUID;
use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid};
pub fn mock_update_category_service(
times: Option<usize>,
cmd: UpdateCategoryCommand,
) -> UpdateCategoryServiceObj {
let mut m = MockUpdateCategoryUseCase::new();
let res = category_updated_event::tests::get_category_updated_event_from_command(&cmd);
if let Some(times) = times {
m.expect_update_category()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_update_category()
.returning(move |_| Ok(res.clone()));
}
Arc::new(m)
}
#[actix_rt::test]
async fn test_service() {
let cmd = get_update_category_command();
let s = UpdateCategoryServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false(
IS_CALLED_ONLY_ONCE,
))
.db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
let res = s.update_category(cmd.clone()).await.unwrap();
assert_eq!(res.new_category().name(), cmd.name());
assert_eq!(res.new_category().description(), cmd.description());
assert_eq!(res.new_category().store_id(), cmd.old_category().store_id());
assert_eq!(
res.new_category().category_id(),
cmd.old_category().category_id()
);
assert_eq!(res.old_category(), cmd.old_category());
assert_eq!(res.added_by_user(), cmd.adding_by());
}
#[actix_rt::test]
async fn test_service_store_doesnt_exist() {
let cmd = get_update_category_command();
let s = UpdateCategoryServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false(
IS_NEVER_CALLED,
))
.db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.update_category(cmd.clone()).await,
Err(OrderingError::StoreIDNotFound)
);
}
#[actix_rt::test]
async fn test_category_id_not_found() {
let cmd = get_update_category_command();
let s = UpdateCategoryServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_true(IS_NEVER_CALLED))
.db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_false(
IS_NEVER_CALLED,
))
.db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.update_category(cmd.clone()).await,
Err(OrderingError::CategoryIDNotFound)
);
}
#[actix_rt::test]
async fn test_duplicate_new_name() {
let cmd = get_update_category_command();
let s = UpdateCategoryServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_category_name_exists_for_store(mock_category_name_exists_for_store_db_port_true(
IS_CALLED_ONLY_ONCE,
))
.db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.update_category(cmd.clone()).await,
Err(OrderingError::DuplicateCategoryName)
);
}
}

View file

@ -0,0 +1,210 @@
// 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::ordering::{
application::port::output::db::{
customization_id_exists::*, customization_name_exists_for_product::*, product_id_exists::*,
},
domain::{
customization_aggregate::*, customization_updated_event::*, update_customization_command::*,
},
};
use crate::utils::uuid::*;
#[automock]
#[async_trait::async_trait]
pub trait UpdateCustomizationUseCase: Send + Sync {
async fn update_customization(
&self,
cmd: UpdateCustomizationCommand,
) -> OrderingResult<CustomizationUpdatedEvent>;
}
pub type UpdateCustomizationServiceObj = Arc<dyn UpdateCustomizationUseCase>;
#[derive(Clone, Builder)]
pub struct UpdateCustomizationService {
// TODO: check if product ID exists
db_customization_name_exists_for_product: CustomizationNameExistsForProductDBPortObj,
db_customization_id_exists: CustomizationIDExistsDBPortObj,
db_product_id_exists: ProductIDExistsDBPortObj,
}
#[async_trait::async_trait]
impl UpdateCustomizationUseCase for UpdateCustomizationService {
async fn update_customization(
&self,
cmd: UpdateCustomizationCommand,
) -> OrderingResult<CustomizationUpdatedEvent> {
if !self
.db_customization_id_exists
.customization_id_exists(cmd.old_customization().customization_id())
.await?
{
return Err(OrderingError::CustomizationIDNotFound);
}
if !self
.db_product_id_exists
.product_id_exists(cmd.old_customization().product_id())
.await?
{
return Err(OrderingError::ProductIDNotFound);
}
let updated_customization = CustomizationBuilder::default()
.name(cmd.name().into())
.product_id(*cmd.old_customization().product_id())
.customization_id(*cmd.old_customization().customization_id())
.deleted(*cmd.old_customization().deleted())
.build()
.unwrap();
if updated_customization.name() != cmd.old_customization().name() {
if self
.db_customization_name_exists_for_product
.customization_name_exists_for_product(&updated_customization)
.await?
{
return Err(OrderingError::DuplicateCustomizationName);
}
}
Ok(CustomizationUpdatedEventBuilder::default()
.added_by_user(*cmd.adding_by())
.old_customization(cmd.old_customization().clone())
.new_customization(updated_customization)
.build()
.unwrap())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::ordering::domain::customization_updated_event;
use crate::ordering::domain::update_customization_command::tests::get_update_customization_command;
use crate::tests::bdd::*;
pub fn mock_update_customization_service(
times: Option<usize>,
cmd: UpdateCustomizationCommand,
) -> UpdateCustomizationServiceObj {
let mut m = MockUpdateCustomizationUseCase::new();
let res =
customization_updated_event::tests::get_customization_updated_event_from_command(&cmd);
if let Some(times) = times {
m.expect_update_customization()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_update_customization()
.returning(move |_| Ok(res.clone()));
}
Arc::new(m)
}
#[actix_rt::test]
async fn test_service() {
let cmd = get_update_customization_command();
let s = UpdateCustomizationServiceBuilder::default()
.db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_customization_name_exists_for_product(
mock_customization_name_exists_for_product_db_port_false(IS_CALLED_ONLY_ONCE),
)
.db_customization_id_exists(mock_customization_id_exists_db_port_true(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
let res = s.update_customization(cmd.clone()).await.unwrap();
assert_eq!(res.new_customization().name(), cmd.name());
assert_eq!(
res.new_customization().product_id(),
cmd.old_customization().product_id()
);
assert_eq!(
res.new_customization().customization_id(),
cmd.old_customization().customization_id()
);
assert_eq!(res.old_customization(), cmd.old_customization());
assert_eq!(res.added_by_user(), cmd.adding_by());
}
#[actix_rt::test]
async fn test_service_product_doesnt_exist() {
let cmd = get_update_customization_command();
let s = UpdateCustomizationServiceBuilder::default()
.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_NEVER_CALLED),
)
.db_customization_id_exists(mock_customization_id_exists_db_port_true(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
assert_eq!(
s.update_customization(cmd.clone()).await,
Err(OrderingError::ProductIDNotFound)
);
}
#[actix_rt::test]
async fn test_customization_id_not_found() {
let cmd = get_update_customization_command();
let s = UpdateCustomizationServiceBuilder::default()
.db_product_id_exists(mock_product_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),
)
.db_customization_id_exists(mock_customization_id_exists_db_port_false(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
assert_eq!(
s.update_customization(cmd.clone()).await,
Err(OrderingError::CustomizationIDNotFound)
);
}
#[actix_rt::test]
async fn test_duplicate_new_name() {
let cmd = get_update_customization_command();
let s = UpdateCustomizationServiceBuilder::default()
.db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_customization_name_exists_for_product(
mock_customization_name_exists_for_product_db_port_true(IS_CALLED_ONLY_ONCE),
)
.db_customization_id_exists(mock_customization_id_exists_db_port_true(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
assert_eq!(
s.update_customization(cmd.clone()).await,
Err(OrderingError::DuplicateCustomizationName)
);
}
}

View file

@ -0,0 +1,206 @@
// 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::ordering::{
application::port::output::db::{
category_id_exists::*, product_id_exists::*, product_name_exists_for_category::*,
},
domain::{product_aggregate::*, product_updated_event::*, update_product_command::*},
};
use crate::utils::uuid::*;
#[automock]
#[async_trait::async_trait]
pub trait UpdateProductUseCase: Send + Sync {
async fn update_product(
&self,
cmd: UpdateProductCommand,
) -> OrderingResult<ProductUpdatedEvent>;
}
pub type UpdateProductServiceObj = Arc<dyn UpdateProductUseCase>;
#[derive(Clone, Builder)]
pub struct UpdateProductService {
// TODO: check if category ID exists
db_product_name_exists_for_category: ProductNameExistsForCategoryDBPortObj,
db_product_id_exists: ProductIDExistsDBPortObj,
db_category_id_exists: CategoryIDExistsDBPortObj,
}
#[async_trait::async_trait]
impl UpdateProductUseCase for UpdateProductService {
async fn update_product(
&self,
cmd: UpdateProductCommand,
) -> OrderingResult<ProductUpdatedEvent> {
if !self
.db_product_id_exists
.product_id_exists(cmd.old_product().product_id())
.await?
{
return Err(OrderingError::ProductIDNotFound);
}
if !self
.db_category_id_exists
.category_id_exists(cmd.category_id())
.await?
{
return Err(OrderingError::CategoryIDNotFound);
}
let updated_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())
.price(cmd.price().clone())
.category_id(*cmd.category_id())
.quantity(cmd.quantity().clone())
.product_id(cmd.old_product().product_id().clone())
.build()
.unwrap();
if updated_product.name() != cmd.old_product().name() {
if self
.db_product_name_exists_for_category
.product_name_exists_for_category(&updated_product)
.await?
{
return Err(OrderingError::DuplicateProductName);
}
}
Ok(ProductUpdatedEventBuilder::default()
.added_by_user(*cmd.adding_by())
.old_product(cmd.old_product().clone())
.new_product(updated_product)
.build()
.unwrap())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::ordering::domain::product_updated_event;
use crate::ordering::domain::update_product_command::tests::get_command;
use crate::utils::uuid::tests::UUID;
use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid};
pub fn mock_update_product_service(
times: Option<usize>,
cmd: UpdateProductCommand,
) -> UpdateProductServiceObj {
let mut m = MockUpdateProductUseCase::new();
let res = product_updated_event::tests::get_event_from_command(&cmd);
if let Some(times) = times {
m.expect_update_product()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_update_product()
.returning(move |_| Ok(res.clone()));
}
Arc::new(m)
}
#[actix_rt::test]
async fn test_service() {
let cmd = get_command();
let s = UpdateProductServiceBuilder::default()
.db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.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_true(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
let res = s.update_product(cmd.clone()).await.unwrap();
assert_eq!(res.new_product().name(), cmd.name());
assert_eq!(res.new_product().description(), cmd.description());
assert_eq!(res.new_product().image(), cmd.image());
assert_eq!(res.new_product().sku_able(), cmd.sku_able());
assert_eq!(res.new_product().price(), cmd.price());
assert_eq!(res.new_product().category_id(), cmd.category_id());
assert_eq!(
res.new_product().product_id(),
cmd.old_product().product_id()
);
assert_eq!(res.new_product().quantity(), cmd.quantity());
assert_eq!(res.old_product(), cmd.old_product());
assert_eq!(res.added_by_user(), cmd.adding_by());
}
#[actix_rt::test]
async fn test_service_category_doesnt_exist() {
let cmd = get_command();
let s = UpdateProductServiceBuilder::default()
.db_category_id_exists(mock_category_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_product_name_exists_for_category(
mock_product_name_exists_for_category_db_port_false(IS_NEVER_CALLED),
)
.db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.update_product(cmd.clone()).await,
Err(OrderingError::CategoryIDNotFound)
);
}
#[actix_rt::test]
async fn test_product_id_not_found() {
let cmd = get_command();
let s = UpdateProductServiceBuilder::default()
.db_category_id_exists(mock_category_id_exists_db_port_true(IS_NEVER_CALLED))
.db_product_name_exists_for_category(
mock_product_name_exists_for_category_db_port_false(IS_NEVER_CALLED),
)
.db_product_id_exists(mock_product_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.update_product(cmd.clone()).await,
Err(OrderingError::ProductIDNotFound)
);
}
#[actix_rt::test]
async fn test_duplicate_new_name() {
let cmd = get_command();
let s = UpdateProductServiceBuilder::default()
.db_category_id_exists(mock_category_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_product_name_exists_for_category(
mock_product_name_exists_for_category_db_port_true(IS_CALLED_ONLY_ONCE),
)
.db_product_id_exists(mock_product_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.update_product(cmd.clone()).await,
Err(OrderingError::DuplicateProductName)
);
}
}

View file

@ -0,0 +1,146 @@
// 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::ordering::{
application::port::output::db::{store_id_exists::*, store_name_exists::*},
domain::{
store_aggregate::*, store_updated_event::*, update_store_command::UpdateStoreCommand,
},
};
use crate::utils::uuid::*;
#[automock]
#[async_trait::async_trait]
pub trait UpdateStoreUseCase: Send + Sync {
async fn update_store(&self, cmd: UpdateStoreCommand) -> OrderingResult<StoreUpdatedEvent>;
}
pub type UpdateStoreServiceObj = Arc<dyn UpdateStoreUseCase>;
#[derive(Clone, Builder)]
pub struct UpdateStoreService {
db_store_id_exists: StoreIDExistsDBPortObj,
db_store_name_exists: StoreNameExistsDBPortObj,
}
#[async_trait::async_trait]
impl UpdateStoreUseCase for UpdateStoreService {
async fn update_store(&self, cmd: UpdateStoreCommand) -> OrderingResult<StoreUpdatedEvent> {
if !self
.db_store_id_exists
.store_id_exists(cmd.old_store().store_id())
.await?
{
return Err(OrderingError::StoreIDNotFound);
}
let store = StoreBuilder::default()
.name(cmd.name().into())
.address(cmd.address().as_ref().map(|s| s.to_string()))
.owner(*cmd.owner())
.store_id(*cmd.old_store().store_id())
.build()
.unwrap();
if cmd.name() != cmd.old_store().name() {
if self.db_store_name_exists.store_name_exists(&store).await? {
return Err(OrderingError::DuplicateStoreName);
}
}
Ok(StoreUpdatedEventBuilder::default()
.added_by_user(*cmd.adding_by())
.new_store(store)
.old_store(cmd.old_store().clone())
.build()
.unwrap())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::ordering::domain::store_updated_event::tests::get_store_updated_event_from_command;
use crate::ordering::domain::update_store_command::tests::get_update_store_cmd;
use crate::tests::bdd::*;
use crate::utils::uuid::tests::*;
pub fn mock_update_store_service(
times: Option<usize>,
cmd: UpdateStoreCommand,
) -> UpdateStoreServiceObj {
let mut m = MockUpdateStoreUseCase::new();
let res = get_store_updated_event_from_command(&cmd);
if let Some(times) = times {
m.expect_update_store()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_update_store().returning(move |_| Ok(res.clone()));
}
Arc::new(m)
}
#[actix_rt::test]
async fn test_service() {
let cmd = get_update_store_cmd();
let s = UpdateStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_store_name_exists(mock_store_name_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
let res = s.update_store(cmd.clone()).await.unwrap();
assert_eq!(res.new_store().name(), cmd.name());
assert_eq!(res.new_store().address(), cmd.address());
assert_eq!(res.new_store().owner(), cmd.owner());
assert_eq!(res.new_store().store_id(), cmd.old_store().store_id());
assert_eq!(res.old_store(), cmd.old_store());
assert_eq!(res.added_by_user(), cmd.adding_by());
}
#[actix_rt::test]
async fn test_service_store_name_exists() {
let cmd = get_update_store_cmd();
let s = UpdateStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_store_name_exists(mock_store_name_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
assert_eq!(
s.update_store(cmd.clone()).await,
Err(OrderingError::DuplicateStoreName)
);
}
#[actix_rt::test]
async fn test_service_store_id_doesnt_exist() {
let cmd = get_update_store_cmd();
let s = UpdateStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_store_name_exists(mock_store_name_exists_db_port_false(IS_NEVER_CALLED))
.build()
.unwrap();
assert_eq!(
s.update_store(cmd.clone()).await,
Err(OrderingError::StoreIDNotFound)
);
}
}

View file

@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
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 AddCategoryCommandError {
NameIsEmpty,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct AddCategoryCommand {
name: String,
description: Option<String>,
store_id: Uuid,
adding_by: Uuid,
}
impl AddCategoryCommand {
pub fn new(
name: String,
description: Option<String>,
store_id: Uuid,
adding_by: Uuid,
) -> Result<Self, AddCategoryCommandError> {
let description: Option<String> = if let Some(description) = description {
let description = description.trim();
if description.is_empty() {
None
} else {
Some(description.to_owned())
}
} else {
None
};
let name = name.trim().to_owned();
if name.is_empty() {
return Err(AddCategoryCommandError::NameIsEmpty);
}
Ok(Self {
name,
store_id,
description,
adding_by,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::uuid::tests::UUID;
#[test]
fn test_cmd() {
let name = "foo";
let description = "bar";
let adding_by = UUID;
let store_id = Uuid::new_v4();
// description = None
let cmd = AddCategoryCommand::new(name.into(), None, store_id, adding_by).unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &None);
assert_eq!(cmd.adding_by(), &adding_by);
assert_eq!(cmd.store_id(), &store_id);
// description = Some
let cmd =
AddCategoryCommand::new(name.into(), Some(description.into()), store_id, adding_by)
.unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &Some(description.to_owned()));
assert_eq!(cmd.adding_by(), &adding_by);
assert_eq!(cmd.store_id(), &store_id);
// AddCategoryCommandError::NameIsEmpty
assert_eq!(
AddCategoryCommand::new("".into(), Some(description.into()), store_id, adding_by,),
Err(AddCategoryCommandError::NameIsEmpty)
)
}
}

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

@ -0,0 +1,256 @@
// 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;
use crate::types::currency::*;
use crate::types::quantity::*;
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum AddProductCommandError {
NameIsEmpty,
}
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct UnvalidatedAddProductCommand {
name: String,
description: Option<String>,
image: Option<String>,
category_id: Uuid,
sku_able: bool,
quantity: Quantity,
price: Price,
adding_by: Uuid,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct AddProductCommand {
name: String,
description: Option<String>,
image: Option<String>,
category_id: Uuid,
sku_able: bool,
price: Price,
quantity: Quantity,
adding_by: Uuid,
}
impl UnvalidatedAddProductCommand {
pub fn validate(self) -> Result<AddProductCommand, AddProductCommandError> {
let description: Option<String> = if let Some(description) = self.description {
let description = description.trim();
if description.is_empty() {
None
} else {
Some(description.to_owned())
}
} else {
None
};
let image: Option<String> = if let Some(image) = self.image {
let image = image.trim();
if image.is_empty() {
None
} else {
Some(image.to_owned())
}
} else {
None
};
let name = self.name.trim().to_owned();
if name.is_empty() {
return Err(AddProductCommandError::NameIsEmpty);
}
Ok(AddProductCommand {
name,
description,
image,
category_id: self.category_id,
sku_able: self.sku_able,
price: self.price,
quantity: self.quantity,
adding_by: self.adding_by,
})
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::utils::uuid::tests::UUID;
pub fn get_command() -> AddProductCommand {
let name = "foo";
let adding_by = UUID;
let category_id = Uuid::new_v4();
let sku_able = false;
let image = Some("image".to_string());
let description = Some("description".to_string());
let price = PriceBuilder::default()
.minor(0)
.major(100)
.currency(Currency::INR)
.build()
.unwrap();
let quantity = QuantityBuilder::default()
.minor(
QuantityPartBuilder::default()
.number(0)
.unit(QuantityUnit::DiscreteNumber)
.build()
.unwrap(),
)
.major(
QuantityPartBuilder::default()
.number(1)
.unit(QuantityUnit::DiscreteNumber)
.build()
.unwrap(),
)
.build()
.unwrap();
let cmd = UnvalidatedAddProductCommandBuilder::default()
.name(name.into())
.description(description.clone())
.image(image.clone())
.category_id(category_id)
.adding_by(adding_by)
.quantity(quantity)
.sku_able(sku_able)
.price(price.clone())
.build()
.unwrap();
cmd.validate().unwrap()
}
#[test]
fn test_description_and_image_none() {
let name = "foo";
let adding_by = UUID;
let category_id = Uuid::new_v4();
let sku_able = false;
let price = PriceBuilder::default()
.minor(0)
.major(100)
.currency(Currency::INR)
.build()
.unwrap();
let quantity = Quantity::default();
// description = None
let cmd = UnvalidatedAddProductCommandBuilder::default()
.name(name.into())
.description(None)
.image(None)
.category_id(category_id)
.adding_by(adding_by)
.quantity(quantity.clone())
.sku_able(sku_able)
.price(price.clone())
.build()
.unwrap();
let cmd = cmd.validate().unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &None);
assert_eq!(cmd.adding_by(), &adding_by);
assert_eq!(cmd.category_id(), &category_id);
assert_eq!(cmd.image(), &None);
assert_eq!(cmd.sku_able(), &sku_able);
assert_eq!(cmd.price(), &price);
assert_eq!(cmd.quantity(), &quantity);
}
#[test]
fn test_description_some() {
let name = "foo";
let adding_by = UUID;
let category_id = Uuid::new_v4();
let sku_able = false;
let image = Some("image".to_string());
let description = Some("description".to_string());
let price = PriceBuilder::default()
.minor(0)
.major(100)
.currency(Currency::INR)
.build()
.unwrap();
let quantity = Quantity::default();
let cmd = UnvalidatedAddProductCommandBuilder::default()
.name(name.into())
.description(description.clone())
.image(image.clone())
.category_id(category_id)
.quantity(quantity.clone())
.adding_by(adding_by)
.sku_able(sku_able)
.price(price.clone())
// .customizations(customizations.clone())
.build()
.unwrap();
let cmd = cmd.validate().unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &description);
assert_eq!(cmd.adding_by(), &adding_by);
assert_eq!(cmd.category_id(), &category_id);
assert_eq!(cmd.image(), &image);
assert_eq!(cmd.sku_able(), &sku_able);
assert_eq!(cmd.price(), &price);
assert_eq!(cmd.quantity(), &quantity);
}
#[test]
fn test_name_is_empty() {
let adding_by = UUID;
let category_id = Uuid::new_v4();
let sku_able = false;
let image = Some("image".to_string());
let description = Some("description".to_string());
let price = PriceBuilder::default()
.minor(0)
.major(100)
.currency(Currency::INR)
.build()
.unwrap();
let quantity = Quantity::default();
let cmd = UnvalidatedAddProductCommandBuilder::default()
.name("".into())
.description(description.clone())
.image(image.clone())
.category_id(category_id)
.adding_by(adding_by)
.quantity(quantity)
.sku_able(sku_able)
.price(price.clone())
.build()
.unwrap();
// AddProductCommandError::NameIsEmpty
assert_eq!(cmd.validate(), Err(AddProductCommandError::NameIsEmpty))
}
}

View file

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
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 AddStoreCommandError {
NameIsEmpty,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct AddStoreCommand {
name: String,
address: Option<String>,
owner: Uuid,
}
impl AddStoreCommand {
pub fn new(
name: String,
address: Option<String>,
owner: Uuid,
) -> Result<Self, AddStoreCommandError> {
let address: Option<String> = if let Some(address) = address {
let address = address.trim();
if address.is_empty() {
None
} else {
Some(address.to_owned())
}
} else {
None
};
let name = name.trim().to_owned();
if name.is_empty() {
return Err(AddStoreCommandError::NameIsEmpty);
}
Ok(Self {
name,
address,
owner,
})
}
}
#[cfg(test)]
mod tests {
use crate::utils::uuid::tests::UUID;
use super::*;
#[test]
fn test_cmd() {
let name = "foo";
let address = "bar";
let owner = UUID;
// address = None
let cmd = AddStoreCommand::new(name.into(), None, owner).unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.address(), &None);
assert_eq!(cmd.owner(), &owner);
// address = Some
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.address(), &Some(address.to_owned()));
assert_eq!(cmd.owner(), &owner);
// AddStoreCommandError::NameIsEmpty
assert_eq!(
AddStoreCommand::new("".into(), Some(address.into()), owner),
Err(AddStoreCommandError::NameIsEmpty)
)
}
}

View file

@ -0,0 +1,19 @@
// 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;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct CategoryAddedEvent {
name: String,
description: Option<String>,
added_by_user: Uuid,
category_id: Uuid,
store_id: Uuid,
}

View file

@ -0,0 +1,156 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use async_trait::async_trait;
use cqrs_es::Aggregate;
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::ordering::application::services::errors::*;
use crate::ordering::application::services::OrderingServicesInterface;
use super::{commands::OrderingCommand, events::OrderingEvent};
#[derive(
Clone, Debug, Serialize, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct Category {
name: String,
description: Option<String>,
store_id: Uuid,
category_id: Uuid,
#[builder(default = "false")]
deleted: bool,
}
#[async_trait]
impl Aggregate for Category {
type Command = OrderingCommand;
type Event = OrderingEvent;
type Error = OrderingError;
type Services = std::sync::Arc<dyn OrderingServicesInterface>;
// This identifier should be unique to the system.
fn aggregate_type() -> String {
"ordering.category".to_string()
}
// The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system
// so expect to use helper functions elsewhere to keep the code clean.
async fn handle(
&self,
command: Self::Command,
services: &Self::Services,
) -> Result<Vec<Self::Event>, Self::Error> {
match command {
OrderingCommand::AddCategory(cmd) => {
let res = services.add_category().add_category(cmd).await?;
Ok(vec![OrderingEvent::CategoryAdded(res)])
}
OrderingCommand::UpdateCategory(cmd) => {
let res = services.update_category().update_category(cmd).await?;
Ok(vec![OrderingEvent::CategoryUpdated(res)])
}
_ => Ok(Vec::default()),
}
}
fn apply(&mut self, event: Self::Event) {
match event {
OrderingEvent::CategoryAdded(e) => {
*self = CategoryBuilder::default()
.name(e.name().into())
.category_id(*e.category_id())
.description(e.description().clone())
.store_id(*e.store_id())
.build()
.unwrap()
}
OrderingEvent::CategoryUpdated(e) => *self = e.new_category().clone(),
_ => (),
}
}
}
#[cfg(test)]
mod aggregate_tests {
use std::sync::Arc;
use cqrs_es::test::TestFramework;
use update_category_service::tests::mock_update_category_service;
use uuid::Uuid;
use super::*;
use crate::ordering::{
application::services::{add_category_service::tests::*, *},
domain::{
add_category_command::*, category_added_event::*,
category_updated_event::tests::get_category_updated_event_from_command,
commands::OrderingCommand, events::OrderingEvent,
update_category_command::tests::get_update_category_command,
},
};
use crate::tests::bdd::*;
use crate::utils::uuid::tests::*;
type CategoryTestFramework = TestFramework<Category>;
#[test]
fn test_create_category() {
let name = "category_name";
let description = Some("category_description".to_string());
let adding_by = UUID;
let store_id = Uuid::new_v4();
let category_id = UUID;
let cmd =
AddCategoryCommand::new(name.into(), description.clone(), store_id, adding_by).unwrap();
let expected = CategoryAddedEventBuilder::default()
.name(cmd.name().into())
.description(cmd.description().as_ref().map(|s| s.to_string()))
.added_by_user(*cmd.adding_by())
.store_id(*cmd.store_id())
.category_id(category_id)
.build()
.unwrap();
let expected = OrderingEvent::CategoryAdded(expected);
let mut services = MockOrderingServicesInterface::new();
services
.expect_add_category()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(mock_add_category_service(IS_CALLED_ONLY_ONCE, cmd.clone()));
CategoryTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(OrderingCommand::AddCategory(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_update_category() {
let cmd = get_update_category_command();
let expected = get_category_updated_event_from_command(&cmd);
let expected = OrderingEvent::CategoryUpdated(expected);
let mut services = MockOrderingServicesInterface::new();
services
.expect_update_category()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(mock_update_category_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
CategoryTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(OrderingCommand::UpdateCategory(cmd))
.then_expect_events(vec![expected]);
}
}

View file

@ -0,0 +1,45 @@
// 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::category_aggregate::*;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct CategoryUpdatedEvent {
added_by_user: Uuid,
new_category: Category,
old_category: Category,
}
#[cfg(test)]
pub mod tests {
use crate::ordering::domain::update_category_command::UpdateCategoryCommand;
use super::*;
pub fn get_category_updated_event_from_command(
cmd: &UpdateCategoryCommand,
) -> CategoryUpdatedEvent {
let category = CategoryBuilder::default()
.name(cmd.name().into())
.description(cmd.description().as_ref().map(|s| s.to_string()))
.category_id(*cmd.old_category().category_id())
.store_id(*cmd.old_category().store_id())
.build()
.unwrap();
CategoryUpdatedEventBuilder::default()
.new_category(category)
.old_category(cmd.old_category().clone())
.added_by_user(*cmd.adding_by())
.build()
.unwrap()
}
}

View file

@ -6,11 +6,15 @@ use mockall::predicate::*;
use serde::{Deserialize, Serialize};
use super::{
add_category_command::AddCategoryCommand, add_customization_command::AddCustomizationCommand,
add_kot_command::AddKotCommand, add_line_item_command::AddLineItemCommand,
add_order_command::AddOrderCommand, delete_kot_command::DeleteKotCommand,
add_order_command::AddOrderCommand, add_product_command::AddProductCommand,
add_store_command::AddStoreCommand, delete_kot_command::DeleteKotCommand,
delete_line_item_command::DeleteLineItemCommand, delete_order_command::DeleteOrderCommand,
update_kot_command::UpdateKotCommand, update_line_item_command::UpdateLineItemCommand,
update_order_command::UpdateOrderCommand,
update_category_command::UpdateCategoryCommand,
update_customization_command::UpdateCustomizationCommand, update_kot_command::UpdateKotCommand,
update_line_item_command::UpdateLineItemCommand, update_order_command::UpdateOrderCommand,
update_product_command::UpdateProductCommand, update_store_command::UpdateStoreCommand,
};
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
@ -24,4 +28,12 @@ pub enum OrderingCommand {
AddKot(AddKotCommand),
UpdateKot(UpdateKotCommand),
DeleteKot(DeleteKotCommand),
AddProduct(AddProductCommand),
UpdateProduct(UpdateProductCommand),
AddStore(AddStoreCommand),
UpdateStore(UpdateStoreCommand),
AddCategory(AddCategoryCommand),
UpdateCategory(UpdateCategoryCommand),
AddCustomization(AddCustomizationCommand),
UpdateCustomization(UpdateCustomizationCommand),
}

View file

@ -0,0 +1,43 @@
// 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,
}
#[cfg(test)]
pub mod tests {
use crate::ordering::domain::add_customization_command::AddCustomizationCommand;
use super::*;
use crate::utils::uuid::tests::UUID;
pub fn get_customization_added_event_from_cmd(
cmd: &AddCustomizationCommand,
) -> CustomizationAddedEvent {
let customization = CustomizationBuilder::default()
.name(cmd.name().into())
.deleted(false)
.product_id(*cmd.product_id())
.customization_id(UUID.clone())
.build()
.unwrap();
CustomizationAddedEventBuilder::default()
.customization(customization)
.build()
.unwrap()
}
}

View file

@ -0,0 +1,143 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::str::FromStr;
use async_trait::async_trait;
use config::builder;
use cqrs_es::Aggregate;
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::{commands::OrderingCommand, events::OrderingEvent};
use crate::ordering::application::services::errors::*;
use crate::ordering::application::services::OrderingServicesInterface;
#[derive(
Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct Customization {
name: String,
customization_id: Uuid,
product_id: Uuid,
#[builder(default = "false")]
deleted: bool,
}
#[async_trait]
impl Aggregate for Customization {
type Command = OrderingCommand;
type Event = OrderingEvent;
type Error = OrderingError;
type Services = std::sync::Arc<dyn OrderingServicesInterface>;
// This identifier should be unique to the system.
fn aggregate_type() -> String {
"ordering.product".to_string()
}
// The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system
// so expect to use helper functions elsewhere to keep the code clean.
async fn handle(
&self,
command: Self::Command,
services: &Self::Services,
) -> Result<Vec<Self::Event>, Self::Error> {
match command {
OrderingCommand::AddCustomization(cmd) => {
let res = services.add_customization().add_customization(cmd).await?;
Ok(vec![OrderingEvent::CustomizationAdded(res)])
}
OrderingCommand::UpdateCustomization(cmd) => {
let res = services
.update_customization()
.update_customization(cmd)
.await?;
Ok(vec![OrderingEvent::CustomizationUpdated(res)])
}
_ => Ok(Vec::default()),
}
}
fn apply(&mut self, event: Self::Event) {
match event {
OrderingEvent::CustomizationAdded(e) => {
*self = e.customization().clone();
}
OrderingEvent::CustomizationUpdated(e) => {
*self = e.new_customization().clone();
}
_ => (),
}
}
}
#[cfg(test)]
mod aggregate_tests {
use std::sync::Arc;
use cqrs_es::test::TestFramework;
use super::*;
use crate::ordering::{
application::services::{
add_customization_service::tests::*, update_customization_service::tests::*, *,
},
domain::{
add_customization_command, commands::OrderingCommand,
customization_added_event::tests::get_customization_added_event_from_cmd,
customization_updated_event::tests::get_customization_updated_event_from_command,
events::OrderingEvent,
update_customization_command::tests::get_update_customization_command,
},
};
use crate::tests::bdd::*;
type CustomizationTestFramework = TestFramework<Customization>;
#[test]
fn test_create_customization() {
let cmd = add_customization_command::tests::get_command();
let expected = get_customization_added_event_from_cmd(&cmd);
let expected = OrderingEvent::CustomizationAdded(expected);
let mut services = MockOrderingServicesInterface::new();
services
.expect_add_customization()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(mock_add_customization_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
CustomizationTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(OrderingCommand::AddCustomization(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_update_customization() {
let cmd = get_update_customization_command();
let expected = get_customization_updated_event_from_command(&cmd);
let expected = OrderingEvent::CustomizationUpdated(expected);
let mut services = MockOrderingServicesInterface::new();
services
.expect_update_customization()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(mock_update_customization_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
CustomizationTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(OrderingCommand::UpdateCustomization(cmd))
.then_expect_events(vec![expected]);
}
}

View file

@ -0,0 +1,49 @@
// 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::Customization;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct CustomizationUpdatedEvent {
added_by_user: Uuid,
old_customization: Customization,
new_customization: Customization,
}
#[cfg(test)]
pub mod tests {
use crate::ordering::domain::{
customization_aggregate::*, update_customization_command::UpdateCustomizationCommand,
};
use crate::utils::uuid::tests::UUID;
use super::*;
pub fn get_customization_updated_event_from_command(
cmd: &UpdateCustomizationCommand,
) -> CustomizationUpdatedEvent {
let customization = CustomizationBuilder::default()
.name(cmd.name().into())
.deleted(false)
.customization_id(UUID.clone())
.product_id(UUID.clone())
.build()
.unwrap();
CustomizationUpdatedEventBuilder::default()
.added_by_user(cmd.adding_by().clone())
.new_customization(customization)
.old_customization(cmd.old_customization().clone())
.build()
.unwrap()
}
}

View file

@ -6,11 +6,15 @@ use cqrs_es::DomainEvent;
use serde::{Deserialize, Serialize};
use super::{
kot_added_event::KotAddedEvent, kot_deleted_event::KotDeletedEvent,
kot_updated_event::KotUpdatedEvent, line_item_added_event::LineItemAddedEvent,
line_item_deleted_event::LineItemDeletedEvent, line_item_updated_event::LineItemUpdatedEvent,
order_added_event::OrderAddedEvent, order_deleted_event::OrderDeletedEvent,
order_updated_event::OrderUpdatedEvent,
category_added_event::CategoryAddedEvent, category_updated_event::CategoryUpdatedEvent,
customization_added_event::CustomizationAddedEvent,
customization_updated_event::CustomizationUpdatedEvent, kot_added_event::KotAddedEvent,
kot_deleted_event::KotDeletedEvent, kot_updated_event::KotUpdatedEvent,
line_item_added_event::LineItemAddedEvent, line_item_deleted_event::LineItemDeletedEvent,
line_item_updated_event::LineItemUpdatedEvent, order_added_event::OrderAddedEvent,
order_deleted_event::OrderDeletedEvent, order_updated_event::OrderUpdatedEvent,
product_added_event::ProductAddedEvent, product_updated_event::ProductUpdatedEvent,
store_added_event::StoreAddedEvent, store_updated_event::StoreUpdatedEvent,
};
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
@ -24,6 +28,14 @@ pub enum OrderingEvent {
KotAdded(KotAddedEvent),
KotUpdated(KotUpdatedEvent),
KotDeleted(KotDeletedEvent),
ProductAdded(ProductAddedEvent),
ProductUpdated(ProductUpdatedEvent),
StoreAdded(StoreAddedEvent),
StoreUpdated(StoreUpdatedEvent),
CategoryAdded(CategoryAddedEvent),
CategoryUpdated(CategoryUpdatedEvent),
CustomizationAdded(CustomizationAddedEvent),
CustomizationUpdated(CustomizationUpdatedEvent),
}
impl DomainEvent for OrderingEvent {
@ -42,6 +54,14 @@ impl DomainEvent for OrderingEvent {
OrderingEvent::KotAdded { .. } => "OrderingKotAdded",
OrderingEvent::KotUpdated { .. } => "OrderingKotUpdated",
OrderingEvent::KotDeleted { .. } => "OrderingKotDeleted",
OrderingEvent::ProductAdded { .. } => "OrderingProductAdded",
OrderingEvent::ProductUpdated { .. } => "OrderingProductUpdated",
OrderingEvent::StoreAdded { .. } => "OrderingStoreAdded",
OrderingEvent::StoreUpdated { .. } => "OrderingStoreUpdated",
OrderingEvent::CategoryAdded { .. } => "OrderingCategoryAdded",
OrderingEvent::CategoryUpdated { .. } => "OrderingCategoryUpdated",
OrderingEvent::CustomizationAdded { .. } => "OrderingCustomizationAdded",
OrderingEvent::CustomizationUpdated { .. } => "OrderingCategoryUpdated",
};
e.to_string()

View file

@ -3,23 +3,40 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// aggregates
pub mod category_aggregate;
pub mod customization_aggregate;
pub mod kot_aggregate;
pub mod line_item_aggregate;
pub mod order_aggregate;
pub mod pantry_aggregate;
pub mod product_aggregate;
pub mod store_aggregate;
// commands
pub mod add_category_command;
pub mod add_customization_command;
pub mod add_kot_command;
pub mod add_line_item_command;
pub mod add_order_command;
pub mod add_product_command;
pub mod add_store_command;
pub mod commands;
pub mod delete_kot_command;
pub mod delete_line_item_command;
pub mod delete_order_command;
pub mod update_category_command;
pub mod update_customization_command;
pub mod update_kot_command;
pub mod update_line_item_command;
pub mod update_order_command;
pub mod update_product_command;
pub mod update_store_command;
// events
pub mod category_added_event;
pub mod category_updated_event;
pub mod customization_added_event;
pub mod customization_updated_event;
pub mod events;
pub mod kot_added_event;
pub mod kot_deleted_event;
@ -30,3 +47,7 @@ pub mod line_item_updated_event;
pub mod order_added_event;
pub mod order_deleted_event;
pub mod order_updated_event;
pub mod product_added_event;
pub mod product_updated_event;
pub mod store_added_event;
pub mod store_updated_event;

View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

View file

@ -0,0 +1,51 @@
// 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 crate::types::currency::*;
use crate::types::quantity::Quantity;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct ProductAddedEvent {
added_by_user: Uuid,
name: String,
description: Option<String>,
image: Option<String>, // string = file_name
price: Price,
quantity: Quantity,
category_id: Uuid,
sku_able: bool,
product_id: Uuid,
}
#[cfg(test)]
pub mod tests {
use crate::ordering::domain::add_product_command::AddProductCommand;
use super::*;
use crate::utils::uuid::tests::UUID;
pub fn get_event_from_command(cmd: &AddProductCommand) -> ProductAddedEvent {
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())
.category_id(*cmd.category_id())
.product_id(UUID)
.price(cmd.price().clone())
.quantity(cmd.quantity().clone())
.added_by_user(*cmd.adding_by())
.build()
.unwrap()
}
}

View file

@ -0,0 +1,166 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use async_trait::async_trait;
use cqrs_es::Aggregate;
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::{commands::OrderingCommand, events::OrderingEvent};
use crate::ordering::application::services::errors::*;
use crate::ordering::application::services::OrderingServicesInterface;
use crate::types::currency::*;
use crate::types::quantity::Quantity;
#[derive(
Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct Product {
name: String,
description: Option<String>,
image: Option<String>, // string = file_name
price: Price,
// stock = Σ (not sold SKU), if SKU is relevant. Where irrelevant; it exists independent of SKU.
// relevancy is determined Product.sku_able
quantity: Quantity,
category_id: Uuid,
sku_able: bool,
product_id: Uuid,
#[builder(default = "false")]
deleted: bool,
}
#[async_trait]
impl Aggregate for Product {
type Command = OrderingCommand;
type Event = OrderingEvent;
type Error = OrderingError;
type Services = std::sync::Arc<dyn OrderingServicesInterface>;
// This identifier should be unique to the system.
fn aggregate_type() -> String {
"ordering.product".to_string()
}
// The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system
// so expect to use helper functions elsewhere to keep the code clean.
async fn handle(
&self,
command: Self::Command,
services: &Self::Services,
) -> Result<Vec<Self::Event>, Self::Error> {
match command {
OrderingCommand::AddProduct(cmd) => {
let res = services.add_product().add_product(cmd).await?;
Ok(vec![OrderingEvent::ProductAdded(res)])
}
OrderingCommand::UpdateProduct(cmd) => {
let res = services.update_product().update_product(cmd).await?;
Ok(vec![OrderingEvent::ProductUpdated(res)])
}
_ => Ok(Vec::default()),
}
}
fn apply(&mut self, event: Self::Event) {
match event {
OrderingEvent::ProductAdded(e) => {
*self = ProductBuilder::default()
.name(e.name().into())
.description(e.description().clone())
.image(e.image().clone())
.price(e.price().clone())
.category_id(e.category_id().clone())
.sku_able(e.sku_able().clone())
.product_id(e.product_id().clone())
.quantity(e.quantity().clone())
.deleted(false)
.build()
.unwrap();
}
OrderingEvent::ProductUpdated(e) => {
*self = e.new_product().clone();
}
_ => (),
}
}
}
#[cfg(test)]
mod aggregate_tests {
use std::str::FromStr;
use std::sync::Arc;
use cqrs_es::test::TestFramework;
use update_product_service::tests::mock_update_product_service;
use super::*;
use crate::ordering::{
application::services::{add_product_service::tests::*, *},
domain::{
add_product_command::tests::get_command, commands::OrderingCommand,
events::OrderingEvent, product_added_event::tests::get_event_from_command,
product_updated_event, update_product_command,
},
};
use crate::tests::bdd::*;
type ProductTestFramework = TestFramework<Product>;
#[test]
fn test_create_product() {
let cmd = get_command();
let expected = get_event_from_command(&cmd);
let expected = OrderingEvent::ProductAdded(expected);
let mut services = MockOrderingServicesInterface::new();
services
.expect_add_product()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(mock_add_product_service(IS_CALLED_ONLY_ONCE, cmd.clone()));
ProductTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(OrderingCommand::AddProduct(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_update_product() {
let cmd = update_product_command::tests::get_command();
let expected = product_updated_event::tests::get_event_from_command(&cmd);
let expected = OrderingEvent::ProductUpdated(expected);
let mut services = MockOrderingServicesInterface::new();
services
.expect_update_product()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(mock_update_product_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
ProductTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(OrderingCommand::UpdateProduct(cmd))
.then_expect_events(vec![expected]);
}
fn test_helper<T>(t: T, str_value: &str) -> bool
where
T: ToString + FromStr + std::fmt::Debug + PartialEq,
<T as FromStr>::Err: std::fmt::Debug,
{
println!("Testing type: {:?} against value {str_value}", t);
assert_eq!(t.to_string(), str_value.to_string());
assert_eq!(T::from_str(str_value).unwrap(), t);
assert_eq!(T::from_str(t.to_string().as_str()).unwrap(), t,);
true
}
}

View file

@ -0,0 +1,53 @@
// 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::product_aggregate::Product;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct ProductUpdatedEvent {
added_by_user: Uuid,
old_product: Product,
new_product: Product,
}
#[cfg(test)]
pub mod tests {
use crate::ordering::domain::{
product_aggregate::*, update_product_command::UpdateProductCommand,
};
use super::*;
#[test]
fn test_name() {}
pub fn get_event_from_command(cmd: &UpdateProductCommand) -> ProductUpdatedEvent {
let updated_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())
.price(cmd.price().clone())
.category_id(*cmd.category_id())
.quantity(cmd.quantity().clone())
.product_id(cmd.old_product().product_id().clone())
.build()
.unwrap();
ProductUpdatedEventBuilder::default()
.added_by_user(cmd.adding_by().clone())
.new_product(updated_product)
.old_product(cmd.old_product().clone())
.build()
.unwrap()
}
}

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;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct StoreAddedEvent {
name: String,
address: Option<String>,
owner: Uuid,
store_id: Uuid,
}

View file

@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use async_trait::async_trait;
use cqrs_es::Aggregate;
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::ordering::application::services::errors::*;
use crate::ordering::application::services::OrderingServicesInterface;
use super::{commands::OrderingCommand, events::OrderingEvent};
#[derive(
Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters,
)]
pub struct Store {
name: String,
address: Option<String>,
owner: Uuid,
store_id: Uuid,
#[builder(default = "false")]
deleted: bool,
}
#[async_trait]
impl Aggregate for Store {
type Command = OrderingCommand;
type Event = OrderingEvent;
type Error = OrderingError;
type Services = std::sync::Arc<dyn OrderingServicesInterface>;
// This identifier should be unique to the system.
fn aggregate_type() -> String {
"ordering.store".to_string()
}
// The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system
// so expect to use helper functions elsewhere to keep the code clean.
async fn handle(
&self,
command: Self::Command,
services: &Self::Services,
) -> Result<Vec<Self::Event>, Self::Error> {
match command {
OrderingCommand::AddStore(cmd) => {
let res = services.add_store().add_store(cmd).await?;
Ok(vec![OrderingEvent::StoreAdded(res)])
}
OrderingCommand::UpdateStore(cmd) => {
let res = services.update_store().update_store(cmd).await?;
Ok(vec![OrderingEvent::StoreUpdated(res)])
}
_ => Ok(Vec::default()),
}
}
fn apply(&mut self, event: Self::Event) {
match event {
OrderingEvent::StoreAdded(e) => {
self.name = e.name().into();
self.address = e.address().as_ref().map(|s| s.to_string());
self.owner = *e.owner();
self.store_id = *e.store_id();
self.deleted = false;
}
// OrderingEvent::StoreUpdated(e) => *self = e.new_store().clone(),
_ => (),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use cqrs_es::test::TestFramework;
use update_store_service::tests::mock_update_store_service;
use super::*;
use crate::ordering::{
application::services::{add_store_service::tests::*, *},
domain::{
add_store_command::*, commands::OrderingCommand, events::OrderingEvent,
store_added_event::*, store_updated_event::tests::get_store_updated_event_from_command,
update_store_command::tests::get_update_store_cmd,
},
};
use crate::tests::bdd::*;
use crate::utils::uuid::tests::*;
// A test framework that will apply our events and command
// and verify that the logic works as expected.
type StoreTestFramework = TestFramework<Store>;
#[test]
fn test_create_store() {
let name = "store_name";
let address = Some("store_address".to_string());
let owner = UUID;
let store_id = UUID;
let expected = StoreAddedEventBuilder::default()
.name(name.into())
.address(address.clone())
.store_id(store_id)
.owner(owner)
.build()
.unwrap();
let expected = OrderingEvent::StoreAdded(expected);
let cmd = AddStoreCommand::new(name.into(), address.clone(), owner).unwrap();
let mut services = MockOrderingServicesInterface::new();
services
.expect_add_store()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(mock_add_store_service(IS_CALLED_ONLY_ONCE, cmd.clone()));
StoreTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(OrderingCommand::AddStore(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_update_store() {
let cmd = get_update_store_cmd();
let expected = OrderingEvent::StoreUpdated(get_store_updated_event_from_command(&cmd));
let mut services = MockOrderingServicesInterface::new();
services
.expect_update_store()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(mock_update_store_service(IS_CALLED_ONLY_ONCE, cmd.clone()));
StoreTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(OrderingCommand::UpdateStore(cmd))
.then_expect_events(vec![expected]);
}
}

View file

@ -0,0 +1,43 @@
// 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::store_aggregate::*;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct StoreUpdatedEvent {
added_by_user: Uuid,
old_store: Store,
new_store: Store,
}
#[cfg(test)]
pub mod tests {
use crate::ordering::domain::update_store_command::UpdateStoreCommand;
use super::*;
pub fn get_store_updated_event_from_command(cmd: &UpdateStoreCommand) -> StoreUpdatedEvent {
let new_store = StoreBuilder::default()
.name(cmd.name().into())
.address(cmd.address().as_ref().map(|s| s.to_string()))
.owner(*cmd.owner())
.store_id(*cmd.old_store().store_id())
.build()
.unwrap();
StoreUpdatedEventBuilder::default()
.new_store(new_store)
.old_store(cmd.old_store().clone())
.added_by_user(*cmd.adding_by())
.build()
.unwrap()
}
}

View file

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_getters::Getters;
use derive_more::{Display, Error};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::category_aggregate::*;
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum UpdateCategoryCommandError {
NameIsEmpty,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct UpdateCategoryCommand {
name: String,
description: Option<String>,
adding_by: Uuid,
old_category: Category,
}
impl UpdateCategoryCommand {
pub fn new(
name: String,
description: Option<String>,
old_category: Category,
adding_by: Uuid,
) -> Result<Self, UpdateCategoryCommandError> {
let description: Option<String> = if let Some(description) = description {
let description = description.trim();
if description.is_empty() {
None
} else {
Some(description.to_owned())
}
} else {
None
};
let name = name.trim().to_owned();
if name.is_empty() {
return Err(UpdateCategoryCommandError::NameIsEmpty);
}
Ok(Self {
name,
description,
old_category,
adding_by,
})
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::utils::uuid::tests::UUID;
pub fn get_update_category_command() -> UpdateCategoryCommand {
let name = "foo";
let description = "bar";
let adding_by = UUID;
let old_category = Category::default();
UpdateCategoryCommand::new(
name.into(),
Some(description.into()),
old_category.clone(),
adding_by,
)
.unwrap()
}
#[test]
fn test_cmd() {
let name = "foo";
let description = "bar";
let adding_by = UUID;
let old_category = Category::default();
// description = None
let cmd =
UpdateCategoryCommand::new(name.into(), None, old_category.clone(), adding_by).unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &None);
assert_eq!(cmd.adding_by(), &adding_by);
assert_eq!(cmd.old_category(), &old_category);
// description = Some
let cmd = UpdateCategoryCommand::new(
name.into(),
Some(description.into()),
old_category.clone(),
adding_by,
)
.unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &Some(description.to_owned()));
assert_eq!(cmd.adding_by(), &adding_by);
assert_eq!(cmd.old_category(), &old_category);
// UpdateCategoryCommandError::NameIsEmpty
assert_eq!(
UpdateCategoryCommand::new(
"".into(),
Some(description.into()),
old_category,
adding_by,
),
Err(UpdateCategoryCommandError::NameIsEmpty)
)
}
}

View file

@ -0,0 +1,101 @@
// 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;
use super::customization_aggregate::Customization;
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum UpdateCustomizationCommandError {
NameIsEmpty,
}
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct UnvalidatedUpdateCustomizationCommand {
name: String,
adding_by: Uuid,
old_customization: Customization,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct UpdateCustomizationCommand {
name: String,
old_customization: Customization,
adding_by: Uuid,
}
impl UnvalidatedUpdateCustomizationCommand {
pub fn validate(self) -> Result<UpdateCustomizationCommand, UpdateCustomizationCommandError> {
let name = self.name.trim().to_owned();
if name.is_empty() {
return Err(UpdateCustomizationCommandError::NameIsEmpty);
}
Ok(UpdateCustomizationCommand {
name,
old_customization: self.old_customization,
adding_by: self.adding_by,
})
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::{
ordering::domain::customization_aggregate::Customization, utils::uuid::tests::UUID,
};
pub fn get_update_customization_command() -> UpdateCustomizationCommand {
let customozation = Customization::default();
UnvalidatedUpdateCustomizationCommandBuilder::default()
.name("foo".into())
.old_customization(customozation)
.adding_by(UUID.clone())
.build()
.unwrap()
.validate()
.unwrap()
}
#[test]
fn test_cmd() {
let name = "foo";
let cmd = UnvalidatedUpdateCustomizationCommandBuilder::default()
.name(name.into())
.old_customization(Customization::default())
.adding_by(UUID.clone())
.build()
.unwrap()
.validate()
.unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.adding_by(), &UUID);
assert_eq!(cmd.old_customization(), &Customization::default());
}
#[test]
fn test_cmd_name_is_empty() {
assert_eq!(
UnvalidatedUpdateCustomizationCommandBuilder::default()
.name("".into())
.adding_by(UUID.clone())
.old_customization(Customization::default())
.build()
.unwrap()
.validate(),
Err(UpdateCustomizationCommandError::NameIsEmpty)
);
}
}

View file

@ -0,0 +1,266 @@
// 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;
use super::product_aggregate::Product;
use crate::types::currency::*;
use crate::types::quantity::Quantity;
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum UpdateProductCommandError {
NameIsEmpty,
}
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct UnvalidatedUpdateProductCommand {
name: String,
description: Option<String>,
image: Option<String>,
category_id: Uuid,
sku_able: bool,
quantity: Quantity,
price: Price,
adding_by: Uuid,
old_product: Product,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct UpdateProductCommand {
name: String,
description: Option<String>,
image: Option<String>,
category_id: Uuid,
sku_able: bool,
price: Price,
quantity: Quantity,
adding_by: Uuid,
old_product: Product,
}
impl UnvalidatedUpdateProductCommand {
pub fn validate(self) -> Result<UpdateProductCommand, UpdateProductCommandError> {
let description: Option<String> = if let Some(description) = self.description {
let description = description.trim();
if description.is_empty() {
None
} else {
Some(description.to_owned())
}
} else {
None
};
let image: Option<String> = if let Some(image) = self.image {
let image = image.trim();
if image.is_empty() {
None
} else {
Some(image.to_owned())
}
} else {
None
};
let name = self.name.trim().to_owned();
if name.is_empty() {
return Err(UpdateProductCommandError::NameIsEmpty);
}
Ok(UpdateProductCommand {
name,
description,
image,
category_id: self.category_id,
sku_able: self.sku_able,
price: self.price,
quantity: self.quantity,
adding_by: self.adding_by,
old_product: self.old_product,
})
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::types::quantity::*;
use crate::utils::uuid::tests::UUID;
pub fn get_command() -> UpdateProductCommand {
let name = "foo";
let adding_by = UUID;
let category_id = Uuid::new_v4();
let sku_able = false;
let image = Some("image".to_string());
let description = Some("description".to_string());
let price = PriceBuilder::default()
.minor(0)
.major(100)
.currency(Currency::INR)
.build()
.unwrap();
let quantity = QuantityBuilder::default()
.minor(
QuantityPartBuilder::default()
.number(0)
.unit(QuantityUnit::DiscreteNumber)
.build()
.unwrap(),
)
.major(
QuantityPartBuilder::default()
.number(1)
.unit(QuantityUnit::DiscreteNumber)
.build()
.unwrap(),
)
.build()
.unwrap();
let cmd = UnvalidatedUpdateProductCommandBuilder::default()
.name(name.into())
.description(description.clone())
.image(image.clone())
.category_id(category_id.clone())
.adding_by(adding_by.clone())
.quantity(quantity)
.sku_able(sku_able)
.price(price.clone())
.old_product(Product::default())
.build()
.unwrap();
cmd.validate().unwrap()
}
#[test]
fn test_description_and_image_none() {
let name = "foo";
let adding_by = UUID;
let category_id = Uuid::new_v4();
let sku_able = false;
let price = PriceBuilder::default()
.minor(0)
.major(100)
.currency(Currency::INR)
.build()
.unwrap();
let quantity = Quantity::default();
// description = None
let cmd = UnvalidatedUpdateProductCommandBuilder::default()
.name(name.into())
.description(None)
.image(None)
.category_id(category_id.clone())
.adding_by(adding_by.clone())
.quantity(quantity.clone())
.sku_able(sku_able)
.price(price.clone())
.old_product(Product::default())
.build()
.unwrap();
let cmd = cmd.validate().unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &None);
assert_eq!(cmd.adding_by(), &adding_by);
assert_eq!(cmd.category_id(), &category_id);
assert_eq!(cmd.image(), &None);
assert_eq!(cmd.sku_able(), &sku_able);
assert_eq!(cmd.old_product(), &Product::default());
assert_eq!(cmd.price(), &price);
assert_eq!(cmd.quantity(), &quantity);
}
#[test]
fn test_description_some() {
let name = "foo";
let adding_by = UUID;
let category_id = Uuid::new_v4();
let sku_able = false;
let image = Some("image".to_string());
let description = Some("description".to_string());
let price = PriceBuilder::default()
.minor(0)
.major(100)
.currency(Currency::INR)
.build()
.unwrap();
let quantity = Quantity::default();
let cmd = UnvalidatedUpdateProductCommandBuilder::default()
.name(name.into())
.description(description.clone())
.image(image.clone())
.category_id(category_id.clone())
.quantity(quantity.clone())
.adding_by(adding_by.clone())
.sku_able(sku_able)
.price(price.clone())
.old_product(Product::default())
.build()
.unwrap();
let cmd = cmd.validate().unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &description);
assert_eq!(cmd.adding_by(), &adding_by);
assert_eq!(cmd.category_id(), &category_id);
assert_eq!(cmd.image(), &image);
assert_eq!(cmd.sku_able(), &sku_able);
assert_eq!(cmd.price(), &price);
assert_eq!(cmd.quantity(), &quantity);
assert_eq!(cmd.old_product(), &Product::default());
}
#[test]
fn test_name_is_empty() {
let adding_by = UUID;
let category_id = Uuid::new_v4();
let sku_able = false;
let image = Some("image".to_string());
let description = Some("description".to_string());
let price = PriceBuilder::default()
.minor(0)
.major(100)
.currency(Currency::INR)
.build()
.unwrap();
let quantity = Quantity::default();
let cmd = UnvalidatedUpdateProductCommandBuilder::default()
.name("".into())
.description(description.clone())
.image(image.clone())
.category_id(category_id.clone())
.adding_by(adding_by.clone())
.quantity(quantity)
.old_product(Product::default())
.sku_able(sku_able)
.price(price.clone())
.build()
.unwrap();
// UpdateProductCommandError::NameIsEmpty
assert_eq!(cmd.validate(), Err(UpdateProductCommandError::NameIsEmpty))
}
}

View file

@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_getters::Getters;
use derive_more::{Display, Error};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::store_aggregate::*;
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum UpdateStoreCommandError {
NameIsEmpty,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct UpdateStoreCommand {
name: String,
address: Option<String>,
owner: Uuid,
old_store: Store,
adding_by: Uuid,
}
impl UpdateStoreCommand {
pub fn new(
name: String,
address: Option<String>,
owner: Uuid,
old_store: Store,
adding_by: Uuid,
) -> Result<Self, UpdateStoreCommandError> {
let address: Option<String> = if let Some(address) = address {
let address = address.trim();
if address.is_empty() {
None
} else {
Some(address.to_owned())
}
} else {
None
};
let name = name.trim().to_owned();
if name.is_empty() {
return Err(UpdateStoreCommandError::NameIsEmpty);
}
Ok(Self {
name,
address,
owner,
old_store,
adding_by,
})
}
}
#[cfg(test)]
pub mod tests {
use crate::utils::uuid::tests::UUID;
use super::*;
pub fn get_update_store_cmd() -> UpdateStoreCommand {
let name = "foo";
let address = "bar";
let owner = UUID;
let adding_by = UUID;
let old_store = Store::default();
UpdateStoreCommand::new(
name.into(),
Some(address.into()),
owner,
old_store.clone(),
adding_by,
)
.unwrap()
}
#[test]
fn test_cmd() {
let name = "foo";
let address = "bar";
let owner = UUID;
let old_store = Store::default();
let adding_by = Uuid::new_v4();
// address = None
let cmd = UpdateStoreCommand::new(name.into(), None, owner, old_store.clone(), adding_by)
.unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.address(), &None);
assert_eq!(cmd.owner(), &owner);
assert_eq!(cmd.old_store(), &old_store);
assert_eq!(cmd.adding_by(), &adding_by);
// address = Some
let cmd = UpdateStoreCommand::new(
name.into(),
Some(address.into()),
owner,
old_store.clone(),
adding_by,
)
.unwrap();
assert_eq!(cmd.name(), name);
assert_eq!(cmd.address(), &Some(address.to_owned()));
assert_eq!(cmd.owner(), &owner);
assert_eq!(cmd.old_store(), &old_store);
assert_eq!(cmd.adding_by(), &adding_by);
// UpdateStoreCommandError::NameIsEmpty
assert_eq!(
UpdateStoreCommand::new(
"".into(),
Some(address.into()),
owner,
old_store.clone(),
adding_by
),
Err(UpdateStoreCommandError::NameIsEmpty)
)
}
}

View file

@ -2,5 +2,5 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod quantity;
pub mod currency;
pub mod quantity;