From 4a51a3d62950d358f80a988c1fbd9569e656d45e Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sat, 21 Sep 2024 19:55:11 +0530 Subject: [PATCH] feat: billing: aggregate IDs are provided by caller & test View impl --- ...11bf302a3750ecadf28b1d6af62da62934f87.json | 27 -- ...fd3fcdd1fc63bdcd95f8a974823089652aa51.json | 26 ++ ...108b23bdf71dd904bdd5287155161138565d.json} | 5 +- ...190bb9746000067d53990eb7cd646ff5d252.json} | 5 +- .../output/db/postgres/bill_id_exists.rs | 2 +- .../adapters/output/db/postgres/bill_view.rs | 284 +++++++++++------- .../output/db/postgres/line_item_view.rs | 234 ++++++++++++++- .../output/db/postgres/store_id_exists.rs | 2 +- .../adapters/output/db/postgres/store_view.rs | 259 +++++++++------- .../application/services/add_bill_service.rs | 18 +- .../services/add_line_item_service.rs | 28 +- .../application/services/add_store_service.rs | 47 +-- src/billing/application/services/errors.rs | 18 +- src/billing/domain/add_bill_command.rs | 7 +- src/billing/domain/add_line_item_command.rs | 116 +++---- src/billing/domain/add_store_command.rs | 162 ++++++++-- src/billing/domain/line_item_aggregate.rs | 2 +- src/billing/domain/store_aggregate.rs | 8 +- src/billing/domain/update_bill_command.rs | 3 - 19 files changed, 812 insertions(+), 441 deletions(-) delete mode 100644 .sqlx/query-0268f0c43abe34a3147f0a43f0e11bf302a3750ecadf28b1d6af62da62934f87.json create mode 100644 .sqlx/query-995cca627c711a87b30723c6ceefd3fcdd1fc63bdcd95f8a974823089652aa51.json rename .sqlx/{query-b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5.json => query-c30f49bb293ca6e184c5110bdfe1108b23bdf71dd904bdd5287155161138565d.json} (54%) rename .sqlx/{query-3811531518316435c32223582f9de6bdfefc19839129577407a1e4071f5c49f6.json => query-d7decc8f70fc4f12d7a1db5009d2190bb9746000067d53990eb7cd646ff5d252.json} (64%) diff --git a/.sqlx/query-0268f0c43abe34a3147f0a43f0e11bf302a3750ecadf28b1d6af62da62934f87.json b/.sqlx/query-0268f0c43abe34a3147f0a43f0e11bf302a3750ecadf28b1d6af62da62934f87.json deleted file mode 100644 index 7e16644..0000000 --- a/.sqlx/query-0268f0c43abe34a3147f0a43f0e11bf302a3750ecadf28b1d6af62da62934f87.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_billing_line_item_query\n SET\n version = $1,\n product_name = $2,\n product_id = $3,\n line_item_id = $4,\n quantity_minor_unit = $5,\n quantity_minor_number = $6,\n quantity_major_unit = $7,\n quantity_major_number = $8,\n created_time = $9,\n bill_id = $10,\n price_per_unit_minor = $11 ,\n price_per_unit_major = $12,\n price_per_unit_currency = $13,\n deleted = $14;", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text", - "Uuid", - "Uuid", - "Text", - "Int4", - "Text", - "Int4", - "Timestamptz", - "Uuid", - "Int4", - "Int4", - "Text", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "0268f0c43abe34a3147f0a43f0e11bf302a3750ecadf28b1d6af62da62934f87" -} diff --git a/.sqlx/query-995cca627c711a87b30723c6ceefd3fcdd1fc63bdcd95f8a974823089652aa51.json b/.sqlx/query-995cca627c711a87b30723c6ceefd3fcdd1fc63bdcd95f8a974823089652aa51.json new file mode 100644 index 0000000..329c0a4 --- /dev/null +++ b/.sqlx/query-995cca627c711a87b30723c6ceefd3fcdd1fc63bdcd95f8a974823089652aa51.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE\n cqrs_billing_line_item_query\n SET\n version = $1,\n product_name = $2,\n product_id = $3,\n quantity_minor_unit = $4,\n quantity_minor_number = $5,\n quantity_major_unit = $6,\n quantity_major_number = $7,\n created_time = $8,\n bill_id = $9,\n price_per_unit_minor = $10 ,\n price_per_unit_major = $11,\n price_per_unit_currency = $12,\n deleted = $13;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Uuid", + "Text", + "Int4", + "Text", + "Int4", + "Timestamptz", + "Uuid", + "Int4", + "Int4", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "995cca627c711a87b30723c6ceefd3fcdd1fc63bdcd95f8a974823089652aa51" +} diff --git a/.sqlx/query-b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5.json b/.sqlx/query-c30f49bb293ca6e184c5110bdfe1108b23bdf71dd904bdd5287155161138565d.json similarity index 54% rename from .sqlx/query-b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5.json rename to .sqlx/query-c30f49bb293ca6e184c5110bdfe1108b23bdf71dd904bdd5287155161138565d.json index e9e4784..6a7638b 100644 --- a/.sqlx/query-b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5.json +++ b/.sqlx/query-c30f49bb293ca6e184c5110bdfe1108b23bdf71dd904bdd5287155161138565d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_billing_bill_query\n SET\n version = $1,\n\n created_time = $2,\n store_id = $3,\n bill_id = $4,\n token_number = $5,\n total_price_major = $6,\n total_price_minor = $7,\n total_price_currency = $8,\n\n deleted = $9;", + "query": "UPDATE\n cqrs_billing_bill_query\n SET\n version = $1,\n\n created_time = $2,\n store_id = $3,\n token_number = $4,\n total_price_major = $5,\n total_price_minor = $6,\n total_price_currency = $7,\n deleted = $8;", "describe": { "columns": [], "parameters": { @@ -8,7 +8,6 @@ "Int8", "Timestamptz", "Uuid", - "Uuid", "Int4", "Int4", "Int4", @@ -18,5 +17,5 @@ }, "nullable": [] }, - "hash": "b335fc519289a42c707855b620a35433d3f8bd1e798772c05fc156494c036ef5" + "hash": "c30f49bb293ca6e184c5110bdfe1108b23bdf71dd904bdd5287155161138565d" } diff --git a/.sqlx/query-3811531518316435c32223582f9de6bdfefc19839129577407a1e4071f5c49f6.json b/.sqlx/query-d7decc8f70fc4f12d7a1db5009d2190bb9746000067d53990eb7cd646ff5d252.json similarity index 64% rename from .sqlx/query-3811531518316435c32223582f9de6bdfefc19839129577407a1e4071f5c49f6.json rename to .sqlx/query-d7decc8f70fc4f12d7a1db5009d2190bb9746000067d53990eb7cd646ff5d252.json index 2a116d6..9d29a5d 100644 --- a/.sqlx/query-3811531518316435c32223582f9de6bdfefc19839129577407a1e4071f5c49f6.json +++ b/.sqlx/query-d7decc8f70fc4f12d7a1db5009d2190bb9746000067d53990eb7cd646ff5d252.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE\n cqrs_billing_store_query\n SET\n version = $1,\n name = $2,\n address = $3,\n store_id = $4,\n owner = $5,\n deleted = $6;", + "query": "UPDATE\n cqrs_billing_store_query\n SET\n version = $1,\n name = $2,\n address = $3,\n owner = $4,\n deleted = $5;", "describe": { "columns": [], "parameters": { @@ -9,11 +9,10 @@ "Text", "Text", "Uuid", - "Uuid", "Bool" ] }, "nullable": [] }, - "hash": "3811531518316435c32223582f9de6bdfefc19839129577407a1e4071f5c49f6" + "hash": "d7decc8f70fc4f12d7a1db5009d2190bb9746000067d53990eb7cd646ff5d252" } diff --git a/src/billing/adapters/output/db/postgres/bill_id_exists.rs b/src/billing/adapters/output/db/postgres/bill_id_exists.rs index 74e2170..9dc3ba8 100644 --- a/src/billing/adapters/output/db/postgres/bill_id_exists.rs +++ b/src/billing/adapters/output/db/postgres/bill_id_exists.rs @@ -36,7 +36,7 @@ pub mod tests { // use crate::billing::domain::add_product_command::tests::get_customizations; use crate::billing::domain::bill_aggregate::*; - async fn create_dummy_bill(bill: &Bill, db: &BillingDBPostgresAdapter) { + pub async fn create_dummy_bill(bill: &Bill, db: &BillingDBPostgresAdapter) { sqlx::query!( "INSERT INTO cqrs_billing_bill_query ( version, diff --git a/src/billing/adapters/output/db/postgres/bill_view.rs b/src/billing/adapters/output/db/postgres/bill_view.rs index 71c73cc..80d06d8 100644 --- a/src/billing/adapters/output/db/postgres/bill_view.rs +++ b/src/billing/adapters/output/db/postgres/bill_view.rs @@ -2,6 +2,8 @@ // // 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}; @@ -11,8 +13,9 @@ use uuid::Uuid; use super::errors::*; use super::BillingDBPostgresAdapter; -use crate::billing::domain::bill_aggregate::Bill; +use crate::billing::domain::bill_aggregate::{Bill, BillBuilder}; use crate::billing::domain::events::BillingEvent; +use crate::types::currency::{self, Currency, PriceBuilder}; use crate::utils::parse_aggregate_id::parse_aggregate_id; pub const NEW_BILL_NON_UUID: &str = "billing_new_bill_non_uuid-asdfa"; @@ -34,6 +37,36 @@ pub struct BillView { deleted: bool, } +impl From for Bill { + fn from(v: BillView) -> Self { + let price = match ( + v.total_price_minor, + v.total_price_major, + v.total_price_currency, + ) { + (Some(minor), Some(major), Some(currency)) => Some( + PriceBuilder::default() + .major(major as usize) + .minor(minor as usize) + .currency(Currency::from_str(¤cy).unwrap()) + .build() + .unwrap(), + ), + _ => None, + }; + + BillBuilder::default() + .created_time(v.created_time) + .store_id(v.store_id) + .bill_id(v.bill_id) + .token_number(v.token_number as usize) + .total_price(price) + .deleted(v.deleted) + .build() + .unwrap() + } +} + impl Default for BillView { fn default() -> Self { Self { @@ -225,17 +258,14 @@ impl ViewRepository for BillingDBPostgresAdapter { created_time = $2, store_id = $3, - bill_id = $4, - token_number = $5, - total_price_major = $6, - total_price_minor = $7, - total_price_currency = $8, - - deleted = $9;", + token_number = $4, + total_price_major = $5, + total_price_minor = $6, + total_price_currency = $7, + deleted = $8;", version, view.created_time, view.store_id, - view.bill_id, view.token_number, view.total_price_major, view.total_price_minor, @@ -281,108 +311,136 @@ impl Query for BillingDBPostgresAdapter { } } -// 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 BillQuery = GenericQuery; -//pub type BillQuery = Query; +#[cfg(test)] +mod tests { + use super::*; -//#[cfg(test)] -//mod tests { -// use super::*; -// -// use postgres_es::PostgresCqrs; -// -// use crate::{ -// db::migrate::*, -// billing::{ -// 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_bill_service::AddBillServiceBuilder, 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_bill_service::tests::mock_update_bill_service, BillingServicesBuilder -// }, -// domain::{ -// add_category_command::AddCategoryCommand, add_customization_command, -// add_product_command::tests::get_command, add_bill_command::AddBillCommand, -// commands::BillingCommand, -// update_category_command::tests::get_update_category_command, -// update_customization_command::tests::get_update_customization_command, -// update_product_command, update_bill_command::tests::get_update_bill_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 = BillingDBPostgresAdapter::new(db.pool.clone()); -// -// let simple_query = SimpleLoggingQuery {}; -// -// let queries: Vec>> = -// vec![Box::new(simple_query), Box::new(db.clone())]; -// -// let services = BillingServicesBuilder::default() -// .add_bill(Arc::new( -// AddBillServiceBuilder::default() -// .db_bill_id_exists(Arc::new(db.clone())) -// .db_bill_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_bill(mock_update_bill_service( -// IS_NEVER_CALLED, -// get_update_bill_cmd(), -// )) -// .build() -// .unwrap(); -// -// let (cqrs, _bill_query): ( -// Arc>, -// Arc>, -// ) = ( -// 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 = AddBillCommand::new(rand.get_random(10), None, UUID).unwrap(); -// cqrs.execute("", BillingCommand::AddBill(cmd.clone())) -// .await -// .unwrap(); -// -// settings.drop_db().await; -// } -//} + use postgres_es::PostgresCqrs; + + use crate::{ + billing::{ + application::services::{ + add_bill_service::AddBillServiceBuilder, update_bill_service::*, + MockBillingServicesInterface, + }, + domain::{ + add_bill_command::*, commands::BillingCommand, store_aggregate::Store, + update_bill_command::*, + }, + }, + db::migrate::*, + tests::bdd::*, + utils::uuid::tests::*, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_billing_bill_view() { + 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 = BillingDBPostgresAdapter::new(db.pool.clone()); + + let simple_query = SimpleLoggingQuery {}; + + let queries: Vec>> = vec![Box::new(simple_query), Box::new(db.clone())]; + + let mut mock_services = MockBillingServicesInterface::new(); + + let store = Store::default(); + crate::billing::adapters::output::db::postgres::store_id_exists::tests::create_dummy_store_record(&store, &db).await; + + let db2 = db.clone(); + mock_services + .expect_add_bill() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddBillServiceBuilder::default() + .db_bill_id_exists(Arc::new(db2.clone())) + .db_next_token_id(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_update_bill() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateBillServiceBuilder::default() + .db_bill_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let (cqrs, bill_query): ( + Arc>, + Arc>, + ) = ( + Arc::new(postgres_es::postgres_cqrs( + db.pool.clone(), + queries, + Arc::new(mock_services), + )), + Arc::new(db.clone()), + ); + + let cmd = AddBillCommandBuilder::default() + .adding_by(UUID) + .bill_id(UUID) + .store_id(*store.store_id()) + .build() + .unwrap(); + + cqrs.execute( + &cmd.bill_id().to_string(), + BillingCommand::AddBill(cmd.clone()), + ) + .await + .unwrap(); + + let bill = bill_query + .load(&(*cmd.bill_id()).to_string()) + .await + .unwrap() + .unwrap(); + let bill: Bill = bill.into(); + assert_eq!(bill.store_id(), cmd.store_id()); + assert_eq!(bill.bill_id(), cmd.bill_id()); + assert!(!bill.deleted()); + + let update_bill_cmd = UpdateBillCommandBuilder::default() + .adding_by(UUID) + .store_id(*store.store_id()) + .total_price(None) + .old_bill(bill.clone()) + .build() + .unwrap(); + + cqrs.execute( + &cmd.bill_id().to_string(), + BillingCommand::UpdateBill(update_bill_cmd.clone()), + ) + .await + .unwrap(); + let bill = bill_query + .load(&(*cmd.bill_id()).to_string()) + .await + .unwrap() + .unwrap(); + let bill: Bill = bill.into(); + assert_eq!(bill.store_id(), cmd.store_id()); + assert_eq!(bill.bill_id(), update_bill_cmd.old_bill().bill_id()); + assert_eq!(bill.total_price(), update_bill_cmd.total_price()); + assert!(!bill.deleted()); + + settings.drop_db().await; + } +} diff --git a/src/billing/adapters/output/db/postgres/line_item_view.rs b/src/billing/adapters/output/db/postgres/line_item_view.rs index c98a875..7559177 100644 --- a/src/billing/adapters/output/db/postgres/line_item_view.rs +++ b/src/billing/adapters/output/db/postgres/line_item_view.rs @@ -309,21 +309,19 @@ impl ViewRepository for BillingDBPostgresAdapter { version = $1, product_name = $2, product_id = $3, - line_item_id = $4, - quantity_minor_unit = $5, - quantity_minor_number = $6, - quantity_major_unit = $7, - quantity_major_number = $8, - created_time = $9, - bill_id = $10, - price_per_unit_minor = $11 , - price_per_unit_major = $12, - price_per_unit_currency = $13, - deleted = $14;", + quantity_minor_unit = $4, + quantity_minor_number = $5, + quantity_major_unit = $6, + quantity_major_number = $7, + created_time = $8, + bill_id = $9, + price_per_unit_minor = $10 , + price_per_unit_major = $11, + price_per_unit_currency = $12, + deleted = $13;", version, view.product_name, view.product_id, - view.line_item_id, view.quantity_minor_unit, view.quantity_minor_number, view.quantity_major_unit, @@ -364,3 +362,215 @@ impl Query for BillingDBPostgresAdapter { self.update_view(view, view_context).await.unwrap(); } } + +#[cfg(test)] +mod tests { + use super::*; + + use postgres_es::PostgresCqrs; + + use crate::{ + billing::{ + application::services::{ + add_line_item_service::AddLineItemServiceBuilder, delete_line_item_service::*, + update_line_item_service::*, MockBillingServicesInterface, + }, + domain::{ + add_line_item_command::*, bill_aggregate::Bill, commands::BillingCommand, + delete_line_item_command::DeleteLineItemCommandBuilder, + update_line_item_command::*, + }, + }, + db::migrate::*, + tests::bdd::*, + types::quantity::*, + utils::{ + random_string::GenerateRandomStringInterface, + uuid::{tests::UUID, *}, + }, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_billing_line_item_view() { + 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 = BillingDBPostgresAdapter::new(db.pool.clone()); + + let queries: Vec>> = vec![Box::new(db.clone())]; + + let mut mock_services = MockBillingServicesInterface::new(); + + let bill = Bill::default(); + crate::billing::adapters::output::db::postgres::bill_id_exists::tests::create_dummy_bill( + &bill, &db, + ) + .await; + + let db2 = db.clone(); + mock_services + .expect_add_line_item() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddLineItemServiceBuilder::default() + .db_line_item_id_exists(Arc::new(db2.clone())) + .db_bill_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_update_line_item() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateLineItemServiceBuilder::default() + .db_line_item_id_exists(Arc::new(db2.clone())) + .db_bill_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_delete_line_item() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + DeleteLineItemServiceBuilder::default() + .db_line_item_id_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let (cqrs, line_item_query): ( + Arc>, + Arc>, + ) = ( + Arc::new(postgres_es::postgres_cqrs( + db.pool.clone(), + queries, + Arc::new(mock_services), + )), + Arc::new(db.clone()), + ); + + let rand = crate::utils::random_string::GenerateRandomString {}; + let uuid = GenerateUUID {}; + let line_item_id = uuid.get_uuid(); + + let cmd = AddLineItemCommandBuilder::default() + .product_name(rand.get_random(10)) + .adding_by(UUID) + .price_per_unit(Price::default()) + .quantity(Quantity::get_quantity()) + .product_id(UUID) + .bill_id(*bill.bill_id()) + .line_item_id(line_item_id) + .build() + .unwrap(); + + cqrs.execute( + &cmd.line_item_id().to_string(), + BillingCommand::AddLineItem(cmd.clone()), + ) + .await + .unwrap(); + + let line_item = line_item_query + .load(&(*cmd.line_item_id()).to_string()) + .await + .unwrap() + .unwrap(); + let line_item: LineItem = line_item.into(); + assert_eq!(line_item.line_item_id(), cmd.line_item_id()); + assert_eq!(line_item.product_name(), cmd.product_name()); + assert_eq!(line_item.product_id(), cmd.product_id()); + assert_eq!(line_item.quantity(), cmd.quantity()); + assert!(!line_item.deleted()); + + let update_line_item_cmd = UnvalidatedUpdateLineItemCommandBuilder::default() + .product_name(rand.get_random(10)) + .adding_by(UUID) + .quantity(Quantity::get_quantity()) + .product_id(UUID) + .bill_id(*bill.bill_id()) + .old_line_item(line_item.clone()) + .price_per_unit(Price::default()) + .build() + .unwrap() + .validate() + .unwrap(); + + cqrs.execute( + &cmd.line_item_id().to_string(), + BillingCommand::UpdateLineItem(update_line_item_cmd.clone()), + ) + .await + .unwrap(); + let line_item = line_item_query + .load(&(*cmd.line_item_id()).to_string()) + .await + .unwrap() + .unwrap(); + let line_item: LineItem = line_item.into(); + assert_eq!( + line_item.line_item_id(), + update_line_item_cmd.old_line_item().line_item_id() + ); + assert_eq!( + line_item.product_name(), + update_line_item_cmd.product_name() + ); + assert_eq!(line_item.product_id(), update_line_item_cmd.product_id()); + assert_eq!(line_item.quantity(), update_line_item_cmd.quantity()); + assert!(!line_item.deleted()); + + // delete + let delete_line_item_cmd = DeleteLineItemCommandBuilder::default() + .line_item(line_item.clone()) + .adding_by(UUID) + .build() + .unwrap(); + cqrs.execute( + &cmd.line_item_id().to_string(), + BillingCommand::DeleteLineItem(delete_line_item_cmd.clone()), + ) + .await + .unwrap(); + let deleted_line_item = line_item_query + .load(&(*cmd.line_item_id()).to_string()) + .await + .unwrap() + .unwrap(); + let deleted_line_item: LineItem = deleted_line_item.into(); + assert_eq!( + deleted_line_item.line_item_id(), + delete_line_item_cmd.line_item().line_item_id() + ); + assert_eq!( + deleted_line_item.product_name(), + delete_line_item_cmd.line_item().product_name() + ); + assert_eq!( + deleted_line_item.product_id(), + delete_line_item_cmd.line_item().product_id() + ); + assert_eq!( + deleted_line_item.quantity(), + delete_line_item_cmd.line_item().quantity() + ); + assert!(deleted_line_item.deleted()); + + settings.drop_db().await; + } +} diff --git a/src/billing/adapters/output/db/postgres/store_id_exists.rs b/src/billing/adapters/output/db/postgres/store_id_exists.rs index 09baa97..b2aabfb 100644 --- a/src/billing/adapters/output/db/postgres/store_id_exists.rs +++ b/src/billing/adapters/output/db/postgres/store_id_exists.rs @@ -45,7 +45,7 @@ pub mod tests { VALUES ($1, $2, $3, $4, $5 ,$6);", 1, s.name(), - s.address().as_ref().unwrap(), + s.address().as_ref().map(|s| s.as_str()), s.store_id(), s.owner(), false diff --git a/src/billing/adapters/output/db/postgres/store_view.rs b/src/billing/adapters/output/db/postgres/store_view.rs index acb6f80..ddb9060 100644 --- a/src/billing/adapters/output/db/postgres/store_view.rs +++ b/src/billing/adapters/output/db/postgres/store_view.rs @@ -11,7 +11,7 @@ use uuid::Uuid; use super::errors::*; use super::BillingDBPostgresAdapter; use crate::billing::domain::events::BillingEvent; -use crate::billing::domain::store_aggregate::Store; +use crate::billing::domain::store_aggregate::*; use crate::utils::parse_aggregate_id::parse_aggregate_id; pub const NEW_STORE_NON_UUID: &str = "billing_new_store_non_uuid-asdfa"; @@ -27,6 +27,19 @@ pub struct StoreView { deleted: bool, } +impl From for Store { + fn from(value: StoreView) -> Self { + StoreBuilder::default() + .name(value.name) + .address(value.address) + .store_id(value.store_id) + .owner(value.owner) + .deleted(value.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. @@ -156,13 +169,11 @@ impl ViewRepository for BillingDBPostgresAdapter { version = $1, name = $2, address = $3, - store_id = $4, - owner = $5, - deleted = $6;", + owner = $4, + deleted = $5;", version, view.name, view.address, - view.store_id, view.owner, view.deleted, ) @@ -205,108 +216,138 @@ impl Query for BillingDBPostgresAdapter { } } -// 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; -//pub type StoreQuery = Query; +#[cfg(test)] +mod tests { + use super::*; -//#[cfg(test)] -//mod tests { -// use super::*; -// -// use postgres_es::PostgresCqrs; -// -// use crate::{ -// db::migrate::*, -// billing::{ -// 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, BillingServicesBuilder -// }, -// domain::{ -// add_category_command::AddCategoryCommand, add_customization_command, -// add_product_command::tests::get_command, add_store_command::AddStoreCommand, -// commands::BillingCommand, -// 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 = BillingDBPostgresAdapter::new(db.pool.clone()); -// -// let simple_query = SimpleLoggingQuery {}; -// -// let queries: Vec>> = -// vec![Box::new(simple_query), Box::new(db.clone())]; -// -// let services = BillingServicesBuilder::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>, -// Arc>, -// ) = ( -// 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("", BillingCommand::AddStore(cmd.clone())) -// .await -// .unwrap(); -// -// settings.drop_db().await; -// } -//} + use postgres_es::PostgresCqrs; + + use crate::{ + billing::{ + application::services::{ + add_store_service::AddStoreServiceBuilder, update_store_service::*, + MockBillingServicesInterface, + }, + domain::add_store_command::*, + domain::commands::BillingCommand, + domain::update_store_command::*, + }, + db::migrate::*, + tests::bdd::*, + utils::{random_string::GenerateRandomStringInterface, uuid::tests::UUID}, + }; + use std::sync::Arc; + + #[actix_rt::test] + async fn pg_query_billing_store_view() { + 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 = BillingDBPostgresAdapter::new(db.pool.clone()); + + let simple_query = SimpleLoggingQuery {}; + + let queries: Vec>> = + vec![Box::new(simple_query), Box::new(db.clone())]; + + let mut mock_services = MockBillingServicesInterface::new(); + + let db2 = db.clone(); + mock_services + .expect_add_store() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + AddStoreServiceBuilder::default() + .db_store_id_exists(Arc::new(db2.clone())) + .db_store_name_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let db2 = db.clone(); + mock_services + .expect_update_store() + .times(IS_CALLED_ONLY_ONCE.unwrap()) + .returning(move || { + Arc::new( + UpdateStoreServiceBuilder::default() + .db_store_id_exists(Arc::new(db2.clone())) + .db_store_name_exists(Arc::new(db2.clone())) + .build() + .unwrap(), + ) + }); + + let (cqrs, store_query): ( + Arc>, + Arc>, + ) = ( + Arc::new(postgres_es::postgres_cqrs( + db.pool.clone(), + queries, + Arc::new(mock_services), + )), + Arc::new(db.clone()), + ); + + let rand = crate::utils::random_string::GenerateRandomString {}; + let cmd = AddStoreCommandBuilder::default() + .name(rand.get_random(10)) + .address(None) + .owner(UUID) + .store_id(UUID) + .build() + .unwrap(); + cqrs.execute( + &cmd.store_id().to_string(), + BillingCommand::AddStore(cmd.clone()), + ) + .await + .unwrap(); + + let store = store_query + .load(&(*cmd.store_id()).to_string()) + .await + .unwrap() + .unwrap(); + let store: Store = store.into(); + assert_eq!(store.name(), cmd.name()); + assert_eq!(store.address(), cmd.address()); + assert_eq!(store.owner(), cmd.owner()); + assert_eq!(store.store_id(), cmd.store_id()); + assert!(!store.deleted()); + + let update_store_cmd = UpdateStoreCommand::new( + rand.get_random(10), + Some(rand.get_random(10)), + UUID, + store, + UUID, + ) + .unwrap(); + cqrs.execute( + &cmd.store_id().to_string(), + BillingCommand::UpdateStore(update_store_cmd.clone()), + ) + .await + .unwrap(); + let store = store_query + .load(&(*cmd.store_id()).to_string()) + .await + .unwrap() + .unwrap(); + let store: Store = store.into(); + assert_eq!(store.name(), update_store_cmd.name()); + assert_eq!(store.address(), update_store_cmd.address()); + assert_eq!(store.owner(), update_store_cmd.owner()); + assert_eq!(store.store_id(), update_store_cmd.old_store().store_id()); + assert!(!store.deleted()); + + settings.drop_db().await; + } +} diff --git a/src/billing/application/services/add_bill_service.rs b/src/billing/application/services/add_bill_service.rs index c8f05fe..0ca81ed 100644 --- a/src/billing/application/services/add_bill_service.rs +++ b/src/billing/application/services/add_bill_service.rs @@ -17,7 +17,6 @@ use crate::billing::{ bill_aggregate::*, }, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -31,27 +30,19 @@ pub type AddBillServiceObj = Arc; pub struct AddBillService { db_bill_id_exists: BillIDExistsDBPortObj, db_next_token_id: NextTokenIDDBPortObj, - get_uuid: GetUUIDInterfaceObj, } #[async_trait::async_trait] impl AddBillUseCase for AddBillService { async fn add_bill(&self, cmd: AddBillCommand) -> BillingResult { - let mut bill_id = self.get_uuid.get_uuid(); - - loop { - if self.db_bill_id_exists.bill_id_exists(&bill_id).await? { - bill_id = self.get_uuid.get_uuid(); - continue; - } else { - break; - } + if self.db_bill_id_exists.bill_id_exists(cmd.bill_id()).await? { + return Err(BillingError::DuplicateBillID); } let token_number = self.db_next_token_id.next_token_id(cmd.store_id()).await?; let bill = BillBuilder::default() - .bill_id(bill_id) + .bill_id(*cmd.bill_id()) .token_number(token_number) .created_time(cmd.created_time().clone()) .store_id(*cmd.store_id()) @@ -80,7 +71,7 @@ pub mod tests { let mut m = MockAddBillUseCase::new(); let bill = BillBuilder::default() - .bill_id(UUID) + .bill_id(*cmd.bill_id()) .token_number(1) .total_price(None) .created_time(cmd.created_time().clone()) @@ -113,7 +104,6 @@ pub mod tests { let s = AddBillServiceBuilder::default() .db_bill_id_exists(mock_bill_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) .db_next_token_id(mock_next_token_id_db_port(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); diff --git a/src/billing/application/services/add_line_item_service.rs b/src/billing/application/services/add_line_item_service.rs index 4d3e268..645fee7 100644 --- a/src/billing/application/services/add_line_item_service.rs +++ b/src/billing/application/services/add_line_item_service.rs @@ -15,7 +15,6 @@ use crate::billing::{ application::port::output::db::line_item_id_exists::*, domain::{add_line_item_command::*, line_item_added_event::*, line_item_aggregate::*}, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -29,7 +28,6 @@ pub type AddLineItemServiceObj = Arc; pub struct AddLineItemService { db_line_item_id_exists: LineItemIDExistsDBPortObj, db_bill_id_exists: BillIDExistsDBPortObj, - get_uuid: GetUUIDInterfaceObj, } #[async_trait::async_trait] @@ -39,19 +37,12 @@ impl AddLineItemUseCase for AddLineItemService { return Err(BillingError::BillIDNotFound); } - let mut line_item_id = self.get_uuid.get_uuid(); - - loop { - if self - .db_line_item_id_exists - .line_item_id_exists(&line_item_id) - .await? - { - line_item_id = self.get_uuid.get_uuid(); - continue; - } else { - break; - } + if self + .db_line_item_id_exists + .line_item_id_exists(cmd.line_item_id()) + .await? + { + return Err(BillingError::DuplicateLineItemID); } let line_item = LineItemBuilder::default() @@ -59,7 +50,7 @@ impl AddLineItemUseCase for AddLineItemService { .product_name(cmd.product_name().into()) .product_id(*cmd.product_id()) .bill_id(*cmd.bill_id()) - .line_item_id(line_item_id) + .line_item_id(*cmd.line_item_id()) .quantity(cmd.quantity().clone()) .price_per_unit(cmd.price_per_unit().clone()) .deleted(false) @@ -79,8 +70,7 @@ pub mod tests { use super::*; use crate::billing::domain::line_item_added_event::tests::get_added_line_item_event_from_command; - use crate::utils::uuid::tests::UUID; - use crate::{tests::bdd::*, utils::uuid::tests::mock_get_uuid}; + use crate::tests::bdd::*; pub fn mock_add_line_item_service( times: Option, @@ -107,7 +97,6 @@ pub mod tests { let s = AddLineItemServiceBuilder::default() .db_line_item_id_exists(mock_line_item_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) .db_bill_id_exists(mock_bill_id_exists_db_port_true(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); @@ -129,7 +118,6 @@ pub mod tests { let s = AddLineItemServiceBuilder::default() .db_line_item_id_exists(mock_line_item_id_exists_db_port_false(IS_NEVER_CALLED)) .db_bill_id_exists(mock_bill_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) - .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) .build() .unwrap(); diff --git a/src/billing/application/services/add_store_service.rs b/src/billing/application/services/add_store_service.rs index 678662a..a6d0b89 100644 --- a/src/billing/application/services/add_store_service.rs +++ b/src/billing/application/services/add_store_service.rs @@ -12,12 +12,11 @@ use super::errors::*; use crate::billing::{ application::port::output::db::{store_id_exists::*, store_name_exists::*}, domain::{ - add_store_command::AddStoreCommand, + add_store_command::*, store_added_event::{StoreAddedEvent, StoreAddedEventBuilder}, store_aggregate::*, }, }; -use crate::utils::uuid::*; #[automock] #[async_trait::async_trait] @@ -31,28 +30,24 @@ pub type AddStoreServiceObj = Arc; 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) -> BillingResult { - 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; - } + if self + .db_store_id_exists + .store_id_exists(cmd.store_id()) + .await? + { + return Err(BillingError::DuplicateStoreID); } 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) + .store_id(*cmd.store_id()) .build() .unwrap(); @@ -64,7 +59,7 @@ impl AddStoreUseCase for AddStoreService { .name(store.name().into()) .address(store.address().as_ref().map(|s| s.to_string())) .owner(*cmd.owner()) - .store_id(store_id) + .store_id(*cmd.store_id()) .build() .unwrap()) } @@ -87,7 +82,7 @@ pub mod tests { .name(cmd.name().into()) .address(cmd.address().as_ref().map(|s| s.to_string())) .owner(*cmd.owner()) - .store_id(UUID) + .store_id(*cmd.store_id()) .build() .unwrap(); @@ -108,13 +103,17 @@ pub mod tests { let address = "bar"; let owner = UUID; - // address = None - let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .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(); @@ -122,7 +121,7 @@ pub mod tests { assert_eq!(res.name(), cmd.name()); assert_eq!(res.address(), cmd.address()); assert_eq!(res.owner(), cmd.owner()); - assert_eq!(res.store_id(), &UUID); + assert_eq!(res.store_id(), cmd.store_id()); } #[actix_rt::test] @@ -131,13 +130,17 @@ pub mod tests { let address = "bar"; let owner = UUID; - // address = None - let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .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(); diff --git a/src/billing/application/services/errors.rs b/src/billing/application/services/errors.rs index ea767a5..0f6301d 100644 --- a/src/billing/application/services/errors.rs +++ b/src/billing/application/services/errors.rs @@ -15,6 +15,9 @@ pub enum BillingError { BillIDNotFound, InternalError, DuplicateStoreName, + DuplicateBillID, + DuplicateLineItemID, + DuplicateStoreID, StoreIDNotFound, LineItemIDNotFound, } @@ -22,21 +25,12 @@ pub enum BillingError { impl From for BillingError { fn from(value: BillingDBError) -> Self { match value { - BillingDBError::DuplicateBillID => { - error!("DuplicateBillID"); - Self::InternalError - } + BillingDBError::DuplicateBillID => Self::DuplicateBillID, BillingDBError::DuplicateStoreName => Self::DuplicateStoreName, - BillingDBError::DuplicateStoreID => { - error!("DuplicateStoreID"); - Self::InternalError - } + BillingDBError::DuplicateStoreID => Self::DuplicateStoreID, BillingDBError::StoreIDNotFound => BillingError::StoreIDNotFound, BillingDBError::InternalError => BillingError::InternalError, - BillingDBError::DuplicateLineItemID => { - error!("DuplicateLineItemID"); - Self::InternalError - } + BillingDBError::DuplicateLineItemID => Self::DuplicateLineItemID, BillingDBError::LineItemIDNotFound => BillingError::LineItemIDNotFound, } } diff --git a/src/billing/domain/add_bill_command.rs b/src/billing/domain/add_bill_command.rs index 21aabb5..9beafbd 100644 --- a/src/billing/domain/add_bill_command.rs +++ b/src/billing/domain/add_bill_command.rs @@ -4,14 +4,10 @@ use derive_builder::Builder; use derive_getters::Getters; -use derive_more::{Display, Error}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; -use crate::types::{currency::*, quantity::*}; -use crate::utils::string::empty_string_err; - #[derive( Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, )] @@ -21,6 +17,7 @@ pub struct AddBillCommand { #[builder(default = "OffsetDateTime::now_utc()")] created_time: OffsetDateTime, + bill_id: Uuid, store_id: Uuid, } @@ -41,6 +38,7 @@ mod tests { .adding_by(adding_by) .created_time(datetime!(1970-01-01 0:00 UTC)) .store_id(store_id) + .bill_id(UUID) .build() .unwrap() } @@ -54,6 +52,7 @@ mod tests { let cmd = AddBillCommandBuilder::default() .adding_by(adding_by) .store_id(store_id) + .bill_id(UUID) .build() .unwrap(); diff --git a/src/billing/domain/add_line_item_command.rs b/src/billing/domain/add_line_item_command.rs index 910950c..4623263 100644 --- a/src/billing/domain/add_line_item_command.rs +++ b/src/billing/domain/add_line_item_command.rs @@ -19,55 +19,42 @@ pub enum AddLineItemCommandError { } #[derive( - Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, + Clone, Debug, Builder, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, )] -pub struct UnvalidatedAddLineItemCommand { - adding_by: Uuid, - +#[builder(build_fn(validate = "Self::validate"))] +pub struct AddLineItemCommand { #[builder(default = "OffsetDateTime::now_utc()")] created_time: OffsetDateTime, + #[builder(setter(custom))] product_name: String, product_id: Uuid, bill_id: Uuid, - quantity: Quantity, - price_per_unit: Price, -} - -impl UnvalidatedAddLineItemCommand { - pub fn validate(self) -> Result { - let product_name = empty_string_err( - self.product_name, - AddLineItemCommandError::ProductNameIsEmpty, - )?; - - if self.quantity.is_empty() { - return Err(AddLineItemCommandError::QuantityIsEmpty); - } - - Ok(AddLineItemCommand { - created_time: self.created_time, - product_name, - product_id: self.product_id, - bill_id: self.bill_id, - quantity: self.quantity, - adding_by: self.adding_by, - price_per_unit: self.price_per_unit, - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] -pub struct AddLineItemCommand { - created_time: OffsetDateTime, - product_name: String, - product_id: Uuid, - bill_id: Uuid, + line_item_id: Uuid, quantity: Quantity, price_per_unit: Price, adding_by: Uuid, } +impl AddLineItemCommandBuilder { + pub fn product_name(&mut self, product_name: String) -> &mut Self { + self.product_name = Some(product_name.trim().to_owned()); + self + } + + fn validate(&self) -> Result<(), String> { + let product_name = self.product_name.as_ref().unwrap().trim().to_owned(); + if product_name.is_empty() { + return Err(AddLineItemCommandError::ProductNameIsEmpty.to_string()); + } + + if self.quantity.as_ref().unwrap().is_empty() { + return Err(AddLineItemCommandError::QuantityIsEmpty.to_string()); + } + Ok(()) + } +} + #[cfg(test)] mod tests { use time::macros::datetime; @@ -84,7 +71,7 @@ mod tests { let adding_by = UUID; let quantity = Quantity::get_quantity(); - UnvalidatedAddLineItemCommandBuilder::default() + AddLineItemCommandBuilder::default() .product_name(product_name.into()) .adding_by(adding_by) .created_time(datetime!(1970-01-01 0:00 UTC)) @@ -92,10 +79,9 @@ mod tests { .price_per_unit(Price::default()) .product_id(product_id) .bill_id(bill_id) + .line_item_id(UUID) .build() .unwrap() - .validate() - .unwrap() } } @@ -107,16 +93,15 @@ mod tests { let adding_by = UUID; let quantity = Quantity::get_quantity(); - let cmd = UnvalidatedAddLineItemCommandBuilder::default() + let cmd = AddLineItemCommandBuilder::default() .product_name(product_name.into()) .adding_by(adding_by) .price_per_unit(Price::default()) .quantity(quantity.clone()) .product_id(product_id) .bill_id(bill_id) + .line_item_id(UUID) .build() - .unwrap() - .validate() .unwrap(); assert_eq!(cmd.quantity(), &quantity); @@ -133,19 +118,16 @@ mod tests { let adding_by = UUID; let quantity = Quantity::get_quantity(); - assert_eq!( - UnvalidatedAddLineItemCommandBuilder::default() - .product_name(product_name.into()) - .adding_by(adding_by) - .quantity(quantity.clone()) - .price_per_unit(Price::default()) - .product_id(product_id) - .bill_id(bill_id) - .build() - .unwrap() - .validate(), - Err(AddLineItemCommandError::ProductNameIsEmpty) - ); + assert!(AddLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .price_per_unit(Price::default()) + .product_id(product_id) + .line_item_id(UUID) + .bill_id(bill_id) + .build() + .is_err()); } #[test] @@ -157,18 +139,14 @@ mod tests { // minor = 0; major = 0; let quantity = Quantity::default(); - assert_eq!( - UnvalidatedAddLineItemCommandBuilder::default() - .product_name(product_name.into()) - .adding_by(adding_by) - .quantity(quantity.clone()) - .product_id(product_id) - .bill_id(bill_id) - .price_per_unit(Price::default()) - .build() - .unwrap() - .validate(), - Err(AddLineItemCommandError::QuantityIsEmpty) - ); + assert!(AddLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .product_id(product_id) + .bill_id(bill_id) + .price_per_unit(Price::default()) + .build() + .is_err()); } } diff --git a/src/billing/domain/add_store_command.rs b/src/billing/domain/add_store_command.rs index 3f11af8..44c745c 100644 --- a/src/billing/domain/add_store_command.rs +++ b/src/billing/domain/add_store_command.rs @@ -1,7 +1,91 @@ +//// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +//// +//// 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, +// owner: Uuid, +//} +// +//impl AddStoreCommand { +// pub fn new( +// name: String, +// address: Option, +// owner: Uuid, +// ) -> Result { +// let address: Option = 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) +// ) +// } +//} + // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // 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}; @@ -12,46 +96,52 @@ pub enum AddStoreCommandError { NameIsEmpty, } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +#[derive( + Clone, Builder, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, +)] +#[builder(build_fn(validate = "Self::validate"))] pub struct AddStoreCommand { + #[builder(setter(custom))] name: String, + #[builder(setter(custom))] address: Option, + store_id: Uuid, owner: Uuid, } -impl AddStoreCommand { - pub fn new( - name: String, - address: Option, - owner: Uuid, - ) -> Result { - let address: Option = if let Some(address) = address { +impl AddStoreCommandBuilder { + pub fn address(&mut self, address: Option) -> &mut Self { + self.address = if let Some(address) = address { let address = address.trim(); if address.is_empty() { - None + Some(None) } else { - Some(address.to_owned()) + Some(Some(address.to_owned())) } } else { - None + Some(None) }; + self + } - let name = name.trim().to_owned(); + pub fn name(&mut self, name: String) -> &mut Self { + self.name = Some(name.trim().to_owned()); + self + } + + fn validate(&self) -> Result<(), String> { + let name = self.name.as_ref().unwrap().trim().to_owned(); if name.is_empty() { - return Err(AddStoreCommandError::NameIsEmpty); + return Err(AddStoreCommandError::NameIsEmpty.to_string()); } - - Ok(Self { - name, - address, - owner, - }) + Ok(()) } } #[cfg(test)] mod tests { - use crate::utils::uuid::tests::UUID; + use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; use super::*; @@ -62,21 +152,41 @@ mod tests { let owner = UUID; // address = None - let cmd = AddStoreCommand::new(name.into(), None, owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(None) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); + // let cmd = AddStoreCommand::new(name.into(), None, owner, UUID).unwrap(); assert_eq!(cmd.name(), name); assert_eq!(cmd.address(), &None); assert_eq!(cmd.owner(), &owner); + assert_eq!(*cmd.store_id(), UUID); // address = Some - let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); + // let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner, UUID).unwrap(); assert_eq!(cmd.name(), name); assert_eq!(cmd.address(), &Some(address.to_owned())); assert_eq!(cmd.owner(), &owner); + assert_eq!(*cmd.store_id(), UUID); // AddStoreCommandError::NameIsEmpty - assert_eq!( - AddStoreCommand::new("".into(), Some(address.into()), owner), - Err(AddStoreCommandError::NameIsEmpty) - ) + + assert!(AddStoreCommandBuilder::default() + .name("".into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .is_err()) } } diff --git a/src/billing/domain/line_item_aggregate.rs b/src/billing/domain/line_item_aggregate.rs index b19c9e1..b0dcbb2 100644 --- a/src/billing/domain/line_item_aggregate.rs +++ b/src/billing/domain/line_item_aggregate.rs @@ -85,7 +85,7 @@ pub mod tests { .quantity(cmd.quantity().clone()) .bill_id(*cmd.bill_id()) .price_per_unit(cmd.price_per_unit().clone()) - .line_item_id(UUID) + .line_item_id(*cmd.line_item_id()) .build() .unwrap() } diff --git a/src/billing/domain/store_aggregate.rs b/src/billing/domain/store_aggregate.rs index c73e0cb..376733c 100644 --- a/src/billing/domain/store_aggregate.rs +++ b/src/billing/domain/store_aggregate.rs @@ -113,7 +113,13 @@ mod tests { .unwrap(); let expected = BillingEvent::StoreAdded(expected); - let cmd = AddStoreCommand::new(name.into(), address.clone(), owner).unwrap(); + let cmd = AddStoreCommandBuilder::default() + .name(name.into()) + .address(address.clone()) + .owner(owner) + .store_id(UUID) + .build() + .unwrap(); let mut services = MockBillingServicesInterface::new(); services diff --git a/src/billing/domain/update_bill_command.rs b/src/billing/domain/update_bill_command.rs index 7120853..b896d43 100644 --- a/src/billing/domain/update_bill_command.rs +++ b/src/billing/domain/update_bill_command.rs @@ -21,7 +21,6 @@ pub struct UpdateBillCommand { created_time: OffsetDateTime, store_id: Uuid, - token_number: usize, total_price: Option, old_bill: Bill, @@ -45,7 +44,6 @@ mod tests { .adding_by(adding_by) .store_id(store_id) .total_price(None) - .token_number(1) .old_bill(Bill::get_bill()) .build() .unwrap() @@ -62,7 +60,6 @@ mod tests { .adding_by(adding_by) .store_id(store_id) .total_price(None) - .token_number(1) .old_bill(old_bill.clone()) .build() .unwrap();