Add service for sales_persond

This commit is contained in:
Simon Goller 2024-05-06 13:33:54 +02:00
parent 20828fb4a1
commit ad88a1c983
9 changed files with 792 additions and 2 deletions

View file

@ -5,6 +5,7 @@ use mockall::automock;
use thiserror::Error; use thiserror::Error;
pub mod permission; pub mod permission;
pub mod sales_person;
pub mod slot; pub mod slot;
pub use permission::MockPermissionDao; pub use permission::MockPermissionDao;

25
dao/src/sales_person.rs Normal file
View file

@ -0,0 +1,25 @@
use std::sync::Arc;
use async_trait::async_trait;
use mockall::automock;
use uuid::Uuid;
use crate::DaoError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SalesPersonEntity {
pub id: Uuid,
pub name: Arc<str>,
pub deleted: Option<time::PrimitiveDateTime>,
pub inactive: bool,
pub version: Uuid,
}
#[automock]
#[async_trait]
pub trait SalesPersonDao {
async fn all(&self) -> Result<Arc<[SalesPersonEntity]>, DaoError>;
async fn find_by_id(&self, id: Uuid) -> Result<Option<SalesPersonEntity>, DaoError>;
async fn create(&self, entity: &SalesPersonEntity, process: &str) -> Result<(), DaoError>;
async fn update(&self, entity: &SalesPersonEntity, process: &str) -> Result<(), DaoError>;
}

View file

@ -6,6 +6,7 @@ use uuid::Uuid;
pub mod clock; pub mod clock;
pub mod permission; pub mod permission;
pub mod sales_person;
pub mod slot; pub mod slot;
pub mod user_service; pub mod user_service;
pub mod uuid_service; pub mod uuid_service;

View file

@ -0,0 +1,58 @@
use std::sync::Arc;
use async_trait::async_trait;
use mockall::automock;
use uuid::Uuid;
use crate::ServiceError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SalesPerson {
pub id: Uuid,
pub name: Arc<str>,
pub inactive: bool,
pub deleted: Option<time::PrimitiveDateTime>,
pub version: Uuid,
}
impl From<&dao::sales_person::SalesPersonEntity> for SalesPerson {
fn from(sales_person: &dao::sales_person::SalesPersonEntity) -> Self {
Self {
id: sales_person.id,
name: sales_person.name.clone(),
inactive: sales_person.inactive,
deleted: sales_person.deleted,
version: sales_person.version,
}
}
}
impl From<&SalesPerson> for dao::sales_person::SalesPersonEntity {
fn from(sales_person: &SalesPerson) -> Self {
Self {
id: sales_person.id,
name: sales_person.name.clone(),
inactive: sales_person.inactive,
deleted: sales_person.deleted,
version: sales_person.version,
}
}
}
#[automock(type Context=();)]
#[async_trait]
pub trait SalesPersonService {
type Context: Clone + Send + Sync + 'static;
async fn get_all(&self, context: Self::Context) -> Result<Arc<[SalesPerson]>, ServiceError>;
async fn get(&self, id: Uuid, context: Self::Context) -> Result<SalesPerson, ServiceError>;
async fn create(
&self,
item: &SalesPerson,
context: Self::Context,
) -> Result<SalesPerson, ServiceError>;
async fn update(
&self,
item: &SalesPerson,
context: Self::Context,
) -> Result<SalesPerson, ServiceError>;
async fn delete(&self, id: Uuid, context: Self::Context) -> Result<(), ServiceError>;
}

View file

@ -4,6 +4,7 @@ use async_trait::async_trait;
pub mod clock; pub mod clock;
pub mod permission; pub mod permission;
pub mod sales_person;
pub mod slot; pub mod slot;
mod test; mod test;
pub mod uuid_service; pub mod uuid_service;

View file

@ -0,0 +1,183 @@
use std::sync::Arc;
use async_trait::async_trait;
use dao::sales_person::SalesPersonEntity;
use service::{sales_person::SalesPerson, ServiceError, ValidationFailureItem};
use uuid::Uuid;
pub struct SalesPersonServiceImpl<SalesPersonDao, PermissionService, ClockService, UuidService>
where
SalesPersonDao: dao::sales_person::SalesPersonDao + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
{
pub sales_person_dao: Arc<SalesPersonDao>,
pub permission_service: Arc<PermissionService>,
pub clock_service: Arc<ClockService>,
pub uuid_service: Arc<UuidService>,
}
impl<SalesPersonDao, PermissionService, ClockService, UuidService>
SalesPersonServiceImpl<SalesPersonDao, PermissionService, ClockService, UuidService>
where
SalesPersonDao: dao::sales_person::SalesPersonDao + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
{
pub fn new(
sales_person_dao: Arc<SalesPersonDao>,
permission_service: Arc<PermissionService>,
clock_service: Arc<ClockService>,
uuid_service: Arc<UuidService>,
) -> Self {
Self {
sales_person_dao,
permission_service,
clock_service,
uuid_service,
}
}
}
const SALES_PERSON_SERVICE_PROCESS: &str = "sales-person-service";
#[async_trait]
impl<SalesPersonDao, PermissionService, ClockService, UuidService>
service::sales_person::SalesPersonService
for SalesPersonServiceImpl<SalesPersonDao, PermissionService, ClockService, UuidService>
where
SalesPersonDao: dao::sales_person::SalesPersonDao + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
{
type Context = PermissionService::Context;
async fn get_all(
&self,
context: Self::Context,
) -> Result<Arc<[service::sales_person::SalesPerson]>, service::ServiceError> {
self.permission_service
.check_permission("hr", context)
.await?;
Ok(self
.sales_person_dao
.all()
.await?
.iter()
.map(SalesPerson::from)
.collect())
}
async fn get(
&self,
id: Uuid,
context: Self::Context,
) -> Result<service::sales_person::SalesPerson, service::ServiceError> {
self.permission_service
.check_permission("hr", context)
.await?;
self.sales_person_dao
.find_by_id(id)
.await?
.as_ref()
.map(SalesPerson::from)
.ok_or(ServiceError::EntityNotFound(id))
}
async fn create(
&self,
sales_person: &SalesPerson,
context: Self::Context,
) -> Result<SalesPerson, service::ServiceError> {
self.permission_service
.check_permission("hr", context)
.await?;
if sales_person.id != Uuid::nil() {
return Err(ServiceError::IdSetOnCreate);
}
if sales_person.version != Uuid::nil() {
return Err(ServiceError::VersionSetOnCreate);
}
let sales_person = SalesPerson {
id: self.uuid_service.new_uuid("sales-person-id"),
version: self.uuid_service.new_uuid("sales-person-version"),
..sales_person.clone()
};
self.sales_person_dao
.create(
&SalesPersonEntity::from(&sales_person),
SALES_PERSON_SERVICE_PROCESS,
)
.await?;
Ok(sales_person)
}
async fn update(
&self,
sales_person: &SalesPerson,
context: Self::Context,
) -> Result<SalesPerson, ServiceError> {
self.permission_service
.check_permission("hr", context)
.await?;
let sales_person_entity = self
.sales_person_dao
.find_by_id(sales_person.id)
.await?
.as_ref()
.map(SalesPerson::from)
.ok_or_else(move || ServiceError::EntityNotFound(sales_person.id))?;
if sales_person.version != sales_person_entity.version {
return Err(ServiceError::EntityConflicts(
sales_person.id,
sales_person_entity.version,
sales_person.version,
));
}
if sales_person.deleted != sales_person_entity.deleted {
return Err(ServiceError::ValidationError(
[ValidationFailureItem::ModificationNotAllowed(
"deleted".into(),
)]
.into(),
));
}
let sales_person = SalesPerson {
version: self.uuid_service.new_uuid("sales-person-version"),
..sales_person.clone()
};
self.sales_person_dao
.update(
&SalesPersonEntity::from(&sales_person),
SALES_PERSON_SERVICE_PROCESS,
)
.await?;
Ok(sales_person)
}
async fn delete(&self, id: Uuid, context: Self::Context) -> Result<(), ServiceError> {
self.permission_service
.check_permission("hr", context)
.await?;
let mut sales_person_entity = self
.sales_person_dao
.find_by_id(id)
.await?
.ok_or(ServiceError::EntityNotFound(id))?;
sales_person_entity.deleted = Some(self.clock_service.date_time_now());
sales_person_entity.version = self.uuid_service.new_uuid("sales-person-version");
self.sales_person_dao
.update(&sales_person_entity, SALES_PERSON_SERVICE_PROCESS)
.await?;
Ok(())
}
}

View file

@ -89,8 +89,8 @@ pub fn test_conflicts<T>(
} }
} }
pub fn test_validation_error( pub fn test_validation_error<T>(
result: &Result<(), service::ServiceError>, result: &Result<T, service::ServiceError>,
validation_failure: &ValidationFailureItem, validation_failure: &ValidationFailureItem,
fail_count: usize, fail_count: usize,
) { ) {

View file

@ -3,4 +3,6 @@ pub mod error_test;
#[cfg(test)] #[cfg(test)]
mod permission_test; mod permission_test;
#[cfg(test)] #[cfg(test)]
pub mod sales_person;
#[cfg(test)]
pub mod slot; pub mod slot;

View file

@ -0,0 +1,519 @@
use super::error_test::*;
use dao::sales_person::{MockSalesPersonDao, SalesPersonEntity};
use mockall::predicate::eq;
use service::{
clock::MockClockService,
sales_person::{SalesPerson, SalesPersonService},
uuid_service::MockUuidService,
MockPermissionService,
};
use time::{Date, Month, PrimitiveDateTime, Time};
use tokio;
use uuid::{uuid, Uuid};
use crate::sales_person::SalesPersonServiceImpl;
pub struct SalesPersonServiceDependencies {
pub sales_person_dao: MockSalesPersonDao,
pub permission_service: MockPermissionService,
pub clock_service: MockClockService,
pub uuid_service: MockUuidService,
}
impl SalesPersonServiceDependencies {
pub fn build_service(
self,
) -> SalesPersonServiceImpl<
MockSalesPersonDao,
MockPermissionService,
MockClockService,
MockUuidService,
> {
SalesPersonServiceImpl::new(
self.sales_person_dao.into(),
self.permission_service.into(),
self.clock_service.into(),
self.uuid_service.into(),
)
}
}
pub fn build_dependencies(permission: bool, role: &'static str) -> SalesPersonServiceDependencies {
let sales_person_dao = MockSalesPersonDao::new();
let mut permission_service = MockPermissionService::new();
permission_service
.expect_check_permission()
.with(eq(role), eq(()))
.returning(move |_, _| {
if permission {
Ok(())
} else {
Err(service::ServiceError::Forbidden)
}
});
permission_service
.expect_check_permission()
.returning(move |_, _| Err(service::ServiceError::Forbidden));
let mut clock_service = MockClockService::new();
clock_service
.expect_time_now()
.returning(|| time::Time::from_hms(23, 42, 0).unwrap());
clock_service
.expect_date_now()
.returning(|| time::Date::from_calendar_date(2063, 4.try_into().unwrap(), 5).unwrap());
clock_service.expect_date_time_now().returning(|| {
time::PrimitiveDateTime::new(
time::Date::from_calendar_date(2063, 4.try_into().unwrap(), 5).unwrap(),
time::Time::from_hms(23, 42, 0).unwrap(),
)
});
let uuid_service = MockUuidService::new();
SalesPersonServiceDependencies {
sales_person_dao,
permission_service,
clock_service,
uuid_service,
}
}
pub fn default_id() -> uuid::Uuid {
uuid!("67D91F86-2EC7-4FA6-8EB4-9C76A2D4C6E0")
}
pub fn alternate_id() -> uuid::Uuid {
uuid!("67D91F86-2EC7-4FA6-8EB4-9C76A2D4C6E1")
}
pub fn default_version() -> uuid::Uuid {
uuid!("CCB5F4E2-8C7D-4388-AC4E-641D43ADF580")
}
pub fn alternate_version() -> uuid::Uuid {
uuid!("CCB5F4E2-8C7D-4388-AC4E-641D43ADF581")
}
pub fn default_sales_person_entity() -> dao::sales_person::SalesPersonEntity {
dao::sales_person::SalesPersonEntity {
id: default_id(),
name: "John Doe".into(),
deleted: None,
inactive: false,
version: default_version(),
}
}
pub fn default_sales_person() -> service::sales_person::SalesPerson {
service::sales_person::SalesPerson {
id: default_id(),
name: "John Doe".into(),
inactive: false,
deleted: None,
version: default_version(),
}
}
#[tokio::test]
async fn test_get_all() {
let mut dependencies = build_dependencies(true, "hr");
dependencies.sales_person_dao.expect_all().returning(|| {
Ok([
default_sales_person_entity(),
SalesPersonEntity {
id: alternate_id(),
name: "Jane Doe".into(),
..default_sales_person_entity()
},
]
.into())
});
let sales_person_service = dependencies.build_service();
let result = sales_person_service.get_all(()).await.unwrap();
assert_eq!(2, result.len());
assert_eq!(default_sales_person(), result[0]);
assert_eq!(
service::sales_person::SalesPerson {
id: alternate_id(),
name: "Jane Doe".into(),
..default_sales_person()
},
result[1]
);
}
#[tokio::test]
async fn test_get_all_no_permission() {
let dependencies = build_dependencies(false, "hr");
let sales_person_service = dependencies.build_service();
let result = sales_person_service.get_all(()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_get() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.times(1)
.returning(|_| Ok(Some(default_sales_person_entity())));
let sales_person_service = dependencies.build_service();
let result = sales_person_service.get(default_id(), ()).await;
assert_eq!(default_sales_person(), result.unwrap());
}
#[tokio::test]
async fn test_get_no_permission() {
let dependencies = build_dependencies(false, "hr");
let sales_person_service = dependencies.build_service();
let result = sales_person_service.get(default_id(), ()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_get_not_found() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.times(1)
.returning(|_| Ok(None));
let sales_person_service = dependencies.build_service();
let result = sales_person_service.get(default_id(), ()).await;
test_not_found(&result, &default_id());
}
#[tokio::test]
async fn test_create() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_create()
.with(
eq(default_sales_person_entity()),
eq("sales-person-service"),
)
.times(1)
.returning(|_, _| Ok(()));
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("sales-person-id"))
.times(1)
.returning(|_| default_id());
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("sales-person-version"))
.times(1)
.returning(|_| default_version());
let sales_person_service = dependencies.build_service();
let result = sales_person_service
.create(
&SalesPerson {
id: Uuid::nil(),
version: Uuid::nil(),
..default_sales_person()
},
(),
)
.await
.unwrap();
assert_eq!(result, default_sales_person());
}
#[tokio::test]
async fn test_create_no_permission() {
let dependencies = build_dependencies(false, "hr");
let sales_person_service = dependencies.build_service();
let result = sales_person_service
.create(
&SalesPerson {
id: Uuid::nil(),
version: Uuid::nil(),
..default_sales_person()
},
(),
)
.await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_create_validation() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("sales-person-id"))
.returning(|_| default_id());
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("sales-person-version"))
.returning(|_| default_version());
let sales_person_service = dependencies.build_service();
let result = sales_person_service
.create(
&SalesPerson {
version: Uuid::nil(),
..default_sales_person()
},
(),
)
.await;
test_zero_id_error(&result);
let result = sales_person_service
.create(
&SalesPerson {
id: Uuid::nil(),
..default_sales_person()
},
(),
)
.await;
test_zero_version_error(&result);
}
#[tokio::test]
async fn test_update_no_permission() {
let dependencies = build_dependencies(false, "hr");
let sales_person_service = dependencies.build_service();
let result = sales_person_service
.update(
&SalesPerson {
name: "Jane Doe".into(),
..default_sales_person()
},
(),
)
.await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_update_not_found() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(None));
let sales_person_service = dependencies.build_service();
let result = sales_person_service
.update(
&SalesPerson {
name: "Jane Doe".into(),
..default_sales_person()
},
(),
)
.await;
test_not_found(&result, &default_id());
}
#[tokio::test]
async fn test_update_conflicts() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(Some(default_sales_person_entity())));
let sales_person_service = dependencies.build_service();
let result = sales_person_service
.update(
&SalesPerson {
version: alternate_version(),
..default_sales_person()
},
(),
)
.await;
test_conflicts(
&result,
&default_id(),
&default_version(),
&alternate_version(),
);
}
#[tokio::test]
async fn test_update_deleted_no_allowed() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(Some(default_sales_person_entity())));
let sales_person_service = dependencies.build_service();
let result = sales_person_service
.update(
&SalesPerson {
deleted: Some(PrimitiveDateTime::new(
Date::from_calendar_date(2000, Month::January, 1).unwrap(),
Time::from_hms(1, 0, 0).unwrap(),
)),
..default_sales_person()
},
(),
)
.await;
test_validation_error(
&result,
&service::ValidationFailureItem::ModificationNotAllowed("deleted".into()),
1,
);
}
#[tokio::test]
async fn test_update_inactive() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(Some(default_sales_person_entity())));
dependencies
.sales_person_dao
.expect_update()
.with(
eq(SalesPersonEntity {
inactive: true,
version: alternate_version(),
..default_sales_person_entity()
}),
eq("sales-person-service"),
)
.returning(|_, _| Ok(()));
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("sales-person-version"))
.returning(|_| alternate_version());
let sales_person_service = dependencies.build_service();
let result = sales_person_service
.update(
&SalesPerson {
inactive: true,
..default_sales_person()
},
(),
)
.await
.unwrap();
assert_eq!(
result,
SalesPerson {
inactive: true,
version: alternate_version(),
..default_sales_person()
}
);
}
#[tokio::test]
async fn test_update_name() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(Some(default_sales_person_entity())));
dependencies
.sales_person_dao
.expect_update()
.with(
eq(SalesPersonEntity {
name: "Jane Doe".into(),
version: alternate_version(),
..default_sales_person_entity()
}),
eq("sales-person-service"),
)
.returning(|_, _| Ok(()));
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("sales-person-version"))
.returning(|_| alternate_version());
let sales_person_service = dependencies.build_service();
let result = sales_person_service
.update(
&SalesPerson {
name: "Jane Doe".into(),
..default_sales_person()
},
(),
)
.await
.unwrap();
assert_eq!(
result,
SalesPerson {
name: "Jane Doe".into(),
version: alternate_version(),
..default_sales_person()
}
);
}
#[tokio::test]
async fn test_delete() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(Some(default_sales_person_entity())));
dependencies
.sales_person_dao
.expect_update()
.with(
eq(SalesPersonEntity {
deleted: Some(PrimitiveDateTime::new(
Date::from_calendar_date(2063, Month::April, 5).unwrap(),
Time::from_hms(23, 42, 0).unwrap(),
)),
version: alternate_version(),
..default_sales_person_entity()
}),
eq("sales-person-service"),
)
.returning(|_, _| Ok(()));
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("sales-person-version"))
.returning(|_| alternate_version());
let sales_person_service = dependencies.build_service();
let result = sales_person_service.delete(default_id(), ()).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_delete_no_permission() {
let mut dependencies = build_dependencies(false, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(Some(default_sales_person_entity())));
let sales_person_service = dependencies.build_service();
let result = sales_person_service.delete(default_id(), ()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_delete_not_found() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.sales_person_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(None));
let sales_person_service = dependencies.build_service();
let result = sales_person_service.delete(default_id(), ()).await;
test_not_found(&result, &default_id());
}