Add endpoint to add extra_hours

This commit is contained in:
Simon Goller 2024-06-24 08:31:47 +02:00
parent d4adcb182f
commit c8f28e1f7b
11 changed files with 335 additions and 38 deletions

View file

@ -49,6 +49,12 @@ type WorkingHoursService = service_impl::working_hours::WorkingHoursServiceImpl<
ClockService,
UuidService,
>;
type ExtraHoursService = service_impl::extra_hours::ExtraHoursServiceImpl<
dao_impl::extra_hours::ExtraHoursDaoImpl,
PermissionService,
ClockService,
UuidService,
>;
#[derive(Clone)]
pub struct RestStateImpl {
@ -59,6 +65,7 @@ pub struct RestStateImpl {
booking_service: Arc<BookingService>,
reporting_service: Arc<ReportingService>,
working_hours_service: Arc<WorkingHoursService>,
extra_hours_service: Arc<ExtraHoursService>,
}
impl rest::RestStateDef for RestStateImpl {
type UserService = UserService;
@ -68,6 +75,7 @@ impl rest::RestStateDef for RestStateImpl {
type BookingService = BookingService;
type ReportingService = ReportingService;
type WorkingHoursService = WorkingHoursService;
type ExtraHoursService = ExtraHoursService;
fn user_service(&self) -> Arc<Self::UserService> {
self.user_service.clone()
@ -90,6 +98,9 @@ impl rest::RestStateDef for RestStateImpl {
fn working_hours_service(&self) -> Arc<Self::WorkingHoursService> {
self.working_hours_service.clone()
}
fn extra_hours_service(&self) -> Arc<Self::ExtraHoursService> {
self.extra_hours_service.clone()
}
}
impl RestStateImpl {
pub fn new(pool: Arc<sqlx::Pool<sqlx::Sqlite>>) -> Self {
@ -140,7 +151,7 @@ impl RestStateImpl {
slot_service.clone(),
));
let reporting_service = Arc::new(service_impl::reporting::ReportingServiceImpl::new(
extra_hours_dao,
extra_hours_dao.clone(),
shiftplan_report_dao,
working_hours_dao.clone(),
sales_person_service.clone(),
@ -152,9 +163,15 @@ impl RestStateImpl {
Arc::new(service_impl::working_hours::WorkingHoursServiceImpl::new(
working_hours_dao,
permission_service.clone(),
clock_service,
uuid_service,
clock_service.clone(),
uuid_service.clone(),
));
let extra_hours_service = Arc::new(service_impl::extra_hours::ExtraHoursServiceImpl::new(
extra_hours_dao,
permission_service.clone(),
clock_service,
uuid_service,
));
Self {
user_service,
permission_service,
@ -163,6 +180,7 @@ impl RestStateImpl {
booking_service,
reporting_service,
working_hours_service,
extra_hours_service,
}
}
}

View file

@ -20,7 +20,9 @@ pub struct ExtraHoursEntity {
pub category: ExtraHoursCategoryEntity,
pub description: Arc<str>,
pub date_time: time::PrimitiveDateTime,
pub created: time::PrimitiveDateTime,
pub deleted: Option<time::PrimitiveDateTime>,
pub version: Uuid,
}
#[automock]

View file

@ -6,7 +6,7 @@ use dao::{
extra_hours::{ExtraHoursCategoryEntity, ExtraHoursDao, ExtraHoursEntity},
DaoError,
};
use sqlx::query_as;
use sqlx::{query, query_as};
use time::{format_description::well_known::Iso8601, PrimitiveDateTime};
use uuid::Uuid;
@ -18,15 +18,17 @@ struct ExtraHoursDb {
category: String,
description: Option<String>,
date_time: String,
created: String,
deleted: Option<String>,
update_version: Vec<u8>,
}
impl TryFrom<&ExtraHoursDb> for ExtraHoursEntity {
type Error = DaoError;
fn try_from(extra_hours: &ExtraHoursDb) -> Result<Self, DaoError> {
Ok(Self {
id: Uuid::from_slice(extra_hours.id.as_ref()).unwrap(),
sales_person_id: Uuid::from_slice(extra_hours.sales_person_id.as_ref()).unwrap(),
id: Uuid::from_slice(extra_hours.id.as_ref())?,
sales_person_id: Uuid::from_slice(extra_hours.sales_person_id.as_ref())?,
amount: extra_hours.amount as f32,
category: match extra_hours.category.as_str() {
"ExtraWork" => ExtraHoursCategoryEntity::ExtraWork,
@ -44,14 +46,14 @@ impl TryFrom<&ExtraHoursDb> for ExtraHoursEntity {
date_time: PrimitiveDateTime::parse(
extra_hours.date_time.as_str(),
&Iso8601::DATE_TIME,
)
.unwrap(),
)?,
created: PrimitiveDateTime::parse(extra_hours.created.as_str(), &Iso8601::DATE_TIME)?,
deleted: extra_hours
.deleted
.as_ref()
.map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE_TIME))
.transpose()
.unwrap(),
.transpose()?,
version: Uuid::from_slice(&extra_hours.update_version)?,
})
}
}
@ -76,7 +78,7 @@ impl ExtraHoursDao for ExtraHoursDaoImpl {
let id_vec = sales_person_id.as_bytes().to_vec();
Ok(query_as!(
ExtraHoursDb,
"SELECT id, sales_person_id, amount, category, description, date_time, deleted FROM extra_hours WHERE sales_person_id = ? AND strftime('%Y', date_time) = ? AND strftime('%m', date_time) <= ?",
"SELECT id, sales_person_id, amount, category, description, date_time, created, deleted, update_version FROM extra_hours WHERE sales_person_id = ? AND CAST(strftime('%Y', date_time) AS INTEGER) = ? AND CAST(strftime('%m', date_time) AS INTEGER) <= ?",
id_vec,
year,
until_week,
@ -90,10 +92,41 @@ impl ExtraHoursDao for ExtraHoursDaoImpl {
}
async fn create(
&self,
_entity: &ExtraHoursEntity,
_process: &str,
entity: &ExtraHoursEntity,
process: &str,
) -> Result<(), crate::DaoError> {
unimplemented!()
let id_vec = entity.id.as_bytes().to_vec();
let sales_person_id_vec = entity.sales_person_id.as_bytes().to_vec();
let category = match entity.category {
ExtraHoursCategoryEntity::ExtraWork => "ExtraWork",
ExtraHoursCategoryEntity::Vacation => "Vacation",
ExtraHoursCategoryEntity::SickLeave => "SickLeave",
ExtraHoursCategoryEntity::Holiday => "Holiday",
};
let description = entity.description.as_ref();
let date_time = entity.date_time.format(&Iso8601::DATE_TIME)?;
let created = entity.created.format(&Iso8601::DATE_TIME)?;
let deleted = entity
.deleted
.map(|deleted| deleted.format(&Iso8601::DATE_TIME))
.transpose()?;
let version_vec = entity.version.as_bytes().to_vec();
query!(
"INSERT INTO extra_hours (id, sales_person_id, amount, category, description, date_time, created, deleted, update_process, update_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
id_vec,
sales_person_id_vec,
entity.amount,
category,
description,
date_time,
created,
deleted,
process,
version_vec,
).execute(self.pool.as_ref())
.await
.map_db_error()?;
Ok(())
}
async fn update(
&self,

View file

@ -237,14 +237,14 @@ impl From<&service::reporting::ShortEmployeeReport> for ShortEmployeeReportTO {
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum ExtraHoursCategoryTO {
pub enum ExtraHoursReportCategoryTO {
Shiftplan,
ExtraWork,
Vacation,
SickLeave,
Holiday,
}
impl From<&service::reporting::ExtraHoursCategory> for ExtraHoursCategoryTO {
impl From<&service::reporting::ExtraHoursCategory> for ExtraHoursReportCategoryTO {
fn from(category: &service::reporting::ExtraHoursCategory) -> Self {
match category {
service::reporting::ExtraHoursCategory::Shiftplan => Self::Shiftplan,
@ -260,7 +260,7 @@ impl From<&service::reporting::ExtraHoursCategory> for ExtraHoursCategoryTO {
pub struct WorkingHoursDayTO {
pub date: time::Date,
pub hours: f32,
pub category: ExtraHoursCategoryTO,
pub category: ExtraHoursReportCategoryTO,
}
impl From<&service::reporting::WorkingHoursDay> for WorkingHoursDayTO {
fn from(day: &service::reporting::WorkingHoursDay) -> Self {
@ -409,3 +409,79 @@ impl From<&WorkingHoursTO> for service::working_hours::WorkingHours {
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum ExtraHoursCategoryTO {
ExtraWork,
Vacation,
SickLeave,
Holiday,
}
impl From<&service::extra_hours::ExtraHoursCategory> for ExtraHoursCategoryTO {
fn from(category: &service::extra_hours::ExtraHoursCategory) -> Self {
match category {
service::extra_hours::ExtraHoursCategory::ExtraWork => Self::ExtraWork,
service::extra_hours::ExtraHoursCategory::Vacation => Self::Vacation,
service::extra_hours::ExtraHoursCategory::SickLeave => Self::SickLeave,
service::extra_hours::ExtraHoursCategory::Holiday => Self::Holiday,
}
}
}
impl From<&ExtraHoursCategoryTO> for service::extra_hours::ExtraHoursCategory {
fn from(category: &ExtraHoursCategoryTO) -> Self {
match category {
ExtraHoursCategoryTO::ExtraWork => Self::ExtraWork,
ExtraHoursCategoryTO::Vacation => Self::Vacation,
ExtraHoursCategoryTO::SickLeave => Self::SickLeave,
ExtraHoursCategoryTO::Holiday => Self::Holiday,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ExtraHoursTO {
#[serde(default)]
pub id: Uuid,
pub sales_person_id: Uuid,
pub amount: f32,
pub category: ExtraHoursCategoryTO,
pub description: Arc<str>,
pub date_time: time::PrimitiveDateTime,
#[serde(default)]
pub created: Option<time::PrimitiveDateTime>,
#[serde(default)]
pub deleted: Option<time::PrimitiveDateTime>,
#[serde(rename = "$version")]
#[serde(default)]
pub version: Uuid,
}
impl From<&service::extra_hours::ExtraHours> for ExtraHoursTO {
fn from(extra_hours: &service::extra_hours::ExtraHours) -> Self {
Self {
id: extra_hours.id,
sales_person_id: extra_hours.sales_person_id,
amount: extra_hours.amount,
category: (&extra_hours.category).into(),
description: extra_hours.description.clone(),
date_time: extra_hours.date_time,
created: extra_hours.created,
deleted: extra_hours.deleted,
version: extra_hours.version,
}
}
}
impl From<&ExtraHoursTO> for service::extra_hours::ExtraHours {
fn from(extra_hours: &ExtraHoursTO) -> Self {
Self {
id: extra_hours.id,
sales_person_id: extra_hours.sales_person_id,
amount: extra_hours.amount,
category: (&extra_hours.category).into(),
description: extra_hours.description.clone(),
date_time: extra_hours.date_time,
created: extra_hours.created,
deleted: extra_hours.deleted,
version: extra_hours.version,
}
}
}

34
rest/src/extra_hours.rs Normal file
View file

@ -0,0 +1,34 @@
use axum::{
body::Body, extract::State, response::Response, routing::post, Extension, Json, Router,
};
use rest_types::ExtraHoursTO;
use service::extra_hours::ExtraHoursService;
use crate::{error_handler, Context, RestStateDef};
pub fn generate_route<RestState: RestStateDef>() -> Router<RestState> {
Router::new().route("/", post(create_extra_hours::<RestState>))
}
pub async fn create_extra_hours<RestState: RestStateDef>(
rest_state: State<RestState>,
Extension(context): Extension<Context>,
Json(sales_person): Json<ExtraHoursTO>,
) -> Response {
error_handler(
(async {
let extra_hours = ExtraHoursTO::from(
&rest_state
.extra_hours_service()
.create(&(&sales_person).into(), context.into())
.await?,
);
Ok(Response::builder()
.status(200)
.body(Body::new(serde_json::to_string(&extra_hours).unwrap()))
.unwrap())
})
.await,
)
}

View file

@ -1,6 +1,7 @@
use std::{convert::Infallible, sync::Arc};
mod booking;
mod extra_hours;
mod permission;
mod report;
mod sales_person;
@ -231,6 +232,10 @@ pub trait RestStateDef: Clone + Send + Sync + 'static {
+ Send
+ Sync
+ 'static;
type ExtraHoursService: service::extra_hours::ExtraHoursService<Context = Context>
+ Send
+ Sync
+ 'static;
fn user_service(&self) -> Arc<Self::UserService>;
fn permission_service(&self) -> Arc<Self::PermissionService>;
@ -239,6 +244,7 @@ pub trait RestStateDef: Clone + Send + Sync + 'static {
fn booking_service(&self) -> Arc<Self::BookingService>;
fn reporting_service(&self) -> Arc<Self::ReportingService>;
fn working_hours_service(&self) -> Arc<Self::WorkingHoursService>;
fn extra_hours_service(&self) -> Arc<Self::ExtraHoursService>;
}
pub struct OidcConfig {
@ -344,6 +350,7 @@ pub async fn start_server<RestState: RestStateDef>(rest_state: RestState) {
.nest("/booking", booking::generate_route())
.nest("/report", report::generate_route())
.nest("/working-hours", working_hours::generate_route())
.nest("/extra-hours", extra_hours::generate_route())
.with_state(rest_state)
.layer(middleware::from_fn(context_extractor));

View file

@ -1,10 +1,11 @@
use std::fmt::Debug;
use std::sync::Arc;
use async_trait::async_trait;
use mockall::automock;
use uuid::Uuid;
use dao::DaoError;
use crate::{permission::Authentication, ServiceError};
#[derive(Clone, Debug, PartialEq)]
pub enum ExtraHoursCategory {
@ -42,7 +43,9 @@ pub struct ExtraHours {
pub category: ExtraHoursCategory,
pub description: Arc<str>,
pub date_time: time::PrimitiveDateTime,
pub created: Option<time::PrimitiveDateTime>,
pub deleted: Option<time::PrimitiveDateTime>,
pub version: Uuid,
}
impl From<&dao::extra_hours::ExtraHoursEntity> for ExtraHours {
fn from(extra_hours: &dao::extra_hours::ExtraHoursEntity) -> Self {
@ -53,34 +56,56 @@ impl From<&dao::extra_hours::ExtraHoursEntity> for ExtraHours {
category: (&extra_hours.category).into(),
description: extra_hours.description.clone(),
date_time: extra_hours.date_time,
created: Some(extra_hours.created),
deleted: extra_hours.deleted,
version: extra_hours.version,
}
}
}
impl From<&ExtraHours> for dao::extra_hours::ExtraHoursEntity {
fn from(extra_hours: &ExtraHours) -> Self {
Self {
impl TryFrom<&ExtraHours> for dao::extra_hours::ExtraHoursEntity {
type Error = ServiceError;
fn try_from(extra_hours: &ExtraHours) -> Result<Self, Self::Error> {
Ok(Self {
id: extra_hours.id,
sales_person_id: extra_hours.sales_person_id,
amount: extra_hours.amount,
category: (&extra_hours.category).into(),
description: extra_hours.description.clone(),
date_time: extra_hours.date_time,
created: extra_hours
.created
.ok_or_else(|| ServiceError::InternalError)?,
deleted: extra_hours.deleted,
}
version: extra_hours.version,
})
}
}
#[automock]
#[automock(type Context=();)]
#[async_trait]
pub trait ExtraHoursService {
fn find_by_sales_person_id_and_year(
type Context: Clone + Debug + PartialEq + Eq + Send + Sync + 'static;
async fn find_by_sales_person_id_and_year(
&self,
sales_person_id: Uuid,
year: u32,
until_week: u8,
) -> Result<Arc<[ExtraHours]>, DaoError>;
fn create(&self, entity: &ExtraHours, process: &str) -> Result<(), DaoError>;
fn update(&self, entity: &ExtraHours, process: &str) -> Result<(), DaoError>;
fn delete(&self, id: Uuid, process: &str) -> Result<(), DaoError>;
context: Authentication<Self::Context>,
) -> Result<Arc<[ExtraHours]>, ServiceError>;
async fn create(
&self,
entity: &ExtraHours,
context: Authentication<Self::Context>,
) -> Result<ExtraHours, ServiceError>;
async fn update(
&self,
entity: &ExtraHours,
context: Authentication<Self::Context>,
) -> Result<ExtraHours, ServiceError>;
async fn delete(
&self,
id: Uuid,
context: Authentication<Self::Context>,
) -> Result<ExtraHours, ServiceError>;
}

View file

@ -0,0 +1,111 @@
use std::sync::Arc;
use async_trait::async_trait;
use dao::extra_hours;
use service::{
extra_hours::ExtraHours,
permission::{Authentication, HR_PRIVILEGE},
ServiceError,
};
use uuid::Uuid;
pub struct ExtraHoursServiceImpl<
ExtraHoursDao: dao::extra_hours::ExtraHoursDao,
PermissionService: service::PermissionService,
ClockService: service::clock::ClockService,
UuidService: service::uuid_service::UuidService,
> {
extra_hours_dao: Arc<ExtraHoursDao>,
permission_service: Arc<PermissionService>,
clock_service: Arc<ClockService>,
uuid_service: Arc<UuidService>,
}
impl<ExtraHoursDao, PermissionService, ClockService, UuidService>
ExtraHoursServiceImpl<ExtraHoursDao, PermissionService, ClockService, UuidService>
where
ExtraHoursDao: dao::extra_hours::ExtraHoursDao + Sync + Send,
PermissionService: service::PermissionService + Sync + Send,
ClockService: service::clock::ClockService + Sync + Send,
UuidService: service::uuid_service::UuidService + Sync + Send,
{
pub fn new(
extra_hours_dao: Arc<ExtraHoursDao>,
permission_service: Arc<PermissionService>,
clock_service: Arc<ClockService>,
uuid_service: Arc<UuidService>,
) -> Self {
Self {
extra_hours_dao,
permission_service,
clock_service,
uuid_service,
}
}
}
#[async_trait]
impl<
ExtraHoursDao: dao::extra_hours::ExtraHoursDao + Sync + Send,
PermissionService: service::PermissionService + Sync + Send,
ClockService: service::clock::ClockService + Sync + Send,
UuidService: service::uuid_service::UuidService + Sync + Send,
> service::extra_hours::ExtraHoursService
for ExtraHoursServiceImpl<ExtraHoursDao, PermissionService, ClockService, UuidService>
{
type Context = PermissionService::Context;
async fn find_by_sales_person_id_and_year(
&self,
_sales_person_id: Uuid,
_year: u32,
_until_week: u8,
_context: Authentication<Self::Context>,
) -> Result<Arc<[ExtraHours]>, ServiceError> {
unimplemented!()
}
async fn create(
&self,
extra_hours: &ExtraHours,
context: Authentication<Self::Context>,
) -> Result<ExtraHours, ServiceError> {
self.permission_service
.check_permission(HR_PRIVILEGE, context)
.await?;
let mut extra_hours = extra_hours.to_owned();
if !extra_hours.id.is_nil() {
return Err(ServiceError::IdSetOnCreate);
}
if !extra_hours.version.is_nil() {
return Err(ServiceError::VersionSetOnCreate);
}
extra_hours.id = self.uuid_service.new_uuid("extra_hours_service::create id");
extra_hours.version = self
.uuid_service
.new_uuid("extra_hours_service::create version");
extra_hours.created = Some(self.clock_service.date_time_now());
let extra_hours_entity = extra_hours::ExtraHoursEntity::try_from(&extra_hours)?;
self.extra_hours_dao
.create(&extra_hours_entity, "extra_hours_service::create")
.await?;
Ok(extra_hours.into())
}
async fn update(
&self,
_entity: &ExtraHours,
_context: Authentication<Self::Context>,
) -> Result<ExtraHours, ServiceError> {
unimplemented!()
}
async fn delete(
&self,
_id: Uuid,
_context: Authentication<Self::Context>,
) -> Result<ExtraHours, ServiceError> {
unimplemented!()
}
}

View file

@ -4,6 +4,7 @@ use async_trait::async_trait;
pub mod booking;
pub mod clock;
pub mod extra_hours;
pub mod permission;
pub mod reporting;
pub mod sales_person;

View file

@ -124,7 +124,6 @@ pub fn find_working_hours_for_calendar_week(
year: u32,
week: u8,
) -> Option<&WorkingHoursEntity> {
dbg!((year, week));
working_hours.iter().find(|wh| {
(year, week) >= (wh.from_year, wh.from_calendar_week)
&& (year, week) <= (wh.to_year, wh.to_calendar_week)

View file

@ -87,14 +87,10 @@ where
.await
.is_err()
{
println!("No HR Role - remove sensitive data");
sales_persons.iter_mut().for_each(|sales_person| {
sales_person.is_paid = None;
});
} else {
println!("HR ROLE - no sensitive data removal");
}
Ok(sales_persons.into())
}
@ -128,7 +124,6 @@ where
.check_permission(HR_PRIVILEGE, context.clone())
);
shiftplanner.or(sales).or(hr)?;
println!("Has roles");
let mut sales_person = self
.sales_person_dao
.find_by_id(id)
@ -143,21 +138,17 @@ where
.await
.is_err()
{
println!("No HR Role - futher checks required");
if let (Some(current_user_id), Some(assigned_user)) = (
self.permission_service
.current_user_id(context.clone())
.await?,
self.get_assigned_user(id, Authentication::Full).await?,
) {
println!("Check if user ID matches");
current_user_id != assigned_user
} else {
println!("UserID or assigned user is missing - must remove sensitive data");
true
}
} else {
println!("HR Role - no sensitive data removal");
false
};