From d48c97edacdbde5b53fd80be381827ad2f59d8bc Mon Sep 17 00:00:00 2001 From: Simon Goller Date: Wed, 19 Jun 2024 09:59:14 +0200 Subject: [PATCH] Introduce is_paid attribute to SalesUser --- dao/src/sales_person.rs | 2 + dao_impl/src/sales_person.rs | 31 +++- .../20240618125847_paid-sales-persons.sql | 37 +++++ rest-types/src/lib.rs | 4 + rest/src/lib.rs | 5 +- service/src/sales_person.rs | 7 + service_impl/src/sales_person.rs | 101 ++++++++++-- service_impl/src/test/sales_person.rs | 153 ++++++++++++++++-- 8 files changed, 304 insertions(+), 36 deletions(-) create mode 100644 migrations/20240618125847_paid-sales-persons.sql diff --git a/dao/src/sales_person.rs b/dao/src/sales_person.rs index 4194d24..156dc45 100644 --- a/dao/src/sales_person.rs +++ b/dao/src/sales_person.rs @@ -11,6 +11,7 @@ pub struct SalesPersonEntity { pub id: Uuid, pub name: Arc, pub background_color: Arc, + pub is_paid: bool, pub deleted: Option, pub inactive: bool, pub version: Uuid, @@ -20,6 +21,7 @@ pub struct SalesPersonEntity { #[async_trait] pub trait SalesPersonDao { async fn all(&self) -> Result, DaoError>; + async fn all_paid(&self) -> Result, DaoError>; async fn find_by_id(&self, id: Uuid) -> Result, DaoError>; async fn find_by_user(&self, user_id: &str) -> Result, DaoError>; async fn create(&self, entity: &SalesPersonEntity, process: &str) -> Result<(), DaoError>; diff --git a/dao_impl/src/sales_person.rs b/dao_impl/src/sales_person.rs index 40c5be1..152857b 100644 --- a/dao_impl/src/sales_person.rs +++ b/dao_impl/src/sales_person.rs @@ -23,6 +23,7 @@ struct SalesPersonDb { id: Vec, name: String, background_color: String, + is_paid: bool, inactive: bool, deleted: Option, update_version: Vec, @@ -34,6 +35,7 @@ impl TryFrom<&SalesPersonDb> for SalesPersonEntity { id: Uuid::from_slice(sales_person.id.as_ref()).unwrap(), name: sales_person.name.as_str().into(), background_color: sales_person.background_color.as_str().into(), + is_paid: sales_person.is_paid, inactive: sales_person.inactive, deleted: sales_person .deleted @@ -50,7 +52,7 @@ impl SalesPersonDao for SalesPersonDaoImpl { async fn all(&self) -> Result, DaoError> { Ok(query_as!( SalesPersonDb, - "SELECT id, name, background_color, inactive, deleted, update_version FROM sales_person WHERE deleted IS NULL" + "SELECT id, name, background_color, is_paid, inactive, deleted, update_version FROM sales_person WHERE deleted IS NULL" ) .fetch_all(self.pool.as_ref()) .await @@ -60,11 +62,26 @@ impl SalesPersonDao for SalesPersonDaoImpl { .collect::, DaoError>>()? ) } + + async fn all_paid(&self) -> Result, DaoError> { + Ok(query_as!( + SalesPersonDb, + "SELECT id, name, background_color, is_paid, inactive, deleted, update_version FROM sales_person WHERE deleted IS NULL AND is_paid = 1" + ) + .fetch_all(self.pool.as_ref()) + .await + .map_db_error()? + .iter() + .map(SalesPersonEntity::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!( SalesPersonDb, - "SELECT id, name, background_color, inactive, deleted, update_version FROM sales_person WHERE id = ?", + "SELECT id, name, background_color, is_paid, inactive, deleted, update_version FROM sales_person WHERE id = ?", id_vec ) .fetch_optional(self.pool.as_ref()) @@ -78,7 +95,7 @@ impl SalesPersonDao for SalesPersonDaoImpl { async fn find_by_user(&self, user_id: &str) -> Result, DaoError> { Ok(query_as!( SalesPersonDb, - "SELECT sp.id, sp.name, sp.background_color, sp.inactive, sp.deleted, sp.update_version FROM sales_person sp JOIN sales_person_user spu ON sp.id = spu.sales_person_id WHERE spu.user_id = ?", + "SELECT sp.id, sp.name, sp.background_color, sp.is_paid, sp.inactive, sp.deleted, sp.update_version FROM sales_person sp JOIN sales_person_user spu ON sp.id = spu.sales_person_id WHERE spu.user_id = ?", user_id ) .fetch_optional(self.pool.as_ref()) @@ -94,9 +111,10 @@ impl SalesPersonDao for SalesPersonDaoImpl { let version = entity.version.as_bytes().to_vec(); let name = entity.name.as_ref(); let background_color = entity.background_color.as_ref(); + let is_paid = entity.is_paid; let inactive = entity.inactive; let deleted = entity.deleted.as_ref().map(|deleted| deleted.to_string()); - query!("INSERT INTO sales_person (id, name, background_color, inactive, deleted, update_version, update_process) VALUES (?, ?, ?, ?, ?, ?, ?)", id, name, background_color, inactive, deleted, version, process) + query!("INSERT INTO sales_person (id, name, background_color, is_paid, inactive, deleted, update_version, update_process) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", id, name, background_color, is_paid, inactive, deleted, version, process) .execute(self.pool.as_ref()) .await .map_db_error()?; @@ -107,9 +125,10 @@ impl SalesPersonDao for SalesPersonDaoImpl { let version = entity.version.as_bytes().to_vec(); let name = entity.name.as_ref(); let background_color = entity.background_color.as_ref(); + let is_paid = entity.is_paid; let inactive = entity.inactive; let deleted = entity.deleted.as_ref().map(|deleted| deleted.to_string()); - query!("UPDATE sales_person SET name = ?, background_color = ?, inactive = ?, deleted = ?, update_version = ?, update_process = ? WHERE id = ?", name, background_color, inactive, deleted, version, process, id) + query!("UPDATE sales_person SET name = ?, background_color = ?, is_paid = ?, inactive = ?, deleted = ?, update_version = ?, update_process = ? WHERE id = ?", name, background_color, is_paid, inactive, deleted, version, process, id) .execute(self.pool.as_ref()) .await .map_db_error()?; @@ -160,7 +179,7 @@ impl SalesPersonDao for SalesPersonDaoImpl { ) -> Result, DaoError> { Ok(query_as!( SalesPersonDb, - "SELECT sp.id, sp.name, sp.background_color, sp.inactive, sp.deleted, sp.update_version FROM sales_person sp JOIN sales_person_user spu ON sp.id = spu.sales_person_id WHERE spu.user_id = ?", + "SELECT sp.id, sp.name, sp.background_color, sp.is_paid, sp.inactive, sp.deleted, sp.update_version FROM sales_person sp JOIN sales_person_user spu ON sp.id = spu.sales_person_id WHERE spu.user_id = ?", user_id ) .fetch_optional(self.pool.as_ref()) diff --git a/migrations/20240618125847_paid-sales-persons.sql b/migrations/20240618125847_paid-sales-persons.sql new file mode 100644 index 0000000..fe1d7e1 --- /dev/null +++ b/migrations/20240618125847_paid-sales-persons.sql @@ -0,0 +1,37 @@ +-- Add migration script here + +ALTER TABLE sales_person +ADD COLUMN is_paid BOOLEAN DEFAULT 0 NOT NULL; + +CREATE TABLE working_hours ( + id blob(16) NOT NULL PRIMARY KEY, + sales_person_id blob(16) NOT NULL, + from_calendar_week INTEGER NOT NULL, + from_year INTEGER NOT NULL, + to_calendar_week INTEGER NOT NULL, + to_year INTEGER NOT NULL, + created TEXT NOT NULL, + deleted TEXT, + + update_timestamp TEXT, + update_process TEXT NOT NULL, + update_version blob(16) NOT NULL, + + FOREIGN KEY (sales_person_id) REFERENCES sales_person(id) +); + +CREATE TABLE extra_hours ( + id blob(16) NOT NULL PRIMARY KEY, + sales_person_id blob(16) NOT NULL, + amount INTEGER NOT NULL, + category TEXT NOT NULL, + date_time TEXT NOT NULL, + created TEXT NOT NULL, + deleted TEXT, + + update_timestamp TEXT, + update_process TEXT NOT NULL, + update_version blob(16) NOT NULL, + + FOREIGN KEY (sales_person_id) REFERENCES sales_person(id) +); \ No newline at end of file diff --git a/rest-types/src/lib.rs b/rest-types/src/lib.rs index 8dca1f7..3fc77cc 100644 --- a/rest-types/src/lib.rs +++ b/rest-types/src/lib.rs @@ -105,6 +105,8 @@ pub struct SalesPersonTO { pub name: Arc, pub background_color: Arc, #[serde(default)] + pub is_paid: Option, + #[serde(default)] pub inactive: bool, #[serde(default)] pub deleted: Option, @@ -118,6 +120,7 @@ impl From<&SalesPerson> for SalesPersonTO { id: sales_person.id, name: sales_person.name.clone(), background_color: sales_person.background_color.clone(), + is_paid: sales_person.is_paid, inactive: sales_person.inactive, deleted: sales_person.deleted, version: sales_person.version, @@ -130,6 +133,7 @@ impl From<&SalesPersonTO> for SalesPerson { id: sales_person.id, name: sales_person.name.clone(), background_color: sales_person.background_color.clone(), + is_paid: sales_person.is_paid, inactive: sales_person.inactive, deleted: sales_person.deleted, version: sales_person.version, diff --git a/rest/src/lib.rs b/rest/src/lib.rs index eb11c9b..86f2fed 100644 --- a/rest/src/lib.rs +++ b/rest/src/lib.rs @@ -302,6 +302,7 @@ pub async fn auth_info( pub async fn start_server(rest_state: RestState) { let app = Router::new(); + let app = app.route("/authenticate", get(login)); #[cfg(feature = "oidc")] let app = { @@ -314,9 +315,7 @@ pub async fn start_server(rest_state: RestState) { })) .layer(OidcLoginLayer::::new()); - app.route("/authenticate", get(login)) - .route("/logout", get(logout)) - .layer(oidc_login_service) + app.route("/logout", get(logout)).layer(oidc_login_service) }; let app = app diff --git a/service/src/sales_person.rs b/service/src/sales_person.rs index 58b258b..2bbbd29 100644 --- a/service/src/sales_person.rs +++ b/service/src/sales_person.rs @@ -13,6 +13,7 @@ pub struct SalesPerson { pub id: Uuid, pub name: Arc, pub background_color: Arc, + pub is_paid: Option, pub inactive: bool, pub deleted: Option, pub version: Uuid, @@ -23,6 +24,7 @@ impl From<&dao::sales_person::SalesPersonEntity> for SalesPerson { id: sales_person.id, name: sales_person.name.clone(), background_color: sales_person.background_color.clone(), + is_paid: Some(sales_person.is_paid), inactive: sales_person.inactive, deleted: sales_person.deleted, version: sales_person.version, @@ -35,6 +37,7 @@ impl From<&SalesPerson> for dao::sales_person::SalesPersonEntity { id: sales_person.id, name: sales_person.name.clone(), background_color: sales_person.background_color.clone(), + is_paid: sales_person.is_paid.unwrap_or(false), inactive: sales_person.inactive, deleted: sales_person.deleted, version: sales_person.version, @@ -51,6 +54,10 @@ pub trait SalesPersonService { &self, context: Authentication, ) -> Result, ServiceError>; + async fn get_all_paid( + &self, + context: Authentication, + ) -> Result, ServiceError>; async fn get( &self, id: Uuid, diff --git a/service_impl/src/sales_person.rs b/service_impl/src/sales_person.rs index fc3dae5..2b3b8d2 100644 --- a/service_impl/src/sales_person.rs +++ b/service_impl/src/sales_person.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use async_trait::async_trait; use dao::sales_person::SalesPersonEntity; use service::{ - permission::{Authentication, SALES_PRIVILEGE, SHIFTPLANNER_PRIVILEGE}, + permission::{Authentication, HR_PRIVILEGE, SALES_PRIVILEGE, SHIFTPLANNER_PRIVILEGE}, sales_person::SalesPerson, ServiceError, ValidationFailureItem, }; @@ -63,19 +63,54 @@ where &self, context: Authentication, ) -> Result, service::ServiceError> { - let (shiftplanner, sales) = join!( + let (shiftplanner, sales, hr) = join!( self.permission_service .check_permission(SHIFTPLANNER_PRIVILEGE, context.clone()), self.permission_service - .check_permission(SALES_PRIVILEGE, context) + .check_permission(SALES_PRIVILEGE, context.clone()), + self.permission_service + .check_permission(HR_PRIVILEGE, context.clone()) ); - shiftplanner.or(sales)?; - Ok(self + shiftplanner.or(sales).or(hr)?; + let mut sales_persons = self .sales_person_dao .all() .await? .iter() .map(SalesPerson::from) + .collect::>(); + + // Remove sensitive information if user is not a sales user. + if self + .permission_service + .check_permission(HR_PRIVILEGE, context) + .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()) + } + + async fn get_all_paid( + &self, + context: Authentication, + ) -> Result, ServiceError> { + self.permission_service + .check_permission(HR_PRIVILEGE, context) + .await?; + Ok(self + .sales_person_dao + .all_paid() + .await? + .iter() + .map(SalesPerson::from) .collect()) } @@ -84,19 +119,53 @@ where id: Uuid, context: Authentication, ) -> Result { - let (shiftplanner, sales) = join!( + let (shiftplanner, sales, hr) = join!( self.permission_service .check_permission(SHIFTPLANNER_PRIVILEGE, context.clone()), self.permission_service - .check_permission(SALES_PRIVILEGE, context) + .check_permission(SALES_PRIVILEGE, context.clone()), + self.permission_service + .check_permission(HR_PRIVILEGE, context.clone()) ); - shiftplanner.or(sales)?; - self.sales_person_dao + shiftplanner.or(sales).or(hr)?; + println!("Has roles"); + let mut sales_person = self + .sales_person_dao .find_by_id(id) .await? .as_ref() .map(SalesPerson::from) - .ok_or(ServiceError::EntityNotFound(id)) + .ok_or(ServiceError::EntityNotFound(id))?; + + let remove_sensitive_data = if self + .permission_service + .check_permission(HR_PRIVILEGE, context.clone()) + .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 + }; + + if remove_sensitive_data { + sales_person.is_paid = None; + } + + Ok(sales_person) } async fn exists( @@ -117,7 +186,7 @@ where context: Authentication, ) -> Result { self.permission_service - .check_permission("hr", context) + .check_permission(HR_PRIVILEGE, context) .await?; if sales_person.id != Uuid::nil() { @@ -147,7 +216,7 @@ where context: Authentication, ) -> Result { self.permission_service - .check_permission("hr", context) + .check_permission(HR_PRIVILEGE, context) .await?; let sales_person_entity = self @@ -195,7 +264,7 @@ where context: Authentication, ) -> Result<(), ServiceError> { self.permission_service - .check_permission("hr", context) + .check_permission(HR_PRIVILEGE, context) .await?; let mut sales_person_entity = self .sales_person_dao @@ -216,7 +285,7 @@ where context: Authentication, ) -> Result>, ServiceError> { self.permission_service - .check_permission("hr", context) + .check_permission(HR_PRIVILEGE, context) .await?; Ok(self .sales_person_dao @@ -231,7 +300,7 @@ where context: Authentication, ) -> Result<(), ServiceError> { self.permission_service - .check_permission("hr", context) + .check_permission(HR_PRIVILEGE, context) .await?; self.sales_person_dao .discard_assigned_user(sales_person_id) @@ -250,7 +319,7 @@ where context: Authentication, ) -> Result, ServiceError> { self.permission_service - .check_permission("hr", context) + .check_permission(HR_PRIVILEGE, context) .await?; Ok(self .sales_person_dao diff --git a/service_impl/src/test/sales_person.rs b/service_impl/src/test/sales_person.rs index 3009b86..16fffbb 100644 --- a/service_impl/src/test/sales_person.rs +++ b/service_impl/src/test/sales_person.rs @@ -1,8 +1,9 @@ use super::error_test::*; use dao::sales_person::{MockSalesPersonDao, SalesPersonEntity}; -use mockall::predicate::eq; +use mockall::predicate::{always, eq}; use service::{ clock::MockClockService, + permission::Authentication, sales_person::{SalesPerson, SalesPersonService}, uuid_service::MockUuidService, MockPermissionService, @@ -42,17 +43,19 @@ pub fn build_dependencies(permission: bool, role: &'static str) -> SalesPersonSe let mut permission_service = MockPermissionService::new(); permission_service .expect_check_permission() - .with(eq(role), eq(().auth())) - .returning(move |_, _| { - if permission { + .with(always(), always()) + .returning(move |inner_role, context| { + if context == Authentication::Full || (permission && inner_role == role) { + println!("Permission granted"); Ok(()) } else { + println!("Permission denied"); Err(service::ServiceError::Forbidden) } }); permission_service - .expect_check_permission() - .returning(move |_, _| Err(service::ServiceError::Forbidden)); + .expect_current_user_id() + .returning(|_| Ok(Some("TESTUSER".into()))); let mut clock_service = MockClockService::new(); clock_service .expect_time_now() @@ -95,6 +98,7 @@ pub fn default_sales_person_entity() -> dao::sales_person::SalesPersonEntity { id: default_id(), name: "John Doe".into(), background_color: "#FFF".into(), + is_paid: false, deleted: None, inactive: false, version: default_version(), @@ -106,6 +110,7 @@ pub fn default_sales_person() -> service::sales_person::SalesPerson { id: default_id(), name: "John Doe".into(), background_color: "#FFF".into(), + is_paid: Some(false), inactive: false, deleted: None, version: default_version(), @@ -113,7 +118,7 @@ pub fn default_sales_person() -> service::sales_person::SalesPerson { } #[tokio::test] -async fn test_get_all() { +async fn test_get_all_shiftplanner() { let mut dependencies = build_dependencies(true, "shiftplanner"); dependencies.sales_person_dao.expect_all().returning(|| { Ok([ @@ -129,11 +134,18 @@ async fn test_get_all() { let sales_person_service = dependencies.build_service(); let result = sales_person_service.get_all(().auth()).await.unwrap(); assert_eq!(2, result.len()); - assert_eq!(default_sales_person(), result[0]); + assert_eq!( + service::sales_person::SalesPerson { + is_paid: None, + ..default_sales_person() + }, + result[0] + ); assert_eq!( service::sales_person::SalesPerson { id: alternate_id(), name: "Jane Doe".into(), + is_paid: None, ..default_sales_person() }, result[1] @@ -157,6 +169,41 @@ async fn test_get_all_sales_user() { let sales_person_service = dependencies.build_service(); let result = sales_person_service.get_all(().auth()).await.unwrap(); assert_eq!(2, result.len()); + assert_eq!( + service::sales_person::SalesPerson { + is_paid: None, + ..default_sales_person() + }, + result[0] + ); + assert_eq!( + service::sales_person::SalesPerson { + id: alternate_id(), + name: "Jane Doe".into(), + is_paid: None, + ..default_sales_person() + }, + result[1] + ); +} + +#[tokio::test] +async fn test_get_all_hr_user() { + 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(().auth()).await.unwrap(); + assert_eq!(2, result.len()); assert_eq!(default_sales_person(), result[0]); assert_eq!( service::sales_person::SalesPerson { @@ -177,7 +224,26 @@ async fn test_get_all_no_permission() { } #[tokio::test] -async fn test_get() { +async fn test_get_hr_user() { + 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()))); + dependencies + .sales_person_dao + .expect_get_assigned_user() + .with(eq(default_id())) + .returning(|_| Ok(Some("TESTUSER".into()))); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.get(default_id(), ().auth()).await; + assert_eq!(default_sales_person(), result.unwrap()); +} + +#[tokio::test] +async fn test_get_shiftplanner_user_other_user() { let mut dependencies = build_dependencies(true, "shiftplanner"); dependencies .sales_person_dao @@ -185,13 +251,24 @@ async fn test_get() { .with(eq(default_id())) .times(1) .returning(|_| Ok(Some(default_sales_person_entity()))); + dependencies + .sales_person_dao + .expect_get_assigned_user() + .with(eq(default_id())) + .returning(|_| Ok(Some("OTHER".into()))); let sales_person_service = dependencies.build_service(); let result = sales_person_service.get(default_id(), ().auth()).await; - assert_eq!(default_sales_person(), result.unwrap()); + assert_eq!( + SalesPerson { + is_paid: None, + ..default_sales_person() + }, + result.unwrap() + ); } #[tokio::test] -async fn test_get_sales_user() { +async fn test_get_sales_user_other_user() { let mut dependencies = build_dependencies(true, "sales"); dependencies .sales_person_dao @@ -199,6 +276,60 @@ async fn test_get_sales_user() { .with(eq(default_id())) .times(1) .returning(|_| Ok(Some(default_sales_person_entity()))); + dependencies + .sales_person_dao + .expect_get_assigned_user() + .with(eq(default_id())) + .returning(|_| Ok(Some("OTHER".into()))); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.get(default_id(), ().auth()).await; + assert_eq!( + SalesPerson { + is_paid: None, + ..default_sales_person() + }, + result.unwrap() + ); +} + +#[tokio::test] +async fn test_get_shiftplanner_user_same_user() { + let mut dependencies = build_dependencies(true, "shiftplanner"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .times(1) + .returning(|_| Ok(Some(default_sales_person_entity()))); + dependencies + .sales_person_dao + .expect_get_assigned_user() + .with(eq(default_id())) + .returning(|_| Ok(Some("TESTUSER".into()))); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.get(default_id(), ().auth()).await; + assert_eq!( + SalesPerson { + ..default_sales_person() + }, + result.unwrap() + ); +} + +#[tokio::test] +async fn test_get_sales_user_same_user() { + let mut dependencies = build_dependencies(true, "sales"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .times(1) + .returning(|_| Ok(Some(default_sales_person_entity()))); + dependencies + .sales_person_dao + .expect_get_assigned_user() + .with(eq(default_id())) + .returning(|_| Ok(Some("TESTUSER".into()))); let sales_person_service = dependencies.build_service(); let result = sales_person_service.get(default_id(), ().auth()).await; assert_eq!(default_sales_person(), result.unwrap());