From 176e9ff6f694d3b6fc3f3899fbe195c5fc681a92 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 9 Jan 2025 01:15:48 +0530 Subject: [PATCH] feat: define Store aggregate, commands and event in Identity domain --- src/identity/application/services/events.rs | 12 +- src/identity/domain/add_store_command.rs | 109 ++++++++++++++ src/identity/domain/mod.rs | 6 +- src/identity/domain/store_added_event.rs | 18 +++ src/identity/domain/store_aggregate.rs | 148 ++++++++++++++++++++ src/identity/domain/store_updated_event.rs | 43 ++++++ src/identity/domain/update_store_command.rs | 127 +++++++++++++++++ 7 files changed, 460 insertions(+), 3 deletions(-) create mode 100644 src/identity/domain/add_store_command.rs create mode 100644 src/identity/domain/store_added_event.rs create mode 100644 src/identity/domain/store_aggregate.rs create mode 100644 src/identity/domain/store_updated_event.rs create mode 100644 src/identity/domain/update_store_command.rs diff --git a/src/identity/application/services/events.rs b/src/identity/application/services/events.rs index fc59564..4961adf 100644 --- a/src/identity/application/services/events.rs +++ b/src/identity/application/services/events.rs @@ -14,8 +14,8 @@ use super::update_password::events::*; use crate::identity::domain::{ employee_logged_in_event::*, employee_registered_event::*, invite_accepted_event::*, login_otp_sent_event::*, organization_exited_event::*, phone_number_changed_event::*, - phone_number_verified_event::*, resend_login_otp_event::*, verification_otp_resent_event::*, - verification_otp_sent_event::*, + phone_number_verified_event::*, resend_login_otp_event::*, store_added_event::*, + store_updated_event::*, verification_otp_resent_event::*, verification_otp_sent_event::*, }; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] @@ -39,6 +39,10 @@ pub enum IdentityEvent { PhoneNumberChanged(PhoneNumberChangedEvent), InviteAccepted(InviteAcceptedEvent), OrganizationExited(OrganizationExitedEvent), + + // store events + StoreAdded(StoreAddedEvent), + StoreUpdated(StoreUpdatedEvent), } //TODO: define password type that takes string and converts to hash @@ -69,6 +73,10 @@ impl DomainEvent for IdentityEvent { IdentityEvent::PhoneNumberChanged { .. } => "EmployeePhoneNumberChanged", IdentityEvent::InviteAccepted { .. } => "EmployeeInviteAccepted", IdentityEvent::OrganizationExited { .. } => "EmployeeOrganizationExited", + + // store + IdentityEvent::StoreAdded { .. } => "IdentityStoreAdded", + IdentityEvent::StoreUpdated { .. } => "IdentityStoreUpdated", }; e.to_string() diff --git a/src/identity/domain/add_store_command.rs b/src/identity/domain/add_store_command.rs new file mode 100644 index 0000000..68cb21e --- /dev/null +++ b/src/identity/domain/add_store_command.rs @@ -0,0 +1,109 @@ +// 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 uuid::Uuid; + +#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddStoreCommandError { + NameIsEmpty, +} + +#[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 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() { + Some(None) + } else { + Some(Some(address.to_owned())) + } + } else { + Some(None) + }; + self + } + + 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.to_string()); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::tests::bdd::*; + use crate::utils::uuid::tests::*; + + use super::*; + + #[test] + fn test_cmd() { + let name = "foo"; + let address = "bar"; + let owner = UUID; + + // address = None + 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 = 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!(AddStoreCommandBuilder::default() + .name("".into()) + .address(Some(address.into())) + .owner(owner) + .store_id(UUID) + .build() + .is_err()) + } +} diff --git a/src/identity/domain/mod.rs b/src/identity/domain/mod.rs index 47da0f3..9acec2f 100644 --- a/src/identity/domain/mod.rs +++ b/src/identity/domain/mod.rs @@ -5,7 +5,7 @@ pub mod aggregate; pub mod employee_aggregate; //pub mod employee_commands; -// pub mod store_aggregate; +pub mod store_aggregate; // pub mod invite; // events @@ -17,15 +17,19 @@ pub mod organization_exited_event; pub mod phone_number_changed_event; pub mod phone_number_verified_event; pub mod resend_login_otp_event; +pub mod store_added_event; +pub mod store_updated_event; pub mod verification_otp_resent_event; pub mod verification_otp_sent_event; // commands pub mod accept_invite_command; +pub mod add_store_command; pub mod change_phone_number_command; pub mod employee_login_command; pub mod employee_register_command; pub mod exit_organization_command; pub mod resend_login_otp_command; pub mod resend_verification_otp_command; +pub mod update_store_command; pub mod verify_phone_number_command; diff --git a/src/identity/domain/store_added_event.rs b/src/identity/domain/store_added_event.rs new file mode 100644 index 0000000..6336da0 --- /dev/null +++ b/src/identity/domain/store_added_event.rs @@ -0,0 +1,18 @@ +// 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; + +#[derive( + Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct StoreAddedEvent { + name: String, + address: Option, + owner: Uuid, + store_id: Uuid, +} diff --git a/src/identity/domain/store_aggregate.rs b/src/identity/domain/store_aggregate.rs new file mode 100644 index 0000000..65a9701 --- /dev/null +++ b/src/identity/domain/store_aggregate.rs @@ -0,0 +1,148 @@ +// 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 uuid::Uuid; + +use crate::identity::application::services::{errors::*, events::*, *}; + +#[derive( + Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters, +)] +pub struct Store { + name: String, + address: Option, + owner: Uuid, + store_id: Uuid, + #[builder(default = "false")] + deleted: bool, +} + +#[async_trait] +impl Aggregate for Store { + type Command = IdentityCommand; + type Event = IdentityEvent; + type Error = IdentityError; + type Services = std::sync::Arc; + + // This identifier should be unique to the system. + fn aggregate_type() -> String { + "billing.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, Self::Error> { + match command { + IdentityCommand::AddStore(cmd) => { + let res = services.add_store().add_store(cmd).await?; + Ok(vec![IdentityEvent::StoreAdded(res)]) + } + IdentityCommand::UpdateStore(cmd) => { + let res = services.update_store().update_store(cmd).await?; + Ok(vec![IdentityEvent::StoreUpdated(res)]) + } + + _ => Ok(Vec::default()), + } + } + + fn apply(&mut self, event: Self::Event) { + match event { + IdentityEvent::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; + } + IdentityEvent::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::billing::{ +// application::services::add_store_service::tests::*, +// domain::{ +// add_store_command::*, commands::IdentityCommand, events::IdentityEvent, +// 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; +// +// #[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 = IdentityEvent::StoreAdded(expected); +// +// let cmd = AddStoreCommandBuilder::default() +// .name(name.into()) +// .address(address.clone()) +// .owner(owner) +// .store_id(UUID) +// .build() +// .unwrap(); +// +// let mut services = MockIdentityServicesInterface::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(IdentityCommand::AddStore(cmd)) +// .then_expect_events(vec![expected]); +// } +// +// #[test] +// fn test_update_store() { +// let cmd = get_update_store_cmd(); +// let expected = IdentityEvent::StoreUpdated(get_store_updated_event_from_command(&cmd)); +// +// let mut services = MockIdentityServicesInterface::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(IdentityCommand::UpdateStore(cmd)) +// .then_expect_events(vec![expected]); +// } +//} diff --git a/src/identity/domain/store_updated_event.rs b/src/identity/domain/store_updated_event.rs new file mode 100644 index 0000000..fb415ce --- /dev/null +++ b/src/identity/domain/store_updated_event.rs @@ -0,0 +1,43 @@ +// 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::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::identity::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() + } +} diff --git a/src/identity/domain/update_store_command.rs b/src/identity/domain/update_store_command.rs new file mode 100644 index 0000000..53c2e2c --- /dev/null +++ b/src/identity/domain/update_store_command.rs @@ -0,0 +1,127 @@ +// 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; + +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, + owner: Uuid, + old_store: Store, + adding_by: Uuid, +} + +impl UpdateStoreCommand { + pub fn new( + name: String, + address: Option, + owner: Uuid, + old_store: Store, + adding_by: 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(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) + ) + } +}