feat: define Store aggregate, commands and event in Identity domain

This commit is contained in:
Aravinth Manivannan 2025-01-09 01:15:48 +05:30
parent 3026294832
commit 176e9ff6f6
Signed by: realaravinth
GPG key ID: F8F50389936984FF
7 changed files with 460 additions and 3 deletions

View file

@ -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()

View file

@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use derive_more::{Display, Error};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum 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<String>,
store_id: Uuid,
owner: Uuid,
}
impl AddStoreCommandBuilder {
pub fn address(&mut self, address: Option<String>) -> &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())
}
}

View file

@ -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;

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct StoreAddedEvent {
name: String,
address: Option<String>,
owner: Uuid,
store_id: Uuid,
}

View file

@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use async_trait::async_trait;
use cqrs_es::Aggregate;
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::identity::application::services::{errors::*, events::*, *};
#[derive(
Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Builder, Getters,
)]
pub struct Store {
name: String,
address: Option<String>,
owner: Uuid,
store_id: Uuid,
#[builder(default = "false")]
deleted: bool,
}
#[async_trait]
impl Aggregate for Store {
type Command = IdentityCommand;
type Event = IdentityEvent;
type Error = IdentityError;
type Services = std::sync::Arc<dyn IdentityServicesInterface>;
// 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<Vec<Self::Event>, 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<Store>;
//
// #[test]
// fn test_create_store() {
// let name = "store_name";
// let address = Some("store_address".to_string());
// let owner = UUID;
// let store_id = UUID;
//
// let expected = StoreAddedEventBuilder::default()
// .name(name.into())
// .address(address.clone())
// .store_id(store_id)
// .owner(owner)
// .build()
// .unwrap();
// let expected = 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]);
// }
//}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::store_aggregate::*;
#[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct StoreUpdatedEvent {
added_by_user: Uuid,
old_store: Store,
new_store: Store,
}
#[cfg(test)]
pub mod tests {
use crate::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()
}
}

View file

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