diff --git a/dao/src/booking.rs b/dao/src/booking.rs new file mode 100644 index 0000000..eea55de --- /dev/null +++ b/dao/src/booking.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use mockall::automock; +use time::PrimitiveDateTime; +use uuid::Uuid; + +use crate::DaoError; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BookingEntity { + pub id: Uuid, + pub sales_person_id: Uuid, + pub slot_id: Uuid, + pub calendar_week: i32, + pub year: u32, + pub deleted: Option, + pub version: Uuid, +} + +#[automock] +#[async_trait] +pub trait BookingDao { + async fn all(&self) -> Result, DaoError>; + async fn find_by_id(&self, id: Uuid) -> Result, DaoError>; + async fn create(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError>; + async fn update(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError>; +} diff --git a/dao/src/lib.rs b/dao/src/lib.rs index 6a77f47..85b7077 100644 --- a/dao/src/lib.rs +++ b/dao/src/lib.rs @@ -4,6 +4,7 @@ use async_trait::async_trait; use mockall::automock; use thiserror::Error; +pub mod booking; pub mod permission; pub mod sales_person; pub mod slot; diff --git a/service/src/booking.rs b/service/src/booking.rs new file mode 100644 index 0000000..7f403fe --- /dev/null +++ b/service/src/booking.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use time::PrimitiveDateTime; +use uuid::Uuid; + +use crate::ServiceError; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Booking { + pub id: Uuid, + pub sales_person_id: Uuid, + pub slot_id: Uuid, + pub calendar_week: i32, + pub year: u32, + pub deleted: Option, + pub version: Uuid, +} + +impl From<&dao::booking::BookingEntity> for Booking { + fn from(booking: &dao::booking::BookingEntity) -> Self { + Self { + id: booking.id, + sales_person_id: booking.sales_person_id, + slot_id: booking.slot_id, + calendar_week: booking.calendar_week, + year: booking.year, + deleted: booking.deleted, + version: booking.version, + } + } +} + +impl From<&Booking> for dao::booking::BookingEntity { + fn from(booking: &Booking) -> Self { + Self { + id: booking.id, + sales_person_id: booking.sales_person_id, + slot_id: booking.slot_id, + calendar_week: booking.calendar_week, + year: booking.year, + deleted: booking.deleted, + version: booking.version, + } + } +} + +#[async_trait] +pub trait BookingService { + type Context: Clone + Send + Sync; + + async fn get_all(&self, context: Self::Context) -> Result, ServiceError>; + async fn get(&self, id: Uuid, context: Self::Context) -> Result; + async fn create( + &self, + booking: &Booking, + context: Self::Context, + ) -> Result; + async fn delete(&self, id: Uuid, context: Self::Context) -> Result<(), ServiceError>; +} diff --git a/service/src/lib.rs b/service/src/lib.rs index 896369d..c22ea66 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -4,6 +4,7 @@ use time::Date; use time::Time; use uuid::Uuid; +pub mod booking; pub mod clock; pub mod permission; pub mod sales_person; diff --git a/service_impl/src/booking.rs b/service_impl/src/booking.rs new file mode 100644 index 0000000..c55bd29 --- /dev/null +++ b/service_impl/src/booking.rs @@ -0,0 +1,131 @@ +use async_trait::async_trait; +use service::{ + booking::{Booking, BookingService}, + ServiceError, +}; +use std::sync::Arc; +use uuid::Uuid; + +const BOOKING_SERVICE_PROCESS: &str = "booking-service"; + +pub struct BookingServiceImpl +where + BookingDao: dao::booking::BookingDao + Send + Sync, + PermissionService: service::permission::PermissionService + Send + Sync, + UuidService: service::uuid_service::UuidService + Send + Sync, + ClockService: service::clock::ClockService + Send + Sync, +{ + pub booking_dao: Arc, + pub permission_service: Arc, + pub clock_service: Arc, + pub uuid_service: Arc, +} +impl + BookingServiceImpl +where + BookingDao: dao::booking::BookingDao + Send + Sync, + PermissionService: service::permission::PermissionService + Send + Sync, + UuidService: service::uuid_service::UuidService + Send + Sync, + ClockService: service::clock::ClockService + Send + Sync, +{ + pub fn new( + booking_dao: Arc, + permission_service: Arc, + clock_service: Arc, + uuid_service: Arc, + ) -> Self { + Self { + booking_dao, + permission_service, + clock_service, + uuid_service, + } + } +} + +#[async_trait] +impl BookingService + for BookingServiceImpl +where + BookingDao: dao::booking::BookingDao + Send + Sync, + PermissionService: service::permission::PermissionService + Send + Sync, + UuidService: service::uuid_service::UuidService + Send + Sync, + ClockService: service::clock::ClockService + Send + Sync, +{ + type Context = PermissionService::Context; + + async fn get_all(&self, context: Self::Context) -> Result, ServiceError> { + self.permission_service + .check_permission("hr", context) + .await?; + Ok(self + .booking_dao + .all() + .await? + .iter() + .map(Booking::from) + .collect()) + } + + async fn get(&self, id: Uuid, context: Self::Context) -> Result { + self.permission_service + .check_permission("hr", context) + .await?; + let booking_entity = self.booking_dao.find_by_id(id).await?; + let booking = booking_entity + .as_ref() + .map(Booking::from) + .ok_or_else(move || ServiceError::EntityNotFound(id))?; + Ok(booking) + } + + async fn create( + &self, + booking: &Booking, + context: Self::Context, + ) -> Result { + self.permission_service + .check_permission("hr", context) + .await?; + + if booking.id != Uuid::nil() { + return Err(ServiceError::IdSetOnCreate); + } + if booking.version != Uuid::nil() { + return Err(ServiceError::VersionSetOnCreate); + } + + let new_id = self.uuid_service.new_uuid("booking-id"); + let new_version = self.uuid_service.new_uuid("booking-version"); + let new_booking = Booking { + id: new_id, + version: new_version, + ..booking.clone() + }; + + self.booking_dao + .create(&(&new_booking).into(), BOOKING_SERVICE_PROCESS) + .await?; + + Ok(new_booking) + } + + async fn delete(&self, id: Uuid, context: Self::Context) -> Result<(), ServiceError> { + self.permission_service + .check_permission("hr", context) + .await?; + + let mut booking_entity = self + .booking_dao + .find_by_id(id) + .await? + .ok_or_else(move || ServiceError::EntityNotFound(id))?; + + booking_entity.deleted = Some(self.clock_service.date_time_now()); + booking_entity.version = self.uuid_service.new_uuid("booking-version"); + self.booking_dao + .update(&booking_entity, BOOKING_SERVICE_PROCESS) + .await?; + Ok(()) + } +} diff --git a/service_impl/src/lib.rs b/service_impl/src/lib.rs index 77eac20..2c031a7 100644 --- a/service_impl/src/lib.rs +++ b/service_impl/src/lib.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use async_trait::async_trait; +pub mod booking; pub mod clock; pub mod permission; pub mod sales_person; diff --git a/service_impl/src/test/booking.rs b/service_impl/src/test/booking.rs new file mode 100644 index 0000000..461b70d --- /dev/null +++ b/service_impl/src/test/booking.rs @@ -0,0 +1,312 @@ +use crate::test::error_test::*; +use dao::booking::{BookingEntity, MockBookingDao}; +use mockall::predicate::eq; +use service::{ + booking::Booking, clock::MockClockService, uuid_service::MockUuidService, MockPermissionService, +}; +use time::{Date, Month, PrimitiveDateTime, Time}; +use uuid::{uuid, Uuid}; + +use crate::booking::BookingServiceImpl; +use service::booking::BookingService; + +pub fn default_id() -> Uuid { + uuid!("CEA260A0-112B-4970-936C-F7E529955BD0") +} +pub fn alternate_id() -> Uuid { + uuid!("CEA260A0-112B-4970-936C-F7E529955BD1") +} +pub fn default_version() -> Uuid { + uuid!("F79C462A-8D4E-42E1-8171-DB4DBD019E50") +} +pub fn alternate_version() -> Uuid { + uuid!("F79C462A-8D4E-42E1-8171-DB4DBD019E51") +} +pub fn default_sales_person_id() -> Uuid { + uuid!("04215DFE-13C4-413C-8C66-77AC741BB5F0") +} +pub fn default_slot_id() -> Uuid { + uuid!("7A7FF57A-782B-4C2E-A68B-4E2D81D79380") +} + +pub fn default_booking() -> Booking { + Booking { + id: default_id(), + sales_person_id: default_sales_person_id(), + slot_id: default_slot_id(), + calendar_week: 3, + year: 2024, + deleted: None, + version: default_version(), + } +} + +pub fn default_booking_entity() -> BookingEntity { + BookingEntity { + id: default_id(), + sales_person_id: default_sales_person_id(), + slot_id: default_slot_id(), + calendar_week: 3, + year: 2024, + deleted: None, + version: default_version(), + } +} + +pub struct BookingServiceDependencies { + pub booking_dao: MockBookingDao, + pub permission_service: MockPermissionService, + pub clock_service: MockClockService, + pub uuid_service: MockUuidService, +} +impl BookingServiceDependencies { + pub fn build_service( + self, + ) -> BookingServiceImpl + { + BookingServiceImpl::new( + self.booking_dao.into(), + self.permission_service.into(), + self.clock_service.into(), + self.uuid_service.into(), + ) + } +} + +pub fn build_dependencies(permission: bool, role: &'static str) -> BookingServiceDependencies { + let booking_dao = MockBookingDao::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(); + + BookingServiceDependencies { + booking_dao, + permission_service, + clock_service, + uuid_service, + } +} + +#[tokio::test] +async fn test_get_all() { + let mut deps = build_dependencies(true, "hr"); + deps.booking_dao.expect_all().returning(|| { + Ok([ + default_booking_entity(), + BookingEntity { + id: alternate_id(), + ..default_booking_entity() + }, + ] + .into()) + }); + let service = deps.build_service(); + let result = service.get_all(()).await; + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(result.len(), 2); + assert_eq!(result[0], default_booking()); + assert_eq!( + result[1], + Booking { + id: alternate_id(), + ..default_booking() + } + ); +} + +#[tokio::test] +async fn test_get_all_no_permission() { + let deps = build_dependencies(false, "hr"); + let service = deps.build_service(); + let result = service.get_all(()).await; + test_forbidden(&result); +} + +#[tokio::test] +async fn test_get() { + let mut deps = build_dependencies(true, "hr"); + deps.booking_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(Some(default_booking_entity()))); + let service = deps.build_service(); + let result = service.get(default_id(), ()).await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), default_booking()); +} + +#[tokio::test] +async fn test_get_not_found() { + let mut deps = build_dependencies(true, "hr"); + deps.booking_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(None)); + let service = deps.build_service(); + let result = service.get(default_id(), ()).await; + test_not_found(&result, &default_id()); +} + +#[tokio::test] +async fn test_get_no_permission() { + let deps = build_dependencies(false, "hr"); + let service = deps.build_service(); + let result = service.get(default_id(), ()).await; + test_forbidden(&result); +} + +#[tokio::test] +async fn test_create() { + let mut deps = build_dependencies(true, "hr"); + deps.booking_dao + .expect_create() + .with(eq(default_booking_entity()), eq("booking-service")) + .returning(|_, _| Ok(())); + deps.uuid_service + .expect_new_uuid() + .with(eq("booking-id")) + .returning(|_| default_id()); + deps.uuid_service + .expect_new_uuid() + .with(eq("booking-version")) + .returning(|_| default_version()); + let service = deps.build_service(); + let result = service + .create( + &Booking { + id: Uuid::nil(), + version: Uuid::nil(), + ..default_booking() + }, + (), + ) + .await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), default_booking()); +} + +#[tokio::test] +async fn test_create_no_permission() { + let deps = build_dependencies(false, "hr"); + let service = deps.build_service(); + let result = service + .create( + &Booking { + id: Uuid::nil(), + version: Uuid::nil(), + ..default_booking() + }, + (), + ) + .await; + test_forbidden(&result); +} + +#[tokio::test] +async fn test_create_with_id() { + let deps = build_dependencies(true, "hr"); + let service = deps.build_service(); + let result = service + .create( + &Booking { + version: Uuid::nil(), + ..default_booking() + }, + (), + ) + .await; + test_zero_id_error(&result); +} + +#[tokio::test] +async fn test_create_with_version() { + let deps = build_dependencies(true, "hr"); + let service = deps.build_service(); + let result = service + .create( + &Booking { + id: Uuid::nil(), + ..default_booking() + }, + (), + ) + .await; + test_zero_version_error(&result); +} + +#[tokio::test] +async fn test_delete_no_permission() { + let deps = build_dependencies(false, "hr"); + let service = deps.build_service(); + let result = service.delete(default_id(), ()).await; + test_forbidden(&result); +} + +#[tokio::test] +async fn test_delete_not_found() { + let mut deps = build_dependencies(true, "hr"); + deps.booking_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(None)); + let service = deps.build_service(); + let result = service.delete(default_id(), ()).await; + test_not_found(&result, &default_id()); +} + +#[tokio::test] +async fn test_delete() { + let mut deps = build_dependencies(true, "hr"); + deps.booking_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(Some(default_booking_entity()))); + deps.booking_dao + .expect_update() + .with( + eq(BookingEntity { + deleted: Some(PrimitiveDateTime::new( + Date::from_calendar_date(2063, Month::April, 5).unwrap(), + Time::from_hms(23, 42, 0).unwrap(), + )), + version: alternate_version(), + ..default_booking_entity() + }), + eq("booking-service"), + ) + .returning(|_, _| Ok(())); + deps.uuid_service + .expect_new_uuid() + .with(eq("booking-version")) + .returning(|_| alternate_version()); + let service = deps.build_service(); + let result = service.delete(default_id(), ()).await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), ()); +} diff --git a/service_impl/src/test/mod.rs b/service_impl/src/test/mod.rs index abd9b9c..39d1e1b 100644 --- a/service_impl/src/test/mod.rs +++ b/service_impl/src/test/mod.rs @@ -1,4 +1,6 @@ #[cfg(test)] +pub mod booking; +#[cfg(test)] pub mod error_test; #[cfg(test)] mod permission_test;