From ac1964d21ac1c2314bd4791275c8ea945301e840 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 17 Jul 2024 23:45:07 +0530 Subject: [PATCH] feat: define LineItem with add and update events and commands --- src/ordering/domain/add_line_item_command.rs | 153 ++++++++++++++++ src/ordering/domain/commands.rs | 18 ++ src/ordering/domain/delete_line_item.rs | 37 ++++ src/ordering/domain/events.rs | 34 ++++ src/ordering/domain/kot_aggregate.rs | 26 +++ src/ordering/domain/line_item_added_event.rs | 41 +++++ src/ordering/domain/line_item_aggregate.rs | 51 ++++++ .../domain/line_item_deleted_event.rs | 41 +++++ .../domain/line_item_updated_event.rs | 52 ++++++ src/ordering/domain/mod.rs | 16 ++ .../domain/update_line_item_command.rs | 166 ++++++++++++++++++ src/utils/mod.rs | 1 + src/utils/string.rs | 61 +++++++ 13 files changed, 697 insertions(+) create mode 100644 src/ordering/domain/add_line_item_command.rs create mode 100644 src/ordering/domain/commands.rs create mode 100644 src/ordering/domain/delete_line_item.rs create mode 100644 src/ordering/domain/events.rs create mode 100644 src/ordering/domain/kot_aggregate.rs create mode 100644 src/ordering/domain/line_item_added_event.rs create mode 100644 src/ordering/domain/line_item_aggregate.rs create mode 100644 src/ordering/domain/line_item_deleted_event.rs create mode 100644 src/ordering/domain/line_item_updated_event.rs create mode 100644 src/ordering/domain/update_line_item_command.rs create mode 100644 src/utils/string.rs diff --git a/src/ordering/domain/add_line_item_command.rs b/src/ordering/domain/add_line_item_command.rs new file mode 100644 index 0000000..d059735 --- /dev/null +++ b/src/ordering/domain/add_line_item_command.rs @@ -0,0 +1,153 @@ +// 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}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::types::quantity::*; +use crate::utils::string::empty_string_err; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddLineItemCommandError { + QuantityIsEmpty, + ProductNameIsEmpty, +} + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct UnvalidatedAddLineItemCommand { + adding_by: Uuid, + + #[builder(default = "OffsetDateTime::now_utc()")] + sale_time: OffsetDateTime, + product_name: String, + product_id: Uuid, + quantity: Quantity, +} + +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 { + sale_time: self.sale_time, + product_name, + product_id: self.product_id, + quantity: self.quantity, + adding_by: self.adding_by, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct AddLineItemCommand { + sale_time: OffsetDateTime, + product_name: String, + product_id: Uuid, + quantity: Quantity, + + adding_by: Uuid, +} + +#[cfg(test)] +mod tests { + use crate::utils::uuid::tests::UUID; + + use super::*; + + impl AddLineItemCommand { + pub fn get_cmd() -> Self { + let product_name = "foo"; + let product_id = UUID; + let adding_by = UUID; + let quantity = Quantity::get_quantity(); + + UnvalidatedAddLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .product_id(product_id) + .build() + .unwrap() + .validate() + .unwrap() + } + } + + #[test] + fn test_cmd() { + let product_name = "foo"; + let product_id = UUID; + let adding_by = UUID; + let quantity = Quantity::get_quantity(); + + let cmd = UnvalidatedAddLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .product_id(product_id) + .build() + .unwrap() + .validate() + .unwrap(); + + assert_eq!(cmd.quantity(), &quantity); + assert_eq!(*cmd.product_id(), product_id); + assert_eq!(*cmd.adding_by(), adding_by); + assert_eq!(cmd.product_name(), product_name); + } + + #[test] + fn test_cmd_product_name_empty() { + let product_name = ""; + let product_id = UUID; + 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()) + .product_id(product_id) + .build() + .unwrap() + .validate(), + Err(AddLineItemCommandError::ProductNameIsEmpty) + ); + } + + #[test] + fn test_cmd_quantity_empty() { + let product_name = "foo"; + let product_id = UUID; + let adding_by = UUID; + // 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) + .build() + .unwrap() + .validate(), + Err(AddLineItemCommandError::QuantityIsEmpty) + ); + } +} diff --git a/src/ordering/domain/commands.rs b/src/ordering/domain/commands.rs new file mode 100644 index 0000000..79054d4 --- /dev/null +++ b/src/ordering/domain/commands.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use serde::{Deserialize, Serialize}; + +use super::{ + add_line_item_command::AddLineItemCommand, delete_line_item::DeleteLineItemCommand, + update_line_item_command::UpdateLineItemCommand, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +pub enum OrderingCommand { + AddLineItem(AddLineItemCommand), + UpdateLineItem(UpdateLineItemCommand), + DeleteLineItem(DeleteLineItemCommand), +} diff --git a/src/ordering/domain/delete_line_item.rs b/src/ordering/domain/delete_line_item.rs new file mode 100644 index 0000000..9587e39 --- /dev/null +++ b/src/ordering/domain/delete_line_item.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::line_item_aggregate::LineItem; + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters, +)] +pub struct DeleteLineItemCommand { + adding_by: Uuid, + line_item: LineItem, +} + +#[cfg(test)] +mod tests { + use crate::utils::uuid::tests::UUID; + + use super::*; + + impl DeleteLineItemCommand { + pub fn get_cmd() -> Self { + let adding_by = UUID; + + DeleteLineItemCommandBuilder::default() + .adding_by(adding_by) + .line_item(LineItem::get_line_item()) + .build() + .unwrap() + } + } +} diff --git a/src/ordering/domain/events.rs b/src/ordering/domain/events.rs new file mode 100644 index 0000000..c82ab04 --- /dev/null +++ b/src/ordering/domain/events.rs @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use cqrs_es::DomainEvent; +use serde::{Deserialize, Serialize}; + +use super::{ + line_item_added_event::LineItemAddedEvent, line_item_deleted_event::LineItemDeletedEvent, + line_item_updated_event::LineItemUpdatedEvent, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +pub enum OrderingEvent { + LineItemAdded(LineItemAddedEvent), + LineItemUpdated(LineItemUpdatedEvent), + LineItemDeleted(LineItemDeletedEvent), +} + +impl DomainEvent for OrderingEvent { + fn event_version(&self) -> String { + "1.0".to_string() + } + + fn event_type(&self) -> String { + let e: &str = match self { + OrderingEvent::LineItemAdded { .. } => "OrderingLineItemAdded", + OrderingEvent::LineItemUpdated { .. } => "OrderingLineItemUpdated", + OrderingEvent::LineItemDeleted { .. } => "OrderingLineItemDeleted", + }; + + e.to_string() + } +} diff --git a/src/ordering/domain/kot_aggregate.rs b/src/ordering/domain/kot_aggregate.rs new file mode 100644 index 0000000..4276087 --- /dev/null +++ b/src/ordering/domain/kot_aggregate.rs @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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 time::OffsetDateTime; +use uuid::Uuid; + +use super::line_item_aggregate::*; + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters, +)] +pub struct Kot { + #[builder(default = "OffsetDateTime::now_utc()")] + created_time: OffsetDateTime, + line_items: Vec, + kot_id: Uuid, + order_id: Uuid, + #[builder(default = "false")] + deleted: bool, +} diff --git a/src/ordering/domain/line_item_added_event.rs b/src/ordering/domain/line_item_added_event.rs new file mode 100644 index 0000000..8facc20 --- /dev/null +++ b/src/ordering/domain/line_item_added_event.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::line_item_aggregate::LineItem; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct LineItemAddedEvent { + added_by_user: Uuid, + + line_item: LineItem, +} + +#[cfg(test)] +pub mod tests { + use crate::ordering::domain::add_line_item_command::AddLineItemCommand; + + use super::*; + + pub fn get_added_line_item_event_from_command(cmd: &AddLineItemCommand) -> LineItemAddedEvent { + let line_item = LineItem::get_line_item(); + + LineItemAddedEventBuilder::default() + .added_by_user(cmd.adding_by().clone()) + .line_item(line_item) + .build() + .unwrap() + } + + #[test] + fn test_event() { + get_added_line_item_event_from_command(&AddLineItemCommand::get_cmd()); + } +} diff --git a/src/ordering/domain/line_item_aggregate.rs b/src/ordering/domain/line_item_aggregate.rs new file mode 100644 index 0000000..b0f4fc4 --- /dev/null +++ b/src/ordering/domain/line_item_aggregate.rs @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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 time::OffsetDateTime; +use uuid::Uuid; + +use crate::types::quantity::Quantity; + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters, +)] +pub struct LineItem { + #[builder(default = "OffsetDateTime::now_utc()")] + sale_time: OffsetDateTime, + product_name: String, + product_id: Uuid, + line_item_id: Uuid, + quantity: Quantity, + #[builder(default = "false")] + deleted: bool, +} + +#[cfg(test)] +mod tests { + use crate::{ + ordering::domain::add_line_item_command::AddLineItemCommand, utils::uuid::tests::UUID, + }; + + use super::*; + + impl LineItem { + pub fn get_line_item() -> Self { + let cmd = AddLineItemCommand::get_cmd(); + + LineItemBuilder::default() + .sale_time(cmd.sale_time().clone()) + .product_name("test_product".into()) + .product_id(*cmd.product_id()) + .quantity(cmd.quantity().clone()) + .line_item_id(UUID) + .build() + .unwrap() + } + } +} diff --git a/src/ordering/domain/line_item_deleted_event.rs b/src/ordering/domain/line_item_deleted_event.rs new file mode 100644 index 0000000..3b12d81 --- /dev/null +++ b/src/ordering/domain/line_item_deleted_event.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::line_item_aggregate::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct LineItemDeletedEvent { + added_by_user: Uuid, + + line_item: LineItem, +} + +#[cfg(test)] +pub mod tests { + use crate::ordering::domain::delete_line_item::DeleteLineItemCommand; + + use super::*; + + pub fn get_updated_line_item_event_from_command( + cmd: &DeleteLineItemCommand, + ) -> LineItemDeletedEvent { + LineItemDeletedEventBuilder::default() + .added_by_user(cmd.adding_by().clone()) + .line_item(cmd.line_item().clone()) + .build() + .unwrap() + } + + #[test] + fn test_event() { + get_updated_line_item_event_from_command(&DeleteLineItemCommand::get_cmd()); + } +} diff --git a/src/ordering/domain/line_item_updated_event.rs b/src/ordering/domain/line_item_updated_event.rs new file mode 100644 index 0000000..dca11b9 --- /dev/null +++ b/src/ordering/domain/line_item_updated_event.rs @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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::line_item_aggregate::*; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct LineItemUpdatedEvent { + added_by_user: Uuid, + + new_line_item: LineItem, + old_line_item: LineItem, +} + +#[cfg(test)] +pub mod tests { + use crate::ordering::domain::update_line_item_command::UpdateLineItemCommand; + + use super::*; + + pub fn get_updated_line_item_event_from_command( + cmd: &UpdateLineItemCommand, + ) -> LineItemUpdatedEvent { + let new_line_item = LineItemBuilder::default() + .sale_time(cmd.sale_time().clone()) + .product_name(cmd.product_name().clone()) + .product_id(*cmd.product_id()) + .quantity(cmd.quantity().clone()) + .line_item_id(*cmd.old_line_item().line_item_id()) + .build() + .unwrap(); + + LineItemUpdatedEventBuilder::default() + .added_by_user(cmd.adding_by().clone()) + .old_line_item(cmd.old_line_item().clone()) + .new_line_item(new_line_item) + .build() + .unwrap() + } + + #[test] + fn test_event() { + get_updated_line_item_event_from_command(&UpdateLineItemCommand::get_cmd()); + } +} diff --git a/src/ordering/domain/mod.rs b/src/ordering/domain/mod.rs index 56f60de..2edcc21 100644 --- a/src/ordering/domain/mod.rs +++ b/src/ordering/domain/mod.rs @@ -1,3 +1,19 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later + +// aggregates +pub mod kot_aggregate; +pub mod line_item_aggregate; + +// commands +pub mod add_line_item_command; +pub mod commands; +pub mod delete_line_item; +pub mod update_line_item_command; + +// events +pub mod events; +pub mod line_item_added_event; +pub mod line_item_deleted_event; +pub mod line_item_updated_event; diff --git a/src/ordering/domain/update_line_item_command.rs b/src/ordering/domain/update_line_item_command.rs new file mode 100644 index 0000000..45d00da --- /dev/null +++ b/src/ordering/domain/update_line_item_command.rs @@ -0,0 +1,166 @@ +// 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}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::types::quantity::*; +use crate::utils::string::empty_string_err; + +use super::line_item_aggregate::LineItem; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum UpdateLineItemCommandError { + QuantityIsEmpty, + ProductNameIsEmpty, +} + +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct UnvalidatedUpdateLineItemCommand { + adding_by: Uuid, + + #[builder(default = "OffsetDateTime::now_utc()")] + sale_time: OffsetDateTime, + product_name: String, + product_id: Uuid, + quantity: Quantity, + + old_line_item: LineItem, +} + +impl UnvalidatedUpdateLineItemCommand { + pub fn validate(self) -> Result { + let product_name = empty_string_err( + self.product_name, + UpdateLineItemCommandError::ProductNameIsEmpty, + )?; + + if self.quantity.is_empty() { + return Err(UpdateLineItemCommandError::QuantityIsEmpty); + } + + Ok(UpdateLineItemCommand { + sale_time: self.sale_time, + product_name, + product_id: self.product_id, + quantity: self.quantity, + adding_by: self.adding_by, + old_line_item: self.old_line_item, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct UpdateLineItemCommand { + sale_time: OffsetDateTime, + product_name: String, + product_id: Uuid, + quantity: Quantity, + + old_line_item: LineItem, + + adding_by: Uuid, +} + +#[cfg(test)] +mod tests { + use crate::utils::uuid::tests::UUID; + + use super::*; + + impl UpdateLineItemCommand { + pub fn get_cmd() -> Self { + let product_name = "foo"; + let product_id = UUID; + let adding_by = UUID; + let quantity = Quantity::get_quantity(); + + UnvalidatedUpdateLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .product_id(product_id) + .old_line_item(LineItem::get_line_item()) + .build() + .unwrap() + .validate() + .unwrap() + } + } + + #[test] + fn test_cmd() { + let product_name = "foo"; + let product_id = UUID; + let adding_by = UUID; + let quantity = Quantity::get_quantity(); + let old_line_item = LineItem::get_line_item(); + + let cmd = UnvalidatedUpdateLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .product_id(product_id) + .old_line_item(old_line_item.clone()) + .build() + .unwrap() + .validate() + .unwrap(); + + assert_eq!(cmd.quantity(), &quantity); + assert_eq!(*cmd.product_id(), product_id); + assert_eq!(*cmd.adding_by(), adding_by); + assert_eq!(cmd.product_name(), product_name); + assert_eq!(cmd.old_line_item(), &old_line_item); + } + + #[test] + fn test_cmd_product_name_empty() { + let product_name = ""; + let product_id = UUID; + let adding_by = UUID; + let quantity = Quantity::get_quantity(); + + assert_eq!( + UnvalidatedUpdateLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .product_id(product_id) + .old_line_item(LineItem::get_line_item()) + .build() + .unwrap() + .validate(), + Err(UpdateLineItemCommandError::ProductNameIsEmpty) + ); + } + + #[test] + fn test_cmd_quantity_empty() { + let product_name = "foo"; + let product_id = UUID; + let adding_by = UUID; + // minor = 0; major = 0; + let quantity = Quantity::default(); + + assert_eq!( + UnvalidatedUpdateLineItemCommandBuilder::default() + .product_name(product_name.into()) + .adding_by(adding_by) + .quantity(quantity.clone()) + .product_id(product_id) + .old_line_item(LineItem::get_line_item()) + .build() + .unwrap() + .validate(), + Err(UpdateLineItemCommandError::QuantityIsEmpty) + ); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index fa2f8f5..9bcf0b0 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,4 +4,5 @@ pub mod parse_aggregate_id; pub mod random_string; +pub mod string; pub mod uuid; diff --git a/src/utils/string.rs b/src/utils/string.rs new file mode 100644 index 0000000..efea56f --- /dev/null +++ b/src/utils/string.rs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// if let s: Option = Some("".into()); transform to None; +pub fn clean_option_empty_string(s: Option) -> Option { + if let Some(s) = s { + let s = s.trim(); + if s.is_empty() { + None + } else { + Some(s.to_owned()) + } + } else { + None + } +} + +pub fn empty_string_err(s: String, e: E) -> Result { + let s = s.trim().to_owned(); + if s.is_empty() { + return Err(e); + } + Ok(s) +} + +#[cfg(test)] +mod tests { + use derive_more::{Display, Error}; + + use super::*; + + #[test] + fn test_clean_option_empty_string() { + assert!(clean_option_empty_string(Some("".into())).is_none()); + assert!(clean_option_empty_string(Some("foo".into())).is_some()); + assert_eq!( + clean_option_empty_string(Some("foo".into())).unwrap(), + "foo" + ); + } + + #[test] + fn test_empty_string_err() { + let s: String = "foo".into(); + + #[derive(Display, Debug, Eq, PartialEq, Error)] + enum TestError { + EmptyString, + } + + assert_eq!( + empty_string_err(format!(" {s} "), TestError::EmptyString).unwrap(), + s + ); + assert_eq!( + empty_string_err(format!(" "), TestError::EmptyString), + Err(TestError::EmptyString) + ); + } +}