Add checks for booking and fix database timestamps

This commit is contained in:
Simon Goller 2024-05-09 09:45:53 +02:00
parent bc8a534353
commit e3ec694876
7 changed files with 73 additions and 7 deletions

View file

@ -24,6 +24,7 @@ pub struct BookingEntity {
pub trait BookingDao { pub trait BookingDao {
async fn all(&self) -> Result<Arc<[BookingEntity]>, DaoError>; async fn all(&self) -> Result<Arc<[BookingEntity]>, DaoError>;
async fn find_by_id(&self, id: Uuid) -> Result<Option<BookingEntity>, DaoError>; async fn find_by_id(&self, id: Uuid) -> Result<Option<BookingEntity>, DaoError>;
async fn find_by_booking_data(&self, sales_person_id: Uuid, slot_id: Uuid, calendar_week: i32, year: u32) -> Result<Option<BookingEntity>, DaoError>;
async fn create(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError>; async fn create(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError>;
async fn update(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError>; async fn update(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError>;
} }

View file

@ -10,6 +10,7 @@ use sqlx::{query, query_as};
use time::{format_description::well_known::Iso8601, PrimitiveDateTime}; use time::{format_description::well_known::Iso8601, PrimitiveDateTime};
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug)]
struct BookingDb { struct BookingDb {
id: Vec<u8>, id: Vec<u8>,
sales_person_id: Vec<u8>, sales_person_id: Vec<u8>,
@ -23,6 +24,7 @@ struct BookingDb {
impl TryFrom<&BookingDb> for BookingEntity { impl TryFrom<&BookingDb> for BookingEntity {
type Error = DaoError; type Error = DaoError;
fn try_from(booking: &BookingDb) -> Result<Self, Self::Error> { fn try_from(booking: &BookingDb) -> Result<Self, Self::Error> {
dbg!(&booking);
Ok(Self { Ok(Self {
id: Uuid::from_slice(booking.id.as_ref()).unwrap(), id: Uuid::from_slice(booking.id.as_ref()).unwrap(),
sales_person_id: Uuid::from_slice(booking.sales_person_id.as_ref()).unwrap(), sales_person_id: Uuid::from_slice(booking.sales_person_id.as_ref()).unwrap(),
@ -78,12 +80,33 @@ impl BookingDao for BookingDaoImpl {
.map(BookingEntity::try_from) .map(BookingEntity::try_from)
.transpose()?) .transpose()?)
} }
async fn find_by_booking_data(&self, sales_person_id: Uuid, slot_id: Uuid, calendar_week: i32, year: u32) -> Result<Option<BookingEntity>, DaoError> {
let sales_person_id_vec = sales_person_id.as_bytes().to_vec();
let slot_id_vec = slot_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 sales_person_id = ? AND slot_id = ? AND calendar_week = ? AND year = ? AND deleted IS NULL",
sales_person_id_vec,
slot_id_vec,
calendar_week,
year,
)
.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> { async fn create(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError> {
let id_vec = entity.id.as_bytes().to_vec(); let id_vec = entity.id.as_bytes().to_vec();
let sales_person_id_vec = entity.sales_person_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 slot_id_vec = entity.slot_id.as_bytes().to_vec();
let created = entity.created.to_string(); let created = entity.created.format(&Iso8601::DATE_TIME).map_db_error()?;
let deleted = entity.deleted.as_ref().map(|deleted| deleted.to_string()); let deleted = entity.deleted.as_ref().map(|deleted| deleted.format(&Iso8601::DATE_TIME)).transpose().map_db_error()?;
let version_vec = entity.version.as_bytes().to_vec(); 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)", 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 id_vec, sales_person_id_vec, slot_id_vec, entity.calendar_week, entity.year, created, deleted, version_vec, process
@ -93,7 +116,7 @@ impl BookingDao for BookingDaoImpl {
async fn update(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError> { async fn update(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError> {
let id_vec = entity.id.as_bytes().to_vec(); let id_vec = entity.id.as_bytes().to_vec();
let version_vec = entity.version.as_bytes().to_vec(); let version_vec = entity.version.as_bytes().to_vec();
let deleted = entity.deleted.as_ref().map(|deleted| deleted.to_string()); let deleted = entity.deleted.as_ref().map(|deleted| deleted.format(&Iso8601::DATE_TIME)).transpose().map_db_error()?;
query!( query!(
"UPDATE booking SET deleted = ?, update_version = ?, update_process = ? WHERE id = ?", "UPDATE booking SET deleted = ?, update_version = ?, update_process = ? WHERE id = ?",
deleted, deleted,

View file

@ -36,7 +36,7 @@ impl TryFrom<&SalesPersonDb> for SalesPersonEntity {
deleted: sales_person deleted: sales_person
.deleted .deleted
.as_ref() .as_ref()
.map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE)) .map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE_TIME))
.transpose()?, .transpose()?,
version: Uuid::from_slice(&sales_person.update_version).unwrap(), version: Uuid::from_slice(&sales_person.update_version).unwrap(),
}) })

View file

@ -90,7 +90,7 @@ impl dao::slot::SlotDao for SlotDaoImpl {
let to = slot.to.to_string(); let to = slot.to.to_string();
let valid_from = slot.valid_from.to_string(); let valid_from = slot.valid_from.to_string();
let valid_to = slot.valid_to.map(|valid_to| valid_to.to_string()); let valid_to = slot.valid_to.map(|valid_to| valid_to.to_string());
let deleted = slot.deleted.map(|deleted| deleted.to_string()); let deleted = slot.deleted.as_ref().map(|deleted| deleted.to_string());
query!("INSERT INTO slot (id, day_of_week, time_from, time_to, valid_from, valid_to, deleted, update_version, update_process) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", query!("INSERT INTO slot (id, day_of_week, time_from, time_to, valid_from, valid_to, deleted, update_version, update_process) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
id_vec, id_vec,
day_of_week, day_of_week,
@ -112,7 +112,7 @@ impl dao::slot::SlotDao for SlotDaoImpl {
let id_vec = slot.id.as_bytes().to_vec(); let id_vec = slot.id.as_bytes().to_vec();
let version_vec = slot.version.as_bytes().to_vec(); let version_vec = slot.version.as_bytes().to_vec();
let valid_to = slot.valid_to.map(|valid_to| valid_to.to_string()); let valid_to = slot.valid_to.map(|valid_to| valid_to.to_string());
let deleted = slot.deleted.map(|deleted| deleted.to_string()); let deleted = slot.deleted.as_ref().map(|deleted| deleted.to_string());
query!("UPDATE slot SET valid_to = ?, deleted = ?, update_version = ?, update_process = ? WHERE id = ?", query!("UPDATE slot SET valid_to = ?, deleted = ?, update_version = ?, update_process = ? WHERE id = ?",
valid_to, valid_to,
deleted, deleted,

View file

@ -57,6 +57,9 @@ pub enum RestError {
} }
fn error_handler(result: Result<Response, RestError>) -> Response { fn error_handler(result: Result<Response, RestError>) -> Response {
if result.is_err() {
println!("REST error mapping: {:?}", result);
}
match result { match result {
Ok(response) => response, Ok(response) => response,
Err(err @ RestError::InconsistentId(_, _)) => Response::builder() Err(err @ RestError::InconsistentId(_, _)) => Response::builder()

View file

@ -170,6 +170,16 @@ where
booking.slot_id, booking.slot_id,
)); ));
} }
if self
.booking_dao
.find_by_booking_data(
booking.sales_person_id,
booking.slot_id,
booking.calendar_week,
booking.year,
).await?.is_some() {
validation.push(ValidationFailureItem::Duplicate);
}
if !validation.is_empty() { if !validation.is_empty() {
return Err(ServiceError::ValidationError(validation.into())); return Err(ServiceError::ValidationError(validation.into()));

View file

@ -94,7 +94,8 @@ impl BookingServiceDependencies {
} }
pub fn build_dependencies(permission: bool, role: &'static str) -> BookingServiceDependencies { pub fn build_dependencies(permission: bool, role: &'static str) -> BookingServiceDependencies {
let booking_dao = MockBookingDao::new(); let mut booking_dao = MockBookingDao::new();
booking_dao.expect_find_by_booking_data().returning(|_, _, _, _| Ok(None));
let mut permission_service = MockPermissionService::new(); let mut permission_service = MockPermissionService::new();
permission_service permission_service
.expect_check_permission() .expect_check_permission()
@ -351,6 +352,34 @@ async fn test_create_sales_person_does_not_exist() {
); );
} }
#[tokio::test]
async fn test_create_booking_data_already_exists() {
let mut deps = build_dependencies(true, "hr");
deps.booking_dao.checkpoint();
deps.booking_dao
.expect_find_by_booking_data()
.with(eq(default_sales_person_id()), eq(default_slot_id()), eq(3), eq(2024))
.returning(|_, _, _, _| Ok(Some(default_booking_entity())));
let service = deps.build_service();
let result = service
.create(
&Booking {
id: Uuid::nil(),
version: Uuid::nil(),
created: None,
..default_booking()
},
(),
)
.await;
test_validation_error(
&result,
&ValidationFailureItem::Duplicate,
1,
);
}
#[tokio::test] #[tokio::test]
async fn test_create_slot_does_not_exist() { async fn test_create_slot_does_not_exist() {
let mut deps = build_dependencies(true, "hr"); let mut deps = build_dependencies(true, "hr");