feat: define Store aggregate, commands and event in Identity domain
This commit is contained in:
parent
3026294832
commit
176e9ff6f6
7 changed files with 460 additions and 3 deletions
|
@ -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()
|
||||
|
|
109
src/identity/domain/add_store_command.rs
Normal file
109
src/identity/domain/add_store_command.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
18
src/identity/domain/store_added_event.rs
Normal file
18
src/identity/domain/store_added_event.rs
Normal 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,
|
||||
}
|
148
src/identity/domain/store_aggregate.rs
Normal file
148
src/identity/domain/store_aggregate.rs
Normal 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]);
|
||||
// }
|
||||
//}
|
43
src/identity/domain/store_updated_event.rs
Normal file
43
src/identity/domain/store_updated_event.rs
Normal 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()
|
||||
}
|
||||
}
|
127
src/identity/domain/update_store_command.rs
Normal file
127
src/identity/domain/update_store_command.rs
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue