feat: define LineItem with add and update events and commands
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful

This commit is contained in:
Aravinth Manivannan 2024-07-17 23:45:07 +05:30
parent 06c97f57a2
commit ac1964d21a
Signed by: realaravinth
GPG key ID: F8F50389936984FF
13 changed files with 697 additions and 0 deletions

View file

@ -0,0 +1,153 @@
// 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 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<AddLineItemCommand, AddLineItemCommandError> {
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)
);
}
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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),
}

View file

@ -0,0 +1,37 @@
// 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::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()
}
}
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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()
}
}

View file

@ -0,0 +1,26 @@
// 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 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<LineItem>,
kot_id: Uuid,
order_id: Uuid,
#[builder(default = "false")]
deleted: bool,
}

View file

@ -0,0 +1,41 @@
// 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::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());
}
}

View file

@ -0,0 +1,51 @@
// 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 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()
}
}
}

View file

@ -0,0 +1,41 @@
// 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::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());
}
}

View file

@ -0,0 +1,52 @@
// 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::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());
}
}

View file

@ -1,3 +1,19 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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;

View file

@ -0,0 +1,166 @@
// 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 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<UpdateLineItemCommand, UpdateLineItemCommandError> {
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)
);
}
}

View file

@ -4,4 +4,5 @@
pub mod parse_aggregate_id;
pub mod random_string;
pub mod string;
pub mod uuid;

61
src/utils/string.rs Normal file
View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// if let s: Option<String> = Some("".into()); transform to None;
pub fn clean_option_empty_string(s: Option<String>) -> Option<String> {
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<E: std::error::Error>(s: String, e: E) -> Result<String, E> {
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)
);
}
}