diff --git a/dao/src/booking.rs b/dao/src/booking.rs index eea55de..e2f40ad 100644 --- a/dao/src/booking.rs +++ b/dao/src/booking.rs @@ -14,6 +14,7 @@ pub struct BookingEntity { pub slot_id: Uuid, pub calendar_week: i32, pub year: u32, + pub created: PrimitiveDateTime, pub deleted: Option, pub version: Uuid, } diff --git a/dao_impl/src/booking.rs b/dao_impl/src/booking.rs new file mode 100644 index 0000000..000091f --- /dev/null +++ b/dao_impl/src/booking.rs @@ -0,0 +1,109 @@ +use std::sync::Arc; + +use crate::ResultDbErrorExt; +use async_trait::async_trait; +use dao::{ + booking::{BookingDao, BookingEntity}, + DaoError, +}; +use sqlx::{query, query_as}; +use time::{format_description::well_known::Iso8601, PrimitiveDateTime}; +use uuid::Uuid; + +struct BookingDb { + id: Vec, + sales_person_id: Vec, + slot_id: Vec, + calendar_week: i64, + year: i64, + created: String, + deleted: Option, + update_version: Vec, +} +impl TryFrom<&BookingDb> for BookingEntity { + type Error = DaoError; + fn try_from(booking: &BookingDb) -> Result { + Ok(Self { + id: Uuid::from_slice(booking.id.as_ref()).unwrap(), + sales_person_id: Uuid::from_slice(booking.sales_person_id.as_ref()).unwrap(), + slot_id: Uuid::from_slice(booking.slot_id.as_ref()).unwrap(), + calendar_week: booking.calendar_week as i32, + year: booking.year as u32, + created: PrimitiveDateTime::parse(&booking.created, &Iso8601::DATE_TIME)?, + deleted: booking + .deleted + .as_ref() + .map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE_TIME)) + .transpose()?, + version: Uuid::from_slice(&booking.update_version).unwrap(), + }) + } +} + +pub struct BookingDaoImpl { + pub pool: Arc, +} +impl BookingDaoImpl { + pub fn new(pool: Arc) -> Self { + Self { pool } + } +} + +#[async_trait] +impl BookingDao for BookingDaoImpl { + async fn all(&self) -> Result, DaoError> { + Ok(query_as!( + BookingDb, + "SELECT id, sales_person_id, slot_id, calendar_week, year, created, deleted, update_version FROM booking WHERE deleted IS NULL" + ) + .fetch_all(self.pool.as_ref()) + .await + .map_db_error()? + .iter() + .map(BookingEntity::try_from) + .collect::, DaoError>>()? + ) + } + async fn find_by_id(&self, id: Uuid) -> Result, DaoError> { + let id_vec = id.as_bytes().to_vec(); + Ok(query_as!( + BookingDb, + "SELECT id, sales_person_id, slot_id, calendar_week, year, created, deleted, update_version FROM booking WHERE id = ?", + id_vec, + ) + .fetch_optional(self.pool.as_ref()) + .await + .map_db_error()? + .as_ref() + .map(BookingEntity::try_from) + .transpose()?) + } + async fn create(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError> { + let id_vec = entity.id.as_bytes().to_vec(); + let sales_person_id_vec = entity.sales_person_id.as_bytes().to_vec(); + let slot_id_vec = entity.slot_id.as_bytes().to_vec(); + let created = entity.created.to_string(); + let deleted = entity.deleted.as_ref().map(|deleted| deleted.to_string()); + let version_vec = entity.version.as_bytes().to_vec(); + query!("INSERT INTO booking (id, sales_person_id, slot_id, calendar_week, year, created, deleted, update_version, update_process) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + id_vec, sales_person_id_vec, slot_id_vec, entity.calendar_week, entity.year, created, deleted, version_vec, process + ).execute(self.pool.as_ref()).await.map_db_error()?; + Ok(()) + } + async fn update(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError> { + let id_vec = entity.id.as_bytes().to_vec(); + let version_vec = entity.version.as_bytes().to_vec(); + let deleted = entity.deleted.as_ref().map(|deleted| deleted.to_string()); + query!( + "UPDATE booking SET deleted = ?, update_version = ?, update_process = ? WHERE id = ?", + deleted, + version_vec, + process, + id_vec + ) + .execute(self.pool.as_ref()) + .await + .map_db_error()?; + Ok(()) + } +} diff --git a/dao_impl/src/lib.rs b/dao_impl/src/lib.rs index bbb1bf9..3919680 100644 --- a/dao_impl/src/lib.rs +++ b/dao_impl/src/lib.rs @@ -4,6 +4,7 @@ use async_trait::async_trait; use dao::DaoError; use sqlx::{query, query_as, SqlitePool}; +pub mod booking; pub mod sales_person; pub mod slot; diff --git a/migrations/20240507063704_add-booking.sql b/migrations/20240507063704_add-booking.sql new file mode 100644 index 0000000..dd079b1 --- /dev/null +++ b/migrations/20240507063704_add-booking.sql @@ -0,0 +1,15 @@ +-- Add migration script here + +CREATE TABLE booking ( + id blob(16) NOT NULL PRIMARY KEY, + sales_person_id blob(16) NOT NULL, + slot_id blob(16) NOT NULL, + calendar_week INTEGER NOT NULL, + year INTEGER NOT NULL, + created TEXT NOT NULL, + deleted TEXT, + + update_timestamp TEXT, + update_process TEXT NOT NULL, + update_version blob(16) NOT NULL +); \ No newline at end of file diff --git a/rest/src/lib.rs b/rest/src/lib.rs index 5f24017..375e9ed 100644 --- a/rest/src/lib.rs +++ b/rest/src/lib.rs @@ -5,6 +5,7 @@ mod sales_person; mod slot; use axum::{body::Body, response::Response, Router}; +use service::ServiceError; use thiserror::Error; use uuid::Uuid; @@ -124,6 +125,10 @@ fn error_handler(result: Result) -> Response { .body(Body::new(err.to_string())) .unwrap() } + Err(RestError::ServiceError(ServiceError::InternalError)) => Response::builder() + .status(500) + .body(Body::new("Internal server error".to_string())) + .unwrap(), } } diff --git a/service/src/booking.rs b/service/src/booking.rs index 7f403fe..aa7bf44 100644 --- a/service/src/booking.rs +++ b/service/src/booking.rs @@ -13,6 +13,7 @@ pub struct Booking { pub slot_id: Uuid, pub calendar_week: i32, pub year: u32, + pub created: Option, pub deleted: Option, pub version: Uuid, } @@ -25,23 +26,26 @@ impl From<&dao::booking::BookingEntity> for Booking { slot_id: booking.slot_id, calendar_week: booking.calendar_week, year: booking.year, + created: Some(booking.created), deleted: booking.deleted, version: booking.version, } } } -impl From<&Booking> for dao::booking::BookingEntity { - fn from(booking: &Booking) -> Self { - Self { +impl TryFrom<&Booking> for dao::booking::BookingEntity { + type Error = ServiceError; + fn try_from(booking: &Booking) -> Result { + Ok(Self { id: booking.id, sales_person_id: booking.sales_person_id, slot_id: booking.slot_id, calendar_week: booking.calendar_week, year: booking.year, + created: booking.created.ok_or_else(|| ServiceError::InternalError)?, deleted: booking.deleted, version: booking.version, - } + }) } } diff --git a/service/src/lib.rs b/service/src/lib.rs index c22ea66..e7b6b2c 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -21,6 +21,7 @@ pub use permission::User; #[derive(Debug, PartialEq, Eq)] pub enum ValidationFailureItem { ModificationNotAllowed(Arc), + InvalidValue(Arc), } #[derive(Debug, Error)] @@ -57,4 +58,7 @@ pub enum ServiceError { #[error("Date order wrong. {0} must is not smaller or equal to {1}")] DateOrderWrong(Date, Date), + + #[error("Internal error")] + InternalError, } diff --git a/service_impl/src/booking.rs b/service_impl/src/booking.rs index c55bd29..e4decbd 100644 --- a/service_impl/src/booking.rs +++ b/service_impl/src/booking.rs @@ -1,7 +1,8 @@ use async_trait::async_trait; +use dao::booking; use service::{ booking::{Booking, BookingService}, - ServiceError, + ServiceError, ValidationFailureItem, }; use std::sync::Arc; use uuid::Uuid; @@ -95,16 +96,40 @@ where return Err(ServiceError::VersionSetOnCreate); } + let mut validation = Vec::with_capacity(8); + if booking.created.is_some() { + validation.push(ValidationFailureItem::InvalidValue("created".into())); + } + if booking.sales_person_id == Uuid::nil() { + validation.push(ValidationFailureItem::InvalidValue( + "sales_person_id".into(), + )); + } + if booking.slot_id == Uuid::nil() { + validation.push(ValidationFailureItem::InvalidValue("slot_id".into())); + } + if booking.calendar_week <= 0 { + validation.push(ValidationFailureItem::InvalidValue("calendar_week".into())); + } + if booking.calendar_week > 53 { + validation.push(ValidationFailureItem::InvalidValue("calendar_week".into())); + } + + if !validation.is_empty() { + return Err(ServiceError::ValidationError(validation.into())); + } + 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, + created: Some(self.clock_service.date_time_now()), ..booking.clone() }; self.booking_dao - .create(&(&new_booking).into(), BOOKING_SERVICE_PROCESS) + .create(&(&new_booking).try_into()?, BOOKING_SERVICE_PROCESS) .await?; Ok(new_booking) diff --git a/service_impl/src/test/booking.rs b/service_impl/src/test/booking.rs index 461b70d..dfdc6cb 100644 --- a/service_impl/src/test/booking.rs +++ b/service_impl/src/test/booking.rs @@ -2,7 +2,8 @@ use crate::test::error_test::*; use dao::booking::{BookingEntity, MockBookingDao}; use mockall::predicate::eq; use service::{ - booking::Booking, clock::MockClockService, uuid_service::MockUuidService, MockPermissionService, + booking::Booking, clock::MockClockService, uuid_service::MockUuidService, + MockPermissionService, ValidationFailureItem, }; use time::{Date, Month, PrimitiveDateTime, Time}; use uuid::{uuid, Uuid}; @@ -36,6 +37,10 @@ pub fn default_booking() -> Booking { slot_id: default_slot_id(), calendar_week: 3, year: 2024, + created: Some(PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 1).unwrap(), + Time::from_hms(0, 0, 0).unwrap(), + )), deleted: None, version: default_version(), } @@ -48,6 +53,10 @@ pub fn default_booking_entity() -> BookingEntity { slot_id: default_slot_id(), calendar_week: 3, year: 2024, + created: PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 1).unwrap(), + Time::from_hms(0, 0, 0).unwrap(), + ), deleted: None, version: default_version(), } @@ -186,7 +195,13 @@ async fn test_create() { let mut deps = build_dependencies(true, "hr"); deps.booking_dao .expect_create() - .with(eq(default_booking_entity()), eq("booking-service")) + .with( + eq(BookingEntity { + created: generate_default_datetime(), + ..default_booking_entity() + }), + eq("booking-service"), + ) .returning(|_, _| Ok(())); deps.uuid_service .expect_new_uuid() @@ -202,13 +217,20 @@ async fn test_create() { &Booking { id: Uuid::nil(), version: Uuid::nil(), + created: None, ..default_booking() }, (), ) .await; assert!(result.is_ok()); - assert_eq!(result.unwrap(), default_booking()); + assert_eq!( + result.unwrap(), + Booking { + created: Some(generate_default_datetime()), + ..default_booking() + } + ); } #[tokio::test] @@ -260,6 +282,27 @@ async fn test_create_with_version() { test_zero_version_error(&result); } +#[tokio::test] +async fn test_create_with_created_fail() { + let deps = build_dependencies(true, "hr"); + let service = deps.build_service(); + let result = service + .create( + &Booking { + id: Uuid::nil(), + version: Uuid::nil(), + ..default_booking() + }, + (), + ) + .await; + test_validation_error( + &result, + &ValidationFailureItem::InvalidValue("created".into()), + 1, + ); +} + #[tokio::test] async fn test_delete_no_permission() { let deps = build_dependencies(false, "hr"); @@ -291,10 +334,7 @@ async fn test_delete() { .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(), - )), + deleted: Some(generate_default_datetime()), version: alternate_version(), ..default_booking_entity() }), diff --git a/service_impl/src/test/error_test.rs b/service_impl/src/test/error_test.rs index d48ca4c..d17f7e2 100644 --- a/service_impl/src/test/error_test.rs +++ b/service_impl/src/test/error_test.rs @@ -1,4 +1,5 @@ use service::ValidationFailureItem; +use time::{Date, Month, PrimitiveDateTime, Time}; use uuid::Uuid; pub fn test_forbidden(result: &Result) { @@ -106,3 +107,10 @@ pub fn test_validation_error( panic!("Expected validation error"); } } + +pub fn generate_default_datetime() -> PrimitiveDateTime { + PrimitiveDateTime::new( + Date::from_calendar_date(2063, Month::April, 5).unwrap(), + Time::from_hms(23, 42, 0).unwrap(), + ) +}