From 2a432ce3bb2931495a4dd16f89e067ab1770b547 Mon Sep 17 00:00:00 2001 From: rschouten97 <69046642+rschouten97@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:25:21 +0100 Subject: [PATCH 1/9] Fixed declare of returnValue in enddate --- mapping.assignments.json | 6 +++--- mapping.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mapping.assignments.json b/mapping.assignments.json index f02a7c4..821f22c 100644 --- a/mapping.assignments.json +++ b/mapping.assignments.json @@ -51,9 +51,9 @@ { "name": "Name.Convention", "mode": "complex", - "value": "function getValue() {\r\n let returnValue = '';\r\n\r\n switch (source.nameAssembleOrder) {\r\n case \"P\": {\r\n returnValue = \"P\";\r\n break;\r\n }\r\n case \"E\": {\r\n returnValue = \"B\";\r\n break;\r\n }\r\n case \"B\": {\r\n returnValue = \"PB\";\r\n break;\r\n }\r\n case \"C\": {\r\n returnValue = \"BP\";\r\n break;\r\n }\r\n case \"D\": {\r\n returnValue = \"BP\";\r\n break;\r\n }\r\n default: {\r\n returnValue = \"B\";\r\n break;\r\n }\r\n }\r\n\r\n return returnValue;\r\n}\r\n\r\ngetValue();", + "value": "function getValue() {\r\n let returnValue = '';\r\n let convention = source.nameAssembleOrder\r\n\r\nif (typeof convention !== 'undefined' && convention ) { \r\n convention = convention.toUpperCase()\r\n \r\n switch (convention) {\r\n case \"P\": {\r\n returnValue = \"P\";\r\n break;\r\n }\r\n case \"E\": {\r\n returnValue = \"B\";\r\n break;\r\n }\r\n case \"B\": {\r\n returnValue = \"PB\";\r\n break;\r\n }\r\n case \"C\": {\r\n returnValue = \"BP\";\r\n break;\r\n }\r\n case \"D\": {\r\n returnValue = \"BP\";\r\n break;\r\n }\r\n default: {\r\n returnValue = \"B\";\r\n break;\r\n }\r\n }\r\n}else{\r\n //if convention is empty:\r\n returnValue = \"B\";\r\n}\r\n\r\n return returnValue;\r\n}\r\n\r\ngetValue();", "validation": { - "required": false + "required": true } }, { @@ -181,7 +181,7 @@ { "name": "EndDate", "mode": "complex", - "value": "function getValue(){\r\n returnValue = sourceContract.assignment_endDate;\r\n\r\n if(sourceContract.assignment_endDate == '9999-12-31'){\r\n returnValue = null;\r\n }\r\n return returnValue;\r\n}\r\n\r\ngetValue();", + "value": "function getValue(){\r\n let returnValue = sourceContract.assignment_endDate;\r\n\r\n if(sourceContract.assignment_endDate == '9999-12-31'){\r\n returnValue = null;\r\n }\r\n return returnValue;\r\n}\r\n\r\ngetValue();", "validation": { "required": false } diff --git a/mapping.json b/mapping.json index d9a4d45..821f22c 100644 --- a/mapping.json +++ b/mapping.json @@ -181,7 +181,7 @@ { "name": "EndDate", "mode": "complex", - "value": "function getValue(){\r\n returnValue = sourceContract.assignment_endDate;\r\n\r\n if(sourceContract.assignment_endDate == '9999-12-31'){\r\n returnValue = null;\r\n }\r\n return returnValue;\r\n}\r\n\r\ngetValue();", + "value": "function getValue(){\r\n let returnValue = sourceContract.assignment_endDate;\r\n\r\n if(sourceContract.assignment_endDate == '9999-12-31'){\r\n returnValue = null;\r\n }\r\n return returnValue;\r\n}\r\n\r\ngetValue();", "validation": { "required": false } From adb05f2ee038a3633083b2dc77d3fdf9e2271fc2 Mon Sep 17 00:00:00 2001 From: rschouten97 <69046642+rschouten97@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:29:50 +0100 Subject: [PATCH 2/9] Added managerRoleCode --- configuration.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/configuration.json b/configuration.json index 0d441f3..9b4047d 100644 --- a/configuration.json +++ b/configuration.json @@ -31,6 +31,16 @@ "required": true } }, + { + "key": "managerRoleCode", + "type": "input", + "defaultValue": "MGR", + "templateOptions": { + "label": "Manager Role Code", + "placeholder": "MGR", + "required": true + } + }, { "key": "includeAssignments", "type": "checkbox", From f4d72404df9db87ce55bda133bab8fefb0919309 Mon Sep 17 00:00:00 2001 From: rschouten97 <69046642+rschouten97@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:31:18 +0100 Subject: [PATCH 3/9] Improved manager calculation --- departments.ps1 | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/departments.ps1 b/departments.ps1 index add50c4..f2a3fab 100644 --- a/departments.ps1 +++ b/departments.ps1 @@ -280,10 +280,21 @@ try { Write-Verbose "Querying roleAssignments" $roleAssignments = Invoke-RaetWebRequestList -Url "$BaseUri/iam/v1.0/roleAssignments" - # Sort Role assignments on personCode to make sure we always have the same manager with the same data - $roleAssignments = $roleAssignments | Sort-Object -Property { [int]$_.personCode } Write-Information "Successfully queried roleAssignments. Result: $($roleAssignments.Count)" + + # Filter Role assignments for only active and specific role, and sort descending on startDate and personCode to ensure consistent manager data + $currentDate = Get-Date + $roleAssignments = $roleAssignments | Where-Object { + $_.startDate -as [datetime] -le $currentDate -and + ($_.endDate -eq $null -or $_.endDate -as [datetime] -ge $currentDate) -and + $_.shortName -eq $managerRoleCode + } | Sort-Object -Property { $_.startDate , [int]$_.personCode } -Descending + + # Group on organizationUnit (to match to department) + $roleAssignmentsGrouped = $roleAssignments | Group-Object -Property organizationUnit -AsHashTable -AsString + + Write-Information "Successfully filtered roleAssignments for only active and specific role [$($managerRoleCode)]. Result: $(@($roleAssignments).Count)" } catch { $ex = $PSItem @@ -303,25 +314,18 @@ try { $managerActiveCompareDate = Get-Date foreach ($organizationUnit in $organizationUnits) { - $ouRoleAssignments = $roleAssignments | Where-Object { $_.organizationUnit -eq $organizationUnit.id } - # Sort role assignments on person code to make sure the order is always the same (if the data is the same) - $ouRoleAssignments = $ouRoleAssignments | Sort-Object personCode - - # Organizational units may contain multiple managers (per organizational unit). There's no way to specify which manager is primary - # We check of the manager assignment is active and then select the first one we come across that's valid - $managerId = $null - foreach ($roleAssignment in $ouRoleAssignments) { - if (![string]::IsNullOrEmpty($roleAssignment)) { - if ($roleAssignment.ShortName -eq 'MGR') { - $startDate = ([Datetime]::ParseExact($roleAssignment.startDate, 'yyyy-MM-dd', $null)) - $endDate = ([Datetime]::ParseExact($roleAssignment.endDate, 'yyyy-MM-dd', $null)) - - if ($startDate -lt $managerActiveCompareDate -and $endDate -ge $managerActiveCompareDate ) { - $managerId = $roleAssignment.personCode - break - } - } - } + # Get manager from roleassignments + $ouRoleAssignments = $null + $ouRoleAssignments = $roleAssignmentsGrouped["$($organizationUnit.id)"] + if ($null -ne $ouRoleAssignments) { + # Organizational units may contain multiple managers (per organizational unit). There's no way to specify which manager is primary + # Therefore we always select the first one we encounter + $roleAssignment = $ouRoleAssignments | Select-Object -First 1 + + $managerId = $roleAssignment.personCode + } + else { + $managerId = $null } $department = [PSCustomObject]@{ From dfa7669eca2be7c45a197135e97ea121c0fd413b Mon Sep 17 00:00:00 2001 From: rschouten97 <69046642+rschouten97@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:31:40 +0100 Subject: [PATCH 4/9] Removed ShortName from output --- departments.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/departments.ps1 b/departments.ps1 index f2a3fab..7011bae 100644 --- a/departments.ps1 +++ b/departments.ps1 @@ -330,7 +330,6 @@ try { $department = [PSCustomObject]@{ ExternalId = $organizationUnit.id - ShortName = $organizationUnit.shortName DisplayName = $organizationUnit.fullName ManagerExternalId = $managerId ParentExternalId = $organizationUnit.parentOrgUnit From a18ae354d6c9026c50fb3d64a6b7416aca3b2894 Mon Sep 17 00:00:00 2001 From: rschouten97 <69046642+rschouten97@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:38:03 +0100 Subject: [PATCH 5/9] Added managerRoleCode from config --- departments.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/departments.ps1 b/departments.ps1 index 7011bae..81d12e3 100644 --- a/departments.ps1 +++ b/departments.ps1 @@ -19,6 +19,7 @@ $WarningPreference = "Continue" $clientId = $c.clientId $clientSecret = $c.clientSecret $tenantId = $c.tenantId +$managerRoleCode = $c.managerRoleCode $Script:AuthenticationUri = "https://connect.visma.com/connect/token" $Script:BaseUri = "https://api.youforce.com" From d063348bb6725af5aef068dbdf2a4b4c19ef8a84 Mon Sep 17 00:00:00 2001 From: rschouten97 <69046642+rschouten97@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:44:28 +0100 Subject: [PATCH 6/9] Improved error handling and logging --- persons.ps1 | 473 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 307 insertions(+), 166 deletions(-) diff --git a/persons.ps1 b/persons.ps1 index 6b407c3..0d34ea0 100644 --- a/persons.ps1 +++ b/persons.ps1 @@ -3,6 +3,9 @@ # # Version: 2.2.0 ##################################################### +$Script:expirationTimeAccessToken = $null +$Script:AuthenticationHeaders = $null + $c = $configuration | ConvertFrom-Json # Set debug logging @@ -23,68 +26,47 @@ $includeAssignments = $c.includeAssignments $includePersonsWithoutAssignments = $c.includePersonsWithoutAssignments $excludePersonsWithoutContractsInHelloID = $c.excludePersonsWithoutContractsInHelloID $includeExtensions = $c.includeExtensions +$managerRoleCode = $c.managerRoleCode $Script:AuthenticationUri = "https://connect.visma.com/connect/token" $Script:BaseUri = "https://api.youforce.com" #region functions -function Resolve-HTTPError { +function Resolve-RaetBeaufortIAMAPIError { [CmdletBinding()] param ( - [Parameter(Mandatory, - ValueFromPipeline - )] - [object]$ErrorObject + [Parameter(Mandatory)] + [object] + $ErrorObject ) process { $httpErrorObj = [PSCustomObject]@{ - FullyQualifiedErrorId = $ErrorObject.FullyQualifiedErrorId - MyCommand = $ErrorObject.InvocationInfo.MyCommand - RequestUri = $ErrorObject.TargetObject.RequestUri - ScriptStackTrace = $ErrorObject.ScriptStackTrace - ErrorMessage = '' + ScriptLineNumber = $ErrorObject.InvocationInfo.ScriptLineNumber + Line = $ErrorObject.InvocationInfo.Line + ErrorDetails = $ErrorObject.Exception.Message + FriendlyMessage = $ErrorObject.Exception.Message } - if ($ErrorObject.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') { - $httpErrorObj.ErrorMessage = $ErrorObject.ErrorDetails.Message + if (-not [string]::IsNullOrEmpty($ErrorObject.ErrorDetails.Message)) { + $httpErrorObj.ErrorDetails = $ErrorObject.ErrorDetails.Message } elseif ($ErrorObject.Exception.GetType().FullName -eq 'System.Net.WebException') { - $httpErrorObj.ErrorMessage = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream()).ReadToEnd() - } - Write-Output $httpErrorObj - } -} - -function Get-ErrorMessage { - [CmdletBinding()] - param ( - [Parameter(Mandatory, - ValueFromPipeline - )] - [object]$ErrorObject - ) - process { - $errorMessage = [PSCustomObject]@{ - VerboseErrorMessage = $null - AuditErrorMessage = $null - } - - if ( $($ErrorObject.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or $($ErrorObject.Exception.GetType().FullName -eq 'System.Net.WebException')) { - $httpErrorObject = Resolve-HTTPError -Error $ErrorObject - - $errorMessage.VerboseErrorMessage = $httpErrorObject.ErrorMessage - - $errorMessage.AuditErrorMessage = $httpErrorObject.ErrorMessage + if ($null -ne $ErrorObject.Exception.Response) { + $streamReaderResponse = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream()).ReadToEnd() + if (-not [string]::IsNullOrEmpty($streamReaderResponse)) { + $httpErrorObj.ErrorDetails = $streamReaderResponse + } + } } - - # If error message empty, fall back on $ex.Exception.Message - if ([String]::IsNullOrEmpty($errorMessage.VerboseErrorMessage)) { - $errorMessage.VerboseErrorMessage = $ErrorObject.Exception.Message + try { + $errorDetailsObject = ($httpErrorObj.ErrorDetails | ConvertFrom-Json) + # Make sure to inspect the error result object and add only the error message as a FriendlyMessage. + # $httpErrorObj.FriendlyMessage = $errorDetailsObject.message + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails # Temporarily assignment } - if ([String]::IsNullOrEmpty($errorMessage.AuditErrorMessage)) { - $errorMessage.AuditErrorMessage = $ErrorObject.Exception.Message + catch { + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails } - - Write-Output $errorMessage + Write-Output $httpErrorObj } } @@ -114,6 +96,8 @@ function New-RaetSession { } try { + $actionMessage = "creating Access Token at uri '$($AuthenticationUri)'" + # Set TLS to accept TLS, TLS 1.1 and TLS 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 @@ -124,19 +108,17 @@ function New-RaetSession { 'tenant_id' = $TenantId } $splatAccessTokenParams = @{ - Uri = $Script:AuthenticationUri - Headers = @{'Cache-Control' = "no-cache" } - Method = 'POST' - ContentType = "application/x-www-form-urlencoded" - Body = $authorisationBody - UseBasicParsing = $true + Uri = $Script:AuthenticationUri + Headers = @{'Cache-Control' = "no-cache" } + Method = 'POST' + ContentType = "application/x-www-form-urlencoded" + Body = $authorisationBody } - Write-Verbose "Creating Access Token at uri '$($splatAccessTokenParams.Uri)'" + $result = Invoke-RestMethod @splatAccessTokenParams -Verbose:$false -ErrorAction Stop - $result = Invoke-RestMethod @splatAccessTokenParams -Verbose:$false if ($null -eq $result.access_token) { - throw $result + throw "Web request was successful, but no access token was returned." } $Script:expirationTimeAccessToken = (Get-Date).AddSeconds($result.expires_in) @@ -150,15 +132,19 @@ function New-RaetSession { } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex - - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($($errorMessage.VerboseErrorMessage))" + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } - $auditLogs.Add([PSCustomObject]@{ - # Action = "" # Optional - Message = "Error creating Access Token at uri ''$($splatAccessTokenParams.Uri)'. Please check credentials. Error Message: $($errorMessage.AuditErrorMessage)" - IsError = $true - }) + Write-Warning $warningMessage + throw $auditMessage } } @@ -187,8 +173,13 @@ function Invoke-RaetWebRequestList { $triesCounter = 0 do { try { + $actionMessage = "checking if access token is valid" + $accessTokenValid = Confirm-AccessTokenIsValid + if ($true -ne $accessTokenValid) { + $actionMessage = "creating new access token" + New-RaetSession -ClientId $clientId -ClientSecret $clientSecret -TenantId $tenantId } @@ -203,15 +194,15 @@ function Invoke-RaetWebRequestList { $counter++ + $actionMessage = "querying data from '$($Url + $SkipTakeUrl)'. Retry: $($retry). Counter: $($counter)" + $splatGetDataParams = @{ - Uri = "$Url$SkipTakeUrl" + Uri = "$($Url + $SkipTakeUrl)" Headers = $Script:AuthenticationHeaders Method = 'GET' ContentType = "application/json" UseBasicParsing = $true } - - Write-Verbose "Querying data from '$($splatGetDataParams.Uri)'" $result = Invoke-RestMethod @splatGetDataParams # Check both the keys "values" and "value", since Extensions endpoint returns the data in "values" instead of "value" @@ -235,26 +226,33 @@ function Invoke-RaetWebRequestList { } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex - - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" - + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + $maxTries = 3 - if ( ($($errorMessage.AuditErrorMessage) -Like "*Too Many Requests*" -or $($errorMessage.AuditErrorMessage) -Like "*Connection timed out*") -and $triesCounter -lt $maxTries ) { + if ( ($($auditMessage) -Like "*Too Many Requests*" -or $($auditMessage) -Like "*Connection timed out*") -and $triesCounter -lt $maxTries ) { $triesCounter++ $retry = $true $delay = 601 # Wait for 0,601 seconds - RAET IAM API allows a maximum of 100 requests a minute (https://community.visma.com/t5/Kennisbank-Youforce-API/API-Status-amp-Policy/ta-p/428099#toc-hId-339419904:~:text=3-,Spike%20arrest%20policy%20(max%20number%20of%20API%20calls%20per%20minute),100%20calls%20per%20minute,-*For%20the%20base). - Write-Warning "Error querying data from '$($splatGetDataParams.Uri)'. Error Message: $($errorMessage.AuditErrorMessage). Trying again in '$delay' milliseconds for a maximum of '$maxTries' tries." + Write-Warning "$auditMessage. Trying again in '$delay' milliseconds for a maximum of '$maxTries' tries." Start-Sleep -Milliseconds $delay } else { $retry = $false - throw "Error querying data from '$($splatGetDataParams.Uri)'. Error Message: $($errorMessage.AuditErrorMessage)" + throw "$auditMessage" } } }while (-NOT[string]::IsNullOrEmpty($result.nextLink) -or $retry -eq $true) - Write-Verbose "Successfully queried data from '$($Url)'. Result count: $($ReturnValue.Count)" + Write-Verbose "Successfully queried data from '$($Url)'. Result count: $(@($ReturnValue).Count)" return $ReturnValue } @@ -262,38 +260,46 @@ function Invoke-RaetWebRequestList { Write-Information "Starting person import. Base URI: $BaseUri" - # Query persons try { - Write-Verbose "Querying persons" + $actionMessage = "querying persons at [$BaseUri/iam/v1.0/persons]" $personsList = Invoke-RaetWebRequestList -Url "$BaseUri/iam/v1.0/persons" # Make sure persons are unique $persons = $personsList | Where-Object { $_.personCode -ne $null } | Sort-Object id -Unique - Write-Information "Successfully queried persons. Result: $($persons.Count)" + Write-Information "Successfully queried persons. Result: $(@($persons).Count)" } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" + Write-Warning $warningMessage - throw "Error querying persons. Error Message: $($errorMessage.AuditErrorMessage)" + throw $auditMessage } # Query person extensions try { if ($true -eq $includeExtensions) { - Write-Verbose "Querying person extensions" + $actionMessage = "querying person extensions at [$BaseUri/extensions/v1.0/iam/persons]" $personExtensionsList = Invoke-RaetWebRequestList -Url "$BaseUri/extensions/v1.0/iam/persons" # Group by personCode $personExtensionsGrouped = $personExtensionsList | Group-Object personCode -CaseSensitive -AsHashTable -AsString - Write-Information "Successfully queried person extensions. Result: $($personExtensionsList.Count)" + Write-Information "Successfully queried person extensions. Result: $(@($personExtensionsList).Count)" } else { Write-Information "Ignored querying person extensions because the configuration toggle to include extensions is: $($includeExtensions)" @@ -301,37 +307,55 @@ try { } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex - - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + + Write-Warning $warningMessage - throw "Error querying person extensions. Error Message: $($errorMessage.AuditErrorMessage)" + throw $auditMessage } # Query employments try { - Write-Verbose "Querying employments" + $actionMessage = "querying employments at [$BaseUri/iam/v1.0/employments]" - $employmentsList = Invoke-RaetWebRequestList -Url "$Script:BaseUri/iam/v1.0/employments" + $employmentsList = Invoke-RaetWebRequestList -Url "$BaseUri/iam/v1.0/employments" # Group by personCode $employmentsGrouped = $employmentsList | Group-Object personCode -CaseSensitive -AsHashTable -AsString - Write-Information "Successfully queried employments. Result: $($employmentsList.Count)" + Write-Information "Successfully queried employments. Result: $(@($employmentsList).Count)" } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex - - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + + Write-Warning $warningMessage - throw "Error querying employments. Error Message: $($errorMessage.AuditErrorMessage)" + throw $auditMessage } # Query employment extensions try { if ($true -eq $includeExtensions) { - Write-Verbose "Querying employment extensions" + $actionMessage = "querying employment extensions at [$BaseUri/extensions/v1.0/iam/employments]" $employmentExtensionsList = Invoke-RaetWebRequestList -Url "$BaseUri/extensions/v1.0/iam/employments" @@ -346,7 +370,7 @@ try { # Group by ExternalId $employmentExtensionsGrouped = $employmentExtensionsList | Group-Object ExternalId -CaseSensitive -AsHashTable -AsString - Write-Information "Successfully queried employment extensions. Result: $($employmentExtensionsList.Count)" + Write-Information "Successfully queried employment extensions. Result: $(@($employmentExtensionsList).Count)" } else { Write-Information "Ignored querying employmens extensions because the configuration toggle to include extensions is: $($includeExtensions)" @@ -354,20 +378,36 @@ try { } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex - - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + + Write-Warning $warningMessage - throw "Error querying employment extensions. Error Message: $($errorMessage.AuditErrorMessage)" + throw $auditMessage } # Query assignments if ($true -eq $includeAssignments) { try { - Write-Verbose "Querying assignments" + $actionMessage = "querying assignments at [$BaseUri/iam/v1.0/assignments]" $assignmentsList = Invoke-RaetWebRequestList -Url "$BaseUri/iam/v1.0/assignments" + Write-Information "Successfully queried assignments. Result: $(@($assignmentsList).Count)" + + # # Filter out archived assignments + # $assignmentsList = $assignmentsList | Where-Object { $_.isActive -ne $false } + + # Write-Information "Successfully filtered out archived assignments. Result: $(@($assignmentsList).Count)" + # Add ExternalId property as linking key to contract, linking key is PersonCode + "_" + employmentCode $assignmentsList | Add-Member -MemberType NoteProperty -Name "ExternalId" -Value $null -Force $assignmentsList | Foreach-Object { @@ -377,99 +417,178 @@ if ($true -eq $includeAssignments) { # Group by ExternalId $assignmentsGrouped = $assignmentsList | Group-Object ExternalId -AsHashTable - Write-Information "Successfully queried assignments. Result: $($assignmentsList.Count)" } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex - - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } - throw "Error querying assignments. Error Message: $($errorMessage.AuditErrorMessage)" + Write-Warning $warningMessage + + throw $auditMessage } } # Query jobProfiles try { - Write-Verbose "Querying jobProfiles" + $actionMessage = "querying jobProfiles at [$BaseUri/iam/v1.0/jobProfiles]" $jobProfilesList = Invoke-RaetWebRequestList -Url "$BaseUri/iam/v1.0/jobProfiles" # Group by id $jobProfilesGrouped = $jobProfilesList | Group-Object Id -AsHashTable - Write-Information "Successfully queried jobProfiles. Result: $($jobProfilesList.Count)" + Write-Information "Successfully queried jobProfiles. Result: $(@($jobProfilesList).Count)" } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex - - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + + Write-Warning $warningMessage - throw "Error querying jobProfiles. Error Message: $($errorMessage.AuditErrorMessage)" + throw $auditMessage } # Query costAllocations try { - Write-Verbose "Querying costAllocations" + $actionMessage = "querying costAllocations at [$BaseUri/iam/v1.0/costAllocations]" $costAllocationsList = Invoke-RaetWebRequestList -Url "$BaseUri/iam/v1.0/costAllocations" - # Add ExternalId property as linking key to contract, linking key is PersonCode + "_" + employmentCode - $costAllocationsList | Add-Member -MemberType NoteProperty -Name "ExternalId" -Value $null -Force + # Add ExternalId property as linking key to employment, linking key is PersonCode + "_" + employmentCode + $costAllocationsList | Add-Member -MemberType NoteProperty -Name "EmploymentExternalId" -Value $null -Force + $costAllocationsList | Add-Member -MemberType NoteProperty -Name "AssignmentExternalId" -Value $null -Force $costAllocationsList | Foreach-Object { - $_.ExternalId = $_.PersonCode + "_" + $_.employmentCode + $_.EmploymentExternalId = $_.PersonCode + "_" + $_.employmentCode + $_.AssignmentExternalId = $_.PersonCode + "_" + $_.costCenterCode } - # Group by ExternalId - $costAllocationsGrouped = $costAllocationsList | Group-Object ExternalId -AsHashTable + # Group by EmploymentExternalId + $costAllocationsGroupedForEmployment = $costAllocationsList | Group-Object EmploymentExternalId -AsHashTable + + # Group by AssignmentExternalId + $costAllocationsGroupedForAssignment = $costAllocationsList | Group-Object AssignmentExternalId -AsHashTable - Write-Information "Successfully queried costAllocations. Result: $($costAllocationsList.Count)" + Write-Information "Successfully queried costAllocations. Result: $(@($costAllocationsList).Count)" } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex - - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + + Write-Warning $warningMessage - throw "Error querying costAllocations. Error Message: $($errorMessage.AuditErrorMessage)" + throw $auditMessage } # Query organizationUnits try { - Write-Verbose "Querying organizationUnits" + $actionMessage = "querying organizationUnits at [$BaseUri/iam/v1.0/organizationUnits]" $organizationUnits = Invoke-RaetWebRequestList -Url "$BaseUri/iam/v1.0/organizationUnits" # Group by ExternalId $organizationUnitsGrouped = $organizationUnits | Group-Object id -AsHashTable -AsString - Write-Information "Successfully queried organizationUnits. Result: $($organizationUnits.Count)" + Write-Information "Successfully queried organizationUnits. Result: $(@($organizationUnits).Count)" } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + + Write-Warning $warningMessage + + throw $auditMessage +} + +# Query roleAssignments +try { + $actionMessage = "querying roleAssignments at [$BaseUri/iam/v1.0/roleAssignments]" + + $roleAssignments = Invoke-RaetWebRequestList -Url "$BaseUri/iam/v1.0/roleAssignments" + Write-Information "Successfully queried roleAssignments. Result: $(@($roleAssignments).Count)" + + # Filter Role assignments for only active and specific role, and sort descending on startDate and personCode to ensure consistent manager data + $currentDate = Get-Date + $roleAssignments = $roleAssignments | Where-Object { + $_.startDate -as [datetime] -le $currentDate -and + ($_.endDate -eq $null -or $_.endDate -as [datetime] -ge $currentDate) -and + $_.shortName -eq $managerRoleCode + } | Sort-Object -Property { $_.startDate , [int]$_.personCode } -Descending - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" + # Group on personCode (to match to person) + $roleAssignmentsGrouped = $roleAssignments | Group-Object personCode -AsHashTable -AsString - throw "Error querying organizationUnits. Error Message: $($errorMessage.AuditErrorMessage)" + Write-Information "Successfully filtered for only active roleAssignments of role [$managerRoleCode]. Result: $(@($roleAssignments).Count)" } +catch { + $ex = $PSItem + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + + Write-Warning $warningMessage + + throw $auditMessage +} +#endRegion try { - Write-Verbose 'Enhancing and exporting person objects to HelloID' + $actionMessage = "enhancing and exporting person objects to HelloID" # Set counter to keep track of actual exported person objects $exportedPersons = 0 - # Enhance the persons model + # Enhance the persons model with required properties $persons | Add-Member -MemberType NoteProperty -Name "ExternalId" -Value $null -Force $persons | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $null -Force $persons | Add-Member -MemberType NoteProperty -Name "Contracts" -Value $null -Force - $persons | ForEach-Object { + $persons | ForEach-Object { # Set required fields for HelloID $_.ExternalId = $_.personCode - $_.DisplayName = "$($_.knownAs) $($_.lastNameAtBirth) ($($_.ExternalId))" + $_.DisplayName = "$($_.knownAs) $($_.lastNameAtBirth) ($($_.ExternalId))" # Transform emailAddresses and add to the person if ($null -ne $_.emailAddresses) { @@ -482,7 +601,7 @@ try { } } - # Remove unneccesary fields from object (to avoid unneccesary large objects) + # Remove unnecessary fields from object (to avoid unnecessary large objects) $_.PSObject.Properties.Remove('emailAddresses') } @@ -497,7 +616,7 @@ try { } } - # Remove unneccesary fields from object (to avoid unneccesary large objects) + # Remove unnecessary fields from object (to avoid unnecessary large objects) $_.PSObject.Properties.Remove('phoneNumbers') } @@ -512,25 +631,32 @@ try { } } - # Remove unneccesary fields from object (to avoid unneccesary large objects) + Remove unnecessary fields from object (to avoid unnecessary large objects) $_.PSObject.Properties.Remove('addresses') } + if ($null -ne $_.addresses) { + $_.PSObject.Properties.Remove('addresses') + } + #endRegion + # Transform extensions and add to the person if ($true -eq $includeExtensions) { if ($null -ne $personExtensionsGrouped) { + $personExtensions = $null $personExtensions = $personExtensionsGrouped[$_.personCode] if ($null -ne $personExtensions) { foreach ($personExtension in $personExtensions) { - # Add a property for each field in object - foreach ($property in $personExtension.PsObject.Properties) { - $_ | Add-Member -MemberType NoteProperty -Name ("extension_" + $personExtension.fieldNameAlias.Replace(' ', '') + "_" + $property.Name) -Value "$($property.value)" -Force + # Add fieldNameAlias, value and description as properties to employment object + foreach ($property in $personExtension.PsObject.Properties | Where-Object { $_.Name -in @('fieldNameAlias', 'value', 'description') }) { + $_ | Add-Member -MemberType NoteProperty -Name ("extension_" + $personExtension.bo4FieldCode.Replace(' ', '') + "_" + $property.Name) -Value "$($property.value)" -Force } } } } } - # Remove unneccesary fields from object (to avoid unneccesary large objects) - Extensions are available via a seperate endpoint + + # Remove unnecessary fields from object (to avoid unnecessary large objects) - Extensions are available via a separate endpoint $_.PSObject.Properties.Remove('extensions') # Create contracts object @@ -600,7 +726,7 @@ try { # Enhance employment with costAllocation for extra information, such as: fullName # Get costAllocation for employment, linking key is PersonCode + "_" + employmentCode - $costAllocation = $costAllocationsGrouped[($_.personCode + "_" + $employment.employmentCode)] + $costAllocation = $costAllocationsGroupedForEmployment[($_.personCode + "_" + $employment.employmentCode)] if ($null -ne $costAllocation) { # In case multiple are found with the same ID, we always select the first one in the array $costAllocation = $costAllocation | Select-Object -First 1 @@ -617,29 +743,34 @@ try { # Get extension for employment, linking key is PersonCode + "_" + employmentCode if ($true -eq $includeExtensions) { if ($null -ne $employmentExtensionsGrouped) { + $employmentExtensions = $null $employmentExtensions = $employmentExtensionsGrouped[($_.personCode + "_" + $employment.employmentCode)] if ($null -ne $employmentExtensions) { foreach ($employmentExtension in $employmentExtensions) { - # Add a property for each field in object - foreach ($property in $employmentExtension.PsObject.Properties) { - $employment | Add-Member -MemberType NoteProperty -Name ("extension_" + $employmentExtension.fieldNameAlias.Replace(' ', '') + "_" + $property.Name) -Value "$($property.value)" -Force + # Add fieldNameAlias, value and description as properties to employment object + foreach ($property in $employmentExtension.PsObject.Properties | Where-Object { $_.Name -in @('fieldNameAlias', 'value', 'description') }) { + $employment | Add-Member -MemberType NoteProperty -Name ("extension_" + $employmentExtension.bo4FieldCode.Replace(' ', '') + "_" + $property.Name) -Value "$($property.value)" -Force } } } } } - # Remove unneccesary fields from object (to avoid unneccesary large objects) - Extensions are available via a seperate endpoint + # Remove unnecessary fields from object (to avoid unnecessary large objects) - Extensions are available via a separate endpoint $employment.PSObject.Properties.Remove('extensions') if ($false -eq $includeAssignments) { # Create Contract object(s) based on employments - # Create custom employment object to include prefix of properties + # Create employment object to include prefix of properties $employmentObject = [PSCustomObject]@{} $employment.psobject.properties | ForEach-Object { $employmentObject | Add-Member -MemberType $_.MemberType -Name "employment_$($_.Name)" -Value $_.Value -Force } + # Add a property to indicate contract is employment + $employmentObject | Add-Member -MemberType NoteProperty -Name "Type" -Value "Employment" -Force + + # Add employment data to contracts [Void]$contractsList.Add($employmentObject) } else { @@ -710,8 +841,8 @@ try { #endregion Custom - Enhance assignment with upper department(s) information # Enhance assignment with costAllocation for extra information, such as: fullName - # Get costAllocation for assignment, linking key is PersonCode + "_" + assignmentCode - $costAllocation = $costAllocationsGrouped[($_.personCode + "_" + $assignment.employmentCode)] + # Get costAllocation for assignment, linking key is PersonCode + "_" + costCenter + $costAllocation = $costAllocationsGroupedForAssignment[($_.personCode + "_" + $assignment.costCenter)] if ($null -ne $costAllocation) { # In case multiple are found with the same ID, we always select the first one in the array $costAllocation = $costAllocation | Select-Object -First 1 @@ -724,7 +855,7 @@ try { } } - # Create custom assignment object to include prefix in properties + # Create assignment object to include prefix in properties $assignmentObject = [PSCustomObject]@{} # Add employment object with prefix for property names @@ -736,7 +867,10 @@ try { $assignment.psobject.properties | ForEach-Object { $assignmentObject | Add-Member -MemberType $_.MemberType -Name "assignment_$($_.Name)" -Value $_.Value -Force } - + + # Add a property to indicate contract is employment + $assignmentObject | Add-Member -MemberType NoteProperty -Name "Type" -Value "Assignment" -Force + # Add employment and position data to contracts [Void]$contractsList.Add($assignmentObject) } @@ -745,12 +879,16 @@ try { if ($true -eq $includePersonsWithoutAssignments) { # Add employment only data to contracts (in case of employments without assignments) - # Create custom employment object to include prefix of properties + # Create employment object to include prefix of properties $employmentObject = [PSCustomObject]@{} $employment.psobject.properties | ForEach-Object { $employmentObject | Add-Member -MemberType $_.MemberType -Name "employment_$($_.Name)" -Value $_.Value -Force } - + + # Add a property to indicate contract is employment + $employmentObject | Add-Member -MemberType NoteProperty -Name "Type" -Value "Employment" -Force + + # Add employment data to contracts [Void]$contractsList.Add($employmentObject) } else { @@ -760,22 +898,18 @@ try { } } - # Remove unneccesary fields from object (to avoid unneccesary large objects) - # Remove employments, since the data is transformed into a seperate object: contracts + # Remove unnecessary fields from object (to avoid unnecessary large objects) + # Remove employments, since the data is transformed into a separate object: contracts $_.PSObject.Properties.Remove('employments') } else { - ### Be very careful when logging in a loop, only use this when the amount is below 100 - ### When this would log over 100 lines, please refer from using this in HelloID and troubleshoot this in local PS - # Write-Warning "No employments found for person: $($_.ExternalId)" + Write-Warning "No employments found for person: $($_.ExternalId)" } # Add Contracts to person if ($null -ne $contractsList) { - # This example can be used by the consultant if you want to filter out persons with an empty array as contract - # *** Please consult with the Tools4ever consultant before enabling this code. *** if ($contractsList.Count -eq 0 -and $true -eq $excludePersonsWithoutContractsInHelloID) { - # Write-Warning "Excluding person from export: $($_.ExternalId). Reason: Contracts is an empty array" + Write-Warning "Excluding person from export: $($_.ExternalId). Reason: Contracts is an empty array" return } else { @@ -783,9 +917,7 @@ try { } } elseif ($true -eq $excludePersonsWithoutContractsInHelloID) { - ### Be very careful when logging in a loop, only use this when the amount is below 100 - ### When this would log over 100 lines, please refer from using this in HelloID and troubleshoot this in local PS - # Write-Warning "Excluding person from export: $($_.ExternalId). Reason: Person has no contract data" + Write-Warning "Excluding person from export: $($_.ExternalId). Reason: Person has no contract data" return } @@ -804,9 +936,18 @@ try { } catch { $ex = $PSItem - $errorMessage = Get-ErrorMessage -ErrorObject $ex - - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)" + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-RaetBeaufortIAMAPIError -ErrorObject $ex + $auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)" + $warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)" + $warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + + Write-Warning $warningMessage - throw "Could not enhance and export person objects to HelloID. Error Message: $($errorMessage.AuditErrorMessage)" -} + throw $auditMessage +} \ No newline at end of file From c6405f1116c3d7e000cc6b5db1cf1c048f679047 Mon Sep 17 00:00:00 2001 From: rschouten97 <69046642+rschouten97@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:45:48 +0100 Subject: [PATCH 7/9] Added departmenthierarchy (all levels up) instead of only upper ou (one level up) --- persons.ps1 | 70 ++++++++++++++--------------------------------------- 1 file changed, 18 insertions(+), 52 deletions(-) diff --git a/persons.ps1 b/persons.ps1 index 0d34ea0..3b0794f 100644 --- a/persons.ps1 +++ b/persons.ps1 @@ -694,35 +694,18 @@ try { } } - #region Custom - Enhance assignment with upper department(s) information - # Enhance employment with upper OU for extra information - $upperOU = $organizationUnitsGrouped["$($employment.organizationUnit_parentOrgUnit)"] - if ($null -ne $upperOU) { - # In case multiple are found with the same ID, we always select the first one in the array - $upperOU = $upperOU | Select-Object -First 1 - - if (![string]::IsNullOrEmpty($upperOU)) { - foreach ($property in $upperOU.PsObject.Properties) { - # Add a property for each field in object - $employment | Add-Member -MemberType NoteProperty -Name ("organizationUnitUpper_" + $property.Name) -Value $property.Value -Force - } + # Enhance employment with comma seperated list of hierarchical department shortnames + $departmentHierarchy = [System.Collections.ArrayList]::new() + if ($null -ne $department) { + [void]$departmentHierarchy.add($department) + while (-NOT[String]::IsNullOrEmpty($department.parentOrgUnit)) { + # In case multiple departments are found with same id, always select first we encounter + $department = $organizationUnitsGrouped["$($department.parentOrgUnit)"] | Select-Object -First 1 + [void]$departmentHierarchy.add($department) } } - # Enhance employment with ipper upper OU for extra information - $upperUpperOU = $organizationUnitsGrouped["$($employment.organizationUnitUpper_parentOrgUnit)"] - if ($null -ne $upperUpperOU) { - # In case multiple are found with the same ID, we always select the first one in the array - $upperUpperOU = $upperUpperOU | Select-Object -First 1 - - if (![string]::IsNullOrEmpty($upperUpperOU)) { - foreach ($property in $upperUpperOU.PsObject.Properties) { - # Add a property for each field in object - $employment | Add-Member -MemberType NoteProperty -Name ("organizationUnitUpperUpper_" + $property.Name) -Value $property.Value -Force - } - } - } - #endregion Custom - Enhance assignment with upper department(s) information + $employment | Add-Member -MemberType NoteProperty -Name "DepartmentHierarchy" -Value ('"{0}"' -f ($departmentHierarchy.shortName -Join '","')) -Force # Enhance employment with costAllocation for extra information, such as: fullName # Get costAllocation for employment, linking key is PersonCode + "_" + employmentCode @@ -810,35 +793,18 @@ try { } } - #region Custom - Enhance assignment with upper department(s) information - # Enhance assignment with upper OU for extra information - $upperOU = $organizationUnitsGrouped["$($assignment.organizationUnit_parentOrgUnit)"] - if ($null -ne $upperOU) { - # In case multiple are found with the same ID, we always select the first one in the array - $upperOU = $upperOU | Select-Object -First 1 - - if (![string]::IsNullOrEmpty($upperOU)) { - foreach ($property in $upperOU.PsObject.Properties) { - # Add a property for each field in object - $assignment | Add-Member -MemberType NoteProperty -Name ("organizationUnitUpper_" + $property.Name) -Value $property.Value -Force - } + # Enhance employment with comma seperated list of hierarchical department shortnames + $departmentHierarchy = [System.Collections.ArrayList]::new() + if ($null -ne $department) { + [void]$departmentHierarchy.add($department) + while (-NOT[String]::IsNullOrEmpty($department.parentOrgUnit)) { + # In case multiple departments are found with same id, always select first we encounter + $department = $organizationUnitsGrouped["$($department.parentOrgUnit)"] | Select-Object -First 1 + [void]$departmentHierarchy.add($department) } } - # Enhance assignment with upper upper OU for extra information - $upperUpperOU = $organizationUnitsGrouped["$($assignment.organizationUnitUpper_parentOrgUnit)"] - if ($null -ne $upperUpperOU) { - # In case multiple are found with the same ID, we always select the first one in the array - $upperUpperOU = $upperUpperOU | Select-Object -First 1 - - if (![string]::IsNullOrEmpty($upperUpperOU)) { - foreach ($property in $upperUpperOU.PsObject.Properties) { - # Add a property for each field in object - $assignment | Add-Member -MemberType NoteProperty -Name ("organizationUnitUpperUpper_" + $property.Name) -Value $property.Value -Force - } - } - } - #endregion Custom - Enhance assignment with upper department(s) information + $assignment | Add-Member -MemberType NoteProperty -Name "DepartmentHierarchy" -Value ('"{0}"' -f ($departmentHierarchy.shortName -Join '","')) -Force # Enhance assignment with costAllocation for extra information, such as: fullName # Get costAllocation for assignment, linking key is PersonCode + "_" + costCenter From 4e48ad31400783428bae37d0a50959d4994e81c4 Mon Sep 17 00:00:00 2001 From: rschouten97 <69046642+rschouten97@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:46:05 +0100 Subject: [PATCH 8/9] Added ManagerOf --- persons.ps1 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/persons.ps1 b/persons.ps1 index 3b0794f..b00d843 100644 --- a/persons.ps1 +++ b/persons.ps1 @@ -585,11 +585,24 @@ try { $persons | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $null -Force $persons | Add-Member -MemberType NoteProperty -Name "Contracts" -Value $null -Force + # Enhance the persons model with additional properties + $persons | Add-Member -MemberType NoteProperty -Name "ManagerOf" -Value $null -Force + $persons | ForEach-Object { # Set required fields for HelloID $_.ExternalId = $_.personCode $_.DisplayName = "$($_.knownAs) $($_.lastNameAtBirth) ($($_.ExternalId))" + # Add comma separated list with all departments person is manager of + $personRoleAssignments = $null + $personRoleAssignments = $roleAssignmentsGrouped["$($_.personCode)"] | Sort-Object -Property organizationUnitCode + if ($null -ne $personRoleAssignments) { + $_.ManagerOf = '"{0}"' -f ($personRoleAssignments.organizationUnitCode -Join '","') + } + else { + $_.ManagerOf = $null + } + # Transform emailAddresses and add to the person if ($null -ne $_.emailAddresses) { foreach ($emailAddress in $_.emailAddresses) { From b50ed55d57eef6b11ed51654d1c6433d75625fd6 Mon Sep 17 00:00:00 2001 From: rschouten97 <69046642+rschouten97@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:49:17 +0100 Subject: [PATCH 9/9] Filter out archived assignments --- persons.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/persons.ps1 b/persons.ps1 index b00d843..0162e53 100644 --- a/persons.ps1 +++ b/persons.ps1 @@ -403,8 +403,8 @@ if ($true -eq $includeAssignments) { Write-Information "Successfully queried assignments. Result: $(@($assignmentsList).Count)" - # # Filter out archived assignments - # $assignmentsList = $assignmentsList | Where-Object { $_.isActive -ne $false } + # Filter out archived assignments + $assignmentsList = $assignmentsList | Where-Object { $_.isActive -ne $false } # Write-Information "Successfully filtered out archived assignments. Result: $(@($assignmentsList).Count)"